diff --git a/dev-packages/node-integration-tests/suites/tracing/nitro/scenario-storage-no-parent.mjs b/dev-packages/node-integration-tests/suites/tracing/nitro/scenario-storage-no-parent.mjs new file mode 100644 index 000000000000..8daaaf9926fd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/nitro/scenario-storage-no-parent.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import * as Sentry from '@sentry/node'; +import { createStorage } from 'unstorage'; +import { withTracing } from 'unstorage/tracing'; + +const storage = withTracing(createStorage()); + +async function readAndWrite(key) { + await storage.setItem(key, 'value'); + assert.equal(await storage.getItem(key), 'value'); +} + +async function run() { + await readAndWrite('before_parent'); + + const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); + await Sentry.withActiveSpan(parent, () => readAndWrite('inside_parent')); + + await readAndWrite('after_parent'); + await storage.dispose(); + + // Flush every operation before the parent signals that span collection is complete. + await Sentry.flush(); + parent.end(); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/nitro/test.ts b/dev-packages/node-integration-tests/suites/tracing/nitro/test.ts index dfd8a50e6e4b..bb1a492c5cd2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nitro/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nitro/test.ts @@ -1,3 +1,4 @@ +import type { SerializedStreamedSpan } from '@sentry/core'; import { CACHE_HIT, CACHE_KEY, @@ -114,6 +115,44 @@ describe.runIf(supports({ min: 20 }))('nitro integrations', () => { }); }); + createEsmTests(__dirname, 'scenario-storage-no-parent.mjs', 'instrument.mjs', (createRunner, test) => { + test('only instruments storage operations with an active parent span', async () => { + const runner = createRunner(); + const cacheSpans = new Map(); + const spansPromise = runner.collectStreamedSpans(spans => { + for (const span of spans) { + if (span.attributes[SENTRY_ORIGIN]?.value === 'auto.cache.nitro') { + cacheSpans.set(span.span_id, span); + } + } + return spans.some(span => span.is_segment && span.name === 'parent operation'); + }); + + await runner.unordered().start().completed(); + + const spans = await spansPromise; + const parent = spans.find(span => span.is_segment && span.name === 'parent operation'); + expect(parent).toBeDefined(); + expect( + Array.from(cacheSpans.values(), span => ({ + operation: span.attributes[DB_OPERATION_NAME]?.value, + key: span.attributes[CACHE_KEY]?.value, + parentSpanId: span.parent_span_id, + traceId: span.trace_id, + isSegment: span.is_segment, + })), + ).toEqual( + ['setItem', 'getItem'].map(operation => ({ + operation, + key: 'inside_parent', + parentSpanId: parent?.span_id, + traceId: parent?.trace_id, + isSegment: false, + })), + ); + }); + }); + createEsmTests(__dirname, 'scenario.mjs', 'instrument-no-tracing.mjs', (createRunner, test) => { test('sets Server-Timing headers without tracing enabled', async () => { const runner = createRunner().start(); diff --git a/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts b/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts index d419951912fa..e7f2ac3068df 100644 --- a/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts +++ b/packages/server-utils/src/integrations/nitro/captureStorageEvents.ts @@ -104,6 +104,7 @@ function setupStorageTracingChannel(operation: TracedOperation): void { }); }, { + requiresParentSpan: true, beforeSpanEnd(span, data) { // Error status is set by the binding; the error itself is captured at the request boundary, // not here (cache ops aren't an error boundary). Only enrich the success path.