diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs index 903470806ad9..298952d58ced 100644 --- a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs @@ -9,6 +9,7 @@ const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })], transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/child-process/child.js b/dev-packages/node-integration-tests/suites/child-process/child.js new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/child.js @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/child-process/child.mjs b/dev-packages/node-integration-tests/suites/child-process/child.mjs new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/child.mjs @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.js b/dev-packages/node-integration-tests/suites/child-process/fork.js new file mode 100644 index 000000000000..c6e5cd3f0b7f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/fork.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const path = require('path'); +const { fork } = require('child_process'); + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// eslint-disable-next-line no-unused-vars +const _child = fork(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + throw new Error('Exiting main process'); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.mjs b/dev-packages/node-integration-tests/suites/child-process/fork.mjs new file mode 100644 index 000000000000..88503fa887a9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/fork.mjs @@ -0,0 +1,19 @@ +import { fork } from 'child_process'; +import * as path from 'path'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +const __dirname = new URL('.', import.meta.url).pathname; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +const _child = fork(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + throw new Error('Exiting main process'); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/child-process/test.ts b/dev-packages/node-integration-tests/suites/child-process/test.ts new file mode 100644 index 000000000000..9b9064dacf3e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/test.ts @@ -0,0 +1,65 @@ +import type { Event } from '@sentry/core'; +import { conditionalTest } from '../../utils'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +const WORKER_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Test error', + mechanism: { + type: 'instrument', + handled: false, + data: { + threadId: expect.any(String), + }, + }, + }, + ], + }, +}; + +const CHILD_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Exiting main process', + }, + ], + }, + breadcrumbs: [ + { + category: 'child_process', + message: "Child process exited with code '1'", + level: 'warning', + }, + ], +}; + +describe('should capture child process events', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + conditionalTest({ min: 20 })('worker', () => { + test('ESM', done => { + createRunner(__dirname, 'worker.mjs').expect({ event: WORKER_EVENT }).start(done); + }); + + test('CJS', done => { + createRunner(__dirname, 'worker.js').expect({ event: WORKER_EVENT }).start(done); + }); + }); + + conditionalTest({ min: 20 })('fork', () => { + test('ESM', done => { + createRunner(__dirname, 'fork.mjs').expect({ event: CHILD_EVENT }).start(done); + }); + + test('CJS', done => { + createRunner(__dirname, 'fork.js').expect({ event: CHILD_EVENT }).start(done); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.js b/dev-packages/node-integration-tests/suites/child-process/worker.js new file mode 100644 index 000000000000..99b645d9001c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/worker.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const path = require('path'); +const { Worker } = require('worker_threads'); + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// eslint-disable-next-line no-unused-vars +const _worker = new Worker(path.join(__dirname, 'child.js')); + +setTimeout(() => { + process.exit(); +}, 3000); diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.mjs b/dev-packages/node-integration-tests/suites/child-process/worker.mjs new file mode 100644 index 000000000000..dcca0bcc4105 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/child-process/worker.mjs @@ -0,0 +1,19 @@ +import * as path from 'path'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; +import { Worker } from 'worker_threads'; + +const __dirname = new URL('.', import.meta.url).pathname; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +const _worker = new Worker(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + process.exit(); +}, 3000); diff --git a/packages/node/src/integrations/childProcess.ts b/packages/node/src/integrations/childProcess.ts index 934bac99dc42..6fc6046c7e2f 100644 --- a/packages/node/src/integrations/childProcess.ts +++ b/packages/node/src/integrations/childProcess.ts @@ -1,7 +1,7 @@ import type { ChildProcess } from 'node:child_process'; import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { Worker } from 'node:worker_threads'; -import { addBreadcrumb, defineIntegration } from '@sentry/core'; +import { addBreadcrumb, captureException, defineIntegration } from '@sentry/core'; interface Options { /** @@ -10,17 +10,24 @@ interface Options { * @default false */ includeChildProcessArgs?: boolean; + + /** + * Whether to capture errors from worker threads. + * + * @default true + */ + captureWorkerErrors?: boolean; } const INTEGRATION_NAME = 'ChildProcess'; /** - * Capture breadcrumbs for child processes and worker threads. + * Capture breadcrumbs and events for child processes and worker threads. */ export const childProcessIntegration = defineIntegration((options: Options = {}) => { return { name: INTEGRATION_NAME, - setup(_client) { + setup() { diagnosticsChannel.channel('child_process').subscribe((event: unknown) => { if (event && typeof event === 'object' && 'process' in event) { captureChildProcessEvents(event.process as ChildProcess, options); @@ -29,7 +36,7 @@ export const childProcessIntegration = defineIntegration((options: Options = {}) diagnosticsChannel.channel('worker_threads').subscribe((event: unknown) => { if (event && typeof event === 'object' && 'worker' in event) { - captureWorkerThreadEvents(event.worker as Worker); + captureWorkerThreadEvents(event.worker as Worker, options); } }); }, @@ -62,7 +69,7 @@ function captureChildProcessEvents(child: ChildProcess, options: Options): void addBreadcrumb({ category: 'child_process', message: `Child process exited with code '${code}'`, - level: 'warning', + level: code === 0 ? 'info' : 'warning', data, }); } @@ -82,7 +89,7 @@ function captureChildProcessEvents(child: ChildProcess, options: Options): void }); } -function captureWorkerThreadEvents(worker: Worker): void { +function captureWorkerThreadEvents(worker: Worker, options: Options): void { let threadId: number | undefined; worker @@ -90,11 +97,17 @@ function captureWorkerThreadEvents(worker: Worker): void { threadId = worker.threadId; }) .on('error', error => { - addBreadcrumb({ - category: 'worker_thread', - message: `Worker thread errored with '${error.message}'`, - level: 'error', - data: { threadId }, - }); + if (options.captureWorkerErrors !== false) { + captureException(error, { + mechanism: { type: 'instrument', handled: false, data: { threadId: String(threadId) } }, + }); + } else { + addBreadcrumb({ + category: 'worker_thread', + message: `Worker thread errored with '${error.message}'`, + level: 'error', + data: { threadId }, + }); + } }); }