From 55d41f7c20b8cfcafda9bbeef75d551c1e7e22d7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 7 May 2024 16:01:12 +0100 Subject: [PATCH] fix(node): Fix cron instrumentation and add tests (#11811) Closes #11766 These tests are also in TypeScript so they check the types too. I found that two out of three cron libraries were actually swallowing exceptions so that they were not captured by Sentry. I added calls to `captureException` to rectify that. --- .../node-integration-tests/package.json | 4 + .../node-integration-tests/src/index.ts | 2 +- .../suites/cron/cron/scenario.ts | 31 ++++++++ .../suites/cron/cron/test.ts | 75 +++++++++++++++++++ .../suites/cron/node-cron/scenario.ts | 38 ++++++++-- .../suites/cron/node-cron/test.ts | 70 ++++++++++++++++- .../suites/cron/node-schedule/scenario.ts | 29 +++++++ .../suites/cron/node-schedule/test.ts | 75 +++++++++++++++++++ .../node-integration-tests/utils/runner.ts | 22 ++++++ packages/node/src/cron/cron.ts | 24 ++++-- packages/node/src/cron/node-cron.ts | 37 +++++---- packages/node/src/cron/node-schedule.ts | 30 +++++--- yarn.lock | 47 +++++++++++- 13 files changed, 444 insertions(+), 40 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/cron/cron/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/cron/cron/test.ts create mode 100644 dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/cron/node-schedule/test.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 930833be0ff5..c8f358c9f0b6 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -50,6 +50,8 @@ "mongoose": "^5.13.22", "mysql": "^2.18.1", "mysql2": "^3.7.1", + "node-cron": "^3.0.3", + "node-schedule": "^2.1.1", "nock": "^13.1.0", "pg": "^8.7.3", "proxy": "^2.1.1", @@ -58,6 +60,8 @@ "yargs": "^16.2.0" }, "devDependencies": { + "@types/node-cron": "^3.0.11", + "@types/node-schedule": "^2.1.7", "globby": "11" }, "config": { diff --git a/dev-packages/node-integration-tests/src/index.ts b/dev-packages/node-integration-tests/src/index.ts index 46c6e98401ae..08afc11fe7ea 100644 --- a/dev-packages/node-integration-tests/src/index.ts +++ b/dev-packages/node-integration-tests/src/index.ts @@ -13,7 +13,7 @@ export function loggingTransport(_options: BaseTransportOptions): Transport { return Promise.resolve({ statusCode: 200 }); }, flush(): PromiseLike { - return Promise.resolve(true); + return new Promise(resolve => setTimeout(() => resolve(true), 1000)); }, }; } diff --git a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts new file mode 100644 index 000000000000..12416fd056ca --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts @@ -0,0 +1,31 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; +import { CronJob } from 'cron'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + transport: loggingTransport, +}); + +const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job'); + +let closeNext = false; + +const cron = new CronJobWithCheckIn('* * * * * *', () => { + if (closeNext) { + cron.stop(); + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext = true; +}); + +cron.start(); + +setTimeout(() => { + process.exit(); +}, 5000); diff --git a/dev-packages/node-integration-tests/suites/cron/cron/test.ts b/dev-packages/node-integration-tests/suites/cron/cron/test.ts new file mode 100644 index 000000000000..ee83f4dc1226 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron/test.ts @@ -0,0 +1,75 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('cron instrumentation', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'error', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in cron job' }] }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts index 71b005d4dfd6..8fe4f1bd34c5 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/scenario.ts @@ -1,9 +1,37 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; -import { CronJob } from 'cron'; +import * as cron from 'node-cron'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job'); +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + transport: loggingTransport, +}); + +const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron); + +let closeNext = false; + +const task = cronWithCheckIn.schedule( + '* * * * * *', + () => { + if (closeNext) { + // https://github.com/node-cron/node-cron/issues/317 + setImmediate(() => { + task.stop(); + }); + + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext = true; + }, + { name: 'my-cron-job' }, +); setTimeout(() => { - process.exit(0); -}, 1_000); + process.exit(); +}, 5000); diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts index b768599cd215..2c3be50907a3 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/test.ts @@ -4,6 +4,72 @@ afterAll(() => { cleanupChildProcesses(); }); -test('node-cron types should match', done => { - createRunner(__dirname, 'scenario.ts').ensureNoErrorOutput().start(done); +test('node-cron instrumentation', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'error', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in cron job' }] }, + }, + }) + .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts new file mode 100644 index 000000000000..badcc87fbbce --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts @@ -0,0 +1,29 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; +import * as schedule from 'node-schedule'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + transport: loggingTransport, +}); + +const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + +let closeNext = false; + +const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * * *', () => { + if (closeNext) { + job.cancel(); + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext = true; +}); + +setTimeout(() => { + process.exit(); +}, 5000); diff --git a/dev-packages/node-integration-tests/suites/cron/node-schedule/test.ts b/dev-packages/node-integration-tests/suites/cron/node-schedule/test.ts new file mode 100644 index 000000000000..84d94c54ad8d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/node-schedule/test.ts @@ -0,0 +1,75 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('node-schedule instrumentation', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'error', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.any(String), + span_id: expect.any(String), + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in cron job' }] }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index bcbbf90417d1..9d0eb8a64b25 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -7,6 +7,7 @@ import type { EnvelopeItemType, Event, EventEnvelope, + SerializedCheckIn, SerializedSession, SessionAggregates, } from '@sentry/types'; @@ -38,6 +39,13 @@ export function assertSentryTransaction(actual: Event, expected: Partial) }); } +export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void { + expect(actual).toMatchObject({ + check_in_id: expect.any(String), + ...expected, + }); +} + export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { expect(actual).toEqual({ event_id: expect.any(String), @@ -137,6 +145,9 @@ type Expected = } | { sessions: Partial | ((event: SessionAggregates) => void); + } + | { + check_in: Partial | ((event: SerializedCheckIn) => void); }; type ExpectedEnvelopeHeader = @@ -300,6 +311,17 @@ export function createRunner(...paths: string[]) { expectCallbackCalled(); } + + if ('check_in' in expected) { + const checkIn = item[1] as SerializedCheckIn; + if (typeof expected.check_in === 'function') { + expected.check_in(checkIn); + } else { + assertSentryCheckIn(checkIn, expected.check_in); + } + + expectCallbackCalled(); + } } catch (e) { complete(e as Error); } diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts index 8b6fc324a7a6..ce6225ced2fa 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -1,4 +1,4 @@ -import { withMonitor } from '@sentry/core'; +import { captureException, withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export type CronJobParams = { @@ -92,11 +92,16 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri const cronString = replaceCronNames(cronTime); - function monitoredTick(context: unknown, onComplete?: unknown): void | Promise { + async function monitoredTick(context: unknown, onComplete?: unknown): Promise { return withMonitor( monitorSlug, - () => { - return onTick(context, onComplete); + async () => { + try { + await onTick(context, onComplete); + } catch (e) { + captureException(e); + throw e; + } }, { schedule: { type: 'crontab', value: cronString }, @@ -124,11 +129,16 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri const cronString = replaceCronNames(cronTime); - param.onTick = (context: unknown, onComplete?: unknown) => { + param.onTick = async (context: unknown, onComplete?: unknown) => { return withMonitor( monitorSlug, - () => { - return onTick(context, onComplete); + async () => { + try { + await onTick(context, onComplete); + } catch (e) { + captureException(e); + throw e; + } }, { schedule: { type: 'crontab', value: cronString }, diff --git a/packages/node/src/cron/node-cron.ts b/packages/node/src/cron/node-cron.ts index 4495a0b54909..efa7cde2b698 100644 --- a/packages/node/src/cron/node-cron.ts +++ b/packages/node/src/cron/node-cron.ts @@ -1,4 +1,4 @@ -import { withMonitor } from '@sentry/core'; +import { captureException, withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export interface NodeCronOptions { @@ -15,7 +15,7 @@ export interface NodeCron { * * ```ts * import * as Sentry from "@sentry/node"; - * import cron from "node-cron"; + * import * as cron from "node-cron"; * * const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron); * @@ -35,22 +35,33 @@ export function instrumentNodeCron(lib: Partial & T): T { // When 'get' is called for schedule, return a proxied version of the schedule function return new Proxy(target.schedule, { apply(target, thisArg, argArray: Parameters) { - const [expression, , options] = argArray; + const [expression, callback, 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: replaceCronNames(expression) }, - timezone: options?.timezone, - }, - ); + async function monitoredCallback(): Promise { + return withMonitor( + options.name, + async () => { + // We have to manually catch here and capture the exception because node-cron swallows errors + // https://github.com/node-cron/node-cron/issues/399 + try { + return await callback(); + } catch (e) { + captureException(e); + throw e; + } + }, + { + schedule: { type: 'crontab', value: replaceCronNames(expression) }, + timezone: options?.timezone, + }, + ); + } + + return target.apply(thisArg, [expression, monitoredCallback, options]); }, }); } else { diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node/src/cron/node-schedule.ts index 79ae44a06e52..35db51618b9a 100644 --- a/packages/node/src/cron/node-schedule.ts +++ b/packages/node/src/cron/node-schedule.ts @@ -30,9 +30,13 @@ export function instrumentNodeSchedule(lib: T & NodeSchedule): T { // eslint-disable-next-line @typescript-eslint/unbound-method return new Proxy(target.scheduleJob, { apply(target, thisArg, argArray: Parameters) { - const [nameOrExpression, expressionOrCallback] = argArray; + const [nameOrExpression, expressionOrCallback, callback] = argArray; - if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') { + if ( + typeof nameOrExpression !== 'string' || + typeof expressionOrCallback !== 'string' || + typeof callback !== 'function' + ) { throw new Error( "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", ); @@ -41,15 +45,19 @@ export function instrumentNodeSchedule(lib: T & NodeSchedule): T { const monitorSlug = nameOrExpression; const expression = expressionOrCallback; - return withMonitor( - monitorSlug, - () => { - return target.apply(thisArg, argArray); - }, - { - schedule: { type: 'crontab', value: replaceCronNames(expression) }, - }, - ); + async function monitoredCallback(): Promise { + return withMonitor( + monitorSlug, + async () => { + await callback?.(); + }, + { + schedule: { type: 'crontab', value: replaceCronNames(expression) }, + }, + ); + } + + return target.apply(thisArg, [monitorSlug, expression, monitoredCallback]); }, }); } diff --git a/yarn.lock b/yarn.lock index 17831308adfb..61e4411b1bcd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8539,6 +8539,11 @@ resolved "https://registry.yarnpkg.com/@types/node-abi/-/node-abi-3.0.3.tgz#a8334d75fe45ccd4cdb2a6c1ae82540a7a76828c" integrity sha512-5oos6sivyXcDEuVC5oX3+wLwfgrGZu4NIOn826PGAjPCHsqp2zSPTGU7H1Tv+GZBOiDUY3nBXY1MdaofSEt4fw== +"@types/node-cron@^3.0.11": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344" + integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg== + "@types/node-fetch@^2.6.0": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" @@ -8554,6 +8559,13 @@ dependencies: "@types/node" "*" +"@types/node-schedule@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-2.1.7.tgz#79a1e61adc7bbf8d8eaabcef307e07d76cb40d82" + integrity sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "17.0.38" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" @@ -13308,6 +13320,13 @@ critters@0.0.16: postcss "^8.3.7" pretty-bytes "^5.3.0" +cron-parser@^4.2.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" + integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== + dependencies: + luxon "^3.2.1" + cron@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.6.tgz#e7e1798a468e017c8d31459ecd7c2d088f97346c" @@ -21029,6 +21048,11 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w== + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -21131,7 +21155,7 @@ lunr@^2.3.8: resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== -luxon@~3.4.0: +luxon@^3.2.1, luxon@~3.4.0: version "3.4.4" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== @@ -22864,6 +22888,13 @@ node-addon-api@^6.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== +node-cron@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.3.tgz#c4bc7173dd96d96c50bdb51122c64415458caff2" + integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A== + dependencies: + uuid "8.3.2" + node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -22998,6 +23029,15 @@ node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== +node-schedule@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-2.1.1.tgz#6958b2c5af8834954f69bb0a7a97c62b97185de3" + integrity sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ== + dependencies: + cron-parser "^4.2.0" + long-timeout "0.1.1" + sorted-array-functions "^1.3.0" + node-source-walk@^4.0.0, node-source-walk@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-4.2.0.tgz#c2efe731ea8ba9c03c562aa0a9d984e54f27bc2c" @@ -27510,6 +27550,11 @@ sort-package-json@^1.57.0: is-plain-obj "2.1.0" sort-object-keys "^1.1.3" +sorted-array-functions@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5" + integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"