Skip to content

Commit d13e55f

Browse files
committed
Merge branch 'develop' into nh/nestjs-websockets-tracing-tests
2 parents 0df7f0a + f820401 commit d13e55f

25 files changed

Lines changed: 870 additions & 32 deletions

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
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.next
2+
.tmp_mock_uploads.json
3+
.tmp_chunks
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use client';
2+
3+
function getGreeting(name: string): string {
4+
return `Hello, ${name}! Welcome to the sourcemap test app.`;
5+
}
6+
7+
export default function ClientPage() {
8+
const greeting = getGreeting('World');
9+
return (
10+
<div>
11+
<h1>{greeting}</h1>
12+
<button
13+
onClick={() => {
14+
throw new Error('Test error from client page');
15+
}}
16+
>
17+
Throw Error
18+
</button>
19+
</div>
20+
);
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Layout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<html lang="en">
4+
<body>{children}</body>
5+
</html>
6+
);
7+
}

0 commit comments

Comments
 (0)