All files / src/lib cache-key.ts

100% Statements 8/8
100% Branches 4/4
100% Functions 1/1
100% Lines 8/8

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59                          2x                                                                   14x   14x 33x 24x     14x 14x 14x    
// Edge cache-key normalization.
//
// Tracking / click-ID params never change the rendered HTML, but they ARE unique
// per click. Left in the cache key, each paid-ad click (Instagram/Facebook append
// a fresh fbclid + utm_* + brid to every URL) becomes its own cache entry → 100%
// edge-cache MISS → every click triggers a live SSR→API fetch → the API saturates
// and the 8s client timeout fires → HTTP 503 storm (prod incident 2026-06-20).
//
// Stripping these collapses all ad clicks onto ONE `/` entry: the first renders +
// caches, the rest are edge HITs that never touch the API. Functional params
// (search, facet filters, page) are preserved, so listing pages still vary.
 
// Known click-ID params (no shared prefix). utm_* is matched by prefix below.
const TRACKING_PARAMS = new Set([
	"fbclid", // Facebook / Instagram
	"brid", // Meta ad click param (seen on m.facebook.com → IG ad referrals)
	"gclid",
	"gad_source",
	"gbraid",
	"wbraid", // Google Ads
	"msclkid", // Microsoft / Bing
	"dclid", // DoubleClick
	"ttclid", // TikTok
	"twclid", // Twitter / X
	"igshid", // Instagram share id
	"li_fat_id", // LinkedIn
	"epik", // Pinterest
	"mc_cid",
	"mc_eid", // Mailchimp
	"yclid", // Yandex
	"scid", // generic source click id
	"_branch_match_id", // Branch
]);
 
/**
 * Build the canonical edge cache-key URL for a request:
 *  - drops `utm_*` and known click-ID params (tracking-only, never affect render),
 *  - sorts remaining params so `?a=1&b=2` and `?b=2&a=1` share one entry,
 *  - appends `__v=<deployVersion>` so each deploy starts a clean cache namespace.
 *
 * Only the cache KEY is normalized; the real request URL (with tracking params)
 * still reaches the page and client-side analytics.
 */
export function buildCacheKeyUrl(
	requestUrl: string,
	deployVersion: string,
): string {
	const u = new URL(requestUrl);
	// Snapshot keys before deleting — mutating the live iterator would skip entries.
	for (const key of [...u.searchParams.keys()]) {
		if (key.startsWith("utm_") || TRACKING_PARAMS.has(key)) {
			u.searchParams.delete(key);
		}
	}
	u.searchParams.sort();
	u.searchParams.set("__v", deployVersion);
	return u.toString();
}