From cf8b5be3d56d3877186f79b841995fbc373c2ec1 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 16 Oct 2026 17:54:55 -0700 Subject: [PATCH 1/4] feat(node): Add cronJobIntegration to monitor cron jobs Sends check-ins for every named CronJob of the cron package (4.3+) through the orchestrion channel injection, with the cron expression and time zone as monitor config when Sentry reads it the same way. Bumps the cron dev dependency of the node integration tests to 4.x. Co-Authored-By: Claude --- .../node-integration-tests/package.json | 2 +- .../cron/cron-integration/instrument.mjs | 9 +++ .../suites/cron/cron-integration/scenario.mjs | 29 ++++++++ .../suites/cron/cron-integration/test.ts | 50 +++++++++++++ .../suites/cron/cron/scenario.ts | 4 +- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/elysia/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/cron/cron.ts | 7 ++ packages/node/src/index.ts | 2 + packages/remix/src/server/index.ts | 1 + packages/server-utils/src/index.ts | 2 + .../server-utils/src/integrations/cron.ts | 73 +++++++++++++++++++ .../server-utils/src/orchestrion/channels.ts | 2 + .../src/orchestrion/config/cron.ts | 17 +++++ .../src/orchestrion/config/index.ts | 2 + .../server-utils/src/utils/cronMonitoring.ts | 3 + .../test/utils/cronMonitoring.test.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 22 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/cron/cron-integration/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/cron/cron-integration/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/cron/cron-integration/test.ts create mode 100644 packages/server-utils/src/integrations/cron.ts create mode 100644 packages/server-utils/src/orchestrion/config/cron.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index fdef92a3ba24..e1b6ef2606af 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -65,7 +65,7 @@ "body-parser": "^2.3.0", "consola": "^3.2.3", "cors": "^2.8.5", - "cron": "^3.1.6", + "cron": "^4.4.0", "dataloader": "2.2.2", "express": "^4.21.2", "fastify": "^5.12.5", diff --git a/dev-packages/node-integration-tests/suites/cron/cron-integration/instrument.mjs b/dev-packages/node-integration-tests/suites/cron/cron-integration/instrument.mjs new file mode 100644 index 000000000000..3ecfc0097afc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron-integration/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + integrations: [Sentry.cronJobIntegration({ excludeJobs: ['excluded-job'] })], +}); diff --git a/dev-packages/node-integration-tests/suites/cron/cron-integration/scenario.mjs b/dev-packages/node-integration-tests/suites/cron/cron-integration/scenario.mjs new file mode 100644 index 000000000000..a608fd44d135 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron-integration/scenario.mjs @@ -0,0 +1,29 @@ +import { CronJob } from 'cron'; + +const excluded = CronJob.from({ cronTime: '0 0 * * *', onTick: () => {}, name: 'excluded-job' }); +const unnamed = new CronJob('0 0 * * *', () => {}); + +let runs = 0; +const job = CronJob.from({ + cronTime: '0 30 9 * * 1-5', + timeZone: 'Europe/Vienna', + name: 'my-cron-job', + waitForCompletion: true, + errorHandler: () => {}, + onTick: async () => { + runs++; + if (runs > 1) { + throw new Error('Error in cron job'); + } + }, +}); + +// Run the jobs without waiting for their schedule. +(async () => { + await excluded.fireOnTick(); + await unnamed.fireOnTick(); + await job.fireOnTick(); + await job.fireOnTick(); +})(); + +setTimeout(() => process.exit(), 10_000); diff --git a/dev-packages/node-integration-tests/suites/cron/cron-integration/test.ts b/dev-packages/node-integration-tests/suites/cron/cron-integration/test.ts new file mode 100644 index 000000000000..7c658b22408d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron-integration/test.ts @@ -0,0 +1,50 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +const MONITOR_CONFIG = { + schedule: { type: 'crontab', value: '30 9 * * 1-5' }, + timezone: 'Europe/Vienna', +}; + +describe('cronJobIntegration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('sends check-ins for named jobs', async () => { + await createRunner() + .ignore('event') + .expect({ + check_in: { monitor_slug: 'my-cron-job', status: 'in_progress', monitor_config: MONITOR_CONFIG }, + }) + .expect({ check_in: { monitor_slug: 'my-cron-job', status: 'ok', duration: expect.any(Number) } }) + .expect({ + check_in: { monitor_slug: 'my-cron-job', status: 'in_progress', monitor_config: MONITOR_CONFIG }, + }) + .expect({ check_in: { monitor_slug: 'my-cron-job', status: 'error', duration: expect.any(Number) } }) + .start() + .completed(); + }); + + test('captures job errors', async () => { + await createRunner() + .unordered() + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'Error in cron job', + mechanism: { type: 'auto.function.cron', handled: false }, + }, + ], + }, + }, + }) + .start() + .completed(); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts index b3966b050d89..68cf28bcde94 100644 --- a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts @@ -28,11 +28,11 @@ cron.start(); // is enough to hit it, and the job is left scheduled for nothing: no ticks, no check-ins, and the // scenario exits on the timeout below having sent nothing at all. Starting again re-reads the clock // away from the boundary. -for (let attempt = 0; attempt < 5 && !cron.running; attempt++) { +for (let attempt = 0; attempt < 5 && !cron.isActive; attempt++) { cron.start(); } -if (!cron.running) { +if (!cron.isActive) { throw new Error('`cron` refused to schedule the job'); } diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 69d9dee08287..e7675e02ab85 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -47,6 +47,7 @@ export { fsIntegration, functionToStringIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, getActiveSpan, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 8c45d2f267d9..3ba287a78bcf 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -116,6 +116,7 @@ export { firebaseIntegration, fsIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, graphqlIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 4c4e7c48b010..3122d4857bbc 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -138,6 +138,7 @@ export { // oxlint-disable-next-line typescript/no-deprecated setupKoaErrorHandler, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, graphqlIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index a796fc84ecc7..60863f006a52 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -117,6 +117,7 @@ export { // oxlint-disable-next-line typescript/no-deprecated setupKoaErrorHandler, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, graphqlIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 317424507421..55aa4f7692dd 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -116,6 +116,7 @@ export { fastifyIntegration, firebaseIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, graphqlIntegration, diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts index 2c764c877c46..e92625ebee18 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -51,6 +51,8 @@ export type CronJobConstructor = { ): CronJob; }; +const MONITORED = Symbol.for('sentry.cron.monitored'); + const ERROR_TEXT = 'Automatic instrumentation of CronJob only supports crontab string'; /** @@ -115,6 +117,9 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri ); } + // Tells `cronJobIntegration` that this job is already monitored. + (monitoredTick as unknown as Record)[MONITORED] = true; + return new target(cronTime, monitoredTick, onComplete, start, timeZone, ...rest); }, get(target, prop: keyof CronJobConstructor) { @@ -157,6 +162,8 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri ); }; + (param.onTick as unknown as Record)[MONITORED] = true; + return target.from(param); }; } else { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 5e4c60e9706e..3afae9e27db9 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -11,6 +11,8 @@ export { type ExpressIntegrationOptions, firebaseIntegration, genericPoolIntegration, + cronJobIntegration, + type CronJobIntegrationOptions, nodeCronIntegration, type NodeCronIntegrationOptions, nodeScheduleIntegration, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 88ef6a841518..b842efd5f534 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -37,6 +37,7 @@ export { flush, functionToStringIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, getActiveSpan, diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 005e8a4a8b79..2550a7fd6884 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -38,6 +38,8 @@ export { anthropicAIIntegration } from './integrations/anthropic'; export { awsIntegration } from './integrations/aws-sdk'; export { dataloaderIntegration } from './integrations/dataloader'; export { genericPoolIntegration } from './integrations/generic-pool'; +export { cronJobIntegration } from './integrations/cron'; +export type { CronJobIntegrationOptions } from './integrations/cron'; export { nodeCronIntegration } from './integrations/node-cron'; export type { NodeCronIntegrationOptions } from './integrations/node-cron'; export { nodeScheduleIntegration } from './integrations/node-schedule'; diff --git a/packages/server-utils/src/integrations/cron.ts b/packages/server-utils/src/integrations/cron.ts new file mode 100644 index 000000000000..c15968bc17c7 --- /dev/null +++ b/packages/server-utils/src/integrations/cron.ts @@ -0,0 +1,73 @@ +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; +import type { IntegrationFn } from '@sentry/core'; +import { debug, defineIntegration, stringMatchesSomePattern } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { CHANNELS } from '../orchestrion/channels'; +import { cronModuleNames } from '../orchestrion/config/cron'; +import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; +import type { AnyFn } from '../utils/cronMonitoring'; +import { cronExpressionToMonitorConfig, wrapScheduledJob } from '../utils/cronMonitoring'; + +const INTEGRATION_NAME = 'CronJob' as const; + +// Set by `Sentry.cron.instrumentCron`, which already sends check-ins. +const MONITORED = Symbol.for('sentry.cron.monitored'); + +const NOOP = (): void => {}; + +export interface CronJobIntegrationOptions { + /** Job names not to monitor. Strings must match the name exactly. */ + excludeJobs?: Array; +} + +let options: CronJobIntegrationOptions = {}; + +const _cronJobIntegration = ((integrationOptions: CronJobIntegrationOptions = {}) => { + return { + name: INTEGRATION_NAME, + setup(client) { + options = integrationOptions; + invokeOrchestrionInstrumentation(client, cronModuleNames, subscribe, [], { + requiresTracingChannelBinding: false, + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Sends Crons check-ins for every `CronJob` of the `cron` package (4.3+) that has a `name`. + * The monitor slug is the job name; jobs without a name, and one-off jobs, are not monitored. + */ +export const cronJobIntegration = defineIntegration(_cronJobIntegration); + +function subscribe(): void { + diagnosticsChannel.tracingChannel<{ arguments: unknown[] }>(CHANNELS.CRON_JOB_CONSTRUCTOR).subscribe({ + start(data) { + try { + // `new CronJob(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, ..., name)` + const args = data.arguments; + const [cronTime, onTick, , , timeZone] = args; + const utcOffset = args[7]; + const name = args[11]; + if ( + typeof name !== 'string' || + !name || + typeof onTick !== 'function' || + (onTick as { [MONITORED]?: boolean })[MONITORED] || + typeof cronTime !== 'string' || + stringMatchesSomePattern(name, options.excludeJobs, true) + ) { + return; + } + const monitorConfig = utcOffset == null ? cronExpressionToMonitorConfig(cronTime, timeZone, 'or') : undefined; + args[1] = wrapScheduledJob(onTick as AnyFn, name, monitorConfig, 'auto.function.cron'); + } catch (e) { + DEBUG_BUILD && debug.error('[CronJob] Failed to set up the cron monitor:', e); + } + }, + end: NOOP, + asyncStart: NOOP, + asyncEnd: NOOP, + error: NOOP, + }); +} diff --git a/packages/server-utils/src/orchestrion/channels.ts b/packages/server-utils/src/orchestrion/channels.ts index 67a88bbb3825..f593fefe0ebd 100644 --- a/packages/server-utils/src/orchestrion/channels.ts +++ b/packages/server-utils/src/orchestrion/channels.ts @@ -1,6 +1,7 @@ import { amqplibChannels } from './config/amqplib'; import { anthropicAiChannels } from './config/anthropic-ai'; import { awsSdkChannels } from './config/aws-sdk'; +import { cronChannels } from './config/cron'; import { dataloaderChannels } from './config/dataloader'; import { expressChannels } from './config/express'; import { firebaseChannels } from './config/firebase'; @@ -60,6 +61,7 @@ export const CHANNELS = { ...amqplibChannels, ...anthropicAiChannels, ...awsSdkChannels, + ...cronChannels, ...dataloaderChannels, ...expressChannels, ...firebaseChannels, diff --git a/packages/server-utils/src/orchestrion/config/cron.ts b/packages/server-utils/src/orchestrion/config/cron.ts new file mode 100644 index 000000000000..d15c1d90821a --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/cron.ts @@ -0,0 +1,17 @@ +import type { InstrumentationConfig } from '../apmTypes'; +import { getModuleNames } from './module-names'; + +// `CronJob.from()` calls the constructor. Job names were added in 4.3.0. +export const cronConfig = [ + { + channelName: 'cronJobConstructor', + module: { name: 'cron', versionRange: '>=4.3.0 <5', filePath: 'dist/job.js' }, + functionQuery: { className: 'CronJob' }, + }, +] satisfies InstrumentationConfig[]; + +export const cronModuleNames = getModuleNames(cronConfig); + +export const cronChannels = { + CRON_JOB_CONSTRUCTOR: 'orchestrion:cron:cronJobConstructor', +} as const; diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index bf2c4b50b178..7636fbef80fc 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -5,6 +5,7 @@ import { uniq } from '@sentry/core'; import { awsSdkConfig } from './aws-sdk'; import { amqplibConfig } from './amqplib'; import { anthropicAiConfig } from './anthropic-ai'; +import { cronConfig } from './cron'; import { dataloaderConfig } from './dataloader'; import { expressConfig } from './express'; import { firebaseConfig } from './firebase'; @@ -66,6 +67,7 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ...amqplibConfig, ...anthropicAiConfig, ...awsSdkConfig, + ...cronConfig, ...dataloaderConfig, ...expressConfig, ...firebaseConfig, diff --git a/packages/server-utils/src/utils/cronMonitoring.ts b/packages/server-utils/src/utils/cronMonitoring.ts index 56d7fe97e321..82c0e8dc3814 100644 --- a/packages/server-utils/src/utils/cronMonitoring.ts +++ b/packages/server-utils/src/utils/cronMonitoring.ts @@ -11,6 +11,9 @@ const NICKNAMES: Record = { '@daily': '0 0 * * *', '@midnight': '0 0 * * *', '@hourly': '0 * * * *', + '@minutely': '* * * * *', + '@weekdays': '0 0 * * 1-5', + '@weekends': '0 0 * * 0,6', }; const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; diff --git a/packages/server-utils/test/utils/cronMonitoring.test.ts b/packages/server-utils/test/utils/cronMonitoring.test.ts index 63495f8512f0..084641817259 100644 --- a/packages/server-utils/test/utils/cronMonitoring.test.ts +++ b/packages/server-utils/test/utils/cronMonitoring.test.ts @@ -13,6 +13,7 @@ describe('cronExpressionToMonitorConfig with node-cron day matching', () => { ['0 9 */2 * 1', '0 9 */2 * 1'], ['@daily', '0 0 * * *'], ['@Weekly', '0 0 * * 0'], + ['@weekdays', '0 0 * * 1-5'], ['*/15 * * * *', '*/15 * * * *'], ])('sends %s as %s', (expression, crontab) => { expect(toMonitorConfig(expression, 'Europe/Vienna')).toEqual({ diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 2237235f5c1b..e0339440f4c9 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -40,6 +40,7 @@ export { flush, functionToStringIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, getActiveSpan, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index e5b73bcd2acc..c6fed60ea263 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -38,6 +38,7 @@ export { flush, functionToStringIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, getActiveSpan, From 1908bf3f7505d75c363abeaf87b2c7fbbef5f289 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 16 Oct 2026 18:11:31 -0700 Subject: [PATCH 2/4] test(node): Skip cron integration suite on bun run Co-Authored-By: Claude --- dev-packages/bun-integration-tests/node-suites/excludes.ts | 1 + .../test/orchestrion/moduleInjectedTransform.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index e38eeeb56384..039e0b07f684 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -62,6 +62,7 @@ const NO_FETCH_INSTRUMENTATION = [ // instrumentation creates no spans. Apps must be built with `@sentry/bun/plugin`. // See https://github.com/getsentry/sentry-javascript/issues/23882 export const NO_AUTO_INSTRUMENTATION = [ + 'suites/cron/cron-integration/test.ts', 'suites/cron/node-cron/integration/test.ts', 'suites/cron/node-schedule-integration/test.ts', 'suites/express/**', diff --git a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts index ed55b1bec34e..5780411ff4c3 100644 --- a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts +++ b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts @@ -44,7 +44,7 @@ describe('channel integration definitions', () => { // `@nestjs/*` and `@remix-run/*` are covered by listeners in their own SDK packages. // Crons integrations are opt-in, so bundler-only SDKs must not install them for every app. - const optIn = new Set(['node-cron', 'node-schedule']); + const optIn = new Set(['cron', 'node-cron', 'node-schedule']); const uncovered = [...configured].filter( name => !defined.has(name) && !optIn.has(name) && !name.startsWith('@nestjs/') && !name.startsWith('@remix-run/'), ); From 487dc0c86292ff95bdfcdbfc7ee5d143230c8ad6 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Mon, 19 Oct 2026 17:49:42 -0700 Subject: [PATCH 3/4] chore(deps): Update pnpm-lock.yaml for cron 4 and dedupe luxon Co-Authored-By: Claude --- pnpm-lock.yaml | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfdd2832b6d3..34148cbb88e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -831,8 +831,8 @@ importers: specifier: ^2.8.5 version: 2.8.6 cron: - specifier: ^3.1.6 - version: 3.1.6 + specifier: ^4.4.0 + version: 4.4.0 dataloader: specifier: 2.2.2 version: 2.2.2 @@ -8702,8 +8702,8 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - '@types/luxon@3.3.8': - resolution: {integrity: sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==} + '@types/luxon@3.7.6': + resolution: {integrity: sha512-6KSjliQAXK8ZLgFQO4B7iE4Rpf/B3rItzCFuXezBY/XIAGxOdLd7vRNK9/StRPwiS/yJsVbB7HKpW46B8tcEuQ==} '@types/mdast@4.0.1': resolution: {integrity: sha512-IlKct1rUTJ1T81d8OHzyop15kGv9A/ff7Gz7IJgrk6jDb4Udw77pCJ+vq8oxZf4Ghpm+616+i1s/LNg/Vh7d+g==} @@ -10753,8 +10753,9 @@ packages: engines: {node: '>=12.0.0'} deprecated: v4 is no longer maintained, upgrade to v5 - cron@3.1.6: - resolution: {integrity: sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==} + cron@4.4.0: + resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} + engines: {node: '>=18.x'} croner@10.0.1: resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} @@ -14009,8 +14010,8 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - luxon@3.4.4: - resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} lz-string@1.5.0: @@ -27027,7 +27028,7 @@ snapshots: '@types/long@4.0.2': {} - '@types/luxon@3.3.8': {} + '@types/luxon@3.7.6': {} '@types/mdast@4.0.1': dependencies: @@ -29587,12 +29588,12 @@ snapshots: cron-parser@4.9.0: dependencies: - luxon: 3.4.4 + luxon: 3.7.2 - cron@3.1.6: + cron@4.4.0: dependencies: - '@types/luxon': 3.3.8 - luxon: 3.4.4 + '@types/luxon': 3.7.6 + luxon: 3.7.2 croner@10.0.1: {} @@ -33527,7 +33528,7 @@ snapshots: lru.min@1.1.4: {} - luxon@3.4.4: {} + luxon@3.7.2: {} lz-string@1.5.0: {} From c11e2fdf45140977bb8a328510c024ece75e4948 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Tue, 20 Oct 2026 10:48:11 -0700 Subject: [PATCH 4/4] ref(node): Use the shared job-creation helpers in cronJobIntegration Co-Authored-By: Claude --- .../server-utils/src/integrations/cron.ts | 58 +++++++------------ 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/packages/server-utils/src/integrations/cron.ts b/packages/server-utils/src/integrations/cron.ts index c15968bc17c7..d815ef084cc6 100644 --- a/packages/server-utils/src/integrations/cron.ts +++ b/packages/server-utils/src/integrations/cron.ts @@ -1,20 +1,18 @@ -import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration, stringMatchesSomePattern } from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; +import { defineIntegration } from '@sentry/core'; import { CHANNELS } from '../orchestrion/channels'; import { cronModuleNames } from '../orchestrion/config/cron'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import type { AnyFn } from '../utils/cronMonitoring'; -import { cronExpressionToMonitorConfig, wrapScheduledJob } from '../utils/cronMonitoring'; +import { + cronExpressionToMonitorConfig, + shouldMonitorJob, + subscribeToJobCreation, + wrapScheduledJob, +} from '../utils/cronMonitoring'; const INTEGRATION_NAME = 'CronJob' as const; -// Set by `Sentry.cron.instrumentCron`, which already sends check-ins. -const MONITORED = Symbol.for('sentry.cron.monitored'); - -const NOOP = (): void => {}; - export interface CronJobIntegrationOptions { /** Job names not to monitor. Strings must match the name exactly. */ excludeJobs?: Array; @@ -41,33 +39,17 @@ const _cronJobIntegration = ((integrationOptions: CronJobIntegrationOptions = {} export const cronJobIntegration = defineIntegration(_cronJobIntegration); function subscribe(): void { - diagnosticsChannel.tracingChannel<{ arguments: unknown[] }>(CHANNELS.CRON_JOB_CONSTRUCTOR).subscribe({ - start(data) { - try { - // `new CronJob(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, ..., name)` - const args = data.arguments; - const [cronTime, onTick, , , timeZone] = args; - const utcOffset = args[7]; - const name = args[11]; - if ( - typeof name !== 'string' || - !name || - typeof onTick !== 'function' || - (onTick as { [MONITORED]?: boolean })[MONITORED] || - typeof cronTime !== 'string' || - stringMatchesSomePattern(name, options.excludeJobs, true) - ) { - return; - } - const monitorConfig = utcOffset == null ? cronExpressionToMonitorConfig(cronTime, timeZone, 'or') : undefined; - args[1] = wrapScheduledJob(onTick as AnyFn, name, monitorConfig, 'auto.function.cron'); - } catch (e) { - DEBUG_BUILD && debug.error('[CronJob] Failed to set up the cron monitor:', e); - } - }, - end: NOOP, - asyncStart: NOOP, - asyncEnd: NOOP, - error: NOOP, - }); + subscribeToJobCreation(CHANNELS.CRON_JOB_CONSTRUCTOR, INTEGRATION_NAME, wrapJob); +} + +// `new CronJob(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, ..., name)` +function wrapJob(args: unknown[]): void { + const [cronTime, onTick, , , timeZone] = args; + const utcOffset = args[7]; + const name = args[11]; + if (typeof cronTime !== 'string' || !shouldMonitorJob(name, onTick, options.excludeJobs)) { + return; + } + const monitorConfig = utcOffset == null ? cronExpressionToMonitorConfig(cronTime, timeZone, 'or') : undefined; + args[1] = wrapScheduledJob(onTick as AnyFn, name, monitorConfig, 'auto.function.cron'); }