Skip to content

Conversation

@logaretm
Copy link
Collaborator

@logaretm logaretm commented Jan 5, 2026

Currently the cache components feature in Next.js prevents us from using any random value APIs like:

  • Date.now
  • performance.now
  • Math.random
  • crypto.*

We tried resolving this by patching several span methods, but then we have plenty of other instances where we use those APIs, like in trace propagation, timestamp generation for logs, and more.

Running around and patching them one by one in the Next.js SDK isn't a viable solution since most of those functionalities are strictly internal and cannot be patched from the outside, and adding escape hatches for each of them is not maintainable.

So I'm testing out the other way around, by hunting those APIs down and wrapping them with a safe runner that acts as an escape hatch. Some of the Vercel engineers suggested doing that, but we need to do it for almost every call (see Josh comment below).

The idea is an SDK can "turn on" the safe runner by injecting a global function that executes a callback and returns its results. I

How does this fix it for Next.js?

The Next.js SDK case, a safe runner would be an AsyncLocalStorage snapshot which is captured at the server runtime init, way before any rendering is done.

const sym = Symbol.for('__SENTRY_SAFE_RANDOM_ID_WRAPPER__');
const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: SafeRandomContextRunner } = GLOBAL_OBJ;

globalWithSymbol[sym] = AsyncLocalStorage.snapshot();

// core SDK then offers a fn to run any random gen function
export function withRandomSafeContext<T>(cb: () => T): T {
  // Looks for the global symbol and if it is set it uses the runner
  // otherwise just runs the callback normally.
}

I kept the API internal as much as possible to avoid users messing up with it, but the @sentry/opentelemetry SDK also needed this functionality so I exported the API with _INTERNAL prefix as we already do.


I tested this in a simple Next.js app and it no longer errors out, and all current tests pass. I still need to take a look at the traces and see how would they look in cached component cases.

Charly is already working on this and may have a proper solution, but I thought to just see if we can ship a stopgap until then.

On the bright side, this seems to fix it as well for Webpack.

closes #18392
closes #18340

@linear
Copy link

linear bot commented Jan 5, 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 24.92 kB +0.33% +80 B 🔺
@sentry/browser - with treeshaking flags 23.43 kB +0.38% +87 B 🔺
@sentry/browser (incl. Tracing) 41.7 kB +0.27% +110 B 🔺
@sentry/browser (incl. Tracing, Profiling) 46.27 kB +0.21% +93 B 🔺
@sentry/browser (incl. Tracing, Replay) 80.3 kB +0.18% +139 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.01 kB +0.16% +107 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 84.98 kB +0.17% +137 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 97.21 kB +0.12% +108 B 🔺
@sentry/browser (incl. Feedback) 41.64 kB +0.18% +71 B 🔺
@sentry/browser (incl. sendFeedback) 29.61 kB +0.29% +84 B 🔺
@sentry/browser (incl. FeedbackAsync) 34.62 kB +0.25% +86 B 🔺
@sentry/browser (incl. Metrics) 25.93 kB +0.31% +80 B 🔺
@sentry/browser (incl. Logs) 26.16 kB +0.35% +89 B 🔺
@sentry/browser (incl. Metrics & Logs) 26.91 kB +0.34% +89 B 🔺
@sentry/react 26.67 kB +0.32% +85 B 🔺
@sentry/react (incl. Tracing) 43.93 kB +0.25% +109 B 🔺
@sentry/vue 29.41 kB +0.36% +105 B 🔺
@sentry/vue (incl. Tracing) 43.53 kB +0.29% +123 B 🔺
@sentry/svelte 24.95 kB +0.36% +88 B 🔺
CDN Bundle 27.36 kB +0.38% +103 B 🔺
CDN Bundle (incl. Tracing) 42.34 kB +0.25% +104 B 🔺
CDN Bundle (incl. Tracing, Replay) 79.05 kB +0.13% +98 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 84.52 kB +0.12% +98 B 🔺
CDN Bundle - uncompressed 80.21 kB +0.24% +188 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 125.6 kB +0.16% +194 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 242.13 kB +0.09% +194 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 254.92 kB +0.08% +194 B 🔺
@sentry/nextjs (client) 46.28 kB +0.22% +98 B 🔺
@sentry/sveltekit (client) 42.05 kB +0.23% +93 B 🔺
@sentry/node-core 51.77 kB +0.15% +73 B 🔺
@sentry/node 161.89 kB +0.07% +101 B 🔺
@sentry/node - without tracing 93.21 kB +0.1% +86 B 🔺
@sentry/aws-serverless 108.72 kB +0.08% +85 B 🔺

View base workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 9,422 - 8,952 +5%
GET With Sentry 1,765 19% 1,730 +2%
GET With Sentry (error only) 6,081 65% 6,208 -2%
POST Baseline 1,212 - 1,173 +3%
POST With Sentry 605 50% 601 +1%
POST With Sentry (error only) 1,078 89% 1,053 +2%
MYSQL Baseline 3,368 - 3,338 +1%
MYSQL With Sentry 481 14% 509 -6%
MYSQL With Sentry (error only) 2,768 82% 2,695 +3%

View base workflow run

@logaretm logaretm force-pushed the awad/js-1250-generatemetadata-breaks-cachecomponents branch from 8138056 to a5c2c86 Compare January 5, 2026 14:14
Comment on lines 10 to 25
export function runInRandomSafeContext<T>(cb: () => T): T {
// Skips future symbol lookups if we've already resolved the runner once
if (RESOLVED_RUNNER) {
return RESOLVED_RUNNER(cb);
}

const sym = Symbol.for('__SENTRY_SAFE_RANDOM_ID_WRAPPER__');
const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: SafeRandomContextRunner } = GLOBAL_OBJ;
if (!(sym in globalWithSymbol) || typeof globalWithSymbol[sym] !== 'function') {
return cb();
}

RESOLVED_RUNNER = globalWithSymbol[sym];

return globalWithSymbol[sym](cb);
}
Copy link
Collaborator Author

@logaretm logaretm Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reviewer: I think we can reduce global symbol lookups overhead by instead having a setter API here, but that needs to be exposed and won't be a "secret" escape hatch anymore even if we prefix it with __INTERNAL.

would it be worth it? given that the random functions are used frequently.

Another concern is serverless runtimes, I think the current approach is safe against them because the lookup happens every time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean setting the snapshot on some internal property directly in nextjs?

Copy link
Collaborator Author

@logaretm logaretm Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, so something like this:

// core
export function setRandomSafeContextRunner(wrapper) {
  // still sets on global but we don't need to lookup the symbol anymore
  global['__sentry_rnd_runner'] = wrapper;
}

Then the Next.js SDK can do:

import { _INTERNAL_setRandomSafeContextRunner }  from '@sentry/core';

_INTERNAL_setRandomSafeContextRunner(AsyncLocalStorage.snapshot());

The global symbol register should be fast tho, so maybe this isn't needed especially since I don't want users to be able to use this.

@logaretm logaretm requested a review from chargome January 6, 2026 15:18
Copy link
Member

@chargome chargome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline I'm really happy that this indeed works! 🚀
I generally wanted to avoid having to adapt any core logic to accommodate for cacheComponents, but we could revert once Vercel maybe ships an escape hatch.

* Which will generate and set a trace id in the propagation context, which should trigger the random API error if unpatched
* See: https://github.com/getsentry/sentry-javascript/issues/18392
*/
export function generateMetadata() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expand this test case, or better duplicate it and run some async computation within generateMetadata?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing, I will add some logic there

Comment on lines 10 to 25
export function runInRandomSafeContext<T>(cb: () => T): T {
// Skips future symbol lookups if we've already resolved the runner once
if (RESOLVED_RUNNER) {
return RESOLVED_RUNNER(cb);
}

const sym = Symbol.for('__SENTRY_SAFE_RANDOM_ID_WRAPPER__');
const globalWithSymbol: typeof GLOBAL_OBJ & { [sym]?: SafeRandomContextRunner } = GLOBAL_OBJ;
if (!(sym in globalWithSymbol) || typeof globalWithSymbol[sym] !== 'function') {
return cb();
}

RESOLVED_RUNNER = globalWithSymbol[sym];

return globalWithSymbol[sym](cb);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean setting the snapshot on some internal property directly in nextjs?

@chargome chargome requested a review from Lms24 January 7, 2026 09:58
@logaretm logaretm force-pushed the awad/js-1250-generatemetadata-breaks-cachecomponents branch from c581f30 to f379ed5 Compare January 7, 2026 12:33
@logaretm logaretm marked this pull request as ready for review January 7, 2026 13:25
@logaretm logaretm requested review from Copilot and removed request for Copilot January 7, 2026 13:25
Copilot AI review requested due to automatic review settings January 7, 2026 14:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a safe runner mechanism to wrap random and time-based APIs (Date.now, Math.random, performance.now, crypto.*) to prevent errors in Next.js cache components. It adds a new ESLint rule to enforce consistent usage of these wrapped APIs across the codebase.

Key Changes:

  • Implemented runInRandomSafeContext() wrapper and safe helper functions (safeDateNow(), safeMathRandom()) in @sentry/core
  • Added Next.js-specific initialization that prepares AsyncLocalStorage snapshot for safe context execution
  • Created ESLint rule no-unsafe-random-apis to enforce usage of safe wrappers in designated packages
  • Updated all existing random/time API calls to use the safe wrappers across core, node, opentelemetry, and Next.js packages

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/core/src/utils/safeRandomGeneratorRunner.ts Core implementation of safe runner mechanism with helper functions for Date.now() and Math.random()
packages/core/src/index.ts Exports safe runner APIs with _INTERNAL prefix for use by other SDK packages
packages/core/src/utils/time.ts Updates time-related functions to use runInRandomSafeContext wrapper
packages/core/src/utils/tracing.ts Updates trace propagation to use safeMathRandom()
packages/core/src/scope.ts Updates propagation context generation to use safeMathRandom()
packages/core/src/tracing/trace.ts Updates trace initialization to use safeMathRandom()
packages/core/src/utils/misc.ts Updates UUID generation to use safe wrappers
packages/core/src/utils/ratelimit.ts Updates rate limiting to use safeDateNow()
packages/core/src/client.ts Updates error sampling to use safeMathRandom()
packages/core/src/integrations/mcp-server/correlation.ts Updates span storage to use safeDateNow()
packages/nextjs/src/server/prepareSafeIdGeneratorContext.ts Next.js-specific implementation that prepares AsyncLocalStorage snapshot as safe context runner
packages/nextjs/src/server/index.ts Calls prepareSafeIdGeneratorContext() at SDK initialization
packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts Updates cron monitoring to use _INTERNAL_safeDateNow()
packages/nextjs/src/config/withSentryConfig.ts Adds eslint-disable comment for intentional Math.random() usage in tunnel route generation
packages/nextjs/src/config/polyfills/perf_hooks.js Adds eslint-disable comment for polyfill code
packages/node-core/src/integrations/context.ts Updates context integration to use _INTERNAL_safeDateNow()
packages/opentelemetry/src/spanExporter.ts Updates span exporter timestamp handling to use _INTERNAL_safeDateNow()
packages/opentelemetry/src/sampler.ts Updates sampling to use _INTERNAL_safeMathRandom()
packages/eslint-plugin-sdk/src/rules/no-unsafe-random-apis.js New ESLint rule to enforce wrapping of random/time APIs
packages/eslint-plugin-sdk/src/index.js Registers the new ESLint rule
packages/eslint-plugin-sdk/test/lib/rules/no-unsafe-random-apis.test.ts Test suite for the new ESLint rule
packages/core/.eslintrc.js Enables no-unsafe-random-apis rule with test file exceptions
packages/nextjs/.eslintrc.js Enables no-unsafe-random-apis rule with test file exceptions
packages/node/.eslintrc.js Enables no-unsafe-random-apis rule with test file exceptions
packages/node-core/.eslintrc.js Enables no-unsafe-random-apis rule with test file exceptions
packages/opentelemetry/.eslintrc.js Enables no-unsafe-random-apis rule with test file exceptions
packages/vercel-edge/.eslintrc.js Enables no-unsafe-random-apis rule with test file exceptions
dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts Adds E2E tests for metadata generation in cache components
dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/metadata/page.tsx Test page for synchronous metadata generation with cache components
dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/metadata-async/page.tsx Test page for asynchronous metadata generation with cache components
.size-limit.js Updates bundle size limits to account for new safe runner code

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@logaretm logaretm force-pushed the awad/js-1250-generatemetadata-breaks-cachecomponents branch from 02878ec to f3c7b35 Compare January 7, 2026 15:49

/** Inits the Sentry NextJS SDK on node. */
export function init(options: NodeOptions): NodeClient | undefined {
prepareSafeIdGeneratorContext();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge runtime missing safe random context setup

Medium Severity

The server runtime calls prepareSafeIdGeneratorContext() at the start of init() to set up the safe random context for cache components, but the edge runtime's init() function does not make this call. Both runtimes export the cache-aware span methods from nextSpan.ts, suggesting cache components are supported in both. If AsyncLocalStorage.snapshot() is available in the Vercel Edge runtime, random API calls in cache component contexts would still trigger errors because the safe context isn't set up. The edge runtime should also attempt to call prepareSafeIdGeneratorContext(), which will gracefully handle cases where the required APIs aren't available.

Fix in Cursor Fix in Web

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional, we don't want to set that up there.

@gnoff
Copy link

gnoff commented Jan 7, 2026

New Eslint Rule: no-unsafe-random-apis

I realize this is hard to maintain, so I've setup an eslint rule to error out in Next dependency SDKs:

@sentry/next
@sentry/core
@sentry/opentelemetry
@sentry/node -> @sentry/node-core
@sentry/vercel-edge

Whenever any of those APIs is used without the runner it will error out. Probably an overkill, but curious to see how well it works 🤔

So the general approach seems sound but I worry about this kind of lint rule.

Exiting out of the context to generate arbitrary random values is potentially wrong when prerendering with Cache Components in Next.js. The reason it makes sense for Span ids is that these ideally don't ever leak into the render context and thus cannot cause a prerender to observe anything non-deterministic. (They're side effects for telemetry only)

This is actually not always true though b/c if you pass a Cache Function to startActiveSpan then the Span object will get passed into the Cache Function and you've just created a situation where the Cache Function will never be a cache hit b/c it always receives an argument with a random value.

The best remediation for non-determinism is to guard it behind something async. Once you're past the first task of the prerender you can do all the non-deterministic stuff you want.

By linting to ensure you bypass the deterministic check for sync APIs you may create a situation in the future where someone without understanding the full implications adds something that does leak into the render context or flows into a Cache Function and breaks something. The idea with erroring in these cases is it allows you to respond before you ship something broken to production but if you disable the ability to error by exiting the ALS it'll be much harder to detect that this has happened.

I was originally thinking only the Span creation would be exempted. But it seems there are things in here for event processing and more. Are these all necessary?

@logaretm
Copy link
Collaborator Author

logaretm commented Jan 7, 2026

@gnoff Thanks for taking a look!

So the general approach seems sound but I worry about this kind of lint rule.

I thought it would be hard for future maintainers to know when they should exit the ALS as they may not be even working on Next.js related stuff.

Exiting out of the context to generate arbitrary random values is potentially wrong....

Thanks for the explanation! I suppose then a better approach is to NOT patch the random value call sites like I did here and instead wrap the APIs that do need them. Like span ID, trace ID, timestamps for logs and spans, etc. Even if it is duplicated around the codebase, since in those cases we know for a fact it won't be used for a side-effect.

Would that be a better approach? It would require implicit understanding how a certain API interacts with Next.js but would better match the intention of wrapping the non-side effect-y APIs.

I was originally thinking only the Span creation would be exempted. But it seems there are things in here for event processing and more. Are these all necessary?

I can revert them one by one to see which ones are necessary, from my tests I needed to patch only those initially:

  • @sentry/core: Timestamp generation, Span ID, Trace ID
  • @sentry/opentelemetry: The sampler timestamps, exporter timestamps

It was a cat and mouse game, every time I patched one call, there was another which prompted me to see if eslint can catch those.

I will try reverting and keep only the minimal changes and we can give it another look to see if it makes sense instead of sprinkling the wrapper everywhere.

Comment on lines 65 to 72

if (!traceparentData?.traceId) {
return {
traceId: generateTraceId(),
sampleRand: Math.random(),
traceId: withRandomSafeContext(generateTraceId),
sampleRand: withRandomSafeContext(() => Math.random()),
};
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unwrapped Math.random() calls in getSampleRandFromTraceparentAndDsc will crash Next.js cache components during distributed tracing, as this function is called when a sentry-trace header is present.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The function getSampleRandFromTraceparentAndDsc contains unwrapped calls to Math.random(). This function is invoked by propagationContextFromHeaders when an incoming request includes a sentry-trace header, a common scenario for distributed tracing. In a Next.js application, if this request processing occurs within a cache component context (e.g., during generateMetadata), the use of the unwrapped Math.random() will violate Next.js's restrictions and cause the application to crash. This oversight undermines the PR's goal of making random API calls safe in such environments.

💡 Suggested Fix

Wrap the Math.random() calls within the getSampleRandFromTraceparentAndDsc function using the withRandomSafeContext utility. This will ensure that random number generation is compatible with Next.js cache component restrictions.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/core/src/utils/tracing.ts#L65-L72

Potential issue: The function `getSampleRandFromTraceparentAndDsc` contains unwrapped
calls to `Math.random()`. This function is invoked by `propagationContextFromHeaders`
when an incoming request includes a `sentry-trace` header, a common scenario for
distributed tracing. In a Next.js application, if this request processing occurs within
a cache component context (e.g., during `generateMetadata`), the use of the unwrapped
`Math.random()` will violate Next.js's restrictions and cause the application to crash.
This oversight undermines the PR's goal of making random API calls safe in such
environments.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8300189

Copy link
Collaborator Author

@logaretm logaretm Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fixes it 🤦‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

generateMetadata breaks cacheComponents on turbopack Support cacheComponents on webpack

4 participants