From 5614c35e0286ccbbfa78fe7c88c2a8a4b1345e66 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:08:54 +0200 Subject: [PATCH 1/4] test(e2e): Add trace test for background use cache revalidation --- .../app/api/use-cache-swr/route.ts | 15 +++ .../cacheOriginLinks-revalidation.spec.ts | 97 +++++++++++++++++ .../app/api/use-cache-swr/route.ts | 15 +++ .../cacheOriginLinks-revalidation.spec.ts | 102 ++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts new file mode 100644 index 000000000000..970d9f5fbc41 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts @@ -0,0 +1,15 @@ +import { cacheLife } from 'next/cache'; +import type { NextRequest } from 'next/server'; + +async function getSwrValue(id: string): Promise<{ id: string; createdAt: number }> { + 'use cache'; + // Past `revalidate` (2s) a read serves the stale value and triggers a background refill; + // `expire` is long so the entry never hard-expires during the test. + cacheLife({ stale: 5, revalidate: 2, expire: 300 }); + return { id, createdAt: Date.now() }; +} + +export async function GET(request: NextRequest) { + const id = request.nextUrl.searchParams.get('id') ?? 'default-id'; + return Response.json(await getSwrValue(id)); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts new file mode 100644 index 000000000000..33b3cd488aa6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -0,0 +1,97 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// Background stale-while-revalidate refills. Target behavior: the revalidation runs in its own +// trace (not grafted onto the serving trace), links back to the request that triggered it, and +// becomes the `cache_origin` for future hits. Not implemented yet — the test is `test.fail()`. + +test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => { + test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production'); + test.fail(); + + const id = crypto.randomUUID(); + + const fillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/use-cache-swr' && + !!transactionEvent.spans?.some(span => span.op === 'cache.put') + ); + }); + + await request.get(`/api/use-cache-swr?id=${id}`); + const fillTx = await fillTxPromise; + + // Sleep past `revalidate` (2s) but not `expire`, so the next read serves the stale value and + // triggers a background refill. + await new Promise(resolve => setTimeout(resolve, 3_000)); + + const staleHitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/use-cache-swr' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) + ); + }); + + const revalidationTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'cache.revalidate'; + }); + + await request.get(`/api/use-cache-swr?id=${id}`); + const staleHitTx = await staleHitTxPromise; + + const fillPutSpan = fillTx.spans?.find(span => span.op === 'cache.put'); + expect(fillPutSpan).toBeDefined(); + + // The stale response still came from the original fill. + const staleHitGetSpan = staleHitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true); + expect(staleHitGetSpan).toBeDefined(); + expect(staleHitGetSpan?.links).toEqual([ + { + trace_id: fillTx.contexts?.trace?.trace_id, + span_id: fillPutSpan!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); + + // Refill work the visitor never waited for is not grafted onto the serving trace: the + // background revalidation is its own trace, linked back to the request that triggered it. + const revalidationTx = await revalidationTxPromise; + expect(revalidationTx.contexts?.trace?.trace_id).not.toBe(staleHitTx.contexts?.trace?.trace_id); + expect(revalidationTx.contexts?.trace?.links).toEqual([ + { + trace_id: staleHitTx.contexts?.trace?.trace_id, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + sampled: true, + // The link type for "revalidation triggered by" is not final yet; the target trace is. + attributes: { 'sentry.link.type': expect.any(String) }, + }, + ]); + + const revalidationPutSpan = revalidationTx.spans?.find(span => span.op === 'cache.put'); + expect(revalidationPutSpan).toBeDefined(); + + // The revalidation becomes the origin for future hits. + const hitAfterRefillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/use-cache-swr' && + !!transactionEvent.spans?.some(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) + ); + }); + + await request.get(`/api/use-cache-swr?id=${id}`); + const hitAfterRefillTx = await hitAfterRefillTxPromise; + + const hitAfterRefillGetSpan = hitAfterRefillTx.spans?.find( + span => span.op === 'cache.get' && span.data?.['cache.hit'] === true, + ); + expect(hitAfterRefillGetSpan).toBeDefined(); + expect(hitAfterRefillGetSpan?.links).toEqual([ + { + trace_id: revalidationTx.contexts?.trace?.trace_id, + span_id: revalidationPutSpan!.span_id, + sampled: true, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ]); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts new file mode 100644 index 000000000000..970d9f5fbc41 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts @@ -0,0 +1,15 @@ +import { cacheLife } from 'next/cache'; +import type { NextRequest } from 'next/server'; + +async function getSwrValue(id: string): Promise<{ id: string; createdAt: number }> { + 'use cache'; + // Past `revalidate` (2s) a read serves the stale value and triggers a background refill; + // `expire` is long so the entry never hard-expires during the test. + cacheLife({ stale: 5, revalidate: 2, expire: 300 }); + return { id, createdAt: Date.now() }; +} + +export async function GET(request: NextRequest) { + const id = request.nextUrl.searchParams.get('id') ?? 'default-id'; + return Response.json(await getSwrValue(id)); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts new file mode 100644 index 000000000000..b4fd33222501 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -0,0 +1,102 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks-utils'; + +// Background stale-while-revalidate refills. Target behavior: the revalidation runs in its own +// trace (not grafted onto the serving trace), links back to the request that triggered it, and +// becomes the `cache_origin` for future hits. Not implemented yet — the test is `test.fail()`. + +test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => { + test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production'); + test.fail(); + + const id = crypto.randomUUID(); + + const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/api/use-cache-swr?id=${id}`); + const fillSpans = await fillSpansPromise; + + // Sleep past `revalidate` (2s) but not `expire`, so the next read serves the stale value and + // triggers a background refill. + await new Promise(resolve => setTimeout(resolve, 3_000)); + + const staleHitSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) + ); + }); + + // Refill work the visitor never waited for is not grafted onto the serving trace: the + // background revalidation is its own trace with a `cache.revalidate` segment. + const revalidationSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => getSpanOp(span) === 'cache.revalidate' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/api/use-cache-swr?id=${id}`); + const staleHitSpans = await staleHitSpansPromise; + + const fillPutSpan = findCacheSpan(fillSpans, 'cache.put'); + expect(fillPutSpan).toBeDefined(); + + // The stale response still came from the original fill. + const staleHitGetSpan = findCacheSpan(staleHitSpans, 'cache.get', true); + expect(staleHitGetSpan).toBeDefined(); + expect(staleHitGetSpan?.links).toEqual([ + { + trace_id: fillPutSpan!.trace_id, + span_id: fillPutSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); + + const revalidationSpans = await revalidationSpansPromise; + const revalidationSegment = revalidationSpans.find(span => span.is_segment && getSpanOp(span) === 'cache.revalidate'); + expect(revalidationSegment).toBeDefined(); + expect(revalidationSegment!.trace_id).not.toBe(staleHitGetSpan!.trace_id); + expect(revalidationSegment!.links).toEqual([ + { + trace_id: staleHitGetSpan!.trace_id, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + sampled: true, + // The link type for "revalidation triggered by" is not final yet; the target trace is. + attributes: { 'sentry.link.type': { value: expect.any(String), type: 'string' } }, + }, + ]); + + const revalidationPutSpan = findCacheSpan(revalidationSpans, 'cache.put'); + expect(revalidationPutSpan).toBeDefined(); + + // The revalidation becomes the origin for future hits. + const hitAfterRefillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get' && span.attributes['cache.hit']?.value === true) && + spansOfTrace.every(span => span.trace_id !== staleHitGetSpan!.trace_id) + ); + }); + + await request.get(`/api/use-cache-swr?id=${id}`); + const hitAfterRefillSpans = await hitAfterRefillSpansPromise; + + const hitAfterRefillGetSpan = findCacheSpan(hitAfterRefillSpans, 'cache.get', true); + expect(hitAfterRefillGetSpan).toBeDefined(); + expect(hitAfterRefillGetSpan?.links).toEqual([ + { + trace_id: revalidationPutSpan!.trace_id, + span_id: revalidationPutSpan!.span_id, + sampled: true, + attributes: CACHE_ORIGIN_LINK_ATTRIBUTES, + }, + ]); +}); From 0f445c88e313011baf06bca697c05f5a2a83531b Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:20:55 +0200 Subject: [PATCH 2/4] improve comment --- .../cacheOriginLinks-revalidation.spec.ts | 29 ++++++++++++++----- .../cacheOriginLinks-revalidation.spec.ts | 26 +++++++++++++---- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts index 33b3cd488aa6..b060592f116c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -1,13 +1,29 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -// Background stale-while-revalidate refills. Target behavior: the revalidation runs in its own -// trace (not grafted onto the serving trace), links back to the request that triggered it, and -// becomes the `cache_origin` for future hits. Not implemented yet — the test is `test.fail()`. +// Background stale-while-revalidate refills. Target behavior: the revalidation gets its own trace, +// separate from the request trace that served the stale value. The revalidation trace links back to +// that request and becomes the `cache_origin` for future hits. + +/* + +Trace1 (fill) Trace2 (stale hit) Trace3 (hit) +|- put key:A <----link-----o get hit=true key:A |- get hit=true key:A +| | +TraceR: cache.revalidate (own trace, NOT part of Trace2) | +^- link back to Trace2 (link type TBD in the RFC) | +| | +|- put key:A <----------------------link-------------------o cache_origin + +*/ + +// Not implemented yet. Unlike the nesting specs, this cannot be `test.fail()`: the test waits for a +// `cache.revalidate` trace that never arrives, so it would time out — and Playwright reports a +// timeout as a real failure even under `test.fail()`. Hence `test.fixme()`. test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => { test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production'); - test.fail(); + test.fixme(); const id = crypto.randomUUID(); @@ -21,8 +37,7 @@ test('runs background revalidation in its own trace linked to the triggering req await request.get(`/api/use-cache-swr?id=${id}`); const fillTx = await fillTxPromise; - // Sleep past `revalidate` (2s) but not `expire`, so the next read serves the stale value and - // triggers a background refill. + // Sleep past `revalidate` (2s) but not `expire`, so the next read serves the stale value and triggers a background refill. await new Promise(resolve => setTimeout(resolve, 3_000)); const staleHitTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { @@ -54,7 +69,7 @@ test('runs background revalidation in its own trace linked to the triggering req }, ]); - // Refill work the visitor never waited for is not grafted onto the serving trace: the + // The visitor never waited for the refill, so it is not part of the serving trace: the // background revalidation is its own trace, linked back to the request that triggered it. const revalidationTx = await revalidationTxPromise; expect(revalidationTx.contexts?.trace?.trace_id).not.toBe(staleHitTx.contexts?.trace?.trace_id); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts index b4fd33222501..89b2afbb6a3f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -2,13 +2,29 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks-utils'; -// Background stale-while-revalidate refills. Target behavior: the revalidation runs in its own -// trace (not grafted onto the serving trace), links back to the request that triggered it, and -// becomes the `cache_origin` for future hits. Not implemented yet — the test is `test.fail()`. +// Background stale-while-revalidate refills. Target behavior: the revalidation gets its own trace, +// separate from the request trace that served the stale value. The revalidation trace links back to +// that request and becomes the `cache_origin` for future hits. + +/* + +Trace1 (fill) Trace2 (stale hit) Trace3 (hit) +|- put key:A <----link-----o get hit=true key:A |- get hit=true key:A +| | +TraceR: cache.revalidate (own trace, NOT part of Trace2) | +^- link back to Trace2 (link type TBD in the RFC) | +| | +|- put key:A <----------------------link-------------------o cache_origin + +*/ + +// Not implemented yet. Unlike the nesting specs, this cannot be `test.fail()`: the test waits for a +// `cache.revalidate` trace that never arrives, so it would time out — and Playwright reports a +// timeout as a real failure even under `test.fail()`. Hence `test.fixme()`. test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => { test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production'); - test.fail(); + test.fixme(); const id = crypto.randomUUID(); @@ -33,7 +49,7 @@ test('runs background revalidation in its own trace linked to the triggering req ); }); - // Refill work the visitor never waited for is not grafted onto the serving trace: the + // The visitor never waited for the refill, so it is not part of the serving trace: the // background revalidation is its own trace with a `cache.revalidate` segment. const revalidationSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { return ( From 5442877539963c0fb07a928774652d3f43828310 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:48:19 +0200 Subject: [PATCH 3/4] improve comment and ascii trace --- .../app/api/use-cache-swr/route.ts | 4 ++-- .../cacheOriginLinks-revalidation.spec.ts | 21 ++++++++++--------- .../app/api/use-cache-swr/route.ts | 4 ++-- .../cacheOriginLinks-revalidation.spec.ts | 21 ++++++++++--------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts index 970d9f5fbc41..0332efe522b3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/api/use-cache-swr/route.ts @@ -3,8 +3,8 @@ import type { NextRequest } from 'next/server'; async function getSwrValue(id: string): Promise<{ id: string; createdAt: number }> { 'use cache'; - // Past `revalidate` (2s) a read serves the stale value and triggers a background refill; - // `expire` is long so the entry never hard-expires during the test. + // After `revalidate` (2s), a read serves the stale value and triggers a background refill. + // `expire` is long so the entry stays valid for the whole test. cacheLife({ stale: 5, revalidate: 2, expire: 300 }); return { id, createdAt: Date.now() }; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts index b060592f116c..a87f837b1074 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -3,23 +3,24 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; // Background stale-while-revalidate refills. Target behavior: the revalidation gets its own trace, // separate from the request trace that served the stale value. The revalidation trace links back to -// that request and becomes the `cache_origin` for future hits. +// that request, and its `cache.put` becomes the `cache_origin` for future hits. /* Trace1 (fill) Trace2 (stale hit) Trace3 (hit) |- put key:A <----link-----o get hit=true key:A |- get hit=true key:A -| | -TraceR: cache.revalidate (own trace, NOT part of Trace2) | -^- link back to Trace2 (link type TBD in the RFC) | + ^ | + | link (type TBD in the RFC) | +TraceR: cache.revalidate --o (own trace) | | | |- put key:A <----------------------link-------------------o cache_origin */ -// Not implemented yet. Unlike the nesting specs, this cannot be `test.fail()`: the test waits for a -// `cache.revalidate` trace that never arrives, so it would time out — and Playwright reports a -// timeout as a real failure even under `test.fail()`. Hence `test.fixme()`. +// Not implemented yet. Unlike cacheOriginLinks-nesting.spec.ts, this test cannot document the +// target behavior with `test.fail()`: it would wait for a `cache.revalidate` trace that never +// arrives and time out, and Playwright reports a timeout as a real failure even under +// `test.fail()`. So the test is `test.fixme()` until the SDK emits the revalidation trace. test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => { test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production'); @@ -69,7 +70,7 @@ test('runs background revalidation in its own trace linked to the triggering req }, ]); - // The visitor never waited for the refill, so it is not part of the serving trace: the + // The visitor never waited for the refill, so the refill is not part of the serving trace: the // background revalidation is its own trace, linked back to the request that triggered it. const revalidationTx = await revalidationTxPromise; expect(revalidationTx.contexts?.trace?.trace_id).not.toBe(staleHitTx.contexts?.trace?.trace_id); @@ -78,7 +79,7 @@ test('runs background revalidation in its own trace linked to the triggering req trace_id: staleHitTx.contexts?.trace?.trace_id, span_id: expect.stringMatching(/^[0-9a-f]{16}$/), sampled: true, - // The link type for "revalidation triggered by" is not final yet; the target trace is. + // No link type for cache spans specced yet, so assert only the link target. attributes: { 'sentry.link.type': expect.any(String) }, }, ]); @@ -86,7 +87,7 @@ test('runs background revalidation in its own trace linked to the triggering req const revalidationPutSpan = revalidationTx.spans?.find(span => span.op === 'cache.put'); expect(revalidationPutSpan).toBeDefined(); - // The revalidation becomes the origin for future hits. + // The revalidation's `cache.put` becomes the `cache_origin` for future hits. const hitAfterRefillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => { return ( transactionEvent.transaction === 'GET /api/use-cache-swr' && diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts index 970d9f5fbc41..0332efe522b3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/api/use-cache-swr/route.ts @@ -3,8 +3,8 @@ import type { NextRequest } from 'next/server'; async function getSwrValue(id: string): Promise<{ id: string; createdAt: number }> { 'use cache'; - // Past `revalidate` (2s) a read serves the stale value and triggers a background refill; - // `expire` is long so the entry never hard-expires during the test. + // After `revalidate` (2s), a read serves the stale value and triggers a background refill. + // `expire` is long so the entry stays valid for the whole test. cacheLife({ stale: 5, revalidate: 2, expire: 300 }); return { id, createdAt: Date.now() }; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts index 89b2afbb6a3f..bc76efa4f5b1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -4,23 +4,24 @@ import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks- // Background stale-while-revalidate refills. Target behavior: the revalidation gets its own trace, // separate from the request trace that served the stale value. The revalidation trace links back to -// that request and becomes the `cache_origin` for future hits. +// that request, and its `cache.put` becomes the `cache_origin` for future hits. /* Trace1 (fill) Trace2 (stale hit) Trace3 (hit) |- put key:A <----link-----o get hit=true key:A |- get hit=true key:A -| | -TraceR: cache.revalidate (own trace, NOT part of Trace2) | -^- link back to Trace2 (link type TBD in the RFC) | + ^ | + | link (type TBD in the RFC) | +TraceR: cache.revalidate --o (own trace) | | | |- put key:A <----------------------link-------------------o cache_origin */ -// Not implemented yet. Unlike the nesting specs, this cannot be `test.fail()`: the test waits for a -// `cache.revalidate` trace that never arrives, so it would time out — and Playwright reports a -// timeout as a real failure even under `test.fail()`. Hence `test.fixme()`. +// Not implemented yet. Unlike cacheOriginLinks-nesting.spec.ts, this test cannot document the +// target behavior with `test.fail()`: it would wait for a `cache.revalidate` trace that never +// arrives and time out, and Playwright reports a timeout as a real failure even under +// `test.fail()`. So the test is `test.fixme()` until the SDK emits the revalidation trace. test('runs background revalidation in its own trace linked to the triggering request', async ({ request }) => { test.skip(process.env.TEST_ENV !== 'production', 'SWR revalidation timing only holds in production'); @@ -49,7 +50,7 @@ test('runs background revalidation in its own trace linked to the triggering req ); }); - // The visitor never waited for the refill, so it is not part of the serving trace: the + // The visitor never waited for the refill, so the refill is not part of the serving trace: the // background revalidation is its own trace with a `cache.revalidate` segment. const revalidationSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { return ( @@ -85,7 +86,7 @@ test('runs background revalidation in its own trace linked to the triggering req trace_id: staleHitGetSpan!.trace_id, span_id: expect.stringMatching(/^[0-9a-f]{16}$/), sampled: true, - // The link type for "revalidation triggered by" is not final yet; the target trace is. + // No link type for cache spans specced yet, so assert only the link target. attributes: { 'sentry.link.type': { value: expect.any(String), type: 'string' } }, }, ]); @@ -93,7 +94,7 @@ test('runs background revalidation in its own trace linked to the triggering req const revalidationPutSpan = findCacheSpan(revalidationSpans, 'cache.put'); expect(revalidationPutSpan).toBeDefined(); - // The revalidation becomes the origin for future hits. + // The revalidation's `cache.put` becomes the `cache_origin` for future hits. const hitAfterRefillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { return ( spansOfTrace.some(span => span.name === 'GET /api/use-cache-swr' && span.is_segment) && From 01b59baac431a38a570dbf46ff75bb134db4ae47 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:49:17 +0200 Subject: [PATCH 4/4] use spec instead of rfc --- .../tests/cacheOriginLinks-revalidation.spec.ts | 2 +- .../tests/cacheOriginLinks-revalidation.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts index a87f837b1074..50f88a41bdbb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -10,7 +10,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; Trace1 (fill) Trace2 (stale hit) Trace3 (hit) |- put key:A <----link-----o get hit=true key:A |- get hit=true key:A ^ | - | link (type TBD in the RFC) | + | link (type TBD in the spec) | TraceR: cache.revalidate --o (own trace) | | | |- put key:A <----------------------link-------------------o cache_origin diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts index bc76efa4f5b1..b6aa21aa89c2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-revalidation.spec.ts @@ -11,7 +11,7 @@ import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks- Trace1 (fill) Trace2 (stale hit) Trace3 (hit) |- put key:A <----link-----o get hit=true key:A |- get hit=true key:A ^ | - | link (type TBD in the RFC) | + | link (type TBD in the spec) | TraceR: cache.revalidate --o (own trace) | | | |- put key:A <----------------------link-------------------o cache_origin