Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class ExampleGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ExamplePipe implements PipeTransform {
transform(value: any) {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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();
});
Loading