From 9342433693a50fbeace3b05f9c03dc59204f8b61 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 13 Jul 2026 13:36:37 +0200 Subject: [PATCH 1/2] ref(server-utils): Move prisma instrumentation to server-utils --- packages/node/src/index.ts | 2 +- .../node/src/integrations/tracing/index.ts | 2 +- .../src/integrations/tracing/prisma/index.ts | 236 ------------ .../prisma/vendored/active-tracing-helper.ts | 209 ----------- .../tracing/prisma/vendored/constants.ts | 19 - .../prisma/vendored/instrumentation.ts | 54 --- .../prisma/vendored/v5-tracing-helper.ts | 41 --- .../prisma/vendored/v6-tracing-helper.ts | 38 -- .../test/integrations/tracing/prisma.test.ts | 38 -- packages/server-utils/src/index.ts | 2 + .../src/prisma}/global.ts | 0 packages/server-utils/src/prisma/index.ts | 67 ++++ .../server-utils/src/prisma/tracing-helper.ts | 336 ++++++++++++++++++ .../src/prisma}/types.ts | 11 +- packages/server-utils/test/prisma.test.ts | 48 +++ 15 files changed, 463 insertions(+), 640 deletions(-) delete mode 100644 packages/node/src/integrations/tracing/prisma/index.ts delete mode 100644 packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts delete mode 100644 packages/node/src/integrations/tracing/prisma/vendored/constants.ts delete mode 100644 packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/prisma/vendored/v5-tracing-helper.ts delete mode 100644 packages/node/src/integrations/tracing/prisma/vendored/v6-tracing-helper.ts delete mode 100644 packages/node/test/integrations/tracing/prisma.test.ts rename packages/{node/src/integrations/tracing/prisma/vendored => server-utils/src/prisma}/global.ts (100%) create mode 100644 packages/server-utils/src/prisma/index.ts create mode 100644 packages/server-utils/src/prisma/tracing-helper.ts rename packages/{node/src/integrations/tracing/prisma/vendored => server-utils/src/prisma}/types.ts (78%) create mode 100644 packages/server-utils/test/prisma.test.ts diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 907adfa6b012..61c80f44794f 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -13,7 +13,7 @@ export { mysql2Integration } from './integrations/tracing/mysql2'; export { redisIntegration } from './integrations/tracing/redis'; export { postgresIntegration } from './integrations/tracing/postgres'; export { postgresJsIntegration } from './integrations/tracing/postgresjs'; -export { prismaIntegration } from './integrations/tracing/prisma'; +export { prismaIntegration } from '@sentry/server-utils'; export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; // eslint-disable-next-line typescript/no-deprecated export { honoIntegration, setupHonoErrorHandler } from './integrations/tracing/hono'; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index ec891e51d86d..32857f2049ee 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -1,4 +1,5 @@ import type { Integration } from '@sentry/core'; +import { prismaIntegration } from '@sentry/server-utils'; import { instrumentSentryHttp } from '../http'; import { amqplibIntegration, instrumentAmqplib } from './amqplib'; import { anthropicAIIntegration, instrumentAnthropicAi } from './anthropic-ai'; @@ -23,7 +24,6 @@ import { instrumentMysql2, mysql2Integration } from './mysql2'; import { instrumentOpenAi, openAIIntegration } from './openai'; import { instrumentPostgres, postgresIntegration } from './postgres'; import { instrumentPostgresJs, postgresJsIntegration } from './postgresjs'; -import { prismaIntegration } from './prisma'; import { instrumentRedis, redisIntegration } from './redis'; import { instrumentTedious, tediousIntegration } from './tedious'; import { instrumentVercelAi, vercelAIIntegration } from './vercelai'; diff --git a/packages/node/src/integrations/tracing/prisma/index.ts b/packages/node/src/integrations/tracing/prisma/index.ts deleted file mode 100644 index 9b28af687df3..000000000000 --- a/packages/node/src/integrations/tracing/prisma/index.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type { Instrumentation } from '@opentelemetry/instrumentation'; -import type { Span } from '@sentry/core'; -import { - debug, - defineIntegration, - LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_KIND, - spanToJSON, - startInactiveSpan, -} from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node-core'; -import { DEBUG_BUILD } from '../../../debug-build'; -import { PrismaInstrumentation } from './vendored/instrumentation'; -import type { PrismaV5TracingHelper } from './vendored/v5-tracing-helper'; -import type { PrismaV6TracingHelper } from './vendored/v6-tracing-helper'; - -const INTEGRATION_NAME = 'Prisma' as const; - -type CompatibilityLayerTraceHelper = PrismaV5TracingHelper & PrismaV6TracingHelper; - -// Vendored in from @prisma/instrumentation v5: -type V5EngineSpanEvent = { - span: boolean; - spans: V5EngineSpan[]; -}; - -type V5EngineSpanKind = 'client' | 'internal'; - -type V5EngineSpan = { - span: boolean; - name: string; - trace_id: string; - span_id: string; - parent_span_id: string; - start_time: [number, number]; - end_time: [number, number]; - attributes?: Record; - links?: { trace_id: string; span_id: string }[]; - kind: V5EngineSpanKind; -}; - -function isPrismaV6TracingHelper(helper: unknown): helper is PrismaV6TracingHelper { - return !!helper && typeof helper === 'object' && 'dispatchEngineSpans' in helper; -} - -function getPrismaTracingHelper(): unknown | undefined { - const prismaInstrumentationObject = (globalThis as Record).PRISMA_INSTRUMENTATION; - const prismaTracingHelper = - prismaInstrumentationObject && - typeof prismaInstrumentationObject === 'object' && - 'helper' in prismaInstrumentationObject - ? prismaInstrumentationObject.helper - : undefined; - - return prismaTracingHelper; -} - -// Prisma v5 dispatches engine spans (`prisma:engine:*`, which carry the SQL `db.statement`) one batch -// at a time, out of order, and detached from the active span, naming their parent only by id: a span -// references either a client span (by the Sentry span id Prisma learned via `getTraceParent`) or a -// sibling engine span (by the engine's own id), and a batch may not contain the parent it references. -// By the time the engine reports them the client span may already have ended, so `getActiveSpan()` -// can't be used to find it. `prismaSpanRegistry` maps span ids to their Sentry span, and an engine -// span whose parent isn't registered yet waits in `pendingEngineSpans` until a later batch registers it. -const MAX_TRACKED_PRISMA_SPANS = 1000; -const prismaSpanRegistry = new LRUMap(MAX_TRACKED_PRISMA_SPANS); -const pendingEngineSpans: V5EngineSpan[] = []; - -/** Register a span so v5 engine spans can later resolve it as a parent by the id Prisma reports it under. */ -function registerPrismaSpan(id: string, span: Span): void { - prismaSpanRegistry.set(id, span); -} - -/** - * Create every pending v5 engine span whose parent is now registered, repeating until no further span - * resolves (so a child queued before its parent is created once the parent arrives in a later batch). - * Each span is created under its resolved parent and registered by its engine id so its own children - * can find it; origin, the `prisma:engine:db_query` to SQL rename, and the `db.system` backfill are - * applied by the `spanStart` hook, exactly as for v6/v7 engine spans. - */ -function createResolvedEngineSpans(): void { - // Terminates: `createdSpan` is only set true right after a `splice`, so every pass that keeps the - // loop going strictly shrinks the finite `pendingEngineSpans`; a pass that resolves nothing exits. - let createdSpan = true; - while (createdSpan) { - createdSpan = false; - for (let i = pendingEngineSpans.length - 1; i >= 0; i--) { - const engineSpan = pendingEngineSpans[i]!; - const parentSpan = prismaSpanRegistry.get(engineSpan.parent_span_id); - if (!parentSpan) { - continue; - } - - const span = startInactiveSpan({ - name: engineSpan.name, - attributes: engineSpan.attributes, - kind: engineSpan.kind === 'client' ? SPAN_KIND.CLIENT : SPAN_KIND.INTERNAL, - startTime: engineSpan.start_time, - parentSpan, - }); - registerPrismaSpan(engineSpan.span_id, span); - - // Engine links reference other engine spans by their engine id; re-point them at the Sentry spans - // we minted (mirroring v6/v7's `dispatchEngineSpan`). Links must be added before the span ends, - // since the SentryTracerProvider seals spans on end. Links to spans we haven't created are dropped. - if (engineSpan.links) { - span.addLinks( - engineSpan.links.flatMap(link => { - const linkedSpan = prismaSpanRegistry.get(link.span_id); - return linkedSpan ? [{ context: linkedSpan.spanContext() }] : []; - }), - ); - } - - span.end(engineSpan.end_time); - - pendingEngineSpans.splice(i, 1); - createdSpan = true; - } - } -} - -interface PrismaOptions { - /** - * @deprecated This is no longer used, v5 works out of the box. - */ - prismaInstrumentation?: Instrumentation; - /** - * Configuration passed through to the {@link PrismaInstrumentation} constructor. - */ - instrumentationConfig?: ConstructorParameters[0]; -} - -class SentryPrismaInteropInstrumentation extends PrismaInstrumentation { - public constructor(options?: PrismaOptions) { - super(options?.instrumentationConfig); - } - - public enable(): void { - super.enable(); - - // The PrismaIntegration (super class) defines a global variable `global["PRISMA_INSTRUMENTATION"]` when `enable()` is called. This global variable holds a "TracingHelper" which Prisma uses internally to create tracing data. It's their way of not depending on OTEL with their main package. The sucky thing is, prisma broke the interface of the tracing helper with the v6 major update. This means that if you use Prisma 5 with the v6 instrumentation (or vice versa) Prisma just blows up, because tries to call methods on the helper that no longer exist. - // Because we actually want to use the v6/v7 instrumentation and not blow up in Prisma 5 users' faces, we backfill the v5-only `createEngineSpan` method so v5 engine spans are minted through Sentry's span APIs instead of crashing. - const prismaTracingHelper = getPrismaTracingHelper(); - - if (isPrismaV6TracingHelper(prismaTracingHelper)) { - (prismaTracingHelper as CompatibilityLayerTraceHelper).createEngineSpan = ( - engineSpanEvent: V5EngineSpanEvent, - ) => { - pendingEngineSpans.push(...engineSpanEvent.spans); - - // Resolve before capping so a span is only ever dropped after a full resolution attempt, never - // before it had a chance to find its parent. What remains are orphans whose parent was never - // registered (e.g. evicted from the registry under sustained load); cap that backlog so it can't - // grow without bound. - createResolvedEngineSpans(); - - const overflow = pendingEngineSpans.length - MAX_TRACKED_PRISMA_SPANS; - if (overflow > 0) { - DEBUG_BUILD && - debug.log(`[Prisma] Dropping ${overflow} unresolved v5 engine span(s) whose parent was never registered.`); - pendingEngineSpans.splice(0, overflow); - } - }; - } - } -} - -export const instrumentPrisma = generateInstrumentOnce(INTEGRATION_NAME, options => { - return new SentryPrismaInteropInstrumentation(options); -}); - -/** - * 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: 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 { - * provider = "prisma-client-js" - * previewFeatures = ["tracing"] - * } - * ``` - */ -export const prismaIntegration = defineIntegration((options?: PrismaOptions) => { - return { - name: INTEGRATION_NAME, - setupOnce() { - instrumentPrisma(options); - }, - setup(client) { - // If no tracing helper exists, we skip any work here - // this means that prisma is not being used - if (!getPrismaTracingHelper()) { - 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:')) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.prisma'); - - // Register client spans so a v5 engine span (dispatched later, detached) can resolve one as a - // parent by the Sentry span id Prisma reported via `getTraceParent` (see - // `createResolvedEngineSpans`). Only client spans are referenced this way; engine spans - // reference their parent by the engine's own id and are registered in `createResolvedEngineSpans`, - // so registering them here too would just leave entries that are never looked up. - if (spanJSON.description.startsWith('prisma:client:')) { - registerPrismaSpan(span.spanContext().spanId, span); - } - } - - // Make sure we use the query text as the span name, for ex. SELECT * FROM "User" WHERE "id" = $1. - // v5/v6 emit `prisma:engine:db_query`; v7 inlined the engine and emits `prisma:client:db_query`. - if ( - (spanJSON.description === 'prisma:engine:db_query' || spanJSON.description === 'prisma:client:db_query') && - spanJSON.data['db.query.text'] - ) { - span.updateName(spanJSON.data['db.query.text'] as string); - } - - // In Prisma v5.22+, the `db.system` attribute is automatically set - // On older versions, this is missing, so we add it here - if (spanJSON.description === 'prisma:engine:db_query' && !spanJSON.data['db.system']) { - span.setAttribute('db.system', '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 deleted file mode 100644 index b4df64efa414..000000000000 --- a/packages/node/src/integrations/tracing/prisma/vendored/active-tracing-helper.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright Prisma - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - 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 - * - 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 - */ - -import type { Span, SpanAttributes, SpanKindValue, SpanLink } from '@sentry/core'; -import { - 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 = { - ignoreSpanTypes: (string | RegExp)[]; -}; - -/** - * Older Prisma versions emit `prisma:engine:db_query` spans without a `db.system`, so it's backfilled here. - */ -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; -} - -/** - * 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 ignoreSpanTypes: (string | RegExp)[]; - - public constructor({ ignoreSpanTypes }: Options) { - this.ignoreSpanTypes = ignoreSpanTypes; - } - - public isEnabled(): boolean { - return true; - } - - public getTraceParent(span?: Span): string { - const spanContext = (span ?? getActiveSpan())?.spanContext(); - if (spanContext) { - return `00-${spanContext.traceId}-${spanContext.spanId}-0${spanContext.traceFlags}`; - } - return nonSampledTraceParent; - } - - public dispatchEngineSpans(spans: EngineSpan[]): void { - const linkIds = new Map(); - const roots = spans.filter(span => span.parentId === null); - - for (const root of roots) { - dispatchEngineSpan(root, spans, linkIds, this.ignoreSpanTypes); - } - } - - public getActiveContext(): Span | undefined { - return getActiveSpan(); - } - - public runInChildSpan(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback): R { - const options: ExtendedSpanOptions = typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions; - - if (options.internal && !showAllTraces) { - return callback(); - } - - const name = `prisma:client:${options.name}`; - - if (shouldIgnoreSpan(name, this.ignoreSpanTypes)) { - return callback(); - } - - const parentSpan = getActiveSpan(); - - 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, - parentSpan, - }; - - if (options.active === false) { - const span = startInactiveSpan(spanOptions); - return endSpan(span, () => callback(span, parentSpan)); - } - - return startSpanManual(spanOptions, span => endSpan(span, () => callback(span, parentSpan))); - } -} - -function dispatchEngineSpan( - engineSpan: EngineSpan, - allSpans: EngineSpan[], - linkIds: Map, - ignoreSpanTypes: (string | RegExp)[], -): void { - if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) { - return; - } - - 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(span: Span, run: () => T): T { - let result: T; - try { - result = run(); - } catch (reason) { - span.end(); - throw reason; - } - - if (isPromiseLike(result)) { - return result.then( - value => { - span.end(); - return value; - }, - reason => { - span.end(); - throw reason; - }, - ) as T; - } - span.end(); - return result; -} - -function isPromiseLike(value: unknown): value is PromiseLike { - return value != null && typeof (value as Record)['then'] === 'function'; -} - -function shouldIgnoreSpan(spanName: string, ignoreSpanTypes: (string | RegExp)[]): boolean { - return ignoreSpanTypes.some(pattern => (typeof pattern === 'string' ? pattern === spanName : pattern.test(spanName))); -} diff --git a/packages/node/src/integrations/tracing/prisma/vendored/constants.ts b/packages/node/src/integrations/tracing/prisma/vendored/constants.ts deleted file mode 100644 index 6b9b70a0a02f..000000000000 --- a/packages/node/src/integrations/tracing/prisma/vendored/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Prisma - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/prisma/prisma/tree/b6feea5565ec577545a79547d24273ccdd11b4c7/packages/instrumentation - * - Upstream version: @prisma/instrumentation@7.8.0 - * - Replaced `import packageJson from '../package.json'` with hardcoded values - */ - -import { SDK_VERSION } from '@sentry/core'; - -export const VERSION = SDK_VERSION; - -export const NAME = '@sentry/instrumentation-prisma'; - -export const MODULE_NAME = '@prisma/client'; - -export const SUPPORTED_MODULE_VERSIONS = ['>=5.0.0']; diff --git a/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts deleted file mode 100644 index f71ab390cfeb..000000000000 --- a/packages/node/src/integrations/tracing/prisma/vendored/instrumentation.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Prisma - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - 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 equivalents - * - Replaced `import { VERSION, NAME, MODULE_NAME } from './constants'` with local vendored constants - * - Removed the unused `setTracerProvider`/`tracerProvider` members; spans are created through Sentry's - * span APIs, which resolve the active client themselves - */ - -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)[]; -} - -type Config = PrismaInstrumentationConfig & InstrumentationConfig; - -export class PrismaInstrumentation extends InstrumentationBase { - public constructor(config: Config = {}) { - super(NAME, VERSION, config); - } - - public init(): InstrumentationNodeModuleDefinition[] { - const module = new InstrumentationNodeModuleDefinition(MODULE_NAME, SUPPORTED_MODULE_VERSIONS); - - return [module]; - } - - public enable(): void { - const config = this._config as Config; - - setGlobalTracingHelper( - new ActiveTracingHelper({ - ignoreSpanTypes: config.ignoreSpanTypes ?? [], - }), - ); - } - - public disable(): void { - clearGlobalTracingHelper(); - } - - public isEnabled(): boolean { - return getGlobalTracingHelper() !== undefined; - } -} diff --git a/packages/node/src/integrations/tracing/prisma/vendored/v5-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendored/v5-tracing-helper.ts deleted file mode 100644 index 8823a8ca7728..000000000000 --- a/packages/node/src/integrations/tracing/prisma/vendored/v5-tracing-helper.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Vendored from https://github.com/prisma/prisma/blob/718358aa37975c18e5ea62f5b659fb47630b7609/packages/internals/src/tracing/types.ts#L1 - -import type { Context, Span, SpanOptions } from '@opentelemetry/api'; - -type V5SpanCallback = (span?: Span, context?: Context) => R; - -type V5ExtendedSpanOptions = SpanOptions & { - name: string; - internal?: boolean; - middleware?: boolean; - active?: boolean; - context?: Context; -}; - -type EngineSpanEvent = { - span: boolean; - spans: V5EngineSpan[]; -}; - -type V5EngineSpanKind = 'client' | 'internal'; - -type V5EngineSpan = { - span: boolean; - name: string; - trace_id: string; - span_id: string; - parent_span_id: string; - start_time: [number, number]; - end_time: [number, number]; - attributes?: Record; - links?: { trace_id: string; span_id: string }[]; - kind: V5EngineSpanKind; -}; - -export interface PrismaV5TracingHelper { - isEnabled(): boolean; - getTraceParent(context?: Context): string; - createEngineSpan(engineSpanEvent: EngineSpanEvent): void; - getActiveContext(): Context | undefined; - runInChildSpan(nameOrOptions: string | V5ExtendedSpanOptions, callback: V5SpanCallback): R; -} diff --git a/packages/node/src/integrations/tracing/prisma/vendored/v6-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendored/v6-tracing-helper.ts deleted file mode 100644 index 2ad1482a2e1a..000000000000 --- a/packages/node/src/integrations/tracing/prisma/vendored/v6-tracing-helper.ts +++ /dev/null @@ -1,38 +0,0 @@ -// https://github.com/prisma/prisma/blob/d45607dfa10c4ef08cb8f79f18fa84ef33910150/packages/internals/src/tracing/types.ts#L1 - -import type { Context, Span, SpanOptions } from '@opentelemetry/api'; - -type V6SpanCallback = (span?: Span, context?: Context) => R; - -type V6ExtendedSpanOptions = SpanOptions & { - name: string; - internal?: boolean; - middleware?: boolean; - active?: boolean; - context?: Context; -}; - -type V6EngineSpanId = string; - -type V6HrTime = [number, number]; - -type EngineSpanKind = 'client' | 'internal'; - -type PrismaV6EngineSpan = { - id: V6EngineSpanId; - parentId: string | null; - name: string; - startTime: V6HrTime; - endTime: V6HrTime; - kind: EngineSpanKind; - attributes?: Record; - links?: V6EngineSpanId[]; -}; - -export interface PrismaV6TracingHelper { - isEnabled(): boolean; - getTraceParent(context?: Context): string; - dispatchEngineSpans(spans: PrismaV6EngineSpan[]): void; - getActiveContext(): Context | undefined; - runInChildSpan(nameOrOptions: string | V6ExtendedSpanOptions, callback: V6SpanCallback): R; -} diff --git a/packages/node/test/integrations/tracing/prisma.test.ts b/packages/node/test/integrations/tracing/prisma.test.ts deleted file mode 100644 index 3833b1adb34f..000000000000 --- a/packages/node/test/integrations/tracing/prisma.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PrismaInstrumentation } from '../../../src/integrations/tracing/prisma/vendored/instrumentation'; -import { INSTRUMENTED } from '@sentry/node-core'; -import { beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; -import { instrumentPrisma } from '../../../src/integrations/tracing/prisma'; - -vi.mock('../../../src/integrations/tracing/prisma/vendored/instrumentation'); - -describe('Prisma', () => { - beforeEach(() => { - vi.clearAllMocks(); - delete INSTRUMENTED.Prisma; - - (PrismaInstrumentation as unknown as MockInstance).mockImplementation(() => { - return { - setTracerProvider: () => undefined, - setMeterProvider: () => undefined, - getConfig: () => ({}), - setConfig: () => ({}), - enable: () => undefined, - }; - }); - }); - - it('defaults are correct for instrumentPrisma', () => { - instrumentPrisma(); - - expect(PrismaInstrumentation).toHaveBeenCalledTimes(1); - expect(PrismaInstrumentation).toHaveBeenCalledWith(undefined); - }); - - it('passes instrumentationConfig option to PrismaInstrumentation', () => { - const config = { ignoreSpanTypes: [] }; - instrumentPrisma({ instrumentationConfig: config }); - - expect(PrismaInstrumentation).toHaveBeenCalledTimes(1); - expect(PrismaInstrumentation).toHaveBeenCalledWith(config); - }); -}); diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index b697f70cd198..c7a1784f3e94 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -7,6 +7,8 @@ export { graphqlIntegration } from './graphql'; export { mongooseIntegration } from './mongoose'; export { mysql2Integration } from './mysql2'; +export { instrumentPrisma, prismaIntegration } from './prisma'; +export type { PrismaInstrumentationConfig, PrismaOptions } from './prisma'; export { redisIntegration, type RedisDiagnosticChannelsOptions } from './redis'; export type { RedisDiagnosticChannelResponseHook } from './redis/redis-dc-subscriber'; export { defaultDbStatementSerializer } from './redis/redis-statement-serializer'; diff --git a/packages/node/src/integrations/tracing/prisma/vendored/global.ts b/packages/server-utils/src/prisma/global.ts similarity index 100% rename from packages/node/src/integrations/tracing/prisma/vendored/global.ts rename to packages/server-utils/src/prisma/global.ts diff --git a/packages/server-utils/src/prisma/index.ts b/packages/server-utils/src/prisma/index.ts new file mode 100644 index 000000000000..8a71c4bc3f48 --- /dev/null +++ b/packages/server-utils/src/prisma/index.ts @@ -0,0 +1,67 @@ +import type { IntegrationFn } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; +import { setGlobalTracingHelper } from './global'; +import { ActiveTracingHelper } from './tracing-helper'; + +const INTEGRATION_NAME = 'Prisma' as const; + +export interface PrismaInstrumentationConfig { + /** + * Span types that should not be traced. Matched against the full span name (e.g. `prisma:client:operation`), + * either exactly (string) or by pattern (RegExp). + */ + ignoreSpanTypes?: (string | RegExp)[]; +} + +export interface PrismaOptions { + /** + * @deprecated This is no longer used, v5 works out of the box. + */ + prismaInstrumentation?: unknown; + /** + * Configuration for the Prisma tracing helper. + */ + instrumentationConfig?: PrismaInstrumentationConfig; +} + +/** + * Sets up the global Prisma tracing helper that Prisma looks up on `globalThis` to emit tracing data. + * + * Prisma reads `globalThis.PRISMA_INSTRUMENTATION` (and its versioned variant) to find a "TracingHelper" + * which it uses internally to create spans, so it can produce tracing data without depending on + * OpenTelemetry. The helper we install here mints spans through Sentry's span APIs. A single helper + * serves both Prisma v5 (which calls `createEngineSpan`) and v6/v7 (which call `dispatchEngineSpans`), + * so it doesn't blow up on version mismatches. + */ +export function instrumentPrisma(options?: PrismaOptions): void { + setGlobalTracingHelper( + new ActiveTracingHelper({ + ignoreSpanTypes: options?.instrumentationConfig?.ignoreSpanTypes ?? [], + }), + ); +} + +const _prismaIntegration = ((options?: PrismaOptions) => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentPrisma(options); + }, + }; +}) satisfies IntegrationFn; + +/** + * 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: 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 { + * provider = "prisma-client-js" + * previewFeatures = ["tracing"] + * } + * ``` + */ +export const prismaIntegration = defineIntegration(_prismaIntegration); diff --git a/packages/server-utils/src/prisma/tracing-helper.ts b/packages/server-utils/src/prisma/tracing-helper.ts new file mode 100644 index 000000000000..3e7209c34c7d --- /dev/null +++ b/packages/server-utils/src/prisma/tracing-helper.ts @@ -0,0 +1,336 @@ +/* + * Copyright Prisma + * SPDX-License-Identifier: Apache-2.0 + * + * NOTICE from the Sentry authors: + * - 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 + * - 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 + * - Added a `createEngineSpan` method so a single helper serves both Prisma v5 (which calls + * `createEngineSpan`) and v6/v7 (which call `dispatchEngineSpans`) + */ + +import type { Span, SpanAttributes } from '@sentry/core'; +import { + debug, + getActiveSpan, + LRUMap, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_KIND, + startInactiveSpan, + startSpanManual, +} from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import type { EngineSpan, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; +import { DB_SYSTEM } from '@sentry/conventions/attributes'; + +const showAllTraces = process.env.PRISMA_SHOW_ALL_TRACES === 'true'; + +const nonSampledTraceParent = `00-10-10-00`; + +const PRISMA_ORIGIN = 'auto.db.otel.prisma'; + +type Options = { + ignoreSpanTypes: (string | RegExp)[]; +}; + +// Vendored in from @prisma/instrumentation v5: +type V5EngineSpanEvent = { + span: boolean; + spans: V5EngineSpan[]; +}; + +type V5EngineSpanKind = 'client' | 'internal'; + +type V5EngineSpan = { + span: boolean; + name: string; + trace_id: string; + span_id: string; + parent_span_id: string; + start_time: [number, number]; + end_time: [number, number]; + attributes?: Record; + links?: { trace_id: string; span_id: string }[]; + kind: V5EngineSpanKind; +}; + +// Prisma v5 dispatches engine spans (`prisma:engine:*`, which carry the SQL `db.statement`) one batch +// at a time, out of order, and detached from the active span, naming their parent only by id: a span +// references either a client span (by the Sentry span id Prisma learned via `getTraceParent`) or a +// sibling engine span (by the engine's own id), and a batch may not contain the parent it references. +// By the time the engine reports them the client span may already have ended, so `getActiveSpan()` +// can't be used to find it. `prismaSpanRegistry` maps span ids to their Sentry span, and an engine +// span whose parent isn't registered yet waits in `pendingEngineSpans` until a later batch registers it. +const MAX_TRACKED_PRISMA_SPANS = 1000; +const prismaSpanRegistry = new LRUMap(MAX_TRACKED_PRISMA_SPANS); +const pendingEngineSpans: V5EngineSpan[] = []; + +/** Register a span so v5 engine spans can later resolve it as a parent by the id Prisma reports it under. */ +function registerPrismaSpan(id: string, span: Span): void { + prismaSpanRegistry.set(id, span); +} + +/** + * Older Prisma versions emit `prisma:engine:db_query` spans without a `db.system`, so it's backfilled here. + */ +function buildSpanAttributes(name: string, attributes: Record | undefined): SpanAttributes { + const merged: SpanAttributes = { + ...(attributes as SpanAttributes | undefined), + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, + }; + + // oxlint-disable-next-line typescript/no-deprecated + if (name === 'prisma:engine:db_query' && merged[DB_SYSTEM] == null) { + // oxlint-disable-next-line typescript/no-deprecated + 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; +} + +/** + * Create every pending v5 engine span whose parent is now registered, repeating until no further span + * resolves (so a child queued before its parent is created once the parent arrives in a later batch). + * Each span is created under its resolved parent and registered by its engine id so its own children + * can find it; origin, the `prisma:engine:db_query` to SQL rename, and the `db.system` backfill are + * applied here, exactly as for v6/v7 engine spans. + */ +function createResolvedEngineSpans(): void { + // Terminates: `createdSpan` is only set true right after a `splice`, so every pass that keeps the + // loop going strictly shrinks the finite `pendingEngineSpans`; a pass that resolves nothing exits. + let createdSpan = true; + while (createdSpan) { + createdSpan = false; + for (let i = pendingEngineSpans.length - 1; i >= 0; i--) { + const engineSpan = pendingEngineSpans[i]!; + const parentSpan = prismaSpanRegistry.get(engineSpan.parent_span_id); + if (!parentSpan) { + continue; + } + + const attributes = buildSpanAttributes(engineSpan.name, engineSpan.attributes); + const span = startInactiveSpan({ + name: buildSpanName(engineSpan.name, attributes), + attributes, + kind: engineSpan.kind === 'client' ? SPAN_KIND.CLIENT : SPAN_KIND.INTERNAL, + startTime: engineSpan.start_time, + parentSpan, + }); + registerPrismaSpan(engineSpan.span_id, span); + + // Engine links reference other engine spans by their engine id; re-point them at the Sentry spans + // we minted (mirroring v6/v7's `dispatchEngineSpan`). Links must be added before the span ends, + // since the SentryTracerProvider seals spans on end. Links to spans we haven't created are dropped. + if (engineSpan.links) { + span.addLinks( + engineSpan.links.flatMap(link => { + const linkedSpan = prismaSpanRegistry.get(link.span_id); + return linkedSpan ? [{ context: linkedSpan.spanContext() }] : []; + }), + ); + } + + span.end(engineSpan.end_time); + + pendingEngineSpans.splice(i, 1); + createdSpan = true; + } + } +} + +/** + * This satisifes the TracingHelper interface for Prisma v5 and v6/v7. + */ +export class ActiveTracingHelper implements TracingHelper { + private ignoreSpanTypes: (string | RegExp)[]; + + public constructor({ ignoreSpanTypes }: Options) { + this.ignoreSpanTypes = ignoreSpanTypes; + } + + public isEnabled(): boolean { + return true; + } + + public getTraceParent(span?: Span): string { + const spanContext = (span ?? getActiveSpan())?.spanContext(); + if (spanContext) { + return `00-${spanContext.traceId}-${spanContext.spanId}-0${spanContext.traceFlags}`; + } + return nonSampledTraceParent; + } + + public dispatchEngineSpans(spans: EngineSpan[]): void { + const linkIds = new Map(); + const roots = spans.filter(span => span.parentId === null); + + for (const root of roots) { + dispatchEngineSpan(root, spans, linkIds, this.ignoreSpanTypes); + } + } + + /** + * Prisma v5 broke the tracing helper interface with the v6 major, replacing `createEngineSpan` with + * `dispatchEngineSpans`. We implement the v6/v7 interface (`dispatchEngineSpans`) but also keep this + * v5-only method so the same helper doesn't blow up in Prisma 5 users' faces, minting v5 engine spans + * through Sentry's span APIs instead of crashing. + */ + public createEngineSpan(engineSpanEvent: V5EngineSpanEvent): void { + pendingEngineSpans.push(...engineSpanEvent.spans); + + // Resolve before capping so a span is only ever dropped after a full resolution attempt, never + // before it had a chance to find its parent. What remains are orphans whose parent was never + // registered (e.g. evicted from the registry under sustained load); cap that backlog so it can't + // grow without bound. + createResolvedEngineSpans(); + + const overflow = pendingEngineSpans.length - MAX_TRACKED_PRISMA_SPANS; + if (overflow > 0) { + DEBUG_BUILD && + debug.log(`[Prisma] Dropping ${overflow} unresolved v5 engine span(s) whose parent was never registered.`); + pendingEngineSpans.splice(0, overflow); + } + } + + public getActiveContext(): Span | undefined { + return getActiveSpan(); + } + + public runInChildSpan(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback): R { + const options: ExtendedSpanOptions = typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions; + + if (options.internal && !showAllTraces) { + return callback(); + } + + const name = `prisma:client:${options.name}`; + + if (shouldIgnoreSpan(name, this.ignoreSpanTypes)) { + return callback(); + } + + const parentSpan = getActiveSpan(); + + const attributes = buildSpanAttributes(name, options.attributes as Record | undefined); + const spanOptions = { + name: buildSpanName(name, attributes), + attributes, + kind: options.kind, + links: options.links, + startTime: options.startTime, + parentSpan, + }; + + if (options.active === false) { + const span = startInactiveSpan(spanOptions); + // Register the client span so a v5 engine span (dispatched later, detached) can resolve it as a + // parent by the Sentry span id Prisma reported via `getTraceParent` (see `createResolvedEngineSpans`). + registerPrismaSpan(span.spanContext().spanId, span); + return endSpan(span, () => callback(span, parentSpan)); + } + + return startSpanManual(spanOptions, span => { + registerPrismaSpan(span.spanContext().spanId, span); + return endSpan(span, () => callback(span, parentSpan)); + }); + } +} + +function dispatchEngineSpan( + engineSpan: EngineSpan, + allSpans: EngineSpan[], + linkIds: Map, + ignoreSpanTypes: (string | RegExp)[], +): void { + if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) { + return; + } + + 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(span: Span, run: () => T): T { + let result: T; + try { + result = run(); + } catch (reason) { + span.end(); + throw reason; + } + + if (isPromiseLike(result)) { + return result.then( + value => { + span.end(); + return value; + }, + reason => { + span.end(); + throw reason; + }, + ) as T; + } + span.end(); + return result; +} + +function isPromiseLike(value: unknown): value is PromiseLike { + return value != null && typeof (value as Record)['then'] === 'function'; +} + +function shouldIgnoreSpan(spanName: string, ignoreSpanTypes: (string | RegExp)[]): boolean { + return ignoreSpanTypes.some(pattern => (typeof pattern === 'string' ? pattern === spanName : pattern.test(spanName))); +} diff --git a/packages/node/src/integrations/tracing/prisma/vendored/types.ts b/packages/server-utils/src/prisma/types.ts similarity index 78% rename from packages/node/src/integrations/tracing/prisma/vendored/types.ts rename to packages/server-utils/src/prisma/types.ts index 7b77d9911f50..409d166efee9 100644 --- a/packages/node/src/integrations/tracing/prisma/vendored/types.ts +++ b/packages/server-utils/src/prisma/types.ts @@ -7,20 +7,25 @@ * - 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) + * - Replaced the `@opentelemetry/api` `SpanOptions` dependency with Sentry's own span types, so this + * package does not need to depend on OpenTelemetry */ -import type { SpanOptions } from '@opentelemetry/api'; -import type { Span } from '@sentry/core'; +import type { Span, SpanAttributes, SpanKindValue, SpanLink, SpanTimeInput } from '@sentry/core'; export type SpanCallback = (span?: Span, parentSpan?: Span) => R; -export interface ExtendedSpanOptions extends SpanOptions { +export interface ExtendedSpanOptions { /** The name of the span */ name: string; /* Internal spans are not shown unless PRISMA_SHOW_ALL_TRACES=true env var is set */ internal?: boolean; /** Whether it propagates context (?=true) */ active?: boolean; + attributes?: SpanAttributes; + kind?: SpanKindValue; + links?: SpanLink[]; + startTime?: SpanTimeInput; } export type EngineSpanId = string; diff --git a/packages/server-utils/test/prisma.test.ts b/packages/server-utils/test/prisma.test.ts new file mode 100644 index 000000000000..8d887118b683 --- /dev/null +++ b/packages/server-utils/test/prisma.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { instrumentPrisma } from '../src/prisma'; +import type { TracingHelper } from '../src/prisma/types'; + +type PrismaGlobal = typeof globalThis & { + PRISMA_INSTRUMENTATION?: { helper?: TracingHelper }; + V7_PRISMA_INSTRUMENTATION?: { helper?: TracingHelper }; +}; + +function getHelper(): (TracingHelper & { createEngineSpan?: unknown }) | undefined { + return (globalThis as PrismaGlobal).PRISMA_INSTRUMENTATION?.helper; +} + +describe('instrumentPrisma', () => { + afterEach(() => { + const g = globalThis as PrismaGlobal; + g.PRISMA_INSTRUMENTATION = undefined; + g.V7_PRISMA_INSTRUMENTATION = undefined; + }); + + it('installs a tracing helper on both the versioned and fallback globals', () => { + instrumentPrisma(); + + const g = globalThis as PrismaGlobal; + expect(g.PRISMA_INSTRUMENTATION?.helper).toBeDefined(); + expect(g.V7_PRISMA_INSTRUMENTATION?.helper).toBe(g.PRISMA_INSTRUMENTATION?.helper); + }); + + it('installs a helper that serves both the v5 and v6/v7 interfaces', () => { + instrumentPrisma(); + + const helper = getHelper(); + // v6/v7 interface + expect(typeof helper?.dispatchEngineSpans).toBe('function'); + expect(typeof helper?.runInChildSpan).toBe('function'); + expect(typeof helper?.getTraceParent).toBe('function'); + // v5-only interface, backfilled so Prisma 5 doesn't crash calling a missing method + expect(typeof helper?.createEngineSpan).toBe('function'); + expect(helper?.isEnabled()).toBe(true); + }); + + it('accepts the instrumentationConfig option', () => { + expect(() => + instrumentPrisma({ instrumentationConfig: { ignoreSpanTypes: ['prisma:client:operation'] } }), + ).not.toThrow(); + expect(getHelper()).toBeDefined(); + }); +}); From 48bb0c8877af5b1e2456d5b67caabb56ad8745ca Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 13 Jul 2026 14:42:52 +0200 Subject: [PATCH 2/2] fix for deno --- packages/deno/test/mod.test.ts | 4 ++-- packages/server-utils/src/prisma/tracing-helper.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index 87eb84b4cb1c..ecc3a6d4fe9e 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -215,11 +215,11 @@ Deno.test('App runs without errors', async _ => { }); const output = await cmd.output(); - assertEquals(output.success, true); const td = new TextDecoder(); const outString = td.decode(output.stdout); const errString = td.decode(output.stderr); - assertEquals(outString, 'App has started\n'); assertEquals(errString, ''); + assertEquals(outString, 'App has started\n'); + assertEquals(output.success, true); }); diff --git a/packages/server-utils/src/prisma/tracing-helper.ts b/packages/server-utils/src/prisma/tracing-helper.ts index 3e7209c34c7d..bef80a9acfee 100644 --- a/packages/server-utils/src/prisma/tracing-helper.ts +++ b/packages/server-utils/src/prisma/tracing-helper.ts @@ -27,7 +27,15 @@ import { DEBUG_BUILD } from '../debug-build'; import type { EngineSpan, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; import { DB_SYSTEM } from '@sentry/conventions/attributes'; -const showAllTraces = process.env.PRISMA_SHOW_ALL_TRACES === 'true'; +// Reading `process.env` can throw in runtimes that gate env access (e.g. Deno without `--allow-env`) +// and `process` may be absent altogether (edge runtimes), so this degrades to `false` in those cases. +const showAllTraces = ((): boolean => { + try { + return process.env.PRISMA_SHOW_ALL_TRACES === 'true'; + } catch { + return false; + } +})(); const nonSampledTraceParent = `00-10-10-00`;