Skip to content

Commit c21bf07

Browse files
authored
feat(nestjs): Instrument event handlers (#14307)
1 parent a5a214c commit c21bf07

File tree

14 files changed

+380
-4
lines changed

14 files changed

+380
-4
lines changed

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@nestjs/common": "^10.0.0",
1919
"@nestjs/core": "^10.0.0",
2020
"@nestjs/platform-express": "^10.0.0",
21+
"@nestjs/event-emitter": "^2.0.0",
2122
"@sentry/nestjs": "latest || *",
2223
"@sentry/types": "latest || *",
2324
"reflect-metadata": "^0.2.0",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Controller, Get } from '@nestjs/common';
2+
import { EventsService } from './events.service';
3+
4+
@Controller('events')
5+
export class EventsController {
6+
constructor(private readonly eventsService: EventsService) {}
7+
8+
@Get('emit')
9+
async emitEvents() {
10+
await this.eventsService.emitEvents();
11+
12+
return { message: 'Events emitted' };
13+
}
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Module } from '@nestjs/common';
2+
import { APP_FILTER } from '@nestjs/core';
3+
import { EventEmitterModule } from '@nestjs/event-emitter';
4+
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
5+
import { EventsController } from './events.controller';
6+
import { EventsService } from './events.service';
7+
import { TestEventListener } from './listeners/test-event.listener';
8+
9+
@Module({
10+
imports: [SentryModule.forRoot(), EventEmitterModule.forRoot()],
11+
controllers: [EventsController],
12+
providers: [
13+
{
14+
provide: APP_FILTER,
15+
useClass: SentryGlobalFilter,
16+
},
17+
EventsService,
18+
TestEventListener,
19+
],
20+
})
21+
export class EventsModule {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { EventEmitter2 } from '@nestjs/event-emitter';
3+
4+
@Injectable()
5+
export class EventsService {
6+
constructor(private readonly eventEmitter: EventEmitter2) {}
7+
8+
async emitEvents() {
9+
await this.eventEmitter.emit('myEvent.pass', { data: 'test' });
10+
await this.eventEmitter.emit('myEvent.throw');
11+
12+
return { message: 'Events emitted' };
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { OnEvent } from '@nestjs/event-emitter';
3+
4+
@Injectable()
5+
export class TestEventListener {
6+
@OnEvent('myEvent.pass')
7+
async handlePassEvent(payload: any): Promise<void> {
8+
await new Promise(resolve => setTimeout(resolve, 100));
9+
}
10+
11+
@OnEvent('myEvent.throw')
12+
async handleThrowEvent(): Promise<void> {
13+
await new Promise(resolve => setTimeout(resolve, 100));
14+
throw new Error('Test error from event handler');
15+
}
16+
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ import './instrument';
33

44
// Import other modules
55
import { NestFactory } from '@nestjs/core';
6+
import { EventsModule } from './events.module';
67
import { TraceInitiatorModule } from './trace-initiator.module';
78
import { TraceReceiverModule } from './trace-receiver.module';
89

910
const TRACE_INITIATOR_PORT = 3030;
1011
const TRACE_RECEIVER_PORT = 3040;
12+
const EVENTS_PORT = 3050;
1113

1214
async function bootstrap() {
1315
const trace_initiator_app = await NestFactory.create(TraceInitiatorModule);
1416
await trace_initiator_app.listen(TRACE_INITIATOR_PORT);
1517

1618
const trace_receiver_app = await NestFactory.create(TraceReceiverModule);
1719
await trace_receiver_app.listen(TRACE_RECEIVER_PORT);
20+
21+
const events_app = await NestFactory.create(EventsModule);
22+
await events_app.listen(EVENTS_PORT);
1823
}
1924

2025
bootstrap();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Event emitter', async () => {
5+
const eventErrorPromise = waitForError('nestjs-distributed-tracing', errorEvent => {
6+
return errorEvent.exception.values[0].value === 'Test error from event handler';
7+
});
8+
const successEventTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
9+
return transactionEvent.transaction === 'event myEvent.pass';
10+
});
11+
12+
const eventsUrl = `http://localhost:3050/events/emit`;
13+
await fetch(eventsUrl);
14+
15+
const eventError = await eventErrorPromise;
16+
const successEventTransaction = await successEventTransactionPromise;
17+
18+
expect(eventError.exception).toEqual({
19+
values: [
20+
{
21+
type: 'Error',
22+
value: 'Test error from event handler',
23+
stacktrace: expect.any(Object),
24+
mechanism: expect.any(Object),
25+
},
26+
],
27+
});
28+
29+
expect(successEventTransaction.contexts.trace).toEqual({
30+
parent_span_id: expect.any(String),
31+
span_id: expect.any(String),
32+
trace_id: expect.any(String),
33+
data: {
34+
'sentry.source': 'custom',
35+
'sentry.sample_rate': 1,
36+
'sentry.op': 'event.nestjs',
37+
'sentry.origin': 'auto.event.nestjs',
38+
},
39+
origin: 'auto.event.nestjs',
40+
op: 'event.nestjs',
41+
status: 'ok',
42+
});
43+
});

packages/nestjs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
"@sentry/utils": "8.38.0"
5151
},
5252
"devDependencies": {
53-
"@nestjs/common": "10.4.7",
54-
"@nestjs/core": "10.4.7"
53+
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
54+
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
5555
},
5656
"peerDependencies": {
5757
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",

packages/node/src/integrations/tracing/nest/helpers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget,
3636
};
3737
}
3838

39+
/**
40+
* Returns span options for nest event spans.
41+
*/
42+
export function getEventSpanOptions(event: string): {
43+
name: string;
44+
attributes: Record<string, string>;
45+
forceTransaction: boolean;
46+
} {
47+
return {
48+
name: `event ${event}`,
49+
attributes: {
50+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'event.nestjs',
51+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs',
52+
},
53+
forceTransaction: true,
54+
};
55+
}
56+
3957
/**
4058
* Adds instrumentation to a js observable and attaches the span to an active parent span.
4159
*/

packages/node/src/integrations/tracing/nest/nest.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import type { IntegrationFn, Span } from '@sentry/types';
1313
import { logger } from '@sentry/utils';
1414
import { generateInstrumentOnce } from '../../../otel/instrument';
15+
import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation';
1516
import { SentryNestInstrumentation } from './sentry-nest-instrumentation';
1617
import type { MinimalNestJsApp, NestJsErrorFilter } from './types';
1718

@@ -25,10 +26,15 @@ const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => {
2526
return new SentryNestInstrumentation();
2627
});
2728

29+
const instrumentNestEvent = generateInstrumentOnce('Nest-Event', () => {
30+
return new SentryNestEventInstrumentation();
31+
});
32+
2833
export const instrumentNest = Object.assign(
2934
(): void => {
3035
instrumentNestCore();
3136
instrumentNestCommon();
37+
instrumentNestEvent();
3238
},
3339
{ id: INTEGRATION_NAME },
3440
);

0 commit comments

Comments
 (0)