From 3fef8b9d84ea48c2f31dffc79d828dea1790cee1 Mon Sep 17 00:00:00 2001 From: nicohrubec Date: Thu, 1 Aug 2024 12:54:06 +0200 Subject: [PATCH 1/5] Add interceptor span before route handler execution --- packages/nestjs/src/setup.ts | 2 + .../node/src/integrations/tracing/nest.ts | 58 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 7402d3f374f0..92d031d9fc5b 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -32,6 +32,8 @@ import type { Observable } from 'rxjs'; * Interceptor to add Sentry tracing capabilities to Nest.js applications. */ class SentryTracingInterceptor implements NestInterceptor { + public static readonly __SENTRY_INTERNAL__ = true; + /** * Intercepts HTTP requests to set the transaction name for Sentry tracing. */ diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index cb3097b06228..f1ab91f4c6a5 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -61,12 +61,21 @@ const supportedVersions = ['>=8.0.0 <11']; const sentryPatched = 'sentryPatched'; +/** + * A NestJS call handler. Used in interceptors to start the route execution. + */ +export interface CallHandler { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handle(...args: any[]): Observable; +} + /** * Represents an injectable target class in NestJS. */ export interface InjectableTarget { name: string; sentryPatched?: boolean; + __SENTRY_INTERNAL__?: boolean; prototype: { // eslint-disable-next-line @typescript-eslint/no-explicit-any use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void; @@ -74,6 +83,8 @@ export interface InjectableTarget { canActivate?: (...args: any[]) => boolean | Promise | Observable; // eslint-disable-next-line @typescript-eslint/no-explicit-any transform?: (...args: any[]) => any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + intercept?: (context: unknown, next: CallHandler, ...args: any[]) => Observable; }; } @@ -148,7 +159,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { return function wrappedInjectable(options?: unknown) { return function (target: InjectableTarget) { // patch middleware - if (typeof target.prototype.use === 'function') { + if (typeof target.prototype.use === 'function' && !target.__SENTRY_INTERNAL__) { // patch only once if (isPatched(target)) { return original(options)(target); @@ -190,7 +201,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { } // patch guards - if (typeof target.prototype.canActivate === 'function') { + if (typeof target.prototype.canActivate === 'function' && !target.__SENTRY_INTERNAL__) { // patch only once if (isPatched(target)) { return original(options)(target); @@ -215,7 +226,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { } // patch pipes - if (typeof target.prototype.transform === 'function') { + if (typeof target.prototype.transform === 'function' && !target.__SENTRY_INTERNAL__) { if (isPatched(target)) { return original(options)(target); } @@ -238,6 +249,47 @@ export class SentryNestInstrumentation extends InstrumentationBase { }); } + // patch interceptors + if (typeof target.prototype.intercept === 'function' && !target.__SENTRY_INTERNAL__) { + if (isPatched(target)) { + return original(options)(target); + } + + target.prototype.intercept = new Proxy(target.prototype.intercept, { + apply: (originalIntercept, thisArgIntercept, argsIntercept) => { + const [executionContext, next, args] = argsIntercept; + + return startSpanManual( + { + name: target.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + }, + }, + (span: Span) => { + const nextProxy = new Proxy(next, { + get: (thisArgNext, property, receiver) => { + if (property === 'handle') { + const originalHandle = Reflect.get(thisArgNext, property, receiver); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (...args: any[]) => { + span.end(); + return Reflect.apply(originalHandle, thisArgNext, args); + }; + } + + return Reflect.get(target, property, receiver); + }, + }); + + return originalIntercept.apply(thisArgIntercept, [executionContext, nextProxy, args]); + }, + ); + }, + }); + } + return original(options)(target); }; }; From 36a4a1c758ec88d811e385678904b2e9bde618a6 Mon Sep 17 00:00:00 2001 From: nicohrubec Date: Thu, 1 Aug 2024 13:15:01 +0200 Subject: [PATCH 2/5] Attach spans correctly --- packages/node/src/integrations/tracing/nest.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index f1ab91f4c6a5..aea7b34c901d 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -184,7 +184,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { span.end(); if (prevSpan) { - withActiveSpan(prevSpan, () => { + return withActiveSpan(prevSpan, () => { return Reflect.apply(originalNext, thisArgNext, argsNext); }); } else { @@ -258,6 +258,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.intercept = new Proxy(target.prototype.intercept, { apply: (originalIntercept, thisArgIntercept, argsIntercept) => { const [executionContext, next, args] = argsIntercept; + const prevSpan = getActiveSpan(); return startSpanManual( { @@ -275,7 +276,14 @@ export class SentryNestInstrumentation extends InstrumentationBase { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (...args: any[]) => { span.end(); - return Reflect.apply(originalHandle, thisArgNext, args); + + if (prevSpan) { + return withActiveSpan(prevSpan, () => { + return Reflect.apply(originalHandle, thisArgNext, args); + }); + } else { + return Reflect.apply(originalHandle, thisArgNext, args); + } }; } From 1faac78ec56f47a2d5cbcf8111f9e094d52855f8 Mon Sep 17 00:00:00 2001 From: nicohrubec Date: Thu, 1 Aug 2024 13:22:06 +0200 Subject: [PATCH 3/5] Little refactor --- .../node/src/integrations/tracing/nest.ts | 135 +++++++----------- 1 file changed, 55 insertions(+), 80 deletions(-) diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index aea7b34c901d..6b452ab3add3 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -103,6 +103,17 @@ export function isPatched(target: InjectableTarget): boolean { return false; } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function getMiddlewareSpanOptions(target: InjectableTarget) { + return { + name: target.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + }, + }; +} + /** * Custom instrumentation for nestjs. * @@ -170,32 +181,23 @@ export class SentryNestInstrumentation extends InstrumentationBase { const [req, res, next, ...args] = argsUse; const prevSpan = getActiveSpan(); - return startSpanManual( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }, - (span: Span) => { - const nextProxy = new Proxy(next, { - apply: (originalNext, thisArgNext, argsNext) => { - span.end(); - - if (prevSpan) { - return withActiveSpan(prevSpan, () => { - return Reflect.apply(originalNext, thisArgNext, argsNext); - }); - } else { + return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { + const nextProxy = new Proxy(next, { + apply: (originalNext, thisArgNext, argsNext) => { + span.end(); + + if (prevSpan) { + return withActiveSpan(prevSpan, () => { return Reflect.apply(originalNext, thisArgNext, argsNext); - } - }, - }); + }); + } else { + return Reflect.apply(originalNext, thisArgNext, argsNext); + } + }, + }); - return originalUse.apply(thisArgUse, [req, res, nextProxy, args]); - }, - ); + return originalUse.apply(thisArgUse, [req, res, nextProxy, args]); + }); }, }); } @@ -209,18 +211,9 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.canActivate = new Proxy(target.prototype.canActivate, { apply: (originalCanActivate, thisArgCanActivate, argsCanActivate) => { - return startSpan( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }, - () => { - return originalCanActivate.apply(thisArgCanActivate, argsCanActivate); - }, - ); + return startSpan(getMiddlewareSpanOptions(target), () => { + return originalCanActivate.apply(thisArgCanActivate, argsCanActivate); + }); }, }); } @@ -233,18 +226,9 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.transform = new Proxy(target.prototype.transform, { apply: (originalTransform, thisArgTransform, argsTransform) => { - return startSpan( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }, - () => { - return originalTransform.apply(thisArgTransform, argsTransform); - }, - ); + return startSpan(getMiddlewareSpanOptions(target), () => { + return originalTransform.apply(thisArgTransform, argsTransform); + }); }, }); } @@ -260,40 +244,31 @@ export class SentryNestInstrumentation extends InstrumentationBase { const [executionContext, next, args] = argsIntercept; const prevSpan = getActiveSpan(); - return startSpanManual( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }, - (span: Span) => { - const nextProxy = new Proxy(next, { - get: (thisArgNext, property, receiver) => { - if (property === 'handle') { - const originalHandle = Reflect.get(thisArgNext, property, receiver); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (...args: any[]) => { - span.end(); - - if (prevSpan) { - return withActiveSpan(prevSpan, () => { - return Reflect.apply(originalHandle, thisArgNext, args); - }); - } else { + return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { + const nextProxy = new Proxy(next, { + get: (thisArgNext, property, receiver) => { + if (property === 'handle') { + const originalHandle = Reflect.get(thisArgNext, property, receiver); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (...args: any[]) => { + span.end(); + + if (prevSpan) { + return withActiveSpan(prevSpan, () => { return Reflect.apply(originalHandle, thisArgNext, args); - } - }; - } - - return Reflect.get(target, property, receiver); - }, - }); + }); + } else { + return Reflect.apply(originalHandle, thisArgNext, args); + } + }; + } + + return Reflect.get(target, property, receiver); + }, + }); - return originalIntercept.apply(thisArgIntercept, [executionContext, nextProxy, args]); - }, - ); + return originalIntercept.apply(thisArgIntercept, [executionContext, nextProxy, args]); + }); }, }); } From 5e20ebb6f849322ac41fae9b03b9482c17d52839 Mon Sep 17 00:00:00 2001 From: nicohrubec Date: Thu, 1 Aug 2024 14:23:04 +0200 Subject: [PATCH 4/5] Add tests --- .../nestjs-basic/src/app.controller.ts | 11 ++- .../nestjs-basic/src/app.service.ts | 2 +- .../nestjs-basic/src/example.interceptor.ts | 10 +++ .../nestjs-basic/tests/transactions.test.ts | 82 +++++++++++++++++++ .../node-nestjs-basic/src/app.controller.ts | 11 ++- .../node-nestjs-basic/src/app.service.ts | 2 +- .../src/example.interceptor.ts | 10 +++ .../tests/transactions.test.ts | 82 +++++++++++++++++++ 8 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts index 40ee93adaa90..c04fd5613e95 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; +import { ExampleInterceptor } from './example.interceptor'; @Controller() export class AppController { @@ -13,7 +14,7 @@ export class AppController { @Get('test-middleware-instrumentation') testMiddlewareInstrumentation() { - return this.appService.testMiddleware(); + return this.appService.testSpan(); } @Get('test-guard-instrumentation') @@ -22,6 +23,12 @@ export class AppController { return {}; } + @Get('test-interceptor-instrumentation') + @UseInterceptors(ExampleInterceptor) + testInterceptorInstrumentation() { + return this.appService.testSpan(); + } + @Get('test-pipe-instrumentation/:id') testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { return { value: id }; diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts index 1ae4c50d8901..b2dadbb0a269 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts @@ -21,7 +21,7 @@ export class AppService { }); } - testMiddleware() { + testSpan() { // span that should not be a child span of the middleware span Sentry.startSpan({ name: 'test-controller-span' }, () => {}); } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts new file mode 100644 index 000000000000..75c301b4cffc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts @@ -0,0 +1,10 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); + return next.handle().pipe(); + } +} 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 33e56cd5695e..23756a0956fe 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 @@ -338,3 +338,85 @@ test('API route transaction includes nest pipe span for invalid request', async }), ); }); + +test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs', 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; + + console.log(transactionEvent.spans); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor'); + const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleInterceptor' is the parent of 'test-interceptor-span' + expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId); + + // 'ExampleInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts index 40ee93adaa90..c04fd5613e95 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; +import { ExampleInterceptor } from './example.interceptor'; @Controller() export class AppController { @@ -13,7 +14,7 @@ export class AppController { @Get('test-middleware-instrumentation') testMiddlewareInstrumentation() { - return this.appService.testMiddleware(); + return this.appService.testSpan(); } @Get('test-guard-instrumentation') @@ -22,6 +23,12 @@ export class AppController { return {}; } + @Get('test-interceptor-instrumentation') + @UseInterceptors(ExampleInterceptor) + testInterceptorInstrumentation() { + return this.appService.testSpan(); + } + @Get('test-pipe-instrumentation/:id') testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { return { value: id }; diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts index 1ae4c50d8901..b2dadbb0a269 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts @@ -21,7 +21,7 @@ export class AppService { }); } - testMiddleware() { + testSpan() { // span that should not be a child span of the middleware span Sentry.startSpan({ name: 'test-controller-span' }, () => {}); } diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts new file mode 100644 index 000000000000..75c301b4cffc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts @@ -0,0 +1,10 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); + return next.handle().pipe(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts index 754d545979e5..df03e46098b0 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts @@ -338,3 +338,85 @@ test('API route transaction includes nest pipe span for invalid request', async }), ); }); + +test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs', 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; + + console.log(transactionEvent.spans); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor'); + const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleInterceptor' is the parent of 'test-interceptor-span' + expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId); + + // 'ExampleInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId); +}); From 90fffad738f996b72820219deb83f1cf8d16cdf7 Mon Sep 17 00:00:00 2001 From: nicohrubec Date: Thu, 1 Aug 2024 16:24:23 +0200 Subject: [PATCH 5/5] Address pr comments --- .../test-applications/nestjs-basic/tests/transactions.test.ts | 2 -- .../node-nestjs-basic/tests/transactions.test.ts | 2 -- packages/nestjs/src/setup.ts | 1 + 3 files changed, 1 insertion(+), 4 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 23756a0956fe..78b3e0d3102a 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 @@ -354,8 +354,6 @@ test('API route transaction includes nest interceptor span. Spans created in and const transactionEvent = await pageloadTransactionEventPromise; - console.log(transactionEvent.spans); - expect(transactionEvent).toEqual( expect.objectContaining({ spans: expect.arrayContaining([ diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts index df03e46098b0..62c882eb7f4b 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts @@ -354,8 +354,6 @@ test('API route transaction includes nest interceptor span. Spans created in and const transactionEvent = await pageloadTransactionEventPromise; - console.log(transactionEvent.spans); - expect(transactionEvent).toEqual( expect.objectContaining({ spans: expect.arrayContaining([ diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 92d031d9fc5b..f788ccb9b67c 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -32,6 +32,7 @@ import type { Observable } from 'rxjs'; * Interceptor to add Sentry tracing capabilities to Nest.js applications. */ class SentryTracingInterceptor implements NestInterceptor { + // used to exclude this class from being auto-instrumented public static readonly __SENTRY_INTERNAL__ = true; /**