From d43375a3ce177ef0c8fa3dd72eb3451edd451cb8 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 7 Sep 2026 11:21:29 +0200 Subject: [PATCH] test(e2e): Migrate Koa and Hapi to span streaming Co-Authored-By: GPT-6 --- .../test-applications/node-hapi/src/app.js | 3 +- .../node-hapi/tests/errors.test.ts | 63 ++- .../node-hapi/tests/spans.test.ts | 149 +++++ .../node-hapi/tests/transactions.test.ts | 150 ----- .../test-applications/node-koa/index.js | 1 - .../node-koa/tests/errors.test.ts | 30 +- .../node-koa/tests/instrumented-paths.test.ts | 115 ++-- .../node-koa/tests/propagation.test.ts | 516 ++++++++++-------- .../node-koa/tests/spans.test.ts | 154 ++++++ .../node-koa/tests/transactions.test.ts | 168 ------ 10 files changed, 708 insertions(+), 641 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-hapi/tests/spans.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-koa/tests/spans.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js index 8526846eafd8..506b05812477 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js @@ -1,7 +1,6 @@ const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, includeLocalVariables: true, @@ -31,7 +30,7 @@ const init = async () => { method: 'GET', path: '/test-param/{param}', handler: function (request, h) { - Sentry.setTag(`param-${request.params.param}`, 'yes'); + Sentry.setAttribute(`param-${request.params.param}`, 'yes'); return { paramWas: request.params.param }; }, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts index 39edc8bcde0e..f355f253b53e 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts @@ -1,22 +1,23 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends thrown error to Sentry', async ({ baseURL }) => { const errorEventPromise = waitForError('node-hapi', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'This is an error'; }); - const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-failure'; - }); + const segmentEventPromise = waitForStreamedSpan( + 'node-hapi', + segment => segment.is_segment && segment.name === 'GET /test-failure', + ); await fetch(`${baseURL}/test-failure`); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const segmentEvent = await segmentEventPromise; - expect(transactionEvent.transaction).toBe('GET /test-failure'); - expect(transactionEvent.contexts?.trace).toMatchObject({ + expect(segmentEvent.name).toBe('GET /test-failure'); + expect(segmentEvent).toMatchObject({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); @@ -43,8 +44,8 @@ test('Sends thrown error to Sentry', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), }); - expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); - expect(errorEvent.contexts?.trace?.span_id).toBe(transactionEvent.contexts?.trace?.span_id); + expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentEvent?.trace_id); + expect(errorEvent.contexts?.trace?.span_id).toBe(segmentEvent?.span_id); }); test('sends error with parameterized transaction name', async ({ baseURL }) => { @@ -71,13 +72,15 @@ test('Does not send errors to Sentry if boom throws in "onPreResponse" after JS return false; // expects to return a boolean (but not relevant here) }); - const transactionEventPromise4xx = waitForTransaction('node-hapi', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-failure-boom-4xx'; - }); + const segmentEventPromise4xx = waitForStreamedSpan( + 'node-hapi', + segment => segment.is_segment && segment.name === 'GET /test-failure-boom-4xx', + ); - const transactionEventPromise5xx = waitForTransaction('node-hapi', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-failure-boom-5xx'; - }); + const segmentEventPromise5xx = waitForStreamedSpan( + 'node-hapi', + segment => segment.is_segment && segment.name === 'GET /test-failure-boom-5xx', + ); const response4xx = await fetch(`${baseURL}/test-failure-boom-4xx`); const response5xx = await fetch(`${baseURL}/test-failure-boom-5xx`); @@ -85,12 +88,12 @@ test('Does not send errors to Sentry if boom throws in "onPreResponse" after JS expect(response4xx.status).toBe(400); expect(response5xx.status).toBe(504); - const transactionEvent4xx = await transactionEventPromise4xx; - const transactionEvent5xx = await transactionEventPromise5xx; + const segmentEvent4xx = await segmentEventPromise4xx; + const segmentEvent5xx = await segmentEventPromise5xx; expect(errorEventOccurred).toBe(false); - expect(transactionEvent4xx.transaction).toBe('GET /test-failure-boom-4xx'); - expect(transactionEvent5xx.transaction).toBe('GET /test-failure-boom-5xx'); + expect(segmentEvent4xx.name).toBe('GET /test-failure-boom-4xx'); + expect(segmentEvent5xx.name).toBe('GET /test-failure-boom-5xx'); }); test('Does not send error to Sentry if error response is overwritten with 2xx in "onPreResponse"', async ({ @@ -105,17 +108,18 @@ test('Does not send error to Sentry if error response is overwritten with 2xx in return false; // expects to return a boolean (but not relevant here) }); - const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-failure-2xx-override-onPreResponse'; - }); + const segmentEventPromise = waitForStreamedSpan( + 'node-hapi', + segment => segment.is_segment && segment.name === 'GET /test-failure-2xx-override-onPreResponse', + ); const response = await fetch(`${baseURL}/test-failure-2xx-override-onPreResponse`); - const transactionEvent = await transactionEventPromise; + const segmentEvent = await segmentEventPromise; expect(response.status).toBe(200); expect(errorEventOccurred).toBe(false); - expect(transactionEvent.transaction).toBe('GET /test-failure-2xx-override-onPreResponse'); + expect(segmentEvent.name).toBe('GET /test-failure-2xx-override-onPreResponse'); }); test('Only sends onPreResponse error to Sentry if JS error is thrown in route handler AND onPreResponse', async ({ @@ -137,18 +141,19 @@ test('Only sends onPreResponse error to Sentry if JS error is thrown in route ha return false; // expects to return a boolean (but not relevant here) }); - const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-failure-JS-error-onPreResponse'; - }); + const segmentEventPromise = waitForStreamedSpan( + 'node-hapi', + segment => segment.is_segment && segment.name === 'GET /test-failure-JS-error-onPreResponse', + ); const response = await fetch(`${baseURL}/test-failure-JS-error-onPreResponse`); expect(response.status).toBe(500); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const segmentEvent = await segmentEventPromise; expect(routeHandlerErrorOccurred).toBe(false); - expect(transactionEvent.transaction).toBe('GET /test-failure-JS-error-onPreResponse'); + expect(segmentEvent.name).toBe('GET /test-failure-JS-error-onPreResponse'); expect(errorEvent.transaction).toEqual('GET /test-failure-JS-error-onPreResponse'); }); diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/spans.test.ts new file mode 100644 index 000000000000..86b65ac3eb32 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/spans.test.ts @@ -0,0 +1,149 @@ +import { expect, test } from '@playwright/test'; +import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; + +test('Sends successful span', async ({ baseURL }) => { + const pageloadSegmentEventPromise = collectStreamedSpansUntilSegment( + 'node-hapi', + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success', + ); + + await fetch(`${baseURL}/test-success`); + + const segmentEventSpans = await pageloadSegmentEventPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success', + )!; + + expect(segmentEvent).toEqual( + expect.objectContaining({ + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.http.http_server', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + 'url.full': { value: 'http://localhost:3030/test-success', type: 'string' }, + 'url.path': { value: '/test-success', type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'client.address': { value: '::1', type: 'string' }, + 'client.port': { value: expect.any(Number), type: 'integer' }, + 'network.transport': { value: 'tcp', type: 'string' }, + 'network.local.address': { value: expect.any(String), type: 'string' }, + 'network.local.port': { value: expect.any(Number), type: 'integer' }, + 'network.peer.address': { value: expect.any(String), type: 'string' }, + 'network.peer.port': { value: expect.any(Number), type: 'integer' }, + 'network.protocol.name': { value: 'http', type: 'string' }, + 'network.protocol.version': { value: '1.1', type: 'string' }, + 'server.port': { value: 3030, type: 'integer' }, + 'http.response.status_text': { value: 'OK', type: 'string' }, + 'http.route': { value: '/test-success', type: 'string' }, + 'http.request.header.accept': { value: '*/*', type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' }, + 'http.request.header.accept_language': { value: '*', type: 'string' }, + 'http.request.header.connection': { value: 'keep-alive', type: 'string' }, + 'http.request.header.host': { value: expect.any(String), type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.user_agent': { value: 'node', type: 'string' }, + }), + }), + ); + + expect(segmentEvent).toEqual( + expect.objectContaining({ + name: 'GET /test-success', + is_segment: true, + attributes: expect.objectContaining({ 'sentry.segment.name.source': { value: 'route', type: 'string' } }), + }), + ); + + const spans = segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, + ); + + spans.forEach(span => { + expect(Object.keys(span.attributes).some(key => key.startsWith('http.request.header.'))).toBe(false); + }); + + expect(spans).toEqual([ + expect.objectContaining({ + name: '/test-success', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + end_timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ + 'hapi.type': { value: 'router', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'http.route': { value: '/test-success', type: 'string' }, + 'sentry.op': { value: 'router', type: 'string' }, + 'sentry.origin': { value: 'auto.http.hapi', type: 'string' }, + }), + }), + expect.objectContaining({ + name: 'ext - onPreResponse', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + end_timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ + 'hapi.type': { value: 'server.ext', type: 'string' }, + 'sentry.op': { value: 'middleware', type: 'string' }, + 'sentry.origin': { value: 'auto.http.hapi', type: 'string' }, + 'server.ext.type': { value: 'onPreResponse', type: 'string' }, + }), + }), + ]); +}); + +test('Sends parameterized spans to Sentry', async ({ baseURL }) => { + const pageloadSegmentEventPromise = waitForStreamedSpan( + 'node-hapi', + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-param/{param}', + ); + + await fetch(`${baseURL}/test-param/123`); + + const segmentEvent = await pageloadSegmentEventPromise; + + expect(getSpanOp(segmentEvent)).toBe('http.server'); + expect(segmentEvent.attributes?.['http.route']?.value).toBe('/test-param/{param}'); + expect(segmentEvent.name).toBe('GET /test-param/{param}'); +}); + +test('Isolates requests', async ({ baseURL }) => { + const segment1Promise = waitForStreamedSpan( + 'node-hapi', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === '/test-param/888', + ); + const segment2Promise = waitForStreamedSpan( + 'node-hapi', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === '/test-param/999', + ); + + await Promise.all([fetch(`${baseURL}/test-param/888`), fetch(`${baseURL}/test-param/999`)]); + + const segment1 = await segment1Promise; + const segment2 = await segment2Promise; + + expect(segment1.attributes['param-888']?.value).toBe('yes'); + expect(segment1.attributes['param-999']).toBeUndefined(); + expect(segment2.attributes['param-999']?.value).toBe('yes'); + expect(segment2.attributes['param-888']).toBeUndefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts deleted file mode 100644 index d90e44824dfe..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends successful transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-success' - ); - }); - - await fetch(`${baseURL}/test-success`); - - const transactionEvent = await pageloadTransactionEventPromise; - - expect(transactionEvent.contexts?.trace).toEqual({ - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.http.http_server', - 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'url.full': 'http://localhost:3030/test-success', - 'url.path': '/test-success', - 'server.address': 'localhost', - 'http.request.method': 'GET', - 'url.scheme': 'http', - 'user_agent.original': 'node', - 'client.address': '::1', - 'client.port': expect.any(Number), - 'network.transport': 'tcp', - 'network.local.address': expect.any(String), - 'network.local.port': expect.any(Number), - 'network.peer.address': expect.any(String), - 'network.peer.port': expect.any(Number), - 'network.protocol.name': 'http', - 'network.protocol.version': '1.1', - 'server.port': 3030, - 'http.response.status_text': 'OK', - 'http.route': '/test-success', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - op: 'http.server', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - status: 'ok', - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.http_server', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: 'GET /test-success', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - const spans = transactionEvent.spans || []; - - spans.forEach(span => { - expect(Object.keys(span.data).some(key => key.startsWith('http.request.header.'))).toBe(false); - }); - - expect(spans).toEqual([ - { - data: { - 'hapi.type': 'router', - 'http.request.method': 'GET', - 'http.route': '/test-success', - 'sentry.op': 'router', - 'sentry.origin': 'auto.http.hapi', - }, - description: 'GET /test-success', - op: 'router', - origin: 'auto.http.hapi', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - { - // this comes from "onPreResponse" - data: { - 'hapi.type': 'server.ext', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.hapi', - 'server.ext.type': 'onPreResponse', - }, - description: 'ext - onPreResponse', - op: 'middleware', - origin: 'auto.http.hapi', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - ]); -}); - -test('Sends parameterized transactions to Sentry', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-param/{param}' - ); - }); - - await fetch(`${baseURL}/test-param/123`); - - const transactionEvent = await pageloadTransactionEventPromise; - - expect(transactionEvent?.contexts?.trace?.op).toBe('http.server'); - expect(transactionEvent?.contexts?.trace?.data?.['http.route']).toBe('/test-param/{param}'); - expect(transactionEvent?.transaction).toBe('GET /test-param/{param}'); -}); - -test('Isolates requests', async ({ baseURL }) => { - const transaction1Promise = waitForTransaction('node-hapi', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.contexts?.trace?.data?.['url.path'] === '/test-param/888' - ); - }); - const transaction2Promise = waitForTransaction('node-hapi', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.contexts?.trace?.data?.['url.path'] === '/test-param/999' - ); - }); - - await Promise.all([fetch(`${baseURL}/test-param/888`), fetch(`${baseURL}/test-param/999`)]); - - const transaction1 = await transaction1Promise; - const transaction2 = await transaction2Promise; - - expect(transaction1.tags).toEqual({ 'param-888': 'yes' }); - expect(transaction2.tags).toEqual({ 'param-999': 'yes' }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/index.js b/dev-packages/e2e-tests/test-applications/node-koa/index.js index 87f14af82681..1b27aa924085 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/index.js +++ b/dev-packages/e2e-tests/test-applications/node-koa/index.js @@ -1,7 +1,6 @@ const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, includeLocalVariables: true, diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts index 821e939ff8ad..4ea066a7748d 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts @@ -1,19 +1,20 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-koa', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - const transactionEventPromise = waitForTransaction('node-koa', event => { - return event.transaction === 'GET /test-exception/:id'; - }); + const segmentEventPromise = collectStreamedSpansUntilSegment('node-koa', 'GET /test-exception/:id'); await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const segmentEventSpans = await segmentEventPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && segment.name === 'GET /test-exception/:id', + )!; expect(errorEvent.exception?.values).toHaveLength(1); const exception = errorEvent.exception?.values?.[0]; @@ -39,11 +40,16 @@ test('Sends correct error event', async ({ baseURL }) => { parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); - // The error is attached to the same trace as the request transaction, and to a - // span that belongs to that transaction (its root span or one of its children). - const transactionTrace = transactionEvent.contexts?.trace; - expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); - - const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; - expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); + // The error is attached to the same trace as the request segment, and to a + // span in that segment. + const segmentTrace = segmentEvent; + expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentTrace?.trace_id); + + const segmentSpanIds = [ + segmentTrace?.span_id, + ...segmentEventSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id) + .map(span => span.span_id), + ]; + expect(segmentSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/instrumented-paths.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/instrumented-paths.test.ts index f74be599663b..1a25576cfe79 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/instrumented-paths.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/instrumented-paths.test.ts @@ -1,27 +1,31 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; test('instruments RegExp router routes', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && !!transactionEvent.transaction?.includes('test-regexp') - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes('test-regexp'), + ); await fetch(`${baseURL}/test-regexp`); - const transactionEvent = await transactionPromise; + const segmentEventSpans = await segmentPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes('test-regexp'), + )!; - expect(transactionEvent.spans).toEqual( + expect( + segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, + ), + ).toEqual( expect.arrayContaining([ expect.objectContaining({ - op: 'router', - origin: 'auto.http.koa', - data: expect.objectContaining({ - 'koa.type': 'router', - 'sentry.op': 'router', - 'sentry.origin': 'auto.http.koa', - 'http.route': '/^\\/test-regexp/', + attributes: expect.objectContaining({ + 'koa.type': { value: 'router', type: 'string' }, + 'sentry.op': { value: 'router', type: 'string' }, + 'sentry.origin': { value: 'auto.http.koa', type: 'string' }, + 'http.route': { value: '/^\\/test-regexp/', type: 'string' }, }), }), ]), @@ -29,27 +33,31 @@ test('instruments RegExp router routes', async ({ baseURL }) => { }); test('instruments nested routers with the composed http.route', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.transaction === 'GET /:first/details/:id' - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /:first/details/:id', + ); await fetch(`${baseURL}/shop/details/1`); - const transactionEvent = await transactionPromise; + const segmentEventSpans = await segmentPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /:first/details/:id', + )!; - expect(transactionEvent.spans).toEqual( + expect( + segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, + ), + ).toEqual( expect.arrayContaining([ expect.objectContaining({ - op: 'router', - description: '/:first/details/:id', - data: expect.objectContaining({ - 'koa.type': 'router', - 'http.route': '/:first/details/:id', - 'sentry.op': 'router', - 'sentry.origin': 'auto.http.koa', + name: '/:first/details/:id', + attributes: expect.objectContaining({ + 'koa.type': { value: 'router', type: 'string' }, + 'http.route': { value: '/:first/details/:id', type: 'string' }, + 'sentry.op': { value: 'router', type: 'string' }, + 'sentry.origin': { value: 'auto.http.koa', type: 'string' }, }), }), ]), @@ -57,40 +65,51 @@ test('instruments nested routers with the composed http.route', async ({ baseURL }); test('does not instrument the same middleware twice', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /test-dedup' - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-dedup', + ); await fetch(`${baseURL}/test-dedup`); - const transactionEvent = await transactionPromise; + const segmentEventSpans = await segmentPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-dedup', + )!; // The route stack is [sharedRouteMiddleware, sharedRouteMiddleware, handler]; the repeated // middleware instance is skipped, leaving one span for it plus the handler span. - const dedupSpans = transactionEvent.spans?.filter(span => span.op === 'router' && span.description === '/test-dedup'); + const dedupSpans = segmentEventSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id) + .filter(span => getSpanOp(span) === 'router' && span.name === '/test-dedup'); expect(dedupSpans).toHaveLength(2); }); test('marks the layer span as errored when a handler throws', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.transaction === 'GET /test-exception/:id' - ); - }); + const segmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-exception/:id', + ); await fetch(`${baseURL}/test-exception/123`); - const transactionEvent = await transactionPromise; + const segmentEventSpans = await segmentPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-exception/:id', + )!; - expect(transactionEvent.spans).toEqual( + expect( + segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, + ), + ).toEqual( expect.arrayContaining([ expect.objectContaining({ - op: 'router', - origin: 'auto.http.koa', - status: 'internal_error', + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'router', type: 'string' }, + 'sentry.origin': { value: 'auto.http.koa', type: 'string' }, + }), }), ]), ); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts index 940541b27e00..fcb898828ed0 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts @@ -1,37 +1,45 @@ import crypto from 'crypto'; import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-koa', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => + getSpanOp(segment) === 'http.server' && segment.attributes?.['url.path']?.value === `/test-outgoing-http/${id}`, + ); const response = await fetch(`${baseURL}/test-outgoing-http/${id}`); const data = await response.json(); - const inboundTransaction = await inboundTransactionPromise; - const outboundTransaction = await outboundTransactionPromise; - - const traceId = outboundTransaction?.contexts?.trace?.trace_id; - const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client'); + const inboundSegment = await inboundSegmentPromise; + const outboundSegmentSpans = await outboundSegmentPromise; + const outboundSegment = outboundSegmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-http/${id}`, + )!; + + const traceId = outboundSegment?.trace_id; + const outgoingHttpSpan = outboundSegmentSpans + .filter(span => !span.is_segment) + ?.find(span => getSpanOp(span) === 'http.client'); expect(outgoingHttpSpan).toBeDefined(); const outgoingHttpSpanId = outgoingHttpSpan?.span_id; - const outgoingHttpSpanData = outgoingHttpSpan?.data || {}; + const outgoingHttpSpanData = outgoingHttpSpan?.attributes || {}; // Outgoing span (`http.client`) does not include headers as attributes expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false); @@ -53,116 +61,127 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { ]), ); - expect(outboundTransaction.contexts?.trace).toEqual({ - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.http.http_server', - 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'url.full': `http://localhost:3030/test-outgoing-http/${id}`, - 'url.path': `/test-outgoing-http/${id}`, - 'server.address': 'localhost', - 'http.request.method': 'GET', - 'url.scheme': 'http', - 'user_agent.original': 'node', - 'client.address': '::1', - 'client.port': expect.any(Number), - 'network.transport': 'tcp', - 'network.local.address': expect.any(String), - 'network.local.port': expect.any(Number), - 'network.peer.address': expect.any(String), - 'network.peer.port': expect.any(Number), - 'network.protocol.name': 'http', - 'network.protocol.version': '1.1', - 'server.port': 3030, - 'http.response.status_text': 'OK', - 'http.route': '/test-outgoing-http/:id', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - op: 'http.server', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - status: 'ok', - trace_id: traceId, - origin: 'auto.http.http_server', - }); + expect(outboundSegment).toEqual( + expect.objectContaining({ + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.http.http_server', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + 'url.full': { value: `http://localhost:3030/test-outgoing-http/${id}`, type: 'string' }, + 'url.path': { value: `/test-outgoing-http/${id}`, type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'client.address': { value: '::1', type: 'string' }, + 'client.port': { value: expect.any(Number), type: 'integer' }, + 'network.transport': { value: 'tcp', type: 'string' }, + 'network.local.address': { value: expect.any(String), type: 'string' }, + 'network.local.port': { value: expect.any(Number), type: 'integer' }, + 'network.peer.address': { value: expect.any(String), type: 'string' }, + 'network.peer.port': { value: expect.any(Number), type: 'integer' }, + 'network.protocol.name': { value: 'http', type: 'string' }, + 'network.protocol.version': { value: '1.1', type: 'string' }, + 'server.port': { value: 3030, type: 'integer' }, + 'http.response.status_text': { value: 'OK', type: 'string' }, + 'http.route': { value: '/test-outgoing-http/:id', type: 'string' }, + 'http.request.header.accept': { value: '*/*', type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' }, + 'http.request.header.accept_language': { value: '*', type: 'string' }, + 'http.request.header.connection': { value: 'keep-alive', type: 'string' }, + 'http.request.header.host': { value: expect.any(String), type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.user_agent': { value: 'node', type: 'string' }, + }), + }), + ); - expect(inboundTransaction.contexts?.trace).toEqual({ - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.http.http_server', - 'sentry.op': 'http.server', - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, - 'url.path': `/test-inbound-headers/${id}`, - 'server.address': 'localhost', - 'http.request.method': 'GET', - 'url.scheme': 'http', - 'client.address': '::1', - 'client.port': expect.any(Number), - 'network.transport': 'tcp', - 'network.local.address': expect.any(String), - 'network.local.port': expect.any(Number), - 'network.peer.address': expect.any(String), - 'network.peer.port': expect.any(Number), - 'network.protocol.name': 'http', - 'network.protocol.version': '1.1', - 'server.port': 3030, - 'http.response.status_text': 'OK', - 'http.route': '/test-inbound-headers/:id', - 'http.request.header.baggage': expect.stringContaining(traceId!), // we already check if traceId is defined - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), - }, - op: 'http.server', - parent_span_id: outgoingHttpSpanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - status: 'ok', - trace_id: traceId, - origin: 'auto.http.http_server', - }); + expect(inboundSegment).toEqual( + expect.objectContaining({ + parent_span_id: outgoingHttpSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.http.http_server', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + 'url.full': { value: `http://localhost:3030/test-inbound-headers/${id}`, type: 'string' }, + 'url.path': { value: `/test-inbound-headers/${id}`, type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, + 'client.address': { value: '::1', type: 'string' }, + 'client.port': { value: expect.any(Number), type: 'integer' }, + 'network.transport': { value: 'tcp', type: 'string' }, + 'network.local.address': { value: expect.any(String), type: 'string' }, + 'network.local.port': { value: expect.any(Number), type: 'integer' }, + 'network.peer.address': { value: expect.any(String), type: 'string' }, + 'network.peer.port': { value: expect.any(Number), type: 'integer' }, + 'network.protocol.name': { value: 'http', type: 'string' }, + 'network.protocol.version': { value: '1.1', type: 'string' }, + 'server.port': { value: 3030, type: 'integer' }, + 'http.response.status_text': { value: 'OK', type: 'string' }, + 'http.route': { value: '/test-inbound-headers/:id', type: 'string' }, + 'http.request.header.baggage': { value: expect.stringContaining(traceId!), type: 'string' }, + 'http.request.header.connection': { value: 'keep-alive', type: 'string' }, + 'http.request.header.host': { value: expect.any(String), type: 'string' }, + 'http.request.header.sentry_trace': { + value: expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), + type: 'string', + }, + }), + }), + ); }); test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-koa', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => + getSpanOp(segment) === 'http.server' && segment.attributes?.['url.path']?.value === `/test-outgoing-fetch/${id}`, + ); const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`); const data = await response.json(); - const inboundTransaction = await inboundTransactionPromise; - const outboundTransaction = await outboundTransactionPromise; + const inboundSegment = await inboundSegmentPromise; + const outboundSegmentSpans = await outboundSegmentPromise; + const outboundSegment = outboundSegmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-fetch/${id}`, + )!; - const traceId = outboundTransaction?.contexts?.trace?.trace_id; - const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client'); + const traceId = outboundSegment?.trace_id; + const outgoingHttpSpan = outboundSegmentSpans + .filter(span => !span.is_segment) + ?.find(span => getSpanOp(span) === 'http.client'); expect(outgoingHttpSpan).toBeDefined(); const outgoingHttpSpanId = outgoingHttpSpan?.span_id; - const outgoingHttpSpanData = outgoingHttpSpan?.data || {}; + const outgoingHttpSpanData = outgoingHttpSpan?.attributes || {}; // Outgoing span (`http.client`) does not include headers as attributes expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false); @@ -184,106 +203,117 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { ]), ); - expect(outboundTransaction.contexts?.trace).toEqual({ - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.http.http_server', - 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'url.full': `http://localhost:3030/test-outgoing-fetch/${id}`, - 'url.path': `/test-outgoing-fetch/${id}`, - 'server.address': 'localhost', - 'http.request.method': 'GET', - 'url.scheme': 'http', - 'user_agent.original': 'node', - 'client.address': '::1', - 'client.port': expect.any(Number), - 'network.transport': 'tcp', - 'network.local.address': expect.any(String), - 'network.local.port': expect.any(Number), - 'network.peer.address': expect.any(String), - 'network.peer.port': expect.any(Number), - 'network.protocol.name': 'http', - 'network.protocol.version': '1.1', - 'server.port': 3030, - 'http.response.status_text': 'OK', - 'http.route': '/test-outgoing-fetch/:id', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': 'localhost:3030', - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - op: 'http.server', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - status: 'ok', - trace_id: traceId, - origin: 'auto.http.http_server', - }); + expect(outboundSegment).toEqual( + expect.objectContaining({ + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.http.http_server', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + 'url.full': { value: `http://localhost:3030/test-outgoing-fetch/${id}`, type: 'string' }, + 'url.path': { value: `/test-outgoing-fetch/${id}`, type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'client.address': { value: '::1', type: 'string' }, + 'client.port': { value: expect.any(Number), type: 'integer' }, + 'network.transport': { value: 'tcp', type: 'string' }, + 'network.local.address': { value: expect.any(String), type: 'string' }, + 'network.local.port': { value: expect.any(Number), type: 'integer' }, + 'network.peer.address': { value: expect.any(String), type: 'string' }, + 'network.peer.port': { value: expect.any(Number), type: 'integer' }, + 'network.protocol.name': { value: 'http', type: 'string' }, + 'network.protocol.version': { value: '1.1', type: 'string' }, + 'server.port': { value: 3030, type: 'integer' }, + 'http.response.status_text': { value: 'OK', type: 'string' }, + 'http.route': { value: '/test-outgoing-fetch/:id', type: 'string' }, + 'http.request.header.accept': { value: '*/*', type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' }, + 'http.request.header.accept_language': { value: '*', type: 'string' }, + 'http.request.header.connection': { value: 'keep-alive', type: 'string' }, + 'http.request.header.host': { value: 'localhost:3030', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.user_agent': { value: 'node', type: 'string' }, + }), + }), + ); - expect(inboundTransaction.contexts?.trace).toEqual({ - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.http.http_server', - 'sentry.op': 'http.server', - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, - 'url.path': `/test-inbound-headers/${id}`, - 'server.address': 'localhost', - 'http.request.method': 'GET', - 'url.scheme': 'http', - 'client.address': '::1', - 'client.port': expect.any(Number), - 'network.transport': 'tcp', - 'network.local.address': expect.any(String), - 'network.local.port': expect.any(Number), - 'network.peer.address': expect.any(String), - 'network.peer.port': expect.any(Number), - 'network.protocol.name': 'http', - 'network.protocol.version': '1.1', - 'server.port': 3030, - 'http.response.status_text': 'OK', - 'http.route': '/test-inbound-headers/:id', - 'user_agent.original': 'node', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.baggage': expect.stringContaining(traceId!), // we already check if traceId is defined - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), - 'http.request.header.user_agent': 'node', - }, - op: 'http.server', - parent_span_id: outgoingHttpSpanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - status: 'ok', - trace_id: traceId, - origin: 'auto.http.http_server', - }); + expect(inboundSegment).toEqual( + expect.objectContaining({ + parent_span_id: outgoingHttpSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.http.http_server', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + 'url.full': { value: `http://localhost:3030/test-inbound-headers/${id}`, type: 'string' }, + 'url.path': { value: `/test-inbound-headers/${id}`, type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, + 'client.address': { value: '::1', type: 'string' }, + 'client.port': { value: expect.any(Number), type: 'integer' }, + 'network.transport': { value: 'tcp', type: 'string' }, + 'network.local.address': { value: expect.any(String), type: 'string' }, + 'network.local.port': { value: expect.any(Number), type: 'integer' }, + 'network.peer.address': { value: expect.any(String), type: 'string' }, + 'network.peer.port': { value: expect.any(Number), type: 'integer' }, + 'network.protocol.name': { value: 'http', type: 'string' }, + 'network.protocol.version': { value: '1.1', type: 'string' }, + 'server.port': { value: 3030, type: 'integer' }, + 'http.response.status_text': { value: 'OK', type: 'string' }, + 'http.route': { value: '/test-inbound-headers/:id', type: 'string' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'http.request.header.accept': { value: '*/*', type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' }, + 'http.request.header.accept_language': { value: '*', type: 'string' }, + 'http.request.header.baggage': { value: expect.stringContaining(traceId!), type: 'string' }, + 'http.request.header.connection': { value: 'keep-alive', type: 'string' }, + 'http.request.header.host': { value: expect.any(String), type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.sentry_trace': { + value: expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), + type: 'string', + }, + 'http.request.header.user_agent': { value: 'node', type: 'string' }, + }), + }), + ); }); test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-http-external-allowed`, + ); const response = await fetch(`${baseURL}/test-outgoing-http-external-allowed`); const data = await response.json(); - const inboundTransaction = await inboundTransactionPromise; + const inboundSegmentSpans = await inboundSegmentPromise; + const inboundSegment = inboundSegmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-http-external-allowed`, + )!; - const traceId = inboundTransaction?.contexts?.trace?.trace_id; - const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + const traceId = inboundSegment?.trace_id; + const spanId = inboundSegmentSpans + .filter(span => !span.is_segment) + ?.find(span => getSpanOp(span) === 'http.client')?.span_id; expect(traceId).toEqual(expect.any(String)); expect(spanId).toEqual(expect.any(String)); @@ -307,20 +337,28 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) }); test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-http-external-disallowed`, + ); const response = await fetch(`${baseURL}/test-outgoing-http-external-disallowed`); const data = await response.json(); - const inboundTransaction = await inboundTransactionPromise; + const inboundSegmentSpans = await inboundSegmentPromise; + const inboundSegment = inboundSegmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-http-external-disallowed`, + )!; - const traceId = inboundTransaction?.contexts?.trace?.trace_id; - const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + const traceId = inboundSegment?.trace_id; + const spanId = inboundSegmentSpans + .filter(span => !span.is_segment) + ?.find(span => getSpanOp(span) === 'http.client')?.span_id; expect(traceId).toEqual(expect.any(String)); expect(spanId).toEqual(expect.any(String)); @@ -331,20 +369,28 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT }); test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-fetch-external-allowed`, + ); const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`); const data = await response.json(); - const inboundTransaction = await inboundTransactionPromise; + const inboundSegmentSpans = await inboundSegmentPromise; + const inboundSegment = inboundSegmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-fetch-external-allowed`, + )!; - const traceId = inboundTransaction?.contexts?.trace?.trace_id; - const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + const traceId = inboundSegment?.trace_id; + const spanId = inboundSegmentSpans + .filter(span => !span.is_segment) + ?.find(span => getSpanOp(span) === 'http.client')?.span_id; expect(traceId).toEqual(expect.any(String)); expect(spanId).toEqual(expect.any(String)); @@ -368,20 +414,28 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } }); test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-fetch-external-disallowed`, + ); const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`); const data = await response.json(); - const inboundTransaction = await inboundTransactionPromise; - - const traceId = inboundTransaction?.contexts?.trace?.trace_id; - const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + const inboundSegmentSpans = await inboundSegmentPromise; + const inboundSegment = inboundSegmentSpans.find( + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-outgoing-fetch-external-disallowed`, + )!; + + const traceId = inboundSegment?.trace_id; + const spanId = inboundSegmentSpans + .filter(span => !span.is_segment) + ?.find(span => getSpanOp(span) === 'http.client')?.span_id; expect(traceId).toEqual(expect.any(String)); expect(spanId).toEqual(expect.any(String)); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/spans.test.ts new file mode 100644 index 000000000000..fbf2ceaaf621 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/spans.test.ts @@ -0,0 +1,154 @@ +import { expect, test } from '@playwright/test'; +import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; + +test('Sends an API route span', async ({ baseURL }) => { + const pageloadSegmentEventPromise = collectStreamedSpansUntilSegment( + 'node-koa', + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction', + ); + + await fetch(`${baseURL}/test-transaction`); + + const segmentEventSpans = await pageloadSegmentEventPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction', + )!; + + expect(segmentEvent).toEqual( + expect.objectContaining({ + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.http.http_server', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + 'url.full': { value: 'http://localhost:3030/test-transaction', type: 'string' }, + 'url.path': { value: '/test-transaction', type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'client.address': { value: '::1', type: 'string' }, + 'client.port': { value: expect.any(Number), type: 'integer' }, + 'network.transport': { value: 'tcp', type: 'string' }, + 'network.local.address': { value: expect.any(String), type: 'string' }, + 'network.local.port': { value: expect.any(Number), type: 'integer' }, + 'network.peer.address': { value: expect.any(String), type: 'string' }, + 'network.peer.port': { value: expect.any(Number), type: 'integer' }, + 'network.protocol.name': { value: 'http', type: 'string' }, + 'network.protocol.version': { value: '1.1', type: 'string' }, + 'server.port': { value: 3030, type: 'integer' }, + 'http.response.status_text': { value: 'OK', type: 'string' }, + 'http.route': { value: '/test-transaction', type: 'string' }, + 'http.request.header.accept': { value: '*/*', type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' }, + 'http.request.header.accept_language': { value: '*', type: 'string' }, + 'http.request.header.connection': { value: 'keep-alive', type: 'string' }, + 'http.request.header.host': { value: expect.any(String), type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.user_agent': { value: 'node', type: 'string' }, + }), + }), + ); + + expect(segmentEvent).toEqual( + expect.objectContaining({ + name: 'GET /test-transaction', + is_segment: true, + attributes: expect.objectContaining({ 'sentry.segment.name.source': { value: 'route', type: 'string' } }), + }), + ); + + const spans = segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, + ); + + expect(spans.sort((a, b) => a.start_timestamp - b.start_timestamp)).toEqual([ + expect.objectContaining({ + name: 'bodyParser', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + end_timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ + 'koa.name': { value: 'bodyParser', type: 'string' }, + 'code.function.name': { value: 'bodyParser', type: 'string' }, + 'koa.type': { value: 'middleware', type: 'string' }, + 'sentry.op': { value: 'middleware', type: 'string' }, + 'sentry.origin': { value: 'auto.http.koa', type: 'string' }, + }), + }), + expect.objectContaining({ + name: '/test-transaction', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + end_timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ + 'http.route': { value: '/test-transaction', type: 'string' }, + 'koa.name': { value: '/test-transaction', type: 'string' }, + 'koa.type': { value: 'router', type: 'string' }, + 'sentry.origin': { value: 'auto.http.koa', type: 'string' }, + 'sentry.op': { value: 'router', type: 'string' }, + }), + }), + expect.objectContaining({ + name: 'test-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + end_timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }), + }), + expect.objectContaining({ + name: 'child-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + end_timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }), + }), + ]); +}); + +test('Captures request metadata', async ({ baseURL }) => { + const segmentEventPromise = waitForStreamedSpan( + 'node-koa', + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'POST /test-post', + ); + + const res = await fetch(`${baseURL}/test-post`, { + method: 'POST', + body: JSON.stringify({ foo: 'bar', other: 1 }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const resBody = await res.json(); + + expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } }); + + const segmentEvent = await segmentEventPromise; + + expect(segmentEvent.attributes['url.full']?.value).toEqual( + expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + ); + expect(segmentEvent.attributes['http.request.method']?.value).toEqual('POST'); + expect(segmentEvent.attributes['http.request.header.user_agent']?.value).toEqual(expect.stringContaining('')); + expect(segmentEvent.attributes['http.request.header.content_type']?.value).toEqual('application/json'); + + expect(segmentEvent.attributes['http.request.body.data']?.value).toBe(JSON.stringify({ foo: 'bar', other: 1 })); + expect(segmentEvent.attributes['user.ip_address']?.value).toEqual('::1'); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts deleted file mode 100644 index b4af3c532e7f..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-transaction' - ); - }); - - await fetch(`${baseURL}/test-transaction`); - - const transactionEvent = await pageloadTransactionEventPromise; - - expect(transactionEvent.contexts?.trace).toEqual({ - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.http.http_server', - 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'url.full': 'http://localhost:3030/test-transaction', - 'url.path': '/test-transaction', - 'server.address': 'localhost', - 'http.request.method': 'GET', - 'url.scheme': 'http', - 'user_agent.original': 'node', - 'client.address': '::1', - 'client.port': expect.any(Number), - 'network.transport': 'tcp', - 'network.local.address': expect.any(String), - 'network.local.port': expect.any(Number), - 'network.peer.address': expect.any(String), - 'network.peer.port': expect.any(Number), - 'network.protocol.name': 'http', - 'network.protocol.version': '1.1', - 'server.port': 3030, - 'http.response.status_text': 'OK', - 'http.route': '/test-transaction', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - op: 'http.server', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - status: 'ok', - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.http_server', - }); - - expect(transactionEvent).toMatchObject({ - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }); - - const spans = transactionEvent.spans || []; - - expect(spans).toEqual([ - { - data: { - 'koa.name': 'bodyParser', - 'code.function.name': 'bodyParser', - 'koa.type': 'middleware', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.koa', - }, - description: 'bodyParser', - op: 'middleware', - origin: 'auto.http.koa', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - { - data: { - 'http.route': '/test-transaction', - 'koa.name': '/test-transaction', - 'koa.type': 'router', - 'sentry.origin': 'auto.http.koa', - 'sentry.op': 'router', - }, - op: 'router', - description: '/test-transaction', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.koa', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'test-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'manual', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'child-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'manual', - }, - ]); -}); - -test('Captures request metadata', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-koa', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'POST /test-post' - ); - }); - - const res = await fetch(`${baseURL}/test-post`, { - method: 'POST', - body: JSON.stringify({ foo: 'bar', other: 1 }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const resBody = await res.json(); - - expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } }); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.request).toEqual({ - cookies: {}, - url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), - method: 'POST', - headers: expect.objectContaining({ - 'user-agent': expect.stringContaining(''), - 'content-type': 'application/json', - }), - data: JSON.stringify({ - foo: 'bar', - other: 1, - }), - }); - - expect(transactionEvent.user).toEqual({ ip_address: '::1' }); -});