diff --git a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs new file mode 100644 index 000000000000..f58eb87f8237 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs @@ -0,0 +1,36 @@ +import * as assert from 'assert'; +import * as crypto from 'crypto'; + +import * as Sentry from '@sentry/node'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + autoSessionTracking: false, + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })], +}); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); + +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 18777e5ecdbd..454a35605925 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -30,3 +30,8 @@ function longWork() { setTimeout(() => { longWork(); }, 1000); + +// Ensure we only send one event even with multiple blocking events +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index b1750b308d28..d1d9c684bf60 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -101,7 +101,7 @@ const ANR_EVENT_WITH_DEBUG_META: Event = { { type: 'sourcemap', debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - code_file: expect.stringContaining('basic.'), + code_file: expect.stringContaining('basic'), }, ], }, @@ -123,6 +123,14 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => .start(done); }); + test('multiple events via maxAnrEvents', done => { + createRunner(__dirname, 'basic-multiple.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start(done); + }); + test('blocked indefinitely', done => { createRunner(__dirname, 'indefinite.mjs').withMockSentryServer().expect({ event: ANR_EVENT }).start(done); }); diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts index e2666e3ecd3e..fc1b23e35b1d 100644 --- a/packages/node/src/integrations/anr/common.ts +++ b/packages/node/src/integrations/anr/common.ts @@ -21,6 +21,12 @@ export interface AnrIntegrationOptions { * This uses the node debugger which enables the inspector API and opens the required ports. */ captureStackTrace: boolean; + /** + * Maximum number of ANR events to send. + * + * Defaults to 1. + */ + maxAnrEvents: number; /** * Tags to include with ANR events. */ diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index f0cef4d60831..b71bdfa49c52 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -160,6 +160,7 @@ async function _startWorker( pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL, anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD, captureStackTrace: !!integrationOptions.captureStackTrace, + maxAnrEvents: integrationOptions.maxAnrEvents || 1, staticTags: integrationOptions.staticTags || {}, contexts, }; diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index 354cea514618..a1ad39294c3e 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -23,7 +23,7 @@ type VoidFunction = () => void; const options: WorkerStartData = workerData; let session: Session | undefined; -let hasSentAnrEvent = false; +let sentAnrEvents = 0; let mainDebugImages: Record = {}; function log(msg: string): void { @@ -134,11 +134,11 @@ function applyScopeToEvent(event: Event, scope: ScopeData): void { } async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { - if (hasSentAnrEvent) { + if (sentAnrEvents >= options.maxAnrEvents) { return; } - hasSentAnrEvent = true; + sentAnrEvents += 1; await sendAbnormalSession(); @@ -179,11 +179,13 @@ async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { - process.exit(0); - }, 5_000); + if (sentAnrEvents >= options.maxAnrEvents) { + // Delay for 5 seconds so that stdio can flush if the main event loop ever restarts. + // This is mainly for the benefit of logging or debugging. + setTimeout(() => { + process.exit(0); + }, 5_000); + } } let debuggerPause: VoidFunction | undefined;