@forgecart/cli 2.202608160103.0 → 2.202608200713.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
|
@@ -145,6 +145,12 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
145
145
|
|
|
146
146
|
const incomingSession = request.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
147
147
|
const forwardedFor = request.headers.get('x-forwarded-for');
|
|
148
|
+
// #1014: mirror the browser's low-entropy Client Hints trio upstream —
|
|
149
|
+
// Chromium sends them on every request; the shop API's device
|
|
150
|
+
// identification prefers them over the frozen UA.
|
|
151
|
+
const secChUa = request.headers.get('sec-ch-ua') ?? undefined;
|
|
152
|
+
const secChUaMobile = request.headers.get('sec-ch-ua-mobile') ?? undefined;
|
|
153
|
+
const secChUaPlatform = request.headers.get('sec-ch-ua-platform') ?? undefined;
|
|
148
154
|
|
|
149
155
|
let sessionToken = incomingSession;
|
|
150
156
|
const results: TrackItemResult[] = [];
|
|
@@ -157,7 +163,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
157
163
|
results.push({ accepted: false, eventId: null });
|
|
158
164
|
continue;
|
|
159
165
|
}
|
|
160
|
-
const outcome = await forwardTrackEvent(input, { sessionToken, userAgent, forwardedFor });
|
|
166
|
+
const outcome = await forwardTrackEvent(input, { sessionToken, userAgent, forwardedFor, secChUa, secChUaMobile, secChUaPlatform });
|
|
161
167
|
results.push({ accepted: outcome.accepted, eventId: outcome.eventId });
|
|
162
168
|
if (outcome.sessionToken) sessionToken = outcome.sessionToken;
|
|
163
169
|
}
|
|
@@ -73,6 +73,12 @@ const FLUSH_INTERVAL_MS = 500;
|
|
|
73
73
|
const ESTABLISHMENT_GRACE_MS = 2_000;
|
|
74
74
|
/** Same-signal suppression window (per click slug; per page_view path). */
|
|
75
75
|
const SUPPRESS_MS = 300;
|
|
76
|
+
/**
|
|
77
|
+
* Minimum VISIBLE dwell before a route's scroll_depth summary is worth a
|
|
78
|
+
* row (#1022 phase 2b) — filters bounce-through navigations and StrictMode's
|
|
79
|
+
* dev-only phantom unmount.
|
|
80
|
+
*/
|
|
81
|
+
const ENGAGEMENT_MIN_DWELL_MS = 250;
|
|
76
82
|
const TRACK_ENDPOINT = '/__fc/track';
|
|
77
83
|
|
|
78
84
|
/** One buffered event — the relay forwards these fields to `trackEvent`. */
|
|
@@ -277,5 +283,90 @@ export function ForgeTracker({ enabled }: { enabled: boolean }) {
|
|
|
277
283
|
};
|
|
278
284
|
}, [enabled]);
|
|
279
285
|
|
|
286
|
+
// scroll_depth — ONE engagement summary per route (#1022 phase 2b): the
|
|
287
|
+
// max scroll depth reached (percent, 0-100) and the VISIBLE dwell time,
|
|
288
|
+
// emitted when the route unmounts (client-side navigation) or the page
|
|
289
|
+
// hides. Rides the dormant `scroll_depth` event type already in the
|
|
290
|
+
// default shop allowlist; the server hoists both properties into the
|
|
291
|
+
// typed `scrollDepth`/`durationMs` ClickHouse columns (CH 005).
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!enabled) return;
|
|
294
|
+
if (window.parent !== window) return;
|
|
295
|
+
|
|
296
|
+
let maxDepthPct = 0;
|
|
297
|
+
let visibleMs = 0;
|
|
298
|
+
let visibleSince: number | null =
|
|
299
|
+
document.visibilityState === 'visible' ? performance.now() : null;
|
|
300
|
+
let ticking = false;
|
|
301
|
+
let emitted = false;
|
|
302
|
+
|
|
303
|
+
const measure = (): void => {
|
|
304
|
+
ticking = false;
|
|
305
|
+
const doc = document.documentElement;
|
|
306
|
+
// A page shorter than the viewport is fully seen — 100 by definition;
|
|
307
|
+
// otherwise percent of total document height brought into view.
|
|
308
|
+
const pct =
|
|
309
|
+
doc.scrollHeight <= window.innerHeight
|
|
310
|
+
? 100
|
|
311
|
+
: Math.min(
|
|
312
|
+
100,
|
|
313
|
+
Math.round(((window.scrollY + window.innerHeight) / doc.scrollHeight) * 100),
|
|
314
|
+
);
|
|
315
|
+
if (pct > maxDepthPct) maxDepthPct = pct;
|
|
316
|
+
};
|
|
317
|
+
const onScroll = (): void => {
|
|
318
|
+
if (ticking) return;
|
|
319
|
+
ticking = true;
|
|
320
|
+
requestAnimationFrame(measure);
|
|
321
|
+
};
|
|
322
|
+
const onVisibility = (): void => {
|
|
323
|
+
const now = performance.now();
|
|
324
|
+
if (document.visibilityState === 'visible') {
|
|
325
|
+
visibleSince ??= now;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (visibleSince !== null) {
|
|
329
|
+
visibleMs += now - visibleSince;
|
|
330
|
+
visibleSince = null;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const emit = (): void => {
|
|
334
|
+
if (emitted) return;
|
|
335
|
+
if (visibleSince !== null) {
|
|
336
|
+
visibleMs += performance.now() - visibleSince;
|
|
337
|
+
visibleSince = null;
|
|
338
|
+
}
|
|
339
|
+
// Dwell floor: a route that never accumulated meaningful visible time
|
|
340
|
+
// has no engagement story — this also swallows StrictMode's dev-only
|
|
341
|
+
// mount/unmount phantom, which would otherwise emit a zero-dwell row.
|
|
342
|
+
if (visibleMs < ENGAGEMENT_MIN_DWELL_MS) return;
|
|
343
|
+
emitted = true;
|
|
344
|
+
enqueue('scroll_depth', {
|
|
345
|
+
path: window.location.pathname,
|
|
346
|
+
scrollDepth: maxDepthPct,
|
|
347
|
+
durationMs: Math.round(visibleMs),
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
const onPagehide = (): void => {
|
|
351
|
+
// Emit BEFORE the flush so the summary rides the pagehide beacon —
|
|
352
|
+
// flushOnPagehide drains-and-no-ops on empty, so the sibling pagehide
|
|
353
|
+
// listener's own call stays harmless.
|
|
354
|
+
emit();
|
|
355
|
+
flushOnPagehide();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
measure(); // above-the-fold baseline before any scroll fires
|
|
359
|
+
|
|
360
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
361
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
362
|
+
window.addEventListener('pagehide', onPagehide);
|
|
363
|
+
return () => {
|
|
364
|
+
window.removeEventListener('scroll', onScroll);
|
|
365
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
366
|
+
window.removeEventListener('pagehide', onPagehide);
|
|
367
|
+
emit(); // client-side route change: summarize the route being left
|
|
368
|
+
};
|
|
369
|
+
}, [enabled, pathname]);
|
|
370
|
+
|
|
280
371
|
return null;
|
|
281
372
|
}
|
|
@@ -74,6 +74,16 @@ export interface ForwardHeaders {
|
|
|
74
74
|
sessionToken: string | null;
|
|
75
75
|
/** The browser's own User-Agent, forwarded for device enrichment. */
|
|
76
76
|
userAgent: string;
|
|
77
|
+
/**
|
|
78
|
+
* The browser's low-entropy Client Hints (`sec-ch-ua`, `sec-ch-ua-mobile`,
|
|
79
|
+
* `sec-ch-ua-platform`), mirrored verbatim so device identification can
|
|
80
|
+
* outrank the frozen UA per-field (#1014). Chromium sends the trio on
|
|
81
|
+
* every request — no handshake, no `Accept-CH` — so this is a pure
|
|
82
|
+
* pass-through; absent on non-Chromium browsers.
|
|
83
|
+
*/
|
|
84
|
+
secChUa?: string;
|
|
85
|
+
secChUaMobile?: string;
|
|
86
|
+
secChUaPlatform?: string;
|
|
77
87
|
/**
|
|
78
88
|
* Client IP as `x-forwarded-for` (the proxy-standard contract the shop API
|
|
79
89
|
* trusts) — NEVER ForgeCart's edge-secret-gated client-IP override header
|
|
@@ -119,7 +129,7 @@ export function isObviousBot(userAgent: string): boolean {
|
|
|
119
129
|
*/
|
|
120
130
|
export async function forwardTrackEvent(
|
|
121
131
|
input: TrackEventInput,
|
|
122
|
-
{ sessionToken, userAgent, forwardedFor }: ForwardHeaders,
|
|
132
|
+
{ sessionToken, userAgent, forwardedFor, secChUa, secChUaMobile, secChUaPlatform }: ForwardHeaders,
|
|
123
133
|
): Promise<UpstreamOutcome> {
|
|
124
134
|
const upstream = getUpstreamConfig();
|
|
125
135
|
if (!upstream) return { accepted: false, eventId: null, sessionToken: null };
|
|
@@ -131,6 +141,9 @@ export async function forwardTrackEvent(
|
|
|
131
141
|
if (sessionToken) headers['Authorization'] = `Bearer ${sessionToken}`;
|
|
132
142
|
if (userAgent) headers['user-agent'] = userAgent;
|
|
133
143
|
if (forwardedFor) headers['x-forwarded-for'] = forwardedFor;
|
|
144
|
+
if (secChUa) headers['sec-ch-ua'] = secChUa;
|
|
145
|
+
if (secChUaMobile) headers['sec-ch-ua-mobile'] = secChUaMobile;
|
|
146
|
+
if (secChUaPlatform) headers['sec-ch-ua-platform'] = secChUaPlatform;
|
|
134
147
|
|
|
135
148
|
try {
|
|
136
149
|
const response = await fetch(upstream.shopApiUrl, {
|