From ffcb90d9b4a5f1d835d2052e273b5bd645c51dc4 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 7 Sep 2026 11:21:29 +0200 Subject: [PATCH] test(e2e): Migrate Fastify to span streaming Co-Authored-By: GPT-6 --- .../node-fastify-3/src/app.ts | 1 - .../node-fastify-3/tests/errors.test.ts | 28 +- .../node-fastify-3/tests/propagation.test.ts | 514 ++++++++++-------- .../node-fastify-3/tests/spans.test.ts | 147 +++++ .../node-fastify-3/tests/transactions.test.ts | 153 ------ .../node-fastify-4/src/app.ts | 1 - .../node-fastify-4/tests/errors.test.ts | 30 +- .../node-fastify-4/tests/propagation.test.ts | 514 ++++++++++-------- .../node-fastify-4/tests/spans.test.ts | 147 +++++ .../node-fastify-4/tests/transactions.test.ts | 153 ------ .../node-fastify-5/src/app.ts | 1 - .../node-fastify-5/tests/errors.test.ts | 48 +- .../node-fastify-5/tests/propagation.test.ts | 514 ++++++++++-------- .../node-fastify-5/tests/spans.test.ts | 147 +++++ .../node-fastify-5/tests/transactions.test.ts | 153 ------ 15 files changed, 1356 insertions(+), 1195 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-3/tests/spans.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-4/tests/spans.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-5/tests/spans.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts index 2725ed19b3a7..fd06169c60ec 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts @@ -15,7 +15,6 @@ console.warn = new Proxy(console.warn, { }); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts index 6a548391eea5..572ca2b49c1e 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/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-fastify-3', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - const transactionEventPromise = waitForTransaction('node-fastify-3', event => { - return event.transaction === 'GET /test-exception/:id'; - }); + const segmentEventPromise = collectStreamedSpansUntilSegment('node-fastify-3', '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); expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); @@ -33,13 +34,18 @@ 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); + // 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 transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; - expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_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); }); test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts index d2333b55156d..af1db58e2532 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts @@ -1,38 +1,46 @@ 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-fastify-3', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-fastify-3', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-3', + 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 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 = 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); @@ -54,116 +62,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.any(String), - '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.any(String), 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-fastify-3', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-fastify-3', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-3', + 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); @@ -185,106 +204,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': 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-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: 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', - 'user_agent.original': 'node', - 'http.route': '/test-inbound-headers/:id', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.baggage': expect.any(String), - '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' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'http.route': { value: '/test-inbound-headers/: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.baggage': { value: expect.any(String), 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-fastify-3', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-3', + 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)); @@ -308,20 +338,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-fastify-3', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-3', + 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)); @@ -332,20 +370,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-fastify-3', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-3', + 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)); @@ -369,20 +415,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-fastify-3', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-3', + 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-fastify-3/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/spans.test.ts new file mode 100644 index 000000000000..9c7927242db5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/spans.test.ts @@ -0,0 +1,147 @@ +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-fastify-3', + 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.filter(span => span.name === '/test-transaction' && span.attributes['http.request.method']?.value === 'GET'), + ).toEqual([ + 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({ + 'sentry.origin': { value: 'auto.http.fastify', type: 'string' }, + 'sentry.op': { value: 'handler', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.path': { value: '/test-transaction', type: 'string' }, + 'http.route': { value: '/test-transaction', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + }), + }), + ]); + + expect(spans.filter(span => span.name === 'test-span')).toEqual([ + 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(spans.filter(span => span.name === 'child-span')).toEqual([ + 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-fastify-3', + 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-fastify-3/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts deleted file mode 100644 index 28b5baaeaed8..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts +++ /dev/null @@ -1,153 +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-fastify-3', 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).toEqual( - expect.objectContaining({ - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.fastify', - 'sentry.op': 'handler', - 'http.request.method': 'GET', - 'url.path': '/test-transaction', - 'http.route': '/test-transaction', - 'http.response.status_code': 200, - }, - description: 'GET /test-transaction', - op: 'handler', - 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.fastify', - }); - - expect(spans).toContainEqual({ - 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', - }); - - expect(spans).toContainEqual({ - 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-fastify-3', 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' }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts index cbb38b3f0850..67fbe9692959 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts @@ -15,7 +15,6 @@ console.warn = new Proxy(console.warn, { }); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts index 4e905a12098e..3e53369b24b1 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/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-fastify-4', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - const transactionEventPromise = waitForTransaction('node-fastify-4', event => { - return event.transaction === 'GET /test-exception/:id'; - }); + const segmentEventPromise = collectStreamedSpansUntilSegment('node-fastify-4', '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); expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); @@ -33,13 +34,18 @@ 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); }); test('Does not send 4xx errors by default', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts index 1c817ec8f17b..0e41a9ce84cf 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts @@ -1,38 +1,46 @@ 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-fastify-4', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-fastify-4', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-4', + 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 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 = 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); @@ -54,116 +62,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.any(String), - '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.any(String), 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-fastify-4', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-fastify-4', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-4', + 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); @@ -185,106 +204,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': 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-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: 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', - '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.any(String), - '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.any(String), 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-fastify-4', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-4', + 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)); @@ -308,20 +338,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-fastify-4', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-4', + 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)); @@ -332,20 +370,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-fastify-4', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-4', + 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)); @@ -369,20 +415,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-fastify-4', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-4', + 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-fastify-4/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/spans.test.ts new file mode 100644 index 000000000000..b0fc71f7ea19 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/spans.test.ts @@ -0,0 +1,147 @@ +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-fastify-4', + 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.filter(span => span.name === '/test-transaction' && span.attributes['http.request.method']?.value === 'GET'), + ).toEqual([ + 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({ + 'sentry.origin': { value: 'auto.http.fastify', type: 'string' }, + 'sentry.op': { value: 'handler', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.path': { value: '/test-transaction', type: 'string' }, + 'http.route': { value: '/test-transaction', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + }), + }), + ]); + + expect(spans.filter(span => span.name === 'test-span')).toEqual([ + 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(spans.filter(span => span.name === 'child-span')).toEqual([ + 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-fastify-4', + 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-fastify-4/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts deleted file mode 100644 index 3d716e387927..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts +++ /dev/null @@ -1,153 +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-fastify-4', 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).toEqual( - expect.objectContaining({ - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.fastify', - 'sentry.op': 'handler', - 'http.request.method': 'GET', - 'url.path': '/test-transaction', - 'http.route': '/test-transaction', - 'http.response.status_code': 200, - }, - description: 'GET /test-transaction', - op: 'handler', - 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.fastify', - }); - - expect(spans).toContainEqual({ - 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', - }); - - expect(spans).toContainEqual({ - 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-fastify-4', 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' }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts index 7055d0ed259e..93c942bd16ef 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts @@ -15,7 +15,6 @@ console.warn = new Proxy(console.warn, { }); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts index a50d9f3020b5..256c56117969 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts @@ -1,19 +1,20 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, waitForStreamedSpan, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-fastify-5', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - const transactionEventPromise = waitForTransaction('node-fastify-5', event => { - return event.transaction === 'GET /test-exception/:id'; - }); + const segmentEventPromise = collectStreamedSpansUntilSegment('node-fastify-5', '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]; @@ -38,13 +39,18 @@ 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); }); test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { @@ -57,13 +63,14 @@ test('Does not send error when shouldHandleError returns false', async ({ baseUR return event?.transaction === 'GET /test-error-not-captured'; }); - const transactionEventPromise = waitForTransaction('node-fastify-5', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-error-not-captured'; - }); + const segmentEventPromise = waitForStreamedSpan( + 'node-fastify-5', + segment => segment.is_segment && segment.name === 'GET /test-error-not-captured', + ); const response = await fetch(`${baseURL}/test-error-not-captured`); - await transactionEventPromise; + await segmentEventPromise; const flushResponse = await fetch(`${baseURL}/flush`); @@ -85,13 +92,14 @@ test('Error in child plugin with rethrown error handler reports correct 500 stat return event?.transaction === 'GET /test-error-ignored'; }); - const transactionEventPromise = waitForTransaction('node-fastify-5', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-error-ignored'; - }); + const segmentEventPromise = waitForStreamedSpan( + 'node-fastify-5', + segment => segment.is_segment && segment.name === 'GET /test-error-ignored', + ); const response = await fetch(`${baseURL}/test-error-ignored`); - await transactionEventPromise; + await segmentEventPromise; const flushResponse = await fetch(`${baseURL}/flush`); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts index ebff1774678e..7e111ca46dd6 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts @@ -1,38 +1,46 @@ 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-fastify-5', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-fastify-5', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-fastify-5', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-5', + 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 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 = 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); @@ -54,116 +62,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': '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-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: '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', - 'http.request.header.baggage': expect.any(String), - '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.any(String), 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-fastify-5', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-inbound-headers/${id}` - ); - }); + const inboundSegmentPromise = waitForStreamedSpan( + 'node-fastify-5', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.attributes?.['url.path']?.value === `/test-inbound-headers/${id}`, + ); - const outboundTransactionPromise = waitForTransaction('node-fastify-5', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch/${id}` - ); - }); + const outboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-5', + 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); @@ -185,106 +204,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': 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-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: 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', - 'user_agent.original': 'node', - 'http.route': '/test-inbound-headers/:id', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.baggage': expect.any(String), - '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' }, + 'user_agent.original': { value: 'node', type: 'string' }, + 'http.route': { value: '/test-inbound-headers/: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.baggage': { value: expect.any(String), 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-fastify-5', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-5', + 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)); @@ -308,20 +338,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-fastify-5', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-http-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-5', + 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)); @@ -332,20 +370,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-fastify-5', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-allowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-5', + 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)); @@ -369,20 +415,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-fastify-5', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent.contexts?.trace?.data?.['url.path'] === `/test-outgoing-fetch-external-disallowed` - ); - }); + const inboundSegmentPromise = collectStreamedSpansUntilSegment( + 'node-fastify-5', + 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-fastify-5/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/spans.test.ts new file mode 100644 index 000000000000..2e9c3ba18e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/spans.test.ts @@ -0,0 +1,147 @@ +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-fastify-5', + 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.filter(span => span.name === '/test-transaction' && span.attributes['http.request.method']?.value === 'GET'), + ).toEqual([ + 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({ + 'sentry.origin': { value: 'auto.http.fastify', type: 'string' }, + 'sentry.op': { value: 'handler', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.path': { value: '/test-transaction', type: 'string' }, + 'http.route': { value: '/test-transaction', type: 'string' }, + 'http.response.status_code': { value: 200, type: 'integer' }, + }), + }), + ]); + + expect(spans.filter(span => span.name === 'test-span')).toEqual([ + 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(spans.filter(span => span.name === 'child-span')).toEqual([ + 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-fastify-5', + 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-fastify-5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts deleted file mode 100644 index 283ff1370753..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts +++ /dev/null @@ -1,153 +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-fastify-5', 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).toEqual( - expect.objectContaining({ - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.fastify', - 'sentry.op': 'handler', - 'http.request.method': 'GET', - 'url.path': '/test-transaction', - 'http.route': '/test-transaction', - 'http.response.status_code': 200, - }, - description: 'GET /test-transaction', - op: 'handler', - 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.fastify', - }); - - expect(spans).toContainEqual({ - 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', - }); - - expect(spans).toContainEqual({ - 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-fastify-5', 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' }); -});