@@ -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,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
173202Captures 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
182214type 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