mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
This PR makes Sync IO behavior more restrictive when `partialPrefetching` is on (either globally or for a page). - Previously, we allowed `await cookies(); Date.now()` in in segments that won't ever be runtime prefetched. This was achieved by separating segments into "early" and "late" stages and only erroring in the "early" ones. - After this PR, it will always be an error to do sync IO anywhere other than after `io()`/`connection()`/uncached IO (i.e. outside the Dynamic stage). We're doing this mainly because with `partialPrefetching`, `cookies()` resolves in App Shells, and sync IO causes a severe deopt in any prerender. `allow-runtime` also opts a route into `partialPrefetching`, which means that even the segments above the runtime prefetch boundary will need an App Shell and can't tolerate Sync IO. This PR removes all early/late stage separation, because we no longer need to vary the Sync IO behavior on the segment. Instead, we now pick whether a stageController should use - `SyncIOMode.AllowedInRuntimeOrDynamic` (legacy) - `SyncIOMode.AllowedInDynamic` (`partialPrefetching`) - `SyncIOMode.Untracked` if it needs to opt out I've removed the existing tests that asserted that sync IO is allowed in segments above the `allow-runtime` boundary. Instead, we check that sync io either errors (if partialPrefetching is on) or is allowed (if partialPrefetching is off) Closes NAR-855
384 lines
19 KiB
TypeScript
384 lines
19 KiB
TypeScript
import { isNextDev, nextTestSetup } from 'e2e-utils'
|
|
|
|
describe('cache-components OTEL spans', () => {
|
|
const { next, isTurbopack, isNextDeploy } = nextTestSetup({
|
|
files: __dirname,
|
|
dependencies: require('./package.json').dependencies,
|
|
// This test sometimes takes longer than the default timeout, extending it bit longer
|
|
// to avoid flakiness.
|
|
startServerTimeout: 15_000,
|
|
})
|
|
|
|
if (isNextDev) {
|
|
it('should allow creating spans during cache component validation without triggering sync IO bailouts - inside a Cache Component - without prerendering the page', async () => {
|
|
const browser = await next.browser('/novel/cache')
|
|
if (isTurbopack) {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Cache",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ <anonymous>
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"<anonymous> app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"Page <anonymous>",
|
|
],
|
|
}
|
|
`)
|
|
} else {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Cache",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ eval
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"eval app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"Page <anonymous>",
|
|
],
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Ideally we would assert the cached/loading status of each test case in dev but there are bugs with warmup renders that make this racey
|
|
// For now we just assert that we actually get the non-zero span ids.
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
expect(parseInt(await t7.textContent())).not.toEqual(0)
|
|
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
expect(parseInt(await t8.textContent())).not.toEqual(0)
|
|
|
|
console.log('t7', await t7.textContent())
|
|
console.log('t8', await t8.textContent())
|
|
})
|
|
it('should allow creating spans during cache component validation without triggering sync IO bailouts - inside a Cache Component - with prerendering the page', async () => {
|
|
// In dev there really isn't any prerendering but since this test case exists for prod testing I want to keep it exercised in the dev pathway too
|
|
const browser = await next.browser('/prerendered/cache')
|
|
if (isTurbopack) {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Cache",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ <anonymous>
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"<anonymous> app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"Page <anonymous>",
|
|
],
|
|
}
|
|
`)
|
|
} else {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Cache",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ eval
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"eval app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"Page <anonymous>",
|
|
],
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Ideally we would assert the cached/loading status of each test case in dev but there are bugs with warmup renders that make this racey
|
|
// For now we just assert that we actually get the non-zero span ids.
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
expect(parseInt(await t7.textContent())).not.toEqual(0)
|
|
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
expect(parseInt(await t8.textContent())).not.toEqual(0)
|
|
|
|
console.log('t7', await t7.textContent())
|
|
console.log('t8', await t8.textContent())
|
|
})
|
|
it('should allow creating spans during cache component validation without triggering sync IO bailouts - inside a Server Component - without prerendering the page', async () => {
|
|
const browser = await next.browser('/novel/server')
|
|
if (isTurbopack) {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Prefetch",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ <anonymous>
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"<anonymous> app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"CachedInnerTraceActiveSpan app/traced-work.tsx (104:9)",
|
|
"Page app/[slug]/server/page.tsx (36:7)",
|
|
],
|
|
}
|
|
`)
|
|
} else {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Prefetch",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ eval
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"eval app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"CachedInnerTraceActiveSpan app/traced-work.tsx (104:9)",
|
|
"Page app/[slug]/server/page.tsx (36:7)",
|
|
],
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Ideally we would assert the cached/loading status of each test case in dev but there are bugs with warmup renders that make this racey
|
|
// For now we just assert that we actually get the non-zero span ids.
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
expect(parseInt(await t7.textContent())).not.toEqual(0)
|
|
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
expect(parseInt(await t8.textContent())).not.toEqual(0)
|
|
|
|
console.log('t7', await t7.textContent())
|
|
console.log('t8', await t8.textContent())
|
|
})
|
|
it('should allow creating spans during cache component validation without triggering sync IO bailouts - inside a Server Component - with prerendering the page', async () => {
|
|
// In dev there really isn't any prerendering but since this test case exists for prod testing I want to keep it exercised in the dev pathway too
|
|
const browser = await next.browser('/prerendered/server')
|
|
if (isTurbopack) {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Prerender",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ <anonymous>
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"<anonymous> app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"CachedInnerTraceActiveSpan app/traced-work.tsx (104:9)",
|
|
"Page app/[slug]/server/page.tsx (36:7)",
|
|
],
|
|
}
|
|
`)
|
|
} else {
|
|
await expect(browser).toDisplayCollapsedRedbox(`
|
|
{
|
|
"code": "E394",
|
|
"description": "A Cache Function (\`use cache\`) was passed to startActiveSpan which means it will receive a Span argument with a possibly random ID on every invocation leading to cache misses. Provide a wrapping function around the Cache Function that does not forward the Span argument to avoid this issue.",
|
|
"environmentLabel": "Prerender",
|
|
"label": "Console Error",
|
|
"source": "app/traced-work.tsx (26:19) @ eval
|
|
> 26 | return tracer.startActiveSpan('span-active-span', fn)
|
|
| ^",
|
|
"stack": [
|
|
"eval app/traced-work.tsx (26:19)",
|
|
"Inner app/traced-work.tsx (97:26)",
|
|
"CachedInnerTraceActiveSpan app/traced-work.tsx (104:9)",
|
|
"Page app/[slug]/server/page.tsx (36:7)",
|
|
],
|
|
}
|
|
`)
|
|
}
|
|
|
|
// Ideally we would assert the cached/loading status of each test case in dev but there are bugs with warmup renders that make this racey
|
|
// For now we just assert that we actually get the non-zero span ids.
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
expect(parseInt(await t7.textContent())).not.toEqual(0)
|
|
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
expect(parseInt(await t8.textContent())).not.toEqual(0)
|
|
|
|
console.log('t7', await t7.textContent())
|
|
console.log('t8', await t8.textContent())
|
|
})
|
|
} else {
|
|
it('should allow creating Spans during prerendering during the build - inside a Cache Components', async () => {
|
|
const browser = await next.browser('/prerendered/cache')
|
|
{
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t7.textContent())).toEqual(0)
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t8.textContent())).toEqual(0)
|
|
|
|
// load again
|
|
await browser.loadPage(`${next.url}/prerendered/cache`)
|
|
const t7again = await browser.elementByCss('#t7 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t7again.textContent())).toEqual(0)
|
|
const t8again = await browser.elementByCss('#t8 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t8again.textContent())).toEqual(0)
|
|
}
|
|
|
|
{
|
|
await browser.loadPage(`${next.url}/prerendered/server`)
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t7.textContent())).toEqual(0)
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t8.textContent())).toEqual(0)
|
|
|
|
// load again
|
|
await browser.loadPage(`${next.url}/prerendered/server`)
|
|
const t7again = await browser.elementByCss('#t7 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t7again.textContent())).toEqual(0)
|
|
const t8again = await browser.elementByCss('#t8 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t8again.textContent())).toEqual(0)
|
|
}
|
|
|
|
{
|
|
await browser.loadPage(`${next.url}/prerendered/fallback`)
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t7.textContent())).toEqual(0)
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t8.textContent())).toEqual(0)
|
|
|
|
// load again
|
|
await browser.loadPage(`${next.url}/prerendered/fallback`)
|
|
const t7again = await browser.elementByCss('#t7 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t7again.textContent())).toEqual(0)
|
|
const t8again = await browser.elementByCss('#t8 .span')
|
|
// the span was prerendered during the build
|
|
expect(parseInt(await t8again.textContent())).toEqual(0)
|
|
}
|
|
})
|
|
it('should allow creating Spans during prerendering at runtime - inside a Cache Components', async () => {
|
|
const browser = await next.browser('/novel/cache')
|
|
{
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
const t7value = parseInt(await t7.textContent())
|
|
// the span was prerendered at runtime
|
|
expect(t7value).not.toEqual(0)
|
|
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
const t8value = parseInt(await t8.textContent())
|
|
// the span was prerendered at runtime
|
|
expect(t8value).not.toEqual(0)
|
|
|
|
// load again
|
|
await browser.loadPage(`${next.url}/novel/cache`)
|
|
const t7again = await browser.elementByCss('#t7 .span')
|
|
const t7againValue = parseInt(await t7again.textContent())
|
|
// this page was cached so the span should be cached too
|
|
expect(t7againValue).toEqual(t7value)
|
|
|
|
const t8again = await browser.elementByCss('#t8 .span')
|
|
const t8againValue = parseInt(await t8again.textContent())
|
|
// this page was cached so the span should be cached too
|
|
expect(t8againValue).toEqual(t8value)
|
|
}
|
|
|
|
{
|
|
await browser.loadPage(`${next.url}/novel/server`)
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
const t7value = parseInt(await t7.textContent())
|
|
// the span was prerendered at runtime
|
|
expect(t7value).not.toEqual(0)
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
const t8value = parseInt(await t8.textContent())
|
|
// the span was prerendered at runtime
|
|
expect(t8value).not.toEqual(0)
|
|
|
|
// load again
|
|
await browser.loadPage(`${next.url}/novel/server`)
|
|
const t7again = await browser.elementByCss('#t7 .span')
|
|
const t7againValue = parseInt(await t7again.textContent())
|
|
const t8again = await browser.elementByCss('#t8 .span')
|
|
const t8againValue = parseInt(await t8again.textContent())
|
|
// this page was cached so the spans should be cached too
|
|
// TODO: Normally we'd expect the first request to be a blocking
|
|
// prerender for the unknown param, which means the served response is
|
|
// the same response that's saved and served on subsequent requests.
|
|
// However, this appeared to have regressed recently with `next start`,
|
|
// so instead a dynamic SSR response is served on the first request, and
|
|
// in the background the prerendered response is generated and saved for
|
|
// subsequent requests. This means the first request's span values are
|
|
// different from the second request's span values. When this regression
|
|
// is fixed, the following assertions should be consolidated to just
|
|
// assert that the second request's span values equal the first
|
|
// request's span values. The failure is masked in CI because of the
|
|
// built-in jest retry. On the retry attempt the requests use the
|
|
// prerendered response from the first attempt, thus making the test
|
|
// succeed. That retry behavior is disabled though when the test is run
|
|
// in the flaky detection CI job, which is why it fails whenever it is
|
|
// touched.
|
|
if (isNextDeploy) {
|
|
expect(t7againValue).toEqual(t7value)
|
|
expect(t8againValue).toEqual(t8value)
|
|
} else {
|
|
expect(t7againValue).not.toEqual(t7value)
|
|
expect(t8againValue).not.toEqual(t8value)
|
|
}
|
|
}
|
|
})
|
|
it('should allow creating Spans during resuming a fallback - inside a Cache Component', async () => {
|
|
const browser = await next.browser('/novel/fallback')
|
|
{
|
|
const t7 = await browser.elementByCss('#t7 .span')
|
|
const t7value = parseInt(await t7.textContent())
|
|
// the span was prerendered at runtime
|
|
expect(t7value).not.toEqual(0)
|
|
const t8 = await browser.elementByCss('#t8 .span')
|
|
const t8value = parseInt(await t8.textContent())
|
|
// the span was prerendered at runtime
|
|
expect(t8value).not.toEqual(0)
|
|
|
|
// load again
|
|
await browser.loadPage(`${next.url}/novel/fallback`)
|
|
const t7again = await browser.elementByCss('#t7 .span')
|
|
const t7againValue = parseInt(await t7again.textContent())
|
|
// this page renders the spans in the resume on each request
|
|
expect(t7againValue).not.toEqual(t7value)
|
|
expect(t7againValue).not.toEqual(0)
|
|
|
|
const t8again = await browser.elementByCss('#t8 .span')
|
|
const t8againValue = parseInt(await t8again.textContent())
|
|
// this page renders the spans in the resume on each request
|
|
expect(t8againValue).not.toEqual(t8value)
|
|
expect(t8againValue).not.toEqual(0)
|
|
}
|
|
})
|
|
it('should allow creating Spans from a tracer acquired before provider registration', async () => {
|
|
const outputIndex = next.cliOutput.length
|
|
const browser = await next.browser('/novel/early-span')
|
|
// Guard the reported regression directly: span ID generation must not be treated as dynamic Math.random() access during prerendering.
|
|
expect(
|
|
next.cliOutput
|
|
.slice(outputIndex)
|
|
.match(/unstable value.*Math\.random\(\).*prerendering/)
|
|
).toBeNull()
|
|
const result = await browser.elementByCss('#t9 .result')
|
|
expect(await result.textContent()).toEqual('42')
|
|
})
|
|
}
|
|
})
|