From ebd108be83bec4fec3a3d2810020dd243c793658 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 16 Oct 2026 17:40:37 -0700 Subject: [PATCH 01/20] feat(node): Add Crons check-ins for BullMQ job schedulers `Sentry.cron.instrumentBullMQWorker(worker)` sends check-ins for every job the worker runs from a job scheduler, using the scheduler id as the monitor slug and its `pattern` or `every` as the monitor schedule. Co-Authored-By: Claude --- .../node-integration-tests/package.json | 1 + .../suites/cron/bullmq/docker-compose.yml | 14 ++ .../suites/cron/bullmq/scenario.ts | 44 +++++ .../suites/cron/bullmq/test.ts | 51 +++++ packages/node/src/cron/bullmq.ts | 181 ++++++++++++++++++ packages/node/src/cron/index.ts | 2 + packages/node/test/cron/bullmq.test.ts | 140 ++++++++++++++ 7 files changed, 433 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/test.ts create mode 100644 packages/node/src/cron/bullmq.ts create mode 100644 packages/node/test/cron/bullmq.test.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index fdef92a3ba24..b7fcb74c73af 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -63,6 +63,7 @@ "ai": "^4.3.16", "amqplib": "^0.10.9", "body-parser": "^2.3.0", + "bullmq": "5.81.5", "consola": "^3.2.3", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml b/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml new file mode 100644 index 000000000000..1196c2651032 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.9' + +services: + db: + image: redis:latest + restart: always + ports: + - '6384:6379' + healthcheck: + test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts new file mode 100644 index 000000000000..dac500641cb2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts @@ -0,0 +1,44 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { Queue, Worker } from 'bullmq'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +const connection = { host: 'localhost', port: 6384 }; + +async function run(): Promise { + const queue = new Queue('crons', { connection }); + await queue.obliterate({ force: true }); + + const worker = new Worker( + 'crons', + async job => { + if (job.name === 'failing') { + throw new Error('Error in scheduled job'); + } + }, + { connection }, + ); + Sentry.cron.instrumentBullMQWorker(worker); + + await queue.add('plain', {}); + await queue.upsertJobScheduler('every-scheduler', { every: 300_000 }, { name: 'every' }); + await queue.upsertJobScheduler( + 'pattern-scheduler', + { pattern: '0 0 9 * * 1-5', tz: 'Europe/Vienna', immediately: true }, + { name: 'failing' }, + ); + + setTimeout(async () => { + await worker.close(); + await queue.obliterate({ force: true }); + await queue.close(); + process.exit(); + }, 5_000); +} + +void run(); diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts new file mode 100644 index 000000000000..62c18dc3dc52 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts @@ -0,0 +1,51 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner, describeWithDockerCompose } from '../../../utils/runner'; + +describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: [__dirname] }, () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('sends check-ins with the job scheduler schedule', { timeout: 60_000 }, async () => { + await createRunner(__dirname, 'scenario.ts') + .unordered() + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'every-scheduler', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'interval', value: 5, unit: 'minute' } }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'every-scheduler', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'pattern-scheduler', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'pattern-scheduler', + status: 'error', + release: '1.0', + duration: expect.any(Number), + }, + }) + .start() + .completed(); + }); +}); diff --git a/packages/node/src/cron/bullmq.ts b/packages/node/src/cron/bullmq.ts new file mode 100644 index 000000000000..818e5a1e915f --- /dev/null +++ b/packages/node/src/cron/bullmq.ts @@ -0,0 +1,181 @@ +import { captureCheckIn, debug, type MonitorConfig, timestampInSeconds } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { replaceCronNames } from './common'; + +interface BullMQRepeatOptions { + pattern?: unknown; + every?: unknown; + tz?: unknown; + limit?: unknown; + endDate?: unknown; +} + +interface BullMQJob { + repeatJobKey?: unknown; + opts?: { repeat?: BullMQRepeatOptions }; +} + +export interface InstrumentBullMQWorkerOptions { + /** Job scheduler ids to not monitor. Strings match the id exactly. */ + excludeJobSchedulers?: Array; +} + +const CRON_PRESETS: Record = { + '@yearly': '0 0 1 1 *', + '@annually': '0 0 1 1 *', + '@monthly': '0 0 1 * *', + '@weekly': '0 0 * * 0', + '@daily': '0 0 * * *', + '@hourly': '0 * * * *', +}; + +const FULL_DAY_OF_WEEK_RANGES = new Set(['0-6', '0-7', '1-7']); + +// Legacy repeatable jobs (`queue.add` with `repeat`) are keyed by a hash or `name:id:endDate:tz:pattern`. +const LEGACY_REPEAT_KEY = /^[\da-f]{32,}$|^(?:[^:]*:){4}/; + +const instrumentedWorkers = new WeakSet(); + +/** + * Sends check-ins to Sentry for every job a BullMQ worker runs from a job scheduler. + * The job scheduler id is the monitor slug, and its `pattern` or `every` is sent as the monitor schedule. + * + * ```ts + * import * as Sentry from '@sentry/node'; + * import { Worker } from 'bullmq'; + * + * const worker = new Worker('reports', processor, { connection }); + * Sentry.cron.instrumentBullMQWorker(worker, { excludeJobSchedulers: ['cache-warmup'] }); + * ``` + */ +export function instrumentBullMQWorker unknown }>( + worker: T, + options: InstrumentBullMQWorkerOptions = {}, +): T { + if (instrumentedWorkers.has(worker)) { + return worker; + } + instrumentedWorkers.add(worker); + + const running = new WeakMap(); + const on = (event: string, listener: (job?: BullMQJob) => void): unknown => + (worker.on as unknown as typeof on).call(worker, event, listener); + + on('active', job => { + try { + const monitorSlug = getJobSchedulerId(job, options.excludeJobSchedulers); + if (job && monitorSlug) { + const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, getMonitorConfig(job.opts?.repeat)); + running.set(job, { monitorSlug, checkInId, start: timestampInSeconds() }); + } + } catch (e) { + DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); + } + }); + + const finish = + (status: 'ok' | 'error') => + (job?: BullMQJob): void => { + try { + const run = job && running.get(job); + if (job && run) { + running.delete(job); + const { monitorSlug, checkInId, start } = run; + captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - start }); + } + } catch (e) { + DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); + } + }; + on('completed', finish('ok')); + on('failed', finish('error')); + + return worker; +} + +function getJobSchedulerId(job: BullMQJob | undefined, exclude: Array = []): string | undefined { + const id = job?.repeatJobKey; + if (typeof id !== 'string' || !id || LEGACY_REPEAT_KEY.test(id)) { + return undefined; + } + return exclude.some(pattern => (typeof pattern === 'string' ? pattern === id : pattern.test(id))) ? undefined : id; +} + +function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfig | undefined { + // A scheduler that stops after `limit` runs or `endDate` would show up as missed. + if (!repeat || repeat.limit != null || repeat.endDate != null) { + return undefined; + } + + if (typeof repeat.every === 'number') { + const minutes = repeat.every / 60_000; + if (!Number.isInteger(minutes) || minutes <= 0) { + return undefined; + } + if (minutes % 1440 === 0) { + return { schedule: { type: 'interval', value: minutes / 1440, unit: 'day' } }; + } + if (minutes % 60 === 0) { + return { schedule: { type: 'interval', value: minutes / 60, unit: 'hour' } }; + } + return { schedule: { type: 'interval', value: minutes, unit: 'minute' } }; + } + + const crontab = typeof repeat.pattern === 'string' ? toCrontab(repeat.pattern) : undefined; + // Without `tz`, cron-parser uses the server's time zone. + const timezone = toSentryTimeZone(typeof repeat.tz === 'string' && repeat.tz ? repeat.tz : getLocalTimeZone()); + return crontab && timezone ? { schedule: { type: 'crontab', value: crontab }, timezone } : undefined; +} + +/** + * Converts a cron-parser pattern to a crontab that Sentry reads the same way, if there is one. + */ +function toCrontab(pattern: string): string | undefined { + const preset = CRON_PRESETS[pattern.trim().toLowerCase()]; + if (preset) { + return preset; + } + + const fields = replaceCronNames(pattern).trim().split(/\s+/); + if (fields.length === 6 && /^0+$/.test(fields[0] as string)) { + fields.shift(); + } + if (fields.length !== 5 || fields.some(field => !/^[\d*,/\-L#]+$/.test(field))) { + return undefined; + } + + // With both day fields set, cron-parser runs on either, but Sentry needs both when one starts with `*`. + // cron-parser 4 also treats `1-31` as `*`. + const [, , dayOfMonth = '', , dayOfWeek = ''] = fields; + if ( + dayOfMonth !== '*' && + dayOfWeek !== '*' && + (dayOfMonth.startsWith('*') || + dayOfWeek.startsWith('*') || + dayOfMonth === '1-31' || + FULL_DAY_OF_WEEK_RANGES.has(dayOfWeek)) + ) { + return undefined; + } + + return fields.join(' '); +} + +function toSentryTimeZone(timezone: string | undefined): string | undefined { + if (!timezone || timezone === 'Etc/Unknown' || /^(?:utc|gmt)?[+-]/i.test(timezone)) { + return undefined; + } + try { + return new Intl.DateTimeFormat('en-US', { timeZone: timezone }).resolvedOptions().timeZone || undefined; + } catch { + return undefined; + } +} + +function getLocalTimeZone(): string | undefined { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined; + } catch { + return undefined; + } +} diff --git a/packages/node/src/cron/index.ts b/packages/node/src/cron/index.ts index eb4b915cff66..093e2ceacfb7 100644 --- a/packages/node/src/cron/index.ts +++ b/packages/node/src/cron/index.ts @@ -1,9 +1,11 @@ +import { instrumentBullMQWorker } from './bullmq'; import { instrumentCron } from './cron'; import { instrumentNodeCron } from './node-cron'; import { instrumentNodeSchedule } from './node-schedule'; /** Methods to instrument cron libraries for Sentry check-ins */ export const cron = { + instrumentBullMQWorker, instrumentCron, instrumentNodeCron, instrumentNodeSchedule, diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/node/test/cron/bullmq.test.ts new file mode 100644 index 000000000000..a5dacce91619 --- /dev/null +++ b/packages/node/test/cron/bullmq.test.ts @@ -0,0 +1,140 @@ +import { EventEmitter } from 'node:events'; +import * as SentryCore from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, type MockInstance, test, vi } from 'vitest'; +import { cron } from '../../src'; + +function schedulerJob(id: string, repeat: Record): object { + return { name: 'job', repeatJobKey: id, opts: { repeat: { count: 1, ...repeat } } }; +} + +describe('cron.instrumentBullMQWorker', () => { + let captureCheckInSpy: MockInstance; + + beforeEach(() => { + captureCheckInSpy = vi.spyOn(SentryCore, 'captureCheckIn').mockReturnValue('check-in-id'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + function runJob(job: object, outcome: 'completed' | 'failed' = 'completed', worker = new EventEmitter()): void { + cron.instrumentBullMQWorker(worker); + worker.emit('active', job, 'waiting'); + worker.emit(outcome, job, outcome === 'failed' ? new Error('boom') : 'result', 'active'); + } + + function sentConfig(repeat: Record): unknown { + runJob(schedulerJob('my-scheduler', repeat)); + return captureCheckInSpy.mock.calls[0]?.[1]; + } + + test('sends in_progress and ok check-ins with the scheduler id as slug', () => { + runJob(schedulerJob('daily-report', { pattern: '0 9 * * 1-5', tz: 'Europe/Vienna' })); + + expect(captureCheckInSpy).toHaveBeenNthCalledWith( + 1, + { monitorSlug: 'daily-report', status: 'in_progress' }, + { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }, + ); + expect(captureCheckInSpy).toHaveBeenNthCalledWith(2, { + monitorSlug: 'daily-report', + status: 'ok', + checkInId: 'check-in-id', + duration: expect.any(Number), + }); + }); + + test('sends an error check-in when the job fails', () => { + runJob(schedulerJob('daily-report', { every: 60_000 }), 'failed'); + + expect(captureCheckInSpy).toHaveBeenLastCalledWith(expect.objectContaining({ status: 'error' })); + }); + + test.each([ + ['0 0 9 * * *', '0 9 * * *'], + ['*/15 9-17 * * mon-fri', '*/15 9-17 * * 1-5'], + ['0 0 1 JAN *', '0 0 1 1 *'], + ['0 0 L * *', '0 0 L * *'], + ['0 0 * * 5L', '0 0 * * 5L'], + ['0 0 * * 1#2', '0 0 * * 1#2'], + ['0 0 1 * 1', '0 0 1 * 1'], + ['@daily', '0 0 * * *'], + ])('converts pattern %j to crontab %j', (pattern, crontab) => { + expect(sentConfig({ pattern, tz: 'UTC' })).toEqual({ + schedule: { type: 'crontab', value: crontab }, + timezone: 'UTC', + }); + }); + + test.each([ + '*/10 * * * * *', + '0 0 ? * 1', + '0 0 15W * *', + '0 0 */2 * 1', + '0 0 1 * */2', + '0 0 1-31 * 1', + '0 0 1 * 0-6', + '0 0 * * * * 2026', + ])('sends no config for pattern %j', pattern => { + expect(sentConfig({ pattern, tz: 'UTC' })).toBeUndefined(); + }); + + test('uses the local time zone without tz', () => { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + expect(sentConfig({ pattern: '0 * * * *' })).toEqual({ + schedule: { type: 'crontab', value: '0 * * * *' }, + timezone, + }); + }); + + test('sends no config for a fixed UTC offset', () => { + expect(sentConfig({ pattern: '0 * * * *', tz: 'UTC+5' })).toBeUndefined(); + }); + + test.each([ + [300_000, { type: 'interval', value: 5, unit: 'minute' }], + [7_200_000, { type: 'interval', value: 2, unit: 'hour' }], + [86_400_000, { type: 'interval', value: 1, unit: 'day' }], + ])('converts every %j ms to an interval', (every, schedule) => { + expect(sentConfig({ every })).toEqual({ schedule }); + }); + + test.each([30_000, 90_000])('sends no config for every %j ms', every => { + expect(sentConfig({ every })).toBeUndefined(); + }); + + test.each([{ limit: 3 }, { endDate: Date.now() + 60_000 }])('sends no config for a scheduler that ends: %j', end => { + expect(sentConfig({ every: 60_000, ...end })).toBeUndefined(); + }); + + test.each([ + ['a job that is not from a scheduler', { name: 'job', opts: {} }], + ['a legacy repeatable job', schedulerJob('ef2e05b7df2d56840b735ad4ee3a6937', { every: 60_000 })], + ['an excluded scheduler', schedulerJob('cache-warmup', { every: 60_000 })], + ['a scheduler matching an excluded pattern', schedulerJob('tmp-cleanup', { every: 60_000 })], + ])('ignores %s', (_, job) => { + const worker = new EventEmitter(); + cron.instrumentBullMQWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }); + worker.emit('active', job); + worker.emit('completed', job); + + expect(captureCheckInSpy).not.toHaveBeenCalled(); + }); + + test('instruments a worker only once', () => { + const worker = new EventEmitter(); + cron.instrumentBullMQWorker(worker); + runJob(schedulerJob('my-scheduler', { every: 60_000 }), 'completed', worker); + + expect(captureCheckInSpy).toHaveBeenCalledTimes(2); + }); + + test('does not throw into the worker when sending a check-in fails', () => { + captureCheckInSpy.mockImplementation(() => { + throw new Error('sentry failed'); + }); + + expect(() => runJob(schedulerJob('my-scheduler', { every: 60_000 }))).not.toThrow(); + }); +}); From b7b621ec8c0ca3a6eb40bea3cce17e5c09fd61b4 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Mon, 19 Oct 2026 17:47:10 -0700 Subject: [PATCH 02/20] chore(deps): Add bullmq to pnpm-lock.yaml Co-Authored-By: Claude --- pnpm-lock.yaml | 126 ++++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 1 + 2 files changed, 127 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfdd2832b6d3..19b7546eab4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -824,6 +824,9 @@ importers: body-parser: specifier: ^2.3.0 version: 2.3.0(supports-color@10.0.0) + bullmq: + specifier: 5.81.5 + version: 5.81.5(redis@5.12.1(@opentelemetry/api@1.9.1))(supports-color@10.0.0) consola: specifier: ^3.2.3 version: 3.4.2 @@ -5952,6 +5955,36 @@ packages: '@mongodb-js/saslprep@1.4.6': resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} + cpu: [x64] + os: [win32] + '@mswjs/interceptors@0.41.9': resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} @@ -10226,6 +10259,15 @@ packages: builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + bullmq@5.81.5: + resolution: {integrity: sha512-pW7pFc/X+9AEIx91TWTdIpZ1icfbJnA0unsyzD7VICjCUw8a3qedSjej5dxxn6LjfM1kr2jvalOCGbCmoA5Kxg==} + engines: {node: '>=12.22.0'} + peerDependencies: + redis: '>=5.0.0' + peerDependenciesMeta: + redis: + optional: true + bun-types@1.2.9: resolution: {integrity: sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw==} @@ -13050,6 +13092,10 @@ packages: resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} engines: {node: '>=12.22.0'} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + ioredis@6.0.0: resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==} engines: {node: '>=20.0.0'} @@ -14593,6 +14639,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} + hasBin: true + + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -14802,6 +14855,9 @@ packages: resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} engines: {node: '>=10'} + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} @@ -14829,6 +14885,10 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true @@ -24111,6 +24171,24 @@ snapshots: dependencies: sparse-bitfield: 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + optional: true + '@mswjs/interceptors@0.41.9': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -29040,6 +29118,19 @@ snapshots: dependencies: semver: 7.5.3 + bullmq@5.81.5(redis@5.12.1(@opentelemetry/api@1.9.1))(supports-color@10.0.0): + dependencies: + cron-parser: 4.9.0 + ioredis: 5.11.1(supports-color@10.0.0) + msgpackr: 2.0.5 + node-abort-controller: 3.1.1 + semver: 7.8.5 + tslib: 2.8.1 + optionalDependencies: + redis: 5.12.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + bun-types@1.2.9: dependencies: '@types/node': 20.19.43 @@ -32531,6 +32622,18 @@ snapshots: transitivePeerDependencies: - supports-color + ioredis@5.11.1(supports-color@10.0.0): + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3(supports-color@10.0.0) + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ioredis@6.0.0(supports-color@10.0.0): dependencies: '@ioredis/commands': 2.0.0 @@ -34316,6 +34419,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.4: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 + optional: true + + msgpackr@2.0.5: + optionalDependencies: + msgpackr-extract: 3.0.4 + muggle-string@0.4.1: {} multer@2.1.1: @@ -34931,6 +35050,8 @@ snapshots: dependencies: semver: 7.8.5 + node-abort-controller@3.1.1: {} + node-addon-api@3.2.1: optional: true @@ -34948,6 +35069,11 @@ snapshots: node-forge@1.4.0: {} + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-gyp-build@4.6.0: {} node-gyp@9.3.0(supports-color@10.0.0): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ee2f49ffa6d..95d8a1e2da57 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -90,6 +90,7 @@ allowBuilds: core-js-pure: false esbuild: true mongodb-memory-server-global: true + msgpackr-extract: true nice-napi: true nx: true prisma: true From d20f4237e87122d882d21ffc3e76562df8f398ba Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Mon, 19 Oct 2026 17:53:20 -0700 Subject: [PATCH 03/20] chore(deps): Dedupe ioredis in pnpm-lock.yaml Co-Authored-By: Claude --- pnpm-lock.yaml | 102 +++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19b7546eab4c..c614db7bcee7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -871,7 +871,7 @@ importers: version: 5.10.1(supports-color@10.0.0) ioredis-5: specifier: npm:ioredis@^5.11.0 - version: ioredis@5.11.0(supports-color@10.0.0) + version: ioredis@5.11.1(supports-color@10.0.0) ioredis-6: specifier: npm:ioredis@^6.0.0 version: ioredis@6.0.0(supports-color@10.0.0) @@ -1711,10 +1711,10 @@ importers: version: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) nitro: specifier: ^3.0.260415-beta - version: 3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) + version: 3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) unstorage: specifier: ^2.0.0-alpha.7 - version: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1) + version: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1) packages/node: dependencies: @@ -1795,19 +1795,19 @@ importers: devDependencies: '@nuxt/nitro-server': specifier: ^3.21.11 - version: 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) + version: 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) h3: specifier: ^1.15.10 version: 1.15.11 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) + version: 3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) nuxi: specifier: ^3.25.1 version: 3.25.1 nuxt: specifier: 3.21.11 - version: 3.21.11(655d928df5d3f11c3078cc77ff7227cd) + version: 3.21.11(da89d59432831a5304eb6a0ba7a35a74) vite: specifier: ^8.3.1 version: 8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) @@ -2205,7 +2205,7 @@ importers: version: 1.0.0(solid-js@1.9.11) '@solidjs/start': specifier: ^1.3.2 - version: 1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) + version: 1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) '@solidjs/testing-library': specifier: 0.8.5 version: 0.8.5(@solidjs/router@1.0.0(solid-js@1.9.11))(solid-js@1.9.11) @@ -2220,7 +2220,7 @@ importers: version: 1.9.11 vinxi: specifier: ^0.5.11 - version: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) + version: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) vite: specifier: ^8.3.1 version: 8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) @@ -13088,10 +13088,6 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ioredis@5.11.0: - resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} - engines: {node: '>=12.22.0'} - ioredis@5.11.1: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} @@ -24433,7 +24429,7 @@ snapshots: pkg-types: 2.3.1 semver: 7.8.5 - '@nuxt/devtools@3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2))': + '@nuxt/devtools@3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2))': dependencies: '@nuxt/devtools-kit': 3.4.1(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) '@nuxt/devtools-wizard': 3.4.1 @@ -24463,7 +24459,7 @@ snapshots: sirv: 3.0.2 structured-clone-es: 2.0.1 tinyglobby: 0.2.17 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vite: 8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.0(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) vite-plugin-vue-tracer: 1.5.0(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2)) @@ -24629,7 +24625,7 @@ snapshots: - magic-string - unplugin - '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))': + '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.11(magicast@0.5.4) @@ -24647,7 +24643,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.143.0)(rolldown@1.2.11)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) - nuxt: 3.21.11(655d928df5d3f11c3078cc77ff7227cd) + nuxt: 3.21.11(da89d59432831a5304eb6a0ba7a35a74) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.1 @@ -24655,7 +24651,7 @@ snapshots: std-env: 4.3.0 ufo: 1.6.4 unctx: 2.5.0 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vue: 3.5.41(typescript@7.0.2) vue-bundle-renderer: 2.3.2 vue-devtools-stub: 0.1.0 @@ -24703,7 +24699,7 @@ snapshots: - webpack - xml2js - '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))': + '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.11(magicast@0.5.4) @@ -24721,7 +24717,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) - nuxt: 3.21.11(655d928df5d3f11c3078cc77ff7227cd) + nuxt: 3.21.11(da89d59432831a5304eb6a0ba7a35a74) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.1 @@ -24729,7 +24725,7 @@ snapshots: std-env: 4.3.0 ufo: 1.6.4 unctx: 2.5.0 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vue: 3.5.41(typescript@7.0.2) vue-bundle-renderer: 2.3.2 vue-devtools-stub: 0.1.0 @@ -24798,7 +24794,7 @@ snapshots: rc9: 3.1.0 std-env: 4.3.0 - '@nuxt/vite-builder@3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd))(optionator@0.9.4)(oxlint@1.75.0(oxlint-tsgolint@7.0.2003))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.41(typescript@7.0.2))(yaml@2.9.0)': + '@nuxt/vite-builder@3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74))(optionator@0.9.4)(oxlint@1.75.0(oxlint-tsgolint@7.0.2003))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.41(typescript@7.0.2))(yaml@2.9.0)': dependencies: '@nuxt/kit': 3.21.11(magicast@0.5.4) '@rollup/plugin-replace': 6.0.3(rollup@4.62.3) @@ -24818,7 +24814,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 3.21.11(655d928df5d3f11c3078cc77ff7227cd) + nuxt: 3.21.11(da89d59432831a5304eb6a0ba7a35a74) nypm: 0.6.9 ohash: 2.0.11 pathe: 2.0.3 @@ -26606,11 +26602,11 @@ snapshots: dependencies: solid-js: 1.9.11 - '@solidjs/start@1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))': + '@solidjs/start@1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.21(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0)) - '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0)) cookie-es: 2.0.1 defu: 6.1.7 error-stack-parser: 2.1.4 @@ -26622,7 +26618,7 @@ snapshots: source-map-js: 1.2.2 terracotta: 1.0.5(solid-js@1.9.11) tinyglobby: 0.2.17 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) vite-plugin-solid: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) transitivePeerDependencies: - '@testing-library/jest-dom' @@ -27563,7 +27559,7 @@ snapshots: untun: 0.1.3 uqr: 0.1.3 - '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))': + '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))': dependencies: '@babel/parser': 7.29.8 acorn: 8.18.0 @@ -27574,18 +27570,18 @@ snapshots: magicast: 0.2.11 recast: 0.23.11 tslib: 2.8.1 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) - '@vinxi/server-components@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))': + '@vinxi/server-components@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0))': dependencies: - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0)) acorn: 8.18.0 acorn-loose: 8.4.0 acorn-typescript: 1.4.13(acorn@8.18.0) astring: 1.9.0 magicast: 0.2.11 recast: 0.23.11 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0) '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.0.0)(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2))': dependencies: @@ -32610,18 +32606,6 @@ snapshots: transitivePeerDependencies: - supports-color - ioredis@5.11.0(supports-color@10.0.0): - dependencies: - '@ioredis/commands': 1.10.0 - cluster-key-slot: 1.1.1 - debug: 4.4.3(supports-color@10.0.0) - denque: 2.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - ioredis@5.11.1(supports-color@10.0.0): dependencies: '@ioredis/commands': 1.10.0 @@ -34598,7 +34582,7 @@ snapshots: node-gyp-build: 4.6.0 optional: true - nitro@3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): + nitro@3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -34613,7 +34597,7 @@ snapshots: rolldown: 1.2.11 srvx: 0.11.22 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 3.3.1 @@ -34651,7 +34635,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): + nitro@3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -34666,7 +34650,7 @@ snapshots: rolldown: 1.2.11 srvx: 0.11.22 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 3.3.1 @@ -35239,16 +35223,16 @@ snapshots: nuxi@3.25.1: {} - nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd): + nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74): dependencies: '@dxup/nuxt': 0.5.6(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(esbuild@0.28.1)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) '@nuxt/cli': 3.37.0(@nuxt/schema@3.21.11)(@parcel/watcher@2.5.6)(cac@6.7.14)(commander@14.0.3)(magicast@0.5.4)(supports-color@10.0.0) - '@nuxt/devtools': 3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2)) + '@nuxt/devtools': 3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.41(typescript@7.0.2)) '@nuxt/kit': 3.21.11(magicast@0.5.4) - '@nuxt/nitro-server': 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) + '@nuxt/nitro-server': 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3)) '@nuxt/schema': 3.21.11 '@nuxt/telemetry': 2.8.0(@nuxt/kit@3.21.11(magicast@0.5.4)) - '@nuxt/vite-builder': 3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(655d928df5d3f11c3078cc77ff7227cd))(optionator@0.9.4)(oxlint@1.75.0(oxlint-tsgolint@7.0.2003))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.41(typescript@7.0.2))(yaml@2.9.0) + '@nuxt/vite-builder': 3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(da89d59432831a5304eb6a0ba7a35a74))(optionator@0.9.4)(oxlint@1.75.0(oxlint-tsgolint@7.0.2003))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.41(typescript@7.0.2))(yaml@2.9.0) '@unhead/vue': 2.1.17(vue@3.5.41(typescript@7.0.2)) '@vue/shared': 3.5.41 c12: 3.3.4(magicast@0.5.4) @@ -39578,7 +39562,7 @@ snapshots: db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) ioredis: 5.10.1(supports-color@10.0.0) - unstorage@1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)): + unstorage@1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -39591,7 +39575,7 @@ snapshots: optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.0(supports-color@10.0.0) + ioredis: 5.11.1(supports-color@10.0.0) unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@3.7.4)(ofetch@1.5.1): optionalDependencies: @@ -39603,22 +39587,22 @@ snapshots: mongodb: 3.7.4 ofetch: 1.5.1 - unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1): + unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1): optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) chokidar: 5.0.0 db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.0(supports-color@10.0.0) + ioredis: 5.11.1(supports-color@10.0.0) lru-cache: 11.4.0 mongodb: 7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7) ofetch: 1.5.1 - unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3): optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) chokidar: 5.0.0 db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.0(supports-color@10.0.0) + ioredis: 5.11.1(supports-color@10.0.0) lru-cache: 11.4.0 mongodb: 7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7) ofetch: 2.0.0-alpha.3 @@ -39739,7 +39723,7 @@ snapshots: '@types/unist': 3.0.0 vfile-message: 4.0.3 - vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0): + vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.28))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.28)(uglify-js@3.13.3))(yaml@2.9.0): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) @@ -39772,7 +39756,7 @@ snapshots: ufo: 1.6.4 unctx: 2.5.0 unenv: 1.10.0 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vite: 6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) zod: 4.6.5 transitivePeerDependencies: From 3184ccacff2b762b62c03df4ea09139eb7646073 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Tue, 20 Oct 2026 10:41:48 -0700 Subject: [PATCH 04/20] fix(node): Send UTC for BullMQ schedulers with the utc option Co-Authored-By: Claude --- packages/node/src/cron/bullmq.ts | 7 +++++-- packages/node/test/cron/bullmq.test.ts | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/node/src/cron/bullmq.ts b/packages/node/src/cron/bullmq.ts index 818e5a1e915f..57acc6e98746 100644 --- a/packages/node/src/cron/bullmq.ts +++ b/packages/node/src/cron/bullmq.ts @@ -6,6 +6,7 @@ interface BullMQRepeatOptions { pattern?: unknown; every?: unknown; tz?: unknown; + utc?: unknown; limit?: unknown; endDate?: unknown; } @@ -122,8 +123,10 @@ function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfi } const crontab = typeof repeat.pattern === 'string' ? toCrontab(repeat.pattern) : undefined; - // Without `tz`, cron-parser uses the server's time zone. - const timezone = toSentryTimeZone(typeof repeat.tz === 'string' && repeat.tz ? repeat.tz : getLocalTimeZone()); + // cron-parser 4 (BullMQ 5) prefers `utc` over `tz`, and uses the server's time zone without either. + const timezone = toSentryTimeZone( + repeat.utc === true ? 'UTC' : typeof repeat.tz === 'string' && repeat.tz ? repeat.tz : getLocalTimeZone(), + ); return crontab && timezone ? { schedule: { type: 'crontab', value: crontab }, timezone } : undefined; } diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/node/test/cron/bullmq.test.ts index a5dacce91619..73a2a4e7fd16 100644 --- a/packages/node/test/cron/bullmq.test.ts +++ b/packages/node/test/cron/bullmq.test.ts @@ -88,6 +88,13 @@ describe('cron.instrumentBullMQWorker', () => { }); }); + test('uses UTC with utc: true', () => { + expect(sentConfig({ pattern: '0 * * * *', tz: 'Europe/Vienna', utc: true })).toEqual({ + schedule: { type: 'crontab', value: '0 * * * *' }, + timezone: 'UTC', + }); + }); + test('sends no config for a fixed UTC offset', () => { expect(sentConfig({ pattern: '0 * * * *', tz: 'UTC+5' })).toBeUndefined(); }); From b6b9096552ff01773e2548eec46d7a6e8c597e4f Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Mon, 26 Oct 2026 12:49:34 -0700 Subject: [PATCH 05/20] chore(deps): Dedupe the lockfile --- pnpm-lock.yaml | 104 +++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a61c12a696d4..99cc5e73040c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -741,7 +741,7 @@ importers: version: 5.10.1(supports-color@10.0.0) ioredis-5: specifier: npm:ioredis@^5.11.0 - version: ioredis@5.11.0(supports-color@10.0.0) + version: ioredis@5.11.1(supports-color@10.0.0) ioredis-6: specifier: npm:ioredis@^6.0.0 version: ioredis@6.0.0(supports-color@10.0.0) @@ -1718,13 +1718,13 @@ importers: version: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) nitro: specifier: ^3.0.260415-beta - version: 3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) + version: 3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) srvx: specifier: ^0.11.22 version: 0.11.22 unstorage: specifier: ^2.0.0-alpha.7 - version: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1) + version: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1) packages/node: dependencies: @@ -1814,7 +1814,7 @@ importers: version: 4.20260519.1 '@nuxt/nitro-server': specifier: ^3.21.11 - version: 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) + version: 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(955174819940f662d7743f134f0d89c6))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) '@nuxt/schema': specifier: ^3.21.11 version: 3.21.11 @@ -1829,7 +1829,7 @@ importers: version: 1.15.11 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) + version: 3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) nitropack: specifier: ^2.13.4 version: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) @@ -1838,10 +1838,10 @@ importers: version: 3.25.1 nuxt: specifier: 3.21.11 - version: 3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c) + version: 3.21.11(955174819940f662d7743f134f0d89c6) unstorage: specifier: ^1.17.5 - version: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + version: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vite: specifier: ^8.3.1 version: 8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) @@ -2306,7 +2306,7 @@ importers: version: 1.0.0(solid-js@1.9.11) '@solidjs/start': specifier: ^1.3.2 - version: 1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) + version: 1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) '@solidjs/testing-library': specifier: 0.8.5 version: 0.8.5(@solidjs/router@1.0.0(solid-js@1.9.11))(solid-js@1.9.11) @@ -2327,7 +2327,7 @@ importers: version: 1.9.11 vinxi: specifier: ^0.5.11 - version: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) + version: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) vite: specifier: ^8.3.1 version: 8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) @@ -13214,10 +13214,6 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ioredis@5.11.0: - resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} - engines: {node: '>=12.22.0'} - ioredis@5.11.1: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} @@ -24561,7 +24557,7 @@ snapshots: pkg-types: 2.3.1 semver: 7.8.5 - '@nuxt/devtools@3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': + '@nuxt/devtools@3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': dependencies: '@nuxt/devtools-kit': 3.4.1(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) '@nuxt/devtools-wizard': 3.4.1 @@ -24591,7 +24587,7 @@ snapshots: sirv: 3.0.2 structured-clone-es: 2.0.1 tinyglobby: 0.2.17 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vite: 8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.0(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) vite-plugin-vue-tracer: 1.5.0(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) @@ -24757,7 +24753,7 @@ snapshots: - magic-string - unplugin - '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))': + '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(955174819940f662d7743f134f0d89c6))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.11(magicast@0.5.4) @@ -24775,7 +24771,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.143.0)(rolldown@1.2.11)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) - nuxt: 3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c) + nuxt: 3.21.11(955174819940f662d7743f134f0d89c6) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.1 @@ -24783,7 +24779,7 @@ snapshots: std-env: 4.3.0 ufo: 1.6.4 unctx: 2.5.0 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vue: 3.5.44(typescript@7.0.2) vue-bundle-renderer: 2.3.2 vue-devtools-stub: 0.1.0 @@ -24831,7 +24827,7 @@ snapshots: - webpack - xml2js - '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))': + '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(955174819940f662d7743f134f0d89c6))(oxc-parser@0.152.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.11(magicast@0.5.4) @@ -24849,7 +24845,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) - nuxt: 3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c) + nuxt: 3.21.11(955174819940f662d7743f134f0d89c6) ohash: 2.0.11 pathe: 2.0.3 pkg-types: 2.3.1 @@ -24857,7 +24853,7 @@ snapshots: std-env: 4.3.0 ufo: 1.6.4 unctx: 2.5.0 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vue: 3.5.44(typescript@7.0.2) vue-bundle-renderer: 2.3.2 vue-devtools-stub: 0.1.0 @@ -24926,7 +24922,7 @@ snapshots: rc9: 3.1.0 std-env: 4.3.0 - '@nuxt/vite-builder@3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0)': + '@nuxt/vite-builder@3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(955174819940f662d7743f134f0d89c6))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0)': dependencies: '@nuxt/kit': 3.21.11(magicast@0.5.4) '@rollup/plugin-replace': 6.0.3(rollup@4.62.3) @@ -24946,7 +24942,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c) + nuxt: 3.21.11(955174819940f662d7743f134f0d89c6) nypm: 0.6.9 ohash: 2.0.11 pathe: 2.0.3 @@ -26734,11 +26730,11 @@ snapshots: dependencies: solid-js: 1.9.11 - '@solidjs/start@1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))': + '@solidjs/start@1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.21(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0)) - '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0)) cookie-es: 2.0.1 defu: 6.1.7 error-stack-parser: 2.1.4 @@ -26750,7 +26746,7 @@ snapshots: source-map-js: 1.2.2 terracotta: 1.0.5(solid-js@1.9.11) tinyglobby: 0.2.17 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) vite-plugin-solid: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(supports-color@10.0.0)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)) transitivePeerDependencies: - '@testing-library/jest-dom' @@ -27691,7 +27687,7 @@ snapshots: untun: 0.1.3 uqr: 0.1.3 - '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))': + '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))': dependencies: '@babel/parser': 7.29.9 acorn: 8.18.0 @@ -27702,18 +27698,18 @@ snapshots: magicast: 0.2.11 recast: 0.23.11 tslib: 2.8.1 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) - '@vinxi/server-components@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))': + '@vinxi/server-components@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0))': dependencies: - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0)) acorn: 8.18.0 acorn-loose: 8.4.0 acorn-typescript: 1.4.13(acorn@8.18.0) astring: 1.9.0 magicast: 0.2.11 recast: 0.23.11 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0) '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.0.0)(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': dependencies: @@ -32677,18 +32673,6 @@ snapshots: transitivePeerDependencies: - supports-color - ioredis@5.11.0(supports-color@10.0.0): - dependencies: - '@ioredis/commands': 1.10.0 - cluster-key-slot: 1.1.1 - debug: 4.4.3(supports-color@10.0.0) - denque: 2.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - ioredis@5.11.1(supports-color@10.0.0): dependencies: '@ioredis/commands': 1.10.0 @@ -34661,7 +34645,7 @@ snapshots: node-gyp-build: 4.6.0 optional: true - nitro@3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): + nitro@3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -34676,7 +34660,7 @@ snapshots: rolldown: 1.2.11 srvx: 0.11.22 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 3.3.1 @@ -34714,7 +34698,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): + nitro@3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -34729,7 +34713,7 @@ snapshots: rolldown: 1.2.11 srvx: 0.11.22 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 3.3.1 @@ -35302,16 +35286,16 @@ snapshots: nuxi@3.25.1: {} - nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c): + nuxt@3.21.11(955174819940f662d7743f134f0d89c6): dependencies: '@dxup/nuxt': 0.5.6(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(esbuild@0.28.1)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) '@nuxt/cli': 3.37.0(@nuxt/schema@3.21.11)(@parcel/watcher@2.5.6)(cac@6.7.14)(commander@14.0.3)(magicast@0.5.4)(supports-color@10.0.0) - '@nuxt/devtools': 3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) + '@nuxt/devtools': 3.4.1(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.11)(supports-color@10.0.0)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.11)(rollup@4.62.3)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)))(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) '@nuxt/kit': 3.21.11(magicast@0.5.4) - '@nuxt/nitro-server': 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.0(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) + '@nuxt/nitro-server': 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.4)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(955174819940f662d7743f134f0d89c6))(oxc-parser@0.143.0)(rolldown@1.2.11)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.1(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0))(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3)) '@nuxt/schema': 3.21.11 '@nuxt/telemetry': 2.8.0(@nuxt/kit@3.21.11(magicast@0.5.4)) - '@nuxt/vite-builder': 3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(0dc0d2df1a20b551c0e4dd687b74c07c))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0) + '@nuxt/vite-builder': 3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.4)(meow@13.2.0)(nuxt@3.21.11(955174819940f662d7743f134f0d89c6))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.11)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.11)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0) '@unhead/vue': 2.1.17(vue@3.5.44(typescript@7.0.2)) '@vue/shared': 3.5.44 c12: 3.3.4(magicast@0.5.4) @@ -39641,7 +39625,7 @@ snapshots: db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) ioredis: 5.10.1(supports-color@10.0.0) - unstorage@1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)): + unstorage@1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -39654,7 +39638,7 @@ snapshots: optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.0(supports-color@10.0.0) + ioredis: 5.11.1(supports-color@10.0.0) unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@3.7.4)(ofetch@1.5.1): optionalDependencies: @@ -39666,22 +39650,22 @@ snapshots: mongodb: 3.7.4 ofetch: 1.5.1 - unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1): + unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1): optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) chokidar: 5.0.0 db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.0(supports-color@10.0.0) + ioredis: 5.11.1(supports-color@10.0.0) lru-cache: 11.4.0 mongodb: 7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7) ofetch: 1.5.1 - unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3): optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) chokidar: 5.0.0 db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.0(supports-color@10.0.0) + ioredis: 5.11.1(supports-color@10.0.0) lru-cache: 11.4.0 mongodb: 7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7) ofetch: 2.0.0-alpha.3 @@ -39802,7 +39786,7 @@ snapshots: '@types/unist': 3.0.0 vfile-message: 4.0.3 - vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.0(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0): + vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.11)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.43.1)(tsx@4.23.0)(webpack@5.107.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(uglify-js@3.13.3))(yaml@2.9.0): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) @@ -39835,7 +39819,7 @@ snapshots: ufo: 1.6.4 unctx: 2.5.0 unenv: 1.10.0 - unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.0(supports-color@10.0.0)) + unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vite: 6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.43.1)(tsx@4.23.0)(yaml@2.9.0) zod: 4.6.5 transitivePeerDependencies: From b8c15b634fe38d552a62b3a9c1f1c9b9de458506 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 28 Oct 2026 16:51:21 -0700 Subject: [PATCH 06/20] test(node): Use a separate Redis port for the BullMQ cron suite The BullMQ tracing suite from develop also binds 6384. --- .../suites/cron/bullmq/docker-compose.yml | 2 +- .../node-integration-tests/suites/cron/bullmq/scenario.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml b/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml index 1196c2651032..c54562dfc169 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/docker-compose.yml @@ -5,7 +5,7 @@ services: image: redis:latest restart: always ports: - - '6384:6379' + - '6385:6379' healthcheck: test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG'] interval: 2s diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts index dac500641cb2..91df1313b067 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts @@ -8,7 +8,7 @@ Sentry.init({ transport: loggingTransport, }); -const connection = { host: 'localhost', port: 6384 }; +const connection = { host: 'localhost', port: 6385 }; async function run(): Promise { const queue = new Queue('crons', { connection }); From 3e16f88c2d778e7cccd9429806896dd494acd90a Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 28 Oct 2026 17:46:45 -0700 Subject: [PATCH 07/20] fix(node): Send the BullMQ in_progress check-in in the job's trace With BullMQTelemetry, the worker emits `active` before the job's process span starts, so the in_progress check-in linked to the previous job's trace. Sentry keeps that trace for the check-in. Send it from the processor instead, and add an integration test that runs a job scheduler with both enabled. --- .../suites/cron/bullmq/scenario-telemetry.mjs | 64 ++++++++++++++++++ .../suites/cron/bullmq/test.ts | 67 +++++++++++++++++++ packages/node/src/cron/bullmq.ts | 26 +++++-- packages/node/test/cron/bullmq.test.ts | 47 +++++++++++-- 4 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs new file mode 100644 index 000000000000..42ac3f479d31 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { Queue, Worker } from 'bullmq'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +const connection = { host: 'localhost', port: 6385 }; +const telemetry = new Sentry.BullMQTelemetry(); +const instrumentBeforeRun = process.env.INSTRUMENT_BEFORE_RUN === 'true'; + +async function run() { + const queue = new Queue('crons-telemetry', { connection, telemetry }); + await queue.obliterate({ force: true }); + + await queue.upsertJobScheduler('ok-scheduler', { every: 300_000 }, { name: 'ok-job' }); + await queue.upsertJobScheduler( + 'failing-scheduler', + { pattern: '0 0 9 * * 1-5', tz: 'Europe/Vienna', immediately: true }, + { name: 'failing-job' }, + ); + + const worker = new Worker( + 'crons-telemetry', + async job => { + await Sentry.startSpan({ name: `work ${job.name}` }, async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + }); + if (job.name === 'failing-job') { + throw new Error('Error in scheduled job'); + } + }, + { connection, telemetry, autorun: !instrumentBeforeRun }, + ); + Sentry.cron.instrumentBullMQWorker(worker); + + const finished = new Promise(resolve => { + let count = 0; + const done = () => { + if (++count === 2) { + resolve(); + } + }; + worker.on('completed', done); + worker.on('failed', done); + }); + + if (instrumentBeforeRun) { + void worker.run(); + } + + await finished; + await worker.close(); + await queue.obliterate({ force: true }); + await queue.close(); + await Sentry.flush(); + process.exit(); +} + +void run(); diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts index 62c18dc3dc52..f3c684b09ed9 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts @@ -1,3 +1,4 @@ +import type { Event, SerializedCheckIn } from '@sentry/core'; import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner, describeWithDockerCompose } from '../../../utils/runner'; @@ -48,4 +49,70 @@ describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: .start() .completed(); }); + + test.each([ + ['after the worker starts', 'false'], + ['before the worker runs', 'true'], + ])( + 'sends one check-in pair per run in the job trace with BullMQTelemetry, instrumented %s', + { timeout: 60_000 }, + async (_, instrumentBeforeRun) => { + const checkIns: SerializedCheckIn[] = []; + const errors: Event[] = []; + const runner = createRunner(__dirname, 'scenario-telemetry.mjs') + .withEnv({ INSTRUMENT_BEFORE_RUN: instrumentBeforeRun }) + .unordered() + .expectN(4, { check_in: checkIn => void checkIns.push(checkIn) }) + .expect({ event: event => void errors.push(event) }); + const spansByJob = ['ok-job', 'failing-job'].map(jobName => + runner.collectStreamedSpans(spans => + spans.some( + span => + span.is_segment && + span.name === 'process crons-telemetry' && + span.attributes['messaging.bullmq.job.name']?.value === jobName, + ), + ), + ); + + await runner.start().completed(); + + const [okSpans, failingSpans] = await Promise.all(spansByJob); + const okSegment = okSpans!.find(span => span.is_segment)!; + const failingSegment = failingSpans!.find(span => span.is_segment)!; + + expect(okSegment.status).toBe('ok'); + expect(failingSegment.status).toBe('error'); + expect(okSpans!.some(span => span.name === 'work ok-job' && span.parent_span_id === okSegment.span_id)).toBe( + true, + ); + expect( + failingSpans!.some(span => span.name === 'work failing-job' && span.parent_span_id === failingSegment.span_id), + ).toBe(true); + + const summary = (slug: string): unknown => + checkIns + .filter(checkIn => checkIn.monitor_slug === slug) + .map(checkIn => ({ + status: checkIn.status, + check_in_id: checkIn.check_in_id, + trace_id: checkIn.contexts?.trace?.trace_id, + })); + const okCheckInId = checkIns.find(checkIn => checkIn.monitor_slug === 'ok-scheduler')?.check_in_id; + const failingCheckInId = checkIns.find(checkIn => checkIn.monitor_slug === 'failing-scheduler')?.check_in_id; + + expect(summary('ok-scheduler')).toEqual([ + { status: 'in_progress', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, + { status: 'ok', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, + ]); + expect(summary('failing-scheduler')).toEqual([ + { status: 'in_progress', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, + { status: 'error', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, + ]); + + expect(errors).toHaveLength(1); + expect(errors[0]?.exception?.values?.[0]?.value).toBe('Error in scheduled job'); + expect(errors[0]?.contexts?.trace?.trace_id).toBe(failingSegment.trace_id); + }, + ); }); diff --git a/packages/node/src/cron/bullmq.ts b/packages/node/src/cron/bullmq.ts index 57acc6e98746..3dd448221a33 100644 --- a/packages/node/src/cron/bullmq.ts +++ b/packages/node/src/cron/bullmq.ts @@ -58,11 +58,17 @@ export function instrumentBullMQWorker unk } instrumentedWorkers.add(worker); + // Sentry keeps the trace of the `in_progress` check-in. BullMQ emits `active` before the job's trace starts, + // often inside the previous job's, so send it from the processor instead. + const target = worker as unknown as { processFn?: (job: BullMQJob, ...args: unknown[]) => unknown }; + const processFn = target.processFn; + if (typeof processFn !== 'function') { + return worker; + } + const running = new WeakMap(); - const on = (event: string, listener: (job?: BullMQJob) => void): unknown => - (worker.on as unknown as typeof on).call(worker, event, listener); - on('active', job => { + target.processFn = function (this: unknown, job: BullMQJob, ...args: unknown[]): unknown { try { const monitorSlug = getJobSchedulerId(job, options.excludeJobSchedulers); if (job && monitorSlug) { @@ -72,7 +78,8 @@ export function instrumentBullMQWorker unk } catch (e) { DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); } - }); + return processFn.call(this, job, ...args); + }; const finish = (status: 'ok' | 'error') => @@ -83,13 +90,20 @@ export function instrumentBullMQWorker unk running.delete(job); const { monitorSlug, checkInId, start } = run; captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - start }); + return; + } + // BullMQ fails some jobs, such as ones that stalled too often, without running the processor. + const monitorSlug = status === 'error' && getJobSchedulerId(job, options.excludeJobSchedulers); + if (job && monitorSlug) { + captureCheckIn({ monitorSlug, status }, getMonitorConfig(job.opts?.repeat)); } } catch (e) { DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); } }; - on('completed', finish('ok')); - on('failed', finish('error')); + const on = worker.on as unknown as (event: string, listener: (job?: BullMQJob) => void) => unknown; + on.call(worker, 'completed', finish('ok')); + on.call(worker, 'failed', finish('error')); return worker; } diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/node/test/cron/bullmq.test.ts index 73a2a4e7fd16..c6d7dc28618a 100644 --- a/packages/node/test/cron/bullmq.test.ts +++ b/packages/node/test/cron/bullmq.test.ts @@ -18,9 +18,13 @@ describe('cron.instrumentBullMQWorker', () => { vi.restoreAllMocks(); }); - function runJob(job: object, outcome: 'completed' | 'failed' = 'completed', worker = new EventEmitter()): void { + function createWorker(): EventEmitter & { processFn: (job: object) => unknown } { + return Object.assign(new EventEmitter(), { processFn: vi.fn((_job: object) => 'result') }); + } + + function runJob(job: object, outcome: 'completed' | 'failed' = 'completed', worker = createWorker()): void { cron.instrumentBullMQWorker(worker); - worker.emit('active', job, 'waiting'); + worker.processFn(job); worker.emit(outcome, job, outcome === 'failed' ? new Error('boom') : 'result', 'active'); } @@ -121,16 +125,47 @@ describe('cron.instrumentBullMQWorker', () => { ['an excluded scheduler', schedulerJob('cache-warmup', { every: 60_000 })], ['a scheduler matching an excluded pattern', schedulerJob('tmp-cleanup', { every: 60_000 })], ])('ignores %s', (_, job) => { - const worker = new EventEmitter(); + const worker = createWorker(); cron.instrumentBullMQWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }); - worker.emit('active', job); - worker.emit('completed', job); + worker.processFn(job); + worker.emit('failed', job); + + expect(captureCheckInSpy).not.toHaveBeenCalled(); + }); + + test('calls the processor with the worker and its arguments', () => { + const worker = createWorker(); + const processor = worker.processFn; + cron.instrumentBullMQWorker(worker); + const job = schedulerJob('my-scheduler', { every: 60_000 }); + + expect(worker.processFn.call(worker, job, 'token')).toBe('result'); + expect(processor).toHaveBeenCalledWith(job, 'token'); + expect(vi.mocked(processor).mock.contexts[0]).toBe(worker); + }); + + test('sends an error check-in for a job that fails without running the processor', () => { + const worker = createWorker(); + cron.instrumentBullMQWorker(worker); + worker.emit('failed', schedulerJob('my-scheduler', { every: 60_000 }), new Error('stalled'), 'active'); + + expect(captureCheckInSpy).toHaveBeenCalledTimes(1); + expect(captureCheckInSpy).toHaveBeenCalledWith( + { monitorSlug: 'my-scheduler', status: 'error' }, + { schedule: { type: 'interval', value: 1, unit: 'minute' } }, + ); + }); + + test('sends no check-in for a job that completes without running the processor', () => { + const worker = createWorker(); + cron.instrumentBullMQWorker(worker); + worker.emit('completed', schedulerJob('my-scheduler', { every: 60_000 }), 'result', 'active'); expect(captureCheckInSpy).not.toHaveBeenCalled(); }); test('instruments a worker only once', () => { - const worker = new EventEmitter(); + const worker = createWorker(); cron.instrumentBullMQWorker(worker); runJob(schedulerJob('my-scheduler', { every: 60_000 }), 'completed', worker); From 8af9c7710dca30de6eaceff3beb5daa76692d473 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 29 Oct 2026 11:02:49 -0700 Subject: [PATCH 08/20] fix(node): Skip jobs with unsupported schedules when monitoring all jobs instrumentBullMQWorker only monitors job schedulers whose schedule Sentry can represent. --- packages/node/src/cron/bullmq.ts | 30 ++++++++++++++++++++------ packages/node/test/cron/bullmq.test.ts | 24 ++++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/node/src/cron/bullmq.ts b/packages/node/src/cron/bullmq.ts index 3dd448221a33..65cb43481162 100644 --- a/packages/node/src/cron/bullmq.ts +++ b/packages/node/src/cron/bullmq.ts @@ -40,6 +40,7 @@ const instrumentedWorkers = new WeakSet(); /** * Sends check-ins to Sentry for every job a BullMQ worker runs from a job scheduler. * The job scheduler id is the monitor slug, and its `pattern` or `every` is sent as the monitor schedule. + * Schedulers whose schedule Sentry can't represent, or that set `limit` or `endDate`, are not monitored. * * ```ts * import * as Sentry from '@sentry/node'; @@ -70,9 +71,10 @@ export function instrumentBullMQWorker unk target.processFn = function (this: unknown, job: BullMQJob, ...args: unknown[]): unknown { try { - const monitorSlug = getJobSchedulerId(job, options.excludeJobSchedulers); - if (job && monitorSlug) { - const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, getMonitorConfig(job.opts?.repeat)); + const monitor = getMonitor(job, options.excludeJobSchedulers); + if (job && monitor) { + const { monitorSlug, config } = monitor; + const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, config); running.set(job, { monitorSlug, checkInId, start: timestampInSeconds() }); } } catch (e) { @@ -93,9 +95,9 @@ export function instrumentBullMQWorker unk return; } // BullMQ fails some jobs, such as ones that stalled too often, without running the processor. - const monitorSlug = status === 'error' && getJobSchedulerId(job, options.excludeJobSchedulers); - if (job && monitorSlug) { - captureCheckIn({ monitorSlug, status }, getMonitorConfig(job.opts?.repeat)); + const monitor = status === 'error' && getMonitor(job, options.excludeJobSchedulers); + if (monitor) { + captureCheckIn({ monitorSlug: monitor.monitorSlug, status }, monitor.config); } } catch (e) { DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); @@ -108,6 +110,22 @@ export function instrumentBullMQWorker unk return worker; } +function getMonitor( + job: BullMQJob | undefined, + exclude: Array | undefined, +): { monitorSlug: string; config: MonitorConfig } | undefined { + const monitorSlug = getJobSchedulerId(job, exclude); + if (!monitorSlug) { + return undefined; + } + const config = getMonitorConfig(job?.opts?.repeat); + if (!config) { + DEBUG_BUILD && debug.log(`[BullMQ] Not monitoring ${monitorSlug}: Sentry can't represent its schedule.`); + return undefined; + } + return { monitorSlug, config }; +} + function getJobSchedulerId(job: BullMQJob | undefined, exclude: Array = []): string | undefined { const id = job?.repeatJobKey; if (typeof id !== 'string' || !id || LEGACY_REPEAT_KEY.test(id)) { diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/node/test/cron/bullmq.test.ts index c6d7dc28618a..033b9b2b2db5 100644 --- a/packages/node/test/cron/bullmq.test.ts +++ b/packages/node/test/cron/bullmq.test.ts @@ -33,6 +33,14 @@ describe('cron.instrumentBullMQWorker', () => { return captureCheckInSpy.mock.calls[0]?.[1]; } + function expectSkipped(repeat: Record): void { + runJob(schedulerJob('my-scheduler', repeat), 'failed'); + const worker = createWorker(); + cron.instrumentBullMQWorker(worker); + worker.emit('failed', schedulerJob('my-scheduler', repeat), new Error('stalled'), 'active'); + expect(captureCheckInSpy).not.toHaveBeenCalled(); + } + test('sends in_progress and ok check-ins with the scheduler id as slug', () => { runJob(schedulerJob('daily-report', { pattern: '0 9 * * 1-5', tz: 'Europe/Vienna' })); @@ -80,8 +88,8 @@ describe('cron.instrumentBullMQWorker', () => { '0 0 1-31 * 1', '0 0 1 * 0-6', '0 0 * * * * 2026', - ])('sends no config for pattern %j', pattern => { - expect(sentConfig({ pattern, tz: 'UTC' })).toBeUndefined(); + ])('skips pattern %j', pattern => { + expectSkipped({ pattern, tz: 'UTC' }); }); test('uses the local time zone without tz', () => { @@ -99,8 +107,8 @@ describe('cron.instrumentBullMQWorker', () => { }); }); - test('sends no config for a fixed UTC offset', () => { - expect(sentConfig({ pattern: '0 * * * *', tz: 'UTC+5' })).toBeUndefined(); + test('skips a fixed UTC offset', () => { + expectSkipped({ pattern: '0 * * * *', tz: 'UTC+5' }); }); test.each([ @@ -111,12 +119,12 @@ describe('cron.instrumentBullMQWorker', () => { expect(sentConfig({ every })).toEqual({ schedule }); }); - test.each([30_000, 90_000])('sends no config for every %j ms', every => { - expect(sentConfig({ every })).toBeUndefined(); + test.each([30_000, 90_000])('skips every %j ms', every => { + expectSkipped({ every }); }); - test.each([{ limit: 3 }, { endDate: Date.now() + 60_000 }])('sends no config for a scheduler that ends: %j', end => { - expect(sentConfig({ every: 60_000, ...end })).toBeUndefined(); + test.each([{ limit: 3 }, { endDate: Date.now() + 60_000 }])('skips a scheduler that ends: %j', end => { + expectSkipped({ every: 60_000, ...end }); }); test.each([ From 3adc3367f015173a610bba0fcc510ce4587ef182 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 10:29:25 -0800 Subject: [PATCH 09/20] fix(node): Keep one BullMQ check-in across job retries BullMQ emits `failed` for every attempt, so a retried job sent an error check-in and then opened a second in_progress. The check-in id now comes from the job, so retries and reruns of stalled jobs update the first attempt's check-in, even on another worker, and only the last failure sends an error. Also skips `pattern` schedules when the worker has a custom repeat strategy. Co-Authored-By: Claude --- CHANGELOG.md | 4 + .../suites/cron/bullmq/scenario.ts | 3 +- .../suites/cron/bullmq/test.ts | 73 +++++------ packages/node/src/cron/bullmq.ts | 99 +++++++++++---- packages/node/test/cron/bullmq.test.ts | 116 +++++++++++++++--- 5 files changed, 216 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd97aa38ba7..cbb2eb1023ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ Work in this release was contributed by @lazerg, @chiliec, @61465, @jpdefo, and Add `Sentry.cronJobIntegration()` to `integrations` to send check-ins for every named `CronJob` of the `cron` package (4.3+), with the job name as the monitor slug. It's off by default. Each monitored job creates a monitor in Sentry, and monitors are billed. Use `excludeJobs` to skip jobs by name. `Sentry.cron.instrumentCron` is deprecated; use `cronJobIntegration` instead. +- **feat(node): Add Crons monitoring for BullMQ job schedulers ([#25472](https://github.com/getsentry/sentry-javascript/pull/25472))** + + Call `Sentry.cron.instrumentBullMQWorker(worker)` to send check-ins for every job the worker runs from a job scheduler, with the scheduler id as the monitor slug and its `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. + - **feat(nextjs): Mark pageloads of cached pages ([#25318](https://github.com/getsentry/sentry-javascript/pull/25318))** When Next.js serves a page from its cache, for example an ISR page or a `cacheComponents` shell, the pageload now has the `page.server_rendering.type` attribute. It is `static` if the whole page came from the cache, and `partially_static` if parts of it rendered for this request. Then the pageload stays connected to this request, as before. If the request that rendered the cached page was sampled, the pageload also links to it. So in Sentry, a cached page no longer looks like missing server instrumentation. diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts index 91df1313b067..5e70150a5bef 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts @@ -17,7 +17,7 @@ async function run(): Promise { const worker = new Worker( 'crons', async job => { - if (job.name === 'failing') { + if (job.name === 'failing' || (job.name === 'flaky' && job.attemptsMade === 0)) { throw new Error('Error in scheduled job'); } }, @@ -32,6 +32,7 @@ async function run(): Promise { { pattern: '0 0 9 * * 1-5', tz: 'Europe/Vienna', immediately: true }, { name: 'failing' }, ); + await queue.upsertJobScheduler('retry-scheduler', { every: 600_000 }, { name: 'flaky', opts: { attempts: 2 } }); setTimeout(async () => { await worker.close(); diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts index f3c684b09ed9..84cf4d475b95 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts @@ -8,46 +8,47 @@ describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: }); test('sends check-ins with the job scheduler schedule', { timeout: 60_000 }, async () => { + const checkIns: SerializedCheckIn[] = []; await createRunner(__dirname, 'scenario.ts') .unordered() - .expect({ - check_in: { - check_in_id: expect.any(String), - monitor_slug: 'every-scheduler', - status: 'in_progress', - release: '1.0', - monitor_config: { schedule: { type: 'interval', value: 5, unit: 'minute' } }, - }, - }) - .expect({ - check_in: { - check_in_id: expect.any(String), - monitor_slug: 'every-scheduler', - status: 'ok', - release: '1.0', - duration: expect.any(Number), - }, - }) - .expect({ - check_in: { - check_in_id: expect.any(String), - monitor_slug: 'pattern-scheduler', - status: 'in_progress', - release: '1.0', - monitor_config: { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }, - }, - }) - .expect({ - check_in: { - check_in_id: expect.any(String), - monitor_slug: 'pattern-scheduler', - status: 'error', - release: '1.0', - duration: expect.any(Number), - }, - }) + .expectN(7, { check_in: checkIn => void checkIns.push(checkIn) }) .start() .completed(); + + const bySlug = (slug: string): SerializedCheckIn[] => checkIns.filter(checkIn => checkIn.monitor_slug === slug); + const everyCheckIns = bySlug('every-scheduler'); + const patternCheckIns = bySlug('pattern-scheduler'); + const retryCheckIns = bySlug('retry-scheduler'); + const everyConfig = { schedule: { type: 'interval', value: 5, unit: 'minute' } }; + const patternConfig = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; + const retryConfig = { schedule: { type: 'interval', value: 10, unit: 'minute' } }; + + expect(everyCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: everyConfig }), + expect.objectContaining({ + status: 'ok', + check_in_id: everyCheckIns[0]?.check_in_id, + duration: expect.any(Number), + monitor_config: everyConfig, + }), + ]); + expect(patternCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: patternConfig }), + expect.objectContaining({ + status: 'error', + check_in_id: patternCheckIns[0]?.check_in_id, + duration: expect.any(Number), + monitor_config: patternConfig, + }), + ]); + // The retry updates the first attempt's check-in, and Sentry measures its duration. + const retryCheckInId = retryCheckIns[0]?.check_in_id; + expect(retryCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), + expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), + expect.objectContaining({ status: 'ok', check_in_id: retryCheckInId, monitor_config: retryConfig }), + ]); + expect(retryCheckIns[2]).not.toHaveProperty('duration'); }); test.each([ diff --git a/packages/node/src/cron/bullmq.ts b/packages/node/src/cron/bullmq.ts index 65cb43481162..4588f8229848 100644 --- a/packages/node/src/cron/bullmq.ts +++ b/packages/node/src/cron/bullmq.ts @@ -1,4 +1,13 @@ -import { captureCheckIn, debug, type MonitorConfig, timestampInSeconds } from '@sentry/core'; +import { createHash } from 'node:crypto'; +import { + captureCheckIn, + debug, + getClient, + type InProgressCheckIn, + type MonitorConfig, + stringMatchesSomePattern, + timestampInSeconds, +} from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { replaceCronNames } from './common'; @@ -12,7 +21,13 @@ interface BullMQRepeatOptions { } interface BullMQJob { + id?: unknown; + queueQualifiedName?: unknown; + timestamp?: unknown; repeatJobKey?: unknown; + attemptsStarted?: unknown; + attemptsMade?: unknown; + finishedOn?: unknown; opts?: { repeat?: BullMQRepeatOptions }; } @@ -37,6 +52,15 @@ const LEGACY_REPEAT_KEY = /^[\da-f]{32,}$|^(?:[^:]*:){4}/; const instrumentedWorkers = new WeakSet(); +interface Monitor { + monitorSlug: string; + checkInId: string; + config: MonitorConfig; +} + +// `captureCheckIn` keeps a given `checkInId` for `in_progress` check-ins too. +type InProgressCheckInWithId = InProgressCheckIn & { checkInId: string }; + /** * Sends check-ins to Sentry for every job a BullMQ worker runs from a job scheduler. * The job scheduler id is the monitor slug, and its `pattern` or `every` is sent as the monitor schedule. @@ -61,21 +85,31 @@ export function instrumentBullMQWorker unk // Sentry keeps the trace of the `in_progress` check-in. BullMQ emits `active` before the job's trace starts, // often inside the previous job's, so send it from the processor instead. - const target = worker as unknown as { processFn?: (job: BullMQJob, ...args: unknown[]) => unknown }; + const target = worker as unknown as { + processFn?: (job: BullMQJob, ...args: unknown[]) => unknown; + opts?: { settings?: { repeatStrategy?: unknown } }; + }; const processFn = target.processFn; if (typeof processFn !== 'function') { return worker; } + // A custom repeat strategy decides what `pattern` means. + const hasCustomRepeatStrategy = !!target.opts?.settings?.repeatStrategy; + const monitorFor = (job: BullMQJob | undefined): Monitor | undefined => + getMonitor(job, options.excludeJobSchedulers, hasCustomRepeatStrategy); - const running = new WeakMap(); + const startTimes = new WeakMap(); target.processFn = function (this: unknown, job: BullMQJob, ...args: unknown[]): unknown { try { - const monitor = getMonitor(job, options.excludeJobSchedulers); + const monitor = monitorFor(job); if (job && monitor) { - const { monitorSlug, config } = monitor; - const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, config); - running.set(job, { monitorSlug, checkInId, start: timestampInSeconds() }); + const { monitorSlug, checkInId, config } = monitor; + // Retries and reruns of stalled jobs update the first attempt's check-in, which may be on another worker. + captureCheckIn({ monitorSlug, status: 'in_progress', checkInId } as InProgressCheckInWithId, config); + if (isFirstAttempt(job)) { + startTimes.set(job, timestampInSeconds()); + } } } catch (e) { DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); @@ -87,18 +121,21 @@ export function instrumentBullMQWorker unk (status: 'ok' | 'error') => (job?: BullMQJob): void => { try { - const run = job && running.get(job); - if (job && run) { - running.delete(job); - const { monitorSlug, checkInId, start } = run; - captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - start }); + // BullMQ emits `failed` for every attempt, and only sets `finishedOn` once it stops retrying. + if (!job || (status === 'error' && !job.finishedOn)) { return; } - // BullMQ fails some jobs, such as ones that stalled too often, without running the processor. - const monitor = status === 'error' && getMonitor(job, options.excludeJobSchedulers); - if (monitor) { - captureCheckIn({ monitorSlug: monitor.monitorSlug, status }, monitor.config); + const monitor = monitorFor(job); + if (!monitor) { + return; } + const { monitorSlug, checkInId, config } = monitor; + const start = startTimes.get(job); + // Without a start time, Sentry measures the duration from the first `in_progress` check-in. + const duration = start === undefined ? undefined : timestampInSeconds() - start; + // BullMQ fails some jobs, such as ones that started too often, without running the processor, so send the + // config in case the check-in doesn't exist yet. + captureCheckIn({ monitorSlug, status, checkInId, ...(duration !== undefined && { duration }) }, config); } catch (e) { DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); } @@ -113,25 +150,43 @@ export function instrumentBullMQWorker unk function getMonitor( job: BullMQJob | undefined, exclude: Array | undefined, -): { monitorSlug: string; config: MonitorConfig } | undefined { + hasCustomRepeatStrategy: boolean, +): Monitor | undefined { const monitorSlug = getJobSchedulerId(job, exclude); - if (!monitorSlug) { + if (!job || !monitorSlug) { return undefined; } - const config = getMonitorConfig(job?.opts?.repeat); + const repeat = job.opts?.repeat; + const config = hasCustomRepeatStrategy && repeat?.pattern != null ? undefined : getMonitorConfig(repeat); if (!config) { DEBUG_BUILD && debug.log(`[BullMQ] Not monitoring ${monitorSlug}: Sentry can't represent its schedule.`); return undefined; } - return { monitorSlug, config }; + return { monitorSlug, checkInId: getCheckInId(job), config }; } -function getJobSchedulerId(job: BullMQJob | undefined, exclude: Array = []): string | undefined { +// Check-in ids are global in Sentry, so mix in the DSN and environment, not just the job. +function getCheckInId(job: BullMQJob): string { + const options = getClient()?.getOptions(); + return createHash('sha256') + .update(JSON.stringify([options?.dsn, options?.environment, job.queueQualifiedName, job.id, job.timestamp])) + .digest('hex') + .slice(0, 32); +} + +function isFirstAttempt(job: BullMQJob): boolean { + return !job.attemptsMade && (typeof job.attemptsStarted !== 'number' || job.attemptsStarted <= 1); +} + +function getJobSchedulerId( + job: BullMQJob | undefined, + exclude: Array | undefined, +): string | undefined { const id = job?.repeatJobKey; if (typeof id !== 'string' || !id || LEGACY_REPEAT_KEY.test(id)) { return undefined; } - return exclude.some(pattern => (typeof pattern === 'string' ? pattern === id : pattern.test(id))) ? undefined : id; + return stringMatchesSomePattern(id, exclude, true) ? undefined : id; } function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfig | undefined { diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/node/test/cron/bullmq.test.ts index 033b9b2b2db5..ca7de45ec26c 100644 --- a/packages/node/test/cron/bullmq.test.ts +++ b/packages/node/test/cron/bullmq.test.ts @@ -3,8 +3,23 @@ import * as SentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, type MockInstance, test, vi } from 'vitest'; import { cron } from '../../src'; -function schedulerJob(id: string, repeat: Record): object { - return { name: 'job', repeatJobKey: id, opts: { repeat: { count: 1, ...repeat } } }; +function schedulerJob(id: string, repeat: Record, attempt: Record = {}): object { + return { + id: `repeat:${id}:1767225600000`, + queueQualifiedName: 'bull:reports', + timestamp: 1767225500000, + name: 'job', + repeatJobKey: id, + attemptsStarted: 1, + attemptsMade: 0, + opts: { repeat: { count: 1, ...repeat } }, + ...attempt, + }; +} + +// BullMQ sets `finishedOn` when a job fails for the last time. +function finalFailure(job: object): object { + return Object.assign(job, { finishedOn: Date.now() }); } describe('cron.instrumentBullMQWorker', () => { @@ -18,14 +33,23 @@ describe('cron.instrumentBullMQWorker', () => { vi.restoreAllMocks(); }); - function createWorker(): EventEmitter & { processFn: (job: object) => unknown } { - return Object.assign(new EventEmitter(), { processFn: vi.fn((_job: object) => 'result') }); + function createWorker( + opts: object = {}, + ): EventEmitter & { processFn: (job: object, ...args: unknown[]) => unknown; opts: object } { + return Object.assign(new EventEmitter(), { + processFn: vi.fn((_job: object, ..._args: unknown[]) => 'result'), + opts, + }); } function runJob(job: object, outcome: 'completed' | 'failed' = 'completed', worker = createWorker()): void { cron.instrumentBullMQWorker(worker); worker.processFn(job); - worker.emit(outcome, job, outcome === 'failed' ? new Error('boom') : 'result', 'active'); + if (outcome === 'failed') { + worker.emit('failed', finalFailure(job), new Error('boom'), 'active'); + } else { + worker.emit('completed', job, 'result', 'active'); + } } function sentConfig(repeat: Record): unknown { @@ -37,30 +61,31 @@ describe('cron.instrumentBullMQWorker', () => { runJob(schedulerJob('my-scheduler', repeat), 'failed'); const worker = createWorker(); cron.instrumentBullMQWorker(worker); - worker.emit('failed', schedulerJob('my-scheduler', repeat), new Error('stalled'), 'active'); + worker.emit('failed', finalFailure(schedulerJob('my-scheduler', repeat)), new Error('stalled'), 'active'); expect(captureCheckInSpy).not.toHaveBeenCalled(); } test('sends in_progress and ok check-ins with the scheduler id as slug', () => { runJob(schedulerJob('daily-report', { pattern: '0 9 * * 1-5', tz: 'Europe/Vienna' })); + const config = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; expect(captureCheckInSpy).toHaveBeenNthCalledWith( 1, - { monitorSlug: 'daily-report', status: 'in_progress' }, - { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }, + { monitorSlug: 'daily-report', status: 'in_progress', checkInId: expect.stringMatching(/^[\da-f]{32}$/) }, + config, + ); + const checkInId = captureCheckInSpy.mock.calls[0]?.[0].checkInId; + expect(captureCheckInSpy).toHaveBeenNthCalledWith( + 2, + { monitorSlug: 'daily-report', status: 'ok', checkInId, duration: expect.any(Number) }, + config, ); - expect(captureCheckInSpy).toHaveBeenNthCalledWith(2, { - monitorSlug: 'daily-report', - status: 'ok', - checkInId: 'check-in-id', - duration: expect.any(Number), - }); }); test('sends an error check-in when the job fails', () => { runJob(schedulerJob('daily-report', { every: 60_000 }), 'failed'); - expect(captureCheckInSpy).toHaveBeenLastCalledWith(expect.objectContaining({ status: 'error' })); + expect(captureCheckInSpy).toHaveBeenLastCalledWith(expect.objectContaining({ status: 'error' }), expect.anything()); }); test.each([ @@ -136,7 +161,7 @@ describe('cron.instrumentBullMQWorker', () => { const worker = createWorker(); cron.instrumentBullMQWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }); worker.processFn(job); - worker.emit('failed', job); + worker.emit('failed', finalFailure(job)); expect(captureCheckInSpy).not.toHaveBeenCalled(); }); @@ -155,21 +180,72 @@ describe('cron.instrumentBullMQWorker', () => { test('sends an error check-in for a job that fails without running the processor', () => { const worker = createWorker(); cron.instrumentBullMQWorker(worker); - worker.emit('failed', schedulerJob('my-scheduler', { every: 60_000 }), new Error('stalled'), 'active'); + const job = finalFailure(schedulerJob('my-scheduler', { every: 60_000 }, { attemptsStarted: 3 })); + worker.emit('failed', job, new Error('job started more than allowable limit'), 'active'); expect(captureCheckInSpy).toHaveBeenCalledTimes(1); expect(captureCheckInSpy).toHaveBeenCalledWith( - { monitorSlug: 'my-scheduler', status: 'error' }, + { monitorSlug: 'my-scheduler', status: 'error', checkInId: expect.any(String) }, { schedule: { type: 'interval', value: 1, unit: 'minute' } }, ); }); - test('sends no check-in for a job that completes without running the processor', () => { + test('keeps one check-in open across retries, on any worker', () => { + const repeat = { every: 60_000 }; + const firstWorker = createWorker(); + cron.instrumentBullMQWorker(firstWorker); + const firstAttempt = schedulerJob('my-scheduler', repeat); + firstWorker.processFn(firstAttempt); + firstWorker.emit('failed', Object.assign(firstAttempt, { attemptsMade: 1 }), new Error('boom'), 'active'); + + const secondWorker = createWorker(); + cron.instrumentBullMQWorker(secondWorker); + const retry = schedulerJob('my-scheduler', repeat, { attemptsStarted: 2, attemptsMade: 1 }); + secondWorker.processFn(retry); + secondWorker.emit('completed', retry, 'result', 'active'); + + const checkIns = captureCheckInSpy.mock.calls.map(([checkIn]) => checkIn); + const checkInId = checkIns[0].checkInId; + // Without a duration, Sentry measures from the first attempt's `in_progress` check-in. + expect(checkIns).toEqual([ + { monitorSlug: 'my-scheduler', status: 'in_progress', checkInId }, + { monitorSlug: 'my-scheduler', status: 'in_progress', checkInId }, + { monitorSlug: 'my-scheduler', status: 'ok', checkInId }, + ]); + }); + + test('uses a different check-in id for each run', () => { + runJob(schedulerJob('my-scheduler', { every: 60_000 })); + runJob(schedulerJob('my-scheduler', { every: 60_000 }, { id: 'repeat:my-scheduler:1767225660000' })); + + const [first, , second] = captureCheckInSpy.mock.calls.map(([checkIn]) => checkIn.checkInId); + expect(first).not.toBe(second); + }); + + test('sends an ok check-in for a job that completes without running the processor', () => { const worker = createWorker(); cron.instrumentBullMQWorker(worker); worker.emit('completed', schedulerJob('my-scheduler', { every: 60_000 }), 'result', 'active'); - expect(captureCheckInSpy).not.toHaveBeenCalled(); + expect(captureCheckInSpy).toHaveBeenCalledWith( + { monitorSlug: 'my-scheduler', status: 'ok', checkInId: expect.any(String) }, + { schedule: { type: 'interval', value: 1, unit: 'minute' } }, + ); + }); + + test('skips patterns with a custom repeat strategy', () => { + const worker = createWorker({ settings: { repeatStrategy: () => Date.now() } }); + cron.instrumentBullMQWorker(worker); + for (const repeat of [{ pattern: '0 * * * *', tz: 'UTC' }, { every: 60_000 }]) { + const job = schedulerJob('my-scheduler', repeat); + worker.processFn(job); + worker.emit('completed', job, 'result', 'active'); + } + + expect(captureCheckInSpy.mock.calls.map(([, config]) => config)).toEqual([ + { schedule: { type: 'interval', value: 1, unit: 'minute' } }, + { schedule: { type: 'interval', value: 1, unit: 'minute' } }, + ]); }); test('instruments a worker only once', () => { From aa898160738e662dfa3c0c227c886df26b6f6ff5 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 11:09:20 -0800 Subject: [PATCH 10/20] ref(node): Use the shared cron converter for BullMQ schedulers BullMQ patterns go through `getCronMonitorConfig` with the `cron-parser` dialect, like `node-schedule`. The monitor slug is `-`, since scheduler ids are only unique within a queue. Co-Authored-By: Claude --- CHANGELOG.md | 2 +- .../suites/cron/bullmq/test.ts | 18 ++-- packages/node/src/cron/bullmq.ts | 89 +++---------------- packages/node/test/cron/bullmq.test.ts | 17 ++-- 4 files changed, 35 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ede79ee1f9..840cd8b3a9d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Work in this release was contributed by @kunal649. Thank you for your contributi - **feat(node): Add Crons monitoring for BullMQ job schedulers ([#25472](https://github.com/getsentry/sentry-javascript/pull/25472))** - Call `Sentry.cron.instrumentBullMQWorker(worker)` to send check-ins for every job the worker runs from a job scheduler, with the scheduler id as the monitor slug and its `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. + Call `Sentry.cron.instrumentBullMQWorker(worker)` to send check-ins for every job the worker runs from a job scheduler, with `-` as the monitor slug and the scheduler's `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. ## 11.12.0 diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts index 84cf4d475b95..5b4afef82581 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts @@ -16,9 +16,9 @@ describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: .completed(); const bySlug = (slug: string): SerializedCheckIn[] => checkIns.filter(checkIn => checkIn.monitor_slug === slug); - const everyCheckIns = bySlug('every-scheduler'); - const patternCheckIns = bySlug('pattern-scheduler'); - const retryCheckIns = bySlug('retry-scheduler'); + const everyCheckIns = bySlug('crons-every-scheduler'); + const patternCheckIns = bySlug('crons-pattern-scheduler'); + const retryCheckIns = bySlug('crons-retry-scheduler'); const everyConfig = { schedule: { type: 'interval', value: 5, unit: 'minute' } }; const patternConfig = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; const retryConfig = { schedule: { type: 'interval', value: 10, unit: 'minute' } }; @@ -99,14 +99,18 @@ describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: check_in_id: checkIn.check_in_id, trace_id: checkIn.contexts?.trace?.trace_id, })); - const okCheckInId = checkIns.find(checkIn => checkIn.monitor_slug === 'ok-scheduler')?.check_in_id; - const failingCheckInId = checkIns.find(checkIn => checkIn.monitor_slug === 'failing-scheduler')?.check_in_id; + const okCheckInId = checkIns.find( + checkIn => checkIn.monitor_slug === 'crons-telemetry-ok-scheduler', + )?.check_in_id; + const failingCheckInId = checkIns.find( + checkIn => checkIn.monitor_slug === 'crons-telemetry-failing-scheduler', + )?.check_in_id; - expect(summary('ok-scheduler')).toEqual([ + expect(summary('crons-telemetry-ok-scheduler')).toEqual([ { status: 'in_progress', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, { status: 'ok', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, ]); - expect(summary('failing-scheduler')).toEqual([ + expect(summary('crons-telemetry-failing-scheduler')).toEqual([ { status: 'in_progress', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, { status: 'error', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, ]); diff --git a/packages/node/src/cron/bullmq.ts b/packages/node/src/cron/bullmq.ts index 4588f8229848..a45df39a5693 100644 --- a/packages/node/src/cron/bullmq.ts +++ b/packages/node/src/cron/bullmq.ts @@ -9,7 +9,7 @@ import { timestampInSeconds, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; -import { replaceCronNames } from './common'; +import { getCronMonitorConfig } from './common'; interface BullMQRepeatOptions { pattern?: unknown; @@ -36,17 +36,6 @@ export interface InstrumentBullMQWorkerOptions { excludeJobSchedulers?: Array; } -const CRON_PRESETS: Record = { - '@yearly': '0 0 1 1 *', - '@annually': '0 0 1 1 *', - '@monthly': '0 0 1 * *', - '@weekly': '0 0 * * 0', - '@daily': '0 0 * * *', - '@hourly': '0 * * * *', -}; - -const FULL_DAY_OF_WEEK_RANGES = new Set(['0-6', '0-7', '1-7']); - // Legacy repeatable jobs (`queue.add` with `repeat`) are keyed by a hash or `name:id:endDate:tz:pattern`. const LEGACY_REPEAT_KEY = /^[\da-f]{32,}$|^(?:[^:]*:){4}/; @@ -63,7 +52,7 @@ type InProgressCheckInWithId = InProgressCheckIn & { checkInId: string }; /** * Sends check-ins to Sentry for every job a BullMQ worker runs from a job scheduler. - * The job scheduler id is the monitor slug, and its `pattern` or `every` is sent as the monitor schedule. + * The monitor slug is `-`, and the scheduler's `pattern` or `every` is the monitor schedule. * Schedulers whose schedule Sentry can't represent, or that set `limit` or `endDate`, are not monitored. * * ```ts @@ -86,17 +75,18 @@ export function instrumentBullMQWorker unk // Sentry keeps the trace of the `in_progress` check-in. BullMQ emits `active` before the job's trace starts, // often inside the previous job's, so send it from the processor instead. const target = worker as unknown as { + name?: unknown; processFn?: (job: BullMQJob, ...args: unknown[]) => unknown; opts?: { settings?: { repeatStrategy?: unknown } }; }; - const processFn = target.processFn; - if (typeof processFn !== 'function') { + const { name: queueName, processFn } = target; + if (typeof processFn !== 'function' || typeof queueName !== 'string') { return worker; } // A custom repeat strategy decides what `pattern` means. const hasCustomRepeatStrategy = !!target.opts?.settings?.repeatStrategy; const monitorFor = (job: BullMQJob | undefined): Monitor | undefined => - getMonitor(job, options.excludeJobSchedulers, hasCustomRepeatStrategy); + getMonitor(job, queueName, options.excludeJobSchedulers, hasCustomRepeatStrategy); const startTimes = new WeakMap(); @@ -149,13 +139,16 @@ export function instrumentBullMQWorker unk function getMonitor( job: BullMQJob | undefined, + queueName: string, exclude: Array | undefined, hasCustomRepeatStrategy: boolean, ): Monitor | undefined { - const monitorSlug = getJobSchedulerId(job, exclude); - if (!job || !monitorSlug) { + const schedulerId = getJobSchedulerId(job, exclude); + if (!job || !schedulerId) { return undefined; } + // Scheduler ids are only unique within a queue. + const monitorSlug = `${queueName}-${schedulerId}`; const repeat = job.opts?.repeat; const config = hasCustomRepeatStrategy && repeat?.pattern != null ? undefined : getMonitorConfig(repeat); if (!config) { @@ -209,63 +202,9 @@ function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfi return { schedule: { type: 'interval', value: minutes, unit: 'minute' } }; } - const crontab = typeof repeat.pattern === 'string' ? toCrontab(repeat.pattern) : undefined; - // cron-parser 4 (BullMQ 5) prefers `utc` over `tz`, and uses the server's time zone without either. - const timezone = toSentryTimeZone( - repeat.utc === true ? 'UTC' : typeof repeat.tz === 'string' && repeat.tz ? repeat.tz : getLocalTimeZone(), - ); - return crontab && timezone ? { schedule: { type: 'crontab', value: crontab }, timezone } : undefined; -} - -/** - * Converts a cron-parser pattern to a crontab that Sentry reads the same way, if there is one. - */ -function toCrontab(pattern: string): string | undefined { - const preset = CRON_PRESETS[pattern.trim().toLowerCase()]; - if (preset) { - return preset; - } - - const fields = replaceCronNames(pattern).trim().split(/\s+/); - if (fields.length === 6 && /^0+$/.test(fields[0] as string)) { - fields.shift(); - } - if (fields.length !== 5 || fields.some(field => !/^[\d*,/\-L#]+$/.test(field))) { - return undefined; - } - - // With both day fields set, cron-parser runs on either, but Sentry needs both when one starts with `*`. - // cron-parser 4 also treats `1-31` as `*`. - const [, , dayOfMonth = '', , dayOfWeek = ''] = fields; - if ( - dayOfMonth !== '*' && - dayOfWeek !== '*' && - (dayOfMonth.startsWith('*') || - dayOfWeek.startsWith('*') || - dayOfMonth === '1-31' || - FULL_DAY_OF_WEEK_RANGES.has(dayOfWeek)) - ) { - return undefined; - } - - return fields.join(' '); -} - -function toSentryTimeZone(timezone: string | undefined): string | undefined { - if (!timezone || timezone === 'Etc/Unknown' || /^(?:utc|gmt)?[+-]/i.test(timezone)) { - return undefined; - } - try { - return new Intl.DateTimeFormat('en-US', { timeZone: timezone }).resolvedOptions().timeZone || undefined; - } catch { - return undefined; - } -} - -function getLocalTimeZone(): string | undefined { - try { - return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined; - } catch { + if (typeof repeat.pattern !== 'string') { return undefined; } + // BullMQ 5's cron-parser prefers `utc` over `tz`. + return getCronMonitorConfig(repeat.pattern, repeat.utc === true ? 'UTC' : repeat.tz, 'cron-parser'); } diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/node/test/cron/bullmq.test.ts index ca7de45ec26c..fa19b977c44e 100644 --- a/packages/node/test/cron/bullmq.test.ts +++ b/packages/node/test/cron/bullmq.test.ts @@ -37,6 +37,7 @@ describe('cron.instrumentBullMQWorker', () => { opts: object = {}, ): EventEmitter & { processFn: (job: object, ...args: unknown[]) => unknown; opts: object } { return Object.assign(new EventEmitter(), { + name: 'reports', processFn: vi.fn((_job: object, ..._args: unknown[]) => 'result'), opts, }); @@ -71,13 +72,13 @@ describe('cron.instrumentBullMQWorker', () => { const config = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; expect(captureCheckInSpy).toHaveBeenNthCalledWith( 1, - { monitorSlug: 'daily-report', status: 'in_progress', checkInId: expect.stringMatching(/^[\da-f]{32}$/) }, + { monitorSlug: 'reports-daily-report', status: 'in_progress', checkInId: expect.stringMatching(/^[\da-f]{32}$/) }, config, ); const checkInId = captureCheckInSpy.mock.calls[0]?.[0].checkInId; expect(captureCheckInSpy).toHaveBeenNthCalledWith( 2, - { monitorSlug: 'daily-report', status: 'ok', checkInId, duration: expect.any(Number) }, + { monitorSlug: 'reports-daily-report', status: 'ok', checkInId, duration: expect.any(Number) }, config, ); }); @@ -96,6 +97,7 @@ describe('cron.instrumentBullMQWorker', () => { ['0 0 * * 5L', '0 0 * * 5L'], ['0 0 * * 1#2', '0 0 * * 1#2'], ['0 0 1 * 1', '0 0 1 * 1'], + ['0 0 1 * 0-6', '0 0 1 * 0-6'], ['@daily', '0 0 * * *'], ])('converts pattern %j to crontab %j', (pattern, crontab) => { expect(sentConfig({ pattern, tz: 'UTC' })).toEqual({ @@ -111,7 +113,6 @@ describe('cron.instrumentBullMQWorker', () => { '0 0 */2 * 1', '0 0 1 * */2', '0 0 1-31 * 1', - '0 0 1 * 0-6', '0 0 * * * * 2026', ])('skips pattern %j', pattern => { expectSkipped({ pattern, tz: 'UTC' }); @@ -185,7 +186,7 @@ describe('cron.instrumentBullMQWorker', () => { expect(captureCheckInSpy).toHaveBeenCalledTimes(1); expect(captureCheckInSpy).toHaveBeenCalledWith( - { monitorSlug: 'my-scheduler', status: 'error', checkInId: expect.any(String) }, + { monitorSlug: 'reports-my-scheduler', status: 'error', checkInId: expect.any(String) }, { schedule: { type: 'interval', value: 1, unit: 'minute' } }, ); }); @@ -208,9 +209,9 @@ describe('cron.instrumentBullMQWorker', () => { const checkInId = checkIns[0].checkInId; // Without a duration, Sentry measures from the first attempt's `in_progress` check-in. expect(checkIns).toEqual([ - { monitorSlug: 'my-scheduler', status: 'in_progress', checkInId }, - { monitorSlug: 'my-scheduler', status: 'in_progress', checkInId }, - { monitorSlug: 'my-scheduler', status: 'ok', checkInId }, + { monitorSlug: 'reports-my-scheduler', status: 'in_progress', checkInId }, + { monitorSlug: 'reports-my-scheduler', status: 'in_progress', checkInId }, + { monitorSlug: 'reports-my-scheduler', status: 'ok', checkInId }, ]); }); @@ -228,7 +229,7 @@ describe('cron.instrumentBullMQWorker', () => { worker.emit('completed', schedulerJob('my-scheduler', { every: 60_000 }), 'result', 'active'); expect(captureCheckInSpy).toHaveBeenCalledWith( - { monitorSlug: 'my-scheduler', status: 'ok', checkInId: expect.any(String) }, + { monitorSlug: 'reports-my-scheduler', status: 'ok', checkInId: expect.any(String) }, { schedule: { type: 'interval', value: 1, unit: 'minute' } }, ); }); From bff29e638cd6a8b200430f21b9cfa061bbf69ea3 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 12:00:56 -0800 Subject: [PATCH 11/20] feat(node): Replace instrumentBullMQWorker with bullmqJobSchedulerIntegration Adding the integration is the opt-in, and it monitors every BullMQ Worker without a per-worker call. It lives in server-utils next to the other cron integrations and hooks the Worker constructor through the runtime diagnostics-channel injection, in CJS and ESM. BullMQ is only in the runtime instrumentation list. In the bundler list, the plugins would force it into bundles, where it can't find the files it starts sandboxed processors from. Co-Authored-By: Claude --- CHANGELOG.md | 11 +- .../node-suites/excludes.ts | 1 + .../suites/cron/bullmq/instrument-tracing.mjs | 10 + .../suites/cron/bullmq/instrument.mjs | 9 + .../suites/cron/bullmq/scenario-telemetry.mjs | 15 +- .../cron/bullmq/{scenario.ts => scenario.mjs} | 12 +- .../suites/cron/bullmq/test.ts | 215 +++++++++--------- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/elysia/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/cron/index.ts | 2 - packages/node/src/index.ts | 2 + packages/remix/src/server/index.ts | 1 + packages/server-utils/src/index.ts | 2 + .../src/integrations}/bullmq.ts | 120 ++++++---- .../server-utils/src/orchestrion/channels.ts | 2 + .../src/orchestrion/config/bullmq.ts | 20 ++ .../src/orchestrion/config/index.ts | 16 +- .../test/integrations}/bullmq.test.ts | 24 +- .../test/orchestrion/config.test.ts | 19 +- packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 25 files changed, 290 insertions(+), 199 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/instrument-tracing.mjs create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/instrument.mjs rename dev-packages/node-integration-tests/suites/cron/bullmq/{scenario.ts => scenario.mjs} (75%) rename packages/{node/src/cron => server-utils/src/integrations}/bullmq.ts (65%) create mode 100644 packages/server-utils/src/orchestrion/config/bullmq.ts rename packages/{node/test/cron => server-utils/test/integrations}/bullmq.test.ts (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 840cd8b3a9d1..75f9c9d91597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,16 @@ Work in this release was contributed by @kunal649. Thank you for your contributi `Sentry.cron.instrumentCron`, `instrumentNodeCron` and `instrumentNodeSchedule` read schedules the same way as `@SentryCron` and the cron integrations. Without a time zone, they send the server's time zone, where the scheduler runs the job, instead of letting Sentry assume UTC. For expressions Sentry would read differently than the scheduler, like per-second schedules, they send no monitor config, so existing monitors keep their schedule. They send names and nicknames like `MON` or `@daily` as numbers, and drop a fixed seconds field. `instrumentNodeSchedule` also sends `L` and `#`, where Sentry reads them the same way. -- **feat(node): Add Crons monitoring for BullMQ job schedulers ([#25472](https://github.com/getsentry/sentry-javascript/pull/25472))** +- **feat(node): Add `bullmqJobSchedulerIntegration` to monitor BullMQ job schedulers ([#25472](https://github.com/getsentry/sentry-javascript/pull/25472))** - Call `Sentry.cron.instrumentBullMQWorker(worker)` to send check-ins for every job the worker runs from a job scheduler, with `-` as the monitor slug and the scheduler's `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. + Add `Sentry.bullmqJobSchedulerIntegration()` to send check-ins for every job a BullMQ 5.16+ worker runs from a job scheduler, with `-` as the monitor slug and the scheduler's `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. + + ```js + Sentry.init({ + dsn: '__DSN__', + integrations: [Sentry.bullmqJobSchedulerIntegration({ excludeJobSchedulers: ['cache-warmup'] })], + }); + ``` ## 11.12.0 diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 60d47967bd65..8dc309fdad0d 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -66,6 +66,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/bullmq/test.ts', 'suites/cron/cron-integration/test.ts', 'suites/cron/node-cron/integration/test.ts', 'suites/cron/node-schedule-integration/test.ts', diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/instrument-tracing.mjs b/dev-packages/node-integration-tests/suites/cron/bullmq/instrument-tracing.mjs new file mode 100644 index 000000000000..ed997320d0b6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/instrument-tracing.mjs @@ -0,0 +1,10 @@ +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', + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.bullmqJobSchedulerIntegration()], +}); diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/instrument.mjs b/dev-packages/node-integration-tests/suites/cron/bullmq/instrument.mjs new file mode 100644 index 000000000000..686a33cb283b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/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.bullmqJobSchedulerIntegration({ excludeJobSchedulers: ['excluded-scheduler'] })], +}); diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs index 42ac3f479d31..a0a93ad2ebe9 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario-telemetry.mjs @@ -1,17 +1,9 @@ import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; import { Queue, Worker } from 'bullmq'; -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, -}); - const connection = { host: 'localhost', port: 6385 }; const telemetry = new Sentry.BullMQTelemetry(); -const instrumentBeforeRun = process.env.INSTRUMENT_BEFORE_RUN === 'true'; +const autorun = process.env.AUTORUN === 'true'; async function run() { const queue = new Queue('crons-telemetry', { connection, telemetry }); @@ -34,9 +26,8 @@ async function run() { throw new Error('Error in scheduled job'); } }, - { connection, telemetry, autorun: !instrumentBeforeRun }, + { connection, telemetry, autorun }, ); - Sentry.cron.instrumentBullMQWorker(worker); const finished = new Promise(resolve => { let count = 0; @@ -49,7 +40,7 @@ async function run() { worker.on('failed', done); }); - if (instrumentBeforeRun) { + if (!autorun) { void worker.run(); } diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs similarity index 75% rename from dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts rename to dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs index 5e70150a5bef..68a9c0b8c20b 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs @@ -1,16 +1,8 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; import { Queue, Worker } from 'bullmq'; -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - transport: loggingTransport, -}); - const connection = { host: 'localhost', port: 6385 }; -async function run(): Promise { +async function run() { const queue = new Queue('crons', { connection }); await queue.obliterate({ force: true }); @@ -23,9 +15,9 @@ async function run(): Promise { }, { connection }, ); - Sentry.cron.instrumentBullMQWorker(worker); await queue.add('plain', {}); + await queue.upsertJobScheduler('excluded-scheduler', { every: 300_000 }, { name: 'excluded' }); await queue.upsertJobScheduler('every-scheduler', { every: 300_000 }, { name: 'every' }); await queue.upsertJobScheduler( 'pattern-scheduler', diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts index 5b4afef82581..048319cfb1da 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts @@ -1,123 +1,130 @@ import type { Event, SerializedCheckIn } from '@sentry/core'; -import { afterAll, expect, test } from 'vitest'; -import { cleanupChildProcesses, createRunner, describeWithDockerCompose } from '../../../utils/runner'; +import { afterAll, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: [__dirname] }, () => { afterAll(() => { cleanupChildProcesses(); }); - test('sends check-ins with the job scheduler schedule', { timeout: 60_000 }, async () => { - const checkIns: SerializedCheckIn[] = []; - await createRunner(__dirname, 'scenario.ts') - .unordered() - .expectN(7, { check_in: checkIn => void checkIns.push(checkIn) }) - .start() - .completed(); + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('sends check-ins with the job scheduler schedule', { timeout: 60_000 }, async () => { + const checkIns: SerializedCheckIn[] = []; + await createRunner() + .unordered() + .expectN(7, { check_in: checkIn => void checkIns.push(checkIn) }) + .start() + .completed(); - const bySlug = (slug: string): SerializedCheckIn[] => checkIns.filter(checkIn => checkIn.monitor_slug === slug); - const everyCheckIns = bySlug('crons-every-scheduler'); - const patternCheckIns = bySlug('crons-pattern-scheduler'); - const retryCheckIns = bySlug('crons-retry-scheduler'); - const everyConfig = { schedule: { type: 'interval', value: 5, unit: 'minute' } }; - const patternConfig = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; - const retryConfig = { schedule: { type: 'interval', value: 10, unit: 'minute' } }; + const bySlug = (slug: string): SerializedCheckIn[] => checkIns.filter(checkIn => checkIn.monitor_slug === slug); + const everyCheckIns = bySlug('crons-every-scheduler'); + const patternCheckIns = bySlug('crons-pattern-scheduler'); + const retryCheckIns = bySlug('crons-retry-scheduler'); + const everyConfig = { schedule: { type: 'interval', value: 5, unit: 'minute' } }; + const patternConfig = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; + const retryConfig = { schedule: { type: 'interval', value: 10, unit: 'minute' } }; - expect(everyCheckIns).toEqual([ - expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: everyConfig }), - expect.objectContaining({ - status: 'ok', - check_in_id: everyCheckIns[0]?.check_in_id, - duration: expect.any(Number), - monitor_config: everyConfig, - }), - ]); - expect(patternCheckIns).toEqual([ - expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: patternConfig }), - expect.objectContaining({ - status: 'error', - check_in_id: patternCheckIns[0]?.check_in_id, - duration: expect.any(Number), - monitor_config: patternConfig, - }), - ]); - // The retry updates the first attempt's check-in, and Sentry measures its duration. - const retryCheckInId = retryCheckIns[0]?.check_in_id; - expect(retryCheckIns).toEqual([ - expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), - expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), - expect.objectContaining({ status: 'ok', check_in_id: retryCheckInId, monitor_config: retryConfig }), - ]); - expect(retryCheckIns[2]).not.toHaveProperty('duration'); + expect(everyCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: everyConfig }), + expect.objectContaining({ + status: 'ok', + check_in_id: everyCheckIns[0]?.check_in_id, + duration: expect.any(Number), + monitor_config: everyConfig, + }), + ]); + expect(patternCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: patternConfig }), + expect.objectContaining({ + status: 'error', + check_in_id: patternCheckIns[0]?.check_in_id, + duration: expect.any(Number), + monitor_config: patternConfig, + }), + ]); + // The retry updates the first attempt's check-in, and Sentry measures its duration. + const retryCheckInId = retryCheckIns[0]?.check_in_id; + expect(retryCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), + expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), + expect.objectContaining({ status: 'ok', check_in_id: retryCheckInId, monitor_config: retryConfig }), + ]); + expect(retryCheckIns[2]).not.toHaveProperty('duration'); + expect(bySlug('crons-excluded-scheduler')).toEqual([]); + }); }); - test.each([ - ['after the worker starts', 'false'], - ['before the worker runs', 'true'], - ])( - 'sends one check-in pair per run in the job trace with BullMQTelemetry, instrumented %s', - { timeout: 60_000 }, - async (_, instrumentBeforeRun) => { - const checkIns: SerializedCheckIn[] = []; - const errors: Event[] = []; - const runner = createRunner(__dirname, 'scenario-telemetry.mjs') - .withEnv({ INSTRUMENT_BEFORE_RUN: instrumentBeforeRun }) - .unordered() - .expectN(4, { check_in: checkIn => void checkIns.push(checkIn) }) - .expect({ event: event => void errors.push(event) }); - const spansByJob = ['ok-job', 'failing-job'].map(jobName => - runner.collectStreamedSpans(spans => - spans.some( - span => - span.is_segment && - span.name === 'process crons-telemetry' && - span.attributes['messaging.bullmq.job.name']?.value === jobName, + createEsmAndCjsTests(__dirname, 'scenario-telemetry.mjs', 'instrument-tracing.mjs', (createRunner, test) => { + test.each([ + ['with autorun', 'true'], + ['with a later run()', 'false'], + ])( + 'sends one check-in pair per run in the job trace with BullMQTelemetry, %s', + { timeout: 60_000 }, + async (_, autorun) => { + const checkIns: SerializedCheckIn[] = []; + const errors: Event[] = []; + const runner = createRunner() + .withEnv({ AUTORUN: autorun }) + .unordered() + .expectN(4, { check_in: checkIn => void checkIns.push(checkIn) }) + .expect({ event: event => void errors.push(event) }); + const spansByJob = ['ok-job', 'failing-job'].map(jobName => + runner.collectStreamedSpans(spans => + spans.some( + span => + span.is_segment && + span.name === 'process crons-telemetry' && + span.attributes['messaging.bullmq.job.name']?.value === jobName, + ), ), - ), - ); + ); - await runner.start().completed(); + await runner.start().completed(); - const [okSpans, failingSpans] = await Promise.all(spansByJob); - const okSegment = okSpans!.find(span => span.is_segment)!; - const failingSegment = failingSpans!.find(span => span.is_segment)!; + const [okSpans, failingSpans] = await Promise.all(spansByJob); + const okSegment = okSpans!.find(span => span.is_segment)!; + const failingSegment = failingSpans!.find(span => span.is_segment)!; - expect(okSegment.status).toBe('ok'); - expect(failingSegment.status).toBe('error'); - expect(okSpans!.some(span => span.name === 'work ok-job' && span.parent_span_id === okSegment.span_id)).toBe( - true, - ); - expect( - failingSpans!.some(span => span.name === 'work failing-job' && span.parent_span_id === failingSegment.span_id), - ).toBe(true); + expect(okSegment.status).toBe('ok'); + expect(failingSegment.status).toBe('error'); + expect(okSpans!.some(span => span.name === 'work ok-job' && span.parent_span_id === okSegment.span_id)).toBe( + true, + ); + expect( + failingSpans!.some( + span => span.name === 'work failing-job' && span.parent_span_id === failingSegment.span_id, + ), + ).toBe(true); - const summary = (slug: string): unknown => - checkIns - .filter(checkIn => checkIn.monitor_slug === slug) - .map(checkIn => ({ - status: checkIn.status, - check_in_id: checkIn.check_in_id, - trace_id: checkIn.contexts?.trace?.trace_id, - })); - const okCheckInId = checkIns.find( - checkIn => checkIn.monitor_slug === 'crons-telemetry-ok-scheduler', - )?.check_in_id; - const failingCheckInId = checkIns.find( - checkIn => checkIn.monitor_slug === 'crons-telemetry-failing-scheduler', - )?.check_in_id; + const summary = (slug: string): unknown => + checkIns + .filter(checkIn => checkIn.monitor_slug === slug) + .map(checkIn => ({ + status: checkIn.status, + check_in_id: checkIn.check_in_id, + trace_id: checkIn.contexts?.trace?.trace_id, + })); + const okCheckInId = checkIns.find( + checkIn => checkIn.monitor_slug === 'crons-telemetry-ok-scheduler', + )?.check_in_id; + const failingCheckInId = checkIns.find( + checkIn => checkIn.monitor_slug === 'crons-telemetry-failing-scheduler', + )?.check_in_id; - expect(summary('crons-telemetry-ok-scheduler')).toEqual([ - { status: 'in_progress', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, - { status: 'ok', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, - ]); - expect(summary('crons-telemetry-failing-scheduler')).toEqual([ - { status: 'in_progress', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, - { status: 'error', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, - ]); + expect(summary('crons-telemetry-ok-scheduler')).toEqual([ + { status: 'in_progress', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, + { status: 'ok', check_in_id: okCheckInId, trace_id: okSegment.trace_id }, + ]); + expect(summary('crons-telemetry-failing-scheduler')).toEqual([ + { status: 'in_progress', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, + { status: 'error', check_in_id: failingCheckInId, trace_id: failingSegment.trace_id }, + ]); - expect(errors).toHaveLength(1); - expect(errors[0]?.exception?.values?.[0]?.value).toBe('Error in scheduled job'); - expect(errors[0]?.contexts?.trace?.trace_id).toBe(failingSegment.trace_id); - }, - ); + expect(errors).toHaveLength(1); + expect(errors[0]?.exception?.values?.[0]?.value).toBe('Error in scheduled job'); + expect(errors[0]?.contexts?.trace?.trace_id).toBe(failingSegment.trace_id); + }, + ); + }); }); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index f407a5723a5b..bf7d28442a5a 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -49,6 +49,7 @@ export { fsIntegration, functionToStringIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index f7310775b554..f018b7ad0c62 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -119,6 +119,7 @@ export { firebaseIntegration, fsIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 678df401163e..ba5552efff67 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, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 4cf53f81d690..f4a4144f7ada 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -195,6 +195,7 @@ export { postgresIntegration, postgresJsIntegration, neonIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 107545f0cb43..21bfa01f1adc 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, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 107674b83b95..e7b011243467 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -119,6 +119,7 @@ export { fastifyIntegration, firebaseIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/node/src/cron/index.ts b/packages/node/src/cron/index.ts index 91edc0c7cdeb..890d88441ec1 100644 --- a/packages/node/src/cron/index.ts +++ b/packages/node/src/cron/index.ts @@ -1,11 +1,9 @@ -import { instrumentBullMQWorker } from './bullmq'; import { instrumentCron } from './cron'; import { instrumentNodeCron } from './node-cron'; import { instrumentNodeSchedule } from './node-schedule'; /** Methods to instrument cron libraries for Sentry check-ins */ export const cron = { - instrumentBullMQWorker, // oxlint-disable-next-line typescript/no-deprecated instrumentCron, instrumentNodeCron, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 5297b54a8c25..e198d796e49c 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -13,6 +13,8 @@ export { type ExpressIntegrationOptions, firebaseIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, + type BullMQJobSchedulerIntegrationOptions, cronJobIntegration, type CronJobIntegrationOptions, nodeCronIntegration, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index e034d9404d05..09b46d0a8da5 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -37,6 +37,7 @@ export { flush, functionToStringIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 3ab5b322c212..a6b87b1403be 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 { bullmqJobSchedulerIntegration } from './integrations/bullmq'; +export type { BullMQJobSchedulerIntegrationOptions } from './integrations/bullmq'; export { cronJobIntegration } from './integrations/cron'; export type { CronJobIntegrationOptions } from './integrations/cron'; export { isReadAlikeByNodeCron3, nodeCronIntegration } from './integrations/node-cron'; diff --git a/packages/node/src/cron/bullmq.ts b/packages/server-utils/src/integrations/bullmq.ts similarity index 65% rename from packages/node/src/cron/bullmq.ts rename to packages/server-utils/src/integrations/bullmq.ts index a45df39a5693..bbedd6c04690 100644 --- a/packages/node/src/cron/bullmq.ts +++ b/packages/server-utils/src/integrations/bullmq.ts @@ -1,15 +1,54 @@ import { createHash } from 'node:crypto'; +import type { InProgressCheckIn, IntegrationFn, MonitorConfig } from '@sentry/core'; import { captureCheckIn, debug, + defineIntegration, getClient, - type InProgressCheckIn, - type MonitorConfig, stringMatchesSomePattern, timestampInSeconds, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; -import { getCronMonitorConfig } from './common'; +import { CHANNELS } from '../orchestrion/channels'; +import { bullmqModuleNames } from '../orchestrion/config/bullmq'; +import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; +import { cronExpressionToMonitorConfig } from '../utils/cronMonitoring'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; + +const INTEGRATION_NAME = 'BullMQJobScheduler' as const; + +export interface BullMQJobSchedulerIntegrationOptions { + /** Job scheduler ids not to monitor. Strings must match the id exactly. */ + excludeJobSchedulers?: Array; +} + +const _bullmqJobSchedulerIntegration = ((options: BullMQJobSchedulerIntegrationOptions = {}) => { + return { + name: INTEGRATION_NAME, + setup(client) { + invokeOrchestrionInstrumentation(client, bullmqModuleNames, subscribe, [options], { + requiresTracingChannelBinding: false, + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Sends Crons check-ins for every job a BullMQ (5.16+) worker runs from a job scheduler, with the scheduler's `pattern` + * or `every` as the monitor config. The monitor slug is `-`. Schedulers with a schedule Crons + * can't express, or that set `limit` or `endDate`, are not monitored. + */ +export const bullmqJobSchedulerIntegration = defineIntegration(_bullmqJobSchedulerIntegration); + +function subscribe(options: BullMQJobSchedulerIntegrationOptions): void { + diagnosticsChannel.tracingChannel<{ self?: unknown }>(CHANNELS.BULLMQ_WORKER_CONSTRUCTOR).end.subscribe(message => { + try { + instrumentWorker((message as { self?: unknown }).self, options); + } catch (e) { + DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to set up the cron monitor:`, e); + } + }); +} interface BullMQRepeatOptions { pattern?: unknown; @@ -31,58 +70,42 @@ interface BullMQJob { opts?: { repeat?: BullMQRepeatOptions }; } -export interface InstrumentBullMQWorkerOptions { - /** Job scheduler ids to not monitor. Strings match the id exactly. */ - excludeJobSchedulers?: Array; +interface BullMQWorker { + name?: unknown; + processFn?: (job: BullMQJob, ...args: unknown[]) => unknown; + opts?: { settings?: { repeatStrategy?: unknown } }; + on?: (event: string, listener: (job?: BullMQJob) => void) => unknown; } // Legacy repeatable jobs (`queue.add` with `repeat`) are keyed by a hash or `name:id:endDate:tz:pattern`. const LEGACY_REPEAT_KEY = /^[\da-f]{32,}$|^(?:[^:]*:){4}/; -const instrumentedWorkers = new WeakSet(); - interface Monitor { monitorSlug: string; checkInId: string; config: MonitorConfig; } +const instrumentedWorkers = new WeakSet(); + // `captureCheckIn` keeps a given `checkInId` for `in_progress` check-ins too. type InProgressCheckInWithId = InProgressCheckIn & { checkInId: string }; -/** - * Sends check-ins to Sentry for every job a BullMQ worker runs from a job scheduler. - * The monitor slug is `-`, and the scheduler's `pattern` or `every` is the monitor schedule. - * Schedulers whose schedule Sentry can't represent, or that set `limit` or `endDate`, are not monitored. - * - * ```ts - * import * as Sentry from '@sentry/node'; - * import { Worker } from 'bullmq'; - * - * const worker = new Worker('reports', processor, { connection }); - * Sentry.cron.instrumentBullMQWorker(worker, { excludeJobSchedulers: ['cache-warmup'] }); - * ``` - */ -export function instrumentBullMQWorker unknown }>( - worker: T, - options: InstrumentBullMQWorkerOptions = {}, -): T { - if (instrumentedWorkers.has(worker)) { - return worker; - } - instrumentedWorkers.add(worker); - - // Sentry keeps the trace of the `in_progress` check-in. BullMQ emits `active` before the job's trace starts, - // often inside the previous job's, so send it from the processor instead. - const target = worker as unknown as { - name?: unknown; - processFn?: (job: BullMQJob, ...args: unknown[]) => unknown; - opts?: { settings?: { repeatStrategy?: unknown } }; - }; - const { name: queueName, processFn } = target; - if (typeof processFn !== 'function' || typeof queueName !== 'string') { - return worker; +/** Sends check-ins for the jobs a new BullMQ worker runs from job schedulers. */ +export function instrumentWorker(worker: unknown, options: BullMQJobSchedulerIntegrationOptions): void { + const target = worker as BullMQWorker | undefined; + const queueName = target?.name; + const processFn = target?.processFn; + if ( + !target || + instrumentedWorkers.has(target) || + typeof processFn !== 'function' || + typeof queueName !== 'string' || + typeof target.on !== 'function' + ) { + return; } + instrumentedWorkers.add(target); // A custom repeat strategy decides what `pattern` means. const hasCustomRepeatStrategy = !!target.opts?.settings?.repeatStrategy; const monitorFor = (job: BullMQJob | undefined): Monitor | undefined => @@ -90,6 +113,8 @@ export function instrumentBullMQWorker unk const startTimes = new WeakMap(); + // Sentry keeps the trace of the `in_progress` check-in. BullMQ emits `active` before the job's trace starts, + // often inside the previous job's, so send it from the processor instead. target.processFn = function (this: unknown, job: BullMQJob, ...args: unknown[]): unknown { try { const monitor = monitorFor(job); @@ -102,7 +127,7 @@ export function instrumentBullMQWorker unk } } } catch (e) { - DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); + DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to send check-in:`, e); } return processFn.call(this, job, ...args); }; @@ -127,14 +152,11 @@ export function instrumentBullMQWorker unk // config in case the check-in doesn't exist yet. captureCheckIn({ monitorSlug, status, checkInId, ...(duration !== undefined && { duration }) }, config); } catch (e) { - DEBUG_BUILD && debug.error('[BullMQ] Failed to send check-in:', e); + DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to send check-in:`, e); } }; - const on = worker.on as unknown as (event: string, listener: (job?: BullMQJob) => void) => unknown; - on.call(worker, 'completed', finish('ok')); - on.call(worker, 'failed', finish('error')); - - return worker; + target.on('completed', finish('ok')); + target.on('failed', finish('error')); } function getMonitor( @@ -152,7 +174,7 @@ function getMonitor( const repeat = job.opts?.repeat; const config = hasCustomRepeatStrategy && repeat?.pattern != null ? undefined : getMonitorConfig(repeat); if (!config) { - DEBUG_BUILD && debug.log(`[BullMQ] Not monitoring ${monitorSlug}: Sentry can't represent its schedule.`); + DEBUG_BUILD && debug.log(`[${INTEGRATION_NAME}] Not monitoring ${monitorSlug}: Crons can't express its schedule.`); return undefined; } return { monitorSlug, checkInId: getCheckInId(job), config }; @@ -206,5 +228,5 @@ function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfi return undefined; } // BullMQ 5's cron-parser prefers `utc` over `tz`. - return getCronMonitorConfig(repeat.pattern, repeat.utc === true ? 'UTC' : repeat.tz, 'cron-parser'); + return cronExpressionToMonitorConfig(repeat.pattern, repeat.utc === true ? 'UTC' : repeat.tz, 'cron-parser'); } diff --git a/packages/server-utils/src/orchestrion/channels.ts b/packages/server-utils/src/orchestrion/channels.ts index f593fefe0ebd..aed4a2b336e5 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 { bullmqChannels } from './config/bullmq'; import { cronChannels } from './config/cron'; import { dataloaderChannels } from './config/dataloader'; import { expressChannels } from './config/express'; @@ -61,6 +62,7 @@ export const CHANNELS = { ...amqplibChannels, ...anthropicAiChannels, ...awsSdkChannels, + ...bullmqChannels, ...cronChannels, ...dataloaderChannels, ...expressChannels, diff --git a/packages/server-utils/src/orchestrion/config/bullmq.ts b/packages/server-utils/src/orchestrion/config/bullmq.ts new file mode 100644 index 000000000000..01fa6a78b5ef --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/bullmq.ts @@ -0,0 +1,20 @@ +import type { InstrumentationConfig } from '../apmTypes'; +import { getModuleNames } from './module-names'; + +// Job schedulers were added in 5.16.0. The cron converter follows cron-parser 4, which BullMQ 5 uses, and BullMQ 6 +// uses cron-parser 5. +const module = { name: 'bullmq', versionRange: '>=5.16.0 <6' } as const; + +export const bullmqConfig = ['dist/cjs/classes/worker.js', 'dist/esm/classes/worker.js'].map( + (filePath): InstrumentationConfig => ({ + channelName: 'workerConstructor', + module: { ...module, filePath }, + functionQuery: { className: 'Worker' }, + }), +); + +export const bullmqModuleNames = getModuleNames(bullmqConfig); + +export const bullmqChannels = { + BULLMQ_WORKER_CONSTRUCTOR: 'orchestrion:bullmq:workerConstructor', +} as const; diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index 7636fbef80fc..a8370a3bd881 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -3,6 +3,7 @@ import type { InstrumentationConfig } from '../apmTypes'; import { uniq } from '@sentry/core'; import { awsSdkConfig } from './aws-sdk'; +import { bullmqConfig } from './bullmq'; import { amqplibConfig } from './amqplib'; import { anthropicAiConfig } from './anthropic-ai'; import { cronConfig } from './cron'; @@ -112,9 +113,9 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ]; /** - * The subset of {@link SENTRY_INSTRUMENTATIONS} the RUNTIME loader - * (`@sentry/server-runtime-injection`'s `register`, reached via `--import` or - * `Sentry.init()`) can actually apply. + * The configs the RUNTIME loader (`@sentry/server-runtime-injection`'s + * `register`, reached via `--import` or `Sentry.init()`) applies: the ones in + * {@link SENTRY_INSTRUMENTATIONS} it can apply, plus the runtime-only ones. * * Registration-only configs (native-channel libraries such as `ai` v7, * `ioredis`, `@redis/client`, `mysql2`, `mongoose`) carry the custom @@ -139,9 +140,12 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ * instantiate its integration factory. `@sentry/node` registers its * integrations statically, so it does not need it. */ -export const SENTRY_RUNTIME_INSTRUMENTATIONS: InstrumentationConfig[] = SENTRY_INSTRUMENTATIONS.filter( - config => !config.transform, -); +export const SENTRY_RUNTIME_INSTRUMENTATIONS: InstrumentationConfig[] = [ + ...SENTRY_INSTRUMENTATIONS.filter(config => !config.transform), + // Not in `SENTRY_INSTRUMENTATIONS`, so bundler plugins don't force BullMQ into the bundle. Bundled, it can't find + // the files it starts sandboxed processors from. + ...bullmqConfig, +]; /** * The unique set of package names instrumented by `SENTRY_INSTRUMENTATIONS` diff --git a/packages/node/test/cron/bullmq.test.ts b/packages/server-utils/test/integrations/bullmq.test.ts similarity index 94% rename from packages/node/test/cron/bullmq.test.ts rename to packages/server-utils/test/integrations/bullmq.test.ts index fa19b977c44e..c8d100d531cd 100644 --- a/packages/node/test/cron/bullmq.test.ts +++ b/packages/server-utils/test/integrations/bullmq.test.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events'; import * as SentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, type MockInstance, test, vi } from 'vitest'; -import { cron } from '../../src'; +import { instrumentWorker } from '../../src/integrations/bullmq'; function schedulerJob(id: string, repeat: Record, attempt: Record = {}): object { return { @@ -22,7 +22,7 @@ function finalFailure(job: object): object { return Object.assign(job, { finishedOn: Date.now() }); } -describe('cron.instrumentBullMQWorker', () => { +describe('bullmqJobSchedulerIntegration', () => { let captureCheckInSpy: MockInstance; beforeEach(() => { @@ -44,7 +44,7 @@ describe('cron.instrumentBullMQWorker', () => { } function runJob(job: object, outcome: 'completed' | 'failed' = 'completed', worker = createWorker()): void { - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); worker.processFn(job); if (outcome === 'failed') { worker.emit('failed', finalFailure(job), new Error('boom'), 'active'); @@ -61,7 +61,7 @@ describe('cron.instrumentBullMQWorker', () => { function expectSkipped(repeat: Record): void { runJob(schedulerJob('my-scheduler', repeat), 'failed'); const worker = createWorker(); - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); worker.emit('failed', finalFailure(schedulerJob('my-scheduler', repeat)), new Error('stalled'), 'active'); expect(captureCheckInSpy).not.toHaveBeenCalled(); } @@ -160,7 +160,7 @@ describe('cron.instrumentBullMQWorker', () => { ['a scheduler matching an excluded pattern', schedulerJob('tmp-cleanup', { every: 60_000 })], ])('ignores %s', (_, job) => { const worker = createWorker(); - cron.instrumentBullMQWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }); + instrumentWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }); worker.processFn(job); worker.emit('failed', finalFailure(job)); @@ -170,7 +170,7 @@ describe('cron.instrumentBullMQWorker', () => { test('calls the processor with the worker and its arguments', () => { const worker = createWorker(); const processor = worker.processFn; - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); const job = schedulerJob('my-scheduler', { every: 60_000 }); expect(worker.processFn.call(worker, job, 'token')).toBe('result'); @@ -180,7 +180,7 @@ describe('cron.instrumentBullMQWorker', () => { test('sends an error check-in for a job that fails without running the processor', () => { const worker = createWorker(); - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); const job = finalFailure(schedulerJob('my-scheduler', { every: 60_000 }, { attemptsStarted: 3 })); worker.emit('failed', job, new Error('job started more than allowable limit'), 'active'); @@ -194,13 +194,13 @@ describe('cron.instrumentBullMQWorker', () => { test('keeps one check-in open across retries, on any worker', () => { const repeat = { every: 60_000 }; const firstWorker = createWorker(); - cron.instrumentBullMQWorker(firstWorker); + instrumentWorker(firstWorker, {}); const firstAttempt = schedulerJob('my-scheduler', repeat); firstWorker.processFn(firstAttempt); firstWorker.emit('failed', Object.assign(firstAttempt, { attemptsMade: 1 }), new Error('boom'), 'active'); const secondWorker = createWorker(); - cron.instrumentBullMQWorker(secondWorker); + instrumentWorker(secondWorker, {}); const retry = schedulerJob('my-scheduler', repeat, { attemptsStarted: 2, attemptsMade: 1 }); secondWorker.processFn(retry); secondWorker.emit('completed', retry, 'result', 'active'); @@ -225,7 +225,7 @@ describe('cron.instrumentBullMQWorker', () => { test('sends an ok check-in for a job that completes without running the processor', () => { const worker = createWorker(); - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); worker.emit('completed', schedulerJob('my-scheduler', { every: 60_000 }), 'result', 'active'); expect(captureCheckInSpy).toHaveBeenCalledWith( @@ -236,7 +236,7 @@ describe('cron.instrumentBullMQWorker', () => { test('skips patterns with a custom repeat strategy', () => { const worker = createWorker({ settings: { repeatStrategy: () => Date.now() } }); - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); for (const repeat of [{ pattern: '0 * * * *', tz: 'UTC' }, { every: 60_000 }]) { const job = schedulerJob('my-scheduler', repeat); worker.processFn(job); @@ -251,7 +251,7 @@ describe('cron.instrumentBullMQWorker', () => { test('instruments a worker only once', () => { const worker = createWorker(); - cron.instrumentBullMQWorker(worker); + instrumentWorker(worker, {}); runJob(schedulerJob('my-scheduler', { every: 60_000 }), 'completed', worker); expect(captureCheckInSpy).toHaveBeenCalledTimes(2); diff --git a/packages/server-utils/test/orchestrion/config.test.ts b/packages/server-utils/test/orchestrion/config.test.ts index 0385405c1573..7a350838c030 100644 --- a/packages/server-utils/test/orchestrion/config.test.ts +++ b/packages/server-utils/test/orchestrion/config.test.ts @@ -8,6 +8,7 @@ import { SENTRY_RUNTIME_INSTRUMENTATIONS, withoutInstrumentedExternals, } from '../../src/orchestrion/config'; +import { bullmqConfig } from '../../src/orchestrion/config/bullmq'; import { CHANNEL_INTEGRATION_DEFINITIONS } from '../../src/orchestrion/config/channel-integration-definitions'; import { MODULE_REGISTRATION_TRANSFORM } from '../../src/orchestrion/config/registration-only'; @@ -79,6 +80,18 @@ describe('orchestrion config — Flue', () => { }); }); +describe('orchestrion config — BullMQ', () => { + it('transforms bullmq at runtime', () => { + expect(SENTRY_RUNTIME_INSTRUMENTATIONS.map(i => i.module.name)).toContain('bullmq'); + }); + + // Bundled, BullMQ can't find the files it starts sandboxed processors from. + it('is not force-bundled', () => { + expect(SENTRY_INSTRUMENTATIONS.map(i => i.module.name)).not.toContain('bullmq'); + expect(INSTRUMENTED_MODULE_NAMES).not.toContain('bullmq'); + }); +}); + describe('orchestrion config — custom instrumentations', () => { const customInstrumentation = { module: { name: 'my-lib' } } as InstrumentationConfig; @@ -118,11 +131,13 @@ describe('orchestrion config — SENTRY_RUNTIME_INSTRUMENTATIONS', () => { it('keeps every transform-less config, dropping only the ones with a custom transform', () => { // Nothing the runtime can apply is lost: each transform-less config from the full - // set survives (by reference), and the runtime set adds nothing extra. + // set survives (by reference), and the runtime set only adds the runtime-only BullMQ configs. for (const config of SENTRY_INSTRUMENTATIONS.filter(c => !c.transform)) { expect(SENTRY_RUNTIME_INSTRUMENTATIONS).toContain(config); } - expect(SENTRY_RUNTIME_INSTRUMENTATIONS).toHaveLength(SENTRY_INSTRUMENTATIONS.filter(c => !c.transform).length); + expect(SENTRY_RUNTIME_INSTRUMENTATIONS).toHaveLength( + SENTRY_INSTRUMENTATIONS.filter(c => !c.transform).length + bullmqConfig.length, + ); }); it('excludes only the native-channel modules, and only via their registration-only config', () => { diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 39c5b31b1d38..3a37be1816e2 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -40,6 +40,7 @@ export { flush, functionToStringIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 711a9b4ae3bf..2bc7fc8446e6 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -38,6 +38,7 @@ export { flush, functionToStringIntegration, genericPoolIntegration, + bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, From d09ba09a7c4626adf829fb661633befa53f1f2da Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 12:10:00 -0800 Subject: [PATCH 12/20] test(bun): Skip the BullMQ cron suite in the Bun build project Co-Authored-By: Claude --- dev-packages/bun-integration-tests/node-suites/excludes.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 8dc309fdad0d..37246f86aad5 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -226,6 +226,9 @@ const BUN_BUILD_EAGER_SUBSCRIPTION = ['suites/tracing/orchestrion-lazy-registrat // `http.server.request.start`. const BUN_BUILD_NO_HTTP_SERVER_INTEGRATION = ['suites/express/multiple-init/test.ts']; +// The bundler plugins don't instrument BullMQ, since it can't start sandboxed processors when bundled. +const BUN_BUILD_RUNTIME_ONLY_INSTRUMENTATION = ['suites/cron/bullmq/test.ts']; + // `@mistralai/mistralai` v3 statically imports its optional peer `@livekit/rtc-node`, so Bun cannot // bundle the scenarios. `mistral-v2` still covers Mistral here. const BUN_BUILD_UNBUNDLEABLE_DEPENDENCY = ['suites/tracing/mistral/test.ts']; @@ -269,6 +272,7 @@ export const BUN_BUILD_EXCLUDE = [ 'suites/tracing/tedious/test.ts', ...BUN_BUILD_EAGER_SUBSCRIPTION, ...BUN_BUILD_NO_HTTP_SERVER_INTEGRATION, + ...BUN_BUILD_RUNTIME_ONLY_INSTRUMENTATION, ...BUN_BUILD_UNBUNDLEABLE_DEPENDENCY, ...BUN_BUILD_NOT_TRIAGED, ]; From 53aad8f467fa4d69d7d8654ce67ad95d7e400203 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 12:45:23 -0800 Subject: [PATCH 13/20] feat(server-utils): Keep BullMQ external in getInstrumentedModuleNames Frameworks pass this list to their keep-external option, so BullMQ loads through Node's loader, where the runtime hook instruments it. Co-Authored-By: Claude --- packages/server-utils/src/orchestrion/config/index.ts | 2 +- packages/server-utils/test/orchestrion/config.test.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index a8370a3bd881..4144a38d017d 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -182,7 +182,7 @@ export const INSTRUMENTED_MODULE_NAMES: string[] = instrumentedModuleNames(); * additions — those force a helper package to be *bundled*, the opposite of keeping it external. */ export function getInstrumentedModuleNames(): string[] { - return uniq(SENTRY_INSTRUMENTATIONS.map(instrumentation => instrumentation.module.name)); + return uniq([...SENTRY_INSTRUMENTATIONS, ...bullmqConfig].map(instrumentation => instrumentation.module.name)); } /** diff --git a/packages/server-utils/test/orchestrion/config.test.ts b/packages/server-utils/test/orchestrion/config.test.ts index 7a350838c030..fd02b838c2d3 100644 --- a/packages/server-utils/test/orchestrion/config.test.ts +++ b/packages/server-utils/test/orchestrion/config.test.ts @@ -46,7 +46,7 @@ describe('getInstrumentedModuleNames', () => { expect(getInstrumentedModuleNames()).not.toContain('@remix-run/node'); expect(INSTRUMENTED_MODULE_NAMES).toContain('@remix-run/node'); expect(new Set(getInstrumentedModuleNames())).toEqual( - new Set(SENTRY_INSTRUMENTATIONS.map(instrumentation => instrumentation.module.name)), + new Set([...SENTRY_INSTRUMENTATIONS.map(instrumentation => instrumentation.module.name), 'bullmq']), ); }); }); @@ -90,6 +90,10 @@ describe('orchestrion config — BullMQ', () => { expect(SENTRY_INSTRUMENTATIONS.map(i => i.module.name)).not.toContain('bullmq'); expect(INSTRUMENTED_MODULE_NAMES).not.toContain('bullmq'); }); + + it('is kept external, so the runtime loader can transform it', () => { + expect(getInstrumentedModuleNames()).toContain('bullmq'); + }); }); describe('orchestrion config — custom instrumentations', () => { From ef51a40b73e12c20e09095f98e003ddee7235c1f Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 15:06:10 -0800 Subject: [PATCH 14/20] fix(bun): Don't export bullmqJobSchedulerIntegration `bun run` can't inject the worker channel, and the bundler plugins don't instrument BullMQ, so the integration would never send a check-in on Bun. Co-Authored-By: Claude Opus 5.5 --- .../bun-integration-tests/node-suites/excludes.ts | 9 +++------ .../node-exports-test-app/scripts/consistentExports.ts | 1 + packages/bun/src/index.ts | 1 - packages/elysia/src/index.ts | 1 - 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 37246f86aad5..5b68f898fce9 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -4,13 +4,15 @@ // `supports({ bunMin })`. Neither is listed here. // Node-only features: ANR and native thread watchdogs, child processes, the AWS Lambda Node runtime, -// `node:sqlite`, which `flue` needs, and the Vercel keep-alive, which needs `http.server.response.finish`. +// `node:sqlite`, which `flue` needs, the Vercel keep-alive, which needs `http.server.response.finish`, and +// `bullmqJobSchedulerIntegration`, which `bun run` can't inject and the bundler plugins don't instrument. const NODE_ONLY = [ 'suites/anr/test.ts', 'suites/aws-lambda-web/**', 'suites/aws-serverless/**', 'suites/breadcrumbs/**', 'suites/child-process/test.ts', + 'suites/cron/bullmq/test.ts', 'suites/thread-blocked-native/test.ts', 'suites/tracing/flue/test.ts', 'suites/vercel/keep-alive/test.ts', @@ -66,7 +68,6 @@ 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/bullmq/test.ts', 'suites/cron/cron-integration/test.ts', 'suites/cron/node-cron/integration/test.ts', 'suites/cron/node-schedule-integration/test.ts', @@ -226,9 +227,6 @@ const BUN_BUILD_EAGER_SUBSCRIPTION = ['suites/tracing/orchestrion-lazy-registrat // `http.server.request.start`. const BUN_BUILD_NO_HTTP_SERVER_INTEGRATION = ['suites/express/multiple-init/test.ts']; -// The bundler plugins don't instrument BullMQ, since it can't start sandboxed processors when bundled. -const BUN_BUILD_RUNTIME_ONLY_INSTRUMENTATION = ['suites/cron/bullmq/test.ts']; - // `@mistralai/mistralai` v3 statically imports its optional peer `@livekit/rtc-node`, so Bun cannot // bundle the scenarios. `mistral-v2` still covers Mistral here. const BUN_BUILD_UNBUNDLEABLE_DEPENDENCY = ['suites/tracing/mistral/test.ts']; @@ -272,7 +270,6 @@ export const BUN_BUILD_EXCLUDE = [ 'suites/tracing/tedious/test.ts', ...BUN_BUILD_EAGER_SUBSCRIPTION, ...BUN_BUILD_NO_HTTP_SERVER_INTEGRATION, - ...BUN_BUILD_RUNTIME_ONLY_INSTRUMENTATION, ...BUN_BUILD_UNBUNDLEABLE_DEPENDENCY, ...BUN_BUILD_NOT_TRIAGED, ]; diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 3c880974fe0b..430ba9243afa 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -59,6 +59,7 @@ const DEPENDENTS: Dependent[] = [ 'childProcessIntegration', 'workerThreadsIntegration', 'pinoIntegration', + 'bullmqJobSchedulerIntegration', // Bun will get its own runtime metrics integration 'nodeRuntimeMetricsIntegration', 'NodeRuntimeMetricsOptions', diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index ba5552efff67..678df401163e 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -143,7 +143,6 @@ export { // oxlint-disable-next-line typescript/no-deprecated setupKoaErrorHandler, genericPoolIntegration, - bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 21bfa01f1adc..107545f0cb43 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -117,7 +117,6 @@ export { // oxlint-disable-next-line typescript/no-deprecated setupKoaErrorHandler, genericPoolIntegration, - bullmqJobSchedulerIntegration, cronJobIntegration, nodeCronIntegration, nodeScheduleIntegration, From 48c5da754f56fc310615153313d05753c36adffb Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 15:06:12 -0800 Subject: [PATCH 15/20] feat(server-utils): Support BullMQ 6 and fix BullMQ schedule edge cases BullMQ 6 is npm latest and uses cron-parser 5, which reads the converted expressions like cron-parser 4 (fuzzed against cronsim) but ignores `utc` and `nthDayOfWeek`. BullMQ 5 reads `nthDayOfWeek` like `#n`, so it's sent that way when that's exact and skipped otherwise. Legacy repeatable jobs are told apart from job schedulers by `repeat.key` or by their key matching the name and repeat options, so scheduler ids that look like hashes or have colons are monitored. The reason a scheduler isn't monitored is logged once. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- .../suites/cron/bullmq/processor.cjs | 1 + .../suites/cron/bullmq/scenario.mjs | 35 +++- .../suites/cron/bullmq/test.ts | 126 +++++++++----- .../server-utils/src/integrations/bullmq.ts | 161 +++++++++++++----- .../src/orchestrion/config/bullmq.ts | 22 +-- .../test/integrations/bullmq.test.ts | 115 +++++++++++-- 7 files changed, 342 insertions(+), 120 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/cron/bullmq/processor.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a1026601f1e..699dd68a000a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Work in this release was contributed by @kunal649. Thank you for your contributi - **feat(node): Add `bullmqJobSchedulerIntegration` to monitor BullMQ job schedulers ([#25472](https://github.com/getsentry/sentry-javascript/pull/25472))** - Add `Sentry.bullmqJobSchedulerIntegration()` to send check-ins for every job a BullMQ 5.16+ worker runs from a job scheduler, with `-` as the monitor slug and the scheduler's `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. + Add `Sentry.bullmqJobSchedulerIntegration()` to send check-ins for every job a BullMQ worker (5.16.0 to 6.x) runs from a job scheduler, with `-` as the monitor slug and the scheduler's `pattern` or `every` as the schedule. Each monitored scheduler creates a monitor in Sentry, and monitors are billed. Use `excludeJobSchedulers` to skip schedulers by id. A retried job keeps one check-in. ```js Sentry.init({ diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/processor.cjs b/dev-packages/node-integration-tests/suites/cron/bullmq/processor.cjs new file mode 100644 index 000000000000..81aff321c0e6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/processor.cjs @@ -0,0 +1 @@ +module.exports = async () => {}; diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs index 68a9c0b8c20b..029d5273e16e 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs @@ -1,10 +1,13 @@ +import { dirname, join } from 'node:path'; import { Queue, Worker } from 'bullmq'; const connection = { host: 'localhost', port: 6385 }; async function run() { const queue = new Queue('crons', { connection }); + const sandboxedQueue = new Queue('crons-sandboxed', { connection }); await queue.obliterate({ force: true }); + await sandboxedQueue.obliterate({ force: true }); const worker = new Worker( 'crons', @@ -15,8 +18,27 @@ async function run() { }, { connection }, ); + const sandboxedWorker = new Worker('crons-sandboxed', join(dirname(process.argv[1]), 'processor.cjs'), { + connection, + }); + + // Runs delayed scheduler jobs now. The loop below and the `completed` listener can race to promote the same job. + const promote = job => job?.promote().catch(() => {}); + + // Runs the iteration the worker created after the first run, instead of waiting 5 minutes. + let promoted = false; + worker.on('completed', async job => { + if (job.name !== 'every' || promoted) { + return; + } + promoted = true; + const delayed = await queue.getDelayed(); + await promote(delayed.find(next => next.repeatJobKey === 'every-scheduler')); + }); await queue.add('plain', {}); + await queue.add('legacy', {}, { repeat: { every: 300_000, immediately: true } }); + await queue.add('legacy-key', {}, { repeat: { every: 300_000, key: 'custom-key', immediately: true } }); await queue.upsertJobScheduler('excluded-scheduler', { every: 300_000 }, { name: 'excluded' }); await queue.upsertJobScheduler('every-scheduler', { every: 300_000 }, { name: 'every' }); await queue.upsertJobScheduler( @@ -25,13 +47,24 @@ async function run() { { name: 'failing' }, ); await queue.upsertJobScheduler('retry-scheduler', { every: 600_000 }, { name: 'flaky', opts: { attempts: 2 } }); + await sandboxedQueue.upsertJobScheduler('sandboxed-scheduler', { every: 300_000 }, { name: 'sandboxed' }); + + // BullMQ 5.16 starts an `every` scheduler at its next full interval, so run the first jobs now. + for (const job of [...(await queue.getDelayed()), ...(await sandboxedQueue.getDelayed())]) { + if (job.opts.repeat?.count === 1) { + await promote(job); + } + } setTimeout(async () => { await worker.close(); + await sandboxedWorker.close(); await queue.obliterate({ force: true }); + await sandboxedQueue.obliterate({ force: true }); await queue.close(); + await sandboxedQueue.close(); process.exit(); - }, 5_000); + }, 8_000); } void run(); diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts index 048319cfb1da..bdc1b101964c 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/test.ts @@ -1,5 +1,5 @@ import type { Event, SerializedCheckIn } from '@sentry/core'; -import { afterAll, expect } from 'vitest'; +import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: [__dirname] }, () => { @@ -7,51 +7,89 @@ describeWithDockerCompose('bullmq job scheduler check-ins', { workingDirectory: cleanupChildProcesses(); }); - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { - test('sends check-ins with the job scheduler schedule', { timeout: 60_000 }, async () => { - const checkIns: SerializedCheckIn[] = []; - await createRunner() - .unordered() - .expectN(7, { check_in: checkIn => void checkIns.push(checkIn) }) - .start() - .completed(); + // The workspace has the latest BullMQ 5. Also run the oldest version with job schedulers and BullMQ 6. + describe.each([ + ['BullMQ 5', undefined], + ['BullMQ 5.16.0', { bullmq: '5.16.0' }], + ['BullMQ 6', { bullmq: '6.7.5' }], + ])('%s', (_, additionalDependencies) => { + createEsmAndCjsTests( + __dirname, + 'scenario.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('sends check-ins with the job scheduler schedule', { timeout: 90_000 }, async () => { + const checkIns: SerializedCheckIn[] = []; + await createRunner() + .unordered() + .expectN(11, { check_in: checkIn => void checkIns.push(checkIn) }) + .start() + .completed(); - const bySlug = (slug: string): SerializedCheckIn[] => checkIns.filter(checkIn => checkIn.monitor_slug === slug); - const everyCheckIns = bySlug('crons-every-scheduler'); - const patternCheckIns = bySlug('crons-pattern-scheduler'); - const retryCheckIns = bySlug('crons-retry-scheduler'); - const everyConfig = { schedule: { type: 'interval', value: 5, unit: 'minute' } }; - const patternConfig = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; - const retryConfig = { schedule: { type: 'interval', value: 10, unit: 'minute' } }; + const bySlug = (slug: string): SerializedCheckIn[] => + checkIns.filter(checkIn => checkIn.monitor_slug === slug); + const everyCheckIns = bySlug('crons-every-scheduler'); + const patternCheckIns = bySlug('crons-pattern-scheduler'); + const retryCheckIns = bySlug('crons-retry-scheduler'); + const sandboxedCheckIns = bySlug('crons-sandboxed-sandboxed-scheduler'); + const everyConfig = { schedule: { type: 'interval', value: 5, unit: 'minute' } }; + const patternConfig = { schedule: { type: 'crontab', value: '0 9 * * 1-5' }, timezone: 'Europe/Vienna' }; + const retryConfig = { schedule: { type: 'interval', value: 10, unit: 'minute' } }; - expect(everyCheckIns).toEqual([ - expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: everyConfig }), - expect.objectContaining({ - status: 'ok', - check_in_id: everyCheckIns[0]?.check_in_id, - duration: expect.any(Number), - monitor_config: everyConfig, - }), - ]); - expect(patternCheckIns).toEqual([ - expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: patternConfig }), - expect.objectContaining({ - status: 'error', - check_in_id: patternCheckIns[0]?.check_in_id, - duration: expect.any(Number), - monitor_config: patternConfig, - }), - ]); - // The retry updates the first attempt's check-in, and Sentry measures its duration. - const retryCheckInId = retryCheckIns[0]?.check_in_id; - expect(retryCheckIns).toEqual([ - expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), - expect.objectContaining({ status: 'in_progress', check_in_id: retryCheckInId, monitor_config: retryConfig }), - expect.objectContaining({ status: 'ok', check_in_id: retryCheckInId, monitor_config: retryConfig }), - ]); - expect(retryCheckIns[2]).not.toHaveProperty('duration'); - expect(bySlug('crons-excluded-scheduler')).toEqual([]); - }); + expect(new Set(checkIns.map(checkIn => checkIn.monitor_slug))).toEqual( + new Set([ + 'crons-every-scheduler', + 'crons-pattern-scheduler', + 'crons-retry-scheduler', + 'crons-sandboxed-sandboxed-scheduler', + ]), + ); + // The second run is the iteration the worker created after the first. + expect(everyCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: everyConfig }), + expect.objectContaining({ + status: 'ok', + check_in_id: everyCheckIns[0]?.check_in_id, + duration: expect.any(Number), + monitor_config: everyConfig, + }), + expect.objectContaining({ status: 'in_progress', monitor_config: everyConfig }), + expect.objectContaining({ status: 'ok', check_in_id: everyCheckIns[2]?.check_in_id }), + ]); + expect(everyCheckIns[2]?.check_in_id).not.toBe(everyCheckIns[0]?.check_in_id); + expect(patternCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', release: '1.0', monitor_config: patternConfig }), + expect.objectContaining({ + status: 'error', + check_in_id: patternCheckIns[0]?.check_in_id, + duration: expect.any(Number), + monitor_config: patternConfig, + }), + ]); + // The retry updates the first attempt's check-in, and Sentry measures its duration. + const retryCheckInId = retryCheckIns[0]?.check_in_id; + expect(retryCheckIns).toEqual([ + expect.objectContaining({ + status: 'in_progress', + check_in_id: retryCheckInId, + monitor_config: retryConfig, + }), + expect.objectContaining({ + status: 'in_progress', + check_in_id: retryCheckInId, + monitor_config: retryConfig, + }), + expect.objectContaining({ status: 'ok', check_in_id: retryCheckInId, monitor_config: retryConfig }), + ]); + expect(retryCheckIns[2]).not.toHaveProperty('duration'); + expect(sandboxedCheckIns).toEqual([ + expect.objectContaining({ status: 'in_progress', monitor_config: everyConfig }), + expect.objectContaining({ status: 'ok', check_in_id: sandboxedCheckIns[0]?.check_in_id }), + ]); + }); + }, + { additionalDependencies, copyPaths: ['processor.cjs'] }, + ); }); createEsmAndCjsTests(__dirname, 'scenario-telemetry.mjs', 'instrument-tracing.mjs', (createRunner, test) => { diff --git a/packages/server-utils/src/integrations/bullmq.ts b/packages/server-utils/src/integrations/bullmq.ts index bbedd6c04690..8b3cb1676c67 100644 --- a/packages/server-utils/src/integrations/bullmq.ts +++ b/packages/server-utils/src/integrations/bullmq.ts @@ -34,20 +34,27 @@ const _bullmqJobSchedulerIntegration = ((options: BullMQJobSchedulerIntegrationO }) satisfies IntegrationFn; /** - * Sends Crons check-ins for every job a BullMQ (5.16+) worker runs from a job scheduler, with the scheduler's `pattern` - * or `every` as the monitor config. The monitor slug is `-`. Schedulers with a schedule Crons - * can't express, or that set `limit` or `endDate`, are not monitored. + * Sends Crons check-ins for the jobs a BullMQ 5.16 to 6.x worker runs from job schedulers, with `-` + * as the monitor slug. Schedulers that end or whose schedule Crons can't express are not monitored. */ export const bullmqJobSchedulerIntegration = defineIntegration(_bullmqJobSchedulerIntegration); +type BullMQMajor = 5 | 6; + function subscribe(options: BullMQJobSchedulerIntegrationOptions): void { - diagnosticsChannel.tracingChannel<{ self?: unknown }>(CHANNELS.BULLMQ_WORKER_CONSTRUCTOR).end.subscribe(message => { - try { - instrumentWorker((message as { self?: unknown }).self, options); - } catch (e) { - DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to set up the cron monitor:`, e); - } - }); + const channels = [ + [CHANNELS.BULLMQ_WORKER_CONSTRUCTOR, 5], + [CHANNELS.BULLMQ_6_WORKER_CONSTRUCTOR, 6], + ] as const; + for (const [channel, major] of channels) { + diagnosticsChannel.tracingChannel<{ self?: unknown }>(channel).end.subscribe(message => { + try { + instrumentWorker((message as { self?: unknown }).self, options, major); + } catch (e) { + DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to set up the cron monitor:`, e); + } + }); + } } interface BullMQRepeatOptions { @@ -55,12 +62,16 @@ interface BullMQRepeatOptions { every?: unknown; tz?: unknown; utc?: unknown; + nthDayOfWeek?: unknown; limit?: unknown; endDate?: unknown; + key?: unknown; + jobId?: unknown; } interface BullMQJob { id?: unknown; + name?: unknown; queueQualifiedName?: unknown; timestamp?: unknown; repeatJobKey?: unknown; @@ -73,26 +84,35 @@ interface BullMQJob { interface BullMQWorker { name?: unknown; processFn?: (job: BullMQJob, ...args: unknown[]) => unknown; - opts?: { settings?: { repeatStrategy?: unknown } }; + opts?: { settings?: { repeatStrategy?: unknown; repeatKeyHashAlgorithm?: unknown } }; on?: (event: string, listener: (job?: BullMQJob) => void) => unknown; } -// Legacy repeatable jobs (`queue.add` with `repeat`) are keyed by a hash or `name:id:endDate:tz:pattern`. -const LEGACY_REPEAT_KEY = /^[\da-f]{32,}$|^(?:[^:]*:){4}/; - interface Monitor { monitorSlug: string; checkInId: string; config: MonitorConfig; } +interface WorkerSettings { + major: BullMQMajor; + hasCustomRepeatStrategy: boolean; + repeatKeyHashAlgorithm: string; + exclude: Array | undefined; +} + const instrumentedWorkers = new WeakSet(); +const loggedSchedulers = new Set(); // `captureCheckIn` keeps a given `checkInId` for `in_progress` check-ins too. type InProgressCheckInWithId = InProgressCheckIn & { checkInId: string }; /** Sends check-ins for the jobs a new BullMQ worker runs from job schedulers. */ -export function instrumentWorker(worker: unknown, options: BullMQJobSchedulerIntegrationOptions): void { +export function instrumentWorker( + worker: unknown, + options: BullMQJobSchedulerIntegrationOptions, + major: BullMQMajor, +): void { const target = worker as BullMQWorker | undefined; const queueName = target?.name; const processFn = target?.processFn; @@ -106,10 +126,15 @@ export function instrumentWorker(worker: unknown, options: BullMQJobSchedulerInt return; } instrumentedWorkers.add(target); - // A custom repeat strategy decides what `pattern` means. - const hasCustomRepeatStrategy = !!target.opts?.settings?.repeatStrategy; - const monitorFor = (job: BullMQJob | undefined): Monitor | undefined => - getMonitor(job, queueName, options.excludeJobSchedulers, hasCustomRepeatStrategy); + const settings = target.opts?.settings; + const workerSettings: WorkerSettings = { + major, + hasCustomRepeatStrategy: !!settings?.repeatStrategy, + repeatKeyHashAlgorithm: + typeof settings?.repeatKeyHashAlgorithm === 'string' ? settings.repeatKeyHashAlgorithm : 'md5', + exclude: options.excludeJobSchedulers, + }; + const monitorFor = (job: BullMQJob | undefined): Monitor | undefined => getMonitor(job, queueName, workerSettings); const startTimes = new WeakMap(); @@ -159,22 +184,19 @@ export function instrumentWorker(worker: unknown, options: BullMQJobSchedulerInt target.on('failed', finish('error')); } -function getMonitor( - job: BullMQJob | undefined, - queueName: string, - exclude: Array | undefined, - hasCustomRepeatStrategy: boolean, -): Monitor | undefined { - const schedulerId = getJobSchedulerId(job, exclude); +function getMonitor(job: BullMQJob | undefined, queueName: string, settings: WorkerSettings): Monitor | undefined { + const schedulerId = job && getJobSchedulerId(job, settings); if (!job || !schedulerId) { return undefined; } // Scheduler ids are only unique within a queue. const monitorSlug = `${queueName}-${schedulerId}`; - const repeat = job.opts?.repeat; - const config = hasCustomRepeatStrategy && repeat?.pattern != null ? undefined : getMonitorConfig(repeat); - if (!config) { - DEBUG_BUILD && debug.log(`[${INTEGRATION_NAME}] Not monitoring ${monitorSlug}: Crons can't express its schedule.`); + const config = getMonitorConfig(job.opts?.repeat, settings); + if (typeof config === 'string') { + if (DEBUG_BUILD && !loggedSchedulers.has(monitorSlug)) { + loggedSchedulers.add(monitorSlug); + debug.log(`[${INTEGRATION_NAME}] Not monitoring ${monitorSlug}: ${config}.`); + } return undefined; } return { monitorSlug, checkInId: getCheckInId(job), config }; @@ -193,27 +215,44 @@ function isFirstAttempt(job: BullMQJob): boolean { return !job.attemptsMade && (typeof job.attemptsStarted !== 'number' || job.attemptsStarted <= 1); } -function getJobSchedulerId( - job: BullMQJob | undefined, - exclude: Array | undefined, -): string | undefined { - const id = job?.repeatJobKey; - if (typeof id !== 'string' || !id || LEGACY_REPEAT_KEY.test(id)) { +function getJobSchedulerId(job: BullMQJob, settings: WorkerSettings): string | undefined { + const id = job.repeatJobKey; + if (typeof id !== 'string' || !id || isLegacyRepeatableJob(job, id, settings.repeatKeyHashAlgorithm)) { return undefined; } - return stringMatchesSomePattern(id, exclude, true) ? undefined : id; + return stringMatchesSomePattern(id, settings.exclude, true) ? undefined : id; } -function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfig | undefined { - // A scheduler that stops after `limit` runs or `endDate` would show up as missed. - if (!repeat || repeat.limit != null || repeat.endDate != null) { - return undefined; +// BullMQ 5 keys repeatable jobs from `queue.add` by `repeat.key`, or by their name and repeat options or a hash of them. +// Job schedulers don't take a `key`. +function isLegacyRepeatableJob(job: BullMQJob, id: string, hashAlgorithm: string): boolean { + const repeat = job.opts?.repeat; + if (!repeat) { + return false; + } + if (repeat.key != null) { + return true; + } + const endDate = repeat.endDate ? new Date(repeat.endDate as string).getTime() : ''; + const suffix = repeat.pattern || String(repeat.every); + const key = `${job.name}:${repeat.jobId || ''}:${endDate}:${repeat.tz || ''}:${suffix}`; + return id === key || id === createHash(hashAlgorithm).update(key).digest('hex'); +} + +/** The monitor config for a job scheduler's repeat options, or why it isn't monitored. */ +function getMonitorConfig(repeat: BullMQRepeatOptions | undefined, settings: WorkerSettings): MonitorConfig | string { + // A scheduler that stops would show up as missed. + if (repeat?.limit != null) { + return 'it sets a limit'; + } + if (repeat?.endDate != null) { + return 'it sets an endDate'; } - if (typeof repeat.every === 'number') { + if (typeof repeat?.every === 'number') { const minutes = repeat.every / 60_000; if (!Number.isInteger(minutes) || minutes <= 0) { - return undefined; + return "Crons can't express its schedule"; } if (minutes % 1440 === 0) { return { schedule: { type: 'interval', value: minutes / 1440, unit: 'day' } }; @@ -224,9 +263,39 @@ function getMonitorConfig(repeat: BullMQRepeatOptions | undefined): MonitorConfi return { schedule: { type: 'interval', value: minutes, unit: 'minute' } }; } - if (typeof repeat.pattern !== 'string') { + if (typeof repeat?.pattern !== 'string') { + return "Crons can't express its schedule"; + } + if (settings.hasCustomRepeatStrategy) { + return 'the worker has a custom repeatStrategy'; + } + // BullMQ 6's cron-parser ignores `utc` and `nthDayOfWeek`. + const pattern = settings.major === 6 ? repeat.pattern : withNthDayOfWeek(repeat.pattern, repeat.nthDayOfWeek); + if (!pattern) { + return "Crons can't express its nthDayOfWeek"; + } + const timezone = settings.major === 5 && repeat.utc ? 'UTC' : repeat.tz; + return cronExpressionToMonitorConfig(pattern, timezone, 'cron-parser') || "Crons can't express its schedule"; +} + +// cron-parser 4 reads `{ pattern: '0 9 * * 1', nthDayOfWeek: 2 }` like `0 9 * * 1#2`, and applies `nthDayOfWeek` to +// any other day of week too. +function withNthDayOfWeek(pattern: string, nthDayOfWeek: unknown): string | undefined { + if (!nthDayOfWeek) { + return pattern; + } + const fields = pattern.trim().split(/\s+/); + const dayOfWeek = fields[fields.length - 1] as string; + if ( + (fields.length !== 5 && fields.length !== 6) || + !/^(?:[0-7]|[a-z]+)$/i.test(dayOfWeek) || + typeof nthDayOfWeek !== 'number' || + !Number.isInteger(nthDayOfWeek) || + nthDayOfWeek < 1 || + nthDayOfWeek > 5 + ) { return undefined; } - // BullMQ 5's cron-parser prefers `utc` over `tz`. - return cronExpressionToMonitorConfig(repeat.pattern, repeat.utc === true ? 'UTC' : repeat.tz, 'cron-parser'); + fields[fields.length - 1] = `${dayOfWeek}#${nthDayOfWeek}`; + return fields.join(' '); } diff --git a/packages/server-utils/src/orchestrion/config/bullmq.ts b/packages/server-utils/src/orchestrion/config/bullmq.ts index 01fa6a78b5ef..82a74cfd9e46 100644 --- a/packages/server-utils/src/orchestrion/config/bullmq.ts +++ b/packages/server-utils/src/orchestrion/config/bullmq.ts @@ -1,20 +1,22 @@ import type { InstrumentationConfig } from '../apmTypes'; import { getModuleNames } from './module-names'; -// Job schedulers were added in 5.16.0. The cron converter follows cron-parser 4, which BullMQ 5 uses, and BullMQ 6 -// uses cron-parser 5. -const module = { name: 'bullmq', versionRange: '>=5.16.0 <6' } as const; - -export const bullmqConfig = ['dist/cjs/classes/worker.js', 'dist/esm/classes/worker.js'].map( - (filePath): InstrumentationConfig => ({ - channelName: 'workerConstructor', - module: { ...module, filePath }, +// Job schedulers were added in 5.16.0. BullMQ 6 gets its own channel, since it reads `pattern` with cron-parser 5. +const workerConfigs = (channelName: string, versionRange: string): InstrumentationConfig[] => + ['dist/cjs/classes/worker.js', 'dist/esm/classes/worker.js'].map(filePath => ({ + channelName, + module: { name: 'bullmq', versionRange, filePath }, functionQuery: { className: 'Worker' }, - }), -); + })); + +export const bullmqConfig = [ + ...workerConfigs('workerConstructor', '>=5.16.0 <6'), + ...workerConfigs('worker6Constructor', '>=6.0.0 <7'), +]; export const bullmqModuleNames = getModuleNames(bullmqConfig); export const bullmqChannels = { BULLMQ_WORKER_CONSTRUCTOR: 'orchestrion:bullmq:workerConstructor', + BULLMQ_6_WORKER_CONSTRUCTOR: 'orchestrion:bullmq:worker6Constructor', } as const; diff --git a/packages/server-utils/test/integrations/bullmq.test.ts b/packages/server-utils/test/integrations/bullmq.test.ts index c8d100d531cd..e9735d0a1419 100644 --- a/packages/server-utils/test/integrations/bullmq.test.ts +++ b/packages/server-utils/test/integrations/bullmq.test.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { EventEmitter } from 'node:events'; import * as SentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, type MockInstance, test, vi } from 'vitest'; @@ -43,8 +44,13 @@ describe('bullmqJobSchedulerIntegration', () => { }); } - function runJob(job: object, outcome: 'completed' | 'failed' = 'completed', worker = createWorker()): void { - instrumentWorker(worker, {}); + function runJob( + job: object, + outcome: 'completed' | 'failed' = 'completed', + worker = createWorker(), + major: 5 | 6 = 5, + ): void { + instrumentWorker(worker, {}, major); worker.processFn(job); if (outcome === 'failed') { worker.emit('failed', finalFailure(job), new Error('boom'), 'active'); @@ -53,15 +59,15 @@ describe('bullmqJobSchedulerIntegration', () => { } } - function sentConfig(repeat: Record): unknown { - runJob(schedulerJob('my-scheduler', repeat)); + function sentConfig(repeat: Record, major: 5 | 6 = 5): unknown { + runJob(schedulerJob('my-scheduler', repeat), 'completed', createWorker(), major); return captureCheckInSpy.mock.calls[0]?.[1]; } - function expectSkipped(repeat: Record): void { - runJob(schedulerJob('my-scheduler', repeat), 'failed'); + function expectSkipped(repeat: Record, major: 5 | 6 = 5): void { + runJob(schedulerJob('my-scheduler', repeat), 'failed', createWorker(), major); const worker = createWorker(); - instrumentWorker(worker, {}); + instrumentWorker(worker, {}, major); worker.emit('failed', finalFailure(schedulerJob('my-scheduler', repeat)), new Error('stalled'), 'active'); expect(captureCheckInSpy).not.toHaveBeenCalled(); } @@ -126,13 +132,64 @@ describe('bullmqJobSchedulerIntegration', () => { }); }); - test('uses UTC with utc: true', () => { - expect(sentConfig({ pattern: '0 * * * *', tz: 'Europe/Vienna', utc: true })).toEqual({ + test.each([true, 1])('uses UTC with utc: %j', utc => { + expect(sentConfig({ pattern: '0 * * * *', tz: 'Europe/Vienna', utc })).toEqual({ schedule: { type: 'crontab', value: '0 * * * *' }, timezone: 'UTC', }); }); + test('ignores utc on BullMQ 6', () => { + expect(sentConfig({ pattern: '0 * * * *', tz: 'Europe/Vienna', utc: true }, 6)).toEqual({ + schedule: { type: 'crontab', value: '0 * * * *' }, + timezone: 'Europe/Vienna', + }); + }); + + test.each([ + ['0 9 * * 1', '0 9 * * 1#2'], + ['0 0 9 * * mon', '0 9 * * 1#2'], + ])('reads nthDayOfWeek with %j as %j', (pattern, crontab) => { + expect(sentConfig({ pattern, nthDayOfWeek: 2, tz: 'UTC' })).toEqual({ + schedule: { type: 'crontab', value: crontab }, + timezone: 'UTC', + }); + }); + + test.each([ + ['0 9 * * *', 2], + ['0 9 * * 1-5', 2], + ['0 9 * * 1,3', 2], + ['0 9 * * 5L', 2], + ['0 9 * * 1#3', 2], + ['0 9 1 * 1', 2], + ['0 9 * * 1', 6], + ['0 9 * * 1', '2'], + ])('skips %j with nthDayOfWeek %j', (pattern, nthDayOfWeek) => { + expectSkipped({ pattern, nthDayOfWeek, tz: 'UTC' }); + }); + + test('ignores nthDayOfWeek on BullMQ 6', () => { + expect(sentConfig({ pattern: '0 9 * * 1', nthDayOfWeek: 2, tz: 'UTC' }, 6)).toEqual({ + schedule: { type: 'crontab', value: '0 9 * * 1' }, + timezone: 'UTC', + }); + }); + + test('logs why a scheduler is not monitored once', () => { + const logSpy = vi.spyOn(SentryCore.debug, 'log').mockImplementation(() => undefined); + const worker = createWorker(); + instrumentWorker(worker, {}, 5); + for (let run = 0; run < 2; run++) { + const job = schedulerJob('limited', { every: 60_000, limit: 3 }); + worker.processFn(job); + worker.emit('completed', job, 'result', 'active'); + } + + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith('[BullMQJobScheduler] Not monitoring reports-limited: it sets a limit.'); + }); + test('skips a fixed UTC offset', () => { expectSkipped({ pattern: '0 * * * *', tz: 'UTC+5' }); }); @@ -155,22 +212,44 @@ describe('bullmqJobSchedulerIntegration', () => { test.each([ ['a job that is not from a scheduler', { name: 'job', opts: {} }], - ['a legacy repeatable job', schedulerJob('ef2e05b7df2d56840b735ad4ee3a6937', { every: 60_000 })], + [ + 'a legacy repeatable job', + schedulerJob(createHash('md5').update('job::::60000').digest('hex'), { every: 60_000, offset: 1000 }), + ], + [ + 'a legacy repeatable job with a jobId and tz', + schedulerJob(createHash('md5').update('job:my-id::UTC:0 * * * *').digest('hex'), { + pattern: '0 * * * *', + tz: 'UTC', + jobId: 'my-id', + }), + ], + ['a legacy repeatable job with an unhashed key', schedulerJob('job::::60000', { every: 60_000 })], + ['a legacy repeatable job with a custom key', schedulerJob('custom-key', { every: 60_000, key: 'custom-key' })], ['an excluded scheduler', schedulerJob('cache-warmup', { every: 60_000 })], ['a scheduler matching an excluded pattern', schedulerJob('tmp-cleanup', { every: 60_000 })], ])('ignores %s', (_, job) => { const worker = createWorker(); - instrumentWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }); + instrumentWorker(worker, { excludeJobSchedulers: ['cache-warmup', /^tmp-/] }, 5); worker.processFn(job); worker.emit('failed', finalFailure(job)); expect(captureCheckInSpy).not.toHaveBeenCalled(); }); + test.each(['0123456789abcdef0123456789abcdef', 'a:b:c:d:e'])('monitors a scheduler with the id %j', id => { + runJob(schedulerJob(id, { every: 60_000 })); + + expect(captureCheckInSpy).toHaveBeenCalledWith( + expect.objectContaining({ monitorSlug: `reports-${id}`, status: 'ok' }), + expect.anything(), + ); + }); + test('calls the processor with the worker and its arguments', () => { const worker = createWorker(); const processor = worker.processFn; - instrumentWorker(worker, {}); + instrumentWorker(worker, {}, 5); const job = schedulerJob('my-scheduler', { every: 60_000 }); expect(worker.processFn.call(worker, job, 'token')).toBe('result'); @@ -180,7 +259,7 @@ describe('bullmqJobSchedulerIntegration', () => { test('sends an error check-in for a job that fails without running the processor', () => { const worker = createWorker(); - instrumentWorker(worker, {}); + instrumentWorker(worker, {}, 5); const job = finalFailure(schedulerJob('my-scheduler', { every: 60_000 }, { attemptsStarted: 3 })); worker.emit('failed', job, new Error('job started more than allowable limit'), 'active'); @@ -194,13 +273,13 @@ describe('bullmqJobSchedulerIntegration', () => { test('keeps one check-in open across retries, on any worker', () => { const repeat = { every: 60_000 }; const firstWorker = createWorker(); - instrumentWorker(firstWorker, {}); + instrumentWorker(firstWorker, {}, 5); const firstAttempt = schedulerJob('my-scheduler', repeat); firstWorker.processFn(firstAttempt); firstWorker.emit('failed', Object.assign(firstAttempt, { attemptsMade: 1 }), new Error('boom'), 'active'); const secondWorker = createWorker(); - instrumentWorker(secondWorker, {}); + instrumentWorker(secondWorker, {}, 5); const retry = schedulerJob('my-scheduler', repeat, { attemptsStarted: 2, attemptsMade: 1 }); secondWorker.processFn(retry); secondWorker.emit('completed', retry, 'result', 'active'); @@ -225,7 +304,7 @@ describe('bullmqJobSchedulerIntegration', () => { test('sends an ok check-in for a job that completes without running the processor', () => { const worker = createWorker(); - instrumentWorker(worker, {}); + instrumentWorker(worker, {}, 5); worker.emit('completed', schedulerJob('my-scheduler', { every: 60_000 }), 'result', 'active'); expect(captureCheckInSpy).toHaveBeenCalledWith( @@ -236,7 +315,7 @@ describe('bullmqJobSchedulerIntegration', () => { test('skips patterns with a custom repeat strategy', () => { const worker = createWorker({ settings: { repeatStrategy: () => Date.now() } }); - instrumentWorker(worker, {}); + instrumentWorker(worker, {}, 5); for (const repeat of [{ pattern: '0 * * * *', tz: 'UTC' }, { every: 60_000 }]) { const job = schedulerJob('my-scheduler', repeat); worker.processFn(job); @@ -251,7 +330,7 @@ describe('bullmqJobSchedulerIntegration', () => { test('instruments a worker only once', () => { const worker = createWorker(); - instrumentWorker(worker, {}); + instrumentWorker(worker, {}, 5); runJob(schedulerJob('my-scheduler', { every: 60_000 }), 'completed', worker); expect(captureCheckInSpy).toHaveBeenCalledTimes(2); From 59b5f8ed7533ca2c75fcfd0b1b92294f249de3e4 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 15:58:12 -0800 Subject: [PATCH 16/20] ref(server-utils): Drop a redundant cast in the BullMQ channel subscriber Co-Authored-By: Claude Opus 5.5 --- packages/server-utils/src/integrations/bullmq.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-utils/src/integrations/bullmq.ts b/packages/server-utils/src/integrations/bullmq.ts index 8b3cb1676c67..d926900bc53c 100644 --- a/packages/server-utils/src/integrations/bullmq.ts +++ b/packages/server-utils/src/integrations/bullmq.ts @@ -49,7 +49,7 @@ function subscribe(options: BullMQJobSchedulerIntegrationOptions): void { for (const [channel, major] of channels) { diagnosticsChannel.tracingChannel<{ self?: unknown }>(channel).end.subscribe(message => { try { - instrumentWorker((message as { self?: unknown }).self, options, major); + instrumentWorker(message.self, options, major); } catch (e) { DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to set up the cron monitor:`, e); } From 8d0f02f25c2d414ad5ac926946503e024a05043e Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 15:59:47 -0800 Subject: [PATCH 17/20] ref(server-utils): Drop the unused type argument instead, since subscribers get unknown Co-Authored-By: Claude Opus 5.5 --- packages/server-utils/src/integrations/bullmq.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/integrations/bullmq.ts b/packages/server-utils/src/integrations/bullmq.ts index d926900bc53c..02bfb972ffaf 100644 --- a/packages/server-utils/src/integrations/bullmq.ts +++ b/packages/server-utils/src/integrations/bullmq.ts @@ -47,9 +47,9 @@ function subscribe(options: BullMQJobSchedulerIntegrationOptions): void { [CHANNELS.BULLMQ_6_WORKER_CONSTRUCTOR, 6], ] as const; for (const [channel, major] of channels) { - diagnosticsChannel.tracingChannel<{ self?: unknown }>(channel).end.subscribe(message => { + diagnosticsChannel.tracingChannel(channel).end.subscribe(message => { try { - instrumentWorker(message.self, options, major); + instrumentWorker((message as { self?: unknown }).self, options, major); } catch (e) { DEBUG_BUILD && debug.error(`[${INTEGRATION_NAME}] Failed to set up the cron monitor:`, e); } From 50795d74ea5757fe14c62b5ee079fbdd3ebee228 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 16:00:41 -0800 Subject: [PATCH 18/20] fix(server-utils): Log skipped legacy BullMQ repeatable jobs and try common key hashes A queue's repeatKeyHashAlgorithm isn't reachable from the worker, so legacy keys hashed with md5, sha1, sha256 or sha512 are recognized too. Co-Authored-By: Claude Opus 5.5 --- .../server-utils/src/integrations/bullmq.ts | 32 ++++++++++++------- .../test/integrations/bullmq.test.ts | 31 ++++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/server-utils/src/integrations/bullmq.ts b/packages/server-utils/src/integrations/bullmq.ts index 02bfb972ffaf..0aa8fd8f5567 100644 --- a/packages/server-utils/src/integrations/bullmq.ts +++ b/packages/server-utils/src/integrations/bullmq.ts @@ -185,13 +185,20 @@ export function instrumentWorker( } function getMonitor(job: BullMQJob | undefined, queueName: string, settings: WorkerSettings): Monitor | undefined { - const schedulerId = job && getJobSchedulerId(job, settings); - if (!job || !schedulerId) { + const schedulerId = job?.repeatJobKey; + if ( + !job || + typeof schedulerId !== 'string' || + !schedulerId || + stringMatchesSomePattern(schedulerId, settings.exclude, true) + ) { return undefined; } // Scheduler ids are only unique within a queue. const monitorSlug = `${queueName}-${schedulerId}`; - const config = getMonitorConfig(job.opts?.repeat, settings); + const config = isLegacyRepeatableJob(job, schedulerId, settings.repeatKeyHashAlgorithm) + ? 'it is a repeatable job added with queue.add, not a job scheduler' + : getMonitorConfig(job.opts?.repeat, settings); if (typeof config === 'string') { if (DEBUG_BUILD && !loggedSchedulers.has(monitorSlug)) { loggedSchedulers.add(monitorSlug); @@ -215,13 +222,8 @@ function isFirstAttempt(job: BullMQJob): boolean { return !job.attemptsMade && (typeof job.attemptsStarted !== 'number' || job.attemptsStarted <= 1); } -function getJobSchedulerId(job: BullMQJob, settings: WorkerSettings): string | undefined { - const id = job.repeatJobKey; - if (typeof id !== 'string' || !id || isLegacyRepeatableJob(job, id, settings.repeatKeyHashAlgorithm)) { - return undefined; - } - return stringMatchesSomePattern(id, settings.exclude, true) ? undefined : id; -} +// The queue's `repeatKeyHashAlgorithm` isn't reachable from a worker, so also try the common ones. +const LEGACY_KEY_HASH_ALGORITHMS = ['md5', 'sha1', 'sha256', 'sha512']; // BullMQ 5 keys repeatable jobs from `queue.add` by `repeat.key`, or by their name and repeat options or a hash of them. // Job schedulers don't take a `key`. @@ -236,7 +238,15 @@ function isLegacyRepeatableJob(job: BullMQJob, id: string, hashAlgorithm: string const endDate = repeat.endDate ? new Date(repeat.endDate as string).getTime() : ''; const suffix = repeat.pattern || String(repeat.every); const key = `${job.name}:${repeat.jobId || ''}:${endDate}:${repeat.tz || ''}:${suffix}`; - return id === key || id === createHash(hashAlgorithm).update(key).digest('hex'); + if (id === key) { + return true; + } + return ( + /^[\da-f]+$/.test(id) && + [hashAlgorithm, ...LEGACY_KEY_HASH_ALGORITHMS].some( + algorithm => createHash(algorithm).update(key).digest('hex') === id, + ) + ); } /** The monitor config for a job scheduler's repeat options, or why it isn't monitored. */ diff --git a/packages/server-utils/test/integrations/bullmq.test.ts b/packages/server-utils/test/integrations/bullmq.test.ts index e9735d0a1419..451af151a7f1 100644 --- a/packages/server-utils/test/integrations/bullmq.test.ts +++ b/packages/server-utils/test/integrations/bullmq.test.ts @@ -224,6 +224,10 @@ describe('bullmqJobSchedulerIntegration', () => { jobId: 'my-id', }), ], + [ + 'a legacy repeatable job from a queue that hashes keys with sha1', + schedulerJob(createHash('sha1').update('job::::60000').digest('hex'), { every: 60_000 }), + ], ['a legacy repeatable job with an unhashed key', schedulerJob('job::::60000', { every: 60_000 })], ['a legacy repeatable job with a custom key', schedulerJob('custom-key', { every: 60_000, key: 'custom-key' })], ['an excluded scheduler', schedulerJob('cache-warmup', { every: 60_000 })], @@ -237,6 +241,33 @@ describe('bullmqJobSchedulerIntegration', () => { expect(captureCheckInSpy).not.toHaveBeenCalled(); }); + test("ignores a legacy repeatable job hashed with the worker's repeatKeyHashAlgorithm", () => { + const worker = createWorker({ settings: { repeatKeyHashAlgorithm: 'sha384' } }); + const job = schedulerJob(createHash('sha384').update('job::::60000').digest('hex'), { every: 60_000 }); + runJob(job, 'completed', worker); + + expect(captureCheckInSpy).not.toHaveBeenCalled(); + }); + + test('logs that a legacy repeatable job with an endDate is not monitored once', () => { + const logSpy = vi.spyOn(SentryCore.debug, 'log').mockImplementation(() => undefined); + const endDate = Date.UTC(2030, 0, 1); + const id = createHash('md5').update(`job::${endDate}::60000`).digest('hex'); + const worker = createWorker(); + instrumentWorker(worker, {}, 5); + for (let run = 0; run < 2; run++) { + const job = schedulerJob(id, { every: 60_000, endDate: new Date(endDate).toISOString() }); + worker.processFn(job); + worker.emit('completed', job, 'result', 'active'); + } + + expect(captureCheckInSpy).not.toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith( + `[BullMQJobScheduler] Not monitoring reports-${id}: it is a repeatable job added with queue.add, not a job scheduler.`, + ); + }); + test.each(['0123456789abcdef0123456789abcdef', 'a:b:c:d:e'])('monitors a scheduler with the id %j', id => { runJob(schedulerJob(id, { every: 60_000 })); From e264c214b90e5fe7e87ef14c8f7ac28b29253ff8 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 16:01:07 -0800 Subject: [PATCH 19/20] fix(server-utils): Cap the schedulers the BullMQ integration remembers logging Co-Authored-By: Claude Opus 5.5 --- packages/server-utils/src/integrations/bullmq.ts | 4 ++++ .../server-utils/test/integrations/bullmq.test.ts | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/server-utils/src/integrations/bullmq.ts b/packages/server-utils/src/integrations/bullmq.ts index 0aa8fd8f5567..e4d94f945465 100644 --- a/packages/server-utils/src/integrations/bullmq.ts +++ b/packages/server-utils/src/integrations/bullmq.ts @@ -103,6 +103,7 @@ interface WorkerSettings { const instrumentedWorkers = new WeakSet(); const loggedSchedulers = new Set(); +const MAX_LOGGED_SCHEDULERS = 100; // `captureCheckIn` keeps a given `checkInId` for `in_progress` check-ins too. type InProgressCheckInWithId = InProgressCheckIn & { checkInId: string }; @@ -201,6 +202,9 @@ function getMonitor(job: BullMQJob | undefined, queueName: string, settings: Wor : getMonitorConfig(job.opts?.repeat, settings); if (typeof config === 'string') { if (DEBUG_BUILD && !loggedSchedulers.has(monitorSlug)) { + if (loggedSchedulers.size >= MAX_LOGGED_SCHEDULERS) { + loggedSchedulers.clear(); + } loggedSchedulers.add(monitorSlug); debug.log(`[${INTEGRATION_NAME}] Not monitoring ${monitorSlug}: ${config}.`); } diff --git a/packages/server-utils/test/integrations/bullmq.test.ts b/packages/server-utils/test/integrations/bullmq.test.ts index 451af151a7f1..ff9e2a4f580a 100644 --- a/packages/server-utils/test/integrations/bullmq.test.ts +++ b/packages/server-utils/test/integrations/bullmq.test.ts @@ -190,6 +190,17 @@ describe('bullmqJobSchedulerIntegration', () => { expect(logSpy).toHaveBeenCalledWith('[BullMQJobScheduler] Not monitoring reports-limited: it sets a limit.'); }); + test('remembers only the last 100 schedulers it logged', () => { + const logSpy = vi.spyOn(SentryCore.debug, 'log').mockImplementation(() => undefined); + const worker = createWorker(); + instrumentWorker(worker, {}, 5); + for (const id of ['first', ...Array.from({ length: 100 }, (_, i) => `other-${i}`), 'first']) { + worker.emit('completed', schedulerJob(id, { every: 60_000, limit: 3 }), 'result', 'active'); + } + + expect(logSpy.mock.calls.filter(([message]) => String(message).includes('reports-first:'))).toHaveLength(2); + }); + test('skips a fixed UTC offset', () => { expectSkipped({ pattern: '0 * * * *', tz: 'UTC+5' }); }); From 077a0f73c9ff0b0081c69134118418984fc540e9 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Wed, 11 Nov 2026 16:02:51 -0800 Subject: [PATCH 20/20] test(node): Wait for the BullMQ scenario's jobs instead of a fixed timeout Co-Authored-By: Claude Opus 5.5 --- .../suites/cron/bullmq/scenario.mjs | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs index 029d5273e16e..ec61949740a9 100644 --- a/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/cron/bullmq/scenario.mjs @@ -22,6 +22,38 @@ async function run() { connection, }); + // Waits for every job to finish, including the ones that send no check-ins. + const pending = new Map([ + ['plain', 1], + ['legacy', 1], + ['legacy-key', 1], + ['excluded', 1], + ['every', 2], + ['failing', 1], + ['flaky', 1], + ['sandboxed', 1], + ]); + const finished = new Promise(resolve => { + const done = job => { + const left = pending.get(job?.name); + if (left === undefined) { + return; + } + if (left > 1) { + pending.set(job.name, left - 1); + } else { + pending.delete(job.name); + } + if (!pending.size) { + resolve(); + } + }; + for (const w of [worker, sandboxedWorker]) { + w.on('completed', done); + w.on('failed', job => job?.finishedOn && done(job)); + } + }); + // Runs delayed scheduler jobs now. The loop below and the `completed` listener can race to promote the same job. const promote = job => job?.promote().catch(() => {}); @@ -56,15 +88,14 @@ async function run() { } } - setTimeout(async () => { - await worker.close(); - await sandboxedWorker.close(); - await queue.obliterate({ force: true }); - await sandboxedQueue.obliterate({ force: true }); - await queue.close(); - await sandboxedQueue.close(); - process.exit(); - }, 8_000); + await finished; + await worker.close(); + await sandboxedWorker.close(); + await queue.obliterate({ force: true }); + await sandboxedQueue.obliterate({ force: true }); + await queue.close(); + await sandboxedQueue.close(); + process.exit(); } void run();