Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/node/src/cron/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { instrumentNodeCron } from './node-cron';

/**
* Methods to instrument cron libraries for Sentry check-ins.
*/
export const cron = {
instrumentNodeCron,
};
108 changes: 108 additions & 0 deletions packages/node/src/cron/node-cron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { withMonitor } from '@sentry/core';

export interface NodeCronOptions {
name?: string;
timezone?: string;
}

export interface NodeCron {
schedule: (cronExpression: string, callback: () => void, options?: NodeCronOptions) => unknown;
}

const replacements: [string, string][] = [
['january', '1'],
['february', '2'],
['march', '3'],
['april', '4'],
['may', '5'],
['june', '6'],
['july', '7'],
['august', '8'],
['september', '9'],
['october', '10'],
['november', '11'],
['december', '12'],
['jan', '1'],
['feb', '2'],
['mar', '3'],
['apr', '4'],
['may', '5'],
['jun', '6'],
['jul', '7'],
['aug', '8'],
['sep', '9'],
['oct', '10'],
['nov', '11'],
['dec', '12'],
['sunday', '0'],
['monday', '1'],
['tuesday', '2'],
['wednesday', '3'],
['thursday', '4'],
['friday', '5'],
['saturday', '6'],
['sun', '0'],
['mon', '1'],
['tue', '2'],
['wed', '3'],
['thu', '4'],
['fri', '5'],
['sat', '6'],
];

function toSentryCrontab(cronExpression: string): string {
return replacements.reduce(
(acc, [name, replacement]) => acc.replace(new RegExp(name, 'gi'), replacement),
cronExpression,
);
}

/**
* Wraps the node-cron library with check-in monitoring.
*
* ```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" },
* );
* ```
*/
export function instrumentNodeCron<T>(lib: Partial<NodeCron> & T): T {
return new Proxy(lib, {
get(target, prop: keyof NodeCron) {
if (prop === 'schedule' && target.schedule) {
// When 'get' is called for schedule, return a proxied version of the schedule function
return new Proxy(target.schedule, {
apply(target, thisArg, argArray: Parameters<NodeCron['schedule']>) {
const [expression, _, options] = argArray;

if (!options?.name) {
throw new Error('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.');
}

return withMonitor(
options.name,
() => {
return target.apply(thisArg, argArray);
},
{
schedule: { type: 'crontab', value: toSentryCrontab(expression) },
timezone: options?.timezone,
},
);
},
});
} else {
return target[prop];
}
},
});
}
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from
export { deepReadDirSync } from './utils';
export { getModuleFromFilename } from './module';
export { enableAnrDetection, isAnrChildProcess } from './anr';
export { cron } from './cron';

import { Integrations as CoreIntegrations } from '@sentry/core';

Expand Down
59 changes: 59 additions & 0 deletions packages/node/test/cron.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as SentryCore from '@sentry/core';

import type { NodeCron, NodeCronOptions } from '../src/cron/node-cron';
import { instrumentNodeCron } from '../src/cron/node-cron';

describe('cron', () => {
let withMonitorSpy: jest.SpyInstance;

beforeEach(() => {
withMonitorSpy = jest.spyOn(SentryCore, 'withMonitor');
});

afterEach(() => {
jest.restoreAllMocks();
});

describe('node-cron', () => {
test('calls withMonitor', done => {
const nodeCron: NodeCron = {
schedule: (expression: string, callback: () => void, options?: NodeCronOptions): unknown => {
expect(expression).toBe('* * * Jan,Sep Sun');
expect(callback).toBeInstanceOf(Function);
expect(options?.name).toBe('my-cron-job');
return callback();
},
};

const cronWithCheckIn = instrumentNodeCron(nodeCron);

cronWithCheckIn.schedule(
'* * * Jan,Sep Sun',
() => {
expect(withMonitorSpy).toHaveBeenCalledTimes(1);
expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), {
schedule: { type: 'crontab', value: '* * * 1,9 0' },
});
done();
},
{ name: 'my-cron-job' },
);
});

test('throws without supplied name', () => {
const nodeCron: NodeCron = {
schedule: (): unknown => {
return undefined;
},
};

const cronWithCheckIn = instrumentNodeCron(nodeCron);

expect(() => {
cronWithCheckIn.schedule('* * * * *', () => {
//
});
}).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.');
});
});
});