From 9035426bbdc9fbdeb9cbc9b8561d9e21eec36dc5 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:50:12 +0200 Subject: [PATCH 1/2] test(e2e): Migrate nitro-3 to span streaming Co-Authored-By: Claude Fable 5 --- .../test-applications/nitro-3/instrument.mjs | 1 - .../test-applications/nitro-3/src/main.ts | 1 - .../nitro-3/tests/cache.test.ts | 123 ++++++------ .../nitro-3/tests/isolation.test.ts | 14 +- .../nitro-3/tests/middleware.test.ts | 32 ++-- .../nitro-3/tests/span-nesting.test.ts | 93 ++++++---- .../nitro-3/tests/storage-aliases.test.ts | 131 ++++++------- .../nitro-3/tests/storage.test.ts | 175 +++++++++--------- .../nitro-3/tests/trace-propagation.test.ts | 13 +- .../nitro-3/tests/transactions.test.ts | 77 ++++---- 10 files changed, 331 insertions(+), 329 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs index eed3a8f5cd01..53b80d309a5b 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/nitro'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts b/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts index c190f5df30bf..d27d0ba1763a 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/browser'; // Let's us test trace propagation Sentry.init({ - traceLifecycle: 'static', environment: 'qa', dsn: 'https://public@dsn.ingest.sentry.io/1337', tunnel: 'http://localhost:3031/', // proxy server diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/cache.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/cache.test.ts index 336e74c8e030..a83b70c5ca2e 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/cache.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/cache.test.ts @@ -1,31 +1,38 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nitro'; +import { collectStreamedSpans } from '@sentry-internal/test-utils'; test.describe('Cache Instrumentation', () => { const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; + // Streamed spans arrive across several envelopes (a child can flush before its segment), + // so accumulate until the segment span has arrived and filter by its trace. + async function collectCacheSpans() { + const spans = await collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.attributes['url.path']?.value === '/api/test-cache'), + ); + const segmentSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/test-cache'); + + return spans.filter( + span => span.trace_id === segmentSpan?.trace_id && span.attributes['sentry.origin']?.value === 'auto.cache.nitro', + ); + } + test('instruments cachedFunction and cachedHandler calls and creates spans with correct attributes', async ({ request, }) => { - const transactionPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent.transaction?.includes('GET /api/test-cache') ?? false; - }); + const cacheSpansPromise = collectCacheSpans(); const response = await request.get('/api/test-cache'); expect(response.status()).toBe(200); - const transaction = await transactionPromise; + const allCacheSpans = await cacheSpansPromise; + expect(allCacheSpans.length).toBeGreaterThan(0); - const findSpansByMethod = (method: string) => { - return transaction.spans?.filter(span => span.data?.['db.operation.name'] === method) || []; - }; + const findSpansByMethod = (method: string) => + allCacheSpans.filter(span => span.attributes['db.operation.name']?.value === method); - const allCacheSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nitro', - ); - expect(allCacheSpans?.length).toBeGreaterThan(0); + const getCacheKey = (span: (typeof allCacheSpans)[number]) => span.attributes[SEMANTIC_ATTRIBUTE_CACHE_KEY]?.value; // getItem spans for cachedFunction - should have both cache miss and cache hit const getItemSpans = findSpansByMethod('getItem'); @@ -34,31 +41,31 @@ test.describe('Cache Instrumentation', () => { // Find cache miss (first call to getCachedUser('123')) const cacheMissSpan = getItemSpans.find( span => - typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' && - span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('user:123') && - !span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT], + typeof getCacheKey(span) === 'string' && + (getCacheKey(span) as string).includes('user:123') && + !span.attributes[SEMANTIC_ATTRIBUTE_CACHE_HIT]?.value, ); expect(cacheMissSpan).toBeDefined(); - expect(cacheMissSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: false, - 'db.operation.name': 'getItem', + expect(cacheMissSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false }, + 'db.operation.name': { type: 'string', value: 'getItem' }, }); // Find cache hit (second call to getCachedUser('123')) const cacheHitSpan = getItemSpans.find( span => - typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' && - span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('user:123') && - span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT], + typeof getCacheKey(span) === 'string' && + (getCacheKey(span) as string).includes('user:123') && + span.attributes[SEMANTIC_ATTRIBUTE_CACHE_HIT]?.value, ); expect(cacheHitSpan).toBeDefined(); - expect(cacheHitSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, - 'db.operation.name': 'getItem', + expect(cacheHitSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, + 'db.operation.name': { type: 'string', value: 'getItem' }, }); // setItem spans for cachedFunction - when cache miss occurs, value is set @@ -66,40 +73,31 @@ test.describe('Cache Instrumentation', () => { expect(setItemSpans.length).toBeGreaterThan(0); const cacheSetSpan = setItemSpans.find( - span => - typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' && - span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('user:123'), + span => typeof getCacheKey(span) === 'string' && (getCacheKey(span) as string).includes('user:123'), ); expect(cacheSetSpan).toBeDefined(); - expect(cacheSetSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - 'db.operation.name': 'setItem', + expect(cacheSetSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.put' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + 'db.operation.name': { type: 'string', value: 'setItem' }, }); // Spans for different cached functions const dataKeySpans = getItemSpans.filter( - span => - typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' && - span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('data:test-key'), + span => typeof getCacheKey(span) === 'string' && (getCacheKey(span) as string).includes('data:test-key'), ); expect(dataKeySpans.length).toBeGreaterThan(0); // Spans for cachedHandler const cachedHandlerSpans = getItemSpans.filter( - span => - typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' && - span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('cachedHandler'), + span => typeof getCacheKey(span) === 'string' && (getCacheKey(span) as string).includes('cachedHandler'), ); expect(cachedHandlerSpans.length).toBeGreaterThan(0); - // Verify all cache spans have OK status - allCacheSpans?.forEach(span => { + // Verify all cache spans have OK status and are nested under the request's segment span + allCacheSpans.forEach(span => { expect(span.status).toBe('ok'); - }); - - // Verify cache spans are properly nested under the transaction - allCacheSpans?.forEach(span => { + expect(span.is_segment).toBe(false); expect(span.parent_span_id).toBeDefined(); }); }); @@ -108,31 +106,28 @@ test.describe('Cache Instrumentation', () => { const uniqueUser = `test-${Date.now()}`; const uniqueData = `data-${Date.now()}`; - const transactionPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent.transaction?.includes('GET /api/test-cache') ?? false; - }); + const cacheSpansPromise = collectCacheSpans(); await request.get(`/api/test-cache?user=${uniqueUser}&data=${uniqueData}`); - const transaction = await transactionPromise; - const allCacheSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nitro', - ); - expect(allCacheSpans?.length).toBeGreaterThan(0); + const allCacheSpans = await cacheSpansPromise; + expect(allCacheSpans.length).toBeGreaterThan(0); - const allGetItemSpans = allCacheSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.get'); - const allSetItemSpans = allCacheSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.put'); + const allGetItemSpans = allCacheSpans.filter(span => span.attributes['sentry.op']?.value === 'cache.get'); + const allSetItemSpans = allCacheSpans.filter(span => span.attributes['sentry.op']?.value === 'cache.put'); - expect(allGetItemSpans?.length).toBeGreaterThan(0); - expect(allSetItemSpans?.length).toBeGreaterThan(0); + expect(allGetItemSpans.length).toBeGreaterThan(0); + expect(allSetItemSpans.length).toBeGreaterThan(0); - const cacheMissSpans = allGetItemSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT] === false); - const cacheHitSpans = allGetItemSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT] === true); + const cacheMissSpans = allGetItemSpans.filter( + span => span.attributes[SEMANTIC_ATTRIBUTE_CACHE_HIT]?.value === false, + ); + const cacheHitSpans = allGetItemSpans.filter(span => span.attributes[SEMANTIC_ATTRIBUTE_CACHE_HIT]?.value === true); // At least one cache miss (first calls to getCachedUser and getCachedData) - expect(cacheMissSpans?.length).toBeGreaterThanOrEqual(1); + expect(cacheMissSpans.length).toBeGreaterThanOrEqual(1); // At least one cache hit (second calls to getCachedUser and getCachedData) - expect(cacheHitSpans?.length).toBeGreaterThanOrEqual(1); + expect(cacheHitSpans.length).toBeGreaterThanOrEqual(1); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts index 7234fa0948ca..b2826856c3a1 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts @@ -1,9 +1,9 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Isolation scope prevents tag leaking between requests', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-isolation/:id'; + const segmentSpanPromise = waitForStreamedSpan('nitro-3', span => { + return span.is_segment && span.name === 'GET /api/test-isolation/:id'; }); const errorPromise = waitForError('nitro-3', event => { @@ -14,12 +14,12 @@ test('Isolation scope prevents tag leaking between requests', async ({ request } // noop - route throws }); - const transactionEvent = await transactionEventPromise; + const segmentSpan = await segmentSpanPromise; const error = await errorPromise; - // Assert that isolation scope works properly + expect(segmentSpan).toBeDefined(); + + // Streamed spans do not carry scope tags, so the isolation check relies on the error event expect(error.tags?.['my-isolated-tag']).toBe(true); expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); - expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts index 50aed0ddabe2..38621f9096fc 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts @@ -1,22 +1,26 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Creates middleware spans for requests', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-transaction'; - }); + const spansPromise = collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.name === 'GET /api/test-transaction'), + ); const response = await request.get('/api/test-transaction'); expect(response.headers()['x-sentry-test-middleware']).toBe('executed'); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment && span.name === 'GET /api/test-transaction'); // h3 middleware spans have origin auto.http.nitro.h3 and op middleware - const h3MiddlewareSpans = transactionEvent.spans?.filter( - span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware', + const h3MiddlewareSpans = spans.filter( + span => + span.trace_id === segmentSpan?.trace_id && + span.attributes['sentry.origin']?.value === 'auto.http.nitro.h3' && + getSpanOp(span) === 'middleware', ); - expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1); + expect(h3MiddlewareSpans.length).toBeGreaterThanOrEqual(1); }); test('Captures errors thrown in middleware with error status on span', async ({ request }) => { @@ -24,8 +28,10 @@ test('Captures errors thrown in middleware with error status on span', async ({ return !event.type && !!event.exception?.values?.some(v => v.value === 'Middleware error'); }); - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-transaction' && event?.contexts?.trace?.status === 'internal_error'; + // The middleware throws before the route handler runs, so the segment may keep its bare + // method-only name - select it by URL path instead. + const segmentSpanPromise = waitForStreamedSpan('nitro-3', span => { + return span.is_segment && span.attributes['url.path']?.value === '/api/test-transaction' && span.status === 'error'; }); await request.get('/api/test-transaction?middleware-error=1'); @@ -33,8 +39,8 @@ test('Captures errors thrown in middleware with error status on span', async ({ const errorEvent = await errorEventPromise; expect(errorEvent.exception?.values?.some(v => v.value === 'Middleware error')).toBe(true); - const transactionEvent = await transactionEventPromise; + const segmentSpan = await segmentSpanPromise; - // The transaction span should have error status - expect(transactionEvent.contexts?.trace?.status).toBe('internal_error'); + // The segment span should have error status + expect(segmentSpan.status).toBe('error'); }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts index 841566b32d74..b522be6d30e0 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts @@ -1,61 +1,78 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; + +// Streamed spans arrive across several envelopes (a child can flush before its segment), +// so accumulate until the segment span has arrived and filter by its trace. +async function collectNestingSpans() { + const spans = await collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.name === 'GET /api/test-nesting'), + ); + const segmentSpan = spans.find(span => span.is_segment && span.name === 'GET /api/test-nesting'); + return spans.filter(span => span.trace_id === segmentSpan?.trace_id); +} test('Span nesting: all spans share the same trace_id', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-nesting'; - }); + const spansPromise = collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.name === 'GET /api/test-nesting'), + ); await request.get('/api/test-nesting'); - const event = await transactionEventPromise; - const traceId = event.contexts?.trace?.trace_id; + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment && span.name === 'GET /api/test-nesting'); + const traceId = segmentSpan?.trace_id; expect(traceId).toMatch(/[a-f0-9]{32}/); - // Every child span must belong to the same trace - for (const span of event.spans ?? []) { - expect(span.trace_id).toBe(traceId); + // Every span created during the request must belong to the same trace + for (const name of ['db.select', 'db.insert', 'db.serialize']) { + const span = spans.find(s => s.name === name); + expect(span).toBeDefined(); + expect(span?.trace_id).toBe(traceId); } }); test('Span nesting: h3 middleware spans are children of the srvx request span', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-nesting'; - }); + const spansPromise = collectNestingSpans(); await request.get('/api/test-nesting'); - const event = await transactionEventPromise; + const spans = await spansPromise; - // Find the srvx request span - const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server'); + // The srvx request span nests below the http_server segment span of the request + const segmentSpan = spans.find(span => span.is_segment && span.name === 'GET /api/test-nesting'); + const srvxSpan = spans.find( + span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.srvx' && getSpanOp(span) === 'http.server', + ); expect(srvxSpan).toBeDefined(); + expect(srvxSpan?.parent_span_id).toBe(segmentSpan!.span_id); // All h3 middleware spans should be children of the srvx span - const h3MiddlewareSpans = event.spans?.filter( - span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware', + const h3MiddlewareSpans = spans.filter( + span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.h3' && getSpanOp(span) === 'middleware', ); - expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1); + expect(h3MiddlewareSpans.length).toBeGreaterThanOrEqual(1); - for (const span of h3MiddlewareSpans ?? []) { + for (const span of h3MiddlewareSpans) { expect(span.parent_span_id).toBe(srvxSpan!.span_id); } }); test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-nesting'; - }); + const spansPromise = collectNestingSpans(); await request.get('/api/test-nesting'); - const event = await transactionEventPromise; + const spans = await spansPromise; - const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server'); + const srvxSpan = spans.find( + span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.srvx' && getSpanOp(span) === 'http.server', + ); expect(srvxSpan).toBeDefined(); - const h3HandlerSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server'); + const h3HandlerSpan = spans.find( + span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.h3' && getSpanOp(span) === 'http.server', + ); expect(h3HandlerSpan).toBeDefined(); expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id); }); @@ -63,21 +80,21 @@ test('Span nesting: h3 route handler span is a child of the srvx request span', test('Span nesting: manual startSpan calls inside route handler are children of the h3 route handler span', async ({ request, }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-nesting'; - }); + const spansPromise = collectNestingSpans(); await request.get('/api/test-nesting'); - const event = await transactionEventPromise; + const spans = await spansPromise; // Find the h3 route handler span - const h3HandlerSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server'); + const h3HandlerSpan = spans.find( + span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.h3' && getSpanOp(span) === 'http.server', + ); expect(h3HandlerSpan).toBeDefined(); // Find the manually created db spans - const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select'); - const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert'); + const dbSelectSpan = spans.find(span => getSpanOp(span) === 'db' && span.name === 'db.select'); + const dbInsertSpan = spans.find(span => getSpanOp(span) === 'db' && span.name === 'db.insert'); expect(dbSelectSpan).toBeDefined(); expect(dbInsertSpan).toBeDefined(); @@ -89,23 +106,21 @@ test('Span nesting: manual startSpan calls inside route handler are children of expect(dbSelectSpan!.parent_span_id).toBe(dbInsertSpan!.parent_span_id); // The serialize span should be nested inside the db.insert span - const serializeSpan = event.spans?.find(span => span.op === 'serialize' && span.description === 'db.serialize'); + const serializeSpan = spans.find(span => getSpanOp(span) === 'serialize' && span.name === 'db.serialize'); expect(serializeSpan).toBeDefined(); expect(serializeSpan!.parent_span_id).toBe(dbInsertSpan!.span_id); }); test('Span nesting: middleware spans start before manual spans in the span tree', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', event => { - return event?.transaction === 'GET /api/test-nesting'; - }); + const spansPromise = collectNestingSpans(); await request.get('/api/test-nesting'); - const event = await transactionEventPromise; + const spans = await spansPromise; // Middleware spans should start before the manual db spans - const middlewareSpans = event.spans?.filter(span => span.op === 'middleware') ?? []; - const dbSpans = event.spans?.filter(span => span.op === 'db') ?? []; + const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); expect(middlewareSpans.length).toBeGreaterThanOrEqual(1); expect(dbSpans.length).toBeGreaterThanOrEqual(1); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts index 2109810bc9be..8b8091512afb 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts @@ -1,6 +1,18 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nitro'; +import { collectStreamedSpans } from '@sentry-internal/test-utils'; + +// Streamed spans arrive across several envelopes (a child can flush before its segment), +// so accumulate until the segment span has arrived and filter by its trace. +async function collectStorageSpans(route: string) { + const spans = await collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.attributes['url.path']?.value === route), + ); + const segmentSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === route); + + return spans.filter( + span => span.trace_id === segmentSpan?.trace_id && span.attributes['sentry.origin']?.value === 'auto.cache.nitro', + ); +} test.describe('Storage Instrumentation - Aliases', () => { const prefixKey = (key: string) => `cache:${key}`; @@ -8,95 +20,88 @@ test.describe('Storage Instrumentation - Aliases', () => { const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; test('instruments storage alias methods (get, set, has, del, remove) and creates spans', async ({ request }) => { - const transactionPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent.transaction?.includes('GET /api/test-storage-aliases') ?? false; - }); + const storageSpansPromise = collectStorageSpans('/api/test-storage-aliases'); const response = await request.get('/api/test-storage-aliases'); expect(response.status()).toBe(200); - const transaction = await transactionPromise; + const allStorageSpans = await storageSpansPromise; // Helper to find spans by operation - const findSpansByMethod = (method: string) => { - return transaction.spans?.filter(span => span.data?.['db.operation.name'] === method) || []; - }; + const findSpansByMethod = (method: string) => + allStorageSpans.filter(span => span.attributes['db.operation.name']?.value === method); + + const findByKey = (method: string, key: string) => + findSpansByMethod(method).find(span => span.attributes[SEMANTIC_ATTRIBUTE_CACHE_KEY]?.value === key); // Test set (alias for setItem) - const setSpans = findSpansByMethod('setItem'); - expect(setSpans.length).toBeGreaterThanOrEqual(1); - const setSpan = setSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(findSpansByMethod('setItem').length).toBeGreaterThanOrEqual(1); + const setSpan = findByKey('setItem', prefixKey('alias:user')); expect(setSpan).toBeDefined(); - expect(setSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), - 'db.operation.name': 'setItem', - 'db.system.name': expect.any(String), + expect(setSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.put' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') }, + 'db.operation.name': { type: 'string', value: 'setItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(setSpan?.description).toBe(prefixKey('alias:user')); + expect(setSpan?.name).toBe(prefixKey('alias:user')); // Test get (alias for getItem) - const getSpans = findSpansByMethod('getItem'); - expect(getSpans.length).toBeGreaterThanOrEqual(1); - const getSpan = getSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1); + const getSpan = findByKey('getItem', prefixKey('alias:user')); expect(getSpan).toBeDefined(); - expect(getSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, - 'db.operation.name': 'getItem', - 'db.system.name': expect.any(String), + expect(getSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, + 'db.operation.name': { type: 'string', value: 'getItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(getSpan?.description).toBe(prefixKey('alias:user')); + expect(getSpan?.name).toBe(prefixKey('alias:user')); // Test has (alias for hasItem) - const hasSpans = findSpansByMethod('hasItem'); - expect(hasSpans.length).toBeGreaterThanOrEqual(1); - const hasSpan = hasSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1); + const hasSpan = findByKey('hasItem', prefixKey('alias:user')); expect(hasSpan).toBeDefined(); - expect(hasSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, - 'db.operation.name': 'hasItem', - 'db.system.name': expect.any(String), + expect(hasSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, + 'db.operation.name': { type: 'string', value: 'hasItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Test del and remove (both aliases for removeItem) - const removeSpans = findSpansByMethod('removeItem'); - expect(removeSpans.length).toBeGreaterThanOrEqual(2); + expect(findSpansByMethod('removeItem').length).toBeGreaterThanOrEqual(2); - const delSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp1')); + const delSpan = findByKey('removeItem', prefixKey('alias:temp1')); expect(delSpan).toBeDefined(); - expect(delSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp1'), - 'db.operation.name': 'removeItem', - 'db.system.name': expect.any(String), + expect(delSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') }, + 'db.operation.name': { type: 'string', value: 'removeItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(delSpan?.description).toBe(prefixKey('alias:temp1')); + expect(delSpan?.name).toBe(prefixKey('alias:temp1')); - const removeSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp2')); + const removeSpan = findByKey('removeItem', prefixKey('alias:temp2')); expect(removeSpan).toBeDefined(); - expect(removeSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp2'), - 'db.operation.name': 'removeItem', - 'db.system.name': expect.any(String), + expect(removeSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') }, + 'db.operation.name': { type: 'string', value: 'removeItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(removeSpan?.description).toBe(prefixKey('alias:temp2')); + expect(removeSpan?.name).toBe(prefixKey('alias:temp2')); // Verify all spans have OK status - const allStorageSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nitro', - ); - expect(allStorageSpans?.length).toBeGreaterThan(0); - allStorageSpans?.forEach(span => { + expect(allStorageSpans.length).toBeGreaterThan(0); + allStorageSpans.forEach(span => { expect(span.status).toBe('ok'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts index 21d3ccc9c343..0879a1b1534a 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts @@ -1,6 +1,18 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nitro'; +import { collectStreamedSpans } from '@sentry-internal/test-utils'; + +// Streamed spans arrive across several envelopes (a child can flush before its segment), +// so accumulate until the segment span has arrived and filter by its trace. +async function collectStorageSpans(route: string) { + const spans = await collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.attributes['url.path']?.value === route), + ); + const segmentSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === route); + + return spans.filter( + span => span.trace_id === segmentSpan?.trace_id && span.attributes['sentry.origin']?.value === 'auto.cache.nitro', + ); +} test.describe('Storage Instrumentation', () => { const prefixKey = (key: string) => `cache:${key}`; @@ -8,135 +20,120 @@ test.describe('Storage Instrumentation', () => { const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; test('instruments all storage operations and creates spans with correct attributes', async ({ request }) => { - const transactionPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent.transaction?.includes('GET /api/test-storage') ?? false; - }); + const storageSpansPromise = collectStorageSpans('/api/test-storage'); const response = await request.get('/api/test-storage'); expect(response.status()).toBe(200); - const transaction = await transactionPromise; + const allStorageSpans = await storageSpansPromise; // Helper to find spans by operation - const findSpansByMethod = (method: string) => { - return transaction.spans?.filter(span => span.data?.['db.operation.name'] === method) || []; - }; + const findSpansByMethod = (method: string) => + allStorageSpans.filter(span => span.attributes['db.operation.name']?.value === method); + + const findSpanByCacheKey = (method: string, key: string) => + findSpansByMethod(method).find(span => span.attributes[SEMANTIC_ATTRIBUTE_CACHE_KEY]?.value === key); // Test setItem spans - const setItemSpans = findSpansByMethod('setItem'); - expect(setItemSpans.length).toBeGreaterThanOrEqual(1); - const setItemSpan = setItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(findSpansByMethod('setItem').length).toBeGreaterThanOrEqual(1); + const setItemSpan = findSpanByCacheKey('setItem', prefixKey('user:123')); expect(setItemSpan).toBeDefined(); - expect(setItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), - 'db.operation.name': 'setItem', - 'db.system.name': expect.any(String), + expect(setItemSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.put' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') }, + 'db.operation.name': { type: 'string', value: 'setItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(setItemSpan?.description).toBe(prefixKey('user:123')); + expect(setItemSpan?.name).toBe(prefixKey('user:123')); // Test setItemRaw spans - const setItemRawSpans = findSpansByMethod('setItemRaw'); - expect(setItemRawSpans.length).toBeGreaterThanOrEqual(1); - const setItemRawSpan = setItemRawSpans.find( - span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('raw:data'), - ); + expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1); + const setItemRawSpan = findSpanByCacheKey('setItemRaw', prefixKey('raw:data')); expect(setItemRawSpan).toBeDefined(); - expect(setItemRawSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), - 'db.operation.name': 'setItemRaw', - 'db.system.name': expect.any(String), + expect(setItemRawSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.put' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') }, + 'db.operation.name': { type: 'string', value: 'setItemRaw' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Test hasItem spans - should have cache hit attribute - const hasItemSpans = findSpansByMethod('hasItem'); - expect(hasItemSpans.length).toBeGreaterThanOrEqual(1); - const hasItemSpan = hasItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1); + const hasItemSpan = findSpanByCacheKey('hasItem', prefixKey('user:123')); expect(hasItemSpan).toBeDefined(); - expect(hasItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, - 'db.operation.name': 'hasItem', - 'db.system.name': expect.any(String), + expect(hasItemSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, + 'db.operation.name': { type: 'string', value: 'hasItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Test getItem spans - should have cache hit attribute - const getItemSpans = findSpansByMethod('getItem'); - expect(getItemSpans.length).toBeGreaterThanOrEqual(1); - const getItemSpan = getItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1); + const getItemSpan = findSpanByCacheKey('getItem', prefixKey('user:123')); expect(getItemSpan).toBeDefined(); - expect(getItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, - 'db.operation.name': 'getItem', - 'db.system.name': expect.any(String), + expect(getItemSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, + 'db.operation.name': { type: 'string', value: 'getItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(getItemSpan?.description).toBe(prefixKey('user:123')); + expect(getItemSpan?.name).toBe(prefixKey('user:123')); // Test getItemRaw spans - should have cache hit attribute - const getItemRawSpans = findSpansByMethod('getItemRaw'); - expect(getItemRawSpans.length).toBeGreaterThanOrEqual(1); - const getItemRawSpan = getItemRawSpans.find( - span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('raw:data'), - ); + expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1); + const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data')); expect(getItemRawSpan).toBeDefined(); - expect(getItemRawSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), - [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, - 'db.operation.name': 'getItemRaw', - 'db.system.name': expect.any(String), + expect(getItemRawSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') }, + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, + 'db.operation.name': { type: 'string', value: 'getItemRaw' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Test getKeys spans const getKeysSpans = findSpansByMethod('getKeys'); expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); - expect(getKeysSpans[0]?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - 'db.operation.name': 'getKeys', - 'db.system.name': expect.any(String), + expect(getKeysSpans[0]?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.get' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + 'db.operation.name': { type: 'string', value: 'getKeys' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Test removeItem spans - const removeItemSpans = findSpansByMethod('removeItem'); - expect(removeItemSpans.length).toBeGreaterThanOrEqual(1); - const removeItemSpan = removeItemSpans.find( - span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('batch:1'), - ); + expect(findSpansByMethod('removeItem').length).toBeGreaterThanOrEqual(1); + const removeItemSpan = findSpanByCacheKey('removeItem', prefixKey('batch:1')); expect(removeItemSpan).toBeDefined(); - expect(removeItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('batch:1'), - 'db.operation.name': 'removeItem', - 'db.system.name': expect.any(String), + expect(removeItemSpan?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') }, + 'db.operation.name': { type: 'string', value: 'removeItem' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Test clear spans const clearSpans = findSpansByMethod('clear'); expect(clearSpans.length).toBeGreaterThanOrEqual(1); - expect(clearSpans[0]?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nitro', - 'db.operation.name': 'clear', - 'db.system.name': expect.any(String), + expect(clearSpans[0]?.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, + 'db.operation.name': { type: 'string', value: 'clear' }, + 'db.system.name': { type: 'string', value: expect.any(String) }, }); // Verify all spans have OK status - const allStorageSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nitro', - ); - expect(allStorageSpans?.length).toBeGreaterThan(0); - allStorageSpans?.forEach(span => { + expect(allStorageSpans.length).toBeGreaterThan(0); + allStorageSpans.forEach(span => { expect(span.status).toBe('ok'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts index 705521ad759d..a45c151e3813 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts @@ -1,16 +1,15 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Propagates server trace to client pageload via Server-Timing headers', async ({ page }) => { - const clientTxnPromise = waitForTransaction('nitro-3', event => { - return event?.contexts?.trace?.op === 'pageload'; + const clientSpanPromise = waitForStreamedSpan('nitro-3', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); await page.goto('/'); - const clientTxn = await clientTxnPromise; + const clientSpan = await clientSpanPromise; - expect(clientTxn.contexts?.trace?.trace_id).toBeDefined(); - expect(clientTxn.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/); - expect(clientTxn.contexts?.trace?.op).toBe('pageload'); + expect(clientSpan.trace_id).toMatch(/[a-f0-9]{32}/); + expect(getSpanOp(clientSpan)).toBe('pageload'); }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts index 48de9c4349df..12fe83043014 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts @@ -1,71 +1,58 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('Sends a transaction event for a successful route', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent?.transaction === 'GET /api/test-transaction'; - }); +test('Sends a segment span for a successful route', async ({ request }) => { + const spansPromise = collectStreamedSpans('nitro-3', spans => + spans.some(span => span.is_segment && span.name === 'GET /api/test-transaction'), + ); await request.get('/api/test-transaction'); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: 'GET /api/test-transaction', - type: 'transaction', - }), - ); - - // srvx.request creates a span for the request - const srvxSpans = transactionEvent.spans?.filter(span => span.origin === 'auto.http.nitro.srvx'); - expect(srvxSpans?.length).toBeGreaterThanOrEqual(1); + // The node http server integration creates the segment span; srvx and h3 spans nest below it + const segmentSpan = spans.find(span => span.is_segment && span.name === 'GET /api/test-transaction'); + expect(segmentSpan).toBeDefined(); + expect(segmentSpan?.attributes['sentry.origin']?.value).toBe('auto.http.http_server'); + expect(getSpanOp(segmentSpan!)).toBe('http.server'); // h3 creates a child span for the route handler - const h3Spans = transactionEvent.spans?.filter(span => span.origin === 'auto.http.nitro.h3'); - expect(h3Spans?.length).toBeGreaterThanOrEqual(1); + const h3Spans = spans.filter( + span => span.trace_id === segmentSpan?.trace_id && span.attributes['sentry.origin']?.value === 'auto.http.nitro.h3', + ); + expect(h3Spans.length).toBeGreaterThanOrEqual(1); }); -test('Sets correct HTTP status code on transaction', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent?.transaction === 'GET /api/test-transaction'; +test('Sets correct HTTP status code on the segment span', async ({ request }) => { + const segmentSpanPromise = waitForStreamedSpan('nitro-3', span => { + return span.is_segment && span.name === 'GET /api/test-transaction'; }); await request.get('/api/test-transaction'); - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.contexts?.trace?.data).toEqual( - expect.objectContaining({ - 'http.response.status_code': 200, - }), - ); + const segmentSpan = await segmentSpanPromise; - expect(transactionEvent.contexts?.trace?.status).toBe('ok'); + expect(segmentSpan.attributes['http.response.status_code']?.value).toBe(200); + expect(segmentSpan.status).toBe('ok'); }); -test('Uses parameterized route for transaction name', async ({ request }) => { - const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => { - return transactionEvent?.transaction === 'GET /api/test-param/:id'; +test('Uses parameterized route for segment span name', async ({ request }) => { + const segmentSpanPromise = waitForStreamedSpan('nitro-3', span => { + return span.is_segment && span.name === 'GET /api/test-param/:id'; }); await request.get('/api/test-param/123'); - const transactionEvent = await transactionEventPromise; + const segmentSpan = await segmentSpanPromise; - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: 'GET /api/test-param/:id', - transaction_info: expect.objectContaining({ source: 'route' }), - type: 'transaction', + expect(segmentSpan).toMatchObject({ + name: 'GET /api/test-param/:id', + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'http.route': { type: 'string', value: '/api/test-param/:id' }, }), - ); - - expect(transactionEvent.contexts?.trace?.data).toEqual( - expect.objectContaining({ - 'http.route': '/api/test-param/:id', - }), - ); + }); }); test('Sets Server-Timing response headers for trace propagation', async ({ request }) => { From 48ca94d9ac6a276707c6ccfae0746ff91ebb868e Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:38:58 +0200 Subject: [PATCH 2/2] test(e2e): Expect low-cardinality cache span names Storage span names are `cache.{operation}` under span streaming since #23830. Co-Authored-By: Claude Fable 5 --- .../nitro-3/tests/storage-aliases.test.ts | 13 +++++++++---- .../test-applications/nitro-3/tests/storage.test.ts | 12 ++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts index 8b8091512afb..1997290e1e3c 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage-aliases.test.ts @@ -40,12 +40,13 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(setSpan).toBeDefined(); expect(setSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.put' }, + 'cache.operation': { type: 'string', value: 'put' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') }, 'db.operation.name': { type: 'string', value: 'setItem' }, 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(setSpan?.name).toBe(prefixKey('alias:user')); + expect(setSpan?.name).toBe('cache.put'); // Test get (alias for getItem) expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1); @@ -53,13 +54,14 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(getSpan).toBeDefined(); expect(getSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.get' }, + 'cache.operation': { type: 'string', value: 'get' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') }, [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, 'db.operation.name': { type: 'string', value: 'getItem' }, 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(getSpan?.name).toBe(prefixKey('alias:user')); + expect(getSpan?.name).toBe('cache.get'); // Test has (alias for hasItem) expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1); @@ -67,6 +69,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(hasSpan).toBeDefined(); expect(hasSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.get' }, + 'cache.operation': { type: 'string', value: 'get' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') }, [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, @@ -81,23 +84,25 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(delSpan).toBeDefined(); expect(delSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'cache.operation': { type: 'string', value: 'remove' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') }, 'db.operation.name': { type: 'string', value: 'removeItem' }, 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(delSpan?.name).toBe(prefixKey('alias:temp1')); + expect(delSpan?.name).toBe('cache.remove'); const removeSpan = findByKey('removeItem', prefixKey('alias:temp2')); expect(removeSpan).toBeDefined(); expect(removeSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'cache.operation': { type: 'string', value: 'remove' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') }, 'db.operation.name': { type: 'string', value: 'removeItem' }, 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(removeSpan?.name).toBe(prefixKey('alias:temp2')); + expect(removeSpan?.name).toBe('cache.remove'); // Verify all spans have OK status expect(allStorageSpans.length).toBeGreaterThan(0); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts index 0879a1b1534a..787c8cad1d95 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/storage.test.ts @@ -40,12 +40,13 @@ test.describe('Storage Instrumentation', () => { expect(setItemSpan).toBeDefined(); expect(setItemSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.put' }, + 'cache.operation': { type: 'string', value: 'put' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') }, 'db.operation.name': { type: 'string', value: 'setItem' }, 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(setItemSpan?.name).toBe(prefixKey('user:123')); + expect(setItemSpan?.name).toBe('cache.put'); // Test setItemRaw spans expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1); @@ -53,6 +54,7 @@ test.describe('Storage Instrumentation', () => { expect(setItemRawSpan).toBeDefined(); expect(setItemRawSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.put' }, + 'cache.operation': { type: 'string', value: 'put' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') }, 'db.operation.name': { type: 'string', value: 'setItemRaw' }, @@ -65,6 +67,7 @@ test.describe('Storage Instrumentation', () => { expect(hasItemSpan).toBeDefined(); expect(hasItemSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.get' }, + 'cache.operation': { type: 'string', value: 'get' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') }, [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, @@ -78,13 +81,14 @@ test.describe('Storage Instrumentation', () => { expect(getItemSpan).toBeDefined(); expect(getItemSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.get' }, + 'cache.operation': { type: 'string', value: 'get' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') }, [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, 'db.operation.name': { type: 'string', value: 'getItem' }, 'db.system.name': { type: 'string', value: expect.any(String) }, }); - expect(getItemSpan?.name).toBe(prefixKey('user:123')); + expect(getItemSpan?.name).toBe('cache.get'); // Test getItemRaw spans - should have cache hit attribute expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1); @@ -92,6 +96,7 @@ test.describe('Storage Instrumentation', () => { expect(getItemRawSpan).toBeDefined(); expect(getItemRawSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.get' }, + 'cache.operation': { type: 'string', value: 'get' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') }, [SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true }, @@ -104,6 +109,7 @@ test.describe('Storage Instrumentation', () => { expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); expect(getKeysSpans[0]?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.get' }, + 'cache.operation': { type: 'string', value: 'get' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, 'db.operation.name': { type: 'string', value: 'getKeys' }, 'db.system.name': { type: 'string', value: expect.any(String) }, @@ -115,6 +121,7 @@ test.describe('Storage Instrumentation', () => { expect(removeItemSpan).toBeDefined(); expect(removeItemSpan?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'cache.operation': { type: 'string', value: 'remove' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, [SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') }, 'db.operation.name': { type: 'string', value: 'removeItem' }, @@ -126,6 +133,7 @@ test.describe('Storage Instrumentation', () => { expect(clearSpans.length).toBeGreaterThanOrEqual(1); expect(clearSpans[0]?.attributes).toMatchObject({ 'sentry.op': { type: 'string', value: 'cache.remove' }, + 'cache.operation': { type: 'string', value: 'remove' }, 'sentry.origin': { type: 'string', value: 'auto.cache.nitro' }, 'db.operation.name': { type: 'string', value: 'clear' }, 'db.system.name': { type: 'string', value: expect.any(String) },