Skip to content

Commit 268cb2f

Browse files
chargomeclaude
andcommitted
feat(node): Support Prisma 8 in prismaIntegration
Prisma 8 has no tracing surface, so wrap its ORM terminals via orchestrion instead. Each ORM call gets a `prisma:client:operation` span (`db.operation.name`, `db.collection.name`, plus v7's `method`, `model` and `name`) with the `pg` query spans nested underneath. No user code is needed. The patch point is `CollectionImpl` in the transitive `@prisma/orm-family-sql` package, pinned to `>=8.0.0-rc.8 <9`. Lazy terminals (`all`, `createAll`, `updateAll`, `deleteAll`) return a single-use thenable that orchestrion's promise wrapper would consume, so they are wrapped `Sync` and the subscriber hooks `toArray` and the async iterator instead. Terminals calling terminals reuse the outer span. Operation spans require a parent span. CommonJS apps load the ESM-only runtime through `require(esm)`, which only the synchronous module hooks (Node 24.13 / 25.1) transform; older Node keeps `pg`-only spans in CJS. ESM apps are fully covered. Fixes #24237 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent b618c97 commit 268cb2f

10 files changed

Lines changed: 749 additions & 31 deletions

File tree

‎.size-limit.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ module.exports = [
406406
import: createImport('init'),
407407
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
408408
gzip: true,
409-
limit: '135 KB',
409+
limit: '137 KB',
410410
disablePlugins: ['@size-limit/esbuild'],
411411
},
412412
{
@@ -452,7 +452,7 @@ module.exports = [
452452
path: 'packages/node/build/esm/index.js',
453453
import: createImport('init'),
454454
gzip: true,
455-
limit: '114 KB',
455+
limit: '115 KB',
456456
disablePlugins: ['@size-limit/esbuild'],
457457
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
458458
modifyWebpackConfig: function (config) {

‎dev-packages/node-integration-tests/suites/tracing/prisma-orm-v8/test.ts‎

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,115 @@
1+
import type { SerializedStreamedSpan, SerializedStreamedSpanContainer } from '@sentry/core';
12
import { afterAll, describe, expect } from 'vitest';
23
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';
34

45
afterAll(() => {
56
cleanupChildProcesses();
67
});
78

8-
// Prisma 8 ("Prisma Next") has no tracing surface (its scorecard lists OpenTelemetry spans as not in 8.0), so
9-
// `prismaIntegration` is inert and only the `pg` spans from `postgresIntegration` remain. Pinned here so a
10-
// Prisma release that adds a tracing surface shows up. The runtime runs on every Node version in the matrix;
11-
// only the Prisma 8 CLI needs Node 22.18+, which is why the generated files are committed.
9+
// The CJS scenario loads the ESM-only package via `require(esm)`, which Node's module hooks only see from
10+
// Node 22 on; on Node 20 the ORM terminals load uninstrumented.
11+
const cjsLoadsInstrumentedEsm = Number(process.versions.node.split('.')[0]) >= 22;
12+
13+
function getSegment(container: SerializedStreamedSpanContainer): SerializedStreamedSpan {
14+
const segment = container.items.find(item => item.is_segment);
15+
expect(segment?.name).toBe('Test Transaction');
16+
return segment!;
17+
}
18+
19+
function getQuerySpans(container: SerializedStreamedSpanContainer): SerializedStreamedSpan[] {
20+
const querySpans = container.items.filter(
21+
item => item.attributes['sentry.origin']?.value === 'auto.db.postgres' && item.attributes['db.query.text']?.value,
22+
);
23+
expect(querySpans.map(span => span.name)).toEqual(
24+
expect.arrayContaining(['INSERT "public"."user"', 'SELECT "public"."user"', 'DELETE "public"."user"']),
25+
);
26+
querySpans.forEach(span => {
27+
expect(span.attributes['sentry.op']?.value).toBe('db');
28+
expect(span.attributes['db.system.name']?.value).toBe('postgresql');
29+
});
30+
return querySpans;
31+
}
32+
33+
function getOperationSpans(container: SerializedStreamedSpanContainer): SerializedStreamedSpan[] {
34+
return container.items.filter(item => item.attributes['sentry.origin']?.value === 'auto.db.prisma');
35+
}
36+
37+
// Prisma 8 has no tracing surface: the operation spans come from the orchestrion channels the runtime hook
38+
// injects, and the `pg` spans underneath stand in for v7's `db_query` spans.
1239
describe('Prisma ORM v8 Tests', () => {
1340
describeWithDockerCompose('Prisma ORM v8', { workingDirectory: [__dirname] }, () => {
1441
createEsmAndCjsTests(
1542
__dirname,
1643
'scenario.mjs',
1744
'instrument.mjs',
18-
(createRunner, test) => {
19-
test('should instrument PostgreSQL queries from Prisma ORM via pg', { timeout: 75_000 }, async () => {
45+
(createRunner, test, mode) => {
46+
if (mode === 'cjs' && !cjsLoadsInstrumentedEsm) {
47+
test(
48+
'should keep the pg spans on the transaction when require(esm) bypasses the loader',
49+
{ timeout: 75_000 },
50+
async () => {
51+
await createRunner()
52+
.unordered()
53+
.expect({
54+
span: container => {
55+
const segment = getSegment(container);
56+
expect(getOperationSpans(container)).toEqual([]);
57+
getQuerySpans(container).forEach(span => expect(span.parent_span_id).toBe(segment.span_id));
58+
},
59+
})
60+
.start()
61+
.completed();
62+
},
63+
);
64+
return;
65+
}
66+
67+
test('should instrument Prisma ORM operations and nest their queries', { timeout: 75_000 }, async () => {
2068
await createRunner()
2169
.unordered()
2270
.expect({
2371
span: container => {
24-
const segment = container.items.find(item => item.is_segment);
25-
expect(segment?.name).toBe('Test Transaction');
72+
const segment = getSegment(container);
73+
const querySpans = getQuerySpans(container);
74+
const operationSpans = getOperationSpans(container);
2675

27-
const querySpans = container.items.filter(
28-
item =>
29-
item.attributes['sentry.origin']?.value === 'auto.db.postgres' &&
30-
item.attributes['db.query.text']?.value,
31-
);
32-
expect(querySpans.map(span => span.name)).toEqual(
33-
expect.arrayContaining([
34-
'INSERT "public"."user"',
35-
'SELECT "public"."user"',
36-
'DELETE "public"."user"',
37-
]),
38-
);
39-
querySpans.forEach(span => {
40-
expect(span.attributes['sentry.op']?.value).toBe('db');
41-
expect(span.attributes['db.system.name']?.value).toBe('postgresql');
42-
expect(span.parent_span_id).toBe(segment?.span_id);
76+
expect(operationSpans.map(span => span.name)).toEqual([
77+
'prisma:client:operation',
78+
'prisma:client:operation',
79+
'prisma:client:operation',
80+
]);
81+
expect(operationSpans.map(span => span.attributes['method']?.value)).toEqual([
82+
'create',
83+
'all',
84+
'delete',
85+
]);
86+
operationSpans.forEach(span => {
87+
const method = span.attributes['method']?.value;
88+
expect(span.parent_span_id).toBe(segment.span_id);
89+
expect(span.attributes).toMatchObject({
90+
'sentry.origin': { value: 'auto.db.prisma', type: 'string' },
91+
'sentry.op': { value: 'db', type: 'string' },
92+
'db.operation.name': { value: method, type: 'string' },
93+
'db.collection.name': { value: 'user', type: 'string' },
94+
model: { value: 'User', type: 'string' },
95+
name: { value: `User.${method}`, type: 'string' },
96+
});
4397
});
4498

45-
expect(
46-
container.items.filter(item => item.attributes['sentry.origin']?.value === 'auto.db.prisma'),
47-
).toEqual([]);
99+
const queriesByOperation = (method: string): unknown[] => {
100+
const operation = operationSpans.find(span => span.attributes['method']?.value === method);
101+
return querySpans
102+
.filter(span => span.parent_span_id === operation?.span_id)
103+
.map(span => span.attributes['db.query.text']?.value);
104+
};
105+
expect(queriesByOperation('create')).toEqual(
106+
expect.arrayContaining([expect.stringMatching(/^INSERT INTO "public"\."user" /)]),
107+
);
108+
expect(queriesByOperation('all')).toEqual([expect.stringMatching(/^SELECT .* FROM "public"\."user"$/)]);
109+
expect(queriesByOperation('delete')).toEqual(
110+
expect.arrayContaining([expect.stringMatching(/^DELETE FROM "public"\."user" /)]),
111+
);
112+
expect(querySpans.filter(span => span.parent_span_id === segment.span_id)).toEqual([]);
48113
},
49114
})
50115
.start()

‎packages/server-utils/src/integrations/prisma/index.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { IntegrationFn } from '@sentry/core';
22
import { defineIntegration } from '@sentry/core';
3+
import { prismaModuleNames } from '../../orchestrion/config/prisma';
4+
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
35
import { setGlobalTracingHelper } from './global';
6+
import { instrumentPrismaV8 } from './orchestrion';
47
import { ActiveTracingHelper } from './tracing-helper';
58

69
const INTEGRATION_NAME = 'Prisma' as const;
@@ -43,14 +46,22 @@ const _prismaIntegration = ((options?: PrismaOptions) => {
4346
setupOnce() {
4447
instrumentPrisma(options);
4548
},
49+
// Prisma 8 has no tracing helper to install; its ORM terminals are instrumented via orchestrion instead.
50+
setup(client) {
51+
invokeOrchestrionInstrumentation(client, prismaModuleNames, instrumentPrismaV8, [
52+
{ ignoreSpanTypes: options?.instrumentationConfig?.ignoreSpanTypes ?? [] },
53+
]);
54+
},
4655
};
4756
}) satisfies IntegrationFn;
4857

4958
/**
5059
* Adds Sentry tracing instrumentation for the [prisma](https://www.npmjs.com/package/prisma) library.
5160
* For more information, see the [`prismaIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/).
5261
*
53-
* NOTE: This integration works out of the box with Prisma v6, and v7.
62+
* NOTE: This integration works out of the box with Prisma v6, v7 and v8. Prisma v8 ("Prisma Next")
63+
* has no tracing surface of its own, so its ORM calls are instrumented through Sentry's runtime hook
64+
* or bundler plugin, like the other channel-based integrations.
5465
* On Prisma versions prior to v6, add `previewFeatures = ["tracing"]` to the client generator block of your Prisma schema:
5566
*
5667
* ```

0 commit comments

Comments
 (0)