Skip to content

meta: Improve README.md, package.json metadata and add LICENSE #10

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Changelog

## 0.1.1

- meta: Improve `README.md`, `package.json` metadata and add `LICENSE` (#10)

## 0.1.0

Initial release
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
125 changes: 92 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 from any other thread, 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 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
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
{
Expand Down Expand Up @@ -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<State>(): Record<string, Thread<State>>`

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<S> = {
frames: StackFrame[];
state?: S;
};

type StackFrame = {
function: string;
filename: string;
lineno: number;
colno: number;
};
```

#### `threadPoll<State>(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<string, number>`

Returns the time in milliseconds since each registered thread called
`threadPoll()`.
20 changes: 17 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -51,5 +62,8 @@
],
"publishConfig": {
"access": "public"
},
"volta": {
"node": "24.1.0"
}
}
}