diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts index aa4c76f13ee5..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.ts @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/nestjs'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/errors.test.ts index 2b891f247d5f..04f2401ae268 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/errors.test.ts @@ -1,8 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpansUntilSegment, waitForError } from '@sentry-internal/test-utils'; + +const APP_NAME = 'nestjs-12'; test('Sends exception to Sentry', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs-12', event => { + const errorEventPromise = waitForError(APP_NAME, event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -37,7 +39,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs-12', event => { + waitForError(APP_NAME, event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { errorEventOccurred = true; } @@ -45,7 +47,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-400-exception/:id'; }); - waitForError('nestjs-12', event => { + waitForError(APP_NAME, event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { errorEventOccurred = true; } @@ -53,13 +55,10 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-500-exception/:id'; }); - const transactionEventPromise400 = waitForTransaction('nestjs-12', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; - }); - - const transactionEventPromise500 = waitForTransaction('nestjs-12', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; - }); + // Waiting for each request's segment span is how this spec knows the request finished and + // any error it would have produced had its chance to be sent. + const spansPromise400 = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-expected-400-exception/:id'); + const spansPromise500 = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-expected-500-exception/:id'); const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`); expect(response400.status).toBe(400); @@ -67,8 +66,8 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`); expect(response500.status).toBe(500); - await transactionEventPromise400; - await transactionEventPromise500; + await spansPromise400; + await spansPromise500; (await fetch(`${baseURL}/flush`)).text(); @@ -78,7 +77,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs-12', event => { + waitForError(APP_NAME, event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') { errorEventOccurred = true; } @@ -86,14 +85,12 @@ test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-rpc-exception/:id'; }); - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; - }); + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-expected-rpc-exception/:id'); const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); expect(response.status).toBe(500); - await transactionEventPromise; + await spansPromise; (await fetch(`${baseURL}/flush`)).text(); @@ -105,7 +102,7 @@ test('Global exception filter registered in main module is applied and exception }) => { let errorEventOccurred = false; - waitForError('nestjs-12', event => { + waitForError(APP_NAME, event => { if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') { errorEventOccurred = true; } @@ -113,9 +110,7 @@ test('Global exception filter registered in main module is applied and exception return event?.transaction === 'GET /example-exception-global-filter'; }); - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return transactionEvent?.transaction === 'GET /example-exception-global-filter'; - }); + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /example-exception-global-filter'); const response = await fetch(`${baseURL}/example-exception-global-filter`); const responseBody = await response.json(); @@ -128,7 +123,7 @@ test('Global exception filter registered in main module is applied and exception message: 'Example exception was handled by global filter!', }); - await transactionEventPromise; + await spansPromise; (await fetch(`${baseURL}/flush`)).text(); @@ -140,7 +135,7 @@ test('Local exception filter registered in main module is applied and exception }) => { let errorEventOccurred = false; - waitForError('nestjs-12', event => { + waitForError(APP_NAME, event => { if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') { errorEventOccurred = true; } @@ -148,9 +143,7 @@ test('Local exception filter registered in main module is applied and exception return event?.transaction === 'GET /example-exception-local-filter'; }); - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return transactionEvent?.transaction === 'GET /example-exception-local-filter'; - }); + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /example-exception-local-filter'); const response = await fetch(`${baseURL}/example-exception-local-filter`); const responseBody = await response.json(); @@ -163,7 +156,7 @@ test('Local exception filter registered in main module is applied and exception message: 'Example exception was handled by local filter!', }); - await transactionEventPromise; + await spansPromise; (await fetch(`${baseURL}/flush`)).text(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/span-decorator.test.ts index bc07655f9cca..7a15277dce3a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/span-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/span-decorator.test.ts @@ -1,73 +1,67 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; -test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-span-decorator-async' - ); - }); +const APP_NAME = 'nestjs-12'; + +const SPAN_ID = /^[a-f0-9]{16}$/; + +/** The full shape of a `@SentryTraced` span, so `toEqual` catches anything unexpected. */ +function tracedSpan(segmentSpan: SerializedStreamedSpan, name: string, op: string): Record { + return { + name, + span_id: expect.stringMatching(SPAN_ID), + trace_id: segmentSpan.trace_id, + parent_span_id: expect.stringMatching(SPAN_ID), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: false, + status: 'ok', + attributes: { + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + 'sentry.segment.name': { type: 'string', value: segmentSpan.name }, + 'sentry.segment.id': { type: 'string', value: segmentSpan.span_id }, + 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.nestjs' }, + 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.environment': { type: 'string', value: 'qa' }, + // CI builds the apps with a release, local runs have none. It comes from the client + // options, so whatever the segment span got, every other span of the trace got too. + ...(segmentSpan.attributes['sentry.release'] + ? { 'sentry.release': { type: 'string', value: expect.any(String) } } + : {}), + 'sentry.origin': { type: 'string', value: 'auto.function.nestjs.sentry_traced' }, + 'sentry.op': { type: 'string', value: op }, + }, + }; +} + +test('Trace includes span and correct value for decorated async function', async ({ baseURL }) => { + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-span-decorator-async'); const response = await fetch(`${baseURL}/test-span-decorator-async`); const body = await response.json(); expect(body.result).toEqual('test'); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.function.nestjs.sentry_traced', - 'sentry.op': 'wait and return a string', - }, - description: 'wait', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - op: 'wait and return a string', - origin: 'auto.function.nestjs.sentry_traced', - }), - ]), - ); + const segmentSpan = spans.find(span => span.is_segment)!; + expect(spans.find(span => span.name === 'wait')).toEqual(tracedSpan(segmentSpan, 'wait', 'wait and return a string')); }); -test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-span-decorator-sync' - ); - }); +test('Trace includes span and correct value for decorated sync function', async ({ baseURL }) => { + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-span-decorator-sync'); const response = await fetch(`${baseURL}/test-span-decorator-sync`); const body = await response.json(); expect(body.result).toEqual('test'); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.function.nestjs.sentry_traced', - 'sentry.op': 'return a string', - }, - description: 'getString', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - op: 'return a string', - origin: 'auto.function.nestjs.sentry_traced', - }), - ]), + const segmentSpan = spans.find(span => span.is_segment)!; + expect(spans.find(span => span.name === 'getString')).toEqual( + tracedSpan(segmentSpan, 'getString', 'return a string'), ); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/spans.test.ts new file mode 100644 index 000000000000..1285ebc399fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/spans.test.ts @@ -0,0 +1,419 @@ +import { expect, test } from '@playwright/test'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; + +const APP_NAME = 'nestjs-12'; + +const SPAN_ID = /^[a-f0-9]{16}$/; +const TRACE_ID = /^[a-f0-9]{32}$/; + +function findSpan(spans: SerializedStreamedSpan[], name: string): SerializedStreamedSpan | undefined { + return spans.find(span => span.name === name); +} + +/** + * The attributes span streaming puts on every span of a trace. Spelling them out is what lets the + * child span assertions below use `toEqual`, so an unexpected attribute fails the test. + */ +function commonAttributes(segmentSpan: SerializedStreamedSpan): Record { + return { + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + 'sentry.segment.name': { type: 'string', value: segmentSpan.name }, + 'sentry.segment.id': { type: 'string', value: segmentSpan.span_id }, + 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.nestjs' }, + 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.environment': { type: 'string', value: 'qa' }, + // CI builds the apps with a release, local runs have none. It comes from the client + // options, so whatever the segment span got, every other span of the trace got too. + ...(segmentSpan.attributes['sentry.release'] + ? { 'sentry.release': { type: 'string', value: expect.any(String) } } + : {}), + }; +} + +/** A manually started span, which carries nothing beyond the common attributes. */ +function manualSpan( + segmentSpan: SerializedStreamedSpan, + name: string, + parentSpanId: string | RegExp, +): Record { + return { + name, + span_id: expect.stringMatching(SPAN_ID), + trace_id: segmentSpan.trace_id, + parent_span_id: typeof parentSpanId === 'string' ? parentSpanId : expect.stringMatching(parentSpanId), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: false, + status: 'ok', + attributes: { + ...commonAttributes(segmentSpan), + 'sentry.origin': { type: 'string', value: 'manual' }, + }, + }; +} + +/** A Nest middleware-family span (middleware, guard, pipe, interceptor, exception filter). */ +function nestMiddlewareSpan( + segmentSpan: SerializedStreamedSpan, + name: string, + origin: string, +): Record { + return { + name, + span_id: expect.stringMatching(SPAN_ID), + trace_id: segmentSpan.trace_id, + parent_span_id: expect.stringMatching(SPAN_ID), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: false, + status: 'ok', + attributes: { + ...commonAttributes(segmentSpan), + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: origin }, + }, + }; +} + +/** + * Two specs hit each interceptor route, and `collectStreamedSpans` resolves with any trace that + * satisfies the predicate - including one left over from the spec before. Each request therefore + * carries a marker that tells its own trace apart. `url.query` keeps it out of the span name. + */ +function collectSpansOfRequest(segmentName: string, spec: string): Promise { + return collectStreamedSpansUntilSegment( + APP_NAME, + span => span.name === segmentName && span.attributes['url.query']?.value === `spec=${spec}`, + ); +} + +test('Sends streamed spans for an API route', async ({ baseURL }) => { + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-transaction'); + + await fetch(`${baseURL}/test-transaction`); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + // The segment span additionally carries the scope's contexts (os, device, runtime, culture), the + // SDK's integration list and the user's IP, all of which vary by machine, so only the + // request-specific attributes are pinned here. The child spans below are matched exhaustively. + expect(segmentSpan).toEqual({ + name: 'GET /test-transaction', + span_id: expect.stringMatching(SPAN_ID), + trace_id: expect.stringMatching(TRACE_ID), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + attributes: expect.objectContaining({ + ...commonAttributes(segmentSpan), + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'sentry.sample_rate': { type: 'integer', value: 1 }, + 'sentry.kind': { type: 'string', value: 'server' }, + 'http.request.method': { type: 'string', value: 'GET' }, + 'http.route': { type: 'string', value: '/test-transaction' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'http.response.status_text': { type: 'string', value: 'OK' }, + 'url.full': { type: 'string', value: 'http://localhost:3030/test-transaction' }, + 'url.path': { type: 'string', value: '/test-transaction' }, + 'url.scheme': { type: 'string', value: 'http' }, + 'server.address': { type: 'string', value: 'localhost' }, + 'server.port': { type: 'integer', value: 3030 }, + 'user_agent.original': { type: 'string', value: 'node' }, + }), + }); + + expect(findSpan(spans, '/test-transaction')).toEqual({ + name: '/test-transaction', + span_id: expect.stringMatching(SPAN_ID), + trace_id: segmentSpan.trace_id, + parent_span_id: segmentSpan.span_id, + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: false, + status: 'ok', + attributes: { + ...commonAttributes(segmentSpan), + 'sentry.op': { type: 'string', value: 'handler' }, + 'sentry.origin': { type: 'string', value: 'auto.http.express' }, + 'express.name': { type: 'string', value: '/test-transaction' }, + 'express.type': { type: 'string', value: 'request_handler' }, + 'http.route': { type: 'string', value: '/test-transaction' }, + }, + }); + + // The Nest handler span carries the callback name as an attribute rather than in its name, which + // stays low cardinality under span streaming. + const nestHandlerSpan = findSpan(spans, 'Request handler'); + expect(nestHandlerSpan).toEqual({ + name: 'Request handler', + span_id: expect.stringMatching(SPAN_ID), + trace_id: segmentSpan.trace_id, + parent_span_id: expect.stringMatching(SPAN_ID), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: false, + status: 'ok', + attributes: { + ...commonAttributes(segmentSpan), + 'sentry.op': { type: 'string', value: 'handler' }, + 'sentry.origin': { type: 'string', value: 'auto.http.nestjs' }, + component: { type: 'string', value: '@nestjs/core' }, + 'nestjs.type': { type: 'string', value: 'handler' }, + 'nestjs.callback': { type: 'string', value: 'testTransaction' }, + 'nestjs.version': { type: 'string', value: expect.any(String) }, + }, + }); + + const testSpan = findSpan(spans, 'test-span'); + expect(testSpan).toEqual(manualSpan(segmentSpan, 'test-span', nestHandlerSpan!.span_id)); + expect(findSpan(spans, 'child-span')).toEqual(manualSpan(segmentSpan, 'child-span', testSpan!.span_id)); +}); + +test('API route trace includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ + baseURL, +}) => { + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-middleware-instrumentation'); + + const response = await fetch(`${baseURL}/test-middleware-instrumentation`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + const exampleMiddlewareSpan = findSpan(spans, 'ExampleMiddleware'); + expect(exampleMiddlewareSpan).toEqual(nestMiddlewareSpan(segmentSpan, 'ExampleMiddleware', 'auto.middleware.nestjs')); + + // 'ExampleMiddleware' is the parent of 'test-middleware-span' + expect(findSpan(spans, 'test-middleware-span')).toEqual( + manualSpan(segmentSpan, 'test-middleware-span', exampleMiddlewareSpan!.span_id), + ); + + const testControllerSpan = findSpan(spans, 'test-controller-span'); + expect(testControllerSpan).toEqual(manualSpan(segmentSpan, 'test-controller-span', SPAN_ID)); + + // 'ExampleMiddleware' is NOT the parent of 'test-controller-span' + expect(testControllerSpan!.parent_span_id).not.toBe(exampleMiddlewareSpan!.span_id); +}); + +test('API route trace includes nest guard span and span started in guard is nested correctly', async ({ baseURL }) => { + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-guard-instrumentation'); + + const response = await fetch(`${baseURL}/test-guard-instrumentation`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + const exampleGuardSpan = findSpan(spans, 'ExampleGuard'); + expect(exampleGuardSpan).toEqual(nestMiddlewareSpan(segmentSpan, 'ExampleGuard', 'auto.middleware.nestjs.guard')); + + // 'ExampleGuard' is the parent of 'test-guard-span' + expect(findSpan(spans, 'test-guard-span')).toEqual( + manualSpan(segmentSpan, 'test-guard-span', exampleGuardSpan!.span_id), + ); +}); + +test('API route trace includes nest pipe span for valid request', async ({ baseURL }) => { + // Both pipe specs hit the same route, so the segment name alone does not tell their traces apart. + const spansPromise = collectStreamedSpansUntilSegment( + APP_NAME, + span => + span.name === 'GET /test-pipe-instrumentation/:id' && + span.attributes['url.path']?.value === '/test-pipe-instrumentation/123', + ); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + expect(findSpan(spans, 'ParseIntPipe')).toEqual( + nestMiddlewareSpan(segmentSpan, 'ParseIntPipe', 'auto.middleware.nestjs.pipe'), + ); +}); + +test('API route trace includes nest pipe span for invalid request', async ({ baseURL }) => { + const spansPromise = collectStreamedSpansUntilSegment( + APP_NAME, + span => + span.name === 'GET /test-pipe-instrumentation/:id' && + span.attributes['url.path']?.value === '/test-pipe-instrumentation/abc', + ); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`); + expect(response.status).toBe(400); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + // Streamed spans only distinguish `ok` from `error`; the detailed status lives in + // `sentry.status.message`. + expect(findSpan(spans, 'ParseIntPipe')).toEqual({ + name: 'ParseIntPipe', + span_id: expect.stringMatching(SPAN_ID), + trace_id: segmentSpan.trace_id, + parent_span_id: expect.stringMatching(SPAN_ID), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + is_segment: false, + status: 'error', + attributes: { + ...commonAttributes(segmentSpan), + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.nestjs.pipe' }, + 'sentry.status.message': { type: 'string', value: 'internal_error' }, + }, + }); +}); + +test('API route trace includes nest interceptor spans before route execution. Spans created in and after interceptor are nested correctly', async ({ + baseURL, +}) => { + const spansPromise = collectSpansOfRequest('GET /test-interceptor-instrumentation', 'interceptor-before-route'); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation?spec=interceptor-before-route`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + const exampleInterceptor1Span = findSpan(spans, 'ExampleInterceptor1'); + const exampleInterceptor2Span = findSpan(spans, 'ExampleInterceptor2'); + expect(exampleInterceptor1Span).toEqual( + nestMiddlewareSpan(segmentSpan, 'ExampleInterceptor1', 'auto.middleware.nestjs.interceptor'), + ); + expect(exampleInterceptor2Span).toEqual( + nestMiddlewareSpan(segmentSpan, 'ExampleInterceptor2', 'auto.middleware.nestjs.interceptor'), + ); + + // Each interceptor is the parent of the span started inside it + expect(findSpan(spans, 'test-interceptor-span-1')).toEqual( + manualSpan(segmentSpan, 'test-interceptor-span-1', exampleInterceptor1Span!.span_id), + ); + expect(findSpan(spans, 'test-interceptor-span-2')).toEqual( + manualSpan(segmentSpan, 'test-interceptor-span-2', exampleInterceptor2Span!.span_id), + ); + + const testControllerSpan = findSpan(spans, 'test-controller-span'); + expect(testControllerSpan).toEqual(manualSpan(segmentSpan, 'test-controller-span', SPAN_ID)); + + // Neither interceptor is the parent of 'test-controller-span' + expect(testControllerSpan!.parent_span_id).not.toBe(exampleInterceptor1Span!.span_id); + expect(testControllerSpan!.parent_span_id).not.toBe(exampleInterceptor2Span!.span_id); +}); + +test('API route trace includes exactly one nest interceptor span after route execution. Spans created in controller and in interceptor are nested correctly', async ({ + baseURL, +}) => { + const spansPromise = collectSpansOfRequest('GET /test-interceptor-instrumentation', 'interceptor-after-route'); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation?spec=interceptor-after-route`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + const interceptorSpansAfterRoute = spans.filter(span => span.name === 'Interceptors - After Route'); + expect(interceptorSpansAfterRoute).toHaveLength(1); + + const interceptorSpanAfterRoute = interceptorSpansAfterRoute[0]!; + expect(interceptorSpanAfterRoute).toEqual( + nestMiddlewareSpan(segmentSpan, 'Interceptors - After Route', 'auto.middleware.nestjs.interceptor'), + ); + + // 'Interceptors - After Route' is the parent of 'test-interceptor-span-after-route' + expect(findSpan(spans, 'test-interceptor-span-after-route')).toEqual( + manualSpan(segmentSpan, 'test-interceptor-span-after-route', interceptorSpanAfterRoute.span_id), + ); + + // 'Interceptors - After Route' is NOT the parent of 'test-controller-span' + expect(findSpan(spans, 'test-controller-span')!.parent_span_id).not.toBe(interceptorSpanAfterRoute.span_id); +}); + +test('API route trace includes nest async interceptor spans before route execution. Spans created in and after async interceptor are nested correctly', async ({ + baseURL, +}) => { + const spansPromise = collectSpansOfRequest( + 'GET /test-async-interceptor-instrumentation', + 'async-interceptor-before-route', + ); + + const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation?spec=async-interceptor-before-route`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + const asyncInterceptorSpan = findSpan(spans, 'AsyncInterceptor'); + expect(asyncInterceptorSpan).toEqual( + nestMiddlewareSpan(segmentSpan, 'AsyncInterceptor', 'auto.middleware.nestjs.interceptor'), + ); + + // 'AsyncInterceptor' is the parent of 'test-async-interceptor-span' + expect(findSpan(spans, 'test-async-interceptor-span')).toEqual( + manualSpan(segmentSpan, 'test-async-interceptor-span', asyncInterceptorSpan!.span_id), + ); + + const testControllerSpan = findSpan(spans, 'test-controller-span'); + expect(testControllerSpan).toEqual(manualSpan(segmentSpan, 'test-controller-span', SPAN_ID)); + + // 'AsyncInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan!.parent_span_id).not.toBe(asyncInterceptorSpan!.span_id); +}); + +test('API route trace includes exactly one nest async interceptor span after route execution. Spans created in controller and in async interceptor are nested correctly', async ({ + baseURL, +}) => { + const spansPromise = collectSpansOfRequest( + 'GET /test-async-interceptor-instrumentation', + 'async-interceptor-after-route', + ); + + const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation?spec=async-interceptor-after-route`); + expect(response.status).toBe(200); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + + const interceptorSpansAfterRoute = spans.filter(span => span.name === 'Interceptors - After Route'); + expect(interceptorSpansAfterRoute).toHaveLength(1); + + const interceptorSpanAfterRoute = interceptorSpansAfterRoute[0]!; + expect(interceptorSpanAfterRoute).toEqual( + nestMiddlewareSpan(segmentSpan, 'Interceptors - After Route', 'auto.middleware.nestjs.interceptor'), + ); + + // 'Interceptors - After Route' is the parent of 'test-async-interceptor-span-after-route' + expect(findSpan(spans, 'test-async-interceptor-span-after-route')).toEqual( + manualSpan(segmentSpan, 'test-async-interceptor-span-after-route', interceptorSpanAfterRoute.span_id), + ); + + // 'Interceptors - After Route' is NOT the parent of 'test-controller-span' + expect(findSpan(spans, 'test-controller-span')!.parent_span_id).not.toBe(interceptorSpanAfterRoute.span_id); +}); + +test('Calling use method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-use`); + expect(response.status).toBe(200); +}); + +test('Calling transform method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-transform`); + expect(response.status).toBe(200); +}); + +test('Calling intercept method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-intercept`); + expect(response.status).toBe(200); +}); + +test('Calling canActivate method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-canActivate`); + expect(response.status).toBe(200); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-12/tests/transactions.test.ts deleted file mode 100644 index cd10d955538a..000000000000 --- a/dev-packages/e2e-tests/test-applications/nestjs-12/tests/transactions.test.ts +++ /dev/null @@ -1,737 +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('nestjs-12', 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({ - spans: expect.arrayContaining([ - { - data: { - 'express.name': '/test-transaction', - 'express.type': 'request_handler', - 'http.route': '/test-transaction', - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'handler', - }, - op: 'handler', - description: '/test-transaction', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.express', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'test-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'manual', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'child-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'manual', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.http.nestjs', - 'sentry.op': 'handler', - component: '@nestjs/core', - 'nestjs.version': expect.any(String), - 'nestjs.type': 'handler', - 'nestjs.callback': 'testTransaction', - }, - description: 'testTransaction', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'auto.http.nestjs', - op: 'handler', - }, - ]), - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); -}); - -test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ - baseURL, -}) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-middleware-instrumentation' - ); - }); - - const response = await fetch(`${baseURL}/test-middleware-instrumentation`); - expect(response.status).toBe(200); - - const transactionEvent = await pageloadTransactionEventPromise; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs', - }, - description: 'ExampleMiddleware', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs', - }, - ]), - }), - ); - - const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware'); - const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-controller-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-middleware-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - ]), - }), - ); - - // verify correct span parent-child relationships - const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span'); - const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - - // 'ExampleMiddleware' is the parent of 'test-middleware-span' - expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId); - - // 'ExampleMiddleware' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId); -}); - -test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ - baseURL, -}) => { - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-guard-instrumentation' - ); - }); - - const response = await fetch(`${baseURL}/test-guard-instrumentation`); - expect(response.status).toBe(200); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.guard', - }, - description: 'ExampleGuard', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.guard', - }, - ]), - }), - ); - - const exampleGuardSpan = transactionEvent.spans.find(span => span.description === 'ExampleGuard'); - const exampleGuardSpanId = exampleGuardSpan?.span_id; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-guard-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - ]), - }), - ); - - // verify correct span parent-child relationships - const testGuardSpan = transactionEvent.spans.find(span => span.description === 'test-guard-span'); - - // 'ExampleGuard' is the parent of 'test-guard-span' - expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId); -}); - -test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && - transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') - ); - }); - - const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`); - expect(response.status).toBe(200); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.pipe', - }, - description: 'ParseIntPipe', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.pipe', - }, - ]), - }), - ); -}); - -test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && - transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') - ); - }); - - const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`); - expect(response.status).toBe(400); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.pipe', - }, - description: 'ParseIntPipe', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'internal_error', - op: 'middleware', - origin: 'auto.middleware.nestjs.pipe', - }, - ]), - }), - ); -}); - -test('API route transaction includes nest interceptor spans before route execution. Spans created in and after interceptor are nested correctly', async ({ - baseURL, -}) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' - ); - }); - - const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); - expect(response.status).toBe(200); - - const transactionEvent = await pageloadTransactionEventPromise; - - // check if interceptor spans before route execution exist - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.interceptor', - }, - description: 'ExampleInterceptor1', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.interceptor', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.interceptor', - }, - description: 'ExampleInterceptor2', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.interceptor', - }, - ]), - }), - ); - - // get interceptor spans - const exampleInterceptor1Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor1'); - const exampleInterceptor1SpanId = exampleInterceptor1Span?.span_id; - const exampleInterceptor2Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor2'); - const exampleInterceptor2SpanId = exampleInterceptor2Span?.span_id; - - // check if manually started spans exist - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-controller-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-interceptor-span-1', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-interceptor-span-2', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - ]), - }), - ); - - // verify correct span parent-child relationships - const testInterceptor1Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-1'); - const testInterceptor2Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-2'); - const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - - // 'ExampleInterceptor1' is the parent of 'test-interceptor-span-1' - expect(testInterceptor1Span.parent_span_id).toBe(exampleInterceptor1SpanId); - - // 'ExampleInterceptor1' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor1SpanId); - - // 'ExampleInterceptor2' is the parent of 'test-interceptor-span-2' - expect(testInterceptor2Span.parent_span_id).toBe(exampleInterceptor2SpanId); - - // 'ExampleInterceptor2' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor2SpanId); -}); - -test('API route transaction includes exactly one nest interceptor span after route execution. Spans created in controller and in interceptor are nested correctly', async ({ - baseURL, -}) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' - ); - }); - - const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); - expect(response.status).toBe(200); - - const transactionEvent = await pageloadTransactionEventPromise; - - // check if interceptor spans after route execution exist - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.interceptor', - }, - description: 'Interceptors - After Route', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.interceptor', - }, - ]), - }), - ); - - // check that exactly one after route span is sent - const allInterceptorSpansAfterRoute = transactionEvent.spans.filter( - span => span.description === 'Interceptors - After Route', - ); - expect(allInterceptorSpansAfterRoute.length).toBe(1); - - // get interceptor span - const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find( - span => span.description === 'Interceptors - After Route', - ); - const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id; - - // check if manually started span in interceptor after route exists - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-interceptor-span-after-route', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - ]), - }), - ); - - // verify correct span parent-child relationships - const testInterceptorSpanAfterRoute = transactionEvent.spans.find( - span => span.description === 'test-interceptor-span-after-route', - ); - const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - - // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route' - expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId); - - // 'Interceptor - After Route' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId); -}); - -test('API route transaction includes nest async interceptor spans before route execution. Spans created in and after async interceptor are nested correctly', async ({ - baseURL, -}) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' - ); - }); - - const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`); - expect(response.status).toBe(200); - - const transactionEvent = await pageloadTransactionEventPromise; - - // check if interceptor spans before route execution exist - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.interceptor', - }, - description: 'AsyncInterceptor', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.interceptor', - }, - ]), - }), - ); - - // get interceptor spans - const exampleAsyncInterceptor = transactionEvent.spans.find(span => span.description === 'AsyncInterceptor'); - const exampleAsyncInterceptorSpanId = exampleAsyncInterceptor?.span_id; - - // check if manually started spans exist - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-controller-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-async-interceptor-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - ]), - }), - ); - - // verify correct span parent-child relationships - const testAsyncInterceptorSpan = transactionEvent.spans.find( - span => span.description === 'test-async-interceptor-span', - ); - const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - - // 'AsyncInterceptor' is the parent of 'test-async-interceptor-span' - expect(testAsyncInterceptorSpan.parent_span_id).toBe(exampleAsyncInterceptorSpanId); - - // 'AsyncInterceptor' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleAsyncInterceptorSpanId); -}); - -test('API route transaction includes exactly one nest async interceptor span after route execution. Spans created in controller and in async interceptor are nested correctly', async ({ - baseURL, -}) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' - ); - }); - - const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`); - expect(response.status).toBe(200); - - const transactionEvent = await pageloadTransactionEventPromise; - - // check if interceptor spans after route execution exist - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.middleware.nestjs.interceptor', - }, - description: 'Interceptors - After Route', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware', - origin: 'auto.middleware.nestjs.interceptor', - }, - ]), - }), - ); - - // check that exactly one after route span is sent - const allInterceptorSpansAfterRoute = transactionEvent.spans.filter( - span => span.description === 'Interceptors - After Route', - ); - expect(allInterceptorSpansAfterRoute.length).toBe(1); - - // get interceptor span - const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find( - span => span.description === 'Interceptors - After Route', - ); - const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id; - - // check if manually started span in interceptor after route exists - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: expect.arrayContaining([ - { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: expect.any(Object), - description: 'test-async-interceptor-span-after-route', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - origin: 'manual', - }, - ]), - }), - ); - - // verify correct span parent-child relationships - const testInterceptorSpanAfterRoute = transactionEvent.spans.find( - span => span.description === 'test-async-interceptor-span-after-route', - ); - const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - - // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route' - expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId); - - // 'Interceptor - After Route' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId); -}); - -test('Calling use method on service with Injectable decorator returns 200', async ({ baseURL }) => { - const response = await fetch(`${baseURL}/test-service-use`); - expect(response.status).toBe(200); -}); - -test('Calling transform method on service with Injectable decorator returns 200', async ({ baseURL }) => { - const response = await fetch(`${baseURL}/test-service-transform`); - expect(response.status).toBe(200); -}); - -test('Calling intercept method on service with Injectable decorator returns 200', async ({ baseURL }) => { - const response = await fetch(`${baseURL}/test-service-intercept`); - expect(response.status).toBe(200); -}); - -test('Calling canActivate method on service with Injectable decorator returns 200', async ({ baseURL }) => { - const response = await fetch(`${baseURL}/test-service-canActivate`); - expect(response.status).toBe(200); -});