Skip to content

Commit f142d61

Browse files
s1gr1dnicohrubec
andauthored
ref(node): Streamline Prisma instrumentation (v6 and v7) (#21819)
Streamlines the vendored Prisma instrumentation onto Sentry's span APIs (v6/v7): - Folds attributes previously set via the `spanStart` hook into span creation in the instrumentation. - Uses `startSpanManual`/`startInactiveSpan` from `@sentry/core` instead of the OTel tracer in the vendored tracing helper. - Removes unused code: `setTracerProvider`/`tracerProvider` and unused contract types (`EngineTrace`, `EngineTraceEvent`, `LogLevel`). v5 cleanup will be done in a followup. Part of #20744 Closes #21820 --------- Co-authored-by: Nicolas Hrubec <nico.hrubec@sentry.io>
1 parent 9d2ed38 commit f142d61

8 files changed

Lines changed: 181 additions & 166 deletions

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ describe('Prisma ORM v6 Tests', () => {
2525
const spans = transaction.spans || [];
2626
expect(spans.length).toBeGreaterThanOrEqual(5);
2727

28+
// Each operation span is a direct child of the transaction; the db query span is a child of the engine query span.
29+
const rootSpanId = transaction.contexts?.trace?.span_id;
30+
31+
const operationSpans = spans.filter(s => s.description === 'prisma:client:operation');
32+
expect(operationSpans.length).toBeGreaterThanOrEqual(1);
33+
operationSpans.forEach(operation => {
34+
expect(operation.parent_span_id).toBe(rootSpanId);
35+
});
36+
37+
const dbQuerySpan = spans.find(
38+
s => s.data?.['sentry.origin'] === 'auto.db.otel.prisma' && s.data?.['db.query.text'],
39+
);
40+
expect(dbQuerySpan).toBeDefined();
41+
const dbQueryParent = spans.find(s => s.span_id === dbQuerySpan?.parent_span_id);
42+
expect(dbQueryParent?.description).toBe('prisma:engine:query');
43+
2844
function expectPrismaSpanToIncludeSpanWith(span: Partial<SpanJSON>) {
2945
expect(spans).toContainEqual(
3046
expect.objectContaining({
@@ -92,6 +108,10 @@ describe('Prisma ORM v6 Tests', () => {
92108
},
93109
description: 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1',
94110
});
111+
112+
// The db query span name must always be rewritten to the SQL text; the raw engine span
113+
// name should never leak through.
114+
expect(spans.find(span => span.description === 'prisma:engine:db_query')).toBeUndefined();
95115
},
96116
})
97117
.start()

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ conditionalTest({ min: 20 })('Prisma ORM v7 Tests', () => {
2626
const spans = transaction.spans || [];
2727
expect(spans.length).toBeGreaterThanOrEqual(5);
2828

29+
// Each operation span is a direct child of the transaction; the db query span is a child of its operation span.
30+
const rootSpanId = transaction.contexts?.trace?.span_id;
31+
32+
const operationSpans = spans.filter(s => s.description === 'prisma:client:operation');
33+
expect(operationSpans.length).toBeGreaterThanOrEqual(1);
34+
operationSpans.forEach(operation => {
35+
expect(operation.parent_span_id).toBe(rootSpanId);
36+
});
37+
38+
const prismaDbQuerySpan = spans.find(
39+
s => s.data?.['sentry.origin'] === 'auto.db.otel.prisma' && s.data?.['db.query.text'],
40+
);
41+
expect(prismaDbQuerySpan).toBeDefined();
42+
const dbQueryParent = spans.find(s => s.span_id === prismaDbQuerySpan?.parent_span_id);
43+
expect(dbQueryParent?.description).toBe('prisma:client:operation');
44+
2945
// Verify Prisma spans have the correct origin
3046
const prismaSpans = spans.filter(
3147
span => span.data && span.data['sentry.origin'] === 'auto.db.otel.prisma',
@@ -58,6 +74,10 @@ conditionalTest({ min: 20 })('Prisma ORM v7 Tests', () => {
5874
expect(dbQuerySpan?.op).toBe('db');
5975
expect(dbQuerySpan?.description).toBe(dbQuerySpan?.data?.['db.query.text']);
6076
expect(dbQuerySpan?.description).not.toBe('prisma:client:db_query');
77+
78+
// The db query span name must always be rewritten to the SQL text; the raw client span
79+
// name should never leak through.
80+
expect(spans.find(span => span.description === 'prisma:client:db_query')).toBeUndefined();
6181
},
6282
})
6383
.start()

packages/node/src/integrations/tracing/prisma/index.ts

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class SentryPrismaInteropInstrumentation extends PrismaInstrumentation {
104104

105105
try {
106106
engineSpanEvent.spans.forEach(engineSpan => {
107-
const kind = engineSpanKindToOTELSpanKind(engineSpan.kind);
107+
const kind = engineSpan.kind === 'client' ? SpanKind.CLIENT : SpanKind.INTERNAL;
108108

109109
const parentSpanId = engineSpan.parent_span_id;
110110
const spanId = engineSpan.span_id;
@@ -159,16 +159,6 @@ class SentryPrismaInteropInstrumentation extends PrismaInstrumentation {
159159
}
160160
}
161161

162-
function engineSpanKindToOTELSpanKind(engineSpanKind: V5EngineSpanKind): SpanKind {
163-
switch (engineSpanKind) {
164-
case 'client':
165-
return SpanKind.CLIENT;
166-
case 'internal':
167-
default: // Other span kinds aren't currently supported
168-
return SpanKind.INTERNAL;
169-
}
170-
}
171-
172162
export const instrumentPrisma = generateInstrumentOnce<PrismaOptions>(INTEGRATION_NAME, options => {
173163
return new SentryPrismaInteropInstrumentation(options);
174164
});
@@ -177,26 +167,8 @@ export const instrumentPrisma = generateInstrumentOnce<PrismaOptions>(INTEGRATIO
177167
* Adds Sentry tracing instrumentation for the [prisma](https://www.npmjs.com/package/prisma) library.
178168
* For more information, see the [`prismaIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/).
179169
*
180-
* NOTE: By default, this integration works with Prisma version 6.
181-
* To get performance instrumentation for other Prisma versions,
182-
* 1. Install the `@prisma/instrumentation` package with the desired version.
183-
* 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option of this integration:
184-
*
185-
* ```js
186-
* import { PrismaInstrumentation } from '@prisma/instrumentation'
187-
*
188-
* Sentry.init({
189-
* integrations: [
190-
* prismaIntegration({
191-
* // Override the default instrumentation that Sentry uses
192-
* prismaInstrumentation: new PrismaInstrumentation()
193-
* })
194-
* ]
195-
* })
196-
* ```
197-
*
198-
* The passed instrumentation instance will override the default instrumentation instance the integration would use, while the `prismaIntegration` will still ensure data compatibility for the various Prisma versions.
199-
* 1. Depending on your Prisma version (prior to version 6), add `previewFeatures = ["tracing"]` to the client generator block of your Prisma schema:
170+
* NOTE: This integration works out of the box with Prisma v6, and v7.
171+
* On Prisma versions prior to v6, add `previewFeatures = ["tracing"]` to the client generator block of your Prisma schema:
200172
*
201173
* ```
202174
* generator client {
@@ -218,6 +190,9 @@ export const prismaIntegration = defineIntegration((options?: PrismaOptions) =>
218190
return;
219191
}
220192

193+
// Prisma v5 engine spans are created via the `createEngineSpan` path above, which bypasses the
194+
// tracing helper, so this hook applies origin, the db_query rename, and the db.system backfill to
195+
// them. v6/v7 spans already get these from the helper; the guards are idempotent, so it's a no-op there.
221196
client.on('spanStart', span => {
222197
const spanJSON = spanToJSON(span);
223198
if (spanJSON.description?.startsWith('prisma:')) {

packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts

Lines changed: 116 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,149 +6,184 @@
66
* - Vendored from: https://github.com/prisma/prisma/tree/b6feea5565ec577545a79547d24273ccdd11b4c7/packages/instrumentation
77
* - Upstream version: @prisma/instrumentation@7.8.0
88
* - Replaced `@prisma/instrumentation-contract` imports with local vendored types
9-
* - Minor TypeScript strictness adjustments for this repository's compiler settings
9+
* - Span creation uses Sentry's span APIs (`startSpanManual` / `startInactiveSpan`) instead of the OTel tracer
10+
* - Span creation sets the Sentry origin, renames `db_query` spans to their SQL text, and backfills
11+
* `db.system` for older Prisma versions
1012
*/
11-
/* eslint-disable */
1213

14+
import type { Span, SpanAttributes, SpanKindValue, SpanLink } from '@sentry/core';
1315
import {
14-
Attributes,
15-
Context,
16-
context as _context,
17-
Span,
18-
SpanKind,
19-
SpanOptions,
20-
trace,
21-
Tracer,
22-
TracerProvider,
23-
} from '@opentelemetry/api';
24-
import type { EngineSpan, EngineSpanKind, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types';
16+
getActiveSpan,
17+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
18+
SPAN_KIND,
19+
startInactiveSpan,
20+
startSpanManual,
21+
} from '@sentry/core';
22+
import type { EngineSpan, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types';
2523

2624
const showAllTraces = process.env.PRISMA_SHOW_ALL_TRACES === 'true';
2725

2826
const nonSampledTraceParent = `00-10-10-00`;
2927

28+
const PRISMA_ORIGIN = 'auto.db.otel.prisma';
29+
3030
type Options = {
31-
tracerProvider: TracerProvider;
3231
ignoreSpanTypes: (string | RegExp)[];
3332
};
3433

35-
function engineSpanKindToOtelSpanKind(engineSpanKind: EngineSpanKind): SpanKind {
36-
switch (engineSpanKind) {
37-
case 'client':
38-
return SpanKind.CLIENT;
39-
case 'internal':
40-
default:
41-
return SpanKind.INTERNAL;
34+
/**
35+
* Older Prisma versions emit `prisma:engine:db_query` spans without a `db.system`, so it's backfilled here.
36+
*/
37+
function buildSpanAttributes(name: string, attributes: Record<string, unknown> | undefined): SpanAttributes {
38+
const merged: SpanAttributes = {
39+
...(attributes as SpanAttributes | undefined),
40+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN,
41+
};
42+
43+
if (name === 'prisma:engine:db_query' && merged['db.system'] == null) {
44+
merged['db.system'] = 'prisma';
45+
}
46+
47+
return merged;
48+
}
49+
50+
/**
51+
* Db query spans are named after their SQL text (e.g. `SELECT * FROM "User"`) rather than the generic
52+
* engine name. v5/v6 emit `prisma:engine:db_query`; v7 inlined the engine and emits `prisma:client:db_query`.
53+
*/
54+
function buildSpanName(name: string, attributes: SpanAttributes): string {
55+
const queryText = attributes['db.query.text'];
56+
if ((name === 'prisma:engine:db_query' || name === 'prisma:client:db_query') && typeof queryText === 'string') {
57+
return queryText;
4258
}
59+
return name;
4360
}
4461

4562
export class ActiveTracingHelper implements TracingHelper {
46-
private tracerProvider: TracerProvider;
4763
private ignoreSpanTypes: (string | RegExp)[];
4864

49-
constructor({ tracerProvider, ignoreSpanTypes }: Options) {
50-
this.tracerProvider = tracerProvider;
65+
public constructor({ ignoreSpanTypes }: Options) {
5166
this.ignoreSpanTypes = ignoreSpanTypes;
5267
}
5368

54-
isEnabled(): boolean {
69+
public isEnabled(): boolean {
5570
return true;
5671
}
5772

58-
getTraceParent(context?: Context | undefined): string {
59-
const span = trace.getSpanContext(context ?? _context.active());
60-
if (span) {
61-
return `00-${span.traceId}-${span.spanId}-0${span.traceFlags}`;
73+
public getTraceParent(span?: Span): string {
74+
const spanContext = (span ?? getActiveSpan())?.spanContext();
75+
if (spanContext) {
76+
return `00-${spanContext.traceId}-${spanContext.spanId}-0${spanContext.traceFlags}`;
6277
}
6378
return nonSampledTraceParent;
6479
}
6580

66-
dispatchEngineSpans(spans: EngineSpan[]): void {
67-
const tracer = this.tracerProvider.getTracer('prisma');
81+
public dispatchEngineSpans(spans: EngineSpan[]): void {
6882
const linkIds = new Map<string, string>();
6983
const roots = spans.filter(span => span.parentId === null);
7084

7185
for (const root of roots) {
72-
dispatchEngineSpan(tracer, root, spans, linkIds, this.ignoreSpanTypes);
86+
dispatchEngineSpan(root, spans, linkIds, this.ignoreSpanTypes);
7387
}
7488
}
7589

76-
getActiveContext(): Context | undefined {
77-
return _context.active();
90+
public getActiveContext(): Span | undefined {
91+
return getActiveSpan();
7892
}
7993

80-
runInChildSpan<R>(options: string | ExtendedSpanOptions, callback: SpanCallback<R>): R {
81-
if (typeof options === 'string') {
82-
options = { name: options };
83-
}
94+
public runInChildSpan<R>(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback<R>): R {
95+
const options: ExtendedSpanOptions = typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions;
8496

8597
if (options.internal && !showAllTraces) {
8698
return callback();
8799
}
88100

89-
const tracer = this.tracerProvider.getTracer('prisma');
90-
const context = options.context ?? this.getActiveContext();
91101
const name = `prisma:client:${options.name}`;
92102

93103
if (shouldIgnoreSpan(name, this.ignoreSpanTypes)) {
94104
return callback();
95105
}
96106

107+
const parentSpan = getActiveSpan();
108+
109+
const attributes = buildSpanAttributes(name, options.attributes as Record<string, unknown> | undefined);
110+
const spanOptions = {
111+
name: buildSpanName(name, attributes),
112+
attributes,
113+
kind: options.kind as SpanKindValue | undefined,
114+
links: options.links as SpanLink[] | undefined,
115+
startTime: options.startTime,
116+
parentSpan,
117+
};
118+
97119
if (options.active === false) {
98-
const span = tracer.startSpan(name, options, context);
99-
return endSpan(span, callback(span, context));
120+
const span = startInactiveSpan(spanOptions);
121+
return endSpan(span, () => callback(span, parentSpan));
100122
}
101123

102-
return tracer.startActiveSpan(name, options, span => endSpan(span, callback(span, context)));
124+
return startSpanManual(spanOptions, span => endSpan(span, () => callback(span, parentSpan)));
103125
}
104126
}
105127

106128
function dispatchEngineSpan(
107-
tracer: Tracer,
108129
engineSpan: EngineSpan,
109130
allSpans: EngineSpan[],
110131
linkIds: Map<string, string>,
111132
ignoreSpanTypes: (string | RegExp)[],
112-
) {
113-
if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) return;
114-
115-
const spanOptions = {
116-
attributes: engineSpan.attributes as Attributes,
117-
kind: engineSpanKindToOtelSpanKind(engineSpan.kind),
118-
startTime: engineSpan.startTime,
119-
} satisfies SpanOptions;
120-
121-
tracer.startActiveSpan(engineSpan.name, spanOptions, span => {
122-
linkIds.set(engineSpan.id, span.spanContext().spanId);
123-
124-
if (engineSpan.links) {
125-
span.addLinks(
126-
engineSpan.links.flatMap(link => {
127-
const linkedId = linkIds.get(link);
128-
if (!linkedId) {
129-
return [];
130-
}
131-
return {
132-
context: {
133-
spanId: linkedId,
134-
traceId: span.spanContext().traceId,
135-
traceFlags: span.spanContext().traceFlags,
136-
},
137-
};
138-
}),
139-
);
140-
}
141-
142-
const children = allSpans.filter(s => s.parentId === engineSpan.id);
143-
for (const child of children) {
144-
dispatchEngineSpan(tracer, child, allSpans, linkIds, ignoreSpanTypes);
145-
}
133+
): void {
134+
if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) {
135+
return;
136+
}
146137

147-
span.end(engineSpan.endTime);
148-
});
138+
const attributes = buildSpanAttributes(engineSpan.name, engineSpan.attributes);
139+
140+
startSpanManual(
141+
{
142+
name: buildSpanName(engineSpan.name, attributes),
143+
attributes,
144+
kind: engineSpan.kind === 'client' ? SPAN_KIND.CLIENT : SPAN_KIND.INTERNAL,
145+
startTime: engineSpan.startTime,
146+
},
147+
span => {
148+
linkIds.set(engineSpan.id, span.spanContext().spanId);
149+
150+
if (engineSpan.links) {
151+
span.addLinks(
152+
engineSpan.links.flatMap(link => {
153+
const linkedId = linkIds.get(link);
154+
if (!linkedId) {
155+
return [];
156+
}
157+
return {
158+
context: {
159+
spanId: linkedId,
160+
traceId: span.spanContext().traceId,
161+
traceFlags: span.spanContext().traceFlags,
162+
},
163+
};
164+
}),
165+
);
166+
}
167+
168+
const children = allSpans.filter(s => s.parentId === engineSpan.id);
169+
for (const child of children) {
170+
dispatchEngineSpan(child, allSpans, linkIds, ignoreSpanTypes);
171+
}
172+
173+
span.end(engineSpan.endTime);
174+
},
175+
);
149176
}
150177

151-
function endSpan<T>(span: Span, result: T): T {
178+
function endSpan<T>(span: Span, run: () => T): T {
179+
let result: T;
180+
try {
181+
result = run();
182+
} catch (reason) {
183+
span.end();
184+
throw reason;
185+
}
186+
152187
if (isPromiseLike(result)) {
153188
return result.then(
154189
value => {

0 commit comments

Comments
 (0)