Skip to content

Commit 0891028

Browse files
authored
chore(e2e): Expand microservices E2E application with auto-tracing tests (#19652)
Auto-instrumentation for guards, interceptors and pipes seems to work out of the box. However, trace propagation seems broken since they are not attached to the main http transaction, instead they become standalone transactions. Add some tests to document current behavior. Closes #19648
1 parent 2109509 commit 0891028

File tree

6 files changed

+109
-18
lines changed

6 files changed

+109
-18
lines changed

dev-packages/e2e-tests/test-applications/nestjs-microservices/src/app.controller.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ export class AppController {
2828
return firstValueFrom(this.client.send({ cmd: 'manual-capture' }, {}));
2929
}
3030

31+
@Get('test-microservice-guard')
32+
async testMicroserviceGuard() {
33+
return firstValueFrom(this.client.send({ cmd: 'test-guard' }, {}));
34+
}
35+
36+
@Get('test-microservice-interceptor')
37+
async testMicroserviceInterceptor() {
38+
return firstValueFrom(this.client.send({ cmd: 'test-interceptor' }, {}));
39+
}
40+
41+
@Get('test-microservice-pipe')
42+
async testMicroservicePipe() {
43+
return firstValueFrom(this.client.send({ cmd: 'test-pipe' }, { value: 123 }));
44+
}
45+
3146
@Get('flush')
3247
async flush() {
3348
await flush();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class ExampleGuard implements CanActivate {
5+
canActivate(context: ExecutionContext): boolean {
6+
return true;
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class ExampleInterceptor implements NestInterceptor {
5+
intercept(context: ExecutionContext, next: CallHandler) {
6+
return next.handle();
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Injectable, PipeTransform } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class ExamplePipe implements PipeTransform {
5+
transform(value: any) {
6+
return value;
7+
}
8+
}

dev-packages/e2e-tests/test-applications/nestjs-microservices/src/microservice.controller.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { Controller } from '@nestjs/common';
1+
import { Controller, UseGuards, UseInterceptors, UsePipes } from '@nestjs/common';
22
import { MessagePattern } from '@nestjs/microservices';
33
import * as Sentry from '@sentry/nestjs';
4+
import { ExampleGuard } from './example.guard';
5+
import { ExampleInterceptor } from './example.interceptor';
6+
import { ExamplePipe } from './example.pipe';
47

58
@Controller()
69
export class MicroserviceController {
@@ -25,4 +28,22 @@ export class MicroserviceController {
2528
}
2629
return { success: true };
2730
}
31+
32+
@UseGuards(ExampleGuard)
33+
@MessagePattern({ cmd: 'test-guard' })
34+
testGuard(): { result: string } {
35+
return { result: 'guard-handled' };
36+
}
37+
38+
@UseInterceptors(ExampleInterceptor)
39+
@MessagePattern({ cmd: 'test-interceptor' })
40+
testInterceptor(): { result: string } {
41+
return { result: 'interceptor-handled' };
42+
}
43+
44+
@UsePipes(ExamplePipe)
45+
@MessagePattern({ cmd: 'test-pipe' })
46+
testPipe(data: { value: number }): { result: number } {
47+
return { result: data.value };
48+
}
2849
}

dev-packages/e2e-tests/test-applications/nestjs-microservices/tests/transactions.test.ts

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ test('Sends an HTTP transaction', async ({ baseURL }) => {
2222
);
2323
});
2424

25-
// Trace context does not propagate over NestJS TCP transport.
26-
// The manual span created inside the microservice handler is orphaned, not a child of the HTTP transaction.
27-
// This test documents this gap — if trace propagation is ever fixed, test.fail() will alert us.
28-
test.fail('Microservice spans are captured as children of the HTTP transaction', async ({ baseURL }) => {
29-
const transactionEventPromise = waitForTransaction('nestjs-microservices', transactionEvent => {
25+
// Trace context does not propagate over NestJS TCP transport, so RPC spans are disconnected from
26+
// the HTTP transaction. Instead of appearing as child spans of the HTTP transaction, auto-instrumented
27+
// NestJS guard/interceptor/pipe spans become separate standalone transactions.
28+
// This documents the current (broken) behavior — ideally these should be connected to the HTTP trace.
29+
30+
test('Microservice spans are not connected to the HTTP transaction', async ({ baseURL }) => {
31+
const httpTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => {
3032
return (
3133
transactionEvent?.contexts?.trace?.op === 'http.server' &&
3234
transactionEvent?.transaction === 'GET /test-microservice-sum'
@@ -36,19 +38,48 @@ test.fail('Microservice spans are captured as children of the HTTP transaction',
3638
const response = await fetch(`${baseURL}/test-microservice-sum`);
3739
expect(response.status).toBe(200);
3840

39-
const body = await response.json();
40-
expect(body.result).toBe(6);
41+
const httpTransaction = await httpTransactionPromise;
4142

42-
const transactionEvent = await transactionEventPromise;
43+
// The microservice span should be part of this transaction but isn't due to missing trace propagation
44+
const microserviceSpan = httpTransaction.spans?.find(span => span.description === 'microservice-sum-operation');
45+
expect(microserviceSpan).toBeUndefined();
46+
});
4347

44-
expect(transactionEvent.contexts?.trace).toEqual(
45-
expect.objectContaining({
46-
op: 'http.server',
47-
status: 'ok',
48-
}),
49-
);
48+
test('Microservice guard is emitted as a standalone transaction instead of being part of the HTTP trace', async ({
49+
baseURL,
50+
}) => {
51+
const guardTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => {
52+
return transactionEvent?.transaction === 'ExampleGuard';
53+
});
54+
55+
await fetch(`${baseURL}/test-microservice-guard`);
56+
57+
const guardTransaction = await guardTransactionPromise;
58+
expect(guardTransaction).toBeDefined();
59+
});
60+
61+
test('Microservice interceptor is emitted as a standalone transaction instead of being part of the HTTP trace', async ({
62+
baseURL,
63+
}) => {
64+
const interceptorTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => {
65+
return transactionEvent?.transaction === 'ExampleInterceptor';
66+
});
67+
68+
await fetch(`${baseURL}/test-microservice-interceptor`);
69+
70+
const interceptorTransaction = await interceptorTransactionPromise;
71+
expect(interceptorTransaction).toBeDefined();
72+
});
73+
74+
test('Microservice pipe is emitted as a standalone transaction instead of being part of the HTTP trace', async ({
75+
baseURL,
76+
}) => {
77+
const pipeTransactionPromise = waitForTransaction('nestjs-microservices', transactionEvent => {
78+
return transactionEvent?.transaction === 'ExamplePipe';
79+
});
80+
81+
await fetch(`${baseURL}/test-microservice-pipe`);
5082

51-
const microserviceSpan = transactionEvent.spans?.find(span => span.description === 'microservice-sum-operation');
52-
expect(microserviceSpan).toBeDefined();
53-
expect(microserviceSpan.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id);
83+
const pipeTransaction = await pipeTransactionPromise;
84+
expect(pipeTransaction).toBeDefined();
5485
});

0 commit comments

Comments
 (0)