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 @@ -25,6 +25,22 @@ describe('Prisma ORM v6 Tests', () => {
const spans = transaction.spans || [];
expect(spans.length).toBeGreaterThanOrEqual(5);

// Each operation span is a direct child of the transaction; the db query span is a child of the engine query span.
const rootSpanId = transaction.contexts?.trace?.span_id;

const operationSpans = spans.filter(s => s.description === 'prisma:client:operation');
expect(operationSpans.length).toBeGreaterThanOrEqual(1);
operationSpans.forEach(operation => {
expect(operation.parent_span_id).toBe(rootSpanId);
});

const dbQuerySpan = spans.find(
s => s.data?.['sentry.origin'] === 'auto.db.otel.prisma' && s.data?.['db.query.text'],
);
expect(dbQuerySpan).toBeDefined();
const dbQueryParent = spans.find(s => s.span_id === dbQuerySpan?.parent_span_id);
expect(dbQueryParent?.description).toBe('prisma:engine:query');

function expectPrismaSpanToIncludeSpanWith(span: Partial<SpanJSON>) {
expect(spans).toContainEqual(
expect.objectContaining({
Expand Down Expand Up @@ -92,6 +108,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 @@ -26,6 +26,22 @@ conditionalTest({ min: 20 })('Prisma ORM v7 Tests', () => {
const spans = transaction.spans || [];
expect(spans.length).toBeGreaterThanOrEqual(5);

// Each operation span is a direct child of the transaction; the db query span is a child of its operation span.
const rootSpanId = transaction.contexts?.trace?.span_id;

const operationSpans = spans.filter(s => s.description === 'prisma:client:operation');
expect(operationSpans.length).toBeGreaterThanOrEqual(1);
operationSpans.forEach(operation => {
expect(operation.parent_span_id).toBe(rootSpanId);
});

const prismaDbQuerySpan = spans.find(
s => s.data?.['sentry.origin'] === 'auto.db.otel.prisma' && s.data?.['db.query.text'],
);
expect(prismaDbQuerySpan).toBeDefined();
const dbQueryParent = spans.find(s => s.span_id === prismaDbQuerySpan?.parent_span_id);
expect(dbQueryParent?.description).toBe('prisma:client:operation');

// Verify Prisma spans have the correct origin
const prismaSpans = spans.filter(
span => span.data && span.data['sentry.origin'] === 'auto.db.otel.prisma',
Expand Down Expand Up @@ -58,6 +74,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
37 changes: 6 additions & 31 deletions packages/node/src/integrations/tracing/prisma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class SentryPrismaInteropInstrumentation extends PrismaInstrumentation {

try {
engineSpanEvent.spans.forEach(engineSpan => {
const kind = engineSpanKindToOTELSpanKind(engineSpan.kind);
const kind = engineSpan.kind === 'client' ? SpanKind.CLIENT : SpanKind.INTERNAL;

const parentSpanId = engineSpan.parent_span_id;
const spanId = engineSpan.span_id;
Expand Down Expand Up @@ -159,16 +159,6 @@ class SentryPrismaInteropInstrumentation extends PrismaInstrumentation {
}
}

function engineSpanKindToOTELSpanKind(engineSpanKind: V5EngineSpanKind): SpanKind {
switch (engineSpanKind) {
case 'client':
return SpanKind.CLIENT;
case 'internal':
default: // Other span kinds aren't currently supported
return SpanKind.INTERNAL;
}
}

export const instrumentPrisma = generateInstrumentOnce<PrismaOptions>(INTEGRATION_NAME, options => {
return new SentryPrismaInteropInstrumentation(options);
});
Expand All @@ -177,26 +167,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 +190,9 @@ export const prismaIntegration = defineIntegration((options?: PrismaOptions) =>
return;
}

// Prisma v5 engine spans are created via the `createEngineSpan` path above, which bypasses the
// tracing helper, so this hook applies origin, the db_query rename, and the db.system backfill to
// them. v6/v7 spans already get these from the helper; the guards are idempotent, so it's a no-op there.
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,149 +6,184 @@
* - 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 uses Sentry's span APIs (`startSpanManual` / `startInactiveSpan`) instead of the OTel tracer
* - Span creation sets the Sentry origin, renames `db_query` spans to their SQL text, and backfills
* `db.system` for older Prisma versions
*/
/* eslint-disable */

import type { Span, SpanAttributes, SpanKindValue, SpanLink } from '@sentry/core';
import {
Attributes,
Context,
context as _context,
Span,
SpanKind,
SpanOptions,
trace,
Tracer,
TracerProvider,
} from '@opentelemetry/api';
import type { EngineSpan, EngineSpanKind, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types';
getActiveSpan,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_KIND,
startInactiveSpan,
startSpanManual,
} from '@sentry/core';
import type { EngineSpan, 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 {
switch (engineSpanKind) {
case 'client':
return SpanKind.CLIENT;
case 'internal':
default:
return SpanKind.INTERNAL;
/**
* Older Prisma versions emit `prisma:engine:db_query` spans without a `db.system`, so it's backfilled here.
*/
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;
}

/**
* Db query spans are named after their SQL text (e.g. `SELECT * FROM "User"`) rather than the generic
* engine name. 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(span?: Span): string {
const spanContext = (span ?? 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 {
return _context.active();
Comment thread
nicohrubec marked this conversation as resolved.
public getActiveContext(): Span | undefined {
return getActiveSpan();
}
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 parentSpan = getActiveSpan();

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,
parentSpan,
};

if (options.active === false) {
const span = tracer.startSpan(name, options, context);
return endSpan(span, callback(span, context));
const span = startInactiveSpan(spanOptions);
return endSpan(span, () => callback(span, parentSpan));
Comment thread
nicohrubec marked this conversation as resolved.
}

return tracer.startActiveSpan(name, options, span => endSpan(span, callback(span, context)));
return startSpanManual(spanOptions, span => endSpan(span, () => callback(span, parentSpan)));
}
}

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: engineSpan.kind === 'client' ? SPAN_KIND.CLIENT : SPAN_KIND.INTERNAL,
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 {
function endSpan<T>(span: Span, run: () => T): T {
let result: T;
try {
result = run();
} catch (reason) {
span.end();
throw reason;
}

if (isPromiseLike(result)) {
return result.then(
value => {
Expand Down
Loading
Loading