Skip to content

fix(node): Send ANR events without scope if event loop blocked indefinitely #11578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 12, 2024
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
31 changes: 31 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/indefinite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as assert from 'assert';
import * as crypto from 'crypto';

import * as Sentry from '@sentry/node';

setTimeout(() => {
process.exit();
}, 10000);

Sentry.init({
dsn: process.env.SENTRY_DSN,
release: '1.0',
autoSessionTracking: false,
integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: '[email protected]' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWork() {
// This loop will run almost indefinitely
for (let i = 0; i < 2000000000; i++) {
const salt = crypto.randomBytes(128).toString('base64');
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
assert.ok(hash);
}
}

setTimeout(() => {
longWork();
}, 1000);
40 changes: 24 additions & 16 deletions dev-packages/node-integration-tests/suites/anr/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { conditionalTest } from '../../utils';
import { cleanupChildProcesses, createRunner } from '../../utils/runner';

const EXPECTED_ANR_EVENT = {
const ANR_EVENT = {
// Ensure we have context
contexts: {
trace: {
Expand All @@ -21,15 +21,6 @@ const EXPECTED_ANR_EVENT = {
timezone: expect.any(String),
},
},
user: {
email: '[email protected]',
},
breadcrumbs: [
{
timestamp: expect.any(Number),
message: 'important message!',
},
],
// and an exception that is our ANR
exception: {
values: [
Expand Down Expand Up @@ -60,24 +51,41 @@ const EXPECTED_ANR_EVENT = {
},
};

const ANR_EVENT_WITH_SCOPE = {
...ANR_EVENT,
user: {
email: '[email protected]',
},
breadcrumbs: [
{
timestamp: expect.any(Number),
message: 'important message!',
},
],
};

conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => {
afterAll(() => {
cleanupChildProcesses();
});

test('CJS', done => {
createRunner(__dirname, 'basic.js').withMockSentryServer().expect({ event: EXPECTED_ANR_EVENT }).start(done);
createRunner(__dirname, 'basic.js').withMockSentryServer().expect({ event: ANR_EVENT_WITH_SCOPE }).start(done);
});

test('ESM', done => {
createRunner(__dirname, 'basic.mjs').withMockSentryServer().expect({ event: EXPECTED_ANR_EVENT }).start(done);
createRunner(__dirname, 'basic.mjs').withMockSentryServer().expect({ event: ANR_EVENT_WITH_SCOPE }).start(done);
});

test('blocked indefinitely', done => {
createRunner(__dirname, 'indefinite.mjs').withMockSentryServer().expect({ event: ANR_EVENT }).start(done);
});

test('With --inspect', done => {
createRunner(__dirname, 'basic.mjs')
.withMockSentryServer()
.withFlags('--inspect')
.expect({ event: EXPECTED_ANR_EVENT })
.expect({ event: ANR_EVENT_WITH_SCOPE })
.start(done);
});

Expand Down Expand Up @@ -108,16 +116,16 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
abnormal_mechanism: 'anr_foreground',
},
})
.expect({ event: EXPECTED_ANR_EVENT })
.expect({ event: ANR_EVENT_WITH_SCOPE })
.start(done);
});

test('from forked process', done => {
createRunner(__dirname, 'forker.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
createRunner(__dirname, 'forker.js').expect({ event: ANR_EVENT_WITH_SCOPE }).start(done);
});

test('worker can be stopped and restarted', done => {
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
createRunner(__dirname, 'stop-and-start.js').expect({ event: ANR_EVENT_WITH_SCOPE }).start(done);
});

const EXPECTED_ISOLATED_EVENT = {
Expand Down
10 changes: 10 additions & 0 deletions packages/node/src/integrations/anr/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ if (options.captureStackTrace) {
callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleName),
);

// Runtime.evaluate may never return if the event loop is blocked indefinitely
// In that case, we want to send the event anyway
const getScopeTimeout = setTimeout(() => {
sendAnrEvent(stackFrames).then(null, () => {
log('Sending ANR event failed.');
});
}, 5_000);

// Evaluate a script in the currently paused context
session.post(
'Runtime.evaluate',
Expand All @@ -203,6 +211,8 @@ if (options.captureStackTrace) {
log(`Error executing script: '${err.message}'`);
}

clearTimeout(getScopeTimeout);

const scopes = param && param.result ? (param.result.value as ScopeData) : undefined;

session.post('Debugger.resume');
Expand Down