From 71a98da4a31744a8bbc82ccd600c7ea7c78383af Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 08:59:16 +0100 Subject: [PATCH 1/3] fix(server-utils): Require a parent span for Prisma operations Co-Authored-By: GPT-6 --- .../suites/tracing/prisma-orm-v5/test.ts | 7 +++ .../suites/tracing/prisma-orm-v6/test.ts | 6 +++ .../tracing/prisma-orm/parent-span-tests.ts | 45 +++++++++++++++++++ .../tracing/prisma-orm/scenario-no-parent.mjs | 24 ++++++++++ .../src/integrations/prisma/tracing-helper.ts | 3 ++ .../test/integrations/prisma.test.ts | 16 +++++-- 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts index 4c528b7c3aa5..00970b7dc525 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -10,6 +10,7 @@ import { DB } from '@sentry/conventions/op'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; +import { parentSpanTests } from '../prisma-orm/parent-span-tests'; afterAll(() => { cleanupChildProcesses(); @@ -158,5 +159,11 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () copyPaths: ['prisma'], }, ); + + createEsmAndCjsTests(__dirname, '../prisma-orm/scenario-no-parent.mjs', 'instrument.mjs', parentSpanTests, { + additionalDependencies: ADDITIONAL_DEPENDENCIES, + afterSetupCommand: AFTER_SETUP_COMMAND, + copyPaths: ['prisma'], + }); }); }); 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 18f44727b1bb..57a46549da40 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 @@ -10,6 +10,7 @@ import { DB } from '@sentry/conventions/op'; import type { SerializedStreamedSpan } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; +import { parentSpanTests } from '../prisma-orm/parent-span-tests'; afterAll(() => { cleanupChildProcesses(); @@ -152,4 +153,9 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] copyPaths: ['prisma'], }, ); + + createEsmAndCjsTests(__dirname, '../prisma-orm/scenario-no-parent.mjs', 'instrument.mjs', parentSpanTests, { + afterSetupCommand: 'prisma generate --schema prisma/schema.prisma', + copyPaths: ['prisma'], + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts new file mode 100644 index 000000000000..57313a112292 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts @@ -0,0 +1,45 @@ +import { DB_QUERY_TEXT, DB_STATEMENT, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import type { SerializedStreamedSpan } from '@sentry/core'; +import { expect } from 'vitest'; +import type { createEsmAndCjsTests } from '../../../utils/runner'; + +function queryText(span: SerializedStreamedSpan): unknown { + return span.attributes[DB_QUERY_TEXT]?.value ?? span.attributes[DB_STATEMENT]?.value; +} + +export const parentSpanTests: Parameters[3] = (createRunner, test) => { + test('only instruments operations with an active parent span', { timeout: 75_000 }, async () => { + const runner = createRunner(); + const prismaSpans = new Map(); + const spansPromise = runner.collectStreamedSpans(spans => { + for (const span of spans) { + if (span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.prisma') { + prismaSpans.set(span.span_id, span); + } + } + return ( + spans.some(span => span.is_segment && span.name === 'parent operation') && + spans.some(span => { + const query = queryText(span); + return typeof query === 'string' && query.includes('inside_parent'); + }) + ); + }); + + await runner.unordered().start().completed(); + + const collected = await spansPromise; + const parent = collected.find(span => span.is_segment && span.name === 'parent operation'); + const spans = [...prismaSpans.values()]; + const operations = spans.filter(span => span.name === 'prisma:client:operation'); + const validParents = new Set([parent?.span_id, ...spans.map(span => span.span_id)]); + + expect(parent).toBeDefined(); + expect(operations.map(span => span.parent_span_id)).toEqual([parent?.span_id]); + expect(spans.filter(span => span.is_segment)).toEqual([]); + expect(spans.every(span => validParents.has(span.parent_span_id))).toBe(true); + expect(spans.map(queryText).filter(query => typeof query === 'string')).toEqual([ + expect.stringContaining('inside_parent'), + ]); + }); +}; diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs new file mode 100644 index 000000000000..ede09a382bb1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import { PrismaClient } from '@prisma/client'; +import * as Sentry from '@sentry/node'; + +async function run() { + const client = new PrismaClient(); + + assert.deepEqual(await client.$queryRawUnsafe('SELECT 1 AS before_parent'), [{ before_parent: 1 }]); + + const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); + await Sentry.withActiveSpan(parent, async () => { + assert.deepEqual(await client.$queryRawUnsafe('SELECT 2 AS inside_parent'), [{ inside_parent: 2 }]); + }); + + assert.deepEqual(await client.$queryRawUnsafe('SELECT 3 AS after_parent'), [{ after_parent: 3 }]); + await client.$disconnect(); + + // Flush every operation before the parent signals that span collection is complete. + await Sentry.flush(); + parent.end(); + await Sentry.flush(); +} + +run(); diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index 218e7c788134..b5f867ed0430 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -291,6 +291,9 @@ export class ActiveTracingHelper implements TracingHelper { } const parentSpan = getActiveSpan(); + if (!parentSpan) { + return callback(); + } const attributes = buildSpanAttributes(name, options.attributes); const spanOptions = { diff --git a/packages/server-utils/test/integrations/prisma.test.ts b/packages/server-utils/test/integrations/prisma.test.ts index 2d02718b6d1e..00af81b1f7d4 100644 --- a/packages/server-utils/test/integrations/prisma.test.ts +++ b/packages/server-utils/test/integrations/prisma.test.ts @@ -1,5 +1,13 @@ import type { Span } from '@sentry/core'; -import { Client, createTransport, getCurrentScope, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core'; +import { + Client, + createTransport, + getCurrentScope, + initAndBind, + resolvedSyncPromise, + spanToJSON, + startSpan, +} from '@sentry/core'; import { afterEach, describe, expect, it } from 'vitest'; import { instrumentPrisma } from '../../src/integrations/prisma'; import type { TracingHelper } from '../../src/integrations/prisma/types'; @@ -37,8 +45,10 @@ function initTestClient(): void { /** Runs a `db_query` span through the installed helper and returns the span it created. */ function runDbQuerySpan(attributes: Record): Span { let span: Span | undefined; - getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => { - span = createdSpan; + startSpan({ name: 'parent' }, () => { + getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => { + span = createdSpan; + }); }); return span!; } From 68f5e6fd63440d1627b0436121dc563720270d7f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 09:52:58 +0100 Subject: [PATCH 2/3] test(server-utils): Reuse existing Prisma integration suites Co-Authored-By: GPT-6 --- .../suites/tracing/prisma-orm-v5/scenario.mjs | 10 +++++ .../suites/tracing/prisma-orm-v5/test.ts | 10 +---- .../suites/tracing/prisma-orm-v6/scenario.mjs | 10 +++++ .../suites/tracing/prisma-orm-v6/test.ts | 9 +--- .../tracing/prisma-orm/parent-span-tests.ts | 45 ------------------- .../tracing/prisma-orm/scenario-no-parent.mjs | 24 ---------- 6 files changed, 24 insertions(+), 84 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts delete mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.mjs index 1c9511f52913..8b88396a686c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.mjs @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import { PrismaClient } from '@prisma/client'; import * as Sentry from '@sentry/node'; import { randomBytes } from 'crypto'; @@ -11,6 +12,11 @@ async function run() { op: 'transaction', }, async span => { + await client.$connect(); + await Sentry.withActiveSpan(null, async () => { + assert.deepEqual(await client.$queryRawUnsafe('SELECT 1 AS before_parent'), [{ before_parent: 1 }]); + }); + await client.user.create({ data: { name: 'Tilda', @@ -40,6 +46,10 @@ async function run() { }, }); + await Sentry.withActiveSpan(null, async () => { + assert.deepEqual(await client.$queryRawUnsafe('SELECT 3 AS after_parent'), [{ after_parent: 3 }]); + }); + setTimeout(async () => { span.end(); await client.$disconnect(); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts index 00970b7dc525..474f3b68a1b0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -10,7 +10,6 @@ import { DB } from '@sentry/conventions/op'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; -import { parentSpanTests } from '../prisma-orm/parent-span-tests'; afterAll(() => { cleanupChildProcesses(); @@ -24,6 +23,7 @@ const ADDITIONAL_DEPENDENCIES = { function expectPrismaV5Spans(container: SerializedStreamedSpanContainer): void { const segment = container.items.find(span => span.is_segment); expect(segment?.name).toBe('Test Transaction'); + expect(container.items.filter(span => span.is_segment)).toHaveLength(1); const spans = container.items.filter(span => !span.is_segment); expect(spans.length).toBeGreaterThanOrEqual(5); @@ -149,7 +149,7 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { - test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => { + test('only instruments PostgreSQL queries with an active parent span', { timeout: 75_000 }, async () => { await createRunner().expect({ span: expectPrismaV5Spans }).start().completed(); }); }, @@ -159,11 +159,5 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () copyPaths: ['prisma'], }, ); - - createEsmAndCjsTests(__dirname, '../prisma-orm/scenario-no-parent.mjs', 'instrument.mjs', parentSpanTests, { - additionalDependencies: ADDITIONAL_DEPENDENCIES, - afterSetupCommand: AFTER_SETUP_COMMAND, - copyPaths: ['prisma'], - }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.mjs index 29862d9fad4e..b2fbfbb40120 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.mjs @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import { PrismaClient } from '@prisma/client'; import * as Sentry from '@sentry/node'; import { randomBytes } from 'crypto'; @@ -11,6 +12,11 @@ async function run() { async span => { const client = new PrismaClient(); + await client.$connect(); + await Sentry.withActiveSpan(null, async () => { + assert.deepEqual(await client.$queryRawUnsafe('SELECT 1 AS before_parent'), [{ before_parent: 1 }]); + }); + await client.user.create({ data: { name: 'Tilda', @@ -28,6 +34,10 @@ async function run() { }, }); + await Sentry.withActiveSpan(null, async () => { + assert.deepEqual(await client.$queryRawUnsafe('SELECT 3 AS after_parent'), [{ after_parent: 3 }]); + }); + setTimeout(async () => { span.end(); await client.$disconnect(); 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 57a46549da40..427d147b6a62 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 @@ -10,7 +10,6 @@ import { DB } from '@sentry/conventions/op'; import type { SerializedStreamedSpan } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; -import { parentSpanTests } from '../prisma-orm/parent-span-tests'; afterAll(() => { cleanupChildProcesses(); @@ -22,12 +21,13 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { - test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => { + test('only instruments PostgreSQL queries with an active parent span', { timeout: 75_000 }, async () => { await createRunner() .expect({ span: container => { const segment = container.items.find(span => span.is_segment); expect(segment?.name).toBe('Test Transaction'); + expect(container.items.filter(span => span.is_segment)).toHaveLength(1); const spans = container.items.filter(span => !span.is_segment); expect(spans.length).toBeGreaterThanOrEqual(5); @@ -153,9 +153,4 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] copyPaths: ['prisma'], }, ); - - createEsmAndCjsTests(__dirname, '../prisma-orm/scenario-no-parent.mjs', 'instrument.mjs', parentSpanTests, { - afterSetupCommand: 'prisma generate --schema prisma/schema.prisma', - copyPaths: ['prisma'], - }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts deleted file mode 100644 index 57313a112292..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/parent-span-tests.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { DB_QUERY_TEXT, DB_STATEMENT, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpan } from '@sentry/core'; -import { expect } from 'vitest'; -import type { createEsmAndCjsTests } from '../../../utils/runner'; - -function queryText(span: SerializedStreamedSpan): unknown { - return span.attributes[DB_QUERY_TEXT]?.value ?? span.attributes[DB_STATEMENT]?.value; -} - -export const parentSpanTests: Parameters[3] = (createRunner, test) => { - test('only instruments operations with an active parent span', { timeout: 75_000 }, async () => { - const runner = createRunner(); - const prismaSpans = new Map(); - const spansPromise = runner.collectStreamedSpans(spans => { - for (const span of spans) { - if (span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.prisma') { - prismaSpans.set(span.span_id, span); - } - } - return ( - spans.some(span => span.is_segment && span.name === 'parent operation') && - spans.some(span => { - const query = queryText(span); - return typeof query === 'string' && query.includes('inside_parent'); - }) - ); - }); - - await runner.unordered().start().completed(); - - const collected = await spansPromise; - const parent = collected.find(span => span.is_segment && span.name === 'parent operation'); - const spans = [...prismaSpans.values()]; - const operations = spans.filter(span => span.name === 'prisma:client:operation'); - const validParents = new Set([parent?.span_id, ...spans.map(span => span.span_id)]); - - expect(parent).toBeDefined(); - expect(operations.map(span => span.parent_span_id)).toEqual([parent?.span_id]); - expect(spans.filter(span => span.is_segment)).toEqual([]); - expect(spans.every(span => validParents.has(span.parent_span_id))).toBe(true); - expect(spans.map(queryText).filter(query => typeof query === 'string')).toEqual([ - expect.stringContaining('inside_parent'), - ]); - }); -}; diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs deleted file mode 100644 index ede09a382bb1..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario-no-parent.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import assert from 'node:assert/strict'; -import { PrismaClient } from '@prisma/client'; -import * as Sentry from '@sentry/node'; - -async function run() { - const client = new PrismaClient(); - - assert.deepEqual(await client.$queryRawUnsafe('SELECT 1 AS before_parent'), [{ before_parent: 1 }]); - - const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); - await Sentry.withActiveSpan(parent, async () => { - assert.deepEqual(await client.$queryRawUnsafe('SELECT 2 AS inside_parent'), [{ inside_parent: 2 }]); - }); - - assert.deepEqual(await client.$queryRawUnsafe('SELECT 3 AS after_parent'), [{ after_parent: 3 }]); - await client.$disconnect(); - - // Flush every operation before the parent signals that span collection is complete. - await Sentry.flush(); - parent.end(); - await Sentry.flush(); -} - -run(); From 9b34091ecc0d35cd57389f40e7013d9e0b5edbc1 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 29 Oct 2026 10:05:39 +0100 Subject: [PATCH 3/3] fix(server-utils): Require a parent for Prisma engine batches Co-Authored-By: GPT-6 --- .../src/integrations/prisma/tracing-helper.ts | 4 + .../test/integrations/prisma.test.ts | 91 +++++++++++++++---- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index b5f867ed0430..1d7bd4d284c4 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -242,6 +242,10 @@ export class ActiveTracingHelper implements TracingHelper { } public dispatchEngineSpans(spans: EngineSpan[]): void { + if (!getActiveSpan()) { + return; + } + const linkIds = new Map(); const roots = spans.filter(span => span.parentId === null); diff --git a/packages/server-utils/test/integrations/prisma.test.ts b/packages/server-utils/test/integrations/prisma.test.ts index 00af81b1f7d4..e9b739e8f216 100644 --- a/packages/server-utils/test/integrations/prisma.test.ts +++ b/packages/server-utils/test/integrations/prisma.test.ts @@ -1,16 +1,17 @@ -import type { Span } from '@sentry/core'; +import type { Client, Span, SpanAttributes } from '@sentry/core'; import { - Client, createTransport, getCurrentScope, initAndBind, resolvedSyncPromise, spanToJSON, startSpan, + withActiveSpan, } from '@sentry/core'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { instrumentPrisma } from '../../src/integrations/prisma'; -import type { TracingHelper } from '../../src/integrations/prisma/types'; +import type { EngineSpan, TracingHelper } from '../../src/integrations/prisma/types'; +import { TestClient } from '../mocks/client'; type PrismaGlobal = typeof globalThis & { PRISMA_INSTRUMENTATION?: { helper?: TracingHelper }; @@ -21,18 +22,9 @@ function getHelper(): (TracingHelper & { createEngineSpan?: unknown }) | undefin return (globalThis as PrismaGlobal).PRISMA_INSTRUMENTATION?.helper; } -class TestClient extends Client { - public eventFromException(): PromiseLike { - return resolvedSyncPromise({}); - } - public eventFromMessage(): PromiseLike { - return resolvedSyncPromise({}); - } -} - -function initTestClient(): void { +function initTestClient(): Client { getCurrentScope().setClient(undefined); - initAndBind(TestClient, { + return initAndBind(TestClient, { dsn: 'https://username@domain/123', integrations: [], sendClientReports: false, @@ -43,7 +35,7 @@ function initTestClient(): void { } /** Runs a `db_query` span through the installed helper and returns the span it created. */ -function runDbQuerySpan(attributes: Record): Span { +function runDbQuerySpan(attributes: SpanAttributes): Span { let span: Span | undefined; startSpan({ name: 'parent' }, () => { getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => { @@ -55,6 +47,7 @@ function runDbQuerySpan(attributes: Record): Span { describe('instrumentPrisma', () => { afterEach(() => { + getCurrentScope().setClient(undefined); const g = globalThis as PrismaGlobal; g.PRISMA_INSTRUMENTATION = undefined; g.V7_PRISMA_INSTRUMENTATION = undefined; @@ -81,6 +74,72 @@ describe('instrumentPrisma', () => { expect(helper?.isEnabled()).toBe(true); }); + describe('engine spans', () => { + const engineSpans: EngineSpan[] = [ + { + id: 'engine-query', + parentId: null, + name: 'prisma:engine:query', + kind: 'internal', + startTime: [1, 0], + endTime: [1, 1_000_000], + }, + { + id: 'db-query', + parentId: 'engine-query', + name: 'prisma:engine:db_query', + kind: 'client', + startTime: [1, 0], + endTime: [1, 1_000_000], + attributes: { + 'db.query.text': 'SELECT * FROM "User"', + 'db.system.name': 'postgresql', + }, + }, + ]; + + it('runs the callback without creating client or engine spans when no parent is active', () => { + const client = initTestClient(); + instrumentPrisma(); + const spanStart = vi.fn(); + client.on('spanStart', spanStart); + const helper = getHelper()!; + const callback = vi.fn(() => { + helper.dispatchEngineSpans(engineSpans); + return 'query result'; + }); + + const result = withActiveSpan(null, () => helper.runInChildSpan('operation', callback)); + + expect(result).toBe('query result'); + expect(callback).toHaveBeenCalledExactlyOnceWith(); + expect(spanStart).not.toHaveBeenCalled(); + }); + + it('attaches the engine batch to the active parent and preserves its child relationships', () => { + const client = initTestClient(); + instrumentPrisma(); + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + const helper = getHelper()!; + + startSpan({ name: 'parent' }, parent => { + helper.dispatchEngineSpans(engineSpans); + + const engineSpan = endedSpans.find(span => spanToJSON(span).name === 'prisma:engine:query')!; + expect( + endedSpans.map(span => ({ + name: spanToJSON(span).name, + parentSpanId: spanToJSON(span).parent_span_id, + })), + ).toEqual([ + { name: 'SELECT "User"', parentSpanId: engineSpan.spanContext().spanId }, + { name: 'prisma:engine:query', parentSpanId: parent.spanContext().spanId }, + ]); + }); + }); + }); + describe('db.query.summary', () => { it('summarizes a standard-dialect statement', () => { initTestClient();