diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 7ac46f1c088a..c7b4b8747def 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -64,6 +64,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/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 2ea842433e12..1024e56fd6ef 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -68,7 +68,7 @@ "bullmq": "^5.79.1", "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.7", 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..0a665ee9727c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron-integration/scenario.mjs @@ -0,0 +1,37 @@ +import { CronJob, CronTime } from 'cron'; + +const excluded = CronJob.from({ cronTime: '0 0 * * *', onTick: () => {}, name: 'excluded-job' }); +const unnamed = new CronJob('0 0 * * *', () => {}); +const oneOff = CronJob.from({ cronTime: new Date(Date.now() + 86_400_000), onTick: () => {}, name: 'one-off-job' }); +const offset = CronJob.from({ cronTime: '0 0 * * *', utcOffset: 60, onTick: () => {}, name: 'offset-job' }); +const subMinute = CronJob.from({ cronTime: '*/10 * * * * *', onTick: () => {}, name: 'sub-minute-job' }); + +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 === 2) { + throw new Error('Error in cron job'); + } + }, +}); + +// Run the jobs without waiting for their schedule. +(async () => { + await excluded.fireOnTick(); + await unnamed.fireOnTick(); + await oneOff.fireOnTick(); + await offset.fireOnTick(); + await subMinute.fireOnTick(); + await job.fireOnTick(); + await job.fireOnTick(); + job.setTime(new CronTime('0 8 * * *', 'UTC')); + 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..87a0286c5309 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/cron-integration/test.ts @@ -0,0 +1,58 @@ +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) } }) + .expect({ + check_in: { + monitor_slug: 'my-cron-job', + status: 'in_progress', + monitor_config: { schedule: { type: 'crontab', value: '0 8 * * *' }, timezone: 'UTC' }, + }, + }) + .expect({ check_in: { monitor_slug: 'my-cron-job', status: 'ok', 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 20d085cb5069..45a4bfe0257f 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -49,6 +49,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 c13ded347d83..84aa97a88381 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -118,6 +118,7 @@ export { firebaseIntegration, fsIntegration, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, graphqlIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index e33986f88bba..a8e107ffdfe8 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -143,6 +143,7 @@ export { // oxlint-disable-next-line typescript/no-deprecated setupKoaErrorHandler, genericPoolIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, graphqlIntegration, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 9f2aac8b30cb..4d5fb75365e7 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -195,6 +195,7 @@ export { postgresIntegration, postgresJsIntegration, neonIntegration, + cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, tediousIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 014bd9145c4a..107545f0cb43 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 31fa1d93337f..827fbca02148 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -118,6 +118,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..f95c1a1b6f9d 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -1,4 +1,4 @@ -import { captureException, withMonitor } from '@sentry/core'; +import { addNonEnumerableProperty, captureException, withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export type CronJobParams = { @@ -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. + addNonEnumerableProperty(monitoredTick, 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 ); }; + addNonEnumerableProperty(param.onTick, MONITORED, true); + return target.from(param); }; } else { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index a1686eb57ebc..72b8addf12c8 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -12,6 +12,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 10fc1a596daa..e034d9404d05 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..8a08c2fede4f --- /dev/null +++ b/packages/server-utils/src/integrations/cron.ts @@ -0,0 +1,71 @@ +import type { IntegrationFn, MonitorConfig } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; +import { CHANNELS } from '../orchestrion/channels'; +import { cronModuleNames } from '../orchestrion/config/cron'; +import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; +import type { AnyFn, ScheduleChange } from '../utils/cronMonitoring'; +import { + cronExpressionToMonitorConfig, + shouldMonitorJob, + subscribeToJobCreation, + subscribeToScheduleChange, + updateMonitorConfig, + wrapScheduledJob, +} from '../utils/cronMonitoring'; + +const INTEGRATION_NAME = 'CronJob' as const; + +export interface CronJobIntegrationOptions { + /** Job names not to monitor. Strings must match the name exactly. */ + excludeJobs?: Array; +} + +const _cronJobIntegration = ((options: CronJobIntegrationOptions = {}) => { + return { + name: INTEGRATION_NAME, + setup(client) { + invokeOrchestrionInstrumentation(client, cronModuleNames, subscribe, [options], { + 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, one-off jobs, and jobs with a schedule Crons can't express + * are not monitored. + */ +export const cronJobIntegration = defineIntegration(_cronJobIntegration); + +function subscribe(options: CronJobIntegrationOptions): void { + subscribeToJobCreation(CHANNELS.CRON_JOB_CONSTRUCTOR, INTEGRATION_NAME, args => wrapJob(args, options)); + subscribeToScheduleChange(CHANNELS.CRON_JOB_SET_TIME, INTEGRATION_NAME, onSetTime); +} + +/** Sets the new time's config after `job.setTime(cronTime)`. Without one, the job doesn't check in. */ +export function onSetTime({ arguments: [cronTime], self }: ScheduleChange): void { + const job = self as { cronTime?: unknown; _callbacks?: unknown[] } | undefined; + // `setTime` throws and keeps the old time when it isn't given a `CronTime`. + if (!job || job.cronTime !== cronTime) { + return; + } + const { source, timeZone, utcOffset } = cronTime as { source?: unknown; timeZone?: unknown; utcOffset?: unknown }; + const monitorConfig: MonitorConfig | undefined = + typeof source === 'string' && utcOffset == null ? cronExpressionToMonitorConfig(source, timeZone, 'or') : undefined; + for (const callback of job._callbacks || []) { + updateMonitorConfig(callback, monitorConfig); + } +} + +// `new CronJob(cronTime, onTick, onComplete, start, timeZone, context, runOnInit, utcOffset, ..., name)` +function wrapJob(args: unknown[], options: CronJobIntegrationOptions): 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'); +} 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..499d224aad9e --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/cron.ts @@ -0,0 +1,23 @@ +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' }, + }, + { + channelName: 'cronJobSetTime', + module: { name: 'cron', versionRange: '>=4.3.0 <5', filePath: 'dist/job.js' }, + functionQuery: { className: 'CronJob', methodName: 'setTime', kind: 'Sync' }, + }, +] satisfies InstrumentationConfig[]; + +export const cronModuleNames = getModuleNames(cronConfig); + +export const cronChannels = { + CRON_JOB_CONSTRUCTOR: 'orchestrion:cron:cronJobConstructor', + CRON_JOB_SET_TIME: 'orchestrion:cron:cronJobSetTime', +} 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 aa751eed9442..4caefd0ea3fa 100644 --- a/packages/server-utils/src/utils/cronMonitoring.ts +++ b/packages/server-utils/src/utils/cronMonitoring.ts @@ -13,7 +13,8 @@ import * as diagnosticsChannel from './diagnosticsChannel'; export type AnyFn = (this: unknown, ...args: unknown[]) => unknown; -// Set by `Sentry.cron.instrumentNodeCron`, `Sentry.cron.instrumentNodeSchedule` and `wrapScheduledJob`, which already send check-ins. +// Set by `Sentry.cron.instrumentCron`, `instrumentNodeCron`, `instrumentNodeSchedule` and `wrapScheduledJob`, which already +// send check-ins. const MONITORED = Symbol.for('sentry.cron.monitored'); /** @@ -65,7 +66,7 @@ export function subscribeToScheduleChange( channel.end.subscribe(handle(onChange)); } -/** Whether a job is named, not excluded, and not already monitored by `Sentry.cron.instrumentNode*` or `wrapScheduledJob`. */ +/** Whether a job is named, not excluded, and not already monitored by `Sentry.cron.instrument*` or `wrapScheduledJob`. */ export function shouldMonitorJob( name: unknown, job: unknown, @@ -88,6 +89,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/integrations/cron.test.ts b/packages/server-utils/test/integrations/cron.test.ts new file mode 100644 index 000000000000..ec83e6882d98 --- /dev/null +++ b/packages/server-utils/test/integrations/cron.test.ts @@ -0,0 +1,69 @@ +import type { MonitorConfig } from '@sentry/core'; +import { setCurrentClient } from '@sentry/core'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { onSetTime } from '../../src/integrations/cron'; +import { cronExpressionToMonitorConfig, wrapScheduledJob } from '../../src/utils/cronMonitoring'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; + +describe('onSetTime', () => { + let sentConfigs: Array; + + beforeEach(() => { + sentConfigs = []; + const client = new TestClient(getDefaultTestClientOptions()); + client.captureCheckIn = (_checkIn, monitorConfig) => { + sentConfigs.push(monitorConfig); + return 'check-in-id'; + }; + setCurrentClient(client); + }); + + const createJob = () => { + const onTick = wrapScheduledJob( + () => {}, + 'my-job', + cronExpressionToMonitorConfig('0 9 * * *', 'UTC', 'or'), + 'auto.function.cron', + ); + return { cronTime: {} as unknown, _callbacks: [onTick], onTick }; + }; + + // `setTime` assigns the new `CronTime` before the channel's `end` fires. + const setTime = (job: ReturnType, cronTime: unknown, applied = true): void => { + if (applied) { + job.cronTime = cronTime; + } + onSetTime({ arguments: [cronTime], self: job }); + }; + + it('sends the new schedule after setTime', () => { + const job = createJob(); + setTime(job, { source: '30 10 * * 1-5', timeZone: 'Europe/Vienna' }); + job.onTick(); + + expect(sentConfigs[0]).toEqual({ + schedule: { type: 'crontab', value: '30 10 * * 1-5' }, + timezone: 'Europe/Vienna', + }); + }); + + it.each([ + ['a date', { source: new Date(), timeZone: 'UTC' }], + ['a UTC offset', { source: '0 9 * * *', utcOffset: 60 }], + ['a sub-minute cron', { source: '*/10 * * * * *', timeZone: 'UTC' }], + ])('stops checking in after setTime with %s', (_, cronTime) => { + const job = createJob(); + setTime(job, cronTime); + job.onTick(); + + expect(sentConfigs).toEqual([]); + }); + + it('keeps the old schedule when setTime throws', () => { + const job = createJob(); + setTime(job, '30 10 * * *', false); + job.onTick(); + + expect(sentConfigs[0]?.schedule).toEqual({ type: 'crontab', value: '0 9 * * *' }); + }); +}); diff --git a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts index 0feac3d58b47..06fa8e34704d 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. // Each cron monitor is billed, so bundler-only SDKs must not install crons integrations 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/'), ); diff --git a/packages/server-utils/test/utils/cronMonitoring.test.ts b/packages/server-utils/test/utils/cronMonitoring.test.ts index 0950a592cc87..6b7e7a380515 100644 --- a/packages/server-utils/test/utils/cronMonitoring.test.ts +++ b/packages/server-utils/test/utils/cronMonitoring.test.ts @@ -15,6 +15,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 * * * *'], ['0 9 * * sun', '0 9 * * 0'], ['0 9 * * sat-sun', '0 9 * * 6-7'], diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 4ac8bff68e34..39c5b31b1d38 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 ff3d3ac7b5dd..711a9b4ae3bf 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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e197899faf36..e801bcfd493a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -704,8 +704,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 @@ -8938,8 +8938,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==} @@ -10951,8 +10951,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==} @@ -14217,8 +14218,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: @@ -27332,7 +27333,7 @@ snapshots: '@types/long@4.0.2': {} - '@types/luxon@3.3.8': {} + '@types/luxon@3.7.6': {} '@types/mdast@4.0.1': dependencies: @@ -29836,12 +29837,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: {} @@ -33784,7 +33785,7 @@ snapshots: lru.min@1.1.4: {} - luxon@3.4.4: {} + luxon@3.7.2: {} lz-string@1.5.0: {}