Skip to content

Commit

Permalink
feat(nestjs): Add nest cron monitoring support (#12781)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicohrubec authored Jul 9, 2024
1 parent 9b5cf26 commit 580e6a4
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/schedule": "^4.1.0",
"@nestjs/platform-express": "^10.0.0",
"@sentry/nestjs": "latest || *",
"@sentry/types": "latest || *",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export class AppController1 {
async testSpanDecoratorSync() {
return { result: await this.appService.testSpanDecoratorSync() };
}

@Get('kill-test-cron')
async killTestCron() {
this.appService.killTestCron();
}
}

@Controller()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { AppController1, AppController2 } from './app.controller';
import { AppService1, AppService2 } from './app.service';

@Module({
imports: [],
imports: [ScheduleModule.forRoot()],
controllers: [AppController1],
providers: [AppService1],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
import * as Sentry from '@sentry/nestjs';
import { SentryTraced } from '@sentry/nestjs';
import { SentryCron, SentryTraced } from '@sentry/nestjs';
import type { MonitorConfig } from '@sentry/types';
import { makeHttpRequest } from './utils';

const monitorConfig: MonitorConfig = {
schedule: {
type: 'crontab',
value: '* * * * *',
},
};

@Injectable()
export class AppService1 {
constructor(private schedulerRegistry: SchedulerRegistry) {}

testSuccess() {
return { version: 'v1' };
}
Expand Down Expand Up @@ -95,6 +106,21 @@ export class AppService1 {
async testSpanDecoratorSync() {
return this.getString();
}

/*
Actual cron schedule differs from schedule defined in config because Sentry
only supports minute granularity, but we don't want to wait (worst case) a
full minute for the tests to finish.
*/
@Cron('*/5 * * * * *', { name: 'test-cron-job' })
@SentryCron('test-cron-slug', monitorConfig)
async testCron() {
console.log('Test cron!');
}

async killTestCron() {
this.schedulerRegistry.deleteCronJob('test-cron-job');
}
}

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

test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => {
const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => {
return envelope[0].type === 'check_in';
});

const inProgressEnvelope = await inProgressEnvelopePromise;

expect(inProgressEnvelope[1]).toEqual(
expect.objectContaining({
check_in_id: expect.any(String),
monitor_slug: 'test-cron-slug',
status: 'in_progress',
environment: 'qa',
monitor_config: {
schedule: {
type: 'crontab',
value: '* * * * *',
},
},
contexts: {
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
},
},
}),
);

// kill cron so tests don't get stuck
await fetch(`${baseURL}/kill-test-cron`);
});
24 changes: 24 additions & 0 deletions packages/nestjs/src/cron-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Sentry from '@sentry/node';
import type { MonitorConfig } from '@sentry/types';

/**
* A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry.
*/
export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): MethodDecorator => {
return (target: unknown, propertyKey, 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 Sentry.withMonitor(
monitorSlug,
() => {
return originalMethod.apply(this, args);
},
monitorConfig,
);
};
return descriptor;
};
};
1 change: 1 addition & 0 deletions packages/nestjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from '@sentry/node';
export { init } from './sdk';

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

0 comments on commit 580e6a4

Please sign in to comment.