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 eb0ead5e32d0..40ee93adaa90 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,4 +1,4 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; @@ -22,6 +22,11 @@ export class AppController { return {}; } + @Get('test-pipe-instrumentation/:id') + testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { + return { value: id }; + } + @Get('test-exception/:id') async testException(@Param('id') id: string) { return this.appService.testException(id); 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 ebd8503e1d42..33e56cd5695e 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 @@ -266,3 +266,75 @@ test('API route transaction includes nest guard span and span started in guard i // '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', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'unknown_error', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); 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 eb0ead5e32d0..40ee93adaa90 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,4 +1,4 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; @@ -22,6 +22,11 @@ export class AppController { return {}; } + @Get('test-pipe-instrumentation/:id') + testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { + return { value: id }; + } + @Get('test-exception/:id') async testException(@Param('id') id: string) { return this.appService.testException(id); 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 c646ac9aea74..754d545979e5 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 @@ -266,3 +266,75 @@ test('API route transaction includes nest guard span and span started in guard i // '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', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'unknown_error', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index dbf3c40ab171..cb3097b06228 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -72,6 +72,8 @@ export interface InjectableTarget { use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any canActivate?: (...args: any[]) => boolean | Promise | Observable; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transform?: (...args: any[]) => any; }; } @@ -212,6 +214,30 @@ export class SentryNestInstrumentation extends InstrumentationBase { }); } + // patch pipes + if (typeof target.prototype.transform === 'function') { + if (isPatched(target)) { + return original(options)(target); + } + + 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 original(options)(target); }; };