diff --git a/CHANGELOG.md b/CHANGELOG.md index 957962e..c01bfc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.2.0 + +- feat: Allow disabling of watchdog tracking per thread (#11) + ## 0.1.1 - meta: Improve `README.md`, `package.json` metadata and add `LICENSE` (#10) diff --git a/README.md b/README.md index 41382aa..8b6e58d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ a threshold, JavaScript stack traces can be captured. The heartbeats can optionally include state information which is included with the corresponding stack trace. +This native module is used for Sentry's +[Event Loop Blocked Detection](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/event-loop-block/) +feature. + ## Basic Usage ### 1. Register threads you want to monitor @@ -38,58 +42,62 @@ Stack traces show where each thread is currently executing: ```js { - '0': [ - { - function: 'from', - filename: 'node:buffer', - lineno: 298, - colno: 28 - }, - { - function: 'pbkdf2Sync', - filename: 'node:internal/crypto/pbkdf2', - lineno: 78, - colno: 17 - }, - { - function: 'longWork', - filename: '/app/test.js', - lineno: 20, - colno: 29 - }, - { - function: '?', - filename: '/app/test.js', - lineno: 24, - colno: 1 - } - ], - '2': [ - { - function: 'from', - filename: 'node:buffer', - lineno: 298, - colno: 28 - }, - { - function: 'pbkdf2Sync', - filename: 'node:internal/crypto/pbkdf2', - lineno: 78, - colno: 17 - }, - { - function: 'longWork', - filename: '/app/worker.js', - lineno: 10, - colno: 29 - }, - { - function: '?', - filename: '/app/worker.js', - lineno: 14, - colno: 1 - } - ] + '0': { // Main thread has ID '0' + frames: [ + { + function: 'from', + filename: 'node:buffer', + lineno: 298, + colno: 28 + }, + { + function: 'pbkdf2Sync', + filename: 'node:internal/crypto/pbkdf2', + lineno: 78, + colno: 17 + }, + { + function: 'longWork', + filename: '/app/test.js', + lineno: 20, + colno: 29 + }, + { + function: '?', + filename: '/app/test.js', + lineno: 24, + colno: 1 + } + ] + }, + '2': { // Worker thread + frames: [ + { + function: 'from', + filename: 'node:buffer', + lineno: 298, + colno: 28 + }, + { + function: 'pbkdf2Sync', + filename: 'node:internal/crypto/pbkdf2', + lineno: 78, + colno: 17 + }, + { + function: 'longWork', + filename: '/app/worker.js', + lineno: 10, + colno: 29 + }, + { + function: '?', + filename: '/app/worker.js', + lineno: 14, + colno: 1 + } + ] + } } ``` @@ -179,12 +187,17 @@ type StackFrame = { }; ``` -#### `threadPoll(state?: State): void` +#### `threadPoll(state?: State, disableLastSeen?: boolean): void` Sends a heartbeat from the current thread with optional state information. The state object will be serialized and included as a JavaScript object with the corresponding stack trace. +- `state` (optional): An object containing state information to include with the + stack trace. +- `disableLastSeen` (optional): If `true`, disables the tracking of the last + seen time for this thread. + #### `getThreadsLastSeen(): Record` Returns the time in milliseconds since each registered thread called diff --git a/module.cc b/module.cc index e8c225b..1056f81 100644 --- a/module.cc +++ b/module.cc @@ -249,7 +249,7 @@ void ThreadPoll(const FunctionCallbackInfo &args) { auto context = isolate->GetCurrentContext(); std::string state_str; - if (args.Length() == 1 && args[0]->IsValue()) { + if (args.Length() > 0 && args[0]->IsValue()) { MaybeLocal maybe_json = v8::JSON::Stringify(context, args[0]); if (!maybe_json.IsEmpty()) { v8::String::Utf8Value utf8_state(isolate, maybe_json.ToLocalChecked()); @@ -261,14 +261,23 @@ void ThreadPoll(const FunctionCallbackInfo &args) { state_str = ""; } + bool disable_last_seen = false; + if (args.Length() > 1 && args[1]->IsBoolean()) { + disable_last_seen = args[1]->BooleanValue(isolate); + } + { std::lock_guard lock(threads_mutex); auto found = threads.find(isolate); if (found != threads.end()) { auto &thread_info = found->second; thread_info.state = state_str; - thread_info.last_seen = - duration_cast(system_clock::now().time_since_epoch()); + if (disable_last_seen) { + thread_info.last_seen = milliseconds::zero(); + } else { + thread_info.last_seen = + duration_cast(system_clock::now().time_since_epoch()); + } } } } diff --git a/src/index.ts b/src/index.ts index c05b75c..a73a1df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ type StackFrame = { interface Native { registerThread(threadName: string): void; - threadPoll(state?: object): void; + threadPoll(state?: object, disableLastSeen?: boolean): void; captureStackTrace(): Record>; getThreadsLastSeen(): Record; } @@ -187,10 +187,11 @@ export function registerThread(threadName: string = String(threadId)): void { * Tells the native module that the thread is still running and updates the state. * * @param state Optional state to pass to the native module. + * @param disableLastSeen If true, disables the last seen tracking for this thread. */ -export function threadPoll(state?: object): void { - if (typeof state === 'object') { - native.threadPoll(state); +export function threadPoll(state?: object, disableLastSeen?: boolean): void { + if (typeof state === 'object' || disableLastSeen) { + native.threadPoll(state, disableLastSeen); } else { native.threadPoll(); } diff --git a/test/e2e.test.mjs b/test/e2e.test.mjs index d97e76a..0676309 100644 --- a/test/e2e.test.mjs +++ b/test/e2e.test.mjs @@ -89,4 +89,13 @@ describe('e2e Tests', { timeout: 20000 }, () => { expect(stacks['2'].frames.length).toEqual(1); }); + + test('can be disabled', { timeout: 20000 }, () => { + const testFile = join(__dirname, 'stalled-disabled.js'); + const result = spawnSync('node', [testFile]); + + expect(result.status).toEqual(0); + + expect(result.stdout.toString()).toContain('complete'); + }); }); diff --git a/test/stalled-disabled.js b/test/stalled-disabled.js new file mode 100644 index 0000000..acb4263 --- /dev/null +++ b/test/stalled-disabled.js @@ -0,0 +1,24 @@ +const { Worker } = require('node:worker_threads'); +const { longWork } = require('./long-work.js'); +const { registerThread, threadPoll } = require('@sentry-internal/node-native-stacktrace'); + +registerThread(); + +setInterval(() => { + threadPoll({ some_property: 'some_value' }, true); +}, 200).unref(); + +const watchdog = new Worker('./test/stalled-watchdog.js'); +watchdog.on('exit', () => process.exit(0)); + +const worker = new Worker('./test/worker-do-nothing.js'); + +setTimeout(() => { + longWork(); + + setTimeout(() => { + console.log('complete'); + process.exit(0); + }, 1000); +}, 2000); +