@@ -5,9 +5,11 @@ main or worker threads from any other thread, even if event loops are blocked.
55
66The module also provides a means to create a watchdog system to track event loop
77blocking 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
1214This 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
106108Set 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
113115import {
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
122130setInterval (() => {
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
173191Captures stack traces from all registered threads. Can be called from any thread
174192but 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
182203type 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