diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs new file mode 100644 index 000000000000..c561b221d95f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs @@ -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); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs new file mode 100644 index 000000000000..32135d2246f2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs @@ -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); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js new file mode 100644 index 000000000000..30740bbd031b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js @@ -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); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs new file mode 100644 index 000000000000..273760a6db39 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs @@ -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); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs new file mode 100644 index 000000000000..55eecb5c23ec --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs @@ -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); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs new file mode 100644 index 000000000000..ee66bf82f8bf --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs @@ -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()], +}); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js new file mode 100644 index 000000000000..55f5358a10fe --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js @@ -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; diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js new file mode 100644 index 000000000000..71622bdbe083 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js @@ -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(); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js new file mode 100644 index 000000000000..cda4c0e10d3a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js @@ -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(); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts b/dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts new file mode 100644 index 000000000000..6798882015f1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts @@ -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(); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs new file mode 100644 index 000000000000..274a4ce9e3a9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs @@ -0,0 +1,5 @@ +import { longWork } from './long-work.js'; + +setTimeout(() => { + longWork(); +}, 2000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs new file mode 100644 index 000000000000..8591be4197e3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs @@ -0,0 +1,14 @@ +import { Worker } from 'node:worker_threads'; +import * as path from 'path'; +import * as url from 'url'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const workerPath = path.join(__dirname, 'worker-block.mjs'); + +const thread = new Worker(workerPath, { stdout: 'inherit' }); +thread.unref(); + +setInterval(() => { + // This keeps the main thread alive to allow the worker to run indefinitely +}, 1000); diff --git a/dev-packages/node-integration-tests/test.txt b/dev-packages/node-integration-tests/test.txt deleted file mode 100644 index 0a0fa7f94de9..000000000000 --- a/dev-packages/node-integration-tests/test.txt +++ /dev/null @@ -1,213 +0,0 @@ -yarn run v1.22.22 -$ /Users/abhijeetprasad/workspace/sentry-javascript/node_modules/.bin/jest contextLines/memory-leak - console.log - starting scenario /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts [ '-r', 'ts-node/register' ] undefined - - at log (utils/runner.ts:462:11) - - console.log - line COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad cwd DIR 1,16 608 107673020 /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad txt REG 1,16 88074480 114479727 /Users/abhijeetprasad/.volta/tools/image/node/18.20.5/bin/node - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 0u unix 0x6a083c8cc83ea8db 0t0 ->0xf2cacdd1d3a0ebec - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 1u unix 0xd99cc422a76ba47f 0t0 ->0x542148981a0b9ef2 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 2u unix 0x97e70527ed5803f8 0t0 ->0xbafdaf00ef20de83 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 3u KQUEUE count=0, state=0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 4 PIPE 0x271836c29e42bc67 16384 ->0x16ac23fcfd4fe1a3 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 5 PIPE 0x16ac23fcfd4fe1a3 16384 ->0x271836c29e42bc67 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 6 PIPE 0xd76fcd4ca2a35fcf 16384 ->0x30d26cd4f0e069b2 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 7 PIPE 0x30d26cd4f0e069b2 16384 ->0xd76fcd4ca2a35fcf - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 8 PIPE 0x37691847717c3d6 16384 ->0x966eedd79d018252 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 9 PIPE 0x966eedd79d018252 16384 ->0x37691847717c3d6 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 10u KQUEUE count=0, state=0xa - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 11 PIPE 0x99c1186f14b865be 16384 ->0xe88675eb1eefb2b - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 12 PIPE 0xe88675eb1eefb2b 16384 ->0x99c1186f14b865be - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 13 PIPE 0x52173210451cdda9 16384 ->0x50bbc31a0f1cc1af - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 14 PIPE 0x50bbc31a0f1cc1af 16384 ->0x52173210451cdda9 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 15u KQUEUE count=0, state=0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 16 PIPE 0xa115aa0653327e72 16384 ->0x100525c465ee1eb0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 17 PIPE 0x100525c465ee1eb0 16384 ->0xa115aa0653327e72 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 18 PIPE 0x41945cf9fe740277 16384 ->0x8791d18eade5b1e0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 19 PIPE 0x8791d18eade5b1e0 16384 ->0x41945cf9fe740277 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 20r CHR 3,2 0t0 333 /dev/null - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 21u KQUEUE count=0, state=0xa - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 22 PIPE 0xf4c6a2f47fb0bff5 16384 ->0xa00185e1c59cedbe - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 23 PIPE 0xa00185e1c59cedbe 16384 ->0xf4c6a2f47fb0bff5 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 24 PIPE 0x4ac25a99f45f7ca4 16384 ->0x2032aef840c94700 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 25 PIPE 0x2032aef840c94700 16384 ->0x4ac25a99f45f7ca4 - - at log (utils/runner.ts:462:11) - - console.log - line null - - at log (utils/runner.ts:462:11) - - console.log - line [{"sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"}},[[{"type":"session"},{"sid":"0ae9ef2ac2ba49dd92b6dab9d81444ac","init":true,"started":"2025-01-13T21:47:47.502Z","timestamp":"2025-01-13T21:47:47.663Z","status":"ok","errors":1,"duration":0.16146087646484375,"attrs":{"release":"1.0","environment":"production"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"2626269e3c634fc289338c441e76412c","sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 0","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"2626269e3c634fc289338c441e76412c","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b1e1b8a0d410ef14"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.528,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"f58236bf0a7f4a999f7daf5283f0400f","sent_at":"2025-01-13T21:47:47.664Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 1","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"f58236bf0a7f4a999f7daf5283f0400f","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9b6ccaf59536bcb4"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.531,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 2","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"82d56f443d3f01f9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.532,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"293d7c8c731c48eca30735b41efd40ba","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 3","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"293d7c8c731c48eca30735b41efd40ba","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8be46494d3555ddb"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"e9273b56624d4261b00f5431852da167","sent_at":"2025-01-13T21:47:47.666Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 4","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"e9273b56624d4261b00f5431852da167","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9a067a8906c8c147"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 5","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"ac2ad9041812f9d9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.534,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"65224267e02049daadbc577de86960f3","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 6","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"65224267e02049daadbc577de86960f3","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b12818330e05cd2f"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.535,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 7","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"83cb86896d96bbf6"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 8","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"a0e8e199fcf05714"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"dc08b3fe26e94759817c7b5e95469727","sent_at":"2025-01-13T21:47:47.669Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 9","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"dc08b3fe26e94759817c7b5e95469727","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8ec7d145c5362df0"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270106624},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.537,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - -Done in 4.21s. diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 97b1efa2dbb4..1006d71bf3f0 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -15,6 +15,7 @@ import { normalize } from '@sentry/core'; import { execSync, spawn, spawnSync } from 'child_process'; import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; +import { inspect } from 'util'; import { afterAll, beforeAll, describe, test } from 'vitest'; import { assertEnvelopeHeader, @@ -338,6 +339,8 @@ export function createRunner(...paths: string[]) { } function newEnvelope(envelope: Envelope): void { + if (process.env.DEBUG) log('newEnvelope', inspect(envelope, false, null, true)); + for (const item of envelope[1]) { const envelopeItemType = item[0].type; @@ -449,6 +452,12 @@ export function createRunner(...paths: string[]) { child = spawn('node', [...flags, testPath], { env }); + child.on('error', e => { + // eslint-disable-next-line no-console + console.error('Error starting child process:', e); + complete(e); + }); + CLEANUP_STEPS.add(() => { child?.kill(); }); diff --git a/packages/core/src/types-hoist/exception.ts b/packages/core/src/types-hoist/exception.ts index a74adf6c1603..27b320363a82 100644 --- a/packages/core/src/types-hoist/exception.ts +++ b/packages/core/src/types-hoist/exception.ts @@ -7,6 +7,6 @@ export interface Exception { value?: string; mechanism?: Mechanism; module?: string; - thread_id?: number; + thread_id?: number | string; stacktrace?: Stacktrace; } diff --git a/packages/core/src/types-hoist/thread.ts b/packages/core/src/types-hoist/thread.ts index 1cfad253a299..76f5592ef401 100644 --- a/packages/core/src/types-hoist/thread.ts +++ b/packages/core/src/types-hoist/thread.ts @@ -2,8 +2,9 @@ import type { Stacktrace } from './stacktrace'; /** JSDoc */ export interface Thread { - id?: number; + id?: number | string; name?: string; + main?: boolean; stacktrace?: Stacktrace; crashed?: boolean; current?: boolean; diff --git a/packages/node-native/README.md b/packages/node-native/README.md index 00779a75666e..4ff7b6fdab45 100644 --- a/packages/node-native/README.md +++ b/packages/node-native/README.md @@ -19,3 +19,50 @@ yarn add @sentry/node @sentry/node-native # Using npm npm install --save @sentry/node @sentry/node-native ``` + +## `eventLoopBlockIntegration` + +The `eventLoopBlockIntegration` can be used to monitor for blocked event loops in +all threads of a Node.js application. + +If you instrument your application via the Node.js `--import` flag, Sentry will +be started and this instrumentation will be automatically applied to all worker +threads. + +`instrument.mjs` + +```javascript +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; + +Sentry.init({ + dsn: '__YOUR_DSN__', + // Capture stack traces when the event loop is blocked for more than 500ms + integrations: [eventLoopBlockIntegration({ threshold: 500 })], +}); +``` + +`app.mjs` + +```javascript +import { Worker } from 'worker_threads'; + +const worker = new Worker(new URL('./worker.mjs', import.meta.url)); + +// This main thread will be monitored for blocked event loops +``` + +`worker.mjs` + +```javascript +// This worker thread will also be monitored for blocked event loops too +``` + +Start your application: + +```bash +node --import instrument.mjs app.mjs +``` + +If a thread is blocked for more than the configured threshold, stack traces will +be captured for all threads and sent to Sentry. diff --git a/packages/node-native/package.json b/packages/node-native/package.json index 79788f1b6c65..bfa03ea947c2 100644 --- a/packages/node-native/package.json +++ b/packages/node-native/package.json @@ -20,6 +20,14 @@ "types": "./build/types/index.d.ts", "default": "./build/cjs/index.js" } + }, + "./event-loop-block-watchdog": { + "import": { + "default": "./build/esm/event-loop-block-watchdog.js" + }, + "require": { + "default": "./build/cjs/event-loop-block-watchdog.js" + } } }, "typesVersions": { @@ -55,6 +63,7 @@ "build:tarball": "npm pack" }, "dependencies": { + "@sentry-internal/node-native-stacktrace": "^0.1.0", "@sentry/core": "9.34.0", "@sentry/node": "9.34.0" }, diff --git a/packages/node-native/rollup.npm.config.mjs b/packages/node-native/rollup.npm.config.mjs index b58b8e8ac027..ce79b0ac9bbb 100644 --- a/packages/node-native/rollup.npm.config.mjs +++ b/packages/node-native/rollup.npm.config.mjs @@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.ts'], + entrypoints: ['src/index.ts', 'src/event-loop-block-watchdog.ts'], packageSpecificConfig: { output: { dir: 'build', diff --git a/packages/node-native/src/common.ts b/packages/node-native/src/common.ts new file mode 100644 index 000000000000..2a96050dbc34 --- /dev/null +++ b/packages/node-native/src/common.ts @@ -0,0 +1,44 @@ +import type { Contexts, DsnComponents, Primitive, SdkMetadata, Session } from '@sentry/core'; + +export const POLL_RATIO = 2; + +export interface ThreadBlockedIntegrationOptions { + /** + * Threshold in milliseconds to trigger an event. + * + * Defaults to 1000ms. + */ + threshold: number; + /** + * Maximum number of blocked events to send per clock hour. + * + * Defaults to 1. + */ + maxEventsPerHour: number; + /** + * Tags to include with blocked events. + */ + staticTags: { [key: string]: Primitive }; + /** + * @ignore Internal use only. + * + * If this is supplied, stack frame filenames will be rewritten to be relative to this path. + */ + appRootPath: string | undefined; +} + +export interface WorkerStartData extends ThreadBlockedIntegrationOptions { + debug: boolean; + sdkMetadata: SdkMetadata; + dsn: DsnComponents; + tunnel: string | undefined; + release: string | undefined; + environment: string; + dist: string | undefined; + contexts: Contexts; +} + +export interface ThreadState { + session: Session | undefined; + debugImages: Record; +} diff --git a/packages/node-native/src/event-loop-block-integration.ts b/packages/node-native/src/event-loop-block-integration.ts new file mode 100644 index 000000000000..6b643e944adf --- /dev/null +++ b/packages/node-native/src/event-loop-block-integration.ts @@ -0,0 +1,167 @@ +import { Worker } from 'node:worker_threads'; +import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/core'; +import { defineIntegration, getFilenameToDebugIdMap, getIsolationScope, logger } from '@sentry/core'; +import type { NodeClient } from '@sentry/node'; +import { registerThread, threadPoll } from '@sentry-internal/node-native-stacktrace'; +import type { ThreadBlockedIntegrationOptions, WorkerStartData } from './common'; +import { POLL_RATIO } from './common'; + +const DEFAULT_THRESHOLD_MS = 1_000; + +function log(message: string, ...args: unknown[]): void { + logger.log(`[Sentry Block Event Loop] ${message}`, ...args); +} + +/** + * Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup + */ +async function getContexts(client: NodeClient): Promise { + let event: Event | null = { message: INTEGRATION_NAME }; + const eventHint: EventHint = {}; + + for (const processor of client.getEventProcessors()) { + if (event === null) break; + event = await processor(event, eventHint); + } + + return event?.contexts || {}; +} + +const INTEGRATION_NAME = 'ThreadBlocked'; + +const _eventLoopBlockIntegration = ((options: Partial = {}) => { + return { + name: INTEGRATION_NAME, + afterAllSetup(client: NodeClient) { + registerThread(); + _startWorker(client, options).catch(err => { + log('Failed to start event loop block worker', err); + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Monitors the Node.js event loop for blocking behavior and reports blocked events to Sentry. + * + * Uses a background worker thread to detect when the main thread is blocked for longer than + * the configured threshold (default: 1 second). + * + * When instrumenting via the `--import` flag, this integration will + * automatically monitor all worker threads as well. + * + * ```js + * // instrument.mjs + * import * as Sentry from '@sentry/node'; + * import { eventLoopBlockIntegration } from '@sentry/node-native'; + * + * Sentry.init({ + * dsn: '__YOUR_DSN__', + * integrations: [ + * eventLoopBlockIntegration({ + * threshold: 500, // Report blocks longer than 500ms + * }), + * ], + * }); + * ``` + * + * Start your application with: + * ```bash + * node --import instrument.mjs app.mjs + * ``` + */ +export const eventLoopBlockIntegration = defineIntegration(_eventLoopBlockIntegration); + +/** + * Starts the worker thread + * + * @returns A function to stop the worker + */ +async function _startWorker( + client: NodeClient, + integrationOptions: Partial, +): Promise<() => void> { + const dsn = client.getDsn(); + + if (!dsn) { + return () => { + // + }; + } + + const contexts = await getContexts(client); + + // These will not be accurate if sent later from the worker thread + delete contexts.app?.app_memory; + delete contexts.device?.free_memory; + + const initOptions = client.getOptions(); + + const sdkMetadata = client.getSdkMetadata() || {}; + if (sdkMetadata.sdk) { + sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name); + } + + const options: WorkerStartData = { + debug: logger.isEnabled(), + dsn, + tunnel: initOptions.tunnel, + environment: initOptions.environment || 'production', + release: initOptions.release, + dist: initOptions.dist, + sdkMetadata, + appRootPath: integrationOptions.appRootPath, + threshold: integrationOptions.threshold || DEFAULT_THRESHOLD_MS, + maxEventsPerHour: integrationOptions.maxEventsPerHour || 1, + staticTags: integrationOptions.staticTags || {}, + contexts, + }; + + const pollInterval = options.threshold / POLL_RATIO; + + const worker = new Worker(new URL('./event-loop-block-watchdog.js', import.meta.url), { + workerData: options, + // We don't want any Node args like --import to be passed to the worker + execArgv: [], + env: { ...process.env, NODE_OPTIONS: undefined }, + }); + + process.on('exit', () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + }); + + const timer = setInterval(() => { + try { + const currentSession = getIsolationScope().getSession(); + // We need to copy the session object and remove the toJSON method so it can be sent to the worker + // serialized without making it a SerializedSession + const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; + // message the worker to tell it the main event loop is still running + threadPoll({ session, debugImages: getFilenameToDebugIdMap(initOptions.stackParser) }); + } catch (_) { + // + } + }, pollInterval); + // Timer should not block exit + timer.unref(); + + worker.once('error', (err: Error) => { + clearInterval(timer); + log('watchdog worker error', err); + }); + + worker.once('exit', (code: number) => { + clearInterval(timer); + log('watchdog worker exit', code); + }); + + // Ensure this thread can't block app exit + worker.unref(); + + return () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + clearInterval(timer); + }; +} diff --git a/packages/node-native/src/event-loop-block-watchdog.ts b/packages/node-native/src/event-loop-block-watchdog.ts new file mode 100644 index 000000000000..8909c00d1ea7 --- /dev/null +++ b/packages/node-native/src/event-loop-block-watchdog.ts @@ -0,0 +1,286 @@ +import { workerData } from 'node:worker_threads'; +import type { DebugImage, Event, Session, StackFrame, Thread } from '@sentry/core'; +import { + createEventEnvelope, + createSessionEnvelope, + filenameIsInApp, + getEnvelopeEndpointWithUrlEncodedAuth, + makeSession, + normalizeUrlToBase, + stripSentryFramesAndReverse, + updateSession, + uuid4, +} from '@sentry/core'; +import { makeNodeTransport } from '@sentry/node'; +import { captureStackTrace, getThreadsLastSeen } from '@sentry-internal/node-native-stacktrace'; +import type { ThreadState, WorkerStartData } from './common'; +import { POLL_RATIO } from './common'; + +const { + threshold, + appRootPath, + contexts, + debug, + dist, + dsn, + environment, + maxEventsPerHour, + release, + sdkMetadata, + staticTags: tags, + tunnel, +} = workerData as WorkerStartData; + +const pollInterval = threshold / POLL_RATIO; +const triggeredThreads = new Set(); + +function log(...msg: unknown[]): void { + if (debug) { + // eslint-disable-next-line no-console + console.log('[Sentry Block Event Loop Watchdog]', ...msg); + } +} + +function createRateLimiter(maxEventsPerHour: number): () => boolean { + let currentHour = 0; + let currentCount = 0; + + return function isRateLimited(): boolean { + const hour = new Date().getHours(); + + if (hour !== currentHour) { + currentHour = hour; + currentCount = 0; + } + + if (currentCount >= maxEventsPerHour) { + if (currentCount === maxEventsPerHour) { + currentCount += 1; + log(`Rate limit reached: ${currentCount} events in this hour`); + } + return true; + } + + currentCount += 1; + return false; + }; +} + +const url = getEnvelopeEndpointWithUrlEncodedAuth(dsn, tunnel, sdkMetadata.sdk); +const transport = makeNodeTransport({ + url, + recordDroppedEvent: () => { + // + }, +}); +const isRateLimited = createRateLimiter(maxEventsPerHour); + +async function sendAbnormalSession(serializedSession: Session | undefined): Promise { + if (!serializedSession) { + return; + } + + log('Sending abnormal session'); + const session = makeSession(serializedSession); + + updateSession(session, { + status: 'abnormal', + abnormal_mechanism: 'anr_foreground', + release, + environment, + }); + + const envelope = createSessionEnvelope(session, dsn, sdkMetadata, tunnel); + // Log the envelope so to aid in testing + log(JSON.stringify(envelope)); + + await transport.send(envelope); +} + +log('Started'); + +function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] | undefined { + if (!stackFrames) { + return undefined; + } + + // Strip Sentry frames and reverse the stack frames so they are in the correct order + const strippedFrames = stripSentryFramesAndReverse(stackFrames); + + for (const frame of strippedFrames) { + if (!frame.filename) { + continue; + } + + frame.in_app = filenameIsInApp(frame.filename); + + // If we have an app root path, rewrite the filenames to be relative to the app root + if (appRootPath) { + frame.filename = normalizeUrlToBase(frame.filename, appRootPath); + } + } + + return strippedFrames; +} + +function stripFileProtocol(filename: string | undefined): string | undefined { + if (!filename) { + return undefined; + } + return filename.replace(/^file:\/\//, ''); +} + +// eslint-disable-next-line complexity +function applyDebugMeta(event: Event, debugImages: Record): void { + if (Object.keys(debugImages).length === 0) { + return; + } + + const normalisedDebugImages = appRootPath ? {} : debugImages; + if (appRootPath) { + for (const [path, debugId] of Object.entries(debugImages)) { + normalisedDebugImages[normalizeUrlToBase(path, appRootPath)] = debugId; + } + } + + const filenameToDebugId = new Map(); + + for (const exception of event.exception?.values || []) { + for (const frame of exception.stacktrace?.frames || []) { + const filename = stripFileProtocol(frame.abs_path || frame.filename); + if (filename && normalisedDebugImages[filename]) { + filenameToDebugId.set(filename, normalisedDebugImages[filename] as string); + } + } + } + + for (const thread of event.threads?.values || []) { + for (const frame of thread.stacktrace?.frames || []) { + const filename = stripFileProtocol(frame.abs_path || frame.filename); + if (filename && normalisedDebugImages[filename]) { + filenameToDebugId.set(filename, normalisedDebugImages[filename] as string); + } + } + } + + if (filenameToDebugId.size > 0) { + const images: DebugImage[] = []; + for (const [code_file, debug_id] of filenameToDebugId.entries()) { + images.push({ + type: 'sourcemap', + code_file, + debug_id, + }); + } + event.debug_meta = { images }; + } +} + +function getExceptionAndThreads( + crashedThreadId: string, + threads: ReturnType>, +): Event { + const crashedThread = threads[crashedThreadId]; + + return { + exception: { + values: [ + { + type: 'EventLoopBlocked', + value: `Event Loop Blocked for at least ${threshold} ms`, + stacktrace: { frames: prepareStackFrames(crashedThread?.frames) }, + // This ensures the UI doesn't say 'Crashed in' for the stack trace + mechanism: { type: 'ANR' }, + thread_id: crashedThreadId, + }, + ], + }, + threads: { + values: Object.entries(threads).map(([threadId, threadState]) => { + const crashed = threadId === crashedThreadId; + + const thread: Thread = { + id: threadId, + name: threadId === '0' ? 'main' : `worker-${threadId}`, + crashed, + current: true, + main: threadId === '0', + }; + + if (!crashed) { + thread.stacktrace = { frames: prepareStackFrames(threadState.frames) }; + } + + return thread; + }), + }, + }; +} + +async function sendBlockEvent(crashedThreadId: string): Promise { + if (isRateLimited()) { + return; + } + + const threads = captureStackTrace(); + const crashedThread = threads[crashedThreadId]; + + if (!crashedThread) { + log(`No thread found with ID '${crashedThreadId}'`); + return; + } + + try { + await sendAbnormalSession(crashedThread.state?.session); + } catch (error) { + log(`Failed to send abnormal session for thread '${crashedThreadId}':`, error); + } + + log('Sending event'); + + const event: Event = { + event_id: uuid4(), + contexts, + release, + environment, + dist, + platform: 'node', + level: 'error', + tags, + ...getExceptionAndThreads(crashedThreadId, threads), + }; + + const allDebugImages: Record = Object.values(threads).reduce((acc, threadState) => { + return { ...acc, ...threadState.state?.debugImages }; + }, {}); + + applyDebugMeta(event, allDebugImages); + + const envelope = createEventEnvelope(event, dsn, sdkMetadata, tunnel); + // Log the envelope to aid in testing + log(JSON.stringify(envelope)); + + await transport.send(envelope); + await transport.flush(2000); +} + +setInterval(async () => { + for (const [threadId, time] of Object.entries(getThreadsLastSeen())) { + if (time > threshold) { + if (triggeredThreads.has(threadId)) { + continue; + } + + log(`Blocked thread detected '${threadId}' last polled ${time} ms ago.`); + triggeredThreads.add(threadId); + + try { + await sendBlockEvent(threadId); + } catch (error) { + log(`Failed to send event for thread '${threadId}':`, error); + } + } else { + triggeredThreads.delete(threadId); + } + } +}, pollInterval); diff --git a/packages/node-native/src/index.ts b/packages/node-native/src/index.ts index cb0ff5c3b541..454be4eb8ad2 100644 --- a/packages/node-native/src/index.ts +++ b/packages/node-native/src/index.ts @@ -1 +1 @@ -export {}; +export { eventLoopBlockIntegration } from './event-loop-block-integration'; diff --git a/yarn.lock b/yarn.lock index a44f353e348e..7575b784e07f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6473,6 +6473,14 @@ detect-libc "^2.0.3" node-abi "^3.73.0" +"@sentry-internal/node-native-stacktrace@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/node-native-stacktrace/-/node-native-stacktrace-0.1.0.tgz#fa0eaf1e66245f463ca2294ff63da74c56d1a052" + integrity sha512-dWkxhDdjcRdEOTk1acrdBledqIroaYJrOSbecx5tJ/m9DiWZ1Oa4eNi/sI2SHLT+hKmsBBxrychf6+Iitz5Bzw== + dependencies: + detect-libc "^2.0.4" + node-abi "^3.73.0" + "@sentry-internal/rrdom@2.34.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.34.0.tgz#fccc9fe211c3995d4200abafbe8d75b671961ee9" @@ -13334,7 +13342,7 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -detect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3: +detect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3, detect-libc@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==