Skip to content

Commit

Permalink
feat(nestjs): Add function-level span decorator to nestjs (#12721)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicohrubec authored Jul 4, 2024
1 parent f8f3c98 commit 41d946e
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export class AppController1 {
async testOutgoingHttpExternalDisallowed() {
return this.appService.testOutgoingHttpExternalDisallowed();
}

@Get('test-span-decorator-async')
async testSpanDecoratorAsync() {
return { result: await this.appService.testSpanDecoratorAsync() };
}

@Get('test-span-decorator-sync')
async testSpanDecoratorSync() {
return { result: await this.appService.testSpanDecoratorSync() };
}
}

@Controller()
Expand Down
20 changes: 20 additions & 0 deletions dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';
import { SentryTraced } from '@sentry/nestjs';
import { makeHttpRequest } from './utils';

@Injectable()
Expand Down Expand Up @@ -75,6 +76,25 @@ export class AppService1 {
async testOutgoingHttpExternalDisallowed() {
return makeHttpRequest('http://localhost:3040/external-disallowed');
}

@SentryTraced('wait and return a string')
async wait() {
await new Promise(resolve => setTimeout(resolve, 500));
return 'test';
}

async testSpanDecoratorAsync() {
return await this.wait();
}

@SentryTraced('return a string')
getString(): string {
return 'test';
}

async testSpanDecoratorSync() {
return this.getString();
}
}

@Injectable()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction === 'GET /test-span-decorator-async'
);
});

const response = await fetch(`${baseURL}/test-span-decorator-async`);
const body = await response.json();

expect(body.result).toEqual('test');

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
span_id: expect.any(String),
trace_id: expect.any(String),
data: {
'sentry.origin': 'manual',
'sentry.op': 'wait and return a string',
'otel.kind': 'INTERNAL',
},
description: 'wait',
parent_span_id: expect.any(String),
start_timestamp: expect.any(Number),
status: 'ok',
op: 'wait and return a string',
origin: 'manual',
}),
]),
);
});

test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction === 'GET /test-span-decorator-sync'
);
});

const response = await fetch(`${baseURL}/test-span-decorator-sync`);
const body = await response.json();

expect(body.result).toEqual('test');

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
span_id: expect.any(String),
trace_id: expect.any(String),
data: {
'sentry.origin': 'manual',
'sentry.op': 'return a string',
'otel.kind': 'INTERNAL',
},
description: 'getString',
parent_span_id: expect.any(String),
start_timestamp: expect.any(Number),
status: 'ok',
op: 'return a string',
origin: 'manual',
}),
]),
);
});
18 changes: 18 additions & 0 deletions packages/nestjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ Sentry.init({

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

## Span Decorator

Use the @SentryTraced() decorator to gain additional performance insights for any function within your NestJS
application.

```js
import { Injectable } from '@nestjs/common';
import { SentryTraced } from '@sentry/nestjs';

@Injectable()
export class ExampleService {
@SentryTraced('example function')
async performTask() {
// Your business logic here
}
}
```

## Links

- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nestjs/)
2 changes: 2 additions & 0 deletions packages/nestjs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from '@sentry/node';

export { init } from './sdk';

export { SentryTraced } from './span-decorator';
25 changes: 25 additions & 0 deletions packages/nestjs/src/span-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { startSpan } from '@sentry/node';

/**
* A decorator usable to wrap arbitrary functions with spans.
*/
export function SentryTraced(op: string = 'function') {
return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalMethod = descriptor.value as (...args: any[]) => Promise<any>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = function (...args: any[]) {
return startSpan(
{
op: op,
name: propertyKey,
},
async () => {
return originalMethod.apply(this, args);
},
);
};
return descriptor;
};
}

0 comments on commit 41d946e

Please sign in to comment.