-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(nextjs): Add ALS runner fallbacks for serverless environments #18889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,4 @@ | ||||||||||||||
| import { | ||||||||||||||
| type _INTERNAL_RandomSafeContextRunner as _INTERNAL_RandomSafeContextRunner, | ||||||||||||||
| debug, | ||||||||||||||
| GLOBAL_OBJ, | ||||||||||||||
| } from '@sentry/core'; | ||||||||||||||
| import { type _INTERNAL_RandomSafeContextRunner as RandomSafeContextRunner, debug, GLOBAL_OBJ } from '@sentry/core'; | ||||||||||||||
| import { DEBUG_BUILD } from '../common/debug-build'; | ||||||||||||||
|
|
||||||||||||||
| // Inline AsyncLocalStorage interface from current types | ||||||||||||||
|
|
@@ -15,17 +11,53 @@ type OriginalAsyncLocalStorage = typeof AsyncLocalStorage; | |||||||||||||
| */ | ||||||||||||||
| export function prepareSafeIdGeneratorContext(): void { | ||||||||||||||
| const sym = Symbol.for('__SENTRY_SAFE_RANDOM_ID_WRAPPER__'); | ||||||||||||||
| const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: _INTERNAL_RandomSafeContextRunner } = GLOBAL_OBJ; | ||||||||||||||
| const als = getAsyncLocalStorage(); | ||||||||||||||
| if (!als || typeof als.snapshot !== 'function') { | ||||||||||||||
| DEBUG_BUILD && | ||||||||||||||
| debug.warn( | ||||||||||||||
| '[@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.', | ||||||||||||||
| ); | ||||||||||||||
| const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: RandomSafeContextRunner } = GLOBAL_OBJ; | ||||||||||||||
|
|
||||||||||||||
| // Get initial snapshot - if unavailable, don't set up the wrapper at all | ||||||||||||||
| const initialSnapshot = getAsyncLocalStorageSnapshot(); | ||||||||||||||
| if (!initialSnapshot) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| globalWithSymbol[sym] = als.snapshot(); | ||||||||||||||
| // We store a wrapper function instead of the raw snapshot because in serverless | ||||||||||||||
| // environments (e.g., Cloudflare Workers), the snapshot is bound to the request | ||||||||||||||
| // context it was created in. Once that request ends, the snapshot becomes invalid. | ||||||||||||||
| // The wrapper catches this and creates a fresh snapshot for the current request context. | ||||||||||||||
| let cachedSnapshot: RandomSafeContextRunner = initialSnapshot; | ||||||||||||||
|
|
||||||||||||||
| globalWithSymbol[sym] = <T>(callback: () => T): T => { | ||||||||||||||
| try { | ||||||||||||||
| return cachedSnapshot(callback); | ||||||||||||||
| } catch (error) { | ||||||||||||||
| // Only handle AsyncLocalStorage-related errors, rethrow others | ||||||||||||||
| if (!isAsyncLocalStorageError(error)) { | ||||||||||||||
| throw error; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Snapshot likely stale, try to get a fresh one and retry | ||||||||||||||
| const freshSnapshot = getAsyncLocalStorageSnapshot(); | ||||||||||||||
| // No snapshot available, fall back to direct execution | ||||||||||||||
| if (!freshSnapshot) { | ||||||||||||||
| return callback(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Update the cached snapshot | ||||||||||||||
| cachedSnapshot = freshSnapshot; | ||||||||||||||
|
|
||||||||||||||
| // Retry the callback with the fresh snapshot | ||||||||||||||
| try { | ||||||||||||||
| return cachedSnapshot(callback); | ||||||||||||||
| } catch (retryError) { | ||||||||||||||
| // Only fall back for AsyncLocalStorage errors, rethrow others | ||||||||||||||
| if (!isAsyncLocalStorageError(retryError)) { | ||||||||||||||
| throw retryError; | ||||||||||||||
| } | ||||||||||||||
| // If fresh snapshot also fails with ALS error, fall back to direct execution | ||||||||||||||
| return callback(); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| }; | ||||||||||||||
logaretm marked this conversation as resolved.
Show resolved
Hide resolved
logaretm marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
|
||||||||||||||
| DEBUG_BUILD && debug.log('[@sentry/nextjs] Prepared safe random ID generator context'); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -47,3 +79,21 @@ function getAsyncLocalStorage(): OriginalAsyncLocalStorage | undefined { | |||||||||||||
|
|
||||||||||||||
| return undefined; | ||||||||||||||
| } | ||||||||||||||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
|
||||||||||||||
| function getAsyncLocalStorageSnapshot(): RandomSafeContextRunner | undefined { | ||||||||||||||
| const als = getAsyncLocalStorage(); | ||||||||||||||
|
|
||||||||||||||
| if (!als || typeof als.snapshot !== 'function') { | ||||||||||||||
| DEBUG_BUILD && | ||||||||||||||
| debug.warn( | ||||||||||||||
| '[@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.', | ||||||||||||||
| ); | ||||||||||||||
| return undefined; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return als.snapshot(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function isAsyncLocalStorageError(error: unknown): boolean { | ||||||||||||||
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); | ||||||||||||||
|
||||||||||||||
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); | |
| return ( | |
| error instanceof Error && | |
| error.message === | |
| 'Cannot call this AsyncLocalStorage bound function outside of the request in which it was created.' | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exact error matching is flimsy IMO, also I am not sure if it would be the exact same error in each serverless environment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also not sure what would be the best call here, I think just AsyncLocalStorage is fine when thrown at this point
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can the snapshot get stale?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it's not the best way to describe this, but this was the closest analogy. This is what I found while debugging, when we set the ALS snapshot on startup:
inittakes effect. It will run with the previous snapshot that was set which would be in a different request context.Or that's what I think is happening.