From 53903560d7afd2f26baf0c32fa6219a465a18901 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Fri, 31 Jul 2026 09:47:13 +0200 Subject: [PATCH] feat(nestjs)!: Use `function` span op for setup & lifecycle handlers Migrate the NestJS setup/lifecycle span ops to the cross-framework `function` op: - `app_creation.nestjs` -> `function` - `request_context.nestjs` -> `function` - `event.nestjs` -> `function` Op is set via the `sentry.op` attribute only; the `nestjs.type` attribute (`app_creation` / `request_context` / `handler`) is unchanged, so span-shape filters keep working. Lookups that previously keyed on the distinct `request_context` op are re-keyed on `nestjs.type`, since it now shares the `function` op and `auto.http.nestjs` origin with the request handler span. Co-Authored-By: Claude Opus 4.8 --- .../nestjs-basic/tests/transactions.test.ts | 4 ++-- .../tests/events.test.ts | 4 ++-- .../nestjs-fastify/tests/transactions.test.ts | 16 ++++++++++++---- .../nestjs-orchestrion/tests/events.test.ts | 2 +- .../tests/transactions.test.ts | 9 ++++++--- packages/nestjs/src/integrations/helpers.ts | 4 ++-- packages/nestjs/src/integrations/wrap-route.ts | 13 ++++++------- .../integrations/orchestrion-subscriber.test.ts | 6 +++--- 8 files changed, 34 insertions(+), 24 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts index 2d3100b95d1d..694d1995cc62 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts @@ -14,14 +14,14 @@ test('Sends an app_creation transaction', async () => { contexts: { trace: { op?: string; origin?: string; data?: Record } }; }; - expect(transaction.contexts.trace.op).toBe('app_creation.nestjs'); + expect(transaction.contexts.trace.op).toBe('function'); expect(transaction.contexts.trace.origin).toBe('auto.http.nestjs'); expect(transaction.contexts.trace.data).toEqual( expect.objectContaining({ component: '@nestjs/core', 'nestjs.type': 'app_creation', 'nestjs.module': 'AppModule', - 'sentry.op': 'app_creation.nestjs', + 'sentry.op': 'function', 'sentry.origin': 'auto.http.nestjs', }), ); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts index 24e93b6cbd86..5be068b5b037 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts @@ -35,11 +35,11 @@ test('Event emitter', async () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { 'sentry.source': 'custom', - 'sentry.op': 'event.nestjs', + 'sentry.op': 'function', 'sentry.origin': 'auto.event.nestjs', }, origin: 'auto.event.nestjs', - op: 'event.nestjs', + op: 'function', status: 'ok', }); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts index 5198eeaa9439..c004dccecb02 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -102,7 +102,7 @@ test.skip('Sends an API route transaction', async ({ baseURL }) => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { 'sentry.origin': 'auto.http.nestjs', - 'sentry.op': 'request_context.nestjs', + 'sentry.op': 'function', component: '@nestjs/core', 'nestjs.version': expect.any(String), 'nestjs.type': 'request_context', @@ -117,7 +117,7 @@ test.skip('Sends an API route transaction', async ({ baseURL }) => { start_timestamp: expect.any(Number), timestamp: expect.any(Number), status: 'ok', - op: 'request_context.nestjs', + op: 'function', origin: 'auto.http.nestjs', }, { @@ -798,8 +798,16 @@ test('Sets error status on nest spans when a handler throws', async ({ baseURL } expect(transactionEvent.spans).toEqual( expect.arrayContaining([ - expect.objectContaining({ op: 'request_context.nestjs', status: 'internal_error' }), - expect.objectContaining({ op: 'function', status: 'internal_error' }), + expect.objectContaining({ + op: 'function', + status: 'internal_error', + data: expect.objectContaining({ 'nestjs.type': 'request_context' }), + }), + expect.objectContaining({ + op: 'function', + status: 'internal_error', + data: expect.objectContaining({ 'nestjs.type': 'handler' }), + }), ]), ); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/events.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/events.test.ts index a4d018635a0a..ca60d931228c 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/events.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/events.test.ts @@ -12,6 +12,6 @@ test('@OnEvent opens an event.nestjs transaction', async ({ baseURL }) => { await fetch(`${baseURL}/test-event`); const transactionEvent = await transactionPromise; - expect(transactionEvent.contexts?.trace?.op).toBe('event.nestjs'); + expect(transactionEvent.contexts?.trace?.op).toBe('function'); expect(transactionEvent.contexts?.trace?.origin).toBe('auto.event.nestjs'); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/transactions.test.ts index ef13bcfcfe7f..bef09459b31f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-orchestrion/tests/transactions.test.ts @@ -25,7 +25,7 @@ test('app_creation: emits a "Create Nest App" transaction at startup', async () contexts: { trace: { op?: string; origin?: string; data?: Record } }; }; - expect(transaction.contexts.trace.op).toBe('app_creation.nestjs'); + expect(transaction.contexts.trace.op).toBe('function'); expect(transaction.contexts.trace.origin).toBe('auto.http.nestjs'); expect(transaction.contexts.trace.data).toEqual( expect.objectContaining({ @@ -47,10 +47,13 @@ test('request_context + handler: a route transaction nests the nestjs spans', as await fetch(`${baseURL}/test-transaction`); const transactionEvent = await transactionPromise; - // request_context span, identified by its controller/callback attributes. + // request_context span, identified by its `nestjs.type` attribute — it shares + // the `function` op and `auto.http.nestjs` origin with the request_handler span. // Its description isn't asserted: the span carries `http.*` attributes, so // the OTel span-name inference rewrites it to `GET /test-transaction` - const requestContext = findSpan(transactionEvent, 'request_context.nestjs', 'auto.http.nestjs'); + const requestContext = (transactionEvent.spans ?? []).find( + span => span.data?.['nestjs.type'] === 'request_context' && span.origin === 'auto.http.nestjs', + ); expect(requestContext).toBeDefined(); expect(requestContext?.data).toMatchObject({ 'nestjs.type': 'request_context', diff --git a/packages/nestjs/src/integrations/helpers.ts b/packages/nestjs/src/integrations/helpers.ts index dd3ea3fb2dbd..f2390e5b32a7 100644 --- a/packages/nestjs/src/integrations/helpers.ts +++ b/packages/nestjs/src/integrations/helpers.ts @@ -1,5 +1,5 @@ import { SENTRY_OP } from '@sentry/conventions/attributes'; -import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op'; +import { WEB_SERVER_FUNCTION_SPAN_OP, WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import { addNonEnumerableProperty, @@ -108,7 +108,7 @@ export function getEventSpanOptions(event: string): { return { name: `event ${event}`, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'event.nestjs', + [SENTRY_OP]: WEB_SERVER_FUNCTION_SPAN_OP, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs', }, forceTransaction: true, diff --git a/packages/nestjs/src/integrations/wrap-route.ts b/packages/nestjs/src/integrations/wrap-route.ts index 271cdd349f21..69c290b12ac9 100644 --- a/packages/nestjs/src/integrations/wrap-route.ts +++ b/packages/nestjs/src/integrations/wrap-route.ts @@ -24,16 +24,16 @@ interface NestRequest { url?: string; } -/** Span options for the `Create Nest App` (app_creation) span. */ +/** Span options for the `Create Nest App` (app_creation, `function` op) span. */ export function getAppCreationSpanOptions( moduleVersion?: string, moduleName?: string, -): { name: string; op: string; attributes: SpanAttributes } { +): { name: string; attributes: SpanAttributes } { return { name: 'Create Nest App', - op: `${NestType.APP_CREATION}.nestjs`, attributes: { component: NESTJS_COMPONENT, + [SENTRY_OP]: WEB_SERVER_FUNCTION_SPAN_OP, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, [AttributeNames.TYPE]: NestType.APP_CREATION, [AttributeNames.VERSION]: moduleVersion || undefined, @@ -73,7 +73,7 @@ export function wrapRouteHandler(callback: AnyFn, moduleVersion?: string): AnyFn /** * Wrap the per-request handler that `RouterExecutionContext.create` returns so - * each request opens the `request_context.nestjs` span (REQUEST_CONTEXT), + * each request opens the request-context (`function` op) span (REQUEST_CONTEXT), * carrying controller/callback names plus the per-request http.* attributes. */ export function wrapRequestContextHandler( @@ -89,6 +89,7 @@ export function wrapRequestContextHandler( const attributes: SpanAttributes = { component: NESTJS_COMPONENT, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_OP]: WEB_SERVER_FUNCTION_SPAN_OP, [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, [AttributeNames.CONTROLLER]: instanceName, [AttributeNames.CALLBACK]: callbackName, @@ -98,9 +99,7 @@ export function wrapRequestContextHandler( [HTTP_METHOD]: req.method || undefined, [URL_FULL]: req.originalUrl || req.url || undefined, }; - return startSpan({ name: spanName, op: `${NestType.REQUEST_CONTEXT}.nestjs`, attributes }, () => - handler.apply(this, handlerArgs), - ); + return startSpan({ name: spanName, attributes }, () => handler.apply(this, handlerArgs)); }; markWrapped(wrapped); return wrapped; diff --git a/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts index 6a38e3b2c916..f49a8bdfc668 100644 --- a/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts +++ b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts @@ -147,7 +147,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => { expect(span).toBeDefined(); const json = spanToJSON(span!); expect(json.description).toBe('Create Nest App'); - expect(json.op).toBe('app_creation.nestjs'); + expect(json.op).toBe('function'); expect(json.origin).toBe('auto.http.nestjs'); expect(json.data).toMatchObject({ component: '@nestjs/core', @@ -241,7 +241,7 @@ describe('NestJS orchestrion subscriber: request_context / request_handler', () expect(contextSpanJson).toBeDefined(); expect(contextSpanJson!.description).toBe('CatsController.getCats'); - expect(contextSpanJson!.op).toBe('request_context.nestjs'); + expect(contextSpanJson!.op).toBe('function'); expect(contextSpanJson!.origin).toBe('auto.http.nestjs'); expect(contextSpanJson!.data).toMatchObject({ component: '@nestjs/core', @@ -872,7 +872,7 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => { const json = spanToJSON(spanInside!); expect(json.description).toBe('event user.created'); - expect(json.op).toBe('event.nestjs'); + expect(json.op).toBe('function'); expect(json.origin).toBe('auto.event.nestjs'); });