From c2622350ffc2be5ce16719484817fbe8e6f58593 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 5 Mar 2026 10:55:20 +0100 Subject: [PATCH 1/4] Add nestjs microservices e2e test application --- .../nestjs-microservices/.npmrc | 2 + .../nestjs-microservices/nest-cli.json | 8 +++ .../nestjs-microservices/package.json | 39 ++++++++++++++ .../playwright.config.mjs | 7 +++ .../src/app.controller.ts | 35 ++++++++++++ .../nestjs-microservices/src/app.module.ts | 30 +++++++++++ .../nestjs-microservices/src/instrument.ts | 11 ++++ .../nestjs-microservices/src/main.ts | 27 ++++++++++ .../src/microservice.controller.ts | 28 ++++++++++ .../start-event-proxy.mjs | 6 +++ .../nestjs-microservices/tests/errors.test.ts | 44 +++++++++++++++ .../tests/transactions.test.ts | 54 +++++++++++++++++++ .../nestjs-microservices/tsconfig.json | 22 ++++++++ 13 files changed, 313 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/nest-cli.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.module.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-microservices/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json b/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json new file mode 100644 index 000000000000..ee3ca5ebf816 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json @@ -0,0 +1,39 @@ +{ + "name": "nestjs-microservices", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "nest build", + "start": "nest start", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:build-latest": "pnpm install && pnpm add @nestjs/common@latest @nestjs/core@latest @nestjs/platform-express@latest @nestjs/microservices@latest && pnpm add -D @nestjs/cli@latest @nestjs/schematics@latest && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@sentry/nestjs": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@types/node": "^18.19.1", + "typescript": "~5.0.0" + }, + "pnpm": { + "overrides": { + "minimatch": "10.0.1" + } + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-microservices/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; 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 new file mode 100644 index 000000000000..fee43f0d57b6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get, Inject, Param } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { flush } from '@sentry/nestjs'; +import { firstValueFrom } from 'rxjs'; + +@Controller() +export class AppController { + constructor(@Inject('MATH_SERVICE') private readonly client: ClientProxy) {} + + @Get('test-transaction') + testTransaction() { + return { message: 'hello' }; + } + + @Get('test-microservice-sum') + async testMicroserviceSum() { + const result = await firstValueFrom(this.client.send({ cmd: 'sum' }, { numbers: [1, 2, 3] })); + return { result }; + } + + @Get('test-microservice-exception/:id') + async testMicroserviceException(@Param('id') id: string) { + return firstValueFrom(this.client.send({ cmd: 'exception' }, { id })); + } + + @Get('test-microservice-manual-capture') + async testMicroserviceManualCapture() { + return firstValueFrom(this.client.send({ cmd: 'manual-capture' }, {})); + } + + @Get('flush') + async flush() { + await flush(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.module.ts new file mode 100644 index 000000000000..5d206170b1df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; +import { AppController } from './app.controller'; +import { MicroserviceController } from './microservice.controller'; + +@Module({ + imports: [ + SentryModule.forRoot(), + ClientsModule.register([ + { + name: 'MATH_SERVICE', + transport: Transport.TCP, + options: { + host: '127.0.0.1', + port: 3040, + }, + }, + ]), + ], + controllers: [AppController, MicroserviceController], + providers: [ + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + ], +}) +export class AppModule {} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts new file mode 100644 index 000000000000..e0a1cead1153 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nestjs'; + +Sentry.init({ + environment: 'qa', + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1, + transportOptions: { + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/main.ts new file mode 100644 index 000000000000..e7a0bd17dc8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/main.ts @@ -0,0 +1,27 @@ +// Import this first +import './instrument'; + +// Import other modules +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { AppModule } from './app.module'; + +const PORT = 3030; +const MICROSERVICE_PORT = 3040; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.connectMicroservice({ + transport: Transport.TCP, + options: { + host: '127.0.0.1', + port: MICROSERVICE_PORT, + }, + }); + + await app.startAllMicroservices(); + await app.listen(PORT); +} + +bootstrap(); 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 new file mode 100644 index 000000000000..eda6c5b6810c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts @@ -0,0 +1,28 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import * as Sentry from '@sentry/nestjs'; + +@Controller() +export class MicroserviceController { + @MessagePattern({ cmd: 'sum' }) + sum(data: { numbers: number[] }): number { + return Sentry.startSpan({ name: 'microservice-sum-operation' }, () => { + return data.numbers.reduce((a, b) => a + b, 0); + }); + } + + @MessagePattern({ cmd: 'exception' }) + exception(data: { id: string }): never { + throw new Error(`Microservice exception with id ${data.id}`); + } + + @MessagePattern({ cmd: 'manual-capture' }) + manualCapture(): { success: boolean } { + try { + throw new Error('Manually captured microservice error'); + } catch (e) { + Sentry.captureException(e); + } + return { success: true }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-microservices/start-event-proxy.mjs new file mode 100644 index 000000000000..d7a398b8f798 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-microservices', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts new file mode 100644 index 000000000000..607e2ed4df50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Captures manually reported error in microservice handler', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-microservices', event => { + return !event.type && event.exception?.values?.[0]?.value === 'Manually captured microservice error'; + }); + + await fetch(`${baseURL}/test-microservice-manual-capture`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Manually captured microservice error'); +}); + +// There is no good mechanism to verify that an event was NOT sent to Sentry. +// The idea here is that we first send a message that triggers an exception which won't be auto-captured, +// and then send a message that triggers a manually captured error which will be sent to Sentry. +// If the manually captured error arrives, we can deduce that the first exception was not sent, +// because both requests go through the same NestJS app and Sentry client, so events are processed in order. +test('Does not automatically capture exceptions thrown in microservice handler', async ({ baseURL }) => { + let autoCaptureFired = false; + + waitForError('nestjs-microservices', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'Microservice exception with id 123') { + autoCaptureFired = true; + } + return false; + }); + + const manualCapturePromise = waitForError('nestjs-microservices', event => { + return !event.type && event.exception?.values?.[0]?.value === 'Manually captured microservice error'; + }); + + await fetch(`${baseURL}/test-microservice-exception/123`); + await fetch(`${baseURL}/test-microservice-manual-capture`); + + await manualCapturePromise; + + await fetch(`${baseURL}/flush`); + + expect(autoCaptureFired).toBe(false); +}); 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 new file mode 100644 index 000000000000..c504336258f4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an HTTP transaction', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-microservices', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + const response = await fetch(`${baseURL}/test-transaction`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual( + expect.objectContaining({ + op: 'http.server', + status: 'ok', + }), + ); +}); + +// 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 => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-microservice-sum' + ); + }); + + const response = await fetch(`${baseURL}/test-microservice-sum`); + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.result).toBe(6); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual( + expect.objectContaining({ + op: 'http.server', + status: 'ok', + }), + ); + + const microserviceSpan = transactionEvent.spans?.find(span => span.description === 'microservice-sum-operation'); + expect(microserviceSpan).toBeDefined(); + expect(microserviceSpan.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tsconfig.json new file mode 100644 index 000000000000..cf79f029c781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "moduleResolution": "Node16" + } +} From 705767cc99a5f43038887fa2e76894d98dace4f5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 5 Mar 2026 11:03:45 +0100 Subject: [PATCH 2/4] Updates --- .../nestjs-microservices/tests/errors.test.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts index 607e2ed4df50..4aaa9b4878be 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Captures manually reported error in microservice handler', async ({ baseURL }) => { const errorEventPromise = waitForError('nestjs-microservices', event => { @@ -14,11 +14,9 @@ test('Captures manually reported error in microservice handler', async ({ baseUR expect(errorEvent.exception?.values?.[0]?.value).toBe('Manually captured microservice error'); }); -// There is no good mechanism to verify that an event was NOT sent to Sentry. -// The idea here is that we first send a message that triggers an exception which won't be auto-captured, -// and then send a message that triggers a manually captured error which will be sent to Sentry. -// If the manually captured error arrives, we can deduce that the first exception was not sent, -// because both requests go through the same NestJS app and Sentry client, so events are processed in order. +// To verify that an exception is NOT automatically captured, we trigger it, +// wait for the transaction from that request to confirm it completed, flush, +// and then assert no error event was received. test('Does not automatically capture exceptions thrown in microservice handler', async ({ baseURL }) => { let autoCaptureFired = false; @@ -29,14 +27,13 @@ test('Does not automatically capture exceptions thrown in microservice handler', return false; }); - const manualCapturePromise = waitForError('nestjs-microservices', event => { - return !event.type && event.exception?.values?.[0]?.value === 'Manually captured microservice error'; + const transactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-microservice-exception/:id'; }); await fetch(`${baseURL}/test-microservice-exception/123`); - await fetch(`${baseURL}/test-microservice-manual-capture`); - await manualCapturePromise; + await transactionPromise; await fetch(`${baseURL}/flush`); From e686dd46aaaa304b6ddff06f0735eea5cf92399a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 5 Mar 2026 11:06:34 +0100 Subject: [PATCH 3/4] canary --- .github/workflows/canary.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 252bbc831239..d17505ac94ee 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -114,6 +114,9 @@ jobs: - test-application: 'nestjs-11' build-command: 'test:build-latest' label: 'nestjs-11 (latest)' + - test-application: 'nestjs-microservices' + build-command: 'test:build-latest' + label: 'nestjs-microservices (latest)' steps: - name: Check out current commit From 8591c49afa59ef448ca9f49a6e48f26986be3480 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 5 Mar 2026 12:51:00 +0100 Subject: [PATCH 4/4] Expand microservices nestjs e2e application with tracing tests --- .../src/app.controller.ts | 15 +++++ .../nestjs-microservices/src/example.guard.ts | 8 +++ .../src/example.interceptor.ts | 8 +++ .../nestjs-microservices/src/example.pipe.ts | 8 +++ .../src/microservice.controller.ts | 24 ++++++- .../tests/transactions.test.ts | 66 ++++++++++++++----- 6 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.guard.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-microservices/src/example.pipe.ts 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..cddb43aa908f 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,23 @@ 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..837b4bb58347 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,49 @@ 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 microserviceSpan = transactionEvent.spans?.find(span => span.description === 'microservice-sum-operation'); - expect(microserviceSpan).toBeDefined(); - expect(microserviceSpan.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id); + 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 pipeTransaction = await pipeTransactionPromise; + expect(pipeTransaction).toBeDefined(); +}); +