From 1b452eb51cfd329e53c3afbc59579757eba4c5b2 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:51:30 +0200 Subject: [PATCH 1/5] test(e2e): Add Next.js cache component nesting scenarios --- .../cached-mid-layout/[id]/layout.tsx | 25 ++ .../cached-mid-layout/[id]/page.tsx | 6 + .../[id]/layout-cached-leaf/page.tsx | 17 ++ .../dynamic-layouts/[id]/layout.tsx | 7 + .../app/(cached-nesting)/layout.tsx | 11 + .../mixed-lifetimes/[id]/layout.tsx | 24 ++ .../mixed-lifetimes/[id]/page.tsx | 19 ++ .../shared-layout/[id]/a/page.tsx | 6 + .../shared-layout/[id]/b/page.tsx | 6 + .../shared-layout/[id]/layout.tsx | 24 ++ .../tests/cacheOriginLinksNesting.spec.ts | 217 ++++++++++++++++ .../cached-mid-layout/[id]/layout.tsx | 25 ++ .../cached-mid-layout/[id]/page.tsx | 6 + .../[id]/layout-cached-leaf/page.tsx | 17 ++ .../dynamic-layouts/[id]/layout.tsx | 7 + .../app/(cached-nesting)/layout.tsx | 11 + .../mixed-lifetimes/[id]/layout.tsx | 24 ++ .../mixed-lifetimes/[id]/page.tsx | 19 ++ .../shared-layout/[id]/a/page.tsx | 6 + .../shared-layout/[id]/b/page.tsx | 6 + .../shared-layout/[id]/layout.tsx | 24 ++ .../tests/cacheOriginLinksNesting.spec.ts | 236 ++++++++++++++++++ 22 files changed, 743 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx new file mode 100644 index 000000000000..a61f15ee7e3f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from 'react'; +import { cacheLife } from 'next/cache'; + +// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss +// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole. +export default async function CachedMidLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ id: string }>; +}) { + 'use cache'; + cacheLife('hours'); + const { id } = await params; + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +
+

+ {id}:{Date.now()} +

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx new file mode 100644 index 000000000000..94196e6e5ee8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx @@ -0,0 +1,6 @@ +import { headers } from 'next/headers'; + +export default async function Page() { + await headers(); + return

Dynamic leaf: {Date.now()}

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx new file mode 100644 index 000000000000..8e7de36d156a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx @@ -0,0 +1,17 @@ +import { cacheLife } from 'next/cache'; + +async function CachedLeaf({ id }: { id: string }) { + 'use cache'; + cacheLife('hours'); + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +

+ {id}:{Date.now()} +

+ ); +} + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx new file mode 100644 index 000000000000..412587fd8f33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react'; +import { headers } from 'next/headers'; + +export default async function DynamicLayout({ children }: { children: ReactNode }) { + await headers(); + return
{children}
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/layout.tsx new file mode 100644 index 000000000000..84a6395b39ac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/layout.tsx @@ -0,0 +1,11 @@ +import { Suspense, type ReactNode } from 'react'; + +// The Suspense boundary lets the nested layouts/pages below use request APIs (`headers()`, +// runtime params) without tripping the Cache Components prerender guards. +export default function Layout({ children }: { children: ReactNode }) { + return ( +
+ Loading...
}>{children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx new file mode 100644 index 000000000000..2b0b77b6e3a9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react'; +import { cacheLife } from 'next/cache'; + +// Hard-expires after 2s while the page's cached component lives for hours, so one request can +// refill the layout while hitting the component: two cached levels with different origin traces. +export default async function ShortLivedLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ id: string }>; +}) { + 'use cache'; + cacheLife({ revalidate: 1, expire: 2 }); + const { id } = await params; + return ( +
+

+ {id}:{Date.now()} +

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx new file mode 100644 index 000000000000..18913cbe9790 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx @@ -0,0 +1,19 @@ +import { headers } from 'next/headers'; +import { cacheLife } from 'next/cache'; + +async function LongLivedComponent({ id }: { id: string }) { + 'use cache'; + cacheLife('hours'); + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +

+ {id}:{Date.now()} +

+ ); +} + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + await headers(); + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx new file mode 100644 index 000000000000..ec8d5e21d947 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx @@ -0,0 +1,6 @@ +import { headers } from 'next/headers'; + +export default async function Page() { + await headers(); + return

Route a: {Date.now()}

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx new file mode 100644 index 000000000000..9a1b84b2ca28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx @@ -0,0 +1,6 @@ +import { headers } from 'next/headers'; + +export default async function Page() { + await headers(); + return

Route b: {Date.now()}

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx new file mode 100644 index 000000000000..220fdcf92ca8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react'; +import { cacheLife } from 'next/cache'; + +// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below. +export default async function SharedLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ id: string }>; +}) { + 'use cache'; + cacheLife('hours'); + const { id } = await params; + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +
+

+ {id}:{Date.now()} +

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts new file mode 100644 index 000000000000..d1fee7430c5f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts @@ -0,0 +1,217 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// Origin links (`sentry.link.type: 'cache_origin'`, see cacheOriginLinks.spec.ts) for `use cache` +// in nested layout trees under `app/(cached-nesting)/`. Not implemented yet — every test is +// `test.fail()` with the final expected assertions. + +// A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id] +// param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the +// cache handlers, there is no `cache.get` span at all — then this stays failing until Next +// exposes RDC reads. +test('links a cached layout hit to the trace that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const missTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /cached-mid-layout/[id]' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === false) + ); + }); + + const hitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /cached-mid-layout/[id]' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) + ); + }); + + await request.get(`/cached-mid-layout/${id}`); + const missTx = await missTxPromise; + + await request.get(`/cached-mid-layout/${id}`); + const hitTx = await hitTxPromise; + + // The layout is the only cached entry on this route. + const putSpans = (missTx.spans ?? []).filter(span => span.op === 'cache.put'); + expect(new Set(putSpans.map(span => span.description)).size).toBe(1); + + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan?.description).toBe(putSpans[0]!.description); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: missTx.contexts?.trace?.trace_id, + span_id: putSpans[0]!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); +}); + +// Inverse nesting: all layouts above are dynamic, only the leaf component is cached — the leaf +// entry is the only span that carries a link. +test('links a cached leaf under dynamic layouts to the trace that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const missTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === false) + ); + }); + + const hitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) + ); + }); + + await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); + const missTx = await missTxPromise; + + await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); + const hitTx = await hitTxPromise; + + const putSpans = (missTx.spans ?? []).filter(span => span.op === 'cache.put'); + expect(new Set(putSpans.map(span => span.description)).size).toBe(1); + + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan?.description).toBe(putSpans[0]!.description); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: missTx.contexts?.trace?.trace_id, + span_id: putSpans[0]!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); +}); + +// Nested levels with different lifetimes: after the layout expired, request 2 refills the layout +// while the component still hits. Request 3 then hits both entries, and its two `cache.get` +// spans point at two different origin traces. +test('links two cached levels to different origin traces after the layout expires', async ({ request }) => { + test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production'); + test.fail(); + + const id = crypto.randomUUID(); + + const fillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /mixed-lifetimes/[id]' && + !!transactionEvent.spans?.some(span => span.op === 'cache.put') + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const fillTx = await fillTxPromise; + + // Layout + component entry. + const fillPutSpans = (fillTx.spans ?? []).filter(span => span.op === 'cache.put'); + expect(new Set(fillPutSpans.map(span => span.description)).size).toBe(2); + + // Sleep past the layout's `expire` (2s); the component entry stays valid for hours. + await new Promise(resolve => setTimeout(resolve, 3_000)); + + const refillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /mixed-lifetimes/[id]' && + !!transactionEvent.spans?.some(span => span.op === 'cache.put') && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const refillTx = await refillTxPromise; + + const hitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /mixed-lifetimes/[id]' && + (transactionEvent.spans?.filter(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true).length ?? + 0) >= 2 + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const hitTx = await hitTxPromise; + + const layoutPutSpan = refillTx.spans?.find(span => span.op === 'cache.put'); + expect(layoutPutSpan).toBeDefined(); + const componentPutSpan = fillPutSpans.find(span => span.description !== layoutPutSpan!.description); + expect(componentPutSpan).toBeDefined(); + expect(refillTx.contexts?.trace?.trace_id).not.toBe(fillTx.contexts?.trace?.trace_id); + + const layoutHitSpan = hitTx.spans?.find( + span => span.op === 'cache.get' && span.description === layoutPutSpan!.description, + ); + expect(layoutHitSpan?.links).toEqual([ + { + trace_id: refillTx.contexts?.trace?.trace_id, + span_id: layoutPutSpan!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); + + const componentHitSpan = hitTx.spans?.find( + span => span.op === 'cache.get' && span.description === componentPutSpan!.description, + ); + expect(componentHitSpan?.links).toEqual([ + { + trace_id: fillTx.contexts?.trace?.trace_id, + span_id: componentPutSpan!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); +}); + +// Routes `a` and `b` share one layout entry: a hit on `b` links to the fill trace of `a`, so the +// origin is a different transaction than the serving one. +test('links a shared layout hit on a sibling route to the route that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const fillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /shared-layout/[id]/a' && + !!transactionEvent.spans?.some(span => span.op === 'cache.put') + ); + }); + + await request.get(`/shared-layout/${id}/a`); + const fillTx = await fillTxPromise; + + const hitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /shared-layout/[id]/b' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) + ); + }); + + await request.get(`/shared-layout/${id}/b`); + const hitTx = await hitTxPromise; + + const putSpan = fillTx.spans?.find(span => span.op === 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan?.description).toBe(putSpan!.description); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: fillTx.contexts?.trace?.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx new file mode 100644 index 000000000000..a61f15ee7e3f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from 'react'; +import { cacheLife } from 'next/cache'; + +// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss +// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole. +export default async function CachedMidLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ id: string }>; +}) { + 'use cache'; + cacheLife('hours'); + const { id } = await params; + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +
+

+ {id}:{Date.now()} +

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx new file mode 100644 index 000000000000..94196e6e5ee8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx @@ -0,0 +1,6 @@ +import { headers } from 'next/headers'; + +export default async function Page() { + await headers(); + return

Dynamic leaf: {Date.now()}

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx new file mode 100644 index 000000000000..8e7de36d156a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx @@ -0,0 +1,17 @@ +import { cacheLife } from 'next/cache'; + +async function CachedLeaf({ id }: { id: string }) { + 'use cache'; + cacheLife('hours'); + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +

+ {id}:{Date.now()} +

+ ); +} + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx new file mode 100644 index 000000000000..412587fd8f33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react'; +import { headers } from 'next/headers'; + +export default async function DynamicLayout({ children }: { children: ReactNode }) { + await headers(); + return
{children}
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/layout.tsx new file mode 100644 index 000000000000..84a6395b39ac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/layout.tsx @@ -0,0 +1,11 @@ +import { Suspense, type ReactNode } from 'react'; + +// The Suspense boundary lets the nested layouts/pages below use request APIs (`headers()`, +// runtime params) without tripping the Cache Components prerender guards. +export default function Layout({ children }: { children: ReactNode }) { + return ( +
+ Loading...
}>{children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx new file mode 100644 index 000000000000..2b0b77b6e3a9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react'; +import { cacheLife } from 'next/cache'; + +// Hard-expires after 2s while the page's cached component lives for hours, so one request can +// refill the layout while hitting the component: two cached levels with different origin traces. +export default async function ShortLivedLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ id: string }>; +}) { + 'use cache'; + cacheLife({ revalidate: 1, expire: 2 }); + const { id } = await params; + return ( +
+

+ {id}:{Date.now()} +

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx new file mode 100644 index 000000000000..18913cbe9790 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx @@ -0,0 +1,19 @@ +import { headers } from 'next/headers'; +import { cacheLife } from 'next/cache'; + +async function LongLivedComponent({ id }: { id: string }) { + 'use cache'; + cacheLife('hours'); + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +

+ {id}:{Date.now()} +

+ ); +} + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + await headers(); + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx new file mode 100644 index 000000000000..ec8d5e21d947 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx @@ -0,0 +1,6 @@ +import { headers } from 'next/headers'; + +export default async function Page() { + await headers(); + return

Route a: {Date.now()}

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx new file mode 100644 index 000000000000..9a1b84b2ca28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx @@ -0,0 +1,6 @@ +import { headers } from 'next/headers'; + +export default async function Page() { + await headers(); + return

Route b: {Date.now()}

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx new file mode 100644 index 000000000000..220fdcf92ca8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react'; +import { cacheLife } from 'next/cache'; + +// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below. +export default async function SharedLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ id: string }>; +}) { + 'use cache'; + cacheLife('hours'); + const { id } = await params; + await new Promise(resolve => setTimeout(resolve, 100)); + return ( +
+

+ {id}:{Date.now()} +

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts new file mode 100644 index 000000000000..c9e69fffdece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts @@ -0,0 +1,236 @@ +import { expect, test } from '@playwright/test'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; + +// Origin links (`sentry.link.type: 'cache_origin'`, see cacheOriginLinks.spec.ts) for `use cache` +// in nested layout trees under `app/(cached-nesting)/`. Not implemented yet — every test is +// `test.fail()` with the final expected assertions. + +const CACHE_ORIGIN_LINK_ATTRIBUTES = { + 'sentry.link.type': { value: 'cache_origin', type: 'string' }, +}; + +function findCacheSpan( + spans: SerializedStreamedSpan[], + op: 'cache.get' | 'cache.put', + hit?: boolean, +): SerializedStreamedSpan | undefined { + return spans.find( + span => getSpanOp(span) === op && (hit === undefined || span.attributes['cache.hit']?.value === hit), + ); +} + +// A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id] +// param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the +// cache handlers, there is no `cache.get` span at all — then this stays failing until Next +// exposes RDC reads. +test('links a cached layout hit to the trace that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /cached-mid-layout/[id]' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/cached-mid-layout/${id}`); + const missSpans = await missSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /cached-mid-layout/[id]' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/cached-mid-layout/${id}`); + const hitSpans = await hitSpansPromise; + + // The layout is the only cached entry on this route. + const putSpan = findCacheSpan(missSpans, 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: putSpan!.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); + +// Inverse nesting: all layouts above are dynamic, only the leaf component is cached — the leaf +// entry is the only span that carries a link. +test('links a cached leaf under dynamic layouts to the trace that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); + const missSpans = await missSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); + const hitSpans = await hitSpansPromise; + + const putSpan = findCacheSpan(missSpans, 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: putSpan!.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); + +// Nested levels with different lifetimes: after the layout expired, request 2 refills the layout +// while the component still hits. Request 3 then hits both entries, and its two `cache.get` +// spans point at two different origin traces. +test('links two cached levels to different origin traces after the layout expires', async ({ request }) => { + test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production'); + test.fail(); + + const id = crypto.randomUUID(); + + const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'cache.put').length >= 2 + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const fillSpans = await fillSpansPromise; + + // Layout + component entry. + const fillPutSpans = fillSpans.filter(span => getSpanOp(span) === 'cache.put'); + expect(new Set(fillPutSpans.map(span => JSON.stringify(span.attributes['cache.key']?.value))).size).toBe(2); + + // Sleep past the layout's `expire` (2s); the component entry stays valid for hours. + await new Promise(resolve => setTimeout(resolve, 3_000)); + + const refillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const refillSpans = await refillSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + .length >= 2 + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const hitSpans = await hitSpansPromise; + + const layoutPutSpan = findCacheSpan(refillSpans, 'cache.put'); + expect(layoutPutSpan).toBeDefined(); + const layoutKey = JSON.stringify(layoutPutSpan!.attributes['cache.key']?.value); + const componentPutSpan = fillPutSpans.find(span => JSON.stringify(span.attributes['cache.key']?.value) !== layoutKey); + expect(componentPutSpan).toBeDefined(); + expect(layoutPutSpan!.trace_id).not.toBe(componentPutSpan!.trace_id); + + const layoutHitSpan = hitSpans.find( + span => getSpanOp(span) === 'cache.get' && JSON.stringify(span.attributes['cache.key']?.value) === layoutKey, + ); + expect(layoutHitSpan?.links).toEqual([ + { + trace_id: layoutPutSpan!.trace_id, + span_id: layoutPutSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); + + const componentHitSpan = hitSpans.find( + span => + getSpanOp(span) === 'cache.get' && + JSON.stringify(span.attributes['cache.key']?.value) === + JSON.stringify(componentPutSpan!.attributes['cache.key']?.value), + ); + expect(componentHitSpan?.links).toEqual([ + { + trace_id: componentPutSpan!.trace_id, + span_id: componentPutSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); + +// Routes `a` and `b` share one layout entry: a hit on `b` links to the fill trace of `a`, so the +// origin is a different transaction than the serving one. +test('links a shared layout hit on a sibling route to the route that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /shared-layout/[id]/a' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/shared-layout/${id}/a`); + const fillSpans = await fillSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /shared-layout/[id]/b' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/shared-layout/${id}/b`); + const hitSpans = await hitSpansPromise; + + const putSpan = findCacheSpan(fillSpans, 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: putSpan!.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); From 420bbaf1dc0281965f9e1ed005f4db1799b3deaa Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:31:31 +0200 Subject: [PATCH 2/5] add utils --- .../tests/cacheOriginLinksNesting.spec.ts | 6 ++--- .../tests/cacheOriginLinkUtils.ts | 16 ++++++++++++++ .../tests/cacheOriginLinksNesting.spec.ts | 22 ++++--------------- 3 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinkUtils.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts index d1fee7430c5f..0c213a2676ad 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -// Origin links (`sentry.link.type: 'cache_origin'`, see cacheOriginLinks.spec.ts) for `use cache` -// in nested layout trees under `app/(cached-nesting)/`. Not implemented yet — every test is -// `test.fail()` with the final expected assertions. +// Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the +// filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`. +// Not implemented yet — every test is `test.fail()` with the final expected assertions. // A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id] // param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinkUtils.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinkUtils.ts new file mode 100644 index 000000000000..d43ce40324fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinkUtils.ts @@ -0,0 +1,16 @@ +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp } from '@sentry-internal/test-utils'; + +export const CACHE_ORIGIN_LINK_ATTRIBUTES = { + 'sentry.link.type': { value: 'cache_origin', type: 'string' }, +}; + +export function findCacheSpan( + spans: SerializedStreamedSpan[], + op: 'cache.get' | 'cache.put', + hit?: boolean, +): SerializedStreamedSpan | undefined { + return spans.find( + span => getSpanOp(span) === op && (hit === undefined || span.attributes['cache.hit']?.value === hit), + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts index c9e69fffdece..db936ca9a300 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts @@ -1,24 +1,10 @@ import { expect, test } from '@playwright/test'; -import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinkUtils'; -// Origin links (`sentry.link.type: 'cache_origin'`, see cacheOriginLinks.spec.ts) for `use cache` -// in nested layout trees under `app/(cached-nesting)/`. Not implemented yet — every test is -// `test.fail()` with the final expected assertions. - -const CACHE_ORIGIN_LINK_ATTRIBUTES = { - 'sentry.link.type': { value: 'cache_origin', type: 'string' }, -}; - -function findCacheSpan( - spans: SerializedStreamedSpan[], - op: 'cache.get' | 'cache.put', - hit?: boolean, -): SerializedStreamedSpan | undefined { - return spans.find( - span => getSpanOp(span) === op && (hit === undefined || span.attributes['cache.hit']?.value === hit), - ); -} +// Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the +// filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`. +// Not implemented yet — every test is `test.fail()` with the final expected assertions. // A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id] // param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the From bf9ffb3b8004d1fb27b3b6984e6b2dff757fdc80 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:19:57 +0200 Subject: [PATCH 3/5] rename test --- .../tests/cacheOriginLinks-nesting.spec.ts | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts new file mode 100644 index 000000000000..db936ca9a300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -0,0 +1,222 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinkUtils'; + +// Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the +// filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`. +// Not implemented yet — every test is `test.fail()` with the final expected assertions. + +// A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id] +// param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the +// cache handlers, there is no `cache.get` span at all — then this stays failing until Next +// exposes RDC reads. +test('links a cached layout hit to the trace that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /cached-mid-layout/[id]' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/cached-mid-layout/${id}`); + const missSpans = await missSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /cached-mid-layout/[id]' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/cached-mid-layout/${id}`); + const hitSpans = await hitSpansPromise; + + // The layout is the only cached entry on this route. + const putSpan = findCacheSpan(missSpans, 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: putSpan!.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); + +// Inverse nesting: all layouts above are dynamic, only the leaf component is cached — the leaf +// entry is the only span that carries a link. +test('links a cached leaf under dynamic layouts to the trace that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); + const missSpans = await missSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); + const hitSpans = await hitSpansPromise; + + const putSpan = findCacheSpan(missSpans, 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: putSpan!.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); + +// Nested levels with different lifetimes: after the layout expired, request 2 refills the layout +// while the component still hits. Request 3 then hits both entries, and its two `cache.get` +// spans point at two different origin traces. +test('links two cached levels to different origin traces after the layout expires', async ({ request }) => { + test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production'); + test.fail(); + + const id = crypto.randomUUID(); + + const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'cache.put').length >= 2 + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const fillSpans = await fillSpansPromise; + + // Layout + component entry. + const fillPutSpans = fillSpans.filter(span => getSpanOp(span) === 'cache.put'); + expect(new Set(fillPutSpans.map(span => JSON.stringify(span.attributes['cache.key']?.value))).size).toBe(2); + + // Sleep past the layout's `expire` (2s); the component entry stays valid for hours. + await new Promise(resolve => setTimeout(resolve, 3_000)); + + const refillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const refillSpans = await refillSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + .length >= 2 + ); + }); + + await request.get(`/mixed-lifetimes/${id}`); + const hitSpans = await hitSpansPromise; + + const layoutPutSpan = findCacheSpan(refillSpans, 'cache.put'); + expect(layoutPutSpan).toBeDefined(); + const layoutKey = JSON.stringify(layoutPutSpan!.attributes['cache.key']?.value); + const componentPutSpan = fillPutSpans.find(span => JSON.stringify(span.attributes['cache.key']?.value) !== layoutKey); + expect(componentPutSpan).toBeDefined(); + expect(layoutPutSpan!.trace_id).not.toBe(componentPutSpan!.trace_id); + + const layoutHitSpan = hitSpans.find( + span => getSpanOp(span) === 'cache.get' && JSON.stringify(span.attributes['cache.key']?.value) === layoutKey, + ); + expect(layoutHitSpan?.links).toEqual([ + { + trace_id: layoutPutSpan!.trace_id, + span_id: layoutPutSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); + + const componentHitSpan = hitSpans.find( + span => + getSpanOp(span) === 'cache.get' && + JSON.stringify(span.attributes['cache.key']?.value) === + JSON.stringify(componentPutSpan!.attributes['cache.key']?.value), + ); + expect(componentHitSpan?.links).toEqual([ + { + trace_id: componentPutSpan!.trace_id, + span_id: componentPutSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); + +// Routes `a` and `b` share one layout entry: a hit on `b` links to the fill trace of `a`, so the +// origin is a different transaction than the serving one. +test('links a shared layout hit on a sibling route to the route that filled it', async ({ request }) => { + test.fail(); + + const id = crypto.randomUUID(); + + const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /shared-layout/[id]/a' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/shared-layout/${id}/a`); + const fillSpans = await fillSpansPromise; + + const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /shared-layout/[id]/b' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + await request.get(`/shared-layout/${id}/b`); + const hitSpans = await hitSpansPromise; + + const putSpan = findCacheSpan(fillSpans, 'cache.put'); + expect(putSpan).toBeDefined(); + + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); + expect(hitGetSpan).toBeDefined(); + expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); + expect(hitGetSpan?.links).toEqual([ + { + trace_id: putSpan!.trace_id, + span_id: putSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); From 64b176ec1165ba202d81a7fbbb0472176513816e Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:32:59 +0200 Subject: [PATCH 4/5] remove prev file --- .../tests/cacheOriginLinksNesting.spec.ts | 222 ------------------ 1 file changed, 222 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts deleted file mode 100644 index db936ca9a300..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinksNesting.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; -import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinkUtils'; - -// Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the -// filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`. -// Not implemented yet — every test is `test.fail()` with the final expected assertions. - -// A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id] -// param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the -// cache handlers, there is no `cache.get` span at all — then this stays failing until Next -// exposes RDC reads. -test('links a cached layout hit to the trace that filled it', async ({ request }) => { - test.fail(); - - const id = crypto.randomUUID(); - - const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /cached-mid-layout/[id]' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.put') - ); - }); - - await request.get(`/cached-mid-layout/${id}`); - const missSpans = await missSpansPromise; - - const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /cached-mid-layout/[id]' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) - ); - }); - - await request.get(`/cached-mid-layout/${id}`); - const hitSpans = await hitSpansPromise; - - // The layout is the only cached entry on this route. - const putSpan = findCacheSpan(missSpans, 'cache.put'); - expect(putSpan).toBeDefined(); - - const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); - expect(hitGetSpan).toBeDefined(); - expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); - expect(hitGetSpan?.links).toEqual([ - { - trace_id: putSpan!.trace_id, - span_id: putSpan!.span_id, - sampled: true, - attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, - }, - ]); -}); - -// Inverse nesting: all layouts above are dynamic, only the leaf component is cached — the leaf -// entry is the only span that carries a link. -test('links a cached leaf under dynamic layouts to the trace that filled it', async ({ request }) => { - test.fail(); - - const id = crypto.randomUUID(); - - const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.put') - ); - }); - - await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); - const missSpans = await missSpansPromise; - - const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /dynamic-layouts/[id]/layout-cached-leaf' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) - ); - }); - - await request.get(`/dynamic-layouts/${id}/layout-cached-leaf`); - const hitSpans = await hitSpansPromise; - - const putSpan = findCacheSpan(missSpans, 'cache.put'); - expect(putSpan).toBeDefined(); - - const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); - expect(hitGetSpan).toBeDefined(); - expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); - expect(hitGetSpan?.links).toEqual([ - { - trace_id: putSpan!.trace_id, - span_id: putSpan!.span_id, - sampled: true, - attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, - }, - ]); -}); - -// Nested levels with different lifetimes: after the layout expired, request 2 refills the layout -// while the component still hits. Request 3 then hits both entries, and its two `cache.get` -// spans point at two different origin traces. -test('links two cached levels to different origin traces after the layout expires', async ({ request }) => { - test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production'); - test.fail(); - - const id = crypto.randomUUID(); - - const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && - spansOfTrace.filter(span => getSpanOp(span) === 'cache.put').length >= 2 - ); - }); - - await request.get(`/mixed-lifetimes/${id}`); - const fillSpans = await fillSpansPromise; - - // Layout + component entry. - const fillPutSpans = fillSpans.filter(span => getSpanOp(span) === 'cache.put'); - expect(new Set(fillPutSpans.map(span => JSON.stringify(span.attributes['cache.key']?.value))).size).toBe(2); - - // Sleep past the layout's `expire` (2s); the component entry stays valid for hours. - await new Promise(resolve => setTimeout(resolve, 3_000)); - - const refillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.put') && - spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) - ); - }); - - await request.get(`/mixed-lifetimes/${id}`); - const refillSpans = await refillSpansPromise; - - const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /mixed-lifetimes/[id]' && span.is_segment) && - spansOfTrace.filter(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) - .length >= 2 - ); - }); - - await request.get(`/mixed-lifetimes/${id}`); - const hitSpans = await hitSpansPromise; - - const layoutPutSpan = findCacheSpan(refillSpans, 'cache.put'); - expect(layoutPutSpan).toBeDefined(); - const layoutKey = JSON.stringify(layoutPutSpan!.attributes['cache.key']?.value); - const componentPutSpan = fillPutSpans.find(span => JSON.stringify(span.attributes['cache.key']?.value) !== layoutKey); - expect(componentPutSpan).toBeDefined(); - expect(layoutPutSpan!.trace_id).not.toBe(componentPutSpan!.trace_id); - - const layoutHitSpan = hitSpans.find( - span => getSpanOp(span) === 'cache.get' && JSON.stringify(span.attributes['cache.key']?.value) === layoutKey, - ); - expect(layoutHitSpan?.links).toEqual([ - { - trace_id: layoutPutSpan!.trace_id, - span_id: layoutPutSpan!.span_id, - sampled: true, - attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, - }, - ]); - - const componentHitSpan = hitSpans.find( - span => - getSpanOp(span) === 'cache.get' && - JSON.stringify(span.attributes['cache.key']?.value) === - JSON.stringify(componentPutSpan!.attributes['cache.key']?.value), - ); - expect(componentHitSpan?.links).toEqual([ - { - trace_id: componentPutSpan!.trace_id, - span_id: componentPutSpan!.span_id, - sampled: true, - attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, - }, - ]); -}); - -// Routes `a` and `b` share one layout entry: a hit on `b` links to the fill trace of `a`, so the -// origin is a different transaction than the serving one. -test('links a shared layout hit on a sibling route to the route that filled it', async ({ request }) => { - test.fail(); - - const id = crypto.randomUUID(); - - const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /shared-layout/[id]/a' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.put') - ); - }); - - await request.get(`/shared-layout/${id}/a`); - const fillSpans = await fillSpansPromise; - - const hitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { - return ( - spansOfTrace.some(span => span.name === 'GET /shared-layout/[id]/b' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) - ); - }); - - await request.get(`/shared-layout/${id}/b`); - const hitSpans = await hitSpansPromise; - - const putSpan = findCacheSpan(fillSpans, 'cache.put'); - expect(putSpan).toBeDefined(); - - const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); - expect(hitGetSpan).toBeDefined(); - expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); - expect(hitGetSpan?.links).toEqual([ - { - trace_id: putSpan!.trace_id, - span_id: putSpan!.span_id, - sampled: true, - attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, - }, - ]); -}); From effe18f0906a30dbe78ac21e6522d2ee34ef4bd0 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:25:22 +0200 Subject: [PATCH 5/5] rename in streaming app --- ...ting.spec.ts => cacheOriginLinks-nesting.spec.ts} | 10 +++++++--- .../tests/cacheOriginLinks-nesting.spec.ts | 12 ++++++++---- ...eOriginLinkUtils.ts => cacheOriginLinks-utils.ts} | 0 3 files changed, 15 insertions(+), 7 deletions(-) rename dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/{cacheOriginLinksNesting.spec.ts => cacheOriginLinks-nesting.spec.ts} (95%) rename dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/{cacheOriginLinkUtils.ts => cacheOriginLinks-utils.ts} (100%) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts rename to dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts index 0c213a2676ad..5025d7d2e104 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinksNesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -9,7 +9,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; // param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the // cache handlers, there is no `cache.get` span at all — then this stays failing until Next // exposes RDC reads. -test('links a cached layout hit to the trace that filled it', async ({ request }) => { +test('links a cached layout hit to the trace that filled the layout entry', async ({ request }) => { test.fail(); const id = crypto.randomUUID(); @@ -53,7 +53,9 @@ test('links a cached layout hit to the trace that filled it', async ({ request } // Inverse nesting: all layouts above are dynamic, only the leaf component is cached — the leaf // entry is the only span that carries a link. -test('links a cached leaf under dynamic layouts to the trace that filled it', async ({ request }) => { +test('links a cached leaf component under dynamic layouts to the trace that filled the leaf entry', async ({ + request, +}) => { test.fail(); const id = crypto.randomUUID(); @@ -175,7 +177,9 @@ test('links two cached levels to different origin traces after the layout expire // Routes `a` and `b` share one layout entry: a hit on `b` links to the fill trace of `a`, so the // origin is a different transaction than the serving one. -test('links a shared layout hit on a sibling route to the route that filled it', async ({ request }) => { +test('links a shared layout hit on a sibling route to the trace of the route that filled the entry', async ({ + request, +}) => { test.fail(); const id = crypto.randomUUID(); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts index db936ca9a300..d456686f24ec 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; -import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinkUtils'; +import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks-utils'; // Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the // filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`. @@ -10,7 +10,7 @@ import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinkUt // param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the // cache handlers, there is no `cache.get` span at all — then this stays failing until Next // exposes RDC reads. -test('links a cached layout hit to the trace that filled it', async ({ request }) => { +test('links a cached layout hit to the trace that filled the layout entry', async ({ request }) => { test.fail(); const id = crypto.randomUUID(); @@ -54,7 +54,9 @@ test('links a cached layout hit to the trace that filled it', async ({ request } // Inverse nesting: all layouts above are dynamic, only the leaf component is cached — the leaf // entry is the only span that carries a link. -test('links a cached leaf under dynamic layouts to the trace that filled it', async ({ request }) => { +test('links a cached leaf component under dynamic layouts to the trace that filled the leaf entry', async ({ + request, +}) => { test.fail(); const id = crypto.randomUUID(); @@ -180,7 +182,9 @@ test('links two cached levels to different origin traces after the layout expire // Routes `a` and `b` share one layout entry: a hit on `b` links to the fill trace of `a`, so the // origin is a different transaction than the serving one. -test('links a shared layout hit on a sibling route to the route that filled it', async ({ request }) => { +test('links a shared layout hit on a sibling route to the trace of the route that filled the entry', async ({ + request, +}) => { test.fail(); const id = crypto.randomUUID(); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinkUtils.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-utils.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinkUtils.ts rename to dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-utils.ts