Skip to content

Commit 4a8292f

Browse files
committed
feat: Capture thread state from AsyncLocalStorage store
1 parent d881dbd commit 4a8292f

13 files changed

+489
-190
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module.exports = {
22
extends: ['@sentry-internal/sdk'],
33
env: {
44
node: true,
5-
es6: true,
5+
es2020: true
66
},
77
parserOptions: {
88
sourceType: 'module',

README.md

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ main or worker threads from any other thread, even if event loops are blocked.
55

66
The module also provides a means to create a watchdog system to track event loop
77
blocking via periodic heartbeats. When the time from the last heartbeat crosses
8-
a threshold, JavaScript stack traces can be captured. The heartbeats can
9-
optionally include state information which is included with the corresponding
10-
stack trace.
8+
a threshold, JavaScript stack traces can be captured.
9+
10+
For Node.js >= v24, this module can also capture state from `AsyncLocalStorage`
11+
at the time of stack trace capture, which can help provide context on what the
12+
thread was working on when it became blocked.
1113

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

106108
Set up automatic detection of blocked event loops:
107109

108-
### 1. Set up thread heartbeats
110+
### 1. Register threads with `AsyncLocalStorage` state tracking and heartbeats
109111

110-
Send regular heartbeats with optional state information:
112+
Send regular heartbeats:
111113

112114
```ts
113115
import {
114116
registerThread,
115117
threadPoll,
116118
} from "@sentry-internal/node-native-stacktrace";
119+
import { AsyncLocalStorage } from "node:async_hooks";
117120

118-
// Register this thread
119-
registerThread();
121+
// Create async local storage for state tracking
122+
const asyncLocalStorage = new AsyncLocalStorage();
123+
// Set some state in the async local storage
124+
asyncLocalStorage.enterWith({ someState: "value" });
120125

121-
// Send heartbeats every 200ms with optional state
126+
// Register this thread with async local storage
127+
registerThread({ asyncLocalStorage });
128+
129+
// Send heartbeats every 200ms
122130
setInterval(() => {
123-
threadPoll({
124-
endpoint: "/api/current-request",
125-
userId: getCurrentUserId(),
126-
});
131+
threadPoll();
127132
}, 200);
128133
```
129134

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

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

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

165-
Registers the current thread for monitoring. Must be called from each thread you
166-
want to capture stack traces from.
170+
#### `registerThread(asyncStorage: AsyncStorageArgs, threadName?: string): void`
171+
172+
Registers the current thread for stack trace capture. Must be called from each
173+
thread you want to capture stack traces from.
167174

168175
- `threadName` (optional): Name for the thread. Defaults to the current thread
169176
ID.
177+
- `asyncStorage`: `AsyncStorageArgs` to fetch state from `AsyncLocalStorage` on
178+
stack trace capture.
179+
180+
```ts
181+
type AsyncStorageArgs = {
182+
// AsyncLocalStorage instance to fetch state from
183+
asyncLocalStorage: AsyncLocalStorage<unknown>;
184+
// Optional key to fetch specific property from the store object
185+
storageKey?: string | symbol;
186+
};
187+
```
170188

171-
#### `captureStackTrace<State>(): Record<string, Thread<State>>`
189+
#### `captureStackTrace<State>(): Record<string, Thread<A, P>>`
172190

173191
Captures stack traces from all registered threads. Can be called from any thread
174192
but will not capture the stack trace of the calling thread itself.
175193

176194
```ts
177-
type Thread<S> = {
195+
type Thread<A = unknown, P = unknown> = {
178196
frames: StackFrame[];
179-
state?: S;
197+
/** State captured from the AsyncLocalStorage */
198+
asyncState?: A;
199+
/** Optional state provided when calling threadPoll */
200+
pollState?: P;
180201
};
181202

182203
type StackFrame = {
@@ -187,16 +208,15 @@ type StackFrame = {
187208
};
188209
```
189210

190-
#### `threadPoll<State>(state?: State, disableLastSeen?: boolean): void`
211+
#### `threadPoll<State>(disableLastSeen?: boolean, pollState?: object): void`
191212

192-
Sends a heartbeat from the current thread with optional state information. The
193-
state object will be serialized and included as a JavaScript object with the
194-
corresponding stack trace.
213+
Sends a heartbeat from the current thread.
195214

196-
- `state` (optional): An object containing state information to include with the
197-
stack trace.
198215
- `disableLastSeen` (optional): If `true`, disables the tracking of the last
199216
seen time for this thread.
217+
- `pollState` (optional): An object containing state to include with the next
218+
stack trace capture. This can be used instead of or in addition to
219+
`AsyncLocalStorage` based state tracking.
200220

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

0 commit comments

Comments
 (0)