Skip to content

Commit da7c959

Browse files
authored
feat(node): Instrumentation for node-cron library (#9904)
### Usage ```ts import * as Sentry from "@sentry/node"; import cron from "node-cron"; const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron); cronWithCheckIn.schedule( "* * * * *", () => { console.log("running a task every minute"); }, { name: "my-cron-job" }, ); ```
1 parent 5cbe28b commit da7c959

File tree

8 files changed

+115
-2
lines changed

8 files changed

+115
-2
lines changed

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export {
6666
startInactiveSpan,
6767
startSpanManual,
6868
continueTrace,
69+
cron,
6970
} from '@sentry/node';
7071

7172
// We can still leave this for the carrier init and type exports

packages/bun/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export {
7373
metrics,
7474
} from '@sentry/core';
7575
export type { SpanStatusType } from '@sentry/core';
76-
export { autoDiscoverNodePerformanceMonitoringIntegrations } from '@sentry/node';
76+
export { autoDiscoverNodePerformanceMonitoringIntegrations, cron } from '@sentry/node';
7777

7878
export { BunClient } from './client';
7979
export { defaultIntegrations, init } from './sdk';

packages/node/src/cron/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const replacements: [string, string][] = [
4444
*/
4545
export function replaceCronNames(cronExpression: string): string {
4646
return replacements.reduce(
47+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
4748
(acc, [name, replacement]) => acc.replace(new RegExp(name, 'gi'), replacement),
4849
cronExpression,
4950
);

packages/node/src/cron/node-cron.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { withMonitor } from '@sentry/core';
2+
import { replaceCronNames } from './common';
3+
4+
export interface NodeCronOptions {
5+
name?: string;
6+
timezone?: string;
7+
}
8+
9+
export interface NodeCron {
10+
schedule: (cronExpression: string, callback: () => void, options?: NodeCronOptions) => unknown;
11+
}
12+
13+
/**
14+
* Wraps the `node-cron` library with check-in monitoring.
15+
*
16+
* ```ts
17+
* import * as Sentry from "@sentry/node";
18+
* import cron from "node-cron";
19+
*
20+
* const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron);
21+
*
22+
* cronWithCheckIn.schedule(
23+
* "* * * * *",
24+
* () => {
25+
* console.log("running a task every minute");
26+
* },
27+
* { name: "my-cron-job" },
28+
* );
29+
* ```
30+
*/
31+
export function instrumentNodeCron<T>(lib: Partial<NodeCron> & T): T {
32+
return new Proxy(lib, {
33+
get(target, prop: keyof NodeCron) {
34+
if (prop === 'schedule' && target.schedule) {
35+
// When 'get' is called for schedule, return a proxied version of the schedule function
36+
return new Proxy(target.schedule, {
37+
apply(target, thisArg, argArray: Parameters<NodeCron['schedule']>) {
38+
const [expression, _, options] = argArray;
39+
40+
if (!options?.name) {
41+
throw new Error('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.');
42+
}
43+
44+
return withMonitor(
45+
options.name,
46+
() => {
47+
return target.apply(thisArg, argArray);
48+
},
49+
{
50+
schedule: { type: 'crontab', value: replaceCronNames(expression) },
51+
timezone: options?.timezone,
52+
},
53+
);
54+
},
55+
});
56+
} else {
57+
return target[prop];
58+
}
59+
},
60+
});
61+
}

packages/node/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ export { INTEGRATIONS as Integrations, Handlers };
120120
export { hapiErrorPlugin } from './integrations/hapi';
121121

122122
import { instrumentCron } from './cron/cron';
123+
import { instrumentNodeCron } from './cron/node-cron';
123124

124125
/** Methods to instrument cron libraries for Sentry check-ins */
125126
export const cron = {
126127
instrumentCron,
128+
instrumentNodeCron,
127129
};

packages/node/test/cron.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import * as SentryCore from '@sentry/core';
22

33
import { cron } from '../src';
44
import type { CronJob, CronJobParams } from '../src/cron/cron';
5+
import type { NodeCron, NodeCronOptions } from '../src/cron/node-cron';
56

6-
describe('cron', () => {
7+
describe('cron check-ins', () => {
78
let withMonitorSpy: jest.SpyInstance;
89

910
beforeEach(() => {
@@ -78,4 +79,49 @@ describe('cron', () => {
7879
});
7980
});
8081
});
82+
83+
describe('node-cron', () => {
84+
test('calls withMonitor', done => {
85+
expect.assertions(5);
86+
87+
const nodeCron: NodeCron = {
88+
schedule: (expression: string, callback: () => void, options?: NodeCronOptions): unknown => {
89+
expect(expression).toBe('* * * Jan,Sep Sun');
90+
expect(callback).toBeInstanceOf(Function);
91+
expect(options?.name).toBe('my-cron-job');
92+
return callback();
93+
},
94+
};
95+
96+
const cronWithCheckIn = cron.instrumentNodeCron(nodeCron);
97+
98+
cronWithCheckIn.schedule(
99+
'* * * Jan,Sep Sun',
100+
() => {
101+
expect(withMonitorSpy).toHaveBeenCalledTimes(1);
102+
expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), {
103+
schedule: { type: 'crontab', value: '* * * 1,9 0' },
104+
});
105+
done();
106+
},
107+
{ name: 'my-cron-job' },
108+
);
109+
});
110+
111+
test('throws without supplied name', () => {
112+
const nodeCron: NodeCron = {
113+
schedule: (): unknown => {
114+
return undefined;
115+
},
116+
};
117+
118+
const cronWithCheckIn = cron.instrumentNodeCron(nodeCron);
119+
120+
expect(() => {
121+
cronWithCheckIn.schedule('* * * * *', () => {
122+
//
123+
});
124+
}).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.');
125+
});
126+
});
81127
});

packages/remix/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export {
5757
deepReadDirSync,
5858
Integrations,
5959
Handlers,
60+
cron,
6061
} from '@sentry/node';
6162

6263
// Keeping the `*` exports for backwards compatibility and types

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export {
6363
startInactiveSpan,
6464
startSpanManual,
6565
continueTrace,
66+
cron,
6667
} from '@sentry/node';
6768

6869
// We can still leave this for the carrier init and type exports

0 commit comments

Comments
 (0)