Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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?
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m/q: Isn't that an issue users would face too?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in one narrow case. A CJS app on Node 22 to 24.12 loads this ESM-only package through require(esm), which Node routes through the async module hooks. The hook thread transforms the file and posts the "module injected" notice over a message port, so the main thread only processes it on a later tick. An ORM call made in the very same tick as the require therefore runs before the SDK subscribes to the channels and gets no operation span; anything after the first await is covered. Real apps make their first query after startup, so I left it as a documented edge (also in the PR body of #24682). It is inherent to the async-hook path for any ESM-only dependency required from CJS, not Prisma specific; from Node 24.13 / 25.1 the sync hooks make it disappear. Happy to look at subscribing eagerly for that path in a follow-up if we think it matters.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add that in the troubleshooting section in the docs, if that really occurs

@chargome chargome Sep 25, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh sorry @JPeer264 my clanker went off limits and responded here


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();
Original file line number Diff line number Diff line change
@@ -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] }, () => {
Comment thread
cursor[bot] marked this conversation as resolved.
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'],
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading