Skip to content
Open
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
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
26 changes: 6 additions & 20 deletions packages/node/src/integrations/tracing/prisma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,26 +177,8 @@ export const instrumentPrisma = generateInstrumentOnce<PrismaOptions>(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 {
Expand All @@ -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:')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

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: we have a SPAN_KIND type in core now that we can use here

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<string, unknown> | 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<string, string>();
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<R>(options: string | ExtendedSpanOptions, callback: SpanCallback<R>): R {
if (typeof options === 'string') {
options = { name: options };
}
public runInChildSpan<R>(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback<R>): 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<string, unknown> | 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))));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: A synchronous error in the callback for runInChildSpan will cause a span leak because endSpan is never called.
Severity: LOW

Suggested Fix

Wrap the callback(span, context) call in a try/catch block within the endSpan function or restructure the code to ensure span.end() is always called in a finally block, regardless of whether the callback succeeds or throws an error.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts#L139

Potential issue: In `runInChildSpan`, the `startSpanManual` function is used in a way
that can lead to a resource leak. If the `callback` function passed into it throws a
synchronous error, the `endSpan` function is never called. Because `startSpanManual` is
configured with `autoEnd = false`, its internal error handling will correctly mark the
span with an error status but will not end it. This results in an unclosed span, which
can lead to increased memory and resource consumption over time.

Did we get this right? 👍 / 👎 to inform future reviews.

}
}

function dispatchEngineSpan(
tracer: Tracer,
engineSpan: EngineSpan,
allSpans: EngineSpan[],
linkIds: Map<string, string>,
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<T>(span: Span, result: T): T {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
Loading
Loading