Skip to content

Commit 41d946e

Browse files
authored
feat(nestjs): Add function-level span decorator to nestjs (#12721)
1 parent f8f3c98 commit 41d946e

File tree

6 files changed

+149
-0
lines changed

6 files changed

+149
-0
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ export class AppController1 {
6969
async testOutgoingHttpExternalDisallowed() {
7070
return this.appService.testOutgoingHttpExternalDisallowed();
7171
}
72+
73+
@Get('test-span-decorator-async')
74+
async testSpanDecoratorAsync() {
75+
return { result: await this.appService.testSpanDecoratorAsync() };
76+
}
77+
78+
@Get('test-span-decorator-sync')
79+
async testSpanDecoratorSync() {
80+
return { result: await this.appService.testSpanDecoratorSync() };
81+
}
7282
}
7383

7484
@Controller()

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
22
import * as Sentry from '@sentry/nestjs';
3+
import { SentryTraced } from '@sentry/nestjs';
34
import { makeHttpRequest } from './utils';
45

56
@Injectable()
@@ -75,6 +76,25 @@ export class AppService1 {
7576
async testOutgoingHttpExternalDisallowed() {
7677
return makeHttpRequest('http://localhost:3040/external-disallowed');
7778
}
79+
80+
@SentryTraced('wait and return a string')
81+
async wait() {
82+
await new Promise(resolve => setTimeout(resolve, 500));
83+
return 'test';
84+
}
85+
86+
async testSpanDecoratorAsync() {
87+
return await this.wait();
88+
}
89+
90+
@SentryTraced('return a string')
91+
getString(): string {
92+
return 'test';
93+
}
94+
95+
async testSpanDecoratorSync() {
96+
return this.getString();
97+
}
7898
}
7999

80100
@Injectable()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => {
5+
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
6+
return (
7+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
8+
transactionEvent?.transaction === 'GET /test-span-decorator-async'
9+
);
10+
});
11+
12+
const response = await fetch(`${baseURL}/test-span-decorator-async`);
13+
const body = await response.json();
14+
15+
expect(body.result).toEqual('test');
16+
17+
const transactionEvent = await transactionEventPromise;
18+
19+
expect(transactionEvent.spans).toEqual(
20+
expect.arrayContaining([
21+
expect.objectContaining({
22+
span_id: expect.any(String),
23+
trace_id: expect.any(String),
24+
data: {
25+
'sentry.origin': 'manual',
26+
'sentry.op': 'wait and return a string',
27+
'otel.kind': 'INTERNAL',
28+
},
29+
description: 'wait',
30+
parent_span_id: expect.any(String),
31+
start_timestamp: expect.any(Number),
32+
status: 'ok',
33+
op: 'wait and return a string',
34+
origin: 'manual',
35+
}),
36+
]),
37+
);
38+
});
39+
40+
test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => {
41+
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
42+
return (
43+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
44+
transactionEvent?.transaction === 'GET /test-span-decorator-sync'
45+
);
46+
});
47+
48+
const response = await fetch(`${baseURL}/test-span-decorator-sync`);
49+
const body = await response.json();
50+
51+
expect(body.result).toEqual('test');
52+
53+
const transactionEvent = await transactionEventPromise;
54+
55+
expect(transactionEvent.spans).toEqual(
56+
expect.arrayContaining([
57+
expect.objectContaining({
58+
span_id: expect.any(String),
59+
trace_id: expect.any(String),
60+
data: {
61+
'sentry.origin': 'manual',
62+
'sentry.op': 'return a string',
63+
'otel.kind': 'INTERNAL',
64+
},
65+
description: 'getString',
66+
parent_span_id: expect.any(String),
67+
start_timestamp: expect.any(Number),
68+
status: 'ok',
69+
op: 'return a string',
70+
origin: 'manual',
71+
}),
72+
]),
73+
);
74+
});

packages/nestjs/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ Sentry.init({
3838

3939
Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**.
4040

41+
## Span Decorator
42+
43+
Use the @SentryTraced() decorator to gain additional performance insights for any function within your NestJS
44+
application.
45+
46+
```js
47+
import { Injectable } from '@nestjs/common';
48+
import { SentryTraced } from '@sentry/nestjs';
49+
50+
@Injectable()
51+
export class ExampleService {
52+
@SentryTraced('example function')
53+
async performTask() {
54+
// Your business logic here
55+
}
56+
}
57+
```
58+
4159
## Links
4260

4361
- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nestjs/)

packages/nestjs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from '@sentry/node';
22

33
export { init } from './sdk';
4+
5+
export { SentryTraced } from './span-decorator';

packages/nestjs/src/span-decorator.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { startSpan } from '@sentry/node';
2+
3+
/**
4+
* A decorator usable to wrap arbitrary functions with spans.
5+
*/
6+
export function SentryTraced(op: string = 'function') {
7+
return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
const originalMethod = descriptor.value as (...args: any[]) => Promise<any>;
10+
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12+
descriptor.value = function (...args: any[]) {
13+
return startSpan(
14+
{
15+
op: op,
16+
name: propertyKey,
17+
},
18+
async () => {
19+
return originalMethod.apply(this, args);
20+
},
21+
);
22+
};
23+
return descriptor;
24+
};
25+
}

0 commit comments

Comments
 (0)