Skip to content

feat(node): Add eventLoopBlockIntegration #16709

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

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
455d77a
new package
timfish Jun 10, 2025
b5b0027
Merge branch 'develop' into timfish/feat/new-anr
timfish Jun 14, 2025
59b6bff
Mostly working
timfish Jun 18, 2025
b49f3f5
Lint 🤦🏻‍♂️
timfish Jun 23, 2025
7a306e4
Merge branch 'develop' into timfish/feat/new-anr
timfish Jun 23, 2025
aa69600
Update Sentry deps
timfish Jun 23, 2025
156f67b
Better thread names
timfish Jun 24, 2025
7dfdd7a
Merge branch 'develop' into timfish/feat/new-anr
timfish Jun 24, 2025
327fa55
yarn.lock changes
timfish Jun 24, 2025
6fa63af
Add to verdaccio config
timfish Jun 24, 2025
0745a20
Fix tests
timfish Jun 24, 2025
3f83db2
rename package export
timfish Jun 24, 2025
c4a718c
Fix flakey worker thread test
timfish Jun 24, 2025
a487e50
Merge remote-tracking branch 'upstream/develop' into timfish/feat/new…
timfish Jun 24, 2025
ad4693a
Apply suggestions from code review
timfish Jun 24, 2025
8443d33
PR review
timfish Jun 24, 2025
1ac4549
Merge branch 'timfish/feat/new-anr' of github.com:getsentry/sentry-ja…
timfish Jun 24, 2025
66e6aa5
Add rate limits
timfish Jun 24, 2025
487e667
PR review
timfish Jun 25, 2025
3dfd01b
Merge branch 'develop' into timfish/feat/new-anr
timfish Jun 25, 2025
7fb37dc
Remove `disableBlockedDetectionForCallback` for now
timfish Jun 26, 2025
5289d11
Merge branch 'timfish/feat/new-anr' of github.com:getsentry/sentry-ja…
timfish Jun 26, 2025
a346e4c
Merge remote-tracking branch 'upstream/develop' into timfish/feat/new…
timfish Jun 26, 2025
c3b9780
remove unused tests
timfish Jun 28, 2025
47edf1c
Merge branch 'develop' into timfish/feat/new-anr
timfish Jun 28, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as Sentry from '@sentry/node';
import { eventLoopBlockIntegration } from '@sentry/node-native';
import * as path from 'path';
import * as url from 'url';
import { longWork } from './long-work.js';

global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));

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

Sentry.init({
dsn: process.env.SENTRY_DSN,
release: '1.0',
integrations: [eventLoopBlockIntegration({ appRootPath: __dirname })],
});

setTimeout(() => {
longWork();
}, 1000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as Sentry from '@sentry/node';
import { eventLoopBlockIntegration } from '@sentry/node-native';
import { longWork } from './long-work.js';

global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };

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

Sentry.init({
dsn: process.env.SENTRY_DSN,
release: '1.0',
integrations: [eventLoopBlockIntegration({ maxEventsPerHour: 2 })],
});

setTimeout(() => {
longWork();
}, 1000);

setTimeout(() => {
longWork();
}, 4000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const Sentry = require('@sentry/node');
const { eventLoopBlockIntegration } = require('@sentry/node-native');
const { longWork } = require('./long-work.js');

global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };

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

Sentry.init({
dsn: process.env.SENTRY_DSN,
release: '1.0',
integrations: [eventLoopBlockIntegration()],
});

setTimeout(() => {
longWork();
}, 2000);

// Ensure we only send one event even with multiple blocking events
setTimeout(() => {
longWork();
}, 5000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Sentry from '@sentry/node';
import { eventLoopBlockIntegration } from '@sentry/node-native';
import { longWork } from './long-work.js';

global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };

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

Sentry.init({
dsn: process.env.SENTRY_DSN,
release: '1.0',
integrations: [eventLoopBlockIntegration()],
});

setTimeout(() => {
longWork();
}, 2000);

// Ensure we only send one event even with multiple blocking events
setTimeout(() => {
longWork();
}, 5000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as Sentry from '@sentry/node';
import { eventLoopBlockIntegration } from '@sentry/node-native';
import * as assert from 'assert';
import * as crypto from 'crypto';

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

Sentry.init({
dsn: process.env.SENTRY_DSN,
release: '1.0',
integrations: [eventLoopBlockIntegration()],
});

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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { eventLoopBlockIntegration } from '@sentry/node-native';

Sentry.init({
debug: true,
dsn: process.env.SENTRY_DSN,
release: '1.0',
integrations: [eventLoopBlockIntegration()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const crypto = require('crypto');
const assert = require('assert');

function longWork() {
for (let i = 0; i < 200; i++) {
const salt = crypto.randomBytes(128).toString('base64');
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
assert.ok(hash);
}
}

exports.longWork = longWork;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const Sentry = require('@sentry/node');
const { eventLoopBlockIntegration } = require('@sentry/node-native');

function configureSentry() {
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
debug: true,
integrations: [eventLoopBlockIntegration()],
});
}

async function main() {
configureSentry();
await new Promise(resolve => setTimeout(resolve, 1000));
process.exit(0);
}

main();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const Sentry = require('@sentry/node');
const { eventLoopBlockIntegration } = require('@sentry/node-native');

function configureSentry() {
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
debug: true,
integrations: [eventLoopBlockIntegration()],
});
}

async function main() {
configureSentry();
await new Promise(resolve => setTimeout(resolve, 1000));
}

main();
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { join } from 'node:path';
import type { Event } from '@sentry/core';
import { afterAll, describe, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../utils/runner';

function EXCEPTION(thread_id = '0') {
return {
values: [
{
type: 'EventLoopBlocked',
value: 'Event Loop Blocked for at least 1000 ms',
mechanism: { type: 'ANR' },
thread_id,
stacktrace: {
frames: expect.arrayContaining([
expect.objectContaining({
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.any(String),
function: '?',
in_app: true,
}),
expect.objectContaining({
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.any(String),
function: 'longWork',
in_app: true,
}),
]),
},
},
],
};
}

const ANR_EVENT = {
// Ensure we have context
contexts: {
device: {
arch: expect.any(String),
},
app: {
app_start_time: expect.any(String),
},
os: {
name: expect.any(String),
},
culture: {
timezone: expect.any(String),
},
},
threads: {
values: [
{
id: '0',
name: 'main',
crashed: true,
current: true,
main: true,
},
],
},
// and an exception that is our ANR
exception: EXCEPTION(),
};

function ANR_EVENT_WITH_DEBUG_META(file: string): Event {
return {
...ANR_EVENT,
debug_meta: {
images: [
{
type: 'sourcemap',
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
code_file: expect.stringContaining(file),
},
],
},
};
}

describe('Thread Blocked Native', { timeout: 30_000 }, () => {
afterAll(() => {
cleanupChildProcesses();
});

test('CJS', async () => {
await createRunner(__dirname, 'basic.js')
.withMockSentryServer()
.expect({ event: ANR_EVENT_WITH_DEBUG_META('basic') })
.start()
.completed();
});

test('ESM', async () => {
await createRunner(__dirname, 'basic.mjs')
.withMockSentryServer()
.expect({ event: ANR_EVENT_WITH_DEBUG_META('basic') })
.start()
.completed();
});

test('Custom appRootPath', async () => {
const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = {
...ANR_EVENT,
debug_meta: {
images: [
{
type: 'sourcemap',
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
code_file: 'app:///app-path.mjs',
},
],
},
};

await createRunner(__dirname, 'app-path.mjs')
.withMockSentryServer()
.expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META })
.start()
.completed();
});

test('multiple events via maxEventsPerHour', async () => {
await createRunner(__dirname, 'basic-multiple.mjs')
.withMockSentryServer()
.expect({ event: ANR_EVENT_WITH_DEBUG_META('basic-multiple') })
.expect({ event: ANR_EVENT_WITH_DEBUG_META('basic-multiple') })
.start()
.completed();
});

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

test('should exit', async () => {
const runner = createRunner(__dirname, 'should-exit.js').start();

await new Promise(resolve => setTimeout(resolve, 5_000));

expect(runner.childHasExited()).toBe(true);
});

test('should exit forced', async () => {
const runner = createRunner(__dirname, 'should-exit-forced.js').start();

await new Promise(resolve => setTimeout(resolve, 5_000));

expect(runner.childHasExited()).toBe(true);
});

test('worker thread', async () => {
const instrument = join(__dirname, 'instrument.mjs');
await createRunner(__dirname, 'worker-main.mjs')
.withMockSentryServer()
.withFlags('--import', instrument)
.expect({
event: event => {
const crashedThread = event.threads?.values?.find(thread => thread.crashed)?.id as string;
expect(crashedThread).toBeDefined();

expect(event).toMatchObject({
...ANR_EVENT,
exception: {
...EXCEPTION(crashedThread),
},
threads: {
values: [
{
id: '0',
name: 'main',
crashed: false,
current: true,
main: true,
stacktrace: {
frames: expect.any(Array),
},
},
{
id: crashedThread,
name: `worker-${crashedThread}`,
crashed: true,
current: true,
main: false,
},
],
},
});
},
})
.start()
.completed();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { longWork } from './long-work.js';

setTimeout(() => {
longWork();
}, 2000);
Loading
Loading