Skip to content

Commit cbd1fc7

Browse files
committed
feat(node): Capture exceptions from worker threads
1 parent 82620ec commit cbd1fc7

File tree

8 files changed

+166
-11
lines changed

8 files changed

+166
-11
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setTimeout(() => {
2+
throw new Error('Test error');
3+
}, 1000);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setTimeout(() => {
2+
throw new Error('Test error');
3+
}, 1000);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const Sentry = require('@sentry/node');
2+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
3+
const path = require('path');
4+
const { fork } = require('child_process');
5+
6+
Sentry.init({
7+
debug: true,
8+
dsn: 'https://[email protected]/1337',
9+
release: '1.0',
10+
integrations: [Sentry.childProcessIntegration()],
11+
transport: loggingTransport,
12+
});
13+
14+
// eslint-disable-next-line no-unused-vars
15+
const _child = fork(path.join(__dirname, 'child.mjs'));
16+
17+
setTimeout(() => {
18+
throw new Error('Exiting main process');
19+
}, 3000);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import * as path from 'path';
4+
import { fork } from 'child_process';
5+
6+
const __dirname = new URL('.', import.meta.url).pathname;
7+
8+
Sentry.init({
9+
debug: true,
10+
dsn: 'https://[email protected]/1337',
11+
release: '1.0',
12+
integrations: [Sentry.childProcessIntegration()],
13+
transport: loggingTransport,
14+
});
15+
16+
const _child = fork(path.join(__dirname, 'child.mjs'));
17+
18+
setTimeout(() => {
19+
throw new Error('Exiting main process');
20+
}, 3000);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Event } from '@sentry/core';
2+
import { conditionalTest } from '../../utils';
3+
import { cleanupChildProcesses, createRunner } from '../../utils/runner';
4+
5+
const WORKER_EVENT: Event = {
6+
exception: {
7+
values: [
8+
{
9+
type: 'Error',
10+
value: 'Test error',
11+
},
12+
],
13+
},
14+
};
15+
16+
const CHILD_EVENT: Event = {
17+
exception: {
18+
values: [
19+
{
20+
type: 'Error',
21+
value: 'Exiting main process',
22+
},
23+
],
24+
},
25+
breadcrumbs: [
26+
{
27+
category: 'child_process',
28+
message: "Child process exited with code '1'",
29+
level: 'info',
30+
},
31+
],
32+
};
33+
34+
describe('should capture child process events', () => {
35+
afterAll(() => {
36+
cleanupChildProcesses();
37+
});
38+
39+
describe('worker', () => {
40+
test('ESM', done => {
41+
createRunner(__dirname, 'worker.mjs').expect({ event: WORKER_EVENT }).start(done);
42+
});
43+
44+
test('CJS', done => {
45+
createRunner(__dirname, 'worker.js').expect({ event: WORKER_EVENT }).start(done);
46+
});
47+
});
48+
49+
conditionalTest({ min: 20 })('fork', () => {
50+
test('ESM', done => {
51+
createRunner(__dirname, 'fork.mjs').expect({ event: CHILD_EVENT }).start(done);
52+
});
53+
54+
test('CJS', done => {
55+
createRunner(__dirname, 'fork.js').expect({ event: CHILD_EVENT }).start(done);
56+
});
57+
});
58+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const Sentry = require('@sentry/node');
2+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
3+
const path = require('path');
4+
const { Worker } = require('worker_threads');
5+
6+
Sentry.init({
7+
debug: true,
8+
dsn: 'https://[email protected]/1337',
9+
release: '1.0',
10+
integrations: [Sentry.childProcessIntegration()],
11+
transport: loggingTransport,
12+
});
13+
14+
// eslint-disable-next-line no-unused-vars
15+
const _worker = new Worker(path.join(__dirname, 'child.js'));
16+
17+
setTimeout(() => {
18+
process.exit();
19+
}, 3000);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import * as path from 'path';
4+
import { Worker } from 'worker_threads';
5+
6+
const __dirname = new URL('.', import.meta.url).pathname;
7+
8+
Sentry.init({
9+
debug: true,
10+
dsn: 'https://[email protected]/1337',
11+
release: '1.0',
12+
integrations: [Sentry.childProcessIntegration()],
13+
transport: loggingTransport,
14+
});
15+
16+
const _worker = new Worker(path.join(__dirname, 'child.mjs'));
17+
18+
setTimeout(() => {
19+
process.exit();
20+
}, 3000);

packages/node/src/integrations/childProcess.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ChildProcess } from 'node:child_process';
22
import * as diagnosticsChannel from 'node:diagnostics_channel';
33
import type { Worker } from 'node:worker_threads';
4-
import { addBreadcrumb, defineIntegration } from '@sentry/core';
4+
import { addBreadcrumb, captureException, defineIntegration } from '@sentry/core';
55

66
interface Options {
77
/**
@@ -10,6 +10,13 @@ interface Options {
1010
* @default false
1111
*/
1212
includeChildProcessArgs?: boolean;
13+
14+
/**
15+
* Whether to capture errors from worker threads.
16+
*
17+
* @default true
18+
*/
19+
captureWorkerErrors?: boolean;
1320
}
1421

1522
const INTEGRATION_NAME = 'ChildProcess';
@@ -20,7 +27,7 @@ const INTEGRATION_NAME = 'ChildProcess';
2027
export const childProcessIntegration = defineIntegration((options: Options = {}) => {
2128
return {
2229
name: INTEGRATION_NAME,
23-
setup(_client) {
30+
setup() {
2431
diagnosticsChannel.channel('child_process').subscribe((event: unknown) => {
2532
if (event && typeof event === 'object' && 'process' in event) {
2633
captureChildProcessEvents(event.process as ChildProcess, options);
@@ -29,7 +36,7 @@ export const childProcessIntegration = defineIntegration((options: Options = {})
2936

3037
diagnosticsChannel.channel('worker_threads').subscribe((event: unknown) => {
3138
if (event && typeof event === 'object' && 'worker' in event) {
32-
captureWorkerThreadEvents(event.worker as Worker);
39+
captureWorkerThreadEvents(event.worker as Worker, options);
3340
}
3441
});
3542
},
@@ -62,7 +69,7 @@ function captureChildProcessEvents(child: ChildProcess, options: Options): void
6269
addBreadcrumb({
6370
category: 'child_process',
6471
message: `Child process exited with code '${code}'`,
65-
level: 'warning',
72+
level: code === 0 ? 'warning' : 'info',
6673
data,
6774
});
6875
}
@@ -82,19 +89,25 @@ function captureChildProcessEvents(child: ChildProcess, options: Options): void
8289
});
8390
}
8491

85-
function captureWorkerThreadEvents(worker: Worker): void {
92+
function captureWorkerThreadEvents(worker: Worker, options: Options): void {
8693
let threadId: number | undefined;
8794

8895
worker
8996
.on('online', () => {
9097
threadId = worker.threadId;
9198
})
9299
.on('error', error => {
93-
addBreadcrumb({
94-
category: 'worker_thread',
95-
message: `Worker thread errored with '${error.message}'`,
96-
level: 'error',
97-
data: { threadId },
98-
});
100+
if (options.captureWorkerErrors !== false) {
101+
captureException(error, {
102+
mechanism: { type: 'instrument', handled: false, data: { threadId: String(threadId) } },
103+
});
104+
} else {
105+
addBreadcrumb({
106+
category: 'worker_thread',
107+
message: `Worker thread errored with '${error.message}'`,
108+
level: 'error',
109+
data: { threadId },
110+
});
111+
}
99112
});
100113
}

0 commit comments

Comments
 (0)