diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts index fee43f0d57b6..bc75e27df2ff 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts @@ -28,6 +28,21 @@ export class AppController { return firstValueFrom(this.client.send({ cmd: 'manual-capture' }, {})); } + @Get('test-microservice-guard') + async testMicroserviceGuard() { + return firstValueFrom(this.client.send({ cmd: 'test-guard' }, {})); + } + + @Get('test-microservice-interceptor') + async testMicroserviceInterceptor() { + return firstValueFrom(this.client.send({ cmd: 'test-interceptor' }, {})); + } + + @Get('test-microservice-pipe') + async testMicroservicePipe() { + return firstValueFrom(this.client.send({ cmd: 'test-pipe' }, { value: 123 })); + } + @Get('flush') async flush() { await flush(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.guard.ts new file mode 100644 index 000000000000..20d099870271 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.guard.ts @@ -0,0 +1,8 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +@Injectable() +export class ExampleGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + return true; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.interceptor.ts new file mode 100644 index 000000000000..e089f9e7f92e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.interceptor.ts @@ -0,0 +1,8 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; + +@Injectable() +export class ExampleInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + return next.handle(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.pipe.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.pipe.ts new file mode 100644 index 000000000000..65b26616b89f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.pipe.ts @@ -0,0 +1,8 @@ +import { Injectable, PipeTransform } from '@nestjs/common'; + +@Injectable() +export class ExamplePipe implements PipeTransform { + transform(value: any) { + return value; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts index eda6c5b6810c..0925e30bcc77 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts @@ -1,6 +1,9 @@ -import { Controller } from '@nestjs/common'; +import { Controller, UseGuards, UseInterceptors, UsePipes } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import * as Sentry from '@sentry/nestjs'; +import { ExampleGuard } from './example.guard'; +import { ExampleInterceptor } from './example.interceptor'; +import { ExamplePipe } from './example.pipe'; @Controller() export class MicroserviceController { @@ -25,4 +28,22 @@ export class MicroserviceController { } return { success: true }; } + + @UseGuards(ExampleGuard) + @MessagePattern({ cmd: 'test-guard' }) + testGuard(): { result: string } { + return { result: 'guard-handled' }; + } + + @UseInterceptors(ExampleInterceptor) + @MessagePattern({ cmd: 'test-interceptor' }) + testInterceptor(): { result: string } { + return { result: 'interceptor-handled' }; + } + + @UsePipes(ExamplePipe) + @MessagePattern({ cmd: 'test-pipe' }) + testPipe(data: { value: number }): { result: number } { + return { result: data.value }; + } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts index c504336258f4..ba2343a5277a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts @@ -22,11 +22,13 @@ test('Sends an HTTP transaction', async ({ baseURL }) => { ); }); -// Trace context does not propagate over NestJS TCP transport. -// The manual span created inside the microservice handler is orphaned, not a child of the HTTP transaction. -// This test documents this gap — if trace propagation is ever fixed, test.fail() will alert us. -test.fail('Microservice spans are captured as children of the HTTP transaction', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-microservices', transactionEvent => { +// Trace context does not propagate over NestJS TCP transport, so RPC spans are disconnected from +// the HTTP transaction. Instead of appearing as child spans of the HTTP transaction, auto-instrumented +// NestJS guard/interceptor/pipe spans become separate standalone transactions. +// This documents the current (broken) behavior — ideally these should be connected to the HTTP trace. + +test('Microservice spans are not connected to the HTTP transaction', async ({ baseURL }) => { + const httpTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-microservice-sum' @@ -36,19 +38,48 @@ test.fail('Microservice spans are captured as children of the HTTP transaction', const response = await fetch(`${baseURL}/test-microservice-sum`); expect(response.status).toBe(200); - const body = await response.json(); - expect(body.result).toBe(6); + const httpTransaction = await httpTransactionPromise; - const transactionEvent = await transactionEventPromise; + // The microservice span should be part of this transaction but isn't due to missing trace propagation + const microserviceSpan = httpTransaction.spans?.find(span => span.description === 'microservice-sum-operation'); + expect(microserviceSpan).toBeUndefined(); +}); - expect(transactionEvent.contexts?.trace).toEqual( - expect.objectContaining({ - op: 'http.server', - status: 'ok', - }), - ); +test('Microservice guard is emitted as a standalone transaction instead of being part of the HTTP trace', async ({ + baseURL, +}) => { + const guardTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => { + return transactionEvent?.transaction === 'ExampleGuard'; + }); + + await fetch(`${baseURL}/test-microservice-guard`); + + const guardTransaction = await guardTransactionPromise; + expect(guardTransaction).toBeDefined(); +}); + +test('Microservice interceptor is emitted as a standalone transaction instead of being part of the HTTP trace', async ({ + baseURL, +}) => { + const interceptorTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => { + return transactionEvent?.transaction === 'ExampleInterceptor'; + }); + + await fetch(`${baseURL}/test-microservice-interceptor`); + + const interceptorTransaction = await interceptorTransactionPromise; + expect(interceptorTransaction).toBeDefined(); +}); + +test('Microservice pipe is emitted as a standalone transaction instead of being part of the HTTP trace', async ({ + baseURL, +}) => { + const pipeTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => { + return transactionEvent?.transaction === 'ExamplePipe'; + }); + + await fetch(`${baseURL}/test-microservice-pipe`); - const microserviceSpan = transactionEvent.spans?.find(span => span.description === 'microservice-sum-operation'); - expect(microserviceSpan).toBeDefined(); - expect(microserviceSpan.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); + const pipeTransaction = await pipeTransactionPromise; + expect(pipeTransaction).toBeDefined(); });