From 1df16ed114f415e8b50ac25733de4c5c2eb5a245 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 7 Sep 2026 11:21:30 +0200 Subject: [PATCH] test(e2e): Migrate Hono to span streaming Co-Authored-By: GPT-6 --- .../test-applications/hono-4/src/entry.bun.ts | 1 - .../hono-4/src/entry.cloudflare.ts | 1 - .../hono-4/src/entry.deno.ts | 1 - .../hono-4/src/instrument.node.ts | 1 - .../tests/basepath-and-late-routes.test.ts | 69 +++-- .../hono-4/tests/errors.test.ts | 129 +++++--- .../hono-4/tests/middleware.test.ts | 293 +++++++++++------- .../hono-4/tests/multi-fetch.test.ts | 288 ++++++++++------- .../hono-4/tests/route-patterns.test.ts | 161 ++++++---- .../hono-4/tests/tracing.test.ts | 155 ++++----- 10 files changed, 644 insertions(+), 455 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts index e79fbf71866f..e057eb78d4c5 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts @@ -6,7 +6,6 @@ const app = new Hono(); app.use( sentry(app, { - traceLifecycle: 'static', dsn: process.env.E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts index e62a66d14a13..e348dde56226 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts @@ -6,7 +6,6 @@ const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>(); app.use( sentry(app, env => ({ - traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts index 029e0c95a414..15bd12a74111 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.deno.ts @@ -6,7 +6,6 @@ const app = new Hono(); app.use( sentry(app, { - traceLifecycle: 'static', dsn: Deno.env.get('E2E_TEST_DSN'), environment: 'qa', dataCollection: {}, diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts index 03b74f8ff7c1..82f2a3864125 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/hono/node'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts index c22897c4a349..c7ad43b6b945 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/basepath-and-late-routes.test.ts @@ -1,12 +1,14 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; test.describe('basePath with sub-app routes', () => { test('traces GET on a sub-app mounted via .basePath().route()', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-basepath/v1/users'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-basepath/v1/users', + ); const response = await fetch(`${baseURL}/test-basepath/v1/users`); expect(response.status).toBe(200); @@ -14,15 +16,19 @@ test.describe('basePath with sub-app routes', () => { const body = await response.json(); expect(body).toEqual({ users: [{ id: 1, name: 'Alice' }] }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /test-basepath/v1/users'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe('GET /test-basepath/v1/users'); + expect(getSpanOp(segment)).toBe('http.server'); }); test('traces parameterized route under .basePath().route()', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-basepath/v1/users/:userId'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === 'GET /test-basepath/v1/users/:userId', + ); const response = await fetch(`${baseURL}/test-basepath/v1/users/42`); expect(response.status).toBe(200); @@ -30,18 +36,19 @@ test.describe('basePath with sub-app routes', () => { const body = await response.json(); expect(body).toEqual({ userId: '42' }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /test-basepath/v1/users/:userId'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe('GET /test-basepath/v1/users/:userId'); + expect(getSpanOp(segment)).toBe('http.server'); }); }); // TODO: this test is currently skipped because we do not yet support middleware registered on new instances (e.g. here via .basePath(..).use(...)). test.skip('.basePath() middleware instrumentation', () => { test('creates middleware span for .use() on .basePath() clone', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-basepath-mw/hello'; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-basepath-mw/hello', + ); const response = await fetch(`${baseURL}/test-basepath-mw/hello`); expect(response.status).toBe(200); @@ -49,24 +56,28 @@ test.skip('.basePath() middleware instrumentation', () => { const body = await response.json(); expect(body).toEqual({ greeting: 'world' }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /test-basepath-mw/hello'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-basepath-mw/hello', + )!; + expect(segment.name).toBe('GET /test-basepath-mw/hello'); - const spans = transaction.spans || []; - const middlewareSpan = spans.find( - (span: { description?: string; op?: string }) => - span.op === 'middleware' && span.description === 'basepathMiddleware', + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, ); + const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'basepathMiddleware'); expect(middlewareSpan).toBeDefined(); - expect(middlewareSpan?.origin).toBe('auto.middleware.hono'); + expect(middlewareSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono'); }); }); test('traces .get() route registered after .basePath()/.route() chains', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-late-get'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-late-get', + ); const response = await fetch(`${baseURL}/test-late-get`); expect(response.status).toBe(200); @@ -74,7 +85,7 @@ test('traces .get() route registered after .basePath()/.route() chains', async ( const body = await response.json(); expect(body).toEqual({ registered: 'after-chains' }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /test-late-get'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe('GET /test-late-get'); + expect(getSpanOp(segment)).toBe('http.server'); }); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts index 06ab4b4b42dc..e3c44c67d4d6 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts @@ -1,5 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { + waitForError, + waitForStreamedSpan, + getSpanOp, + collectStreamedSpansUntilSegment, +} from '@sentry-internal/test-utils'; import { APP_NAME, RUNTIME } from './constants'; test.describe('route handler errors', () => { @@ -8,17 +13,18 @@ test.describe('route handler errors', () => { return event.exception?.values?.[0]?.value === 'This is a test error for Sentry!'; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/error/'); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/error/'), + ); const response = await fetch(`${baseURL}/error/test-cause`); expect(response.status).toBe(500); const errorEvent = await errorPromise; - const transactionEvent = await transactionPromise; + const segmentEvent = await segmentPromise; - expect(transactionEvent.transaction).toBe('GET /error/:cause'); + expect(segmentEvent.name).toBe('GET /error/:cause'); expect(errorEvent.exception?.values).toHaveLength(1); @@ -34,7 +40,7 @@ test.describe('route handler errors', () => { expect(errorEvent.request?.url).toContain('/error/test-cause'); expect(errorEvent.request?.headers).toBeDefined(); - expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); + expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentEvent?.trace_id); }); test('captures three linked errors', async ({ baseURL }) => { @@ -110,11 +116,14 @@ test.describe('HTTPException errors', () => { return false; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return RUNTIME === 'cloudflare' - ? event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/http-exception/') - : event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + (RUNTIME === 'cloudflare' + ? getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/http-exception/') + : getSpanOp(segment) === 'http.server' && segment.name === 'GET /'), + ); const response = await fetch(`${baseURL}/http-exception/${code}`, { redirect: 'manual' }); expect(response.status).toBe(code); @@ -124,10 +133,10 @@ test.describe('HTTPException errors', () => { await fetch(`${baseURL}/`); } - const transaction = await transactionPromise; + const segment = await segmentPromise; if (RUNTIME === 'cloudflare') { - expect(transaction.transaction).toBe('GET /http-exception/:code'); + expect(segment.name).toBe('GET /http-exception/:code'); } expect(errorEventOccurred).toBe(false); @@ -145,11 +154,14 @@ test.describe('HTTPException errors', () => { return false; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return RUNTIME === 'cloudflare' - ? event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/http-exception/') - : event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + (RUNTIME === 'cloudflare' + ? getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/http-exception/') + : getSpanOp(segment) === 'http.server' && segment.name === 'GET /'), + ); const response = await fetch(`${baseURL}/http-exception/${code}`); expect(response.status).toBe(code); @@ -159,10 +171,10 @@ test.describe('HTTPException errors', () => { await fetch(`${baseURL}/`); } - const transaction = await transactionPromise; + const segment = await segmentPromise; if (RUNTIME === 'cloudflare') { - expect(transaction.transaction).toBe('GET /http-exception/:code'); + expect(segment.name).toBe('GET /http-exception/:code'); } expect(errorEventOccurred).toBe(false); @@ -176,12 +188,11 @@ test.describe('middleware errors', () => { return event.exception?.values?.[0]?.value === 'Service Unavailable from middleware'; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - !!event.transaction?.includes('/test-errors/middleware-http-exception') - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => + getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception'), + ); const response = await fetch(`${baseURL}/test-errors/middleware-http-exception`); expect(response.status).toBe(503); @@ -192,9 +203,17 @@ test.describe('middleware errors', () => { expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false); expect(errorEvent.transaction).toBe('GET /test-errors/middleware-http-exception'); - const transaction = await transactionPromise; - const middlewareSpan = (transaction.spans || []).find(s => s.op === 'middleware'); - expect(middlewareSpan?.status).toBe('internal_error'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + !!segment.name?.includes('/test-errors/middleware-http-exception'), + )!; + const middlewareSpan = segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .find(s => getSpanOp(s) === 'middleware'); + expect(middlewareSpan?.status).toBe('error'); }); test('does not capture 4xx HTTPException thrown in middleware', async ({ baseURL }) => { @@ -207,14 +226,13 @@ test.describe('middleware errors', () => { return false; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { + const segmentPromise = collectStreamedSpansUntilSegment(APP_NAME, segment => { if (RUNTIME === 'cloudflare') { return ( - event.contexts?.trace?.op === 'http.server' && - !!event.transaction?.includes('/test-errors/middleware-http-exception-4xx') + getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception-4xx') ); } - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /'; + return getSpanOp(segment) === 'http.server' && segment.name === 'GET /'; }); const response = await fetch(`${baseURL}/test-errors/middleware-http-exception-4xx`); @@ -224,13 +242,24 @@ test.describe('middleware errors', () => { await fetch(`${baseURL}/`); } - const transaction = await transactionPromise; + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find(segment => { + if (!segment.is_segment) return false; + if (RUNTIME === 'cloudflare') { + return ( + getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-errors/middleware-http-exception-4xx') + ); + } + return getSpanOp(segment) === 'http.server' && segment.name === 'GET /'; + })!; if (RUNTIME === 'cloudflare') { - expect(transaction.transaction).toBe('GET /test-errors/middleware-http-exception-4xx'); + expect(segment.name).toBe('GET /test-errors/middleware-http-exception-4xx'); - const middlewareSpan = (transaction.spans || []).find(s => s.op === 'middleware'); - expect(middlewareSpan?.status).not.toBe('internal_error'); + const middlewareSpan = segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .find(s => getSpanOp(s) === 'middleware'); + expect(middlewareSpan?.status).not.toBe('error'); } expect(errorEventOccurred).toBe(false); @@ -243,17 +272,19 @@ test.describe('nested sub-app errors', () => { return event.exception?.values?.[0]?.value === 'Nested child app error'; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/nested/child/error'); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/nested/child/error'), + ); const response = await fetch(`${baseURL}/test-errors/nested/child/error`); expect(response.status).toBe(500); const errorEvent = await errorPromise; - const transaction = await transactionPromise; + const segment = await segmentPromise; - expect(transaction.transaction).toBe('GET /test-errors/nested/child/error'); + expect(segment.name).toBe('GET /test-errors/nested/child/error'); expect(errorEvent.exception?.values?.[0]?.value).toBe('Nested child app error'); expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ @@ -272,9 +303,11 @@ test.describe('custom onError handler', () => { return event.exception?.values?.[0]?.value === 'Error caught by custom onError'; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/custom-on-error/fail'); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/custom-on-error/fail'), + ); const response = await fetch(`${baseURL}/test-errors/custom-on-error/fail`); expect(response.status).toBe(500); @@ -283,9 +316,9 @@ test.describe('custom onError handler', () => { expect(body).toContain('Handled by onError'); const errorEvent = await errorPromise; - const transaction = await transactionPromise; + const segment = await segmentPromise; - expect(transaction.transaction).toBe('GET /test-errors/custom-on-error/fail'); + expect(segment.name).toBe('GET /test-errors/custom-on-error/fail'); expect(errorEvent.exception?.values?.[0]?.value).toBe('Error caught by custom onError'); expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts index 94d7c67036f8..111ff3bfe34a 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -import { type SpanJSON } from '@sentry/core'; +import { waitForError, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const SCENARIOS = [ @@ -17,83 +16,99 @@ const SCENARIOS = [ for (const { name, prefix } of SCENARIOS) { test.describe(name, () => { test('creates a span for named middleware', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/named`); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/named`), + ); const response = await fetch(`${baseURL}${prefix}/named`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/named`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/named`), + )!; + expect(segment.name).toBe(`GET ${prefix}/named`); - const spans = transaction.spans || []; - - const middlewareSpan = spans.find( - (span: { description?: string; op?: string }) => span.op === 'middleware' && span.description === 'middlewareA', + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, ); + const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'middlewareA'); + expect(middlewareSpan).toEqual( expect.objectContaining({ - description: 'middlewareA', - op: 'middleware', - origin: 'auto.middleware.hono', + name: 'middlewareA', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'middleware', type: 'string' }, + 'sentry.origin': { value: 'auto.middleware.hono', type: 'string' }, + }), }), ); - expect(middlewareSpan?.status).not.toBe('internal_error'); + expect(middlewareSpan?.status).not.toBe('error'); - // @ts-expect-error timestamp is defined - const durationMs = (middlewareSpan?.timestamp - middlewareSpan?.start_timestamp) * 1000; + const durationMs = (middlewareSpan!.end_timestamp - middlewareSpan!.start_timestamp) * 1000; expect(durationMs).toBeGreaterThanOrEqual(49); }); test('creates a span for anonymous middleware', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/anonymous`); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/anonymous`), + ); const response = await fetch(`${baseURL}${prefix}/anonymous`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/anonymous`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/anonymous`), + )!; + expect(segment.name).toBe(`GET ${prefix}/anonymous`); - const spans = transaction.spans || []; - - const anonymousSpan = spans.find( - (span: { description?: string; op?: string }) => span.op === 'middleware' && span.description === '', + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, ); + + const anonymousSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === ''); expect(anonymousSpan).toBeDefined(); - expect(anonymousSpan?.origin).toBe('auto.middleware.hono'); - expect(anonymousSpan?.status).not.toBe('internal_error'); + expect(anonymousSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono'); + expect(anonymousSpan?.status).not.toBe('error'); }); test('multiple middleware are sibling spans under the same parent', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/multi`); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/multi`), + ); const response = await fetch(`${baseURL}${prefix}/multi`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/multi`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/multi`), + )!; + expect(segment.name).toBe(`GET ${prefix}/multi`); - const spans = transaction.spans || []; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); const middlewareSpans = spans.sort((a, b) => (a.start_timestamp ?? 0) - (b.start_timestamp ?? 0)); expect(middlewareSpans).toHaveLength(2); - expect(middlewareSpans[0]?.description).toBe('middlewareA'); - expect(middlewareSpans[1]?.description).toBe('middlewareB'); + expect(middlewareSpans[0].name).toBe('middlewareA'); + expect(middlewareSpans[1].name).toBe('middlewareB'); expect(middlewareSpans[0]?.parent_span_id).toBe(middlewareSpans[1]?.parent_span_id); // middlewareA has a 50ms delay, middlewareB has a 60ms delay - // @ts-expect-error timestamp is defined - const aDurationMs = (middlewareSpans[0]?.timestamp - middlewareSpans[0]?.start_timestamp) * 1000; - // @ts-expect-error timestamp is defined - const bDurationMs = (middlewareSpans[1]?.timestamp - middlewareSpans[1]?.start_timestamp) * 1000; + const aDurationMs = (middlewareSpans[0].end_timestamp - middlewareSpans[0]?.start_timestamp) * 1000; + const bDurationMs = (middlewareSpans[1].end_timestamp - middlewareSpans[1]?.start_timestamp) * 1000; expect(aDurationMs).toBeGreaterThanOrEqual(49); expect(bDurationMs).toBeGreaterThanOrEqual(59); }); @@ -120,38 +135,50 @@ for (const { name, prefix } of SCENARIOS) { }); test('sets error status on middleware span when middleware throws', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/error`); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`), + ); await fetch(`${baseURL}${prefix}/error`); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/error`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`), + )!; + expect(segment.name).toBe(`GET ${prefix}/error`); - const spans = transaction.spans || []; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); - const failingSpan = spans.find((span: SpanJSON) => span.op === 'middleware' && span.status === 'internal_error'); + const failingSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.status === 'error'); expect(failingSpan).toBeDefined(); - expect(failingSpan?.status).toBe('internal_error'); + expect(failingSpan?.status).toBe('error'); }); - test('uses parameterized route in transaction name', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/param/`); - }); + test('uses parameterized route in span name', async ({ baseURL }) => { + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/param/`), + ); const response = await fetch(`${baseURL}${prefix}/param/42`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/param/:id`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/param/`), + )!; + expect(segment.name).toBe(`GET ${prefix}/param/:id`); - const spans = transaction.spans || []; - const middlewareSpan = spans.find( - (span: { description?: string; op?: string }) => span.op === 'middleware' && span.description === 'middlewareA', + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, ); + const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'middlewareA'); expect(middlewareSpan).toBeDefined(); }); @@ -175,12 +202,11 @@ for (const { name, prefix } of SCENARIOS) { test.describe('.all() handler in sub-app', () => { test('does not create middleware span for .all() route handler', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - !!event.transaction?.includes('/test-subapp-middleware/all-handler') - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => + getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-subapp-middleware/all-handler'), + ); const response = await fetch(`${baseURL}/test-subapp-middleware/all-handler`); expect(response.status).toBe(200); @@ -188,10 +214,18 @@ test.describe('.all() handler in sub-app', () => { const body = await response.json(); expect(body).toEqual({ handler: 'all' }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /test-subapp-middleware/all-handler'); - - const spans = transaction.spans || []; + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + !!segment.name?.includes('/test-subapp-middleware/all-handler'), + )!; + expect(segment.name).toBe('GET /test-subapp-middleware/all-handler'); + + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); // No middleware is called for this route, so there should be no spans. expect(spans).toEqual([]); @@ -217,15 +251,19 @@ test.describe('inline middleware spans (sub-app)', () => { test(`creates middleware span for ${mwName} middleware via ${regName}`, async ({ baseURL }) => { const fullPath = `${INLINE_PREFIX}${regPath}${mwPath}`; - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(fullPath); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(fullPath), + ); const response = await fetch(`${baseURL}${fullPath}`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${fullPath}`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(fullPath), + )!; + expect(segment.name).toBe(`GET ${fullPath}`); const EXPECTED_DESCRIPTIONS: Record> = { '/direct': { '': 'inlineMiddleware', '/separately': 'inlineSeparateMiddleware' }, @@ -234,11 +272,13 @@ test.describe('inline middleware spans (sub-app)', () => { }; const expectedDescription = EXPECTED_DESCRIPTIONS[regPath]![mwPath]!; - const inlineSpan = (transaction.spans || []).find(s => s.description === expectedDescription); + const inlineSpan = segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .find(s => s.name === expectedDescription); expect(inlineSpan).toBeDefined(); - expect(inlineSpan?.op).toBe('middleware'); - expect(inlineSpan?.origin).toBe('auto.middleware.hono'); - expect(inlineSpan?.status).not.toBe('internal_error'); + expect(getSpanOp(inlineSpan!)).toBe('middleware'); + expect(inlineSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono'); + expect(inlineSpan?.status).not.toBe('error'); }); } } @@ -255,9 +295,10 @@ const MAIN_INLINE_CASES = [ test.describe('inline middleware spans (main app)', () => { test('creates middleware span for inline middleware via .query()', async ({ baseURL }) => { const fullPath = `${MAIN_INLINE_PREFIX}/query`; - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `QUERY ${fullPath}`; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `QUERY ${fullPath}`, + ); const response = await fetch(`${baseURL}${fullPath}`, { method: 'QUERY', @@ -267,19 +308,26 @@ test.describe('inline middleware spans (main app)', () => { expect(response.status).toBe(200); await expect(response.json()).resolves.toEqual({ method: 'QUERY', value: 'query-body' }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`QUERY ${fullPath}`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.transaction_info?.source).toBe('route'); - expect(transaction.request?.method).toBe('QUERY'); - - const middlewareSpans = (transaction.spans || []).filter(span => span.op === 'middleware'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `QUERY ${fullPath}`, + )!; + expect(segment.name).toBe(`QUERY ${fullPath}`); + expect(getSpanOp(segment)).toBe('http.server'); + expect(segment.attributes['sentry.segment.name.source']?.value).toBe('route'); + expect(segment.attributes['http.request.method']?.value).toBe('QUERY'); + + const middlewareSpans = segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .filter(span => getSpanOp(span) === 'middleware'); expect(middlewareSpans).toHaveLength(1); expect(middlewareSpans[0]).toEqual( expect.objectContaining({ - description: 'mainInlineQuery', - op: 'middleware', - origin: 'auto.middleware.hono', + name: 'mainInlineQuery', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'middleware', type: 'string' }, + 'sentry.origin': { value: 'auto.middleware.hono', type: 'string' }, + }), }), ); }); @@ -288,25 +336,32 @@ test.describe('inline middleware spans (main app)', () => { test(`creates middleware span for inline middleware via ${name}`, async ({ baseURL }) => { const fullPath = `${MAIN_INLINE_PREFIX}${path}`; - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `${method} ${fullPath}`; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `${method} ${fullPath}`, + ); const response = await fetch(`${baseURL}${fullPath}`, { method }); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`${method} ${fullPath}`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `${method} ${fullPath}`, + )!; + expect(segment.name).toBe(`${method} ${fullPath}`); - const spans = transaction.spans || []; - const inlineSpan = spans.find(s => s.description === expectedMiddlewareName); + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); + const inlineSpan = spans.find(s => s.name === expectedMiddlewareName); expect(inlineSpan).toBeDefined(); - expect(inlineSpan?.op).toBe('middleware'); - expect(inlineSpan?.origin).toBe('auto.middleware.hono'); - expect(inlineSpan?.status).not.toBe('internal_error'); + expect(getSpanOp(inlineSpan!)).toBe('middleware'); + expect(inlineSpan?.attributes['sentry.origin']?.value).toBe('auto.middleware.hono'); + expect(inlineSpan?.status).not.toBe('error'); - const middlewareSpans = spans.filter(s => s.op === 'middleware'); + const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toHaveLength(1); }); }); @@ -314,30 +369,36 @@ test.describe('inline middleware spans (main app)', () => { test('creates spans for both .use() middleware and inline middleware via .get()', async ({ baseURL }) => { const fullPath = `${MAIN_INLINE_PREFIX}/combined/resource`; - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${fullPath}`; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${fullPath}`, + ); const response = await fetch(`${baseURL}${fullPath}`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${fullPath}`); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${fullPath}`, + )!; + expect(segment.name).toBe(`GET ${fullPath}`); - const spans = transaction.spans || []; - const middlewareSpans = spans.filter(s => s.op === 'middleware'); + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); + const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toHaveLength(2); - const [spanA, spanB] = middlewareSpans.sort((a, b) => (a.description ?? '').localeCompare(b.description ?? '')); - expect(spanA?.description).toBe('combinedInlineMw'); - expect(spanA?.op).toBe('middleware'); - expect(spanA?.origin).toBe('auto.middleware.hono'); - expect(spanA?.status).not.toBe('internal_error'); + const [spanA, spanB] = middlewareSpans.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); + expect(spanA.name).toBe('combinedInlineMw'); + expect(getSpanOp(spanA!)).toBe('middleware'); + expect(spanA.attributes['sentry.origin']?.value).toBe('auto.middleware.hono'); + expect(spanA?.status).not.toBe('error'); - expect(spanB?.description).toBe('middlewareA'); - expect(spanB?.op).toBe('middleware'); - expect(spanB?.origin).toBe('auto.middleware.hono'); - expect(spanB?.status).not.toBe('internal_error'); + expect(spanB.name).toBe('middlewareA'); + expect(getSpanOp(spanB!)).toBe('middleware'); + expect(spanB.attributes['sentry.origin']?.value).toBe('auto.middleware.hono'); + expect(spanB?.status).not.toBe('error'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts index 3904a02ebc30..29298c7b8f89 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/multi-fetch.test.ts @@ -1,5 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { + waitForError, + waitForStreamedSpan, + getSpanOp, + collectStreamedSpansUntilSegment, +} from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const STOREFRONT = '/test-multi-fetch/storefront'; @@ -7,12 +12,14 @@ const INVENTORY = '/test-multi-fetch/inventory'; test.describe('multi-fetch: internal .request() calls between sub-apps', () => { test.describe('single internal fetch', () => { - test('returns enriched product data and creates transaction with parameterized route', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${STOREFRONT}/product/:productId` - ); - }); + test('returns enriched product data and creates span with parameterized route', async ({ baseURL }) => { + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product/:productId`, + ); const response = await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant`); expect(response.status).toBe(200); @@ -28,48 +35,55 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { ); expect(body.source).toBe('storefront'); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${STOREFRONT}/product/:productId`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe(`GET ${STOREFRONT}/product/:productId`); + expect(getSpanOp(segment)).toBe('http.server'); }); test('creates storefrontAuth middleware span', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${STOREFRONT}/product/:productId` - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product/:productId`, + ); const response = await fetch(`${baseURL}${STOREFRONT}/product/solar-powered-cyberdeck`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - const spans = transaction.spans || []; - - const middlewareSpan = spans.find( - (span: { description?: string; op?: string }) => - span.op === 'middleware' && span.description === 'storefrontAuth', + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product/:productId`, + )!; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, ); + const middlewareSpan = spans.find(span => getSpanOp(span) === 'middleware' && span.name === 'storefrontAuth'); + expect(middlewareSpan).toEqual( expect.objectContaining({ - description: 'storefrontAuth', - op: 'middleware', - origin: 'auto.middleware.hono', + name: 'storefrontAuth', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'middleware', type: 'string' }, + 'sentry.origin': { value: 'auto.middleware.hono', type: 'string' }, + }), }), ); - expect(middlewareSpan?.status).not.toBe('internal_error'); + expect(middlewareSpan?.status).not.toBe('error'); }); }); test.describe('parallel internal fetches', () => { test('aggregates data from two concurrent .request() calls', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/compare/:productId1/:productId2` - ); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/compare/:productId1/:productId2`, + ); const response = await fetch(`${baseURL}${STOREFRONT}/compare/self-watering-plant/solar-powered-cyberdeck`); expect(response.status).toBe(200); @@ -80,19 +94,20 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { expect(body.items[1].productId).toBe('solar-powered-cyberdeck'); expect(body.priceDifference).toBe(2500); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${STOREFRONT}/compare/:productId1/:productId2`); + const segment = await segmentPromise; + expect(segment.name).toBe(`GET ${STOREFRONT}/compare/:productId1/:productId2`); }); }); test.describe('sequential chained fetches', () => { test('composes data from item lookup followed by stock check', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/product/:productId/availability` - ); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product/:productId/availability`, + ); const response = await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant/availability`); expect(response.status).toBe(200); @@ -100,18 +115,19 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { const body = await response.json(); expect(body).toEqual({ product: 'Self-Watering Plant', available: true, quantity: 5 }); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${STOREFRONT}/product/:productId/availability`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe(`GET ${STOREFRONT}/product/:productId/availability`); + expect(getSpanOp(segment)).toBe('http.server'); }); test('reports out-of-stock item as unavailable', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/product/:productId/availability` - ); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product/:productId/availability`, + ); const response = await fetch(`${baseURL}${STOREFRONT}/product/solar-powered-cyberdeck/availability`); expect(response.status).toBe(200); @@ -119,7 +135,7 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { const body = await response.json(); expect(body).toEqual({ product: 'Solar-Powered Cyberdeck', available: false, quantity: 0 }); - await transactionPromise; + await segmentPromise; }); }); @@ -129,12 +145,13 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { return event.exception?.values?.[0]?.value === 'Failed to fetch product: nonexistent'; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/product-or-throw/:productId` - ); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`, + ); const response = await fetch(`${baseURL}${STOREFRONT}/product-or-throw/nonexistent`); expect(response.status).toBe(500); @@ -144,9 +161,9 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(expect.objectContaining({ handled: false })); expect(errorEvent.transaction).toBe(`GET ${STOREFRONT}/product-or-throw/:productId`); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${STOREFRONT}/product-or-throw/:productId`); - expect(transaction.contexts?.trace?.status).toBe('internal_error'); + const segment = await segmentPromise; + expect(segment.name).toBe(`GET ${STOREFRONT}/product-or-throw/:productId`); + expect(segment?.status).toBe('error'); }); test('error event includes request data', async ({ baseURL }) => { @@ -167,10 +184,14 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { }); test.describe('inventory sub-app direct access', () => { - test('creates its own transaction when accessed directly via HTTP', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${INVENTORY}/item/:productId`; - }); + test('creates its own span when accessed directly via HTTP', async ({ baseURL }) => { + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${INVENTORY}/item/:productId`, + ); const response = await fetch(`${baseURL}${INVENTORY}/item/self-watering-plant`); expect(response.status).toBe(200); @@ -178,57 +199,73 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { const body = await response.json(); expect(body).toEqual(expect.objectContaining({ productId: 'self-watering-plant', name: 'Self-Watering Plant' })); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${INVENTORY}/item/:productId`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe(`GET ${INVENTORY}/item/:productId`); + expect(getSpanOp(segment)).toBe('http.server'); }); }); test.describe('trace propagation through internal .request() calls', () => { test('single internal fetch produces an internal-request child span', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${STOREFRONT}/product/:productId` - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product/:productId`, + ); await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant`); - const transaction = await transactionPromise; - const traceId = transaction.contexts?.trace?.trace_id; - const spans = transaction.spans || []; + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product/:productId`, + )!; + const traceId = segment?.trace_id; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); const internalRequestSpans = spans.filter( - (s: { origin?: string }) => s.origin === 'auto.http.hono.internal_request', + s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request', ); expect(internalRequestSpans).toHaveLength(1); expect(internalRequestSpans[0]).toEqual( expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.hono.internal_request', trace_id: traceId, + attributes: expect.objectContaining({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.hono.internal_request', type: 'string' }, + }), }), ); - expect(internalRequestSpans[0]?.description).toContain('GET /item/self-watering-plant'); + expect(internalRequestSpans[0].name).toContain('GET /item/self-watering-plant'); }); test('parallel internal fetches produce two sibling internal-request spans', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/compare/:productId1/:productId2` - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => + getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/compare/:productId1/:productId2`, + ); await fetch(`${baseURL}${STOREFRONT}/compare/self-watering-plant/solar-powered-cyberdeck`); - const transaction = await transactionPromise; - const traceId = transaction.contexts?.trace?.trace_id; - const spans = transaction.spans || []; + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/compare/:productId1/:productId2`, + )!; + const traceId = segment?.trace_id; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); const internalRequestSpans = spans.filter( - (s: { origin?: string }) => s.origin === 'auto.http.hono.internal_request', + s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request', ); expect(internalRequestSpans).toHaveLength(2); @@ -238,26 +275,33 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { expect(internalRequestSpans[0]?.trace_id).toBe(traceId); expect(internalRequestSpans[1]?.trace_id).toBe(traceId); - expect(internalRequestSpans[0]?.origin).toBe('auto.http.hono.internal_request'); - expect(internalRequestSpans[1]?.origin).toBe('auto.http.hono.internal_request'); + expect(internalRequestSpans[0].attributes['sentry.origin']?.value).toBe('auto.http.hono.internal_request'); + expect(internalRequestSpans[1].attributes['sentry.origin']?.value).toBe('auto.http.hono.internal_request'); }); test('sequential chained fetches produce two ordered internal-request spans', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/product/:productId/availability` - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => + getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product/:productId/availability`, + ); await fetch(`${baseURL}${STOREFRONT}/product/self-watering-plant/availability`); - const transaction = await transactionPromise; - const traceId = transaction.contexts?.trace?.trace_id; - const spans = transaction.spans || []; + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product/:productId/availability`, + )!; + const traceId = segment?.trace_id; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); const internalRequestSpans = spans - .filter((s: { origin?: string }) => s.origin === 'auto.http.hono.internal_request') + .filter(s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request') .sort( (a: { start_timestamp?: number }, b: { start_timestamp?: number }) => (a.start_timestamp ?? 0) - (b.start_timestamp ?? 0), @@ -267,7 +311,7 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { // Sequential: second span starts at or after first span ends (with tolerance for clock precision) expect(internalRequestSpans[1].start_timestamp).toBeGreaterThanOrEqual( - internalRequestSpans[0].timestamp! - 0.001, + internalRequestSpans[0].end_timestamp! - 0.001, ); expect(internalRequestSpans[0]?.trace_id).toBe(traceId); @@ -275,24 +319,31 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { }); test('internal-request span has no error status for internal 4xx HTTPException', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/product-or-throw/:productId` - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => + getSpanOp(segment) === 'http.server' && segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`, + ); await fetch(`${baseURL}${STOREFRONT}/product-or-throw/ghost`); - const transaction = await transactionPromise; - const spans = transaction.spans || []; + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`, + )!; + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); const internalRequestSpans = spans.filter( - (s: { origin?: string }) => s.origin === 'auto.http.hono.internal_request', + s => s.attributes['sentry.origin']?.value === 'auto.http.hono.internal_request', ); expect(internalRequestSpans).toHaveLength(1); - expect(internalRequestSpans[0]?.status).not.toBe('internal_error'); + expect(internalRequestSpans[0]?.status).not.toBe('error'); }); test('error from failed internal fetch is correlated with the storefront trace', async ({ baseURL }) => { @@ -300,18 +351,19 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => { return event.exception?.values?.[0]?.value === 'Failed to fetch product: ghost'; }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${STOREFRONT}/product-or-throw/:productId` - ); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`, + ); await fetch(`${baseURL}${STOREFRONT}/product-or-throw/ghost`); - const [errorEvent, transaction] = await Promise.all([errorPromise, transactionPromise]); + const [errorEvent, segment] = await Promise.all([errorPromise, segmentPromise]); - expect(errorEvent.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id); + expect(errorEvent.contexts?.trace?.trace_id).toBe(segment?.trace_id); expect(errorEvent.contexts?.trace?.span_id).toBeDefined(); }); }); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts index 58a661d3c7bc..2869530da624 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const PREFIX = '/test-routes'; @@ -13,20 +13,26 @@ const REGISTRATION_STYLES = [ test.describe('HTTP methods', () => { ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => { test(`sends transaction for ${method}`, async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `${method} ${PREFIX}`; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `${method} ${PREFIX}`, + ); const response = await fetch(`${baseURL}${PREFIX}`, { method }); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`${method} ${PREFIX}`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.contexts?.trace?.data?.['sentry.segment.name.source']).toBe('route'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `${method} ${PREFIX}`, + )!; + expect(segment.name).toBe(`${method} ${PREFIX}`); + expect(getSpanOp(segment)).toBe('http.server'); + expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route'); - const spans = transaction.spans || []; - const middlewareSpans = spans.filter(s => s.op === 'middleware'); + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); + const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toEqual([]); }); }); @@ -35,20 +41,27 @@ test.describe('HTTP methods', () => { test.describe('route registration styles', () => { REGISTRATION_STYLES.forEach(({ name, path }) => { test(`${name} sends transaction with route source`, async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${PREFIX}${path}`; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}${path}`, + ); const response = await fetch(`${baseURL}${PREFIX}${path}`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${PREFIX}${path}`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.contexts?.trace?.data?.['sentry.segment.name.source']).toBe('route'); - - const spans = transaction.spans || []; - const middlewareSpans = spans.filter(s => s.op === 'middleware'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}${path}`, + )!; + expect(segment.name).toBe(`GET ${PREFIX}${path}`); + expect(getSpanOp(segment)).toBe('http.server'); + expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route'); + + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); + const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toEqual([]); }); }); @@ -58,62 +71,74 @@ test.describe('route registration styles', () => { { name: '.on()', path: '/on' }, ].forEach(({ name, path }) => { test(`${name} responds to POST`, async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `POST ${PREFIX}${path}`; - }); + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `POST ${PREFIX}${path}`, + ); const response = await fetch(`${baseURL}${PREFIX}${path}`, { method: 'POST' }); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`POST ${PREFIX}${path}`); - expect(transaction.contexts?.trace?.data?.['sentry.segment.name.source']).toBe('route'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `POST ${PREFIX}${path}`, + )!; + expect(segment.name).toBe(`POST ${PREFIX}${path}`); + expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route'); - const spans = transaction.spans || []; - const middlewareSpans = spans.filter(s => s.op === 'middleware'); + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); + const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toEqual([]); }); }); }); test.describe('request data extraction', () => { - test('includes method, url, and headers on transaction', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${PREFIX}/request-data`; - }); + test('includes method, url, and headers on span', async ({ baseURL }) => { + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}/request-data`, + ); const response = await fetch(`${baseURL}${PREFIX}/request-data`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.request?.method).toBe('GET'); - expect(transaction.request?.url).toContain(PREFIX); - expect(transaction.request?.headers).toBeDefined(); + const segment = await segmentPromise; + expect(segment.attributes['http.request.method']?.value).toBe('GET'); + expect(segment.attributes['url.full']?.value).toContain(PREFIX); + expect(segment.attributes['http.request.header.host']).toBeDefined(); }); test('includes query_string when present', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return ( - event.contexts?.trace?.op === 'http.server' && - event.transaction === `GET ${PREFIX}/query-test` && - event.request?.query_string === 'foo=bar&baz=42' - ); - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === `GET ${PREFIX}/query-test` && + segment.attributes['url.query']?.value === 'foo=bar&baz=42', + ); const response = await fetch(`${baseURL}${PREFIX}/query-test?foo=bar&baz=42`); expect(response.status).toBe(200); - const transaction = await transactionPromise; + const segment = await segmentPromise; - expect(transaction.request?.method).toBe('GET'); - expect(transaction.request?.url).toContain(`${PREFIX}/query-test`); - expect(transaction.request?.query_string).toBe('foo=bar&baz=42'); + expect(segment.attributes['http.request.method']?.value).toBe('GET'); + expect(segment.attributes['url.full']?.value).toContain(`${PREFIX}/query-test`); + expect(segment.attributes['url.query']?.value).toBe('foo=bar&baz=42'); }); test('includes request data for POST with headers', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `POST ${PREFIX}/request-data`; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => + segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `POST ${PREFIX}/request-data`, + ); const response = await fetch(`${baseURL}${PREFIX}/request-data`, { method: 'POST', @@ -121,27 +146,33 @@ test.describe('request data extraction', () => { }); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.request?.method).toBe('POST'); - expect(transaction.request?.url).toContain(PREFIX); - expect(transaction.request?.headers?.['x-custom-header']).toBe('test-value'); + const segment = await segmentPromise; + expect(segment.attributes['http.request.method']?.value).toBe('POST'); + expect(segment.attributes['url.full']?.value).toContain(PREFIX); + expect(segment.attributes['http.request.header.x_custom_header']?.value).toBe('test-value'); }); }); -test('async handler sends transaction', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === `GET ${PREFIX}/async`; - }); +test('async handler sends span', async ({ baseURL }) => { + const segmentPromise = collectStreamedSpansUntilSegment( + APP_NAME, + segment => getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}/async`, + ); const response = await fetch(`${baseURL}${PREFIX}/async`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${PREFIX}/async`); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.contexts?.trace?.data?.['sentry.segment.name.source']).toBe('route'); - - const spans = transaction.spans || []; - const middlewareSpans = spans.filter(s => s.op === 'middleware'); + const segmentSpans = await segmentPromise; + const segment = segmentSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === `GET ${PREFIX}/async`, + )!; + expect(segment.name).toBe(`GET ${PREFIX}/async`); + expect(getSpanOp(segment)).toBe('http.server'); + expect(segment.attributes?.['sentry.segment.name.source']?.value).toBe('route'); + + const spans = segmentSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id, + ); + const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toEqual([]); }); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts index 2e2d8aa02c85..23ed5c7837fc 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts @@ -1,71 +1,74 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; import { APP_NAME, RUNTIME } from './constants'; -test('sends a transaction for the index route', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /'; - }); +test('sends a span for the index route', async ({ baseURL }) => { + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /', + ); const response = await fetch(`${baseURL}/`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe('GET /'); + expect(getSpanOp(segment)).toBe('http.server'); }); -test('sends a transaction for a parameterized route', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/test-param/'); - }); +test('sends a span for a parameterized route', async ({ baseURL }) => { + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/test-param/'), + ); const response = await fetch(`${baseURL}/test-param/123`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /test-param/:paramId'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const segment = await segmentPromise; + expect(segment.name).toBe('GET /test-param/:paramId'); + expect(getSpanOp(segment)).toBe('http.server'); }); -test('attaches HTTP connection info to the server transaction', async ({ baseURL, page }) => { +test('attaches HTTP connection info to the server span', async ({ baseURL, page }) => { page.on('console', msg => { console.log(`PAGE LOG: ${msg.text()}`); }); - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /', + ); const response = await fetch(`${baseURL}/`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - const data = transaction.contexts?.trace?.data ?? {}; + const segment = await segmentPromise; + const data = segment.attributes ?? {}; - expect(data['client.address']).toEqual(expect.any(String)); - expect(data['network.peer.address']).toBe(data['client.address']); + expect(data['client.address']?.value).toEqual(expect.any(String)); + expect(data['network.peer.address']?.value).toBe(data['client.address']?.value); if (RUNTIME !== 'deno') { // Only exposed in `hono/deno` - expect(data['network.transport']).toBeUndefined(); + expect(data['network.transport']?.value).toBeUndefined(); } else { - expect(data['network.transport']).toMatch(/tcp/); + expect(data['network.transport']?.value).toMatch(/tcp/); } if (RUNTIME === 'node' || RUNTIME === 'bun') { // Node (@hono/node-server) and Bun expose socket-level port and address family. - expect(data['client.port']).toEqual(expect.any(Number)); - expect(data['network.peer.port']).toBe(data['client.port']); - expect(data['network.type']).toMatch(/^ipv[46]$/); + expect(data['client.port']?.value).toEqual(expect.any(Number)); + expect(data['network.peer.port']?.value).toBe(data['client.port']?.value); + expect(data['network.type']?.value).toMatch(/^ipv[46]$/); } else if (RUNTIME === 'deno') { - expect(data['client.port']).toEqual(expect.any(Number)); - expect(data['network.peer.port']).toBe(data['client.port']); + expect(data['client.port']?.value).toEqual(expect.any(Number)); + expect(data['network.peer.port']?.value).toBe(data['client.port']?.value); } else if (RUNTIME === 'cloudflare') { // Cloudflare Workers expose no port, address family, or transport. // This could change in the future and checking for the absence of these fields allows us to notice if/when that happens. - expect(data['client.port']).toBeUndefined(); - expect(data['network.peer.port']).toBeUndefined(); - expect(data['network.type']).toBeUndefined(); + expect(data['client.port']?.value).toBeUndefined(); + expect(data['network.peer.port']?.value).toBeUndefined(); + expect(data['network.type']?.value).toBeUndefined(); } else { throw new Error(`No tests for runtime: ${RUNTIME}`); } @@ -77,63 +80,65 @@ test('attaches HTTP connection info to the server transaction', async ({ baseURL test("preserves the baseline server.*, client.* and network.* server span attributes that the SDK sends without Hono's conninfo", async ({ baseURL, }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /'; - }); + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /', + ); const response = await fetch(`${baseURL}/`); expect(response.status).toBe(200); - const transaction = await transactionPromise; - const data = transaction.contexts?.trace?.data ?? {}; + const segment = await segmentPromise; + const data = segment.attributes ?? {}; if (RUNTIME === 'node') { - expect(data['server.address']).toBe('localhost'); - expect(data['server.port']).toBe(Number(new URL(baseURL!).port)); - expect(data['client.address']).toEqual(expect.any(String)); - expect(data['client.port']).toEqual(expect.any(Number)); - expect(data['network.type']).toMatch(/^ipv[46]$/); - expect(data['network.protocol.name']).toBe('http'); - expect(data['network.protocol.version']).toBe('1.1'); - expect(data['network.transport']).toBeUndefined(); - expect(data['network.local.port']).toBe(data['server.port']); - expect(data['network.local.address']).toEqual(expect.any(String)); - expect(data['network.peer.address']).toBe(data['client.address']); - expect(data['network.peer.port']).toBe(data['client.port']); + expect(data['server.address']?.value).toBe('localhost'); + expect(data['server.port']?.value).toBe(Number(new URL(baseURL!).port)); + expect(data['client.address']?.value).toEqual(expect.any(String)); + expect(data['client.port']?.value).toEqual(expect.any(Number)); + expect(data['network.type']?.value).toMatch(/^ipv[46]$/); + expect(data['network.protocol.name']?.value).toBe('http'); + expect(data['network.protocol.version']?.value).toBe('1.1'); + expect(data['network.transport']?.value).toBeUndefined(); + expect(data['network.local.port']?.value).toBe(data['server.port']?.value); + expect(data['network.local.address']?.value).toEqual(expect.any(String)); + expect(data['network.peer.address']?.value).toBe(data['client.address']?.value); + expect(data['network.peer.port']?.value).toBe(data['client.port']?.value); } else if (RUNTIME === 'bun') { - expect(data['client.address']).toEqual(expect.any(String)); - expect(data['client.port']).toEqual(expect.any(Number)); - expect(data['network.peer.address']).toBe(data['client.address']); - expect(data['network.peer.port']).toBe(data['client.port']); - expect(data['network.type']).toMatch(/^ipv[46]$/); + expect(data['client.address']?.value).toEqual(expect.any(String)); + expect(data['client.port']?.value).toEqual(expect.any(Number)); + expect(data['network.peer.address']?.value).toBe(data['client.address']?.value); + expect(data['network.peer.port']?.value).toBe(data['client.port']?.value); + expect(data['network.type']?.value).toMatch(/^ipv[46]$/); } else if (RUNTIME === 'cloudflare') { - expect(data['server.address']).toBe('localhost'); - expect(data['client.address']).toBe('::1'); - expect(data['network.peer.address']).toBe(data['client.address']); - expect(data['network.protocol.name']).toBe('http'); - expect(data['network.protocol.version']).toBe('1.1'); + expect(data['server.address']?.value).toBe('localhost'); + expect(data['client.address']?.value).toBe('::1'); + expect(data['network.peer.address']?.value).toBe(data['client.address']?.value); + expect(data['network.protocol.name']?.value).toBe('http'); + expect(data['network.protocol.version']?.value).toBe('1.1'); } else if (RUNTIME === 'deno') { - expect(data['server.address']).toBe('localhost'); - expect(data['client.address']).toEqual(expect.any(String)); - expect(data['client.port']).toEqual(expect.any(Number)); - expect(data['network.peer.address']).toBe(data['client.address']); - expect(data['network.peer.port']).toBe(data['client.port']); - expect(data['network.transport']).toBe('tcp'); - expect(data['network.protocol.name']).toBe('http'); + expect(data['server.address']?.value).toBe('localhost'); + expect(data['client.address']?.value).toEqual(expect.any(String)); + expect(data['client.port']?.value).toEqual(expect.any(Number)); + expect(data['network.peer.address']?.value).toBe(data['client.address']?.value); + expect(data['network.peer.port']?.value).toBe(data['client.port']?.value); + expect(data['network.transport']?.value).toBe('tcp'); + expect(data['network.protocol.name']?.value).toBe('http'); } else { throw new Error(`No tests for runtime: ${RUNTIME}`); } }); -test('sends a transaction for a route that throws', async ({ baseURL }) => { - const transactionPromise = waitForTransaction(APP_NAME, event => { - return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes('/error/'); - }); +test('sends a span for a route that throws', async ({ baseURL }) => { + const segmentPromise = waitForStreamedSpan( + APP_NAME, + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('/error/'), + ); await fetch(`${baseURL}/error/test-cause`); - const transaction = await transactionPromise; - expect(transaction.transaction).toBe('GET /error/:cause'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.contexts?.trace?.status).toBe('internal_error'); + const segment = await segmentPromise; + expect(segment.name).toBe('GET /error/:cause'); + expect(getSpanOp(segment)).toBe('http.server'); + expect(segment?.status).toBe('error'); });