Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
extends: ['@sentry-internal/sdk'],
env: {
node: true,
es6: true,
es2020: true
},
parserOptions: {
sourceType: 'module',
Expand Down
70 changes: 45 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ main or worker threads from any other thread, even if 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.
a threshold, JavaScript stack traces can be captured.

For Node.js >= v24, this module can also capture state from `AsyncLocalStorage`
at the time of stack trace capture, which can help provide context on what the
thread was working on when it became blocked.

This native module is used for Sentry's
[Event Loop Blocked Detection](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/event-loop-block/)
Expand Down Expand Up @@ -70,7 +72,7 @@ Stack traces show where each thread is currently executing:
}
]
},
'2': { // Worker thread
'2': { // Worker thread
frames: [
{
function: 'from',
Expand Down Expand Up @@ -105,25 +107,28 @@ Stack traces show where each thread is currently executing:

Set up automatic detection of blocked event loops:

### 1. Set up thread heartbeats
### 1. Register threads with `AsyncLocalStorage` state tracking and heartbeats

Send regular heartbeats with optional state information:
Send regular heartbeats:

```ts
import {
registerThread,
threadPoll,
} from "@sentry-internal/node-native-stacktrace";
import { AsyncLocalStorage } from "node:async_hooks";

// Register this thread
registerThread();
// Create async local storage for state tracking
const asyncLocalStorage = new AsyncLocalStorage();
// Set some state in the async local storage
asyncLocalStorage.enterWith({ someState: "value" });

// Send heartbeats every 200ms with optional state
// Register this thread with async local storage
registerThread({ asyncLocalStorage });

// Send heartbeats every 200ms
setInterval(() => {
threadPoll({
endpoint: "/api/current-request",
userId: getCurrentUserId(),
});
threadPoll();
}, 200);
```

Expand All @@ -150,7 +155,7 @@ setInterval(() => {

console.error(`🚨 Thread ${threadId} blocked for ${timeSinceLastSeen}ms`);
console.error("Stack trace:", blockedThread.frames);
console.error("Last known state:", blockedThread.state);
console.error("Async state:", blockedThread.asyncState);
}
}
}, 500); // Check every 500ms
Expand All @@ -162,21 +167,37 @@ setInterval(() => {

#### `registerThread(threadName?: string): void`

Registers the current thread for monitoring. Must be called from each thread you
want to capture stack traces from.
#### `registerThread(asyncStorage: AsyncStorageArgs, threadName?: string): void`

Registers the current thread for stack trace capture. 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.
- `asyncStorage`: `AsyncStorageArgs` to fetch state from `AsyncLocalStorage` on
stack trace capture.

```ts
type AsyncStorageArgs = {
// AsyncLocalStorage instance to fetch state from
asyncLocalStorage: AsyncLocalStorage<unknown>;
// Optional key to fetch specific property from the store object
storageKey?: string | symbol;
};
```

#### `captureStackTrace<State>(): Record<string, Thread<State>>`
#### `captureStackTrace<State>(): Record<string, Thread<A, P>>`

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> = {
type Thread<A = unknown, P = unknown> = {
frames: StackFrame[];
state?: S;
/** State captured from the AsyncLocalStorage */
asyncState?: A;
/** Optional state provided when calling threadPoll */
pollState?: P;
};

type StackFrame = {
Expand All @@ -187,16 +208,15 @@ type StackFrame = {
};
```

#### `threadPoll<State>(state?: State, disableLastSeen?: boolean): void`
#### `threadPoll<State>(disableLastSeen?: boolean, pollState?: object): 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.
Sends a heartbeat from the current thread.

- `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.
- `pollState` (optional): An object containing state to include with the next
stack trace capture. This can be used instead of or in addition to
`AsyncLocalStorage` based state tracking.

#### `getThreadsLastSeen(): Record<string, number>`

Expand Down
Loading
Loading