Skip to content

Commit 4aeeac7

Browse files
authored
feat: Capture thread state from AsyncLocalStorage store (#24)
1 parent d881dbd commit 4aeeac7

16 files changed

+823
-206
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',

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.3.0
4+
5+
- feat: Capture thread state from `AsyncLocalStorage` store (#24)
6+
37
## 0.2.3
48

59
- fix: Failing install script (#22)

README.md

Lines changed: 57 additions & 26 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,48 @@ 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` (optional): `AsyncStorageArgs` to fetch state from
178+
`AsyncLocalStorage` on stack trace capture.
179+
180+
```ts
181+
type AsyncStorageArgs = {
182+
/** AsyncLocalStorage instance to fetch state from */
183+
asyncLocalStorage: AsyncLocalStorage<unknown>;
184+
/**
185+
* Optional array of keys to pick a specific property from the store.
186+
* Key will be traversed in order through Objects/Maps to reach the desired property.
187+
*
188+
* This is useful if you want to capture Open Telemetry context values as state.
189+
*
190+
* To get this value:
191+
* context.getValue(MY_UNIQUE_SYMBOL_REF)
192+
*
193+
* You would set:
194+
* stateLookup: ['_currentContext', MY_UNIQUE_SYMBOL_REF]
195+
*/
196+
stateLookup?: Array<string | symbol>;
197+
};
198+
```
170199

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

173202
Captures stack traces from all registered threads. Can be called from any thread
174-
but will not capture the stack trace of the calling thread itself.
203+
but will not capture a stack trace for the calling thread itself.
175204

176205
```ts
177-
type Thread<S> = {
206+
type Thread<A = unknown, P = unknown> = {
178207
frames: StackFrame[];
179-
state?: S;
208+
/** State captured from the AsyncLocalStorage */
209+
asyncState?: A;
210+
/** Optional state provided when calling threadPoll */
211+
pollState?: P;
180212
};
181213

182214
type StackFrame = {
@@ -187,16 +219,15 @@ type StackFrame = {
187219
};
188220
```
189221

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

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.
224+
Sends a heartbeat from the current thread.
195225

196-
- `state` (optional): An object containing state information to include with the
197-
stack trace.
198226
- `disableLastSeen` (optional): If `true`, disables the tracking of the last
199227
seen time for this thread.
228+
- `pollState` (optional): An object containing state to include with the next
229+
stack trace capture. This can be used instead of or in addition to
230+
`AsyncLocalStorage` based state tracking.
200231

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

0 commit comments

Comments
 (0)