diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs index 1d89289c1b49..f3dd95215d03 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, debug: !!process.env.DEBUG, diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts index a5413565ee03..747ebda30062 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/tests/mcp.test.ts @@ -1,9 +1,9 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; import { Client } from '@modelcontextprotocol/client'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; -test('records transactions for stable MCP SDK v2 handlers using the register API', async ({ baseURL }) => { +test('records spans for stable MCP SDK v2 handlers using the register API', async ({ baseURL }) => { const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); const client = new Client({ @@ -11,26 +11,28 @@ test('records transactions for stable MCP SDK v2 handlers using the register API version: '1.0.0', }); - const initializeTransactionPromise = waitForTransaction('node-express-mcp-v2', transactionEvent => { - return transactionEvent.transaction === 'initialize'; - }); + const initializeSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2', + segment => segment.is_segment && segment.name === 'initialize', + ); await client.connect(transport); await test.step('initialize handshake', async () => { - const initializeTransaction = await initializeTransactionPromise; - expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client-v2'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo-V2'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.transport']).toMatch(/StreamableHTTPServerTransport/); + const initializeSegment = await initializeSegmentPromise; + expect(initializeSegment).toBeDefined(); + expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); + expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); + expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-v2'); + expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-V2'); + expect(initializeSegment.attributes?.['mcp.transport']?.value).toMatch(/StreamableHTTPServerTransport/); }); await test.step('registerTool handler', async () => { - const toolTransactionPromise = waitForTransaction('node-express-mcp-v2', transactionEvent => { - return transactionEvent.transaction === 'tools/call echo'; - }); + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2', + segment => segment.is_segment && segment.name === 'tools/call echo', + ); const toolResult = await client.callTool({ name: 'echo', @@ -48,19 +50,20 @@ test('records transactions for stable MCP SDK v2 handlers using the register API ], }); - const toolTransaction = await toolTransactionPromise; - expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); - expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo'); + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment.attributes?.['mcp.method.name']?.value).toEqual('tools/call'); + expect(toolSegment.attributes?.['mcp.tool.name']?.value).toEqual('echo'); // Proves span was completed with results (span correlation worked end-to-end) - expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.result.content_count']).toEqual(1); + expect(toolSegment.attributes?.['mcp.tool.result.content_count']?.value).toEqual(1); }); await test.step('registerResource handler', async () => { - const resourceTransactionPromise = waitForTransaction('node-express-mcp-v2', transactionEvent => { - return transactionEvent.transaction === 'resources/read echo://foobar'; - }); + const resourceSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2', + segment => segment.is_segment && segment.name === 'resources/read', + ); const resourceResult = await client.readResource({ uri: 'echo://foobar', @@ -70,16 +73,17 @@ test('records transactions for stable MCP SDK v2 handlers using the register API contents: [{ text: 'Resource echo: foobar', uri: 'echo://foobar' }], }); - const resourceTransaction = await resourceTransactionPromise; - expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); + const resourceSegment = await resourceSegmentPromise; + expect(resourceSegment).toBeDefined(); + expect(getSpanOp(resourceSegment)).toEqual('mcp.server'); + expect(resourceSegment.attributes?.['mcp.method.name']?.value).toEqual('resources/read'); }); await test.step('registerPrompt handler', async () => { - const promptTransactionPromise = waitForTransaction('node-express-mcp-v2', transactionEvent => { - return transactionEvent.transaction === 'prompts/get echo'; - }); + const promptSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2', + segment => segment.is_segment && segment.name === 'prompts/get echo', + ); const promptResult = await client.getPrompt({ name: 'echo', @@ -100,16 +104,17 @@ test('records transactions for stable MCP SDK v2 handlers using the register API ], }); - const promptTransaction = await promptTransactionPromise; - expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); + const promptSegment = await promptSegmentPromise; + expect(promptSegment).toBeDefined(); + expect(getSpanOp(promptSegment)).toEqual('mcp.server'); + expect(promptSegment.attributes?.['mcp.method.name']?.value).toEqual('prompts/get'); }); - await test.step('error tool sets span status to internal_error', async () => { - const toolTransactionPromise = waitForTransaction('node-express-mcp-v2', transactionEvent => { - return transactionEvent.transaction === 'tools/call always-error'; - }); + await test.step('error tool sets span status to error', async () => { + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-mcp-v2', + segment => segment.is_segment && segment.name === 'tools/call always-error', + ); try { await client.callTool({ name: 'always-error', arguments: {} }); @@ -117,10 +122,10 @@ test('records transactions for stable MCP SDK v2 handlers using the register API // Expected: MCP SDK throws when the tool returns a JSON-RPC error } - const toolTransaction = await toolTransactionPromise; - expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(toolTransaction.contexts?.trace?.status).toEqual('internal_error'); + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment?.status).toEqual('error'); }); await client.close(); diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts index c4b7d08c2f91..d2b7f600040e 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts @@ -7,7 +7,6 @@ declare global { } Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, includeLocalVariables: true, diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts index f7187956f817..2bad35b574d1 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/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-express-v5', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - const transactionEventPromise = waitForTransaction('node-express-v5', event => { - return event.transaction === 'GET /test-exception/:id'; - }); + const segmentEventPromise = collectStreamedSpansUntilSegment('node-express-v5', '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('Should record caught exceptions with local variable', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts index c943ebfd4ab1..65efec350587 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mcp.test.ts @@ -1,10 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -test('Should record transactions for mcp handlers', async ({ baseURL }) => { +test('Should record spans for mcp handlers', async ({ baseURL }) => { const transport = new SSEClientTransport(new URL(`${baseURL}/sse`)); const client = new Client({ @@ -12,28 +12,31 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { version: '1.0.0', }); - const initializeTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'initialize'; - }); + const initializeSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'initialize', + ); await client.connect(transport); await test.step('initialize handshake', async () => { - const initializeTransaction = await initializeTransactionPromise; - expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo'); + const initializeSegment = await initializeSegmentPromise; + expect(initializeSegment).toBeDefined(); + expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); + expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); + expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client'); + expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo'); }); await test.step('tool handler', async () => { - const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'POST /messages'; - }); - const toolTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'tools/call echo'; - }); + const postSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'POST /messages', + ); + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'tools/call echo', + ); const toolResult = await client.callTool({ name: 'echo', @@ -51,22 +54,24 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { ], }); - const postTransaction = await postTransactionPromise; - expect(postTransaction).toBeDefined(); + const postSegment = await postSegmentPromise; + expect(postSegment).toBeDefined(); - const toolTransaction = await toolTransactionPromise; - expect(toolTransaction).toBeDefined(); + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); // TODO: When https://github.com/modelcontextprotocol/typescript-sdk/pull/358 is released check for trace id equality between the post transaction and the handler transaction }); await test.step('registerTool handler', async () => { - const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'POST /messages'; - }); - const toolTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'tools/call echo-register'; - }); + const postSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'POST /messages', + ); + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'tools/call echo-register', + ); const toolResult = await client.callTool({ name: 'echo-register', @@ -84,21 +89,23 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { ], }); - const postTransaction = await postTransactionPromise; - expect(postTransaction).toBeDefined(); + const postSegment = await postSegmentPromise; + expect(postSegment).toBeDefined(); - const toolTransaction = await toolTransactionPromise; - expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo-register'); + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(toolSegment.attributes?.['mcp.tool.name']?.value).toEqual('echo-register'); }); await test.step('resource handler', async () => { - const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'POST /messages'; - }); - const resourceTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'resources/read echo://foobar'; - }); + const postSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'POST /messages', + ); + const resourceSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'resources/read', + ); const resourceResult = await client.readResource({ uri: 'echo://foobar', @@ -108,22 +115,24 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { contents: [{ text: 'Resource echo: foobar', uri: 'echo://foobar' }], }); - const postTransaction = await postTransactionPromise; - expect(postTransaction).toBeDefined(); + const postSegment = await postSegmentPromise; + expect(postSegment).toBeDefined(); - const resourceTransaction = await resourceTransactionPromise; - expect(resourceTransaction).toBeDefined(); + const resourceSegment = await resourceSegmentPromise; + expect(resourceSegment).toBeDefined(); // TODO: When https://github.com/modelcontextprotocol/typescript-sdk/pull/358 is released check for trace id equality between the post transaction and the handler transaction }); await test.step('prompt handler', async () => { - const postTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'POST /messages'; - }); - const promptTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'prompts/get echo'; - }); + const postSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'POST /messages', + ); + const promptSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'prompts/get echo', + ); const promptResult = await client.getPrompt({ name: 'echo', @@ -144,19 +153,20 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { ], }); - const postTransaction = await postTransactionPromise; - expect(postTransaction).toBeDefined(); + const postSegment = await postSegmentPromise; + expect(postSegment).toBeDefined(); - const promptTransaction = await promptTransactionPromise; - expect(promptTransaction).toBeDefined(); + const promptSegment = await promptSegmentPromise; + expect(promptSegment).toBeDefined(); // TODO: When https://github.com/modelcontextprotocol/typescript-sdk/pull/358 is released check for trace id equality between the post transaction and the handler transaction }); - await test.step('error tool sets span status to internal_error', async () => { - const toolTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.transaction === 'tools/call always-error'; - }); + await test.step('error tool sets span status to error', async () => { + const toolSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => segment.is_segment && segment.name === 'tools/call always-error', + ); try { await client.callTool({ name: 'always-error', arguments: {} }); @@ -164,10 +174,10 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { // Expected: MCP SDK throws when the tool returns a JSON-RPC error } - const toolTransaction = await toolTransactionPromise; - expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(toolTransaction.contexts?.trace?.status).toEqual('internal_error'); + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment?.status).toEqual('error'); }); }); @@ -182,7 +192,7 @@ test('Should record transactions for mcp handlers', async ({ baseURL }) => { * * @see https://github.com/getsentry/sentry-mcp/issues/767 */ -test('Should record transactions for streamable HTTP transport (wrapper transport pattern)', async ({ baseURL }) => { +test('Should record spans for streamable HTTP transport (wrapper transport pattern)', async ({ baseURL }) => { const transport = new StreamableHTTPClientTransport(new URL(`${baseURL}/mcp`)); const client = new Client({ @@ -190,32 +200,34 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor version: '1.0.0', }); - const initializeTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - return ( - transactionEvent.transaction === 'initialize' && - transactionEvent.contexts?.trace?.data?.['mcp.server.name'] === 'Echo-Streamable' - ); - }); + const initializeSegmentPromise = waitForStreamedSpan( + 'node-express-v5', + segment => + segment.is_segment && + segment.name === 'initialize' && + segment.attributes?.['mcp.server.name']?.value === 'Echo-Streamable', + ); await client.connect(transport); await test.step('initialize handshake', async () => { - const initializeTransaction = await initializeTransactionPromise; - expect(initializeTransaction).toBeDefined(); - expect(initializeTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('initialize'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.client.name']).toEqual('test-client-streamable'); - expect(initializeTransaction.contexts?.trace?.data?.['mcp.server.name']).toEqual('Echo-Streamable'); + const initializeSegment = await initializeSegmentPromise; + expect(initializeSegment).toBeDefined(); + expect(getSpanOp(initializeSegment)).toEqual('mcp.server'); + expect(initializeSegment.attributes?.['mcp.method.name']?.value).toEqual('initialize'); + expect(initializeSegment.attributes?.['mcp.client.name']?.value).toEqual('test-client-streamable'); + expect(initializeSegment.attributes?.['mcp.server.name']?.value).toEqual('Echo-Streamable'); // Verify it's using a StreamableHTTP transport (may be wrapper or inner depending on environment) - expect(initializeTransaction.contexts?.trace?.data?.['mcp.transport']).toMatch(/StreamableHTTPServerTransport/); + expect(initializeSegment.attributes?.['mcp.transport']?.value).toMatch(/StreamableHTTPServerTransport/); }); await test.step('tool handler (tests wrapper transport correlation)', async () => { // This is the critical test - without the sessionId fix, the span would not be completed // because onmessage and send see different transport instances (wrapper vs inner) - const toolTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - const transport = transactionEvent.contexts?.trace?.data?.['mcp.transport'] as string | undefined; - return transactionEvent.transaction === 'tools/call echo' && transport?.includes('StreamableHTTPServerTransport'); + const toolSegmentPromise = waitForStreamedSpan('node-express-v5', segment => { + if (!segment.is_segment) return false; + const transport = segment.attributes?.['mcp.transport']?.value as string | undefined; + return segment.name === 'tools/call echo' && !!transport?.includes('StreamableHTTPServerTransport'); }); const toolResult = await client.callTool({ @@ -234,22 +246,20 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor ], }); - const toolTransaction = await toolTransactionPromise; - expect(toolTransaction).toBeDefined(); - expect(toolTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(toolTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('tools/call'); - expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.name']).toEqual('echo'); + const toolSegment = await toolSegmentPromise; + expect(toolSegment).toBeDefined(); + expect(getSpanOp(toolSegment)).toEqual('mcp.server'); + expect(toolSegment.attributes?.['mcp.method.name']?.value).toEqual('tools/call'); + expect(toolSegment.attributes?.['mcp.tool.name']?.value).toEqual('echo'); // This attribute proves the span was completed with results (sessionId correlation worked) - expect(toolTransaction.contexts?.trace?.data?.['mcp.tool.result.content_count']).toEqual(1); + expect(toolSegment.attributes?.['mcp.tool.result.content_count']?.value).toEqual(1); }); await test.step('resource handler', async () => { - const resourceTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - const transport = transactionEvent.contexts?.trace?.data?.['mcp.transport'] as string | undefined; - return ( - transactionEvent.transaction === 'resources/read echo://streamable-test' && - transport?.includes('StreamableHTTPServerTransport') - ); + const resourceSegmentPromise = waitForStreamedSpan('node-express-v5', segment => { + if (!segment.is_segment) return false; + const transport = segment.attributes?.['mcp.transport']?.value as string | undefined; + return segment.name === 'resources/read' && !!transport?.includes('StreamableHTTPServerTransport'); }); const resourceResult = await client.readResource({ @@ -260,18 +270,17 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor contents: [{ text: 'Resource echo: streamable-test', uri: 'echo://streamable-test' }], }); - const resourceTransaction = await resourceTransactionPromise; - expect(resourceTransaction).toBeDefined(); - expect(resourceTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(resourceTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('resources/read'); + const resourceSegment = await resourceSegmentPromise; + expect(resourceSegment).toBeDefined(); + expect(getSpanOp(resourceSegment)).toEqual('mcp.server'); + expect(resourceSegment.attributes?.['mcp.method.name']?.value).toEqual('resources/read'); }); await test.step('prompt handler', async () => { - const promptTransactionPromise = waitForTransaction('node-express-v5', transactionEvent => { - const transport = transactionEvent.contexts?.trace?.data?.['mcp.transport'] as string | undefined; - return ( - transactionEvent.transaction === 'prompts/get echo' && transport?.includes('StreamableHTTPServerTransport') - ); + const promptSegmentPromise = waitForStreamedSpan('node-express-v5', segment => { + if (!segment.is_segment) return false; + const transport = segment.attributes?.['mcp.transport']?.value as string | undefined; + return segment.name === 'prompts/get echo' && !!transport?.includes('StreamableHTTPServerTransport'); }); const promptResult = await client.getPrompt({ @@ -293,10 +302,10 @@ test('Should record transactions for streamable HTTP transport (wrapper transpor ], }); - const promptTransaction = await promptTransactionPromise; - expect(promptTransaction).toBeDefined(); - expect(promptTransaction.contexts?.trace?.op).toEqual('mcp.server'); - expect(promptTransaction.contexts?.trace?.data?.['mcp.method.name']).toEqual('prompts/get'); + const promptSegment = await promptSegmentPromise; + expect(promptSegment).toBeDefined(); + expect(getSpanOp(promptSegment)).toEqual('mcp.server'); + expect(promptSegment.attributes?.['mcp.method.name']?.value).toEqual('prompts/get'); }); // Clean up - close the client connection diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts index 76849657bd30..3bd6ea267256 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/mysql.test.ts @@ -1,33 +1,47 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; test('Instruments MySQL via Orchestrion', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-express-v5', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /test-mysql'; - }); + const segmentEventPromise = collectStreamedSpansUntilSegment( + 'node-express-v5', + segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-mysql', + ); await fetch(`${baseURL}/test-mysql`); - const transactionEvent = await transactionEventPromise; + const segmentEventSpans = await segmentEventPromise; + const segmentEvent = segmentEventSpans.find( + segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-mysql', + )!; - expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); - expect(transactionEvent.transaction).toEqual('GET /test-mysql'); - expect(transactionEvent.contexts?.trace?.status).toEqual('ok'); - expect(transactionEvent.contexts?.trace?.data?.['http.response.status_code']).toEqual(200); + expect(getSpanOp(segmentEvent)).toEqual('http.server'); + expect(segmentEvent.name).toEqual('GET /test-mysql'); + expect(segmentEvent?.status).toEqual('ok'); + expect(segmentEvent.attributes?.['http.response.status_code']?.value).toEqual(200); - const spans = transactionEvent.spans || []; - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.mysql', - description: 'SELECT 1 + 1 AS solution', - }), + const spans = segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, ); - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.mysql', - description: 'SELECT NOW()', - }), + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); + expect(dbSpans).toHaveLength(2); + expect(dbSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'SELECT', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: 'auto.db.mysql', type: 'string' }, + 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + }), + }), + expect.objectContaining({ + name: 'SELECT', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: 'auto.db.mysql', type: 'string' }, + 'db.query.text': { value: 'SELECT NOW()', type: 'string' }, + }), + }), + ]), ); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/spans.test.ts new file mode 100644 index 000000000000..b1212fd518b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/spans.test.ts @@ -0,0 +1,125 @@ +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-express-v5', + 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.attributes['http.response.status_code']?.value).toBe(200); + + 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, + ); + + // Manually started span + 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' } }), + }), + ]); + + // auto instrumented span + expect(spans.filter(span => span.name === '/test-transaction')).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.express', type: 'string' }, + 'sentry.op': { value: 'handler', type: 'string' }, + 'http.route': { value: '/test-transaction', type: 'string' }, + 'express.name': { value: '/test-transaction', type: 'string' }, + 'express.type': { value: 'request_handler', type: 'string' }, + }), + }), + ]); +}); + +test('Sends an API route span for an errored route', async ({ baseURL }) => { + const segmentEventPromise = waitForStreamedSpan( + 'node-express-v5', + segment => + segment.is_segment && + getSpanOp(segment) === 'http.server' && + segment.name === 'GET /test-exception/:id' && + segment.attributes['url.full']?.value === 'http://localhost:3030/test-exception/777', + ); + + await fetch(`${baseURL}/test-exception/777`); + + const segmentEvent = await segmentEventPromise; + + expect(getSpanOp(segmentEvent)).toEqual('http.server'); + expect(segmentEvent.name).toEqual('GET /test-exception/:id'); + expect(segmentEvent?.status).toEqual('error'); + expect(segmentEvent.attributes?.['http.response.status_code']?.value).toEqual(500); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts deleted file mode 100644 index f487396b6af2..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts +++ /dev/null @@ -1,124 +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-express-v5', 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.contexts?.response).toEqual({ - status_code: 200, - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - const spans = transactionEvent.spans || []; - - // Manually started span - expect(spans).toContainEqual({ - data: { 'sentry.origin': 'manual' }, - description: 'test-span', - origin: 'manual', - 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}/), - }); - - // auto instrumented span - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'handler', - 'http.route': '/test-transaction', - 'express.name': '/test-transaction', - 'express.type': 'request_handler', - }, - description: '/test-transaction', - op: 'handler', - origin: 'auto.http.express', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }); -}); - -test('Sends an API route transaction for an errored route', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-express-v5', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.transaction === 'GET /test-exception/:id' && - transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' - ); - }); - - await fetch(`${baseURL}/test-exception/777`); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); - expect(transactionEvent.transaction).toEqual('GET /test-exception/:id'); - expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); - expect(transactionEvent.contexts?.trace?.data?.['http.response.status_code']).toEqual(500); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts index 33852add4123..b7e12e05c4c9 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/trpc.test.ts @@ -1,15 +1,17 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, collectStreamedSpans } from '@sentry-internal/test-utils'; import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../src/app'; test('Should record span for trpc query', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-express-v5', transactionEvent => { - return ( - transactionEvent.transaction === 'GET /trpc' && - !!transactionEvent.spans?.find(span => span.description === 'trpc/getSomething') - ); - }); + const segmentEventPromise = collectStreamedSpans('node-express-v5', spans => + spans.some( + segment => + segment.is_segment && + segment.name === 'GET /trpc' && + !!spans.filter(span => !span.is_segment).find(span => span.name === 'trpc/getSomething'), + ), + ); const trpcClient = createTRPCProxyClient({ links: [ @@ -21,27 +23,41 @@ test('Should record span for trpc query', async ({ baseURL }) => { await trpcClient.getSomething.query('foobar'); - await expect(transactionEventPromise).resolves.toBeDefined(); - const transaction = await transactionEventPromise; - - expect(transaction.spans).toContainEqual( + await expect(segmentEventPromise).resolves.toBeDefined(); + const segmentSpans = await segmentEventPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + segment.name === 'GET /trpc' && + !!segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .find(span => span.name === 'trpc/getSomething'), + )!; + + expect( + segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .filter(span => span.name === `trpc/getSomething`), + ).toEqual([ expect.objectContaining({ - data: expect.objectContaining({ - 'sentry.op': 'rpc', - 'sentry.origin': 'auto.rpc.trpc', + name: `trpc/getSomething`, + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: 'auto.rpc.trpc', type: 'string' }, }), - description: `trpc/getSomething`, }), - ); + ]); }); -test('Should record transaction for trpc mutation', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-express-v5', transactionEvent => { - return ( - transactionEvent.transaction === 'POST /trpc' && - !!transactionEvent.spans?.find(span => span.description === 'trpc/createSomething') - ); - }); +test('Should record span for trpc mutation', async ({ baseURL }) => { + const segmentEventPromise = collectStreamedSpans('node-express-v5', spans => + spans.some( + segment => + segment.is_segment && + segment.name === 'POST /trpc' && + !!spans.filter(span => !span.is_segment).find(span => span.name === 'trpc/createSomething'), + ), + ); const trpcClient = createTRPCProxyClient({ links: [ @@ -53,27 +69,41 @@ test('Should record transaction for trpc mutation', async ({ baseURL }) => { await trpcClient.createSomething.mutate(); - await expect(transactionEventPromise).resolves.toBeDefined(); - const transaction = await transactionEventPromise; - - expect(transaction.spans).toContainEqual( + await expect(segmentEventPromise).resolves.toBeDefined(); + const segmentSpans = await segmentEventPromise; + const segment = segmentSpans.find( + segment => + segment.is_segment && + segment.name === 'POST /trpc' && + !!segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .find(span => span.name === 'trpc/createSomething'), + )!; + + expect( + segmentSpans + .filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment.span_id) + .filter(span => span.name === `trpc/createSomething`), + ).toEqual([ expect.objectContaining({ - data: expect.objectContaining({ - 'sentry.op': 'rpc', - 'sentry.origin': 'auto.rpc.trpc', + name: `trpc/createSomething`, + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: 'auto.rpc.trpc', type: 'string' }, }), - description: `trpc/createSomething`, }), - ); + ]); }); -test('Should record transaction and error for a crashing trpc handler', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-express-v5', transactionEvent => { - return ( - transactionEvent.transaction === 'POST /trpc' && - !!transactionEvent.spans?.find(span => span.description === 'trpc/crashSomething') - ); - }); +test('Should record span and error for a crashing trpc handler', async ({ baseURL }) => { + const segmentEventPromise = collectStreamedSpans('node-express-v5', spans => + spans.some( + segment => + segment.is_segment && + segment.name === 'POST /trpc' && + !!spans.filter(span => !span.is_segment).find(span => span.name === 'trpc/crashSomething'), + ), + ); const errorEventPromise = waitForError('node-express-v5', errorEvent => { return !!errorEvent?.exception?.values?.some(exception => exception.value?.includes('I crashed in a trpc handler')); @@ -89,7 +119,7 @@ test('Should record transaction and error for a crashing trpc handler', async ({ await expect(trpcClient.crashSomething.mutate({ nested: { nested: { nested: 'foobar' } } })).rejects.toBeDefined(); - await expect(transactionEventPromise).resolves.toBeDefined(); + await expect(segmentEventPromise).resolves.toBeDefined(); await expect(errorEventPromise).resolves.toBeDefined(); expect((await errorEventPromise).contexts?.trpc?.['procedure_type']).toBe('mutation'); @@ -105,13 +135,15 @@ test('Should record transaction and error for a crashing trpc handler', async ({ }); }); -test('Should record transaction and error for a trpc handler that returns a status code', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-express-v5', transactionEvent => { - return ( - transactionEvent.transaction === 'POST /trpc' && - !!transactionEvent.spans?.find(span => span.description === 'trpc/badRequest') - ); - }); +test('Should record span and error for a trpc handler that returns a status code', async ({ baseURL }) => { + const segmentEventPromise = collectStreamedSpans('node-express-v5', spans => + spans.some( + segment => + segment.is_segment && + segment.name === 'POST /trpc' && + !!spans.filter(span => !span.is_segment).find(span => span.name === 'trpc/badRequest'), + ), + ); const errorEventPromise = waitForError('node-express-v5', errorEvent => { return !!errorEvent?.exception?.values?.some(exception => exception.value?.includes('Bad Request')); @@ -127,6 +159,6 @@ test('Should record transaction and error for a trpc handler that returns a stat await expect(trpcClient.badRequest.mutate()).rejects.toBeDefined(); - await expect(transactionEventPromise).resolves.toBeDefined(); + await expect(segmentEventPromise).resolves.toBeDefined(); await expect(errorEventPromise).resolves.toBeDefined(); });