Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 {
Comment thread
nicohrubec marked this conversation as resolved.
Outdated
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();
Comment thread
nicohrubec marked this conversation as resolved.
}
Comment on lines +90 to 92

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.

This seems weird to me 😬 but I can't tell if it's ok or not. We'd need to investigate when exactly getActiveContext was called before and if it's fine receiving a span now?

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.

from what I can tell this should be fine, the output of getActiveContext is only used by getTraceParent which we also control


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))));
Comment thread
nicohrubec marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}
}

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;
Comment thread
andreiborza marked this conversation as resolved.
}
Loading
Loading