Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,34 @@ import { instrumentLangGraph } from '@sentry/node';
import { instrumentStateGraph } from '@sentry/node';
```

### `childProcess` integration split into `childProcess` and `workerThreads`

Affected SDKs: `@sentry/node` and dependents.

The `childProcessIntegration` was split into a `childProcessIntegration` (for `child_process`) and a separate `workerThreadsIntegration` (for `worker_threads`).

Both integrations are enabled by default, so no change is needed to keep the previous behavior.

The deprecated `captureWorkerErrors` option was removed. Worker thread errors are always captured as events now. To opt out, remove `workerThreadsIntegration` instead:

```js
// before
Sentry.init({
integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })],
});

// after
Sentry.init({
integrations: integrations => integrations.filter(integration => integration.name !== 'WorkerThreads'),
});
```

Note that `captureWorkerErrors: false` used to downgrade worker thread errors to a `worker_thread` breadcrumb. That breadcrumb is gone, so removing the integration drops worker thread errors entirely.

The `includeChildProcessArgs` option stays on `childProcessIntegration`. Disabling `childProcessIntegration` no longer disables worker thread error capture, since that now lives in `workerThreadsIntegration`.

The mechanism type of worker thread errors changed from `auto.child_process.worker_thread` to `auto.node.worker_threads`. Adjust any alerts or filters that match on it.

### Deno default integrations renamed to match the other SDKs

Affected SDKs: `@sentry/deno`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const DEPENDENTS: Dependent[] = [
'NodeClient',
'NODE_VERSION',
'childProcessIntegration',
'workerThreadsIntegration',
'systemErrorIntegration',
'pinoIntegration',
// Bun will get its own runtime metrics integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })],
integrations: [Sentry.childProcessIntegration(), Sentry.workerThreadsIntegration()],
Comment thread
cursor[bot] marked this conversation as resolved.
transport: loggingTransport,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@ import type { Event } from '@sentry/core';
import { afterAll, describe, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

const EVENT = {
// and an exception that is our ANR
const WORKER_ERROR_EVENT = {
exception: {
values: [
{
type: 'Error',
value: 'Worker error',
mechanism: {
type: 'auto.node.worker_threads',
handled: false,
},
},
],
},
};

const TEST_ERROR_EVENT = {
exception: {
values: [
{
Expand All @@ -22,27 +36,20 @@ const EVENT = {
spawnfile: 'sleep',
},
},
{
timestamp: expect.any(Number),
category: 'worker_thread',
message: "Worker thread errored with 'Worker error'",
level: 'error',
data: {
threadId: expect.any(Number),
},
},
],
};

describe('should capture process and thread breadcrumbs', () => {
describe('should capture child process breadcrumbs and worker thread errors', () => {
afterAll(() => {
cleanupChildProcesses();
});

test('ESM', async () => {
await createRunner(__dirname, 'app.mjs')
.withMockSentryServer()
.expect({ event: EVENT as Event })
.expect({ event: WORKER_ERROR_EVENT as Event })
.expect({ event: TEST_ERROR_EVENT as Event })
Comment thread
cursor[bot] marked this conversation as resolved.
.unordered()
.start()
.completed();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const WORKER_EVENT: Event = {
type: 'Error',
value: 'Test error',
mechanism: {
type: 'auto.child_process.worker_thread',
type: 'auto.node.worker_threads',
handled: false,
data: {
threadId: expect.any(String),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('OnUncaughtException integration', () => {
type: 'Error',
value: 'job failed',
mechanism: {
type: 'auto.child_process.worker_thread',
type: 'auto.node.worker_threads',
handled: false,
},
stacktrace: {
Expand Down Expand Up @@ -146,7 +146,7 @@ describe('OnUncaughtException integration', () => {
type: 'Error',
value: 'job failed',
mechanism: {
type: 'auto.child_process.worker_thread',
type: 'auto.node.worker_threads',
handled: false,
},
stacktrace: {
Expand Down Expand Up @@ -178,7 +178,7 @@ describe('OnUncaughtException integration', () => {
type: 'Error',
value: 'job failed',
mechanism: {
type: 'auto.child_process.worker_thread',
type: 'auto.node.worker_threads',
handled: false,
},
stacktrace: {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
getOtlpTracesEndpoint,
processSessionIntegration,
childProcessIntegration,
workerThreadsIntegration,
createSentryWinstonTransport,
redisIntegration,
requestDataIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export {
otlpIntegration,
getOtlpTracesEndpoint,
childProcessIntegration,
workerThreadsIntegration,
createSentryWinstonTransport,
hapiIntegration,
setupHapiErrorHandler,
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export {
anthropicAIIntegration,
googleGenAIIntegration,
childProcessIntegration,
workerThreadsIntegration,
createSentryWinstonTransport,
vercelAIIntegration,
logger,
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export { pinoIntegration } from './integrations/pino';
export { spotlightIntegration } from './integrations/spotlight';
export { systemErrorIntegration } from './integrations/systemError';
export { createSentryWinstonTransport } from './integrations/winston';
export { workerThreadsIntegration } from './integrations/workerThreads';
export { cron } from './cron';
export { NODE_VERSION } from './nodeVersion';
export { defaultStackParser, getSentryRelease } from './sdk/api';
Expand Down
42 changes: 3 additions & 39 deletions packages/node/src/integrations/childProcess.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ChildProcess } from 'node:child_process';
import * as diagnosticsChannel from 'node:diagnostics_channel';
import type { Worker } from 'node:worker_threads';
import { addBreadcrumb, captureException, defineIntegration, isObjectLike } from '@sentry/core';
import { addBreadcrumb, defineIntegration, isObjectLike } from '@sentry/core';

interface Options {
/**
Expand All @@ -10,19 +9,13 @@ interface Options {
* @default false
*/
includeChildProcessArgs?: boolean;

/**
* Whether to capture errors from worker threads.
*
* @default true
*/
captureWorkerErrors?: boolean;
}

const INTEGRATION_NAME = 'ChildProcess' as const;

/**
* Capture breadcrumbs and events for child processes and worker threads.
* Capture breadcrumbs and events for child processes.
* For worker thread events, use `workerThreadsIntegration()` instead.
*/
export const childProcessIntegration = defineIntegration((options: Options = {}) => {
return {
Expand All @@ -33,12 +26,6 @@ export const childProcessIntegration = defineIntegration((options: Options = {})
captureChildProcessEvents(event.process as ChildProcess, options);
}
});

diagnosticsChannel.channel('worker_threads').subscribe((event: unknown) => {
if (isObjectLike(event) && 'worker' in event) {
captureWorkerThreadEvents(event.worker as Worker, options);
}
});
},
};
});
Expand Down Expand Up @@ -88,26 +75,3 @@ function captureChildProcessEvents(child: ChildProcess, options: Options): void
}
});
}

function captureWorkerThreadEvents(worker: Worker, options: Options): void {
let threadId: number | undefined;

worker
.on('online', () => {
threadId = worker.threadId;
})
.on('error', error => {
if (options.captureWorkerErrors !== false) {
captureException(error, {
mechanism: { type: 'auto.child_process.worker_thread', handled: false, data: { threadId: String(threadId) } },
});
} else {
addBreadcrumb({
category: 'worker_thread',
message: `Worker thread errored with '${error.message}'`,
level: 'error',
data: { threadId },
});
}
});
}
40 changes: 40 additions & 0 deletions packages/node/src/integrations/workerThreads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Worker } from 'node:worker_threads';
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { captureException, defineIntegration, isObjectLike } from '@sentry/core';

const INTEGRATION_NAME = 'WorkerThreads' as const;

/**
* Capture events and errors of worker threads.
* For child process events, use `childProcessIntegration()` instead.
*/
export const workerThreadsIntegration = defineIntegration(() => {
Comment thread
cursor[bot] marked this conversation as resolved.
return {
name: INTEGRATION_NAME,
setup() {
diagnosticsChannel.channel('worker_threads').subscribe((event: unknown) => {
if (isObjectLike(event) && 'worker' in event) {
captureWorkerThreadEvents(event.worker as Worker);
}
});
},
};
});

function captureWorkerThreadEvents(worker: Worker): void {
let threadId: number | undefined;

worker
.on('online', () => {
threadId = worker.threadId;
})
.on('error', error => {
captureException(error, {
mechanism: {
Comment thread
sentry[bot] marked this conversation as resolved.
type: 'auto.node.worker_threads',
handled: false,
data: threadId !== undefined ? { threadId: String(threadId) } : undefined,
},
});
});
Comment on lines +29 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The threadId is not captured for worker error events if the worker fails to initialize, as it's only set after the 'online' event fires.
Severity: LOW

Suggested Fix

The worker.threadId property is available immediately after the worker object is created. Capture worker.threadId right after the new Worker(...) call, before any event listeners are attached, to ensure it's available for all events, including initialization errors.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/node/src/integrations/workerThreads.ts#L24-L39

Potential issue: In the `captureWorkerThreadEvents` function, the `threadId` is only
cached within the 'online' event handler. If a worker fails to initialize due to an
error, such as a syntax error or a missing module, the 'error' event will fire before
the 'online' event. In this scenario, the local `threadId` variable remains `undefined`,
and the error event is captured without the `threadId` in its mechanism data. This makes
debugging worker initialization failures more difficult as the specific thread cannot be
identified.

}
2 changes: 2 additions & 0 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { processSessionIntegration } from '../integrations/processSession';
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
import { systemErrorIntegration } from '../integrations/systemError';
import { getAutoPerformanceIntegrations } from '../integrations/tracing';
import { workerThreadsIntegration } from '../integrations/workerThreads';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { getEntryPointType } from '../utils/entry-point';
Expand Down Expand Up @@ -65,6 +66,7 @@ function getBaseDefaultIntegrations(): Integration[] {
localVariablesIntegration(),
nodeContextIntegration(),
childProcessIntegration(),
workerThreadsIntegration(),
processSessionIntegration(),
modulesIntegration(),
];
Expand Down
Loading