diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/base/scenario.ts similarity index 93% rename from dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts rename to dev-packages/node-core-integration-tests/suites/cron/node-cron/base/scenario.ts index 818bf7b63871..0cfa7d79c135 100644 --- a/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts +++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/base/scenario.ts @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/node-core'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as cron from 'node-cron'; -import { setupOtel } from '../../../utils/setupOtel'; +import { setupOtel } from '../../../../utils/setupOtel'; const client = Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/base/test.ts similarity index 96% rename from dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts rename to dev-packages/node-core-integration-tests/suites/cron/node-cron/base/test.ts index dcdb1ba4c4d9..6935fb289b16 100644 --- a/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts +++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/base/test.ts @@ -1,5 +1,5 @@ import { afterAll, expect, test } from 'vitest'; -import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts new file mode 100644 index 000000000000..e06814477bf5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts @@ -0,0 +1,59 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as cron from 'node-cron'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron, { isolateTrace: true }); + +let closeNext1 = false; +let closeNext2 = false; + +const task = cronWithCheckIn.schedule( + '* * * * * *', + () => { + if (closeNext1) { + // https://github.com/node-cron/node-cron/issues/317 + setImmediate(() => { + task.stop(); + }); + + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext1 = true; + }, + { name: 'my-cron-job' }, +); + +const task2 = cronWithCheckIn.schedule( + '* * * * * *', + () => { + if (closeNext2) { + // https://github.com/node-cron/node-cron/issues/317 + setImmediate(() => { + task2.stop(); + }); + + throw new Error('Error in cron job 2'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext2 = true; + }, + { name: 'my-2nd-cron-job' }, +); + +setTimeout(() => { + process.exit(); +}, 5000); diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/isolateTrace/test.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/isolateTrace/test.ts new file mode 100644 index 000000000000..cf469d2e6acd --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/isolateTrace/test.ts @@ -0,0 +1,49 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('node-cron instrumentation with isolateTrace creates distinct traces for each cron job', async () => { + let firstErrorTraceId: string | undefined; + + await createRunner(__dirname, 'scenario.ts') + .ignore('check_in') + .expect({ + event: event => { + const traceId = event.contexts?.trace?.trace_id; + const spanId = event.contexts?.trace?.span_id; + + expect(traceId).toMatch(/[a-f\d]{32}/); + expect(spanId).toMatch(/[a-f\d]{16}/); + + firstErrorTraceId = traceId; + + expect(event.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: expect.stringMatching(/^Error in cron job( 2)?$/), + mechanism: { type: 'auto.function.node-cron.instrumentNodeCron', handled: false }, + }); + }, + }) + .expect({ + event: event => { + const traceId = event.contexts?.trace?.trace_id; + const spanId = event.contexts?.trace?.span_id; + + expect(traceId).toMatch(/[a-f\d]{32}/); + expect(spanId).toMatch(/[a-f\d]{16}/); + + expect(traceId).not.toBe(firstErrorTraceId); + + expect(event.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: expect.stringMatching(/^Error in cron job( 2)?$/), + mechanism: { type: 'auto.function.node-cron.instrumentNodeCron', handled: false }, + }); + }, + }) + .start() + .completed(); +}); diff --git a/packages/node-core/src/cron/node-cron.ts b/packages/node-core/src/cron/node-cron.ts index a2374b06d4b5..2652cbaa8bc0 100644 --- a/packages/node-core/src/cron/node-cron.ts +++ b/packages/node-core/src/cron/node-cron.ts @@ -1,4 +1,4 @@ -import { captureException, withMonitor } from '@sentry/core'; +import { captureException, type MonitorConfig, withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export interface NodeCronOptions { @@ -28,7 +28,10 @@ export interface NodeCron { * ); * ``` */ -export function instrumentNodeCron(lib: Partial & T): T { +export function instrumentNodeCron( + lib: Partial & T, + monitorConfig: Pick = {}, +): T { return new Proxy(lib, { get(target, prop) { if (prop === 'schedule' && target.schedule) { @@ -65,6 +68,7 @@ export function instrumentNodeCron(lib: Partial & T): T { { schedule: { type: 'crontab', value: replaceCronNames(expression) }, timezone, + ...monitorConfig, }, ); };