diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index f7f2045059d9..b40b2a1e5355 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -14,6 +14,8 @@ }, "dependencies": { "@langchain/langgraph": "^1.0.1", + "@prisma/adapter-d1": "6.15.0", + "@prisma/client": "6.15.0", "@sentry/cloudflare": "10.63.0", "@sentry/hono": "10.63.0", "hono": "^4.12.25" @@ -22,6 +24,7 @@ "@cloudflare/workers-types": "^4.20260426.0", "@sentry-internal/test-utils": "10.63.0", "eslint-plugin-regexp": "^3.1.0", + "prisma": "6.15.0", "vitest": "^3.2.6", "wrangler": "4.86.0" }, diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/.gitignore b/dev-packages/cloudflare-integration-tests/suites/prisma/.gitignore new file mode 100644 index 000000000000..9d505a691f1b --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/.gitignore @@ -0,0 +1,4 @@ +generated/ +.wrangler/ +*.db +*.db-journal diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts b/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts new file mode 100644 index 000000000000..58add5bf65bf --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts @@ -0,0 +1,34 @@ +import type { D1Database } from '@cloudflare/workers-types'; +import { PrismaD1 } from '@prisma/adapter-d1'; +import * as Sentry from '@sentry/cloudflare/nodejs_compat'; +import { PrismaClient } from './generated'; + +interface Env { + SENTRY_DSN: string; + DB: D1Database; +} + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1, + integrations: [Sentry.prismaIntegration()], + }), + { + async fetch(_request, env) { + // miniflare starts with an empty D1 database, so create the table Prisma expects. + await env.DB.exec( + 'CREATE TABLE IF NOT EXISTS User (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT)', + ); + + const adapter = new PrismaD1(env.DB); + const prisma = new PrismaClient({ adapter }); + + const users = await prisma.user.findMany(); + + return new Response(JSON.stringify(users), { + headers: { 'Content-Type': 'application/json' }, + }); + }, + } satisfies ExportedHandler, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/schema.prisma b/dev-packages/cloudflare-integration-tests/suites/prisma/schema.prisma new file mode 100644 index 000000000000..c007da25550a --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/schema.prisma @@ -0,0 +1,16 @@ +generator client { + provider = "prisma-client-js" + output = "./generated" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts b/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts new file mode 100644 index 000000000000..2a4c259b4058 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts @@ -0,0 +1,85 @@ +import { execSync } from 'node:child_process'; +import { join } from 'node:path'; +import type { Envelope } from '@sentry/core'; +import { beforeAll, expect, it } from 'vitest'; +import { createRunner } from '../../runner'; + +beforeAll(() => { + // Generate the Prisma client (including the WASM query engine used on Workers) before wrangler + // bundles the worker. The generated client (output: "./generated") is gitignored. + execSync(`yarn prisma generate --schema ${join(__dirname, 'schema.prisma')}`, { cwd: __dirname, stdio: 'inherit' }); +}, 120_000); + +function envelopeItemType(envelope: Envelope): string | undefined { + return envelope[1][0]?.[0]?.type as string | undefined; +} + +function envelopeItem(envelope: Envelope): Record { + return envelope[1][0]![1] as Record; +} + +it('captures a transaction with Prisma spans for a D1 query via the @sentry/cloudflare/nodejs_compat prismaIntegration', async ({ + signal, +}) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect((envelope: Envelope) => { + expect(envelopeItemType(envelope)).toBe('transaction'); + + const transaction = envelopeItem(envelope); + const trace = (transaction.contexts as Record> | undefined)?.trace; + + expect(transaction.transaction).toBe('GET /users'); + expect(trace?.op).toBe('http.server'); + expect(trace?.origin).toBe('auto.http.cloudflare'); + expect(trace?.status).toBe('ok'); + + const createUserTableQuery = + 'CREATE TABLE IF NOT EXISTS User (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT)'; + const selectUsersQuery = + 'SELECT `main`.`User`.`id`, `main`.`User`.`email`, `main`.`User`.`name` FROM `main`.`User` WHERE 1=1 LIMIT ? OFFSET ?'; + + // Only the span shape is stable here - ids, timestamps, and parent ids are not. + const spans = ((transaction.spans as Array>) || []).map(span => ({ + description: span.description, + op: span.op, + origin: span.origin, + })); + + expect(spans).toHaveLength(16); + expect(spans).toEqual( + expect.arrayContaining([ + { + description: createUserTableQuery, + op: 'db.query', + origin: 'auto.db.cloudflare.d1', + }, + { + description: expect.stringMatching( + /^SELECT `main`\.`User`\.`id`, `main`\.`User`\.`email`, `main`\.`User`\.`name` FROM `main`\.`User` WHERE 1=1 LIMIT \? OFFSET \? \/\* traceparent='00-[\da-f]{32}-[\da-f]{16}-01' \*\/$/, + ), + op: 'db.query', + origin: 'auto.db.cloudflare.d1', + }, + { description: 'prisma:client:connect', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:client:load_engine', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:client:operation', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:client:serialize', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:connect', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:connection', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:query', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: selectUsersQuery, op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:js:query:args', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:js:query:sql', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:js:query:result', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:serialize', op: undefined, origin: 'auto.db.otel.prisma' }, + { description: 'prisma:engine:response_json_serialization', op: undefined, origin: 'auto.db.otel.prisma' }, + ]), + ); + expect(spans.filter(span => span.description === 'prisma:engine:connection')).toHaveLength(2); + }) + .start(signal); + + await runner.makeRequest('get', '/users'); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/prisma/wrangler.jsonc new file mode 100644 index 000000000000..6cb9581352c5 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "name": "prisma-d1-test-worker", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"], + "d1_databases": [ + { + "binding": "DB", + "database_name": "test-db", + "database_id": "00000000-0000-0000-0000-000000000000", + }, + ], +} diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 312885e16429..25c8742f187f 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -60,7 +60,8 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/core": "10.63.0" + "@sentry/core": "10.63.0", + "@sentry/node": "10.63.0" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/cloudflare/src/nodejs_compat/index.ts b/packages/cloudflare/src/nodejs_compat/index.ts index 1eb8c8642766..0ae24c02b42b 100644 --- a/packages/cloudflare/src/nodejs_compat/index.ts +++ b/packages/cloudflare/src/nodejs_compat/index.ts @@ -1 +1,2 @@ export * from '../index'; +export { prismaIntegration } from '@sentry/node'; diff --git a/yarn.lock b/yarn.lock index a413856e5de8..6e5fa1537825 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3078,6 +3078,11 @@ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260426.1.tgz#b21d2a24afe0b274982d7ccbe7163a5689da1507" integrity sha512-d3Xj/IjINRgNVwH+eKhpUn4xkkcEewbWXbOvBlapiirKWh5zl9m0Epi3qOqmjyRYK6MICqIGXg4qZBEt0lxudw== +"@cloudflare/workers-types@4.20250214.0": + version "4.20250214.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250214.0.tgz#f9a8109cdba9425e2a934d6a0870eca2769a5b6f" + integrity sha512-+M8oOFVbyXT5GeJrYLWMUGyPf5wGB4+k59PPqdedtOig7NjZ5r4S79wMdaZ/EV5IV8JPtZBSNjTKpDnNmfxjaQ== + "@cloudflare/workers-types@4.20250922.0": version "4.20250922.0" resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885" @@ -6638,6 +6643,15 @@ resolved "https://registry.yarnpkg.com/@poppinss/exception/-/exception-1.2.2.tgz#8d30d42e126c54fe84e997433e4dcac610090743" integrity sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg== +"@prisma/adapter-d1@6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@prisma/adapter-d1/-/adapter-d1-6.15.0.tgz#a3fd43a1b966d0012f1461ea278732b7464fb371" + integrity sha512-ozsANVFfmPyxa4SBQnGITQLSQ2aOVaRRV75CXBaDrhWU+zB+0Df3QUPF4vURSgskhdfUyOo35pzUOf4msPoGuQ== + dependencies: + "@cloudflare/workers-types" "4.20250214.0" + "@prisma/driver-adapter-utils" "6.15.0" + ky "1.7.5" + "@prisma/adapter-pg@7.8.0": version "7.8.0" resolved "https://registry.yarnpkg.com/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz#8f39dc10ec0fa3d5914e09df385977ce0500fbec" @@ -6673,6 +6687,13 @@ resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-7.8.0.tgz#8e2f70d284b3091c2d713aa093a0f5898487e431" integrity sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA== +"@prisma/driver-adapter-utils@6.15.0": + version "6.15.0" + resolved "https://registry.yarnpkg.com/@prisma/driver-adapter-utils/-/driver-adapter-utils-6.15.0.tgz#7e1803eb8ad18f3c8a8e9843049226b66ef3447d" + integrity sha512-tzcMG1eEBM3IJ8TBHo4jGeoUaalctqGXbrvxIoZb8jSEtgR82IUhdVyHHLVTlT8MdrHovcQJJ3jfcQfJARRnaQ== + dependencies: + "@prisma/debug" "6.15.0" + "@prisma/driver-adapter-utils@7.8.0": version "7.8.0" resolved "https://registry.yarnpkg.com/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz#60f42a5bfb257d01185c27d8022a2a500d71d56b" @@ -20155,6 +20176,11 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +ky@1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/ky/-/ky-1.7.5.tgz#69c9b70ff818fd6108fc04a89c1454412c11dfce" + integrity sha512-HzhziW6sc5m0pwi5M196+7cEBtbt0lCYi67wNsiwMUmz833wloE0gbzJPWKs1gliFKQb34huItDQX97LyOdPdA== + langsmith@^0.3.67: version "0.3.74" resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.74.tgz#014d31a9ff7530b54f0d797502abd512ce8fb6fb"