feat(matcher): make matcher-wrapped pages cacheable at CDN edge - #1202
Conversation
Drop `Vary: cookie` from sticky-session matchers and stop treating framework-managed Set-Cookies (deco_matcher_*, deco_segment) as a reason to disable caching. Wire ctx.var.vary.shouldCache into the full-page kill-switch so loaders that declare cache:"no-store" (personalizing loaders) still veto caching. Adds a Deco-Cache-Vary-Cookies hint header so CDN operators can discover which cookies belong in the custom cache key. Extracts applyPageCacheDecision() from the inlined kill-switch as an exported pure function; the request middleware is the only production caller. Adds tests for matcher.ts and middleware.ts — the first tests in blocks/ and for runtime/middleware.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Prerelease TaggingMerging this PR will publish a prerelease automatically.
|
…1601) * feat(commerce): declare cache: "no-store" on personalizing loaders Adds `export const cache = "no-store";` to 24 loaders across VTEX, Linx, Shopify, WAP, Vnda, and Nuvemshop apps. These loaders read auth cookies/headers and bake user-specific data (profile, cart, orders, wishlist, payments) into SSR — they must opt out of full-page CDN caching. Companion to deco-cx/deco#1202 which removes the over-broad `Vary: cookie` blanket from matcher-wrapped pages. After that change lands on stable, the framework's load-bearing safety signal for "this page contains personalized SSR" becomes the loader-level `cache: "no-store"` declaration (already consumed by the partial-render endpoint, newly consumed by the full-page kill-switch). Wake's user/cart/wishlist already declare this; its partner-token loaders use `cacheKey() => null` which already flips `vary.shouldCache=false`. Not modified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: resolve pre-existing type errors blocking CI Fixes 11 type errors that block `deno check **/mod.ts` on PR CI. All pre-existing on main (verified via `git stash`), surfaced now because the Bundle & Check Apps workflow only runs on PRs. - blog `ctx.invoke.records.loaders.drizzle()` (7 sites): the records app is an optional runtime dependency; the existing pattern already handles the unresolved case at runtime. Use the established `(ctx.invoke as any).records` cast (matches the cross-app invoke pattern in vtex/matchers/userSegment.ts:77 and vtex/matchers/birthday.ts:32). - blog/core/records.ts:13 `Record<string, Resolvable<T>>` assignment: ctx.get() returns the literal input type; cast through `unknown`. - blog Seo sections (`ctx.seo`): apply the typeof-narrowed pattern from spire/sections/Seo/SeoBlogPost.tsx. - vtex/loaders/orders/orderplaced.ts:33 cookies: cast Object.fromEntries result to Record<string, string> for stringify(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update version to 0.153.1-next.1 * fix(matchers): declare random matcher cacheable=true Sticky-session A/B test matchers persist the assigned variant in a deco_matcher_* cookie, and CDN cache rules include that cookie in the custom cache key — so per-variant responses get distinct cache entries and the page is safely cacheable. Without this declaration, the framework's applyPageCacheDecision walks ctx.var.flags and forces Cache-Control: no-store when any flag has cacheable !== true. random.ts was the last domino blocking PDPs that use ABTest {{{traffic}}} blocks from being cached at the edge. Cherry-picks the fix from #1602 onto the 1.200.1 prerelease branch so canary sites can soak the full chain (deco@1.200.1-next.2 + apps@0.153.1-next.2) without waiting for #1602 to merge separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update version to 0.153.1-next.2 * fix(matchers): declare cron matcher cacheable=true The cron matcher evaluates a server-side schedule and returns the same answer for every request at any given minute. Within a response's TTL the cron answer is stable; on cache expiry the matcher re-evaluates against the schedule and the next snapshot is taken. There's no per-request variation, no cookie/header dependency outside cache key, no leak surface. Same safety profile as the date matcher, which already declares cacheable=true. Other matchers flagged in the previous audit but NOT promoted in this release: - userAgent.ts: granular UA regex; UA isn't in standard cache key and there's no normalization layer (device.ts works only because the framework provides a 3-way bucket). - location.ts: CF-IP* headers per-IP, not in default cache key. - cookie.ts: arbitrary cookie name=value not in cache key. Leaving those at cacheable !== true is the correct safety default — they force no-store, which prevents cross-user leaks until a site adds the relevant signal to its CDN cache-key rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update version to 0.153.1-next.3 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Vary: cookiefrom sticky-session matchers (blocks/matcher.ts:218) — the cookie value is what determines the variant; CDN cache keys target the specific cookie name, soVary: cookieis overbroad and forces every CDN to bypass cache for matcher pages.deco_matcher_*,deco_segment) out of the kill-switch inruntime/middleware.ts. Only foreign Set-Cookies (cart, profile, etc.) downgradeCache-Controltono-store.ctx.var.vary.shouldCacheinto the full-page kill-switch (Change 2.5). Loaders that declarecache: "no-store"(the documented contract for personalizing loaders) now veto full-page caching the same way they already veto partial-section caching (seeruntime/routes/render.tsx:118-134for the pre-existing reference pattern).Deco-Cache-Vary-Cookieshint header listing the framework cookies present, so CDN operators can discover which cookies belong in the custom cache key.applyPageCacheDecision(headers, input)for testability.blocks/matcher.tsandruntime/middleware.ts.Why
Customer Lojas Torra (~9% of fleet egress, ~5 TB/month) is 100% origin-bound on PDP/PLP/search/home despite a 70-83% overall hit rate. Every matcher-wrapped page emits
Vary: cookie+Cache-Control: no-store, making the response uncacheable at any CDN regardless of cache rules. This is a fleet-wide issue — any deco-cx/deco site with active matchers (A/B tests, banners, sticky variants) has uncacheable pages.After this PR, matcher-wrapped pages emit:
The CDN can then cache them using a custom cache key that includes the matcher cookie values.
Decision table (new)
no-store, no-cache, must-revalidatecache: "no-store"(vary.shouldCache===false)no-store, no-cache, must-revalidatecacheable: falseno-store, no-cache, must-revalidatePAGE_CACHE_CONTROL+Deco-Cache-Vary-CookieshintSafety — landmines in
@deco/appsThe old
Vary: cookie+no-storesledgehammer also incidentally protected pages whose loaders read auth cookies and bake personalization into SSR HTML without emitting any Set-Cookie back. Change 2.5 replaces that protection with the loader-cache: "no-store"contract, but the contract is only as good as the loaders that respect it.Pre-merge audit of
deco-cx/appsfound ~24 personalizing loaders that read auth cookies but do NOT declarecache: "no-store". Each is a silent data-leak vector after this PR. They need a companion PR indeco-cx/apps:VTEX (~14):
user.ts,profile/getCurrentProfile.ts,profile/getProfileByEmail.ts,wishlist.ts,address/getUserAddresses.ts,payment/userPayments.ts,payment/paymentSystems.ts,orders/list.ts,orders/getById.ts,orders/orderplaced.ts,session/getSession.ts,session/getUserSessions.ts,masterdata/searchDocuments.ts,promotion/getPromotionById.tsLinx (3):
user.ts,cart.ts,wishlist/search.tsShopify (2):
user.ts,cart.tsWAP (3):
user.ts,cart.ts,wishlist.tsVnda (1):
cart.tsNuvemshop (1):
cart.tsPer-file fix: add
export const cache = "no-store";.Wake is already correctly protected (
user.ts,cart.ts,wishlist.tsdeclare no-store; partner-token loaders use null cacheKey which already flipsvary.shouldCache=false).Sequencing constraint for stable promotion: the
@deco/appscompanion PR must merge AND be released to npm AND consumed by canary sites BEFORE this framework PR can promote fromnextto stable.Canary gate for stable promotion
next-channel canary must include at least one site exercising all three logged-in patterns: (a) SSR user greeting, (b) SSR mini-cart with items, (c) per-user/B2B pricing baked into product cards. Run 24-48h two-session soak (auth as U1 in browser A, browse same pages in clean B). Stable promotion blocked until zero cross-user leakage observed.Test plan
deno test --unstable-http -A blocks/matcher.test.ts runtime/middleware.test.ts— 12 new tests passdeno test --unstable-http -A runtime/caches/ engine/ clients/ utils/— all 26 existing tests still passdeno check live.tscleannext-tagged site with active matcher;curl -sI→ confirmVary: Accept-Encoding(nocookie) andCache-Control: public, max-age=…@deco/appsPR addingcache: "no-store"to ~24 personalizing loaders (separate PR, blocker for stable)b2cb2d5a5a784f0fb8eba5a39b3784c2, confirmcf-cache-status: HITon PDPs, stats lake shows bypass collapse from ~600k req/day to <50k🤖 Generated with Claude Code
Summary by cubic
Make matcher-wrapped pages cacheable at the CDN edge. Removes overbroad cookie variance and keeps safety by letting loaders and flags veto caching.
New Features
Vary: cookiefrom sticky-session matchers; CDN can cache by specific matcher cookie values.Set-Cookies (deco_matcher_*,deco_segment) no longer forceno-store; only foreign cookies do.ctx.var.vary.shouldCache(e.g., loaders withcache: "no-store").Deco-Cache-Vary-Cookiesheader listing framework cookies to include in CDN cache keys.applyPageCacheDecision(headers, input)for reuse and testing; added tests for matcher and middleware.Migration
deco_matcher_*anddeco_segmentcookie values in the custom cache key for HTML pages.cache: "no-store"; a companion@deco/appsPR is required before promoting this to stable.Written for commit a634468. Summary will update on new commits. Review in cubic