From 5430d50980f9309d745bd76b9a40e3d69455ccfa Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 11 Sep 2026 14:11:48 +0200 Subject: [PATCH] test(node): Add Prisma 8 integration test suite Prisma 8 ("Prisma Next") ships no tracing surface, so `prismaIntegration` is inert there and only the `pg` query spans remain. Pin that behaviour against 8.0.0-rc.8 so a Prisma release that adds a tracing surface, or breaks the `pg` path, shows up. The emitted `contract.json` and an `init.sql` dumped from `prisma-cli db init` are committed: the Prisma 8 CLI needs Node 22.18+ and breaks npm's peer-dependency resolution. The contract has no `DateTime` column, since Prisma 8 decodes timestamps into `Temporal`, global only on Node 26. The runner's ESM-to-CJS converter now drops `with { type: 'json' }` import attributes so the scenario can import the contract. Refs #24237 Co-Authored-By: Claude Fable 5.1 --- .../tracing/prisma-orm-v8/docker-compose.yml | 22 +++ .../tracing/prisma-orm-v8/instrument.mjs | 9 ++ .../prisma-orm-v8/prisma/contract.json | 132 ++++++++++++++++++ .../prisma-orm-v8/prisma/contract.prisma | 9 ++ .../tracing/prisma-orm-v8/prisma/init.sql | 64 +++++++++ .../suites/tracing/prisma-orm-v8/scenario.mjs | 36 +++++ .../suites/tracing/prisma-orm-v8/test.ts | 62 ++++++++ .../utils/runner/createEsmAndCjsTests.ts | 3 +- 8 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.json create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.prisma create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/init.sql create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/docker-compose.yml new file mode 100644 index 000000000000..9acf7180d21c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.9' + +services: + db: + # Prisma 8 requires PostgreSQL 15 or newer. + image: postgres:16 + restart: always + ports: + - '5436:5432' + environment: + POSTGRES_USER: prisma + POSTGRES_PASSWORD: prisma + POSTGRES_DB: tests + # Dumped from `prisma-cli db init`; the Prisma 8 CLI needs Node 22.18+, so it isn't run at test time. + volumes: + - ./prisma/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U prisma -d tests'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/instrument.mjs new file mode 100644 index 000000000000..46a27dd03b74 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.json new file mode 100644 index 000000000000..3b9090e28776 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.json @@ -0,0 +1,132 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "3916f444a8a17ad749191acf9e08dad97d1a327b88c2f1d45d12f240296aa8b2", + "roots": { + "user": { + "model": "User", + "namespace": "public" + } + }, + "domain": { + "namespaces": { + "public": { + "models": { + "User": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "pg/int4@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + } + }, + "namespaceId": "public", + "table": "user" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "public": { + "entries": { + "table": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "pg/int4@1", + "default": { + "expression": "autoincrement()", + "kind": "function" + }, + "nativeType": "int4", + "nullable": false + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [ + { + "columns": ["email"] + } + ] + } + } + }, + "id": "public", + "kind": "postgres-schema" + } + }, + "storageHash": "f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "checkConstraint": true, + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true, + "scalarList": true + } + }, + "extensions": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma contract emit\".", + "regenerate": "To regenerate, run: prisma contract emit" + } +} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.prisma new file mode 100644 index 000000000000..11706f4db019 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/contract.prisma @@ -0,0 +1,9 @@ +// use prisma-next + +// Regenerate `contract.json` with `prisma-cli contract emit` and `init.sql` from `prisma-cli db init` (Node 22.18+). +// No `DateTime` column: Prisma 8 decodes timestamps into `Temporal`, global only on Node 26. +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/init.sql b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/init.sql new file mode 100644 index 000000000000..655d447ca485 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/prisma/init.sql @@ -0,0 +1,64 @@ +CREATE SCHEMA prisma_contract; +CREATE TABLE prisma_contract.contract ( + core_hash text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + contract_json jsonb NOT NULL +); +CREATE TABLE prisma_contract.ledger ( + id bigint NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + space text NOT NULL, + migration_name text NOT NULL, + migration_hash text NOT NULL, + origin_core_hash text, + origin_profile_hash text, + destination_core_hash text NOT NULL, + destination_profile_hash text, + operations jsonb NOT NULL +); +CREATE SEQUENCE prisma_contract.ledger_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE prisma_contract.ledger_id_seq OWNED BY prisma_contract.ledger.id; +CREATE TABLE prisma_contract.marker ( + space text DEFAULT 'app'::text NOT NULL, + core_hash text NOT NULL, + profile_hash text NOT NULL, + contract_json jsonb, + canonical_version integer, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + app_tag text, + meta jsonb DEFAULT '{}'::jsonb NOT NULL, + invariants text[] DEFAULT '{}'::text[] NOT NULL +); +CREATE TABLE public."user" ( + email text NOT NULL, + id integer NOT NULL, + name text +); +CREATE SEQUENCE public.user_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE public.user_id_seq OWNED BY public."user".id; +ALTER TABLE ONLY prisma_contract.ledger ALTER COLUMN id SET DEFAULT nextval('prisma_contract.ledger_id_seq'::regclass); +ALTER TABLE ONLY public."user" ALTER COLUMN id SET DEFAULT nextval('public.user_id_seq'::regclass); +INSERT INTO prisma_contract.contract VALUES ('f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5', '2026-09-09 09:15:28.369559+00', '{"meta": {}, "roots": {"user": {"model": "User", "namespace": "public"}}, "domain": {"namespaces": {"public": {"models": {"User": {"fields": {"id": {"type": {"kind": "scalar", "codecId": "pg/int4@1"}, "nullable": false}, "name": {"type": {"kind": "scalar", "codecId": "pg/text@1"}, "nullable": true}, "email": {"type": {"kind": "scalar", "codecId": "pg/text@1"}, "nullable": false}}, "storage": {"table": "user", "fields": {"id": {"column": "id"}, "name": {"column": "name"}, "email": {"column": "email"}}, "namespaceId": "public"}, "relations": {}}}}}}, "target": "postgres", "storage": {"namespaces": {"public": {"id": "public", "entries": {"table": {"user": {"columns": {"id": {"codecId": "pg/int4@1", "default": {"kind": "function", "expression": "autoincrement()"}, "nullable": false, "nativeType": "int4"}, "name": {"codecId": "pg/text@1", "nullable": true, "nativeType": "text"}, "email": {"codecId": "pg/text@1", "nullable": false, "nativeType": "text"}}, "indexes": [], "uniques": [{"columns": ["email"]}], "primaryKey": {"columns": ["id"]}, "foreignKeys": []}}}}}, "storageHash": "f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5"}, "extensions": {}, "profileHash": "3916f444a8a17ad749191acf9e08dad97d1a327b88c2f1d45d12f240296aa8b2", "capabilities": {"sql": {"enums": true, "lateral": true, "returning": true, "scalarList": true, "checkConstraint": true, "defaultInInsert": true}, "postgres": {"limit": true, "jsonAgg": true, "lateral": true, "orderBy": true, "returning": true, "distinctOn": true}}, "targetFamily": "sql"}'); +INSERT INTO prisma_contract.ledger VALUES (1, '2026-09-09 09:15:28.369559+00', 'app', '', 'f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5', '', NULL, 'f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5', NULL, '[{"id": "table.user", "label": "Create table \"user\"", "target": {"id": "postgres", "details": {"name": "user", "schema": "public", "objectType": "table"}}, "execute": [{"sql": "CREATE TABLE \"public\".\"user\" (\n \"email\" text NOT NULL,\n \"id\" SERIAL NOT NULL,\n \"name\" text,\n PRIMARY KEY (\"id\")\n)", "params": [], "description": "create table \"user\""}], "summary": "Creates table \"user\"", "precheck": [{"sql": "SELECT (to_regclass($1)) IS NULL AS \"result\"", "params": ["\"public\".\"user\""], "description": "ensure table \"user\" does not exist"}], "postcheck": [{"sql": "SELECT (to_regclass($1)) IS NOT NULL AS \"result\"", "params": ["\"public\".\"user\""], "description": "verify table \"user\" exists"}], "operationClass": "additive"}, {"id": "unique.user.user_email_key", "label": "Add unique constraint on \"user\" (email)", "target": {"id": "postgres", "details": {"name": "user_email_key", "table": "user", "schema": "public", "objectType": "unique"}}, "execute": [{"sql": "ALTER TABLE \"public\".\"user\" ADD CONSTRAINT \"user_email_key\" UNIQUE (\"email\")", "description": "add unique constraint \"user_email_key\""}], "precheck": [{"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"", "params": ["user_email_key", "public", "\"public\".\"user\""], "description": "ensure constraint \"user_email_key\" does not exist"}], "postcheck": [{"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"pg_constraint\" AS \"c\" INNER JOIN \"pg_namespace\" AS \"n\" ON \"n\".\"oid\" = \"c\".\"connamespace\" WHERE (\"c\".\"conname\" = $1 AND \"n\".\"nspname\" = $2 AND \"c\".\"conrelid\" = to_regclass($3))) AS \"result\"", "params": ["user_email_key", "public", "\"public\".\"user\""], "description": "verify constraint \"user_email_key\" exists"}], "operationClass": "additive"}]'); +INSERT INTO prisma_contract.marker VALUES ('app', 'f4e1954fd8bed87828d13c3f1a02164dc9796ef1af76ed6f98184c01263169c5', '3916f444a8a17ad749191acf9e08dad97d1a327b88c2f1d45d12f240296aa8b2', NULL, NULL, '2026-09-09 09:15:28.369559+00', NULL, '{}', '{}'); +ALTER TABLE ONLY prisma_contract.contract + ADD CONSTRAINT contract_pkey PRIMARY KEY (core_hash); +ALTER TABLE ONLY prisma_contract.ledger + ADD CONSTRAINT ledger_pkey PRIMARY KEY (id); +ALTER TABLE ONLY prisma_contract.marker + ADD CONSTRAINT marker_pkey PRIMARY KEY (space); +ALTER TABLE ONLY public."user" + ADD CONSTRAINT user_email_key UNIQUE (email); +ALTER TABLE ONLY public."user" + ADD CONSTRAINT user_pkey PRIMARY KEY (id); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/scenario.mjs new file mode 100644 index 000000000000..a588cda77b05 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/scenario.mjs @@ -0,0 +1,36 @@ +// ESM-only package: the CJS variant gets the namespace from `require(esm)`, so the factory is read off `default`. +import * as prismaPostgres from '@prisma/orm-postgres/runtime'; +import * as Sentry from '@sentry/node'; +import { randomBytes } from 'crypto'; +import contractJson from './prisma/contract.json' with { type: 'json' }; + +const url = 'postgresql://prisma:prisma@localhost:5436/tests'; + +async function run() { + const db = prismaPostgres.default({ contractJson, url }); + + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + // With `require(esm)` the module-injection notice arrives on a later tick, so a call in the same + // tick as the require would run before the SDK subscribes to the channels. + await new Promise(resolve => setImmediate(resolve)); + + await db.orm.public.User.create({ + name: 'Tilda', + email: `tilda_${randomBytes(4).toString('hex')}@sentry.io`, + }); + + await db.orm.public.User.all(); + + await db.orm.public.User.where(user => user.email.like('%sentry.io')).delete(); + }, + ); + + await db.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts new file mode 100644 index 000000000000..10e550b305b0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts @@ -0,0 +1,62 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +// Prisma 8 ("Prisma Next") has no tracing surface (its scorecard lists OpenTelemetry spans as not in 8.0), so +// `prismaIntegration` is inert and only the `pg` spans from `postgresIntegration` remain. Pinned here so a +// Prisma release that adds a tracing surface shows up. The runtime runs on every Node version in the matrix; +// only the Prisma 8 CLI needs Node 22.18+, which is why the generated files are committed. +describe('Prisma ORM v8 Tests', () => { + describeWithDockerCompose('Prisma ORM v8', { workingDirectory: [__dirname] }, () => { + createEsmAndCjsTests( + __dirname, + 'scenario.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('should instrument PostgreSQL queries from Prisma ORM via pg', { timeout: 75_000 }, async () => { + await createRunner() + .unordered() + .expect({ + span: container => { + const segment = container.items.find(item => item.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + const querySpans = container.items.filter( + item => + item.attributes['sentry.origin']?.value === 'auto.db.postgres' && + item.attributes['db.query.text']?.value, + ); + expect(querySpans.map(span => span.name)).toEqual( + expect.arrayContaining([ + 'INSERT "public"."user"', + 'SELECT "public"."user"', + 'DELETE "public"."user"', + ]), + ); + querySpans.forEach(span => { + expect(span.attributes['sentry.op']?.value).toBe('db'); + expect(span.attributes['db.system.name']?.value).toBe('postgresql'); + expect(span.parent_span_id).toBe(segment?.span_id); + }); + + expect( + container.items.filter(item => item.attributes['sentry.origin']?.value === 'auto.db.prisma'), + ).toEqual([]); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + '@prisma/orm-postgres': '8.0.0-rc.8', + }, + copyPaths: ['prisma'], + }, + ); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts index d6fea21637cc..3013148f9576 100644 --- a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts +++ b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts @@ -301,9 +301,10 @@ function convertEsmToCjs(content: string): string { let newContent = content; // Handle default imports: import x from 'y' -> const x = require('y') + // Import attributes (`with { type: 'json' }`) are dropped; `require` reads JSON natively. newContent = newContent.replace( // eslint-disable-next-line regexp/optimal-quantifier-concatenation, regexp/no-super-linear-backtracking - /import\s+([\w*{}\s,]+)\s+from\s+['"]([^'"]+)['"]/g, + /import\s+([\w*{}\s,]+)\s+from\s+['"]([^'"]+)['"](?:\s+with\s*\{[^}]*\})?/g, (_, imports: string, module: string) => { if (imports.includes('* as')) { // Handle namespace imports: import * as x from 'y' -> const x = require('y')