From e280d142f3f7eca2cdf74b96daed0940a5b75880 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 16 Oct 2026 17:40:37 -0700 Subject: [PATCH 1/2] 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 ++++++++++++++ yarn.lock | 102 +++++++++- 8 files changed, 528 insertions(+), 7 deletions(-) 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 c44960c5004f..091d2c24b306 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(); + }); +}); diff --git a/yarn.lock b/yarn.lock index ef8f10db812c..fb44b8437c3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5683,6 +5683,36 @@ dependencies: sparse-bitfield "^3.0.3" +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4": + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz#22619f76a6b10ba78c8b74025b0d9754cad69cc7" + integrity sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4": + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz#c2fc0573afe08b0cf213e66eef76842b121d1577" + integrity sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4": + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz#4e3822f5522e18ed92611b894dc5db1bc882f39d" + integrity sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4": + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz#27ec4bc7eb6c311c982a50f1a6e1e1414638a6f8" + integrity sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4": + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz#37317d833c7d01d086f6155fa59c478adb6839f6" + integrity sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4": + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz#a1c79dcc9ae5f8c02aea8c2f144e5af6a822e5e8" + integrity sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ== + "@mswjs/interceptors@^0.41.0": version "0.41.9" resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz#9d90bbd60d1ddc30dbcbb827a9bb2e470493530d" @@ -12426,6 +12456,18 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" +bullmq@5.81.5: + version "5.81.5" + resolved "https://sfw.security.sentry.io/npm/bullmq/-/bullmq-5.81.5.tgz#f091c6e5505128a6b6510aaf22a728d7852bae52" + integrity sha512-pW7pFc/X+9AEIx91TWTdIpZ1icfbJnA0unsyzD7VICjCUw8a3qedSjej5dxxn6LjfM1kr2jvalOCGbCmoA5Kxg== + dependencies: + cron-parser "4.9.0" + ioredis "5.11.1" + msgpackr "2.0.5" + node-abort-controller "3.1.1" + semver "7.8.5" + tslib "2.8.1" + bun-types@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.2.9.tgz#e0208ba62f534eb64284c1f347f73bde7105c0f0" @@ -13376,7 +13418,7 @@ critters@0.0.16: postcss "^8.3.7" pretty-bytes "^5.3.0" -cron-parser@^4.2.0: +cron-parser@4.9.0, cron-parser@^4.2.0: version "4.9.0" resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== @@ -13937,7 +13979,7 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== -detect-libc@^2.0.0, detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2: +detect-libc@^2.0.0, detect-libc@^2.0.1, detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== @@ -17738,6 +17780,19 @@ ioredis@5.10.1, ioredis@^5.10.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ioredis@5.11.1: + version "5.11.1" + resolved "https://sfw.security.sentry.io/npm/ioredis/-/ioredis-5.11.1.tgz#2d1e52e350a79ee3b00883f3681a410427b49f28" + integrity sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A== + dependencies: + "@ioredis/commands" "1.10.0" + cluster-key-slot "1.1.1" + debug "4.4.3" + denque "2.1.0" + redis-errors "1.2.0" + redis-parser "3.0.0" + standard-as-callback "2.1.0" + ip-address@^10.0.1: version "10.7.2" resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.7.2.tgz#5b3b2b7d46c6293861b82dfcd49cc203aa8f2c5c" @@ -20470,6 +20525,27 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^3.0.4: + version "3.0.4" + resolved "https://sfw.security.sentry.io/npm/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz#d252698947f7a1a62478d22bfb789ba48dd4c1ac" + integrity sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw== + 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" + +msgpackr@2.0.5: + version "2.0.5" + resolved "https://sfw.security.sentry.io/npm/msgpackr/-/msgpackr-2.0.5.tgz#ea4308164fad112f1bd4a7bf008445a8a613658a" + integrity sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA== + optionalDependencies: + msgpackr-extract "^3.0.4" + muggle-string@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328" @@ -20834,6 +20910,11 @@ node-abi@^3.73.0, node-abi@^3.89.0: dependencies: semver "^7.3.5" +node-abort-controller@3.1.1: + version "3.1.1" + resolved "https://sfw.security.sentry.io/npm/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + node-addon-api@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -20866,6 +20947,13 @@ node-forge@^1, node-forge@^1.3.1, node-forge@^1.4.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== +node-gyp-build-optional-packages@5.2.2: + version "5.2.2" + resolved "https://sfw.security.sentry.io/npm/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4" + integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw== + dependencies: + detect-libc "^2.0.1" + node-gyp-build@^4.2.2: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" @@ -24880,6 +24968,11 @@ semver@7.7.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +semver@7.8.5, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4, semver@^7.8.5: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + semver@^5.3.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -24890,11 +24983,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4, semver@^7.8.5: - version "7.8.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" - integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== - send@^1.1.0, send@^1.2.0, send@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" From dd87bd31d9e527032485ee5f9daba77a05918ca3 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 16 Oct 2026 17:55:17 -0700 Subject: [PATCH 2/2] chore(deps): Dedupe yarn.lock Co-Authored-By: Claude --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index fb44b8437c3e..dad6ea1e4bd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17765,7 +17765,7 @@ invariant@^2.2.1, invariant@^2.2.2: redis-errors "1.2.0" standard-as-callback "2.1.0" -ioredis@5.10.1, ioredis@^5.10.1: +ioredis@5.10.1: version "5.10.1" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.10.1.tgz#6082781d8aec8d51ee4936bf81d0610404db1e3d" integrity sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA== @@ -17780,7 +17780,7 @@ ioredis@5.10.1, ioredis@^5.10.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" -ioredis@5.11.1: +ioredis@5.11.1, ioredis@^5.10.1: version "5.11.1" resolved "https://sfw.security.sentry.io/npm/ioredis/-/ioredis-5.11.1.tgz#2d1e52e350a79ee3b00883f3681a410427b49f28" integrity sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==