diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/docker-compose.yml b/dev-packages/e2e-tests/test-applications/node-bullmq/docker-compose.yml new file mode 100644 index 000000000000..15a423ffbc07 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/docker-compose.yml @@ -0,0 +1,7 @@ +services: + redis: + image: redis:8 + restart: always + container_name: e2e-tests-node-bullmq-redis + ports: + - '6379:6379' diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/global-setup.mjs b/dev-packages/e2e-tests/test-applications/node-bullmq/global-setup.mjs new file mode 100644 index 000000000000..438b88b61794 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/global-setup.mjs @@ -0,0 +1,13 @@ +import { execSync } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function globalSetup() { + // Start Redis via Docker Compose + execSync('docker compose up -d --wait', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/node-bullmq/global-teardown.mjs new file mode 100644 index 000000000000..35ce41179193 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/global-teardown.mjs @@ -0,0 +1,13 @@ +import { execSync } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function globalTeardown() { + // Stop Redis and remove containers + execSync('docker compose down --volumes', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/package.json b/dev-packages/e2e-tests/test-applications/node-bullmq/package.json new file mode 100644 index 000000000000..b86c4b846925 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/package.json @@ -0,0 +1,25 @@ +{ + "name": "node-bullmq", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "start": "node src/app.mjs", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test": "playwright test", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "bullmq": "^5.0.0", + "express": "^4.21.0" + }, + "devDependencies": { + "@playwright/test": "1.64.1", + "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-bullmq/playwright.config.mjs new file mode 100644 index 000000000000..d5fd0b394f15 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/playwright.config.mjs @@ -0,0 +1,11 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default { + ...config, + globalSetup: './global-setup.mjs', + globalTeardown: './global-teardown.mjs', +}; diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/pnpm-workspace.yaml b/dev-packages/e2e-tests/test-applications/node-bullmq/pnpm-workspace.yaml new file mode 100644 index 000000000000..0f973fea7a88 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + msgpackr-extract: true +minimumReleaseAge: 0 diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-bullmq/src/app.mjs new file mode 100644 index 000000000000..4b51a6f775f5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/src/app.mjs @@ -0,0 +1,49 @@ +import './instrument.mjs'; + +import * as Sentry from '@sentry/node'; +import { Queue, Worker } from 'bullmq'; +import express from 'express'; + +const app = express(); +const port = 3030; + +const connection = { host: '127.0.0.1', port: 6379 }; +const telemetry = new Sentry.BullMQTelemetry({ enableMetrics: true }); + +const testQueue = new Queue('test-queue', { connection, telemetry }); + +const worker = new Worker( + 'test-queue', + async job => { + if (job.name === 'fail-job') { + throw new Error('Test error from BullMQ processor'); + } + + if (job.name === 'breadcrumb-job') { + Sentry.addBreadcrumb({ message: 'breadcrumb-from-bullmq-processor' }); + } + + return { success: true }; + }, + { connection, telemetry }, +); + +worker.on('error', err => { + console.error('Worker error:', err); +}); + +app.get('/enqueue/:jobName', async (req, res) => { + await testQueue.add(req.params.jobName, { data: 'test' }); + res.send('Job enqueued'); +}); + +app.get('/check-isolation', async (req, res) => { + Sentry.captureException(new Error('Isolation check')); + res.send('Isolation check'); +}); + +Sentry.setupExpressErrorHandler(app); + +app.listen(port, () => { + console.log(`App listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/src/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-bullmq/src/instrument.mjs new file mode 100644 index 000000000000..5644b907f3f1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/src/instrument.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + tunnel: 'http://localhost:3031/', + tracesSampleRate: 1.0, + // A fixed release, so that a release from the CI environment does not change the span attributes. + release: '1.0', + // The Context integration adds machine-specific attributes to segment spans, and adds the `os.*` ones only after an + // async lookup. Without it, the tests can compare all span attributes. + integrations: integrations => integrations.filter(integration => integration.name !== 'Context'), +}); diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-bullmq/start-event-proxy.mjs new file mode 100644 index 000000000000..2426471fe487 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-bullmq', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-bullmq/tests/bullmq.test.ts b/dev-packages/e2e-tests/test-applications/node-bullmq/tests/bullmq.test.ts new file mode 100644 index 000000000000..2508bdb5b2b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-bullmq/tests/bullmq.test.ts @@ -0,0 +1,229 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForError, waitForMetric, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +test('Creates a queue.publish span when adding a job', async ({ baseURL }) => { + const publishSpanPromise = waitForStreamedSpan('node-bullmq', span => { + return getSpanOp(span) === 'queue.publish' && span.attributes['messaging.bullmq.job.name']?.value === 'publish-job'; + }); + + await fetch(`${baseURL}/enqueue/publish-job`); + + const publishSpan = await publishSpanPromise; + + expect(publishSpan.name).toBe('add test-queue.publish-job'); + expect(publishSpan.is_segment).toBe(false); + expect(publishSpan.status).toBe('ok'); + expect(publishSpan.attributes).toEqual({ + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.operation.name': { type: 'string', value: 'add' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'publish-job' }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: true }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node' }, + 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, + 'sentry.segment.name': { type: 'string', value: 'GET /enqueue/:jobName' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }); +}); + +test('Creates a segment span for queue.process when processing a job', async ({ baseURL }) => { + const processSpanPromise = waitForStreamedSpan('node-bullmq', span => { + return ( + span.is_segment && + getSpanOp(span) === 'queue.process' && + span.attributes['messaging.bullmq.job.name']?.value === 'process-job' + ); + }); + + await fetch(`${baseURL}/enqueue/process-job`); + + const processSpan = await processSpanPromise; + + expect(processSpan.name).toBe('process test-queue'); + expect(processSpan.status).toBe('ok'); + expect(processSpan.attributes).toEqual({ + 'sentry.op': { type: 'string', value: 'queue.process' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.consumer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.message.retry.count': { type: 'integer', value: 0 }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'process-job' }, + 'messaging.bullmq.job.processed_timestamp': { type: 'integer', value: expect.any(Number) }, + 'messaging.bullmq.job.attempt_finished_timestamp': { type: 'integer', value: expect.any(Number) }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sample_rate': { type: 'integer', value: 1 }, + 'sentry.sdk.integrations': { type: 'array', value: expect.any(Array) }, + 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node' }, + 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.segment.id': { type: 'string', value: processSpan.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process test-queue' }, + 'sentry.segment.name.source': { type: 'string', value: 'custom' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }); +}); + +test('Sends exception to Sentry on error in job processor', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-bullmq', event => { + return !event.type && event.exception?.values?.[0]?.value === 'Test error from BullMQ processor'; + }); + const processSpanPromise = waitForStreamedSpan('node-bullmq', span => { + return ( + span.is_segment && + getSpanOp(span) === 'queue.process' && + span.attributes['messaging.bullmq.job.name']?.value === 'fail-job' + ); + }); + + await fetch(`${baseURL}/enqueue/fail-job`); + + const errorEvent = await errorEventPromise; + const processSpan = await processSpanPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ + handled: false, + type: 'auto.queue.bullmq', + }); + expect(processSpan.status).toBe('error'); + expect(errorEvent.contexts?.trace?.trace_id).toBe(processSpan.trace_id); + expect(errorEvent.contexts?.trace?.span_id).toBe(processSpan.span_id); +}); + +test('BullMQ processor breadcrumbs do not leak into subsequent HTTP requests', async ({ baseURL }) => { + const processSpanPromise = waitForStreamedSpan('node-bullmq', span => { + return ( + span.is_segment && + getSpanOp(span) === 'queue.process' && + span.attributes['messaging.bullmq.job.name']?.value === 'breadcrumb-job' + ); + }); + + await fetch(`${baseURL}/enqueue/breadcrumb-job`); + + await processSpanPromise; + + const errorEventPromise = waitForError('node-bullmq', event => { + return event.exception?.values?.[0]?.value === 'Isolation check'; + }); + + await fetch(`${baseURL}/check-isolation`); + + const errorEvent = await errorEventPromise; + + const leakedBreadcrumb = (errorEvent.breadcrumbs || []).find( + (b: { message?: string }) => b.message === 'breadcrumb-from-bullmq-processor', + ); + expect(leakedBreadcrumb).toBeUndefined(); +}); + +test('Links the queue.process segment span to its producer span', async ({ baseURL }) => { + const producerSpanPromise = waitForStreamedSpan('node-bullmq', span => { + return getSpanOp(span) === 'queue.publish' && span.attributes['messaging.bullmq.job.name']?.value === 'link-job'; + }); + + const consumerSpanPromise = waitForStreamedSpan('node-bullmq', span => { + return ( + span.is_segment && + getSpanOp(span) === 'queue.process' && + span.attributes['messaging.bullmq.job.name']?.value === 'link-job' + ); + }); + + await fetch(`${baseURL}/enqueue/link-job`); + + const producerSpan = await producerSpanPromise; + const consumerSpan = await consumerSpanPromise; + + expect(producerSpan.name).toBe('add test-queue.link-job'); + expect(consumerSpan.name).toBe('process test-queue'); + expect(consumerSpan.trace_id).not.toBe(producerSpan.trace_id); + expect(consumerSpan.links).toEqual([ + { + trace_id: producerSpan.trace_id, + span_id: producerSpan.span_id, + sampled: true, + attributes: { 'sentry.link.type': { type: 'string', value: 'message_origin' } }, + }, + ]); +}); + +test('Emits messaging.bullmq.jobs.completed counter metric on successful job', async ({ baseURL }) => { + const metricPromise = waitForMetric('node-bullmq', metric => { + return ( + metric.name === 'messaging.bullmq.jobs.completed' && + metric.attributes?.['messaging.bullmq.job.name']?.value === 'counter-job' + ); + }); + + await fetch(`${baseURL}/enqueue/counter-job`); + + const metric = await metricPromise; + + expect({ + type: metric.type, + unit: metric.unit, + value: metric.value, + attributes: metric.attributes, + }).toEqual({ + type: 'counter', + unit: 'none', + value: 1, + attributes: { + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.bullmq.job.name': { type: 'string', value: 'counter-job' }, + 'messaging.bullmq.job.state': { type: 'string', value: 'completed' }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node' }, + 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + }); +}); + +test('Emits messaging.process.duration histogram metric on job completion', async ({ baseURL }) => { + const metricPromise = waitForMetric('node-bullmq', metric => { + return ( + metric.name === 'messaging.process.duration' && + metric.attributes?.['messaging.bullmq.job.name']?.value === 'duration-job' + ); + }); + + await fetch(`${baseURL}/enqueue/duration-job`); + + const metric = await metricPromise; + + expect({ + type: metric.type, + unit: metric.unit, + value: metric.value, + attributes: metric.attributes, + }).toEqual({ + type: 'distribution', + unit: 'second', + value: expect.any(Number), + attributes: { + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.bullmq.job.name': { type: 'string', value: 'duration-job' }, + 'messaging.bullmq.job.state': { type: 'string', value: 'completed' }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node' }, + 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + }); +}); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index a3dfca6c43a2..e2c9df0b1d00 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -65,6 +65,7 @@ "ai": "^4.3.16", "amqplib": "^0.10.9", "body-parser": "^2.3.0", + "bullmq": "^5.79.1", "consola": "^3.2.3", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/dev-packages/node-integration-tests/suites/tracing/bullmq/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/bullmq/docker-compose.yml new file mode 100644 index 000000000000..deac6f3496a4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/bullmq/docker-compose.yml @@ -0,0 +1,12 @@ +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/tracing/bullmq/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/bullmq/instrument.mjs new file mode 100644 index 000000000000..039b3bdb5104 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/bullmq/instrument.mjs @@ -0,0 +1,12 @@ +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, + // The Context integration adds machine-specific attributes to segment spans, and adds the `os.*` ones only after an + // async lookup. Without it, the tests can compare all span attributes. + integrations: integrations => integrations.filter(integration => integration.name !== 'Context'), +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario-attributes.mjs b/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario-attributes.mjs new file mode 100644 index 000000000000..1d3b1e445320 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario-attributes.mjs @@ -0,0 +1,68 @@ +import * as Sentry from '@sentry/node'; +import { FlowProducer, Queue, Worker } from 'bullmq'; + +const telemetry = new Sentry.BullMQTelemetry({ enableMetrics: true }); +const connection = { host: '127.0.0.1', port: 6384 }; + +async function run() { + const opsQueue = new Queue('attributes-ops-queue', { connection, telemetry }); + const opsWorker = new Worker('attributes-ops-queue', null, { connection, telemetry, name: 'attributes-ops-worker' }); + const flowProducer = new FlowProducer({ connection, telemetry }); + const processQueue = new Queue('attributes-process-queue', { connection, telemetry }); + const processWorker = new Worker('attributes-process-queue', async () => {}, { + connection, + telemetry, + name: 'attributes-worker', + }); + + const jobProcessed = new Promise(resolve => { + processWorker.once('completed', () => resolve()); + }); + + await Sentry.startSpan({ name: 'bullmq attributes' }, async () => { + await processQueue.add('process-job', { data: 'test-data' }); + + await opsQueue.addBulk([ + { name: 'bulk-job-1', data: {} }, + { name: 'bulk-job-2', data: {} }, + ]); + await flowProducer.add({ + name: 'flow-root', + queueName: 'attributes-ops-queue', + children: [{ name: 'flow-child', queueName: 'attributes-ops-queue' }], + }); + await opsQueue.upsertJobScheduler('attributes-scheduler', { every: 60_000 }, { name: 'scheduled-job' }); + + const progressJob = await opsQueue.add('progress-job', {}); + await opsQueue.updateJobProgress(progressJob.id, { percentage: 50 }); + await opsQueue.remove(progressJob.id, { removeChildren: true }); + await opsQueue.removeDeduplicationKey('attributes-deduplication-id'); + await opsQueue.removeDebounceKey('attributes-debounce-id'); + await opsQueue.retryJobs({ count: 10 }); + await opsQueue.promoteJobs({ count: 10 }); + await opsQueue.trimEvents(100); + await opsQueue.clean(0, 100, 'completed'); + + await opsWorker.getNextJob('attributes-token', { block: false }); + // BullMQ runs these two on timers. Calling them directly runs them once, inside this span. + await opsWorker['moveStalledJobsToWait'](); + await opsWorker['lockManager'].extendLocks([]); + await opsWorker.rateLimit(1); + await opsWorker.pause(true); + await opsWorker.close(true); + + await opsQueue.rateLimit(1); + await opsQueue.drain(true); + }); + + await jobProcessed; + await processQueue.recordJobCountsMetric('waiting', 'completed'); + await processWorker.close(); + + await opsQueue.obliterate({ force: true }); + await processQueue.obliterate({ force: true }); + await Promise.all([flowProducer.close(), opsQueue.close(), processQueue.close()]); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario-failed-job.mjs b/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario-failed-job.mjs new file mode 100644 index 000000000000..90eea3bb176e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario-failed-job.mjs @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; +import { Queue, Worker } from 'bullmq'; + +const telemetry = new Sentry.BullMQTelemetry(); +const connection = { host: '127.0.0.1', port: 6384 }; + +async function run() { + const queue = new Queue('test-failed-queue', { connection, telemetry }); + + const worker = new Worker( + 'test-failed-queue', + async () => { + throw new Error('Job failed on purpose'); + }, + { connection, telemetry }, + ); + + const jobFailed = new Promise(resolve => { + worker.on('failed', () => resolve()); + }); + + await queue.add('failing-job', { data: 'test-data' }); + + await jobFailed; + await worker.close(); + await queue.close(); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario.mjs new file mode 100644 index 000000000000..ffdcead133e6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/bullmq/scenario.mjs @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/node'; +import { Queue, Worker } from 'bullmq'; + +const telemetry = new Sentry.BullMQTelemetry({ enableMetrics: true }); +const connection = { host: '127.0.0.1', port: 6384 }; + +async function run() { + const queue = new Queue('test-queue', { connection, telemetry }); + + const worker = new Worker( + 'test-queue', + async () => { + // job processed + }, + { connection, telemetry }, + ); + + const jobsProcessed = new Promise(resolve => { + let completed = 0; + worker.on('completed', () => { + completed++; + if (completed === 2) { + resolve(); + } + }); + }); + + await Sentry.startSpan({ name: 'enqueue test-jobs' }, async () => { + await queue.add('test-job-1', { data: 'test-data' }); + await queue.add('test-job-2', { data: 'test-data' }); + }); + + await jobsProcessed; + await worker.close(); + await queue.close(); + await Sentry.flush(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/bullmq/test.ts b/dev-packages/node-integration-tests/suites/tracing/bullmq/test.ts new file mode 100644 index 000000000000..49871dcc9786 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/bullmq/test.ts @@ -0,0 +1,781 @@ +import type { Event, SerializedMetric } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import { afterAll, expect } from 'vitest'; +import { EXPECTED_SDK_NAME } from '../../../utils'; +import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; + +describeWithDockerCompose('bullmq', { workingDirectory: [__dirname] }, () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('traces producer and consumer operations with queue attributes', { timeout: 90_000 }, async () => { + const runner = createRunner().ignore('trace_metric'); + const producerSpansPromise = runner.collectStreamedSpansUntilSegment('enqueue test-jobs'); + const consumer1SpansPromise = runner.collectStreamedSpansUntilSegment( + span => span.attributes['messaging.bullmq.job.name']?.value === 'test-job-1', + ); + const consumer2SpansPromise = runner.collectStreamedSpansUntilSegment( + span => span.attributes['messaging.bullmq.job.name']?.value === 'test-job-2', + ); + + await runner.start().completed(); + + const producerSpans = await producerSpansPromise; + const consumerSpans = [await consumer1SpansPromise, await consumer2SpansPromise]; + const producerSegment = producerSpans.find(span => span.is_segment && span.name === 'enqueue test-jobs')!; + const publishSpans = producerSpans + .filter(span => span.attributes['messaging.system']?.value === 'bullmq') + .sort((a, b) => a.start_timestamp - b.start_timestamp); + const consumerSegments = consumerSpans.map(spans => + spans.find(span => span.is_segment && span.name === 'process test-queue')!, + ); + + expect( + publishSpans.map(span => ({ + name: span.name, + parent_span_id: span.parent_span_id, + status: span.status, + attributes: span.attributes, + })), + ).toEqual( + ['test-job-1', 'test-job-2'].map(jobName => ({ + name: `add test-queue.${jobName}`, + parent_span_id: producerSegment.span_id, + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.operation.name': { type: 'string', value: 'add' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: jobName }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: producerSegment.span_id }, + 'sentry.segment.name': { type: 'string', value: 'enqueue test-jobs' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }, + })), + ); + + expect( + consumerSpans.map(spans => + spans + .filter(span => span.attributes['messaging.system']?.value === 'bullmq') + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => ({ + name: span.name, + is_segment: span.is_segment, + status: span.status, + attributes: span.attributes, + })), + ), + ).toEqual( + ['test-job-1', 'test-job-2'].map((jobName, index) => [ + { + name: 'process test-queue', + is_segment: true, + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.process' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.consumer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.message.retry.count': { type: 'integer', value: 0 }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: jobName }, + 'messaging.bullmq.job.processed_timestamp': { type: 'integer', value: expect.any(Number) }, + 'messaging.bullmq.job.attempt_finished_timestamp': { type: 'integer', value: expect.any(Number) }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: consumerSegments[index]!.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process test-queue' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + 'sentry.sample_rate': { type: 'integer', value: 1 }, + 'sentry.sdk.integrations': { type: 'array', value: expect.any(Array) }, + 'sentry.segment.name.source': { type: 'string', value: 'custom' }, + }, + }, + { + name: 'complete test-queue', + is_segment: false, + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.operation.name': { type: 'string', value: 'complete' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: jobName }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: consumerSegments[index]!.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process test-queue' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }, + }, + ]), + ); + + expect(consumerSegments.map(segment => segment.attributes['messaging.message.id'])).toEqual( + publishSpans.map(span => span.attributes['messaging.message.id']), + ); + expect(consumerSegments.map(segment => segment.links)).toEqual( + publishSpans.map(span => [ + { + trace_id: span.trace_id, + span_id: span.span_id, + sampled: true, + attributes: { 'sentry.link.type': { type: 'string', value: 'message_origin' } }, + }, + ]), + ); + expect(new Set([producerSegment.trace_id, ...consumerSegments.map(segment => segment.trace_id)]).size).toBe(3); + }); + + test('emits completion counter and duration histogram for processed jobs', { timeout: 90_000 }, async () => { + let metrics: SerializedMetric[] = []; + + await createRunner() + .ignore('span') + .expect({ + trace_metric: container => { + metrics = container.items; + }, + }) + .start() + .completed(); + + expect( + metrics.map(metric => ({ + name: metric.name, + type: metric.type, + unit: metric.unit, + value: metric.value, + attributes: metric.attributes, + })), + ).toEqual( + ['test-job-1', 'test-job-2'].flatMap(jobName => [ + { + name: 'messaging.bullmq.jobs.completed', + type: 'counter', + unit: 'none', + value: 1, + attributes: { + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.bullmq.job.name': { type: 'string', value: jobName }, + 'messaging.bullmq.job.state': { type: 'string', value: 'completed' }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + }, + { + name: 'messaging.process.duration', + type: 'distribution', + unit: 'second', + value: expect.any(Number), + attributes: { + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.destination.name': { type: 'string', value: 'test-queue' }, + 'messaging.bullmq.job.name': { type: 'string', value: jobName }, + 'messaging.bullmq.job.state': { type: 'string', value: 'completed' }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + }, + ]), + ); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-failed-job.mjs', 'instrument.mjs', (createRunner, test) => { + test( + 'captures an error for a failed job and sets the error status on its process span', + { timeout: 90_000 }, + async () => { + let errorEvent: Event | undefined; + const runner = createRunner().ignore('trace_metric'); + const consumerSpansPromise = runner.collectStreamedSpansUntilSegment('process test-failed-queue'); + + await runner + .expect({ + event: event => { + errorEvent = event; + }, + }) + .start() + .completed(); + + const consumerSpans = await consumerSpansPromise; + const processSpan = consumerSpans.find(span => span.is_segment && span.name === 'process test-failed-queue')!; + + expect( + consumerSpans + .filter(span => span.attributes['messaging.system']?.value === 'bullmq') + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => ({ + name: span.name, + status: span.status, + attributes: span.attributes, + })), + ).toEqual([ + { + name: 'process test-failed-queue', + status: 'error', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.process' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.consumer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-failed-queue' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.message.retry.count': { type: 'integer', value: 0 }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'failing-job' }, + 'messaging.bullmq.job.processed_timestamp': { type: 'integer', value: expect.any(Number) }, + 'messaging.bullmq.job.attempt_finished_timestamp': { type: 'integer', value: expect.any(Number) }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: processSpan.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process test-failed-queue' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + 'sentry.sample_rate': { type: 'integer', value: 1 }, + 'sentry.sdk.integrations': { type: 'array', value: expect.any(Array) }, + 'sentry.segment.name.source': { type: 'string', value: 'custom' }, + 'sentry.status.message': { type: 'string', value: 'internal_error' }, + }, + }, + { + name: 'fail test-failed-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'test-failed-queue' }, + 'messaging.operation.name': { type: 'string', value: 'fail' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'failing-job' }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: processSpan.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process test-failed-queue' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }, + }, + ]); + expect(errorEvent?.exception?.values).toHaveLength(1); + expect(errorEvent?.exception?.values?.[0]?.type).toBe('Error'); + expect(errorEvent?.exception?.values?.[0]?.value).toBe('Job failed on purpose'); + expect(errorEvent?.exception?.values?.[0]?.mechanism).toEqual({ type: 'auto.queue.bullmq', handled: false }); + expect(errorEvent?.exception?.values?.[0]?.stacktrace).toBeUndefined(); + expect(errorEvent?.contexts?.trace?.trace_id).toBe(processSpan.trace_id); + expect(errorEvent?.contexts?.trace?.span_id).toBe(processSpan.span_id); + }, + ); + }); + + createEsmAndCjsTests(__dirname, 'scenario-attributes.mjs', 'instrument.mjs', (createRunner, test) => { + test('sends the attributes and metrics of all BullMQ operations', { timeout: 90_000 }, async () => { + let metrics: SerializedMetric[] = []; + const runner = createRunner(); + const opsSpansPromise = runner.collectStreamedSpansUntilSegment('bullmq attributes'); + const processSpansPromise = runner.collectStreamedSpansUntilSegment('process attributes-process-queue'); + + await runner + .expect({ + trace_metric: container => { + metrics = container.items; + }, + }) + .start() + .completed(); + + const opsSpans = await opsSpansPromise; + const processSpans = await processSpansPromise; + const opsSegment = opsSpans.find(span => span.is_segment && span.name === 'bullmq attributes')!; + const processSegment = processSpans.find( + span => span.is_segment && span.name === 'process attributes-process-queue', + )!; + + expect( + opsSpans + .filter(span => span.attributes['messaging.system']?.value === 'bullmq') + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => ({ + name: span.name, + status: span.status, + attributes: span.attributes, + })), + ).toEqual( + [ + { + name: 'add attributes-process-queue.process-job', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-process-queue' }, + 'messaging.operation.name': { type: 'string', value: 'add' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'process-job' }, + }, + }, + { + name: 'addBulk attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'addBulk' }, + 'messaging.batch.message_count': { type: 'integer', value: 2 }, + 'messaging.bullmq.job.bulk.names': { type: 'array', value: ['bulk-job-1', 'bulk-job-2'] }, + }, + }, + { + name: 'addFlow attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'addFlow' }, + 'messaging.bullmq.flow.name': { type: 'string', value: 'flow-root' }, + }, + }, + { + name: 'addNode attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'addNode' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'flow-root' }, + }, + }, + { + name: 'addNode attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'addNode' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'flow-child' }, + }, + }, + { + name: 'add attributes-ops-queue.scheduled-job', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'add' }, + 'messaging.message.id': { + type: 'string', + value: expect.stringMatching(/^repeat:attributes-scheduler:\d+$/), + }, + 'messaging.bullmq.job.scheduler.id': { type: 'string', value: 'attributes-scheduler' }, + }, + }, + { + name: 'add attributes-ops-queue.progress-job', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.publish' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.producer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'add' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'progress-job' }, + }, + }, + { + name: 'updateJobProgress attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'updateJobProgress' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + }, + }, + { + name: 'remove attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'remove' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + }, + }, + { + name: 'removeDeduplicationKey attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'removeDeduplicationKey' }, + 'messaging.bullmq.job.deduplication.key': { type: 'string', value: 'attributes-deduplication-id' }, + }, + }, + { + name: 'removeDebounceKey attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'removeDebounceKey' }, + 'messaging.bullmq.job.key': { type: 'string', value: 'attributes-debounce-id' }, + }, + }, + { + name: 'retryJobs attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'retryJobs' }, + }, + }, + { + name: 'promoteJobs attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'promoteJobs' }, + }, + }, + { + name: 'trimEvents attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'trimEvents' }, + 'messaging.bullmq.queue.event.max_length': { type: 'integer', value: 100 }, + }, + }, + { + // `messaging.bullmq.queue.clean.count` replaces `messaging.bullmq.job.ids` in BullMQ 6. + name: 'clean attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'clean' }, + 'messaging.bullmq.job.type': { type: 'string', value: 'completed' }, + 'messaging.bullmq.job.ids': { type: 'array', value: [] }, + 'messaging.bullmq.queue.clean.limit': { type: 'integer', value: 100 }, + 'messaging.bullmq.queue.grace': { type: 'integer', value: 0 }, + }, + }, + { + name: 'getNextJob attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'getNextJob' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.name': { type: 'string', value: 'attributes-ops-worker' }, + }, + }, + { + name: 'moveStalledJobsToWait attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'moveStalledJobsToWait' }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.name': { type: 'string', value: 'attributes-ops-worker' }, + 'messaging.bullmq.worker.stalled_jobs': { type: 'array', value: [] }, + }, + }, + { + name: 'extendLocks attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'extendLocks' }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.name': { type: 'string', value: 'attributes-ops-worker' }, + 'messaging.bullmq.worker.jobs_to_extend_locks': { type: 'array', value: [] }, + }, + }, + { + name: 'rateLimit attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'rateLimit' }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.rate_limit': { type: 'integer', value: 1 }, + }, + }, + { + name: 'pause attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'pause' }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.name': { type: 'string', value: 'attributes-ops-worker' }, + 'messaging.bullmq.worker.do_not_wait_active': { type: 'boolean', value: true }, + }, + }, + { + name: 'close attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'close' }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.name': { type: 'string', value: 'attributes-ops-worker' }, + 'messaging.bullmq.worker.force_close': { type: 'boolean', value: true }, + }, + }, + { + name: 'rateLimit attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'rateLimit' }, + 'messaging.bullmq.queue.rate_limit': { type: 'integer', value: 1 }, + }, + }, + { + name: 'drain attributes-ops-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-ops-queue' }, + 'messaging.operation.name': { type: 'string', value: 'drain' }, + 'messaging.bullmq.queue.drain.delay': { type: 'boolean', value: true }, + }, + }, + ].map(span => ({ + ...span, + attributes: { + ...span.attributes, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: opsSegment.span_id }, + 'sentry.segment.name': { type: 'string', value: 'bullmq attributes' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }, + })), + ); + + expect( + processSpans + .filter(span => span.attributes['messaging.system']?.value === 'bullmq') + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => ({ + name: span.name, + status: span.status, + attributes: span.attributes, + })), + ).toEqual([ + { + name: 'process attributes-process-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue.process' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq.consumer' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-process-queue' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.message.retry.count': { type: 'integer', value: 0 }, + 'messaging.client.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.worker.name': { type: 'string', value: 'attributes-worker' }, + 'messaging.bullmq.job.name': { type: 'string', value: 'process-job' }, + 'messaging.bullmq.job.processed_timestamp': { type: 'integer', value: expect.any(Number) }, + 'messaging.bullmq.job.attempt_finished_timestamp': { type: 'integer', value: expect.any(Number) }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: processSegment.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process attributes-process-queue' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + 'sentry.sample_rate': { type: 'integer', value: 1 }, + 'sentry.sdk.integrations': { type: 'array', value: expect.any(Array) }, + 'sentry.segment.name.source': { type: 'string', value: 'custom' }, + }, + }, + { + name: 'complete attributes-process-queue', + status: 'ok', + attributes: { + 'sentry.op': { type: 'string', value: 'queue' }, + 'sentry.origin': { type: 'string', value: 'auto.queue.bullmq' }, + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-process-queue' }, + 'messaging.operation.name': { type: 'string', value: 'complete' }, + 'messaging.message.id': { type: 'string', value: expect.any(String) }, + 'messaging.bullmq.job.name': { type: 'string', value: 'process-job' }, + 'sentry.environment': { type: 'string', value: 'production' }, + 'sentry.is_localhost': { type: 'boolean', value: false }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.segment.id': { type: 'string', value: processSegment.span_id }, + 'sentry.segment.name': { type: 'string', value: 'process attributes-process-queue' }, + 'sentry.trace_lifecycle': { type: 'string', value: 'stream' }, + }, + }, + ]); + + expect( + metrics.map(metric => ({ + name: metric.name, + type: metric.type, + unit: metric.unit, + value: metric.value, + attributes: metric.attributes, + })), + ).toEqual([ + { + name: 'messaging.bullmq.jobs.completed', + type: 'counter', + unit: 'none', + value: 1, + attributes: { + 'messaging.destination.name': { type: 'string', value: 'attributes-process-queue' }, + 'messaging.bullmq.job.name': { type: 'string', value: 'process-job' }, + 'messaging.bullmq.job.state': { type: 'string', value: 'completed' }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + }, + { + name: 'messaging.process.duration', + type: 'distribution', + unit: 'second', + value: expect.any(Number), + attributes: { + 'messaging.system': { type: 'string', value: 'bullmq' }, + 'messaging.operation.name': { type: 'string', value: 'process' }, + 'messaging.destination.name': { type: 'string', value: 'attributes-process-queue' }, + 'messaging.bullmq.job.name': { type: 'string', value: 'process-job' }, + 'messaging.bullmq.job.state': { type: 'string', value: 'completed' }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + }, + ...[ + { state: 'waiting', value: 0 }, + { state: 'completed', value: 1 }, + { state: 'paused', value: 0 }, + ].map(({ state, value }) => ({ + name: 'messaging.bullmq.queue.jobs', + type: 'gauge', + unit: 'none', + value, + attributes: { + 'messaging.destination.name': { type: 'string', value: 'attributes-process-queue' }, + 'messaging.bullmq.queue.jobs.state': { type: 'string', value: state }, + 'sentry.release': { type: 'string', value: '1.0' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, + 'sentry.sdk.version': { type: 'string', value: SDK_VERSION }, + 'sentry.timestamp.sequence': { type: 'integer', value: expect.any(Number) }, + 'server.address': { type: 'string', value: expect.any(String) }, + }, + })), + ]); + }); + }); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 80158e311e49..4710c4b4b20f 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -18,6 +18,7 @@ export { googleGenAIIntegration, // eslint-disable-next-line typescript/no-deprecated disableAnrDetectionForCallback, + BullMQTelemetry, captureCheckIn, captureConsoleIntegration, captureEvent, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index fd07e76a3fba..839aee7ed716 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -162,6 +162,7 @@ export { zodErrorsIntegration, profiler, amqplibIntegration, + BullMQTelemetry, anthropicAIIntegration, googleGenAIIntegration, vercelAIIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 4e91068d21f8..b8efd1d74901 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -179,6 +179,7 @@ export { zodErrorsIntegration, profiler, amqplibIntegration, + BullMQTelemetry, anthropicAIIntegration, googleGenAIIntegration, vercelAIIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index b90e609bbfc1..99a4451345f4 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -159,6 +159,7 @@ export { zodErrorsIntegration, profiler, amqplibIntegration, + BullMQTelemetry, anthropicAIIntegration, googleGenAIIntegration, childProcessIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 130194f72cf5..03db068d2a6a 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -65,6 +65,7 @@ export { export { setupHapiErrorHandler } from './integrations/tracing/hapi'; // oxlint-disable-next-line typescript/no-deprecated -- deprecated but still re-exported for backwards compatibility export { setupKoaErrorHandler } from './integrations/tracing/koa'; +export { BullMQTelemetry } from './integrations/tracing/bullmq'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler, diff --git a/packages/node/src/integrations/tracing/bullmq/contextManager.ts b/packages/node/src/integrations/tracing/bullmq/contextManager.ts new file mode 100644 index 000000000000..ad32e5689cdb --- /dev/null +++ b/packages/node/src/integrations/tracing/bullmq/contextManager.ts @@ -0,0 +1,62 @@ +import type { Span } from '@opentelemetry/api'; +import { + extractTraceparentData, + getActiveSpan, + getCurrentScope, + spanToTraceHeader, + withActiveSpan, + withIsolationScope, +} from '@sentry/core'; +import type { ContextManager, SentryContext } from './types'; + +export class SentryBullMQContextManager implements ContextManager { + public active(): SentryContext { + return { + span: getActiveSpan(), + scope: getCurrentScope(), + }; + } + + public with unknown>(context: SentryContext, fn: A): ReturnType { + if (context.span) { + return withIsolationScope(() => { + return withActiveSpan(context.span as Span, fn) as ReturnType; + }); + } + return withIsolationScope(() => withActiveSpan(null, fn)) as ReturnType; + } + + /** + * Returns a context without a span. BullMQ 6.3.11+ runs the stalled jobs checker in it, so the + * checker does not stay attached to the span that was active when the worker started. + */ + public root(): SentryContext { + return { + span: undefined, + scope: getCurrentScope(), + }; + } + + public getMetadata(context: SentryContext): string { + if (context.span) { + return spanToTraceHeader(context.span); + } + return ''; + } + + public fromMetadata(activeContext: SentryContext, metadata: string): SentryContext { + const traceparent = extractTraceparentData(metadata); + if (!traceparent?.traceId || !traceparent?.parentSpanId) { + return activeContext; + } + + return { + ...activeContext, + producerSpanContext: { + traceId: traceparent.traceId, + spanId: traceparent.parentSpanId, + sampled: traceparent.parentSampled === true, + }, + }; + } +} diff --git a/packages/node/src/integrations/tracing/bullmq/index.ts b/packages/node/src/integrations/tracing/bullmq/index.ts new file mode 100644 index 000000000000..984b1af13591 --- /dev/null +++ b/packages/node/src/integrations/tracing/bullmq/index.ts @@ -0,0 +1,56 @@ +import { SentryBullMQContextManager } from './contextManager'; +import { SentryBullMQMeter } from './meter'; +import { SentryBullMQTracer } from './tracer'; +import type { ContextManager, Meter, Telemetry, Tracer, SentryContext } from './types'; + +/** + * A Sentry-specific telemetry implementation for BullMQ. + * + * This implements BullMQ's Telemetry interface to provide automatic tracing + * for queue operations with proper Queue Insights attributes. + * + * @example + * ```javascript + * import * as Sentry from '@sentry/node'; + * import { Queue, Worker } from 'bullmq'; + * + * const telemetry = new Sentry.BullMQTelemetry(); + * + * const queue = new Queue('myQueue', { + * connection: { host: '127.0.0.1', port: 6379 }, + * telemetry, + * }); + * + * const worker = new Worker('myQueue', async (job) => { + * // Process job + * }, { + * connection: { host: '127.0.0.1', port: 6379 }, + * telemetry, + * }); + * ``` + * + * To also send BullMQ's job metrics to Sentry, use `new Sentry.BullMQTelemetry({ enableMetrics: true })`. + * + * @see https://docs.bullmq.io/guide/telemetry + */ +export class BullMQTelemetry implements Telemetry { + public tracer: Tracer; + public contextManager: ContextManager; + public meter?: Meter; + + public constructor( + options: { + /** + * Sends BullMQ's job metrics, such as `messaging.bullmq.jobs.completed` and `messaging.process.duration`, to Sentry. Defaults to `false`. + */ + enableMetrics?: boolean; + } = {}, + ) { + this.tracer = new SentryBullMQTracer(); + this.contextManager = new SentryBullMQContextManager(); + + if (options.enableMetrics) { + this.meter = new SentryBullMQMeter(); + } + } +} diff --git a/packages/node/src/integrations/tracing/bullmq/meter.ts b/packages/node/src/integrations/tracing/bullmq/meter.ts new file mode 100644 index 000000000000..bfae7aa342f8 --- /dev/null +++ b/packages/node/src/integrations/tracing/bullmq/meter.ts @@ -0,0 +1,121 @@ +import { MESSAGING_OPERATION_NAME, MESSAGING_SYSTEM } from '@sentry/conventions/attributes'; +import { metrics } from '@sentry/core'; +import { toMessagingKey, toSentryAttributes } from './span'; +import type { AttributeValue, Counter, Gauge, Histogram, Meter, MetricOptions } from './types'; + +function toMetricAttributes( + attributes?: Record, +): Record | undefined { + if (!attributes) { + return undefined; + } + const result: Record = {}; + for (const [key, value] of Object.entries(toSentryAttributes(attributes))) { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + result[key] = value; + } + } + return result; +} + +/** + * Converts the OpenTelemetry (UCUM) unit that BullMQ passes to the matching Sentry unit. + */ +function toSentryUnit(unit?: string): string | undefined { + if (unit === 'ms') { + return 'millisecond'; + } + + // In UCUM, `1` and annotations such as `{jobs}` mark a value without a unit. + if (unit === '1' || unit?.startsWith('{')) { + return 'none'; + } + + return unit; +} + +class SentryBullMQCounter implements Counter { + private _name: string; + private _unit?: string; + + public constructor(name: string, unit?: string) { + this._name = name; + this._unit = unit; + } + + public add(value: number, attributes?: Record): void { + metrics.count(this._name, value, { + unit: this._unit, + attributes: toMetricAttributes(attributes), + }); + } +} + +class SentryBullMQHistogram implements Histogram { + private _name: string; + private _unit?: string; + + public constructor(name: string, unit?: string) { + this._name = name; + this._unit = unit; + } + + public record(value: number, attributes?: Record): void { + metrics.distribution(this._name, value, { + unit: this._unit, + attributes: toMetricAttributes(attributes), + }); + } +} + +class SentryBullMQGauge implements Gauge { + private _name: string; + private _unit?: string; + + public constructor(name: string, unit?: string) { + this._name = name; + this._unit = unit; + } + + public record(value: number, attributes?: Record): void { + metrics.gauge(this._name, value, { + unit: this._unit, + attributes: toMetricAttributes(attributes), + }); + } +} + +/** + * Records BullMQ's `bullmq.job.duration` histogram, which is in milliseconds, as the OTel + * `messaging.process.duration` metric in seconds. + */ +class SentryBullMQProcessDuration implements Histogram { + public record(value: number, attributes?: Record): void { + metrics.distribution('messaging.process.duration', value / 1000, { + unit: 'second', + attributes: { + ...toMetricAttributes(attributes), + [MESSAGING_SYSTEM]: 'bullmq', + [MESSAGING_OPERATION_NAME]: 'process', + }, + }); + } +} + +export class SentryBullMQMeter implements Meter { + public createCounter(name: string, options?: MetricOptions): Counter { + return new SentryBullMQCounter(toMessagingKey(name), toSentryUnit(options?.unit)); + } + + public createHistogram(name: string, options?: MetricOptions): Histogram { + if (name === 'bullmq.job.duration') { + return new SentryBullMQProcessDuration(); + } + + return new SentryBullMQHistogram(toMessagingKey(name), toSentryUnit(options?.unit)); + } + + public createGauge(name: string, options?: MetricOptions): Gauge { + return new SentryBullMQGauge(toMessagingKey(name), toSentryUnit(options?.unit)); + } +} diff --git a/packages/node/src/integrations/tracing/bullmq/span.ts b/packages/node/src/integrations/tracing/bullmq/span.ts new file mode 100644 index 000000000000..10ec13bee4c2 --- /dev/null +++ b/packages/node/src/integrations/tracing/bullmq/span.ts @@ -0,0 +1,205 @@ +import type { Attributes, Span } from '@opentelemetry/api'; +import { + MESSAGING_BATCH_MESSAGE_COUNT, + MESSAGING_BULLMQ_FLOW_NAME, + MESSAGING_BULLMQ_JOB_ATTEMPT_FINISHED_TIMESTAMP, + MESSAGING_BULLMQ_JOB_BULK_NAMES, + MESSAGING_BULLMQ_JOB_DEDUPLICATION_KEY, + MESSAGING_BULLMQ_JOB_IDS, + MESSAGING_BULLMQ_JOB_KEY, + MESSAGING_BULLMQ_JOB_NAME, + MESSAGING_BULLMQ_JOB_PROCESSED_TIMESTAMP, + MESSAGING_BULLMQ_JOB_SCHEDULER_ID, + MESSAGING_BULLMQ_JOB_STATE, + MESSAGING_BULLMQ_JOB_TYPE, + MESSAGING_BULLMQ_QUEUE_CLEAN_COUNT, + MESSAGING_BULLMQ_QUEUE_CLEAN_LIMIT, + MESSAGING_BULLMQ_QUEUE_DRAIN_DELAY, + MESSAGING_BULLMQ_QUEUE_EVENT_MAX_LENGTH, + MESSAGING_BULLMQ_QUEUE_GRACE, + MESSAGING_BULLMQ_QUEUE_JOBS_STATE, + MESSAGING_BULLMQ_QUEUE_RATE_LIMIT, + MESSAGING_BULLMQ_WORKER_DO_NOT_WAIT_ACTIVE, + MESSAGING_BULLMQ_WORKER_FORCE_CLOSE, + MESSAGING_BULLMQ_WORKER_JOBS_TO_EXTEND_LOCKS, + MESSAGING_BULLMQ_WORKER_NAME, + MESSAGING_BULLMQ_WORKER_RATE_LIMIT, + MESSAGING_BULLMQ_WORKER_STALLED_JOBS, + MESSAGING_CLIENT_ID, + MESSAGING_DESTINATION_NAME, + MESSAGING_MESSAGE_ID, + MESSAGING_MESSAGE_RETRY_COUNT, + MESSAGING_OPERATION_NAME, +} from '@sentry/conventions/attributes'; +import type { Scope } from '@sentry/core'; +import { captureException, SPAN_STATUS_ERROR } from '@sentry/core'; +import type { AttributeValue, TelemetrySpan } from './types'; + +const DROPPED_ATTRIBUTES = new Set([ + // BullMQ sets these to `JSON.stringify()` output. They are left out until Sentry attributes support objects. + 'bullmq.job.options', + 'bullmq.job.progress', + 'bullmq.queue.options', + 'bullmq.worker.options', + // BullMQ deprecated this for `bullmq.job.attempt_finished_timestamp`, which it sets on the same spans. + 'bullmq.job.finished.timestamp', +]); + +// The BullMQ attribute keys and their `messaging.bullmq.*` keys in the Sentry conventions. Where BullMQ separates +// the words of a key with dots, the Sentry key uses snake_case. +const MESSAGING_BULLMQ_KEYS = new Map([ + ['bullmq.flow.name', MESSAGING_BULLMQ_FLOW_NAME], + ['bullmq.job.attempt_finished_timestamp', MESSAGING_BULLMQ_JOB_ATTEMPT_FINISHED_TIMESTAMP], + ['bullmq.job.bulk.names', MESSAGING_BULLMQ_JOB_BULK_NAMES], + ['bullmq.job.deduplication.key', MESSAGING_BULLMQ_JOB_DEDUPLICATION_KEY], + ['bullmq.job.ids', MESSAGING_BULLMQ_JOB_IDS], + ['bullmq.job.key', MESSAGING_BULLMQ_JOB_KEY], + ['bullmq.job.name', MESSAGING_BULLMQ_JOB_NAME], + ['bullmq.job.processed.timestamp', MESSAGING_BULLMQ_JOB_PROCESSED_TIMESTAMP], + ['bullmq.job.scheduler.id', MESSAGING_BULLMQ_JOB_SCHEDULER_ID], + ['bullmq.job.type', MESSAGING_BULLMQ_JOB_TYPE], + ['bullmq.queue.clean.count', MESSAGING_BULLMQ_QUEUE_CLEAN_COUNT], + ['bullmq.queue.clean.limit', MESSAGING_BULLMQ_QUEUE_CLEAN_LIMIT], + ['bullmq.queue.drain.delay', MESSAGING_BULLMQ_QUEUE_DRAIN_DELAY], + ['bullmq.queue.event.max.length', MESSAGING_BULLMQ_QUEUE_EVENT_MAX_LENGTH], + ['bullmq.queue.grace', MESSAGING_BULLMQ_QUEUE_GRACE], + ['bullmq.queue.jobs.state', MESSAGING_BULLMQ_QUEUE_JOBS_STATE], + ['bullmq.queue.rate.limit', MESSAGING_BULLMQ_QUEUE_RATE_LIMIT], + ['bullmq.worker.do.not.wait.active', MESSAGING_BULLMQ_WORKER_DO_NOT_WAIT_ACTIVE], + ['bullmq.worker.force.close', MESSAGING_BULLMQ_WORKER_FORCE_CLOSE], + ['bullmq.worker.jobs.to.extend.locks', MESSAGING_BULLMQ_WORKER_JOBS_TO_EXTEND_LOCKS], + ['bullmq.worker.name', MESSAGING_BULLMQ_WORKER_NAME], + ['bullmq.worker.rate.limit', MESSAGING_BULLMQ_WORKER_RATE_LIMIT], + ['bullmq.worker.stalled.jobs', MESSAGING_BULLMQ_WORKER_STALLED_JOBS], +]); + +/** + * Returns the Sentry key for a `bullmq.*` attribute key or metric name: the key from `MESSAGING_BULLMQ_KEYS`, or + * `messaging.` for the keys that are not in it, such as the metric names. Returns all other keys unchanged. + */ +export function toMessagingKey(key: string): string { + return MESSAGING_BULLMQ_KEYS.get(key) ?? (key.startsWith('bullmq.') ? `messaging.${key}` : key); +} + +/** + * Returns the BullMQ attributes with Sentry keys: the `messaging.*` convention where one exists, and the + * `messaging.bullmq.*` key for all others. The attributes that hold JSON strings and the deprecated ones are left out. + */ +export function toSentryAttributes(attributes: Record): Attributes { + const { + 'bullmq.queue.name': queueName, + 'bullmq.queue.operation': operation, + 'bullmq.job.id': jobId, + 'bullmq.job.attempts.made': attemptsMade, + 'bullmq.job.bulk.count': bulkCount, + 'bullmq.worker.id': workerId, + 'bullmq.job.state': jobState, + 'bullmq.job.status': jobStatus, + ...rest + } = attributes; + const result: Attributes = {}; + + for (const [key, value] of Object.entries(rest)) { + if (!DROPPED_ATTRIBUTES.has(key)) { + result[toMessagingKey(key)] = value; + } + } + + if (typeof queueName === 'string') { + result[MESSAGING_DESTINATION_NAME] = queueName; + } + + if (typeof operation === 'string') { + result[MESSAGING_OPERATION_NAME] = operation; + } + + if (jobId !== undefined) { + result[MESSAGING_MESSAGE_ID] = String(jobId); + } + + // BullMQ counts the attempt that just finished in `attemptsMade`. + if (typeof attemptsMade === 'number') { + result[MESSAGING_MESSAGE_RETRY_COUNT] = Math.max(attemptsMade - 1, 0); + } + + if (typeof bulkCount === 'number') { + result[MESSAGING_BATCH_MESSAGE_COUNT] = bulkCount; + } + + if (typeof workerId === 'string') { + result[MESSAGING_CLIENT_ID] = workerId; + } + + // BullMQ before 5.81.2 sets `bullmq.job.status` instead of `bullmq.job.state`. + const state = jobState ?? jobStatus; + if (typeof state === 'string') { + result[MESSAGING_BULLMQ_JOB_STATE] = state; + } + + return result; +} + +export class SentryBullMQSpan implements TelemetrySpan { + private _span: Span; + private _scope: Scope; + + public constructor(span: Span, scope: Scope) { + this._span = span; + this._scope = scope; + } + + public setAttribute(key: string, value: AttributeValue): void { + this.setAttributes({ [key]: value }); + } + + public setAttributes(attributes: Record): void { + this._span.setAttributes(toSentryAttributes(attributes)); + } + + public addEvent(name: string, attributes?: Record): void { + this._span.addEvent(name, attributes); + + // BullMQ sends this event from `Worker.handleFailed`, and the event name is not an exported constant: + // https://github.com/taskforcesh/bullmq/blob/01b8b14a973984845dc0bbcefccf35b3ed30782d/src/classes/worker.ts#L1223-L1225 + if (name === 'job failed') { + this._span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + + const reason = attributes?.['bullmq.job.failed.reason']; + const error = new Error(String(reason || 'Unknown error')); + // BullMQ passes only the error message (https://github.com/taskforcesh/bullmq/issues/4902), so this stack has only + // BullMQ and SDK frames, the same for every job. Without it, Sentry groups the errors by their message. + error.stack = undefined; + captureException(error, { + mechanism: { + handled: false, + type: 'auto.queue.bullmq', + }, + }); + } + } + + /** + * Does not capture to Sentry. BullMQ calls this for Redis, lock and serialization errors that it + * retries itself, often several times for one fault. Job failures are captured in `addEvent`. + */ + public recordException(exception: Error | string | { code?: number; message?: string; name?: string }): void { + const error = + exception instanceof Error + ? exception + : new Error(typeof exception === 'string' ? exception : exception.message || 'Unknown error'); + + this._span.recordException(error); + } + + public setSpanOnContext(context: unknown): unknown { + return { + ...(context as object), + span: this._span, + scope: this._scope, + }; + } + + public end(): void { + this._span.end(); + } +} diff --git a/packages/node/src/integrations/tracing/bullmq/tracer.ts b/packages/node/src/integrations/tracing/bullmq/tracer.ts new file mode 100644 index 000000000000..5722b013e451 --- /dev/null +++ b/packages/node/src/integrations/tracing/bullmq/tracer.ts @@ -0,0 +1,80 @@ +import { MESSAGING_SYSTEM, SENTRY_LINK_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { QUEUE, QUEUE_PROCESS, QUEUE_PUBLISH } from '@sentry/conventions/op'; +import type { SpanAttributes } from '@sentry/core'; +import { getCurrentScope, startInactiveSpan, startNewTrace } from '@sentry/core'; +import { SentryBullMQSpan, toSentryAttributes } from './span'; +import type { SpanOptions, TelemetrySpan, Tracer, SentryContext } from './types'; + +// BullMQ span names follow OTel messaging semconv: "{operation} {destination}" +// e.g. "add myQueue", "addBulk myQueue", "process myQueue", "addFlow myQueue" +const PRODUCER_OPERATIONS = new Set(['add', 'addbulk', 'addflow', 'addbulkflows']); +const CONSUMER_OPERATIONS = new Set(['process']); + +function getOperation(name: string): string { + return name.split(' ')[0]!.toLowerCase(); +} + +function getOpFromSpanName(name: string): string { + const operation = getOperation(name); + + if (CONSUMER_OPERATIONS.has(operation)) { + return QUEUE_PROCESS; + } + + if (PRODUCER_OPERATIONS.has(operation)) { + return QUEUE_PUBLISH; + } + + return QUEUE; +} + +function getOriginFromSpanName(name: string): string { + const operation = getOperation(name); + + if (CONSUMER_OPERATIONS.has(operation)) { + return 'auto.queue.bullmq.consumer'; + } + + if (PRODUCER_OPERATIONS.has(operation)) { + return 'auto.queue.bullmq.producer'; + } + + return 'auto.queue.bullmq'; +} + +export class SentryBullMQTracer implements Tracer { + public startSpan(name: string, options?: SpanOptions, context?: SentryContext): TelemetrySpan { + const op = getOpFromSpanName(name); + const origin = getOriginFromSpanName(name); + + const attributes: SpanAttributes = { + [SENTRY_OP]: op, + [SENTRY_ORIGIN]: origin, + [MESSAGING_SYSTEM]: 'bullmq', + ...(options?.attributes && toSentryAttributes(options.attributes)), + }; + + // Internal operations such as `moveStalledJobsToWait` run on timers. Without a parent, each tick would start a new root span. + const span = + op === QUEUE_PROCESS + ? startNewTrace(() => startInactiveSpan({ name, attributes })) + : startInactiveSpan({ name, attributes, onlyIfParent: op === QUEUE }); + + if (context?.producerSpanContext) { + const producerSpanCtx = { + traceId: context.producerSpanContext.traceId, + spanId: context.producerSpanContext.spanId, + traceFlags: context.producerSpanContext.sampled ? 1 : 0, + }; + + span.addLink({ + context: producerSpanCtx, + attributes: { + [SENTRY_LINK_TYPE]: 'message_origin', + }, + }); + } + + return new SentryBullMQSpan(span, getCurrentScope()); + } +} diff --git a/packages/node/src/integrations/tracing/bullmq/types.ts b/packages/node/src/integrations/tracing/bullmq/types.ts new file mode 100644 index 000000000000..81d2e96696ce --- /dev/null +++ b/packages/node/src/integrations/tracing/bullmq/types.ts @@ -0,0 +1,81 @@ +import type { Span } from '@opentelemetry/api'; +import type { Scope } from '@sentry/core'; + +/** + * Vendored from bullmq@5.79.1 — src/interfaces/telemetry.ts + * https://github.com/taskforcesh/bullmq/blob/master/src/interfaces/telemetry.ts + * + * Minimal subset of BullMQ's Telemetry interface so we don't depend on bullmq at runtime. + * + * SPDX-License-Identifier: MIT + * Copyright (c) 2018 BullForce Labs AB and contributors + */ + +export type AttributeValue = string | number | boolean | Array; + +export interface SpanOptions { + kind?: number; + attributes?: Record; + parent?: unknown; +} + +export interface TelemetrySpan { + setAttribute(key: string, value: AttributeValue): void; + setAttributes(attributes: Record): void; + addEvent(name: string, attributes?: Record): void; + recordException(exception: Error | string | { code?: number; message?: string; name?: string }): void; + setSpanOnContext(context: unknown): unknown; + end(): void; +} + +export interface Tracer { + startSpan(name: string, options?: SpanOptions, context?: Context): TelemetrySpan; +} + +export interface ContextManager { + active(): Context; + with unknown>(context: Context, fn: A): ReturnType; + // Added in bullmq@6.3.11 + root?(): Context; + getMetadata(context: Context): string; + fromMetadata(activeContext: Context, metadata: string): Context; +} + +export interface MetricOptions { + description?: string; + unit?: string; +} + +export interface Counter { + add(value: number, attributes?: Record): void; +} + +export interface Histogram { + record(value: number, attributes?: Record): void; +} + +export interface Gauge { + record(value: number, attributes?: Record): void; +} + +export interface Meter { + createCounter(name: string, options?: MetricOptions): Counter; + createHistogram(name: string, options?: MetricOptions): Histogram; + createGauge?(name: string, options?: MetricOptions): Gauge; +} + +export interface Telemetry { + tracer: Tracer; + contextManager: ContextManager; + meter?: Meter; +} + +export interface SentryContext { + span: Span | undefined; + scope: Scope; + producerSpanContext?: { + traceId: string; + spanId: string; + sampled: boolean; + }; +} diff --git a/packages/node/test/integrations/tracing/bullmq.test.ts b/packages/node/test/integrations/tracing/bullmq.test.ts new file mode 100644 index 000000000000..a4fec420645f --- /dev/null +++ b/packages/node/test/integrations/tracing/bullmq.test.ts @@ -0,0 +1,688 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as SentryCore from '@sentry/core'; +import { BullMQTelemetry } from '../../../src/integrations/tracing/bullmq'; + +vi.mock('@sentry/core', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + startInactiveSpan: vi.fn(() => mockOtelSpan()), + getActiveSpan: vi.fn(() => undefined), + getCurrentScope: vi.fn(() => mockScope()), + withActiveSpan: vi.fn((_span, fn) => fn()), + startNewTrace: vi.fn(fn => fn()), + withIsolationScope: vi.fn(fn => fn()), + spanToTraceHeader: vi.fn(() => '00-traceid-spanid-01'), + captureException: vi.fn(), + metrics: { + count: vi.fn(), + distribution: vi.fn(), + gauge: vi.fn(), + }, + }; +}); + +function mockOtelSpan() { + return { + setAttribute: vi.fn(), + setAttributes: vi.fn(), + addEvent: vi.fn(), + addLink: vi.fn(), + recordException: vi.fn(), + setStatus: vi.fn(), + end: vi.fn(), + spanContext: () => ({ spanId: 'abc123', traceId: 'def456', traceFlags: 1 }), + }; +} + +function mockScope() { + return { + setTag: vi.fn(), + setContext: vi.fn(), + }; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('BullMQTelemetry', () => { + it('does not create a meter by default', () => { + const telemetry = new BullMQTelemetry(); + + expect(telemetry.meter).toBeUndefined(); + }); + + it('creates a meter when enableMetrics is true', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + + expect(telemetry.meter).toBeDefined(); + }); +}); + +describe('SentryBullMQTracer', () => { + describe('startSpan', () => { + it.each([ + { name: 'add myQueue', expectedOp: 'queue.publish', expectedOrigin: 'auto.queue.bullmq.producer' }, + { name: 'addBulk myQueue', expectedOp: 'queue.publish', expectedOrigin: 'auto.queue.bullmq.producer' }, + { name: 'addFlow myQueue', expectedOp: 'queue.publish', expectedOrigin: 'auto.queue.bullmq.producer' }, + { name: 'addBulkFlows myQueue', expectedOp: 'queue.publish', expectedOrigin: 'auto.queue.bullmq.producer' }, + { name: 'pause myQueue', expectedOp: 'queue', expectedOrigin: 'auto.queue.bullmq' }, + { name: 'close myQueue', expectedOp: 'queue', expectedOrigin: 'auto.queue.bullmq' }, + { name: 'drain myQueue', expectedOp: 'queue', expectedOrigin: 'auto.queue.bullmq' }, + { name: 'getNextJob myQueue', expectedOp: 'queue', expectedOrigin: 'auto.queue.bullmq' }, + { name: 'moveStalledJobsToWait myQueue', expectedOp: 'queue', expectedOrigin: 'auto.queue.bullmq' }, + ])('maps "$name" to op=$expectedOp and origin=$expectedOrigin', ({ name, expectedOp, expectedOrigin }) => { + const telemetry = new BullMQTelemetry(); + + telemetry.tracer.startSpan(name); + + expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith({ + name, + attributes: { + 'sentry.op': expectedOp, + 'sentry.origin': expectedOrigin, + 'messaging.system': 'bullmq', + }, + onlyIfParent: expectedOp === 'queue', + }); + expect(SentryCore.startNewTrace).not.toHaveBeenCalled(); + }); + + it('maps "process myQueue" to op=queue.process and origin=auto.queue.bullmq.consumer', () => { + const telemetry = new BullMQTelemetry(); + + telemetry.tracer.startSpan('process myQueue'); + + expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith({ + name: 'process myQueue', + attributes: { + 'sentry.op': 'queue.process', + 'sentry.origin': 'auto.queue.bullmq.consumer', + 'messaging.system': 'bullmq', + }, + }); + }); + + it('merges attributes from SpanOptions and gives them messaging keys', () => { + const telemetry = new BullMQTelemetry(); + + telemetry.tracer.startSpan('add emails', { + attributes: { 'bullmq.queue.name': 'emails', 'bullmq.job.name': 'welcome-email' }, + }); + + expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith({ + name: 'add emails', + attributes: { + 'sentry.op': 'queue.publish', + 'sentry.origin': 'auto.queue.bullmq.producer', + 'messaging.system': 'bullmq', + 'messaging.destination.name': 'emails', + 'messaging.bullmq.job.name': 'welcome-email', + }, + onlyIfParent: false, + }); + }); + + it('starts each queue.process span in a new trace', () => { + const telemetry = new BullMQTelemetry(); + + telemetry.tracer.startSpan('process notifications'); + + expect(SentryCore.startNewTrace).toHaveBeenCalledOnce(); + }); + + it('adds span link to producer when context has producerSpanContext', () => { + const telemetry = new BullMQTelemetry(); + const otelSpan = mockOtelSpan(); + vi.mocked(SentryCore.startInactiveSpan).mockReturnValueOnce(otelSpan as any); + + telemetry.tracer.startSpan('process myQueue', undefined, { + span: undefined, + scope: mockScope() as any, + producerSpanContext: { + traceId: 'aabbccddaabbccddaabbccddaabbccdd', + spanId: '1122334455667788', + sampled: true, + }, + }); + + expect(otelSpan.addLink).toHaveBeenCalledWith({ + context: { + traceId: 'aabbccddaabbccddaabbccddaabbccdd', + spanId: '1122334455667788', + traceFlags: 1, + }, + attributes: { + 'sentry.link.type': 'message_origin', + }, + }); + }); + + it('adds the span link with traceFlags 0 when the producer span was not sampled', () => { + const telemetry = new BullMQTelemetry(); + const otelSpan = mockOtelSpan(); + vi.mocked(SentryCore.startInactiveSpan).mockReturnValueOnce(otelSpan as any); + + telemetry.tracer.startSpan('process myQueue', undefined, { + span: undefined, + scope: mockScope() as any, + producerSpanContext: { + traceId: 'aabbccddaabbccddaabbccddaabbccdd', + spanId: '1122334455667788', + sampled: false, + }, + }); + + expect(otelSpan.addLink).toHaveBeenCalledWith({ + context: { + traceId: 'aabbccddaabbccddaabbccddaabbccdd', + spanId: '1122334455667788', + traceFlags: 0, + }, + attributes: { + 'sentry.link.type': 'message_origin', + }, + }); + }); + + it('does not add span link when context has no producerSpanContext', () => { + const telemetry = new BullMQTelemetry(); + const otelSpan = mockOtelSpan(); + vi.mocked(SentryCore.startInactiveSpan).mockReturnValueOnce(otelSpan as any); + + telemetry.tracer.startSpan('process myQueue', undefined, { + span: undefined, + scope: mockScope() as any, + }); + + expect(otelSpan.addLink).not.toHaveBeenCalled(); + }); + }); +}); + +describe('SentryBullMQSpan', () => { + function createSpan() { + const telemetry = new BullMQTelemetry(); + const otelSpan = mockOtelSpan(); + const scope = mockScope(); + vi.mocked(SentryCore.startInactiveSpan).mockReturnValueOnce(otelSpan as any); + vi.mocked(SentryCore.getCurrentScope).mockReturnValueOnce(scope as any); + const span = telemetry.tracer.startSpan('process emails'); + return { span, otelSpan, scope }; + } + + it('replaces the BullMQ attributes that have a messaging convention and prefixes the others', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ + 'bullmq.queue.name': 'emails', + 'bullmq.queue.operation': 'process', + 'bullmq.job.id': '42', + 'bullmq.job.attempts.made': 3, + 'bullmq.job.bulk.count': 5, + 'bullmq.worker.id': '03327d57-05cc-4528-9d24-c59bf6520039', + 'bullmq.job.name': 'welcome-email', + 'bullmq.worker.name': 'mailer', + }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.bullmq.job.name': 'welcome-email', + 'messaging.bullmq.worker.name': 'mailer', + 'messaging.destination.name': 'emails', + 'messaging.operation.name': 'process', + 'messaging.message.id': '42', + 'messaging.message.retry.count': 2, + 'messaging.batch.message_count': 5, + 'messaging.client.id': '03327d57-05cc-4528-9d24-c59bf6520039', + }); + }); + + it('replaces the queue name when BullMQ sets it with setAttribute', () => { + const { span, otelSpan } = createSpan(); + + span.setAttribute('bullmq.queue.name', 'emails'); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.destination.name': 'emails', + }); + }); + + it('sets a retry count of 0 for the first attempt', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ 'bullmq.job.attempts.made': 1 }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.message.retry.count': 0, + }); + }); + + it('leaves out the BullMQ attributes that hold JSON strings', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ + 'bullmq.job.id': '42', + 'bullmq.job.options': '{"removeChildren":true}', + 'bullmq.job.progress': '{"percentage":50}', + 'bullmq.queue.options': '{"count":10}', + 'bullmq.worker.options': '{"block":false}', + }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.message.id': '42', + }); + }); + + it('uses snake_case for the words of multi-word BullMQ keys', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ + 'bullmq.job.processed.timestamp': 1, + 'bullmq.queue.event.max.length': 2, + 'bullmq.queue.rate.limit': 3, + 'bullmq.worker.do.not.wait.active': 4, + 'bullmq.worker.force.close': 5, + 'bullmq.worker.jobs.to.extend.locks': 6, + 'bullmq.worker.rate.limit': 7, + 'bullmq.worker.stalled.jobs': 8, + }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.bullmq.job.processed_timestamp': 1, + 'messaging.bullmq.queue.event.max_length': 2, + 'messaging.bullmq.queue.rate_limit': 3, + 'messaging.bullmq.worker.do_not_wait_active': 4, + 'messaging.bullmq.worker.force_close': 5, + 'messaging.bullmq.worker.jobs_to_extend_locks': 6, + 'messaging.bullmq.worker.rate_limit': 7, + 'messaging.bullmq.worker.stalled_jobs': 8, + }); + }); + + it('leaves out the deprecated bullmq.job.finished.timestamp', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ + 'bullmq.job.finished.timestamp': 1791184722000, + 'bullmq.job.attempt_finished_timestamp': 1791184722000, + }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.bullmq.job.attempt_finished_timestamp': 1791184722000, + }); + }); + + it('sends bullmq.job.status as messaging.bullmq.job.state', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ 'bullmq.job.status': 'completed' }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.bullmq.job.state': 'completed', + }); + }); + + it('prefers bullmq.job.state when BullMQ sets it together with bullmq.job.status', () => { + const { span, otelSpan } = createSpan(); + + span.setAttributes({ 'bullmq.job.state': 'failed', 'bullmq.job.status': 'completed' }); + + expect(otelSpan.setAttributes).toHaveBeenCalledWith({ + 'messaging.bullmq.job.state': 'failed', + }); + }); + + it('passes events to the OTel span without renaming their attributes', () => { + const { span, otelSpan } = createSpan(); + + span.addEvent('job completed', { 'bullmq.job.result': '{"sent":true}' }); + + expect(otelSpan.addEvent).toHaveBeenCalledWith('job completed', { 'bullmq.job.result': '{"sent":true}' }); + }); + + describe('recordException', () => { + it('records Error instances on the OTel span without capturing them', () => { + const { span, otelSpan } = createSpan(); + const error = new Error('Redis connection refused'); + + span.recordException(error); + + expect(otelSpan.recordException).toHaveBeenCalledWith(error); + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + + it('wraps non-Error exception objects in an Error', () => { + const { span, otelSpan } = createSpan(); + + span.recordException({ code: 503, message: 'Service unavailable' }); + + expect(otelSpan.recordException).toHaveBeenCalledWith(new Error('Service unavailable')); + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + + it('wraps string exceptions in an Error', () => { + const { span, otelSpan } = createSpan(); + + (span as any).recordException('Connection timed out'); + + expect(otelSpan.recordException).toHaveBeenCalledWith(new Error('Connection timed out')); + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + + it('uses fallback message when non-Error exception has no message', () => { + const { span, otelSpan } = createSpan(); + + span.recordException({ code: 500 }); + + expect(otelSpan.recordException).toHaveBeenCalledWith(new Error('Unknown error')); + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + }); + + describe('addEvent', () => { + it('captures exception when event name is "job failed"', () => { + const { span } = createSpan(); + + span.addEvent('job failed', { 'bullmq.job.failed.reason': 'Connection refused' }); + + expect(SentryCore.captureException).toHaveBeenCalledWith(new Error('Connection refused'), { + mechanism: { handled: false, type: 'auto.queue.bullmq' }, + }); + }); + + it('sets the error status on the span when the event name is "job failed"', () => { + const { span, otelSpan } = createSpan(); + + span.addEvent('job failed', { 'bullmq.job.failed.reason': 'Connection refused' }); + + expect(otelSpan.setStatus).toHaveBeenCalledWith({ + code: SentryCore.SPAN_STATUS_ERROR, + message: 'internal_error', + }); + }); + + it('does not capture an exception or set an error status for other event names', () => { + const { span, otelSpan } = createSpan(); + + span.addEvent('job completed', { 'bullmq.job.result': '{"ok":true}' }); + + expect(SentryCore.captureException).not.toHaveBeenCalled(); + expect(otelSpan.setStatus).not.toHaveBeenCalled(); + }); + }); + + it('returns context with span and scope from setSpanOnContext', () => { + const { span, otelSpan, scope } = createSpan(); + const inputContext = { existingKey: 'value' }; + + const result = span.setSpanOnContext(inputContext); + + expect(result).toEqual({ existingKey: 'value', span: otelSpan, scope }); + }); + + it('delegates end to the underlying OTel span', () => { + const { span, otelSpan } = createSpan(); + + span.end(); + + expect(otelSpan.end).toHaveBeenCalledOnce(); + }); +}); + +describe('SentryBullMQContextManager', () => { + it('returns current active span and scope from active()', () => { + const telemetry = new BullMQTelemetry(); + const activeSpan = mockOtelSpan(); + const scope = mockScope(); + vi.mocked(SentryCore.getActiveSpan).mockReturnValueOnce(activeSpan as any); + vi.mocked(SentryCore.getCurrentScope).mockReturnValueOnce(scope as any); + + const context = telemetry.contextManager.active(); + + expect(context).toEqual({ span: activeSpan, scope }); + }); + + describe('with', () => { + it('calls withActiveSpan inside withIsolationScope when context has a span', () => { + const telemetry = new BullMQTelemetry(); + const fakeSpan = mockOtelSpan(); + const context = { span: fakeSpan, scope: mockScope() }; + const fn = vi.fn(() => 'result'); + + const result = telemetry.contextManager.with(context as any, fn); + + expect(result).toBe('result'); + expect(SentryCore.withIsolationScope).toHaveBeenCalledOnce(); + expect(SentryCore.withActiveSpan).toHaveBeenCalledWith(fakeSpan, fn); + }); + + it('clears the active span inside withIsolationScope when context has no span', () => { + const telemetry = new BullMQTelemetry(); + const context = { span: undefined, scope: mockScope() }; + const fn = vi.fn(() => 'result'); + + const result = telemetry.contextManager.with(context as any, fn); + + expect(result).toBe('result'); + expect(SentryCore.withIsolationScope).toHaveBeenCalledOnce(); + expect(SentryCore.withActiveSpan).toHaveBeenCalledWith(null, fn); + }); + }); + + describe('root', () => { + it('returns a context without a span that runs callbacks without an active span', () => { + const telemetry = new BullMQTelemetry(); + const fn = vi.fn(() => 'result'); + + const context = telemetry.contextManager.root!(); + const result = telemetry.contextManager.with(context, fn); + + expect(context.span).toBeUndefined(); + expect(result).toBe('result'); + expect(SentryCore.withActiveSpan).toHaveBeenCalledWith(null, fn); + }); + }); + + describe('getMetadata', () => { + it('returns sentry-trace header when context has a span', () => { + const telemetry = new BullMQTelemetry(); + const fakeSpan = mockOtelSpan(); + const context = { span: fakeSpan, scope: mockScope() }; + + const metadata = telemetry.contextManager.getMetadata(context as any); + + expect(SentryCore.spanToTraceHeader).toHaveBeenCalledWith(fakeSpan); + expect(metadata).toBe('00-traceid-spanid-01'); + }); + + it('returns empty string when context has no span', () => { + const telemetry = new BullMQTelemetry(); + const context = { span: undefined, scope: mockScope() }; + + const metadata = telemetry.contextManager.getMetadata(context as any); + + expect(metadata).toBe(''); + expect(SentryCore.spanToTraceHeader).not.toHaveBeenCalled(); + }); + }); + + describe('fromMetadata', () => { + it('parses sentry-trace header and attaches producerSpanContext', () => { + const telemetry = new BullMQTelemetry(); + const fakeSpan = mockOtelSpan(); + const context = { span: fakeSpan, scope: mockScope() }; + + const result = telemetry.contextManager.fromMetadata( + context as any, + 'aabbccddaabbccddaabbccddaabbccdd-1122334455667788-1', + ); + + expect(result).toEqual({ + span: fakeSpan, + scope: context.scope, + producerSpanContext: { + traceId: 'aabbccddaabbccddaabbccddaabbccdd', + spanId: '1122334455667788', + sampled: true, + }, + }); + }); + + it('marks the producer span context as not sampled when the sentry-trace header has no sampled flag', () => { + const telemetry = new BullMQTelemetry(); + const context = { span: undefined, scope: mockScope() }; + + const result = telemetry.contextManager.fromMetadata( + context as any, + 'aabbccddaabbccddaabbccddaabbccdd-1122334455667788', + ); + + expect(result.producerSpanContext).toEqual({ + traceId: 'aabbccddaabbccddaabbccddaabbccdd', + spanId: '1122334455667788', + sampled: false, + }); + }); + + it('returns the active context unchanged when metadata is empty', () => { + const telemetry = new BullMQTelemetry(); + const context = { span: undefined, scope: mockScope() }; + + const result = telemetry.contextManager.fromMetadata(context as any, ''); + + expect(result).toBe(context); + }); + + it('returns the active context unchanged when metadata is invalid', () => { + const telemetry = new BullMQTelemetry(); + const context = { span: undefined, scope: mockScope() }; + + const result = telemetry.contextManager.fromMetadata(context as any, 'not-a-valid-header'); + + expect(result).toBe(context); + }); + }); +}); + +describe('SentryBullMQMeter', () => { + describe('counter', () => { + it('delegates add to metrics.count with name and unit', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const counter = telemetry.meter!.createCounter('bullmq.jobs.completed', { unit: '1' }); + + counter.add(5, { 'queue.name': 'emails' }); + + expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 5, { + unit: 'none', + attributes: { 'queue.name': 'emails' }, + }); + }); + + it('passes undefined attributes when none provided', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const counter = telemetry.meter!.createCounter('bullmq.jobs.failed'); + + counter.add(1); + + expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.failed', 1, { + unit: undefined, + attributes: undefined, + }); + }); + }); + + describe('histogram', () => { + it('records bullmq.job.duration as messaging.process.duration in seconds', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const histogram = telemetry.meter!.createHistogram('bullmq.job.duration', { unit: 'ms' }); + + histogram.record(1500, { 'bullmq.queue.name': 'notifications' }); + + expect(SentryCore.metrics.distribution).toHaveBeenCalledWith('messaging.process.duration', 1.5, { + unit: 'second', + attributes: { + 'messaging.destination.name': 'notifications', + 'messaging.system': 'bullmq', + 'messaging.operation.name': 'process', + }, + }); + }); + + it('keeps a unit that is already a Sentry unit', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const histogram = telemetry.meter!.createHistogram('bullmq.job.payload', { unit: 'byte' }); + + histogram.record(512); + + expect(SentryCore.metrics.distribution).toHaveBeenCalledWith('messaging.bullmq.job.payload', 512, { + unit: 'byte', + attributes: undefined, + }); + }); + }); + + describe('gauge', () => { + it('delegates record to metrics.gauge', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const gauge = telemetry.meter!.createGauge!('bullmq.queue.jobs', { unit: '{jobs}' }); + + gauge.record(37, { 'queue.name': 'reports' }); + + expect(SentryCore.metrics.gauge).toHaveBeenCalledWith('messaging.bullmq.queue.jobs', 37, { + unit: 'none', + attributes: { 'queue.name': 'reports' }, + }); + }); + }); + + describe('attribute filtering', () => { + it('replaces the BullMQ queue name with the messaging destination', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const counter = telemetry.meter!.createCounter('bullmq.jobs.completed'); + + counter.add(1, { 'bullmq.queue.name': 'emails', 'bullmq.job.name': 'welcome-email' }); + + expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 1, { + unit: undefined, + attributes: { + 'messaging.bullmq.job.name': 'welcome-email', + 'messaging.destination.name': 'emails', + }, + }); + }); + + it('filters out array attribute values', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const counter = telemetry.meter!.createCounter('bullmq.jobs.completed'); + + counter.add(1, { + 'queue.name': 'emails', + tags: ['urgent', 'retry'], + priority: 5, + enabled: true, + }); + + expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 1, { + unit: undefined, + attributes: { + 'queue.name': 'emails', + priority: 5, + enabled: true, + }, + }); + }); + + it('returns empty object when all attributes are arrays', () => { + const telemetry = new BullMQTelemetry({ enableMetrics: true }); + const counter = telemetry.meter!.createCounter('bullmq.jobs.completed'); + + counter.add(1, { tags: ['a', 'b'] }); + + expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 1, { + unit: undefined, + attributes: {}, + }); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01bc7f56e472..675cd618f3dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -694,6 +694,9 @@ importers: body-parser: specifier: ^2.3.0 version: 2.3.0(supports-color@10.0.0) + bullmq: + specifier: ^5.79.1 + version: 5.81.5(redis@5.12.1(@opentelemetry/api@1.9.0))(supports-color@10.0.0) consola: specifier: ^3.2.3 version: 3.4.2 @@ -738,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 +1721,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 +1817,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 +1832,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 +1841,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 +2309,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 +2330,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) @@ -6112,6 +6115,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'} @@ -10354,6 +10387,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==} @@ -13175,8 +13217,8 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ioredis@5.11.0: - resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} ioredis@6.0.0: @@ -14719,6 +14761,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==} @@ -14928,6 +14977,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==} @@ -14955,6 +15007,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 @@ -24242,6 +24298,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 @@ -24486,7 +24560,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 @@ -24516,7 +24590,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)) @@ -24682,7 +24756,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) @@ -24700,7 +24774,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 @@ -24708,7 +24782,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 @@ -24756,7 +24830,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) @@ -24774,7 +24848,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 @@ -24782,7 +24856,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 @@ -24851,7 +24925,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) @@ -24871,7 +24945,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 @@ -26659,11 +26733,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 @@ -26675,7 +26749,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' @@ -27616,7 +27690,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 @@ -27627,18 +27701,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: @@ -29110,6 +29184,19 @@ snapshots: dependencies: semver: 7.8.5 + bullmq@5.81.5(redis@5.12.1(@opentelemetry/api@1.9.0))(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.0) + transitivePeerDependencies: + - supports-color + bun-types@1.2.9: dependencies: '@types/node': 20.19.43 @@ -32589,7 +32676,7 @@ snapshots: transitivePeerDependencies: - supports-color - ioredis@5.11.0(supports-color@10.0.0): + ioredis@5.11.1(supports-color@10.0.0): dependencies: '@ioredis/commands': 1.10.0 cluster-key-slot: 1.1.1 @@ -34382,6 +34469,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: @@ -34545,7 +34648,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) @@ -34560,7 +34663,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 @@ -34598,7 +34701,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) @@ -34613,7 +34716,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 @@ -34997,6 +35100,8 @@ snapshots: dependencies: semver: 7.8.5 + node-abort-controller@3.1.1: {} + node-addon-api@3.2.1: optional: true @@ -35014,6 +35119,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): @@ -35179,16 +35289,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) @@ -39518,7 +39628,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 @@ -39531,7 +39641,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: @@ -39543,22 +39653,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 @@ -39679,7 +39789,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)) @@ -39712,7 +39822,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: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3359607240fb..4855dbe48d3b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -108,6 +108,7 @@ allowBuilds: core-js-pure: false esbuild: true mongodb-memory-server-global: true + msgpackr-extract: true nice-napi: true nx: true prisma: true