Skip to content

Commit 3e2c8f4

Browse files
authored
feat(node-experimental): Move cron code over (#10742)
Pending on #10741
1 parent fe41836 commit 3e2c8f4

File tree

7 files changed

+543
-1
lines changed

7 files changed

+543
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const replacements: [string, string][] = [
2+
['january', '1'],
3+
['february', '2'],
4+
['march', '3'],
5+
['april', '4'],
6+
['may', '5'],
7+
['june', '6'],
8+
['july', '7'],
9+
['august', '8'],
10+
['september', '9'],
11+
['october', '10'],
12+
['november', '11'],
13+
['december', '12'],
14+
['jan', '1'],
15+
['feb', '2'],
16+
['mar', '3'],
17+
['apr', '4'],
18+
['may', '5'],
19+
['jun', '6'],
20+
['jul', '7'],
21+
['aug', '8'],
22+
['sep', '9'],
23+
['oct', '10'],
24+
['nov', '11'],
25+
['dec', '12'],
26+
['sunday', '0'],
27+
['monday', '1'],
28+
['tuesday', '2'],
29+
['wednesday', '3'],
30+
['thursday', '4'],
31+
['friday', '5'],
32+
['saturday', '6'],
33+
['sun', '0'],
34+
['mon', '1'],
35+
['tue', '2'],
36+
['wed', '3'],
37+
['thu', '4'],
38+
['fri', '5'],
39+
['sat', '6'],
40+
];
41+
42+
/**
43+
* Replaces names in cron expressions
44+
*/
45+
export function replaceCronNames(cronExpression: string): string {
46+
return replacements.reduce(
47+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
48+
(acc, [name, replacement]) => acc.replace(new RegExp(name, 'gi'), replacement),
49+
cronExpression,
50+
);
51+
}
+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { withMonitor } from '@sentry/core';
2+
import { replaceCronNames } from './common';
3+
4+
export type CronJobParams = {
5+
cronTime: string | Date;
6+
onTick: (context: unknown, onComplete?: unknown) => void | Promise<void>;
7+
onComplete?: () => void | Promise<void>;
8+
start?: boolean | null;
9+
context?: unknown;
10+
runOnInit?: boolean | null;
11+
unrefTimeout?: boolean | null;
12+
} & (
13+
| {
14+
timeZone?: string | null;
15+
utcOffset?: never;
16+
}
17+
| {
18+
timeZone?: never;
19+
utcOffset?: number | null;
20+
}
21+
);
22+
23+
export type CronJob = {
24+
//
25+
};
26+
27+
export type CronJobConstructor = {
28+
from: (param: CronJobParams) => CronJob;
29+
30+
new (
31+
cronTime: CronJobParams['cronTime'],
32+
onTick: CronJobParams['onTick'],
33+
onComplete?: CronJobParams['onComplete'],
34+
start?: CronJobParams['start'],
35+
timeZone?: CronJobParams['timeZone'],
36+
context?: CronJobParams['context'],
37+
runOnInit?: CronJobParams['runOnInit'],
38+
utcOffset?: null,
39+
unrefTimeout?: CronJobParams['unrefTimeout'],
40+
): CronJob;
41+
new (
42+
cronTime: CronJobParams['cronTime'],
43+
onTick: CronJobParams['onTick'],
44+
onComplete?: CronJobParams['onComplete'],
45+
start?: CronJobParams['start'],
46+
timeZone?: null,
47+
context?: CronJobParams['context'],
48+
runOnInit?: CronJobParams['runOnInit'],
49+
utcOffset?: CronJobParams['utcOffset'],
50+
unrefTimeout?: CronJobParams['unrefTimeout'],
51+
): CronJob;
52+
};
53+
54+
const ERROR_TEXT = 'Automatic instrumentation of CronJob only supports crontab string';
55+
56+
/**
57+
* Instruments the `cron` library to send a check-in event to Sentry for each job execution.
58+
*
59+
* ```ts
60+
* import * as Sentry from '@sentry/node';
61+
* import { CronJob } from 'cron';
62+
*
63+
* const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job');
64+
*
65+
* // use the constructor
66+
* const job = new CronJobWithCheckIn('* * * * *', () => {
67+
* console.log('You will see this message every minute');
68+
* });
69+
*
70+
* // or from
71+
* const job = CronJobWithCheckIn.from({ cronTime: '* * * * *', onTick: () => {
72+
* console.log('You will see this message every minute');
73+
* });
74+
* ```
75+
*/
76+
export function instrumentCron<T>(lib: T & CronJobConstructor, monitorSlug: string): T {
77+
let jobScheduled = false;
78+
79+
return new Proxy(lib, {
80+
construct(target, args: ConstructorParameters<CronJobConstructor>) {
81+
const [cronTime, onTick, onComplete, start, timeZone, ...rest] = args;
82+
83+
if (typeof cronTime !== 'string') {
84+
throw new Error(ERROR_TEXT);
85+
}
86+
87+
if (jobScheduled) {
88+
throw new Error(`A job named '${monitorSlug}' has already been scheduled`);
89+
}
90+
91+
jobScheduled = true;
92+
93+
const cronString = replaceCronNames(cronTime);
94+
95+
function monitoredTick(context: unknown, onComplete?: unknown): void | Promise<void> {
96+
return withMonitor(
97+
monitorSlug,
98+
() => {
99+
return onTick(context, onComplete);
100+
},
101+
{
102+
schedule: { type: 'crontab', value: cronString },
103+
...(timeZone ? { timeZone } : {}),
104+
},
105+
);
106+
}
107+
108+
return new target(cronTime, monitoredTick, onComplete, start, timeZone, ...rest);
109+
},
110+
get(target, prop: keyof CronJobConstructor) {
111+
if (prop === 'from') {
112+
return (param: CronJobParams) => {
113+
const { cronTime, onTick, timeZone } = param;
114+
115+
if (typeof cronTime !== 'string') {
116+
throw new Error(ERROR_TEXT);
117+
}
118+
119+
if (jobScheduled) {
120+
throw new Error(`A job named '${monitorSlug}' has already been scheduled`);
121+
}
122+
123+
jobScheduled = true;
124+
125+
const cronString = replaceCronNames(cronTime);
126+
127+
param.onTick = (context: unknown, onComplete?: unknown) => {
128+
return withMonitor(
129+
monitorSlug,
130+
() => {
131+
return onTick(context, onComplete);
132+
},
133+
{
134+
schedule: { type: 'crontab', value: cronString },
135+
...(timeZone ? { timeZone } : {}),
136+
},
137+
);
138+
};
139+
140+
return target.from(param);
141+
};
142+
} else {
143+
return target[prop];
144+
}
145+
},
146+
});
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { instrumentCron } from './cron';
2+
import { instrumentNodeCron } from './node-cron';
3+
import { instrumentNodeSchedule } from './node-schedule';
4+
5+
/** Methods to instrument cron libraries for Sentry check-ins */
6+
export const cron = {
7+
instrumentCron,
8+
instrumentNodeCron,
9+
instrumentNodeSchedule,
10+
};
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { withMonitor } from '@sentry/core';
2+
import { replaceCronNames } from './common';
3+
4+
export interface NodeSchedule {
5+
scheduleJob(
6+
nameOrExpression: string | Date | object,
7+
expressionOrCallback: string | Date | object | (() => void),
8+
callback?: () => void,
9+
): unknown;
10+
}
11+
12+
/**
13+
* Instruments the `node-schedule` library to send a check-in event to Sentry for each job execution.
14+
*
15+
* ```ts
16+
* import * as Sentry from '@sentry/node';
17+
* import * as schedule from 'node-schedule';
18+
*
19+
* const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule);
20+
*
21+
* const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => {
22+
* console.log('You will see this message every minute');
23+
* });
24+
* ```
25+
*/
26+
export function instrumentNodeSchedule<T>(lib: T & NodeSchedule): T {
27+
return new Proxy(lib, {
28+
get(target, prop: keyof NodeSchedule) {
29+
if (prop === 'scheduleJob') {
30+
// eslint-disable-next-line @typescript-eslint/unbound-method
31+
return new Proxy(target.scheduleJob, {
32+
apply(target, thisArg, argArray: Parameters<NodeSchedule['scheduleJob']>) {
33+
const [nameOrExpression, expressionOrCallback] = argArray;
34+
35+
if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') {
36+
throw new Error(
37+
"Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string",
38+
);
39+
}
40+
41+
const monitorSlug = nameOrExpression;
42+
const expression = expressionOrCallback;
43+
44+
return withMonitor(
45+
monitorSlug,
46+
() => {
47+
return target.apply(thisArg, argArray);
48+
},
49+
{
50+
schedule: { type: 'crontab', value: replaceCronNames(expression) },
51+
},
52+
);
53+
},
54+
});
55+
}
56+
57+
return target[prop];
58+
},
59+
});
60+
}

packages/node-experimental/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export { createGetModuleFromFilename } from './utils/module';
2020
export { makeNodeTransport } from './transports';
2121
// eslint-disable-next-line deprecation/deprecation
2222
export { getCurrentHub } from './sdk/hub';
23+
export { cron } from './cron';
2324

2425
export type { Span, NodeOptions } from './types';
2526

@@ -36,7 +37,6 @@ export {
3637
contextLinesIntegration,
3738
nodeContextIntegration,
3839
localVariablesIntegration,
39-
cron,
4040
} from '@sentry/node';
4141

4242
export {

0 commit comments

Comments
 (0)