diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts index b804adb10f71..cf0f6348dfcb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts @@ -92,6 +92,10 @@ describe('Prisma ORM v6 Tests', () => { }, description: 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1', }); + + // The db query span name must always be rewritten to the SQL text; the raw engine span + // name should never leak through. + expect(spans.find(span => span.description === 'prisma:engine:db_query')).toBeUndefined(); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts index e48feac3c793..ce37607841af 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts @@ -58,6 +58,10 @@ conditionalTest({ min: 20 })('Prisma ORM v7 Tests', () => { expect(dbQuerySpan?.op).toBe('db'); expect(dbQuerySpan?.description).toBe(dbQuerySpan?.data?.['db.query.text']); expect(dbQuerySpan?.description).not.toBe('prisma:client:db_query'); + + // The db query span name must always be rewritten to the SQL text; the raw client span + // name should never leak through. + expect(spans.find(span => span.description === 'prisma:client:db_query')).toBeUndefined(); }, }) .start() diff --git a/packages/node/src/integrations/tracing/prisma/index.ts b/packages/node/src/integrations/tracing/prisma/index.ts index 871ffab7b56a..ad9c6b666ed4 100644 --- a/packages/node/src/integrations/tracing/prisma/index.ts +++ b/packages/node/src/integrations/tracing/prisma/index.ts @@ -177,26 +177,8 @@ export const instrumentPrisma = generateInstrumentOnce(INTEGRATIO * Adds Sentry tracing instrumentation for the [prisma](https://www.npmjs.com/package/prisma) library. * For more information, see the [`prismaIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/). * - * NOTE: By default, this integration works with Prisma version 6. - * To get performance instrumentation for other Prisma versions, - * 1. Install the `@prisma/instrumentation` package with the desired version. - * 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option of this integration: - * - * ```js - * import { PrismaInstrumentation } from '@prisma/instrumentation' - * - * Sentry.init({ - * integrations: [ - * prismaIntegration({ - * // Override the default instrumentation that Sentry uses - * prismaInstrumentation: new PrismaInstrumentation() - * }) - * ] - * }) - * ``` - * - * 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. - * 1. Depending on your Prisma version (prior to version 6), add `previewFeatures = ["tracing"]` to the client generator block of your Prisma schema: + * NOTE: This integration works out of the box with Prisma v6, and v7. + * On Prisma versions prior to v6, add `previewFeatures = ["tracing"]` to the client generator block of your Prisma schema: * * ``` * generator client { @@ -218,6 +200,10 @@ export const prismaIntegration = defineIntegration((options?: PrismaOptions) => return; } + // The v6/v7 tracing helper folds origin, the db_query span rename, and the db.system backfill + // directly into span creation (see vendored/active-tracing-helper.ts). This hook backstops the + // Prisma v5 engine spans created via the `createEngineSpan` compatibility path above, which + // bypass the helper. The guards below are idempotent, so it's a no-op for helper-created spans. client.on('spanStart', span => { const spanJSON = spanToJSON(span); if (spanJSON.description?.startsWith('prisma:')) { diff --git a/packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts index e4ef9b17e716..999ed5537701 100644 --- a/packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts +++ b/packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts @@ -6,146 +6,188 @@ * - Vendored from: https://github.com/prisma/prisma/tree/b6feea5565ec577545a79547d24273ccdd11b4c7/packages/instrumentation * - Upstream version: @prisma/instrumentation@7.8.0 * - Replaced `@prisma/instrumentation-contract` imports with local vendored types - * - Minor TypeScript strictness adjustments for this repository's compiler settings + * - Span creation was migrated from the OTel tracer to Sentry's span APIs (`startSpanManual` / + * `startInactiveSpan`) + * - The former `index.ts` `spanStart` hook is folded into span creation: the Sentry origin, the + * `db_query` -> query-text span rename, and the `db.system` backfill for older Prisma versions are + * applied where the spans are started instead of via a client hook */ -/* eslint-disable */ +import type { Context } from '@opentelemetry/api'; +import { context as _context, trace } from '@opentelemetry/api'; +import type { Span, SpanAttributes, SpanKindValue, SpanLink } from '@sentry/core'; import { - Attributes, - Context, - context as _context, - Span, - SpanKind, - SpanOptions, - trace, - Tracer, - TracerProvider, -} from '@opentelemetry/api'; + getActiveSpan, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_KIND, + startInactiveSpan, + startSpanManual, +} from '@sentry/core'; import type { EngineSpan, EngineSpanKind, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; const showAllTraces = process.env.PRISMA_SHOW_ALL_TRACES === 'true'; const nonSampledTraceParent = `00-10-10-00`; +const PRISMA_ORIGIN = 'auto.db.otel.prisma'; + type Options = { - tracerProvider: TracerProvider; ignoreSpanTypes: (string | RegExp)[]; }; -function engineSpanKindToOtelSpanKind(engineSpanKind: EngineSpanKind): SpanKind { +function engineSpanKindToSentrySpanKind(engineSpanKind: EngineSpanKind): SpanKindValue { switch (engineSpanKind) { case 'client': - return SpanKind.CLIENT; + return SPAN_KIND.CLIENT; case 'internal': default: - return SpanKind.INTERNAL; + return SPAN_KIND.INTERNAL; } } +/** + * Folds the former `index.ts` `spanStart` hook into span creation: tags the Sentry origin and + * backfills `db.system` for older Prisma versions that emit `prisma:engine:db_query` without it. + */ +function buildSpanAttributes(name: string, attributes: Record | undefined): SpanAttributes { + const merged: SpanAttributes = { + ...(attributes as SpanAttributes | undefined), + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, + }; + + if (name === 'prisma:engine:db_query' && merged['db.system'] == null) { + merged['db.system'] = 'prisma'; + } + + return merged; +} + +/** + * Uses the query text as the span name for db query spans (e.g. `SELECT * FROM "User"`), matching the + * behavior the SDK previously applied via the `spanStart` hook. v5/v6 emit `prisma:engine:db_query`; + * v7 inlined the engine and emits `prisma:client:db_query`. + */ +function buildSpanName(name: string, attributes: SpanAttributes): string { + const queryText = attributes['db.query.text']; + if ((name === 'prisma:engine:db_query' || name === 'prisma:client:db_query') && typeof queryText === 'string') { + return queryText; + } + return name; +} + export class ActiveTracingHelper implements TracingHelper { - private tracerProvider: TracerProvider; private ignoreSpanTypes: (string | RegExp)[]; - constructor({ tracerProvider, ignoreSpanTypes }: Options) { - this.tracerProvider = tracerProvider; + public constructor({ ignoreSpanTypes }: Options) { this.ignoreSpanTypes = ignoreSpanTypes; } - isEnabled(): boolean { + public isEnabled(): boolean { return true; } - getTraceParent(context?: Context | undefined): string { - const span = trace.getSpanContext(context ?? _context.active()); - if (span) { - return `00-${span.traceId}-${span.spanId}-0${span.traceFlags}`; + public getTraceParent(context?: Context): string { + const spanContext = context ? trace.getSpanContext(context) : getActiveSpan()?.spanContext(); + if (spanContext) { + return `00-${spanContext.traceId}-${spanContext.spanId}-0${spanContext.traceFlags}`; } return nonSampledTraceParent; } - dispatchEngineSpans(spans: EngineSpan[]): void { - const tracer = this.tracerProvider.getTracer('prisma'); + public dispatchEngineSpans(spans: EngineSpan[]): void { const linkIds = new Map(); const roots = spans.filter(span => span.parentId === null); for (const root of roots) { - dispatchEngineSpan(tracer, root, spans, linkIds, this.ignoreSpanTypes); + dispatchEngineSpan(root, spans, linkIds, this.ignoreSpanTypes); } } - getActiveContext(): Context | undefined { + public getActiveContext(): Context | undefined { return _context.active(); } - runInChildSpan(options: string | ExtendedSpanOptions, callback: SpanCallback): R { - if (typeof options === 'string') { - options = { name: options }; - } + public runInChildSpan(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback): R { + const options: ExtendedSpanOptions = typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions; if (options.internal && !showAllTraces) { return callback(); } - const tracer = this.tracerProvider.getTracer('prisma'); - const context = options.context ?? this.getActiveContext(); const name = `prisma:client:${options.name}`; if (shouldIgnoreSpan(name, this.ignoreSpanTypes)) { return callback(); } + const context = options.context ?? _context.active(); + + const attributes = buildSpanAttributes(name, options.attributes as Record | undefined); + const spanOptions = { + name: buildSpanName(name, attributes), + attributes, + kind: options.kind as SpanKindValue | undefined, + links: options.links as SpanLink[] | undefined, + startTime: options.startTime, + }; + if (options.active === false) { - const span = tracer.startSpan(name, options, context); + const span = _context.with(context, () => startInactiveSpan(spanOptions)); return endSpan(span, callback(span, context)); } - return tracer.startActiveSpan(name, options, span => endSpan(span, callback(span, context))); + return _context.with(context, () => startSpanManual(spanOptions, span => endSpan(span, callback(span, context)))); } } function dispatchEngineSpan( - tracer: Tracer, engineSpan: EngineSpan, allSpans: EngineSpan[], linkIds: Map, ignoreSpanTypes: (string | RegExp)[], -) { - if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) return; - - const spanOptions = { - attributes: engineSpan.attributes as Attributes, - kind: engineSpanKindToOtelSpanKind(engineSpan.kind), - startTime: engineSpan.startTime, - } satisfies SpanOptions; - - tracer.startActiveSpan(engineSpan.name, spanOptions, span => { - linkIds.set(engineSpan.id, span.spanContext().spanId); - - if (engineSpan.links) { - span.addLinks( - engineSpan.links.flatMap(link => { - const linkedId = linkIds.get(link); - if (!linkedId) { - return []; - } - return { - context: { - spanId: linkedId, - traceId: span.spanContext().traceId, - traceFlags: span.spanContext().traceFlags, - }, - }; - }), - ); - } - - const children = allSpans.filter(s => s.parentId === engineSpan.id); - for (const child of children) { - dispatchEngineSpan(tracer, child, allSpans, linkIds, ignoreSpanTypes); - } +): void { + if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) { + return; + } - span.end(engineSpan.endTime); - }); + const attributes = buildSpanAttributes(engineSpan.name, engineSpan.attributes); + + startSpanManual( + { + name: buildSpanName(engineSpan.name, attributes), + attributes, + kind: engineSpanKindToSentrySpanKind(engineSpan.kind), + startTime: engineSpan.startTime, + }, + span => { + linkIds.set(engineSpan.id, span.spanContext().spanId); + + if (engineSpan.links) { + span.addLinks( + engineSpan.links.flatMap(link => { + const linkedId = linkIds.get(link); + if (!linkedId) { + return []; + } + return { + context: { + spanId: linkedId, + traceId: span.spanContext().traceId, + traceFlags: span.spanContext().traceFlags, + }, + }; + }), + ); + } + + const children = allSpans.filter(s => s.parentId === engineSpan.id); + for (const child of children) { + dispatchEngineSpan(child, allSpans, linkIds, ignoreSpanTypes); + } + + span.end(engineSpan.endTime); + }, + ); } function endSpan(span: Span, result: T): T { diff --git a/packages/node/src/integrations/tracing/prisma/vendored/constants.ts b/packages/node/src/integrations/tracing/prisma/vendored/constants.ts index 6e9421eb3c5e..6b9b70a0a02f 100644 --- a/packages/node/src/integrations/tracing/prisma/vendored/constants.ts +++ b/packages/node/src/integrations/tracing/prisma/vendored/constants.ts @@ -7,7 +7,6 @@ * - Upstream version: @prisma/instrumentation@7.8.0 * - Replaced `import packageJson from '../package.json'` with hardcoded values */ -/* eslint-disable */ import { SDK_VERSION } from '@sentry/core'; diff --git a/packages/node/src/integrations/tracing/prisma/vendored/global.ts b/packages/node/src/integrations/tracing/prisma/vendored/global.ts index 6a6e24e56782..403dbbf31445 100644 --- a/packages/node/src/integrations/tracing/prisma/vendored/global.ts +++ b/packages/node/src/integrations/tracing/prisma/vendored/global.ts @@ -7,7 +7,6 @@ * - Upstream version: @prisma/instrumentation-contract@7.8.0 * - Replaced `import packageJson from '../package.json'` with hardcoded major version */ -/* eslint-disable */ import type { PrismaInstrumentationGlobalValue, TracingHelper } from './types'; @@ -44,6 +43,6 @@ export function setGlobalTracingHelper(helper: TracingHelper): void { } export function clearGlobalTracingHelper(): void { - delete globalThisWithPrismaInstrumentation[GLOBAL_VERSIONED_INSTRUMENTATION_KEY]; - delete globalThisWithPrismaInstrumentation[GLOBAL_INSTRUMENTATION_KEY]; + globalThisWithPrismaInstrumentation[GLOBAL_VERSIONED_INSTRUMENTATION_KEY] = undefined; + globalThisWithPrismaInstrumentation[GLOBAL_INSTRUMENTATION_KEY] = undefined; } diff --git a/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts index d0bccba7a304..d7d8e6de8d5d 100644 --- a/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts +++ b/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts @@ -7,19 +7,15 @@ * - Upstream version: @prisma/instrumentation@7.8.0 * - Replaced `@prisma/instrumentation-contract` imports with local vendored equivalents * - Replaced `import { VERSION, NAME, MODULE_NAME } from './constants'` with local vendored constants + * - Dropped the unused `setTracerProvider`/`tracerProvider` plumbing; the tracing helper creates spans + * through Sentry's span APIs, which resolve the active client themselves */ -/* eslint-disable */ - -import { trace, TracerProvider } from '@opentelemetry/api'; -import { - InstrumentationBase, - InstrumentationConfig, - InstrumentationNodeModuleDefinition, -} from '@opentelemetry/instrumentation'; -import { clearGlobalTracingHelper, getGlobalTracingHelper, setGlobalTracingHelper } from './global'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import { ActiveTracingHelper } from './active-tracing-helper'; import { MODULE_NAME, NAME, SUPPORTED_MODULE_VERSIONS, VERSION } from './constants'; +import { clearGlobalTracingHelper, getGlobalTracingHelper, setGlobalTracingHelper } from './global'; export interface PrismaInstrumentationConfig { ignoreSpanTypes?: (string | RegExp)[]; @@ -28,38 +24,31 @@ export interface PrismaInstrumentationConfig { type Config = PrismaInstrumentationConfig & InstrumentationConfig; export class PrismaInstrumentation extends InstrumentationBase { - private tracerProvider: TracerProvider | undefined; - - constructor(config: Config = {}) { + public constructor(config: Config = {}) { super(NAME, VERSION, config); } - setTracerProvider(tracerProvider: TracerProvider): void { - this.tracerProvider = tracerProvider; - } - - init() { + public init(): InstrumentationNodeModuleDefinition[] { const module = new InstrumentationNodeModuleDefinition(MODULE_NAME, SUPPORTED_MODULE_VERSIONS); return [module]; } - enable() { + public enable(): void { const config = this._config as Config; setGlobalTracingHelper( new ActiveTracingHelper({ - tracerProvider: this.tracerProvider ?? trace.getTracerProvider(), ignoreSpanTypes: config.ignoreSpanTypes ?? [], }), ); } - disable() { + public disable(): void { clearGlobalTracingHelper(); } - isEnabled() { + public isEnabled(): boolean { return getGlobalTracingHelper() !== undefined; } } diff --git a/packages/node/src/integrations/tracing/prisma/vendored/types.ts b/packages/node/src/integrations/tracing/prisma/vendored/types.ts index d6c5e22ec907..91f112b38946 100644 --- a/packages/node/src/integrations/tracing/prisma/vendored/types.ts +++ b/packages/node/src/integrations/tracing/prisma/vendored/types.ts @@ -5,8 +5,9 @@ * NOTICE from the Sentry authors: * - Vendored from: https://github.com/prisma/prisma/tree/b6feea5565ec577545a79547d24273ccdd11b4c7/packages/instrumentation-contract * - Upstream version: @prisma/instrumentation-contract@7.8.0 + * - Trimmed to the members the SDK's tracing helper relies on (dropped the unused `EngineTrace`, + * `EngineTraceEvent`, and `LogLevel` types) */ -/* eslint-disable */ import type { Context, Span, SpanOptions } from '@opentelemetry/api'; @@ -40,33 +41,11 @@ export type EngineSpan = { links?: EngineSpanId[]; }; -export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'query'; - -export interface EngineTraceEvent { - spanId: EngineSpanId; - target?: string; - level: LogLevel; - timestamp: HrTime; - attributes: Record & { - message?: string; - query?: string; - duration_ms?: number; - params?: string; - }; -} - -export interface EngineTrace { - spans: EngineSpan[]; - events: EngineTraceEvent[]; -} - export interface TracingHelper { isEnabled(): boolean; getTraceParent(context?: Context): string; dispatchEngineSpans(spans: EngineSpan[]): void; - getActiveContext(): Context | undefined; - runInChildSpan(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback): R; }