11import {
2- type _INTERNAL_RandomSafeContextRunner as _INTERNAL_RandomSafeContextRunner ,
2+ type _INTERNAL_RandomSafeContextRunner as RandomSafeContextRunner ,
33 debug ,
44 GLOBAL_OBJ ,
55} from '@sentry/core' ;
@@ -15,17 +15,53 @@ type OriginalAsyncLocalStorage = typeof AsyncLocalStorage;
1515 */
1616export function prepareSafeIdGeneratorContext ( ) : void {
1717 const sym = Symbol . for ( '__SENTRY_SAFE_RANDOM_ID_WRAPPER__' ) ;
18- const globalWithSymbol : typeof GLOBAL_OBJ & { [ sym ] ?: _INTERNAL_RandomSafeContextRunner } = GLOBAL_OBJ ;
19- const als = getAsyncLocalStorage ( ) ;
20- if ( ! als || typeof als . snapshot !== 'function' ) {
21- DEBUG_BUILD &&
22- debug . warn (
23- '[@sentry/nextjs] No AsyncLocalStorage found in the runtime or AsyncLocalStorage.snapshot() is not available, skipping safe random ID generator context preparation, you may see some errors with cache components.' ,
24- ) ;
18+ const globalWithSymbol : typeof GLOBAL_OBJ & { [ sym ] ?: RandomSafeContextRunner } = GLOBAL_OBJ ;
19+
20+ // Get initial snapshot - if unavailable, don't set up the wrapper at all
21+ const initialSnapshot = getAsyncLocalStorageSnapshot ( ) ;
22+ if ( ! initialSnapshot ) {
2523 return ;
2624 }
2725
28- globalWithSymbol [ sym ] = als . snapshot ( ) ;
26+ // We store a wrapper function instead of the raw snapshot because in serverless
27+ // environments (e.g., Cloudflare Workers), the snapshot is bound to the request
28+ // context it was created in. Once that request ends, the snapshot becomes invalid.
29+ // The wrapper catches this and creates a fresh snapshot for the current request context.
30+ let cachedSnapshot : RandomSafeContextRunner = initialSnapshot ;
31+
32+ globalWithSymbol [ sym ] = < T > ( callback : ( ) => T ) : T => {
33+ try {
34+ return cachedSnapshot ( callback ) ;
35+ } catch ( error ) {
36+ // Only handle AsyncLocalStorage-related errors, rethrow others
37+ if ( ! isAsyncLocalStorageError ( error ) ) {
38+ throw error ;
39+ }
40+
41+ // Snapshot likely stale, try to get a fresh one and retry
42+ const freshSnapshot = getAsyncLocalStorageSnapshot ( ) ;
43+ // No snapshot available, fall back to direct execution
44+ if ( ! freshSnapshot ) {
45+ return callback ( ) ;
46+ }
47+
48+ // Update the cached snapshot
49+ cachedSnapshot = freshSnapshot ;
50+
51+ // Retry the callback with the fresh snapshot
52+ try {
53+ return cachedSnapshot ( callback ) ;
54+ } catch ( retryError ) {
55+ // Only fall back for AsyncLocalStorage errors, rethrow others
56+ if ( ! isAsyncLocalStorageError ( retryError ) ) {
57+ throw retryError ;
58+ }
59+ // If fresh snapshot also fails with ALS error, fall back to direct execution
60+ return callback ( ) ;
61+ }
62+ }
63+ } ;
64+
2965 DEBUG_BUILD && debug . log ( '[@sentry/nextjs] Prepared safe random ID generator context' ) ;
3066}
3167
@@ -47,3 +83,21 @@ function getAsyncLocalStorage(): OriginalAsyncLocalStorage | undefined {
4783
4884 return undefined ;
4985}
86+
87+ function getAsyncLocalStorageSnapshot ( ) : RandomSafeContextRunner | undefined {
88+ const als = getAsyncLocalStorage ( ) ;
89+
90+ if ( ! als || typeof als . snapshot !== 'function' ) {
91+ DEBUG_BUILD &&
92+ debug . warn (
93+ '[@sentry/nextjs] No AsyncLocalStorage found in the runtime or AsyncLocalStorage.snapshot() is not available, skipping safe random ID generator context preparation, you may see some errors with cache components.' ,
94+ ) ;
95+ return undefined ;
96+ }
97+
98+ return als . snapshot ( ) ;
99+ }
100+
101+ function isAsyncLocalStorageError ( error : unknown ) : boolean {
102+ return error instanceof Error && error . message . includes ( 'AsyncLocalStorage' ) ;
103+ }
0 commit comments