Skip to content

Commit 580e6a4

Browse files
authored
feat(nestjs): Add nest cron monitoring support (#12781)
1 parent 9b5cf26 commit 580e6a4

File tree

7 files changed

+94
-2
lines changed

7 files changed

+94
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"@nestjs/common": "^10.0.0",
1919
"@nestjs/core": "^10.0.0",
20+
"@nestjs/schedule": "^4.1.0",
2021
"@nestjs/platform-express": "^10.0.0",
2122
"@sentry/nestjs": "latest || *",
2223
"@sentry/types": "latest || *",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export class AppController1 {
7979
async testSpanDecoratorSync() {
8080
return { result: await this.appService.testSpanDecoratorSync() };
8181
}
82+
83+
@Get('kill-test-cron')
84+
async killTestCron() {
85+
this.appService.killTestCron();
86+
}
8287
}
8388

8489
@Controller()

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Module } from '@nestjs/common';
2+
import { ScheduleModule } from '@nestjs/schedule';
23
import { AppController1, AppController2 } from './app.controller';
34
import { AppService1, AppService2 } from './app.service';
45

56
@Module({
6-
imports: [],
7+
imports: [ScheduleModule.forRoot()],
78
controllers: [AppController1],
89
providers: [AppService1],
910
})

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
23
import * as Sentry from '@sentry/nestjs';
3-
import { SentryTraced } from '@sentry/nestjs';
4+
import { SentryCron, SentryTraced } from '@sentry/nestjs';
5+
import type { MonitorConfig } from '@sentry/types';
46
import { makeHttpRequest } from './utils';
57

8+
const monitorConfig: MonitorConfig = {
9+
schedule: {
10+
type: 'crontab',
11+
value: '* * * * *',
12+
},
13+
};
14+
615
@Injectable()
716
export class AppService1 {
17+
constructor(private schedulerRegistry: SchedulerRegistry) {}
18+
819
testSuccess() {
920
return { version: 'v1' };
1021
}
@@ -95,6 +106,21 @@ export class AppService1 {
95106
async testSpanDecoratorSync() {
96107
return this.getString();
97108
}
109+
110+
/*
111+
Actual cron schedule differs from schedule defined in config because Sentry
112+
only supports minute granularity, but we don't want to wait (worst case) a
113+
full minute for the tests to finish.
114+
*/
115+
@Cron('*/5 * * * * *', { name: 'test-cron-job' })
116+
@SentryCron('test-cron-slug', monitorConfig)
117+
async testCron() {
118+
console.log('Test cron!');
119+
}
120+
121+
async killTestCron() {
122+
this.schedulerRegistry.deleteCronJob('test-cron-job');
123+
}
98124
}
99125

100126
@Injectable()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForEnvelopeItem } from '@sentry-internal/test-utils';
3+
4+
test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => {
5+
const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => {
6+
return envelope[0].type === 'check_in';
7+
});
8+
9+
const inProgressEnvelope = await inProgressEnvelopePromise;
10+
11+
expect(inProgressEnvelope[1]).toEqual(
12+
expect.objectContaining({
13+
check_in_id: expect.any(String),
14+
monitor_slug: 'test-cron-slug',
15+
status: 'in_progress',
16+
environment: 'qa',
17+
monitor_config: {
18+
schedule: {
19+
type: 'crontab',
20+
value: '* * * * *',
21+
},
22+
},
23+
contexts: {
24+
trace: {
25+
span_id: expect.any(String),
26+
trace_id: expect.any(String),
27+
},
28+
},
29+
}),
30+
);
31+
32+
// kill cron so tests don't get stuck
33+
await fetch(`${baseURL}/kill-test-cron`);
34+
});

packages/nestjs/src/cron-decorator.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as Sentry from '@sentry/node';
2+
import type { MonitorConfig } from '@sentry/types';
3+
4+
/**
5+
* A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry.
6+
*/
7+
export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): MethodDecorator => {
8+
return (target: unknown, propertyKey, descriptor: PropertyDescriptor) => {
9+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10+
const originalMethod = descriptor.value as (...args: any[]) => Promise<any>;
11+
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13+
descriptor.value = function (...args: any[]) {
14+
return Sentry.withMonitor(
15+
monitorSlug,
16+
() => {
17+
return originalMethod.apply(this, args);
18+
},
19+
monitorConfig,
20+
);
21+
};
22+
return descriptor;
23+
};
24+
};

packages/nestjs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from '@sentry/node';
33
export { init } from './sdk';
44

55
export { SentryTraced } from './span-decorator';
6+
export { SentryCron } from './cron-decorator';

0 commit comments

Comments
 (0)