@decocms/blocks 7.62.2 → 7.62.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sdk/cacheHeaders.test.ts +29 -0
- package/src/sdk/cacheHeaders.ts +29 -4
- package/src/sdk/urlUtils.test.ts +34 -0
- package/src/sdk/urlUtils.ts +32 -0
package/package.json
CHANGED
|
@@ -88,6 +88,35 @@ describe("canonicalizeServerFnPayloadForCacheKey — variant-param cache key", (
|
|
|
88
88
|
expect(a).toBe(b);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it("strips tracking params — paid traffic shares the canonical entry", () => {
|
|
92
|
+
const paid = canonicalizeServerFnPayloadForCacheKey(
|
|
93
|
+
envelope("/aneis?filter.color=ouro&utm_source=google&gclid=abc&srsltid=xyz"),
|
|
94
|
+
);
|
|
95
|
+
const canonical = canonicalizeServerFnPayloadForCacheKey(
|
|
96
|
+
envelope("/aneis?filter.color=ouro"),
|
|
97
|
+
);
|
|
98
|
+
expect(paid).toBe(canonical);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("canonicalizes absolute URLs too, not just paths", () => {
|
|
102
|
+
// Sites embed one in the payload (the deferred-section `pageUrl` is built
|
|
103
|
+
// with `new URL(...)`), and the old guard skipped anything not starting
|
|
104
|
+
// with "/" — so not even skuId was stripped there.
|
|
105
|
+
const messy = canonicalizeServerFnPayloadForCacheKey(
|
|
106
|
+
envelope("https://store.com/mala-preta/p?skuId=148940&utm_source=ads"),
|
|
107
|
+
);
|
|
108
|
+
const canonical = canonicalizeServerFnPayloadForCacheKey(
|
|
109
|
+
envelope("https://store.com/mala-preta/p"),
|
|
110
|
+
);
|
|
111
|
+
expect(messy).toBe(canonical);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("leaves non-URL strings alone", () => {
|
|
115
|
+
const a = canonicalizeServerFnPayloadForCacheKey(envelope("mala preta ?utm_source=x"));
|
|
116
|
+
expect(a).toBe(canonicalizeServerFnPayloadForCacheKey(envelope("mala preta ?utm_source=x")));
|
|
117
|
+
expect(a).toContain("utm_source");
|
|
118
|
+
});
|
|
119
|
+
|
|
91
120
|
it("keeps PLP filter/search params (they DO change the response)", () => {
|
|
92
121
|
const filtered = canonicalizeServerFnPayloadForCacheKey(
|
|
93
122
|
envelope("/c/shoes?filter.size=40"),
|
package/src/sdk/cacheHeaders.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { isTrackingParam } from "./urlUtils";
|
|
21
|
+
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
21
23
|
// Types
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
@@ -520,9 +522,11 @@ export const DEFAULT_SERVERFN_CACHE_IGNORE_PARAMS = ["skuId", "idsku"];
|
|
|
520
522
|
/**
|
|
521
523
|
* Canonicalize a TanStack GET server-fn `payload` for use in the edge cache
|
|
522
524
|
* key: re-serialize it (so equivalent requests always produce byte-identical
|
|
523
|
-
* keys) and strip the
|
|
524
|
-
*
|
|
525
|
-
*
|
|
525
|
+
* keys) and strip the cache-irrelevant search params — `ignoreParams` plus every
|
|
526
|
+
* known tracking param — from any embedded route path or absolute URL. This
|
|
527
|
+
* makes `/product/p?skuId=148940`, `/product/p?utm_source=ads` and `/product/p`
|
|
528
|
+
* share one cache entry, since `loadCmsPage` resolves all three to the same
|
|
529
|
+
* page.
|
|
526
530
|
*
|
|
527
531
|
* Returns the original payload unchanged on any parse failure (fail-safe: the
|
|
528
532
|
* caller keeps the raw payload, never worse than before).
|
|
@@ -532,13 +536,34 @@ export function canonicalizeServerFnPayloadForCacheKey(
|
|
|
532
536
|
ignoreParams: string[] = DEFAULT_SERVERFN_CACHE_IGNORE_PARAMS,
|
|
533
537
|
): string {
|
|
534
538
|
try {
|
|
539
|
+
const drop = (sp: URLSearchParams): void => {
|
|
540
|
+
for (const p of ignoreParams) sp.delete(p);
|
|
541
|
+
// Tracking params never change what the server fn resolves, but they DO
|
|
542
|
+
// change this key — so a shopper arriving from paid media mints a brand
|
|
543
|
+
// new entry on every SPA interaction. Measured on a production
|
|
544
|
+
// storefront: `/_serverFn/:id` at 84% MISS, 1401ms average.
|
|
545
|
+
for (const k of [...sp.keys()]) if (isTrackingParam(k)) sp.delete(k);
|
|
546
|
+
};
|
|
535
547
|
const stripQuery = (s: string): string => {
|
|
548
|
+
// Absolute URL: sites embed one in the payload (the deferred-section
|
|
549
|
+
// `pageUrl` is built with `new URL(...)`), and it fragments the key
|
|
550
|
+
// exactly like a relative path does — but the guard below skipped it, so
|
|
551
|
+
// not even `skuId` was canonicalized there.
|
|
552
|
+
if (/^https?:\/\//i.test(s)) {
|
|
553
|
+
try {
|
|
554
|
+
const u = new URL(s);
|
|
555
|
+
drop(u.searchParams);
|
|
556
|
+
return u.toString();
|
|
557
|
+
} catch {
|
|
558
|
+
return s;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
536
561
|
if (!s.startsWith("/") || s.startsWith("//")) return s;
|
|
537
562
|
const qIdx = s.indexOf("?");
|
|
538
563
|
if (qIdx < 0) return s;
|
|
539
564
|
const path = s.slice(0, qIdx);
|
|
540
565
|
const sp = new URLSearchParams(s.slice(qIdx + 1));
|
|
541
|
-
|
|
566
|
+
drop(sp);
|
|
542
567
|
const rest = sp.toString();
|
|
543
568
|
return rest ? `${path}?${rest}` : path;
|
|
544
569
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { cleanPathForCacheKey, isTrackingParam } from "./urlUtils";
|
|
3
|
+
|
|
4
|
+
describe("tracking params that used to fragment the cache key", () => {
|
|
5
|
+
// Each of these was measured missing the edge cache on every route probed on a
|
|
6
|
+
// production storefront, because it was absent from UTM_PARAMS.
|
|
7
|
+
const cases = [
|
|
8
|
+
["GA4 campaign id", "utm_id"],
|
|
9
|
+
["GA4 source platform", "utm_source_platform"],
|
|
10
|
+
["Google Ads source", "gad_source"],
|
|
11
|
+
["Google Ads iOS click id", "gbraid"],
|
|
12
|
+
["Google Ads web-to-app click id", "wbraid"],
|
|
13
|
+
["GA cross-domain linker", "_gl"],
|
|
14
|
+
["Instagram", "igshid"],
|
|
15
|
+
["Pinterest", "epik"],
|
|
16
|
+
["Marketo", "mkt_tok"],
|
|
17
|
+
["Yandex", "yclid"],
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
for (const [label, param] of cases) {
|
|
21
|
+
it(`strips ${label} (${param})`, () => {
|
|
22
|
+
expect(isTrackingParam(param)).toBe(true);
|
|
23
|
+
expect(cleanPathForCacheKey(`https://x.com/aneis?color=ouro&${param}=abc`)).toBe(
|
|
24
|
+
"/aneis?color=ouro",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("still keeps functional params", () => {
|
|
30
|
+
for (const p of ["page", "order", "map", "PS", "filter.color", "skuId", "q"]) {
|
|
31
|
+
expect(isTrackingParam(p)).toBe(false);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/sdk/urlUtils.ts
CHANGED
|
@@ -25,6 +25,27 @@ const UTM_PARAMS = new Set([
|
|
|
25
25
|
"mc_eid",
|
|
26
26
|
"ttclid",
|
|
27
27
|
"srsltid",
|
|
28
|
+
// GA4's newer campaign params. The five classic `utm_*` above predate them,
|
|
29
|
+
// so a GA4-tagged campaign was still fragmenting the cache key.
|
|
30
|
+
"utm_id",
|
|
31
|
+
"utm_source_platform",
|
|
32
|
+
"utm_creative_format",
|
|
33
|
+
"utm_marketing_tactic",
|
|
34
|
+
// Google Ads click ids that replaced bare `gclid` for privacy-restricted
|
|
35
|
+
// traffic (iOS/Safari), plus the source tag now appended to essentially every
|
|
36
|
+
// Google Ads click. Measured on a production storefront: each of these missed
|
|
37
|
+
// the edge cache on 3/3 routes probed, deterministically.
|
|
38
|
+
"gad_source",
|
|
39
|
+
"gad_campaignid",
|
|
40
|
+
"gbraid",
|
|
41
|
+
"wbraid",
|
|
42
|
+
// Google Analytics cross-domain linker. Sites already strip this by hand in
|
|
43
|
+
// client code, which is the tell that it shows up in real URLs.
|
|
44
|
+
"_gl",
|
|
45
|
+
"igshid",
|
|
46
|
+
"epik",
|
|
47
|
+
"mkt_tok",
|
|
48
|
+
"yclid",
|
|
28
49
|
]);
|
|
29
50
|
|
|
30
51
|
/**
|
|
@@ -44,6 +65,17 @@ export function registerTrackingParams(params: string[]): void {
|
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether a search param is a known tracking/attribution param.
|
|
70
|
+
*
|
|
71
|
+
* Exposed so cache-key builders can filter params in place without having to
|
|
72
|
+
* round-trip through a `URL` (server-fn payloads carry bare relative paths) and
|
|
73
|
+
* without keeping a second copy of the list.
|
|
74
|
+
*/
|
|
75
|
+
export function isTrackingParam(param: string): boolean {
|
|
76
|
+
return UTM_PARAMS.has(param.toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
/**
|
|
48
80
|
* Strip UTM and tracking parameters from a URL.
|
|
49
81
|
*
|