@decocms/apps-website 7.65.1 → 7.66.0
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps-website",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.66.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco generic-site app: SEO, analytics, theme, and content utilities shared across every commerce backend",
|
|
6
6
|
"repository": {
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"lint:unused": "knip"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@decocms/blocks": "7.
|
|
36
|
-
"@decocms/apps-commerce": "7.
|
|
35
|
+
"@decocms/blocks": "7.66.0",
|
|
36
|
+
"@decocms/apps-commerce": "7.66.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^19.0.0",
|
|
@@ -216,3 +216,44 @@ describe("initOneDollarStats", () => {
|
|
|
216
216
|
expect(event).toHaveBeenCalledWith("ev", { keep: "x" });
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
describe("async tracker readiness", () => {
|
|
221
|
+
// The tracker <script> is `async`, so it can resolve after the 10s poll
|
|
222
|
+
// window closes. Its `load` event is the second, unbounded trigger.
|
|
223
|
+
let tracker: HTMLScriptElement;
|
|
224
|
+
|
|
225
|
+
beforeEach(() => {
|
|
226
|
+
tracker = document.createElement("script");
|
|
227
|
+
tracker.id = "onedollarstats-tracker";
|
|
228
|
+
document.head.appendChild(tracker);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
tracker.remove();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("fires the initial pageview on the tracker's load event after the poll window closes", () => {
|
|
236
|
+
const view = vi.fn();
|
|
237
|
+
initOneDollarStats();
|
|
238
|
+
|
|
239
|
+
// Poll window (200 * 50ms) elapses with no stonks — pageview would be lost.
|
|
240
|
+
vi.advanceTimersByTime(11_000);
|
|
241
|
+
expect(view).not.toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
(window as Window & { stonks?: unknown }).stonks = { view };
|
|
244
|
+
tracker.dispatchEvent(new Event("load"));
|
|
245
|
+
|
|
246
|
+
expect(view).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does not double-fire when both the poll and the load event resolve", () => {
|
|
250
|
+
const view = vi.fn();
|
|
251
|
+
initOneDollarStats();
|
|
252
|
+
|
|
253
|
+
(window as Window & { stonks?: unknown }).stonks = { view };
|
|
254
|
+
vi.advanceTimersByTime(100); // poll wins
|
|
255
|
+
tracker.dispatchEvent(new Event("load"));
|
|
256
|
+
|
|
257
|
+
expect(view).toHaveBeenCalledTimes(1);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
* 2. **`useEffect` for client logic.** All side-effects (initial pageview,
|
|
25
25
|
* pushState wrap, DECO event subscribe) run inside a `useEffect`,
|
|
26
26
|
* which fires after hydration. By then `<ScriptOnce>` in
|
|
27
|
-
* `DecoRootLayout` has bootstrapped `window.DECO.events
|
|
28
|
-
* `<script>` (rendered as a sibling)
|
|
29
|
-
*
|
|
30
|
-
* fragile script-execution-order
|
|
27
|
+
* `DecoRootLayout` has bootstrapped `window.DECO.events`. The SDK
|
|
28
|
+
* `<script>` (rendered as a sibling) is `async`, so `window.stonks`
|
|
29
|
+
* may or may not exist yet — see (4) and (5). No inline
|
|
30
|
+
* `dangerouslySetInnerHTML` snippet, no fragile script-execution-order
|
|
31
|
+
* dependency.
|
|
31
32
|
*
|
|
32
33
|
* 3. **Module-level guards.** `window.DECO.events.subscribe()` returns no
|
|
33
34
|
* unsubscribe handle, so we cannot clean up on unmount. We use a
|
|
@@ -39,6 +40,18 @@
|
|
|
39
40
|
* load). We poll every 50 ms for up to 10 s. Production: resolves
|
|
40
41
|
* within one tick.
|
|
41
42
|
*
|
|
43
|
+
* 5. **The tracker `<script>` is `async`, never `defer`.** A third-party
|
|
44
|
+
* analytics tag must not be able to hold the host page's lifecycle
|
|
45
|
+
* hostage: with `defer`, an unreachable `s.lilstts.com` (adblock,
|
|
46
|
+
* blocked route, DNS/CDN outage) delays DOMContentLoaded/load until
|
|
47
|
+
* the TCP timeout — a real client saw the browser spin for ~5 min
|
|
48
|
+
* with the page already painted. `async` is non-blocking for both
|
|
49
|
+
* parsing and DOMContentLoaded. Because the tracker can then resolve
|
|
50
|
+
* at any point, the first pageview is fired from whichever comes
|
|
51
|
+
* first: the readiness poll, or the tracker's own `load` event
|
|
52
|
+
* (which also covers a tracker that arrives after the 10 s poll
|
|
53
|
+
* window closes). See `deco-cx/apps@e0af145` for the Fresh-side fix.
|
|
54
|
+
*
|
|
42
55
|
* ## Behavioural parity vs Fresh `deco-cx/apps`
|
|
43
56
|
*
|
|
44
57
|
* Mirrors the Path B snippet (`analytics/loaders/OneDollarScript.ts`):
|
|
@@ -69,6 +82,9 @@ export interface Props {
|
|
|
69
82
|
staticScriptUrl?: string;
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
/** DOM id of the tracker `<script>`; we listen to its `load` event. */
|
|
86
|
+
const TRACKER_ID = "onedollarstats-tracker";
|
|
87
|
+
|
|
72
88
|
export const DEFAULT_COLLECTOR_ADDRESS = "https://d.lilstts.com/events";
|
|
73
89
|
export const DEFAULT_ANALYTICS_SCRIPT_URL = "https://s.lilstts.com/deco.js";
|
|
74
90
|
|
|
@@ -91,12 +107,12 @@ function OneDollarStats({ collectorAddress, staticScriptUrl }: Props) {
|
|
|
91
107
|
<link rel="dns-prefetch" href={collector} />
|
|
92
108
|
<link rel="preconnect" href={collector} crossOrigin="anonymous" />
|
|
93
109
|
<script
|
|
94
|
-
id=
|
|
110
|
+
id={TRACKER_ID}
|
|
95
111
|
data-autocollect="false"
|
|
96
112
|
data-hash-routing="true"
|
|
97
113
|
data-url={collector}
|
|
98
114
|
src={staticScript}
|
|
99
|
-
|
|
115
|
+
async
|
|
100
116
|
/>
|
|
101
117
|
<OneDollarStatsClient />
|
|
102
118
|
</>
|
|
@@ -199,6 +215,14 @@ function whenReady<T>(
|
|
|
199
215
|
}, intervalMs);
|
|
200
216
|
}
|
|
201
217
|
|
|
218
|
+
type StonksView = NonNullable<NonNullable<Window["stonks"]>["view"]>;
|
|
219
|
+
|
|
220
|
+
function getStonksView(): StonksView | undefined {
|
|
221
|
+
return typeof window.stonks?.view === "function"
|
|
222
|
+
? window.stonks.view.bind(window.stonks)
|
|
223
|
+
: undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
202
226
|
/**
|
|
203
227
|
* Wire up the analytics integration. Idempotent — only the first call has
|
|
204
228
|
* any effect.
|
|
@@ -212,16 +236,29 @@ export function initOneDollarStats(): void {
|
|
|
212
236
|
const flags = readFlagsFromCookie();
|
|
213
237
|
|
|
214
238
|
// 1) Initial pageview + SPA nav tracking, with flag enrichment.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
//
|
|
240
|
+
// The tracker is `async`, so `window.stonks` may land before hydration,
|
|
241
|
+
// after it, or after the poll window closes. Two independent triggers,
|
|
242
|
+
// first one wins (`started` guard) — otherwise a slow tracker silently
|
|
243
|
+
// drops the landing pageview, since `data-autocollect="false"` means the
|
|
244
|
+
// SDK will never send one for us.
|
|
245
|
+
let started = false;
|
|
246
|
+
const start = (view: StonksView) => {
|
|
247
|
+
if (started) return;
|
|
248
|
+
started = true;
|
|
249
|
+
view(flags);
|
|
250
|
+
wrapHistoryPushState(() => view(flags));
|
|
251
|
+
addEventListener("popstate", () => view(flags));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
whenReady(getStonksView, start);
|
|
255
|
+
document.getElementById(TRACKER_ID)?.addEventListener(
|
|
256
|
+
"load",
|
|
257
|
+
() => {
|
|
258
|
+
const view = getStonksView();
|
|
259
|
+
if (view) start(view);
|
|
224
260
|
},
|
|
261
|
+
{ once: true },
|
|
225
262
|
);
|
|
226
263
|
|
|
227
264
|
// 2) Forward DECO events to stonks.event with flag enrichment.
|