From 8a582c0000825c0ac842bbf12c0a83d2f2bc0a1d Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 25 Jun 2025 12:56:51 +0200 Subject: [PATCH 1/3] meta: Improve `README.md`, `package.json` metadata and add `LICENSE` --- LICENSE | 21 +++++++++ README.md | 125 +++++++++++++++++++++++++++++++++++++-------------- package.json | 20 +++++++-- 3 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0da96cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 574dddb..c1f7c83 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,40 @@ # `@sentry-internal/node-native-stacktrace` -Native Node module to capture stack traces from all registered threads. +A native Node.js module that can capture JavaScript stack traces for registered +main or worker threads, even if event loops are blocked. -This allows capturing main and worker thread stack traces from another watchdog -thread, even if the event loops are blocked. +The module provides the means to create a watchdog system to track event loop +blocking via periodic heartbeats. When the time from the last heartbeat crosses +a threshold, JavaScript stack traces can be captured. The heartbeats can +optionally include state information which is included with the corresponding +stack trace. -In the main or worker threads: +## Basic Usage + +### 1. Register threads you want to monitor + +In your main thread or worker threads: ```ts -const { registerThread } = require("@sentry-internal/node-native-stacktrace"); +import { registerThread } from "@sentry-internal/node-native-stacktrace"; +// Register this thread for monitoring registerThread(); ``` -Watchdog thread: +### 2. Capture stack traces from any thread ```ts -const { - captureStackTrace, -} = require("@sentry-internal/node-native-stacktrace"); +import { captureStackTrace } from "@sentry-internal/node-native-stacktrace"; +// Capture stack traces from all registered threads const stacks = captureStackTrace(); console.log(stacks); ``` -Results in: +### Example Output + +Stack traces show where each thread is currently executing: ```js { @@ -83,50 +93,99 @@ Results in: } ``` -## Detecting blocked event loops +## Advanced Usage: Automatic blocked event loop Detection + +Set up automatic detection of blocked event loops: + +### 1. Set up thread heartbeats -In the main or worker threads if you call `registerThread()` regularly, times -are recorded. +Send regular heartbeats with optional state information: ```ts -const { +import { registerThread, threadPoll, -} = require("@sentry-internal/node-native-stacktrace"); +} from "@sentry-internal/node-native-stacktrace"; +// Register this thread registerThread(); +// Send heartbeats every 200ms with optional state setInterval(() => { - threadPoll({ optional_state: "some_value" }); + threadPoll({ + endpoint: "/api/current-request", + userId: getCurrentUserId(), + }); }, 200); ``` -In the watchdog thread you can call `getThreadsLastSeen()` to get how long it's -been in milliseconds since each thread polled. +### 2. Monitor from a watchdog thread -If any thread has exceeded a threshold, you can call `captureStackTrace()` to -get the stack traces for all threads. +Monitor all registered threads from a dedicated thread: ```ts -const { +import { captureStackTrace, getThreadsLastSeen, -} = require("@sentry-internal/node-native-stacktrace"); +} from "@sentry-internal/node-native-stacktrace"; const THRESHOLD = 1000; // 1 second setInterval(() => { - for (const [thread, time] in Object.entries(getThreadsLastSeen())) { - if (time > THRESHOLD) { - const threads = captureStackTrace(); - const blockedThread = threads[thread]; - const { frames, state } = blockedThread; - console.log( - `Thread '${thread}' blocked more than ${THRESHOLD}ms`, - frames, - state, - ); + const threadsLastSeen = getThreadsLastSeen(); + + for (const [threadId, timeSinceLastSeen] of Object.entries(threadsLastSeen)) { + if (timeSinceLastSeen > THRESHOLD) { + // Thread appears to be blocked - capture diagnostics + const stackTraces = captureStackTrace(); + const blockedThread = stackTraces[threadId]; + + console.error(`🚨 Thread ${threadId} blocked for ${timeSinceLastSeen}ms`); + console.error("Stack trace:", blockedThread.frames); + console.error("Last known state:", blockedThread.state); } } -}, 1000); +}, 500); // Check every 500ms +``` + +## API Reference + +### Functions + +#### `registerThread(threadName?: string): void` + +Registers the current thread for monitoring. Must be called from each thread you +want to capture stack traces from. + +- `threadName` (optional): Name for the thread. Defaults to the current thread + ID. + +#### `captureStackTrace(): Record>` + +Captures stack traces from all registered threads. Can be called from any thread +but will not capture the stack trace of the calling thread itself. + +```ts +type Thread = { + frames: StackFrame[]; + state?: S; +}; + +type StackFrame = { + function: string; + filename: string; + lineno: number; + colno: number; +}; ``` + +#### `threadPoll(state?: State): 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. + +#### `getThreadsLastSeen(): Record` + +Returns the time in milliseconds since each registered thread called +`threadPoll()`. diff --git a/package.json b/package.json index f658921..c8e6774 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,18 @@ "version": "0.1.0", "main": "lib/index.js", "types": "lib/index.d.ts", + "repository": "git://github.com/getsentry/sentry-javascript-node-native-stacktrace.git", + "homepage": "https://github.com/getsentry/sentry-javascript-node-native-stacktrace", + "author": "Sentry", "license": "MIT", + "description": "A native Node.js module that can capture JavaScript stack traces from main and worker threads, even with blocked event loops.", + "keywords": [ + "stacktrace", + "native", + "nodejs", + "worker", + "sentry" + ], "scripts": { "install": "node scripts/check-build.mjs", "lint": "yarn lint:eslint && yarn lint:clang", @@ -23,8 +34,8 @@ "clean": "node-gyp clean && rm -rf lib && rm -rf build", "test": "node ./test/prepare.mjs && vitest run --silent=false --disable-console-intercept" }, - "volta": { - "node": "24.1.0" + "engines": { + "node": ">=18" }, "dependencies": { "detect-libc": "^2.0.4", @@ -51,5 +62,8 @@ ], "publishConfig": { "access": "public" + }, + "volta": { + "node": "24.1.0" } -} +} \ No newline at end of file From 2ee5f894e1b7d93f30f23a22045c375a47d8aa61 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 25 Jun 2025 12:58:27 +0200 Subject: [PATCH 2/3] More --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1f7c83..41382aa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # `@sentry-internal/node-native-stacktrace` A native Node.js module that can capture JavaScript stack traces for registered -main or worker threads, even if event loops are blocked. +main or worker threads from any other thread, even if event loops are blocked. -The module provides the means to create a watchdog system to track event loop +The module also provides a means to create a watchdog system to track event loop blocking via periodic heartbeats. When the time from the last heartbeat crosses a threshold, JavaScript stack traces can be captured. The heartbeats can optionally include state information which is included with the corresponding From e19d2404f865c5208ed64ede3933db35f55b4dad Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 25 Jun 2025 17:26:23 +0200 Subject: [PATCH 3/3] add CHANGELOG entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5c9bb..957962e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Changelog + +## 0.1.1 + +- meta: Improve `README.md`, `package.json` metadata and add `LICENSE` (#10) + ## 0.1.0 Initial release