@metricinsights/ca-analytics 0.1.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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/index.cjs +633 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +598 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// src/core/constants.ts
|
|
2
|
+
var ENTITY_NAME = "analytics_events";
|
|
3
|
+
var VERSION = "0.1.0";
|
|
4
|
+
var MI_PAGE_PATH_RE = /^\/(p[tl]?)\/([^/]+)(.*)$/;
|
|
5
|
+
var WIDTHS = {
|
|
6
|
+
app: 100,
|
|
7
|
+
event: 100,
|
|
8
|
+
page_path: 400,
|
|
9
|
+
version: 20,
|
|
10
|
+
meta: 2e3
|
|
11
|
+
};
|
|
12
|
+
var RESERVED_EVENTS = ["page_view", "heartbeat", "component_render", "element_click"];
|
|
13
|
+
var EVENT_NAME_RE = /^[a-z][a-z0-9_]{0,99}$/;
|
|
14
|
+
var LIMITS = { referrer: 300, query: 300 };
|
|
15
|
+
|
|
16
|
+
// src/core/config.ts
|
|
17
|
+
var UNSUBSTITUTED_RE = /^\[[A-Za-z0-9][A-Za-z0-9 _-]*\]$/;
|
|
18
|
+
function portalVariables() {
|
|
19
|
+
if (typeof window === "undefined") return {};
|
|
20
|
+
const block = window.PP_VARIABLES;
|
|
21
|
+
return block && typeof block === "object" && !Array.isArray(block) ? block : {};
|
|
22
|
+
}
|
|
23
|
+
function readFlag(key) {
|
|
24
|
+
const raw = portalVariables()[key];
|
|
25
|
+
if (typeof raw === "boolean") return raw;
|
|
26
|
+
if (typeof raw !== "string") return void 0;
|
|
27
|
+
const value = raw.trim().toLowerCase();
|
|
28
|
+
if (value === "" || UNSUBSTITUTED_RE.test(raw.trim())) return void 0;
|
|
29
|
+
return value !== "0" && value !== "false" && value !== "n" && value !== "no";
|
|
30
|
+
}
|
|
31
|
+
function matchPage() {
|
|
32
|
+
if (typeof window === "undefined" || !window.location) return null;
|
|
33
|
+
return MI_PAGE_PATH_RE.exec(window.location.pathname);
|
|
34
|
+
}
|
|
35
|
+
function currentPagePath() {
|
|
36
|
+
const match = matchPage();
|
|
37
|
+
if (!match) return "/";
|
|
38
|
+
const rest = match[3];
|
|
39
|
+
return (rest && rest !== "" && rest !== "/" ? rest : "/").slice(0, WIDTHS.page_path);
|
|
40
|
+
}
|
|
41
|
+
function resolveConfig(options = {}) {
|
|
42
|
+
const enabled = options.enabled ?? readFlag("CUSTOM_APP_ANALYTICS_ENABLED") ?? true;
|
|
43
|
+
if (!enabled) return null;
|
|
44
|
+
const app = (options.app ?? matchPage()?.[2] ?? "").slice(0, WIDTHS.app);
|
|
45
|
+
if (!app) return null;
|
|
46
|
+
return { app, endpoint: `/data/page/${encodeURIComponent(app)}/${ENTITY_NAME}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/core/event.ts
|
|
50
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
51
|
+
function toId(value) {
|
|
52
|
+
const n = Number(value);
|
|
53
|
+
return Number.isFinite(n) ? Math.trunc(n) : 0;
|
|
54
|
+
}
|
|
55
|
+
function formatTs(date) {
|
|
56
|
+
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
|
|
57
|
+
}
|
|
58
|
+
function stringify(value) {
|
|
59
|
+
try {
|
|
60
|
+
const out = JSON.stringify(value);
|
|
61
|
+
return typeof out === "string" ? out : null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function serializeMeta(meta) {
|
|
67
|
+
if (!meta) return "{}";
|
|
68
|
+
const full = stringify(meta);
|
|
69
|
+
if (full !== null && full.length <= WIDTHS.meta) return full;
|
|
70
|
+
const kept = {};
|
|
71
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
72
|
+
const candidate = stringify({ ...kept, [key]: value, _truncated: 1 });
|
|
73
|
+
if (candidate === null || candidate.length > WIDTHS.meta) continue;
|
|
74
|
+
kept[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return stringify({ ...kept, _truncated: 1 }) ?? "{}";
|
|
77
|
+
}
|
|
78
|
+
function uuid() {
|
|
79
|
+
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
80
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
81
|
+
const bytes = new Uint8Array(16);
|
|
82
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
83
|
+
c.getRandomValues(bytes);
|
|
84
|
+
} else {
|
|
85
|
+
for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
|
|
86
|
+
}
|
|
87
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
88
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
89
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
90
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
91
|
+
}
|
|
92
|
+
function createRow(input) {
|
|
93
|
+
return {
|
|
94
|
+
id: uuid(),
|
|
95
|
+
ts: formatTs(/* @__PURE__ */ new Date()),
|
|
96
|
+
app: input.app.slice(0, WIDTHS.app),
|
|
97
|
+
event: input.event.slice(0, WIDTHS.event),
|
|
98
|
+
session_id: input.sessionId.slice(0, 36),
|
|
99
|
+
page_path: input.pagePath.slice(0, WIDTHS.page_path),
|
|
100
|
+
element_id: toId(input.elementId),
|
|
101
|
+
version: VERSION.slice(0, WIDTHS.version),
|
|
102
|
+
meta: serializeMeta(input.meta)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/core/transport.ts
|
|
107
|
+
var RETRY_DELAY_MS = 2e3;
|
|
108
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
var toArrayBody = (rows) => `[${rows.join(",")}]`;
|
|
110
|
+
async function post(endpoint, body) {
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(endpoint, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
credentials: "same-origin",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
return { ok: false, retryable: response.status === 429 || response.status >= 500 };
|
|
120
|
+
}
|
|
121
|
+
const payload = await response.json().catch(() => null);
|
|
122
|
+
if (payload && typeof payload.resultCode === "number" && payload.resultCode !== 0) {
|
|
123
|
+
return { ok: false, retryable: false };
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, retryable: false };
|
|
126
|
+
} catch {
|
|
127
|
+
return { ok: false, retryable: true };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function send(endpoint, rows) {
|
|
131
|
+
if (rows.length === 0) return "ok";
|
|
132
|
+
const body = toArrayBody(rows);
|
|
133
|
+
const first = await post(endpoint, body);
|
|
134
|
+
if (first.ok) return "ok";
|
|
135
|
+
if (!first.retryable) return "failed";
|
|
136
|
+
await sleep(RETRY_DELAY_MS);
|
|
137
|
+
return (await post(endpoint, body)).ok ? "ok" : "failed";
|
|
138
|
+
}
|
|
139
|
+
function sendOnUnload(endpoint, rows) {
|
|
140
|
+
if (rows.length === 0) return;
|
|
141
|
+
const body = toArrayBody(rows);
|
|
142
|
+
try {
|
|
143
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
144
|
+
if (navigator.sendBeacon && navigator.sendBeacon(endpoint, blob)) return;
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
void fetch(endpoint, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
credentials: "same-origin",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body,
|
|
153
|
+
keepalive: true
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/core/queue.ts
|
|
160
|
+
var FLUSH_INTERVAL_MS = 15e3;
|
|
161
|
+
var MAX_FLUSH_ROWS = 50;
|
|
162
|
+
var MAX_FLUSH_BYTES = 3e4;
|
|
163
|
+
var MAX_CONSECUTIVE_FAILURES = 3;
|
|
164
|
+
var textEncoder = new TextEncoder();
|
|
165
|
+
function createQueue(endpoint, onKilled) {
|
|
166
|
+
let buffer = [];
|
|
167
|
+
let bytes = 0;
|
|
168
|
+
let failures = 0;
|
|
169
|
+
let stopped = false;
|
|
170
|
+
let inFlight = false;
|
|
171
|
+
let killed = false;
|
|
172
|
+
const timer = setInterval(() => {
|
|
173
|
+
void flush();
|
|
174
|
+
}, FLUSH_INTERVAL_MS);
|
|
175
|
+
function stop() {
|
|
176
|
+
stopped = true;
|
|
177
|
+
buffer = [];
|
|
178
|
+
bytes = 0;
|
|
179
|
+
clearInterval(timer);
|
|
180
|
+
}
|
|
181
|
+
async function flush() {
|
|
182
|
+
if (stopped || inFlight || buffer.length === 0) return;
|
|
183
|
+
const batch = buffer;
|
|
184
|
+
buffer = [];
|
|
185
|
+
bytes = 0;
|
|
186
|
+
inFlight = true;
|
|
187
|
+
try {
|
|
188
|
+
let outcome;
|
|
189
|
+
try {
|
|
190
|
+
outcome = await send(
|
|
191
|
+
endpoint,
|
|
192
|
+
batch.map((b) => b.json)
|
|
193
|
+
);
|
|
194
|
+
} catch {
|
|
195
|
+
outcome = "failed";
|
|
196
|
+
}
|
|
197
|
+
if (outcome === "ok") {
|
|
198
|
+
failures = 0;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
failures += 1;
|
|
202
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES && !killed) {
|
|
203
|
+
killed = true;
|
|
204
|
+
stop();
|
|
205
|
+
try {
|
|
206
|
+
onKilled?.();
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
inFlight = false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
push(row) {
|
|
216
|
+
if (stopped) return;
|
|
217
|
+
let json;
|
|
218
|
+
try {
|
|
219
|
+
json = JSON.stringify(row);
|
|
220
|
+
} catch {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const size = textEncoder.encode(json).length;
|
|
224
|
+
buffer.push({ json, size });
|
|
225
|
+
bytes += size;
|
|
226
|
+
if (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES) void flush();
|
|
227
|
+
},
|
|
228
|
+
flush() {
|
|
229
|
+
void flush();
|
|
230
|
+
},
|
|
231
|
+
flushOnUnload() {
|
|
232
|
+
if (stopped || buffer.length === 0) return;
|
|
233
|
+
const batch = buffer;
|
|
234
|
+
buffer = [];
|
|
235
|
+
bytes = 0;
|
|
236
|
+
sendOnUnload(
|
|
237
|
+
endpoint,
|
|
238
|
+
batch.map((b) => b.json)
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
stop
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/core/session.ts
|
|
246
|
+
var SID_KEY = "ca_pa_sid";
|
|
247
|
+
var LAST_KEY = "ca_pa_last";
|
|
248
|
+
var IDLE_MS = 30 * 60 * 1e3;
|
|
249
|
+
var TOUCH_THROTTLE_MS = 5e3;
|
|
250
|
+
var memoryId = null;
|
|
251
|
+
var lastWrittenAt = 0;
|
|
252
|
+
function read(key) {
|
|
253
|
+
try {
|
|
254
|
+
return localStorage.getItem(key);
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function write(key, value) {
|
|
260
|
+
try {
|
|
261
|
+
localStorage.setItem(key, value);
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function newId() {
|
|
266
|
+
try {
|
|
267
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
271
|
+
}
|
|
272
|
+
function resetSession() {
|
|
273
|
+
memoryId = null;
|
|
274
|
+
lastWrittenAt = 0;
|
|
275
|
+
}
|
|
276
|
+
function getSessionId() {
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const storedId = read(SID_KEY);
|
|
279
|
+
const rawLast = read(LAST_KEY);
|
|
280
|
+
const last = rawLast === null ? Number.NaN : Number(rawLast);
|
|
281
|
+
const alive = Number.isFinite(last) && now - last <= IDLE_MS;
|
|
282
|
+
let id;
|
|
283
|
+
if (storedId && alive) {
|
|
284
|
+
id = storedId;
|
|
285
|
+
} else if (!storedId && memoryId) {
|
|
286
|
+
id = memoryId;
|
|
287
|
+
} else {
|
|
288
|
+
id = newId();
|
|
289
|
+
write(SID_KEY, id);
|
|
290
|
+
lastWrittenAt = 0;
|
|
291
|
+
}
|
|
292
|
+
memoryId = id;
|
|
293
|
+
if (now - lastWrittenAt >= TOUCH_THROTTLE_MS) {
|
|
294
|
+
write(LAST_KEY, String(now));
|
|
295
|
+
lastWrittenAt = now;
|
|
296
|
+
}
|
|
297
|
+
return id;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/core/signals/heartbeat.ts
|
|
301
|
+
var HEARTBEAT_FAST_MS = 6e4;
|
|
302
|
+
var HEARTBEAT_SLOW_MS = 3e5;
|
|
303
|
+
var HEARTBEAT_FAST_COUNT = 5;
|
|
304
|
+
function startHeartbeat(emit) {
|
|
305
|
+
let beats = 0;
|
|
306
|
+
let timer;
|
|
307
|
+
let stopped = false;
|
|
308
|
+
const interval = () => beats < HEARTBEAT_FAST_COUNT ? HEARTBEAT_FAST_MS : HEARTBEAT_SLOW_MS;
|
|
309
|
+
function schedule() {
|
|
310
|
+
if (stopped || timer !== void 0) return;
|
|
311
|
+
timer = setTimeout(() => {
|
|
312
|
+
timer = void 0;
|
|
313
|
+
if (stopped || document.visibilityState !== "visible") return;
|
|
314
|
+
beats += 1;
|
|
315
|
+
try {
|
|
316
|
+
emit();
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
schedule();
|
|
320
|
+
}, interval());
|
|
321
|
+
}
|
|
322
|
+
function clear() {
|
|
323
|
+
if (timer !== void 0) {
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
timer = void 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const onVisibilityChange = () => {
|
|
329
|
+
if (stopped) return;
|
|
330
|
+
if (document.visibilityState === "visible") {
|
|
331
|
+
schedule();
|
|
332
|
+
} else {
|
|
333
|
+
clear();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
337
|
+
if (document.visibilityState === "visible") schedule();
|
|
338
|
+
return {
|
|
339
|
+
stop() {
|
|
340
|
+
stopped = true;
|
|
341
|
+
clear();
|
|
342
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/core/signals/miRender.ts
|
|
348
|
+
var RENDER_CAP = 50;
|
|
349
|
+
function startMiRender(emit) {
|
|
350
|
+
let stopped = false;
|
|
351
|
+
const counts = /* @__PURE__ */ new Map();
|
|
352
|
+
const onRender = (event) => {
|
|
353
|
+
if (stopped) return;
|
|
354
|
+
try {
|
|
355
|
+
const detail = event.detail;
|
|
356
|
+
if (!detail || typeof detail !== "object") return;
|
|
357
|
+
const component = typeof detail.component === "string" ? detail.component : "";
|
|
358
|
+
if (!component) return;
|
|
359
|
+
const props = detail.props && typeof detail.props === "object" ? detail.props : {};
|
|
360
|
+
const elementId = toId(props.element ?? props.element_id ?? 0);
|
|
361
|
+
const segmentId = toId(props.segment ?? props.segment_value_id ?? 0);
|
|
362
|
+
const key = `${component}|${elementId}|${segmentId}`;
|
|
363
|
+
const seq = (counts.get(key) ?? 0) + 1;
|
|
364
|
+
if (seq > RENDER_CAP) return;
|
|
365
|
+
counts.set(key, seq);
|
|
366
|
+
const meta = {
|
|
367
|
+
component,
|
|
368
|
+
element_id: elementId,
|
|
369
|
+
segment_id: segmentId
|
|
370
|
+
};
|
|
371
|
+
if (seq === RENDER_CAP) meta.capped = 1;
|
|
372
|
+
emit({ elementId, meta });
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
document.addEventListener("mi-render", onRender);
|
|
377
|
+
return {
|
|
378
|
+
stop() {
|
|
379
|
+
stopped = true;
|
|
380
|
+
document.removeEventListener("mi-render", onRender);
|
|
381
|
+
counts.clear();
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/core/signals/pageview.ts
|
|
387
|
+
var inboundReferrer = () => {
|
|
388
|
+
try {
|
|
389
|
+
const raw = document.referrer;
|
|
390
|
+
if (!raw) return "";
|
|
391
|
+
const url = new URL(raw);
|
|
392
|
+
return `${url.origin}${url.pathname}`;
|
|
393
|
+
} catch {
|
|
394
|
+
return "";
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
function startPageView(emit, currentPath) {
|
|
398
|
+
let stopped = false;
|
|
399
|
+
let lastPath = null;
|
|
400
|
+
let lastLocation = null;
|
|
401
|
+
const locationKey = (path) => {
|
|
402
|
+
try {
|
|
403
|
+
return `${path}${window.location.search}`;
|
|
404
|
+
} catch {
|
|
405
|
+
return path;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const safeEmit = (resolvePath, dedupe) => {
|
|
409
|
+
if (stopped) return;
|
|
410
|
+
try {
|
|
411
|
+
const path = resolvePath();
|
|
412
|
+
const location = locationKey(path);
|
|
413
|
+
if (dedupe && location === lastLocation) return;
|
|
414
|
+
const referrer = lastPath === null ? inboundReferrer() : lastPath;
|
|
415
|
+
lastPath = path;
|
|
416
|
+
lastLocation = location;
|
|
417
|
+
emit(path, referrer);
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const originalPush = window.history.pushState;
|
|
422
|
+
const originalReplace = window.history.replaceState;
|
|
423
|
+
const patch = (original) => function patched(...args) {
|
|
424
|
+
const result = original.apply(this, args);
|
|
425
|
+
safeEmit(currentPath, true);
|
|
426
|
+
return result;
|
|
427
|
+
};
|
|
428
|
+
const patchedPush = patch(originalPush);
|
|
429
|
+
const patchedReplace = patch(originalReplace);
|
|
430
|
+
window.history.pushState = patchedPush;
|
|
431
|
+
window.history.replaceState = patchedReplace;
|
|
432
|
+
const onPopState = () => safeEmit(currentPath, true);
|
|
433
|
+
window.addEventListener("popstate", onPopState);
|
|
434
|
+
safeEmit(currentPath, true);
|
|
435
|
+
return {
|
|
436
|
+
stop() {
|
|
437
|
+
stopped = true;
|
|
438
|
+
lastPath = null;
|
|
439
|
+
lastLocation = null;
|
|
440
|
+
if (window.history.pushState === patchedPush) {
|
|
441
|
+
window.history.pushState = originalPush;
|
|
442
|
+
}
|
|
443
|
+
if (window.history.replaceState === patchedReplace) {
|
|
444
|
+
window.history.replaceState = originalReplace;
|
|
445
|
+
}
|
|
446
|
+
window.removeEventListener("popstate", onPopState);
|
|
447
|
+
},
|
|
448
|
+
emitNow(path) {
|
|
449
|
+
safeEmit(() => path ?? currentPath(), false);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/core/analytics.ts
|
|
455
|
+
var runtime = null;
|
|
456
|
+
var warnedOnce = /* @__PURE__ */ new Set();
|
|
457
|
+
function guard(fn) {
|
|
458
|
+
try {
|
|
459
|
+
fn();
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function warnOnce(key, message) {
|
|
464
|
+
if (warnedOnce.has(key)) return;
|
|
465
|
+
warnedOnce.add(key);
|
|
466
|
+
console.warn(`[ca-analytics] ${message}`);
|
|
467
|
+
}
|
|
468
|
+
function pageViewMeta(referrer) {
|
|
469
|
+
const query = window.location.search.replace(/^\?/, "");
|
|
470
|
+
return { referrer: referrer.slice(0, LIMITS.referrer), query: query.slice(0, LIMITS.query) };
|
|
471
|
+
}
|
|
472
|
+
function stopStarted(parts, flushFirst) {
|
|
473
|
+
if (flushFirst) guard(() => parts.queue.flushOnUnload());
|
|
474
|
+
guard(() => parts.pageView?.stop());
|
|
475
|
+
guard(() => parts.stopHeartbeat?.());
|
|
476
|
+
guard(() => parts.stopMiRender?.());
|
|
477
|
+
guard(() => parts.detachUnload());
|
|
478
|
+
guard(() => parts.queue.stop());
|
|
479
|
+
}
|
|
480
|
+
function teardown(flushFirst) {
|
|
481
|
+
const current = runtime;
|
|
482
|
+
if (!current) return;
|
|
483
|
+
runtime = null;
|
|
484
|
+
warnedOnce.clear();
|
|
485
|
+
stopStarted(current, flushFirst);
|
|
486
|
+
resetSession();
|
|
487
|
+
}
|
|
488
|
+
function init(options = {}) {
|
|
489
|
+
guard(() => {
|
|
490
|
+
if (runtime) return;
|
|
491
|
+
const config = resolveConfig(options);
|
|
492
|
+
if (!config) return;
|
|
493
|
+
const queue = createQueue(config.endpoint, () => shutdown());
|
|
494
|
+
const emit = (event, extra = {}) => {
|
|
495
|
+
queue.push(
|
|
496
|
+
createRow({
|
|
497
|
+
app: config.app,
|
|
498
|
+
event,
|
|
499
|
+
sessionId: getSessionId(),
|
|
500
|
+
pagePath: extra.pagePath ?? currentPagePath(),
|
|
501
|
+
elementId: extra.elementId,
|
|
502
|
+
meta: extra.meta
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
};
|
|
506
|
+
const onUnload = () => guard(() => queue.flushOnUnload());
|
|
507
|
+
const onVisibility = () => {
|
|
508
|
+
if (document.visibilityState === "hidden") onUnload();
|
|
509
|
+
};
|
|
510
|
+
const detachUnload = () => {
|
|
511
|
+
window.removeEventListener("pagehide", onUnload);
|
|
512
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
513
|
+
};
|
|
514
|
+
let pageView;
|
|
515
|
+
let stopHeartbeat;
|
|
516
|
+
let stopMiRender;
|
|
517
|
+
try {
|
|
518
|
+
window.addEventListener("pagehide", onUnload);
|
|
519
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
520
|
+
stopHeartbeat = startHeartbeat(() => emit("heartbeat")).stop;
|
|
521
|
+
stopMiRender = startMiRender(
|
|
522
|
+
(payload) => emit("component_render", { elementId: payload.elementId, meta: payload.meta })
|
|
523
|
+
).stop;
|
|
524
|
+
pageView = startPageView(
|
|
525
|
+
(path, referrer) => emit("page_view", { pagePath: path, meta: pageViewMeta(referrer) }),
|
|
526
|
+
currentPagePath
|
|
527
|
+
);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
stopStarted({ queue, detachUnload, pageView, stopHeartbeat, stopMiRender }, false);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
runtime = { queue, emit, detachUnload, pageView, stopHeartbeat, stopMiRender };
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function track(event, props) {
|
|
536
|
+
guard(() => {
|
|
537
|
+
if (!runtime) return;
|
|
538
|
+
if (!event) return;
|
|
539
|
+
const normalized = event.toLowerCase();
|
|
540
|
+
if (RESERVED_EVENTS.includes(normalized)) {
|
|
541
|
+
warnOnce("reserved", `"${event}" is reserved and was not tracked (further occurrences are silent)`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (!EVENT_NAME_RE.test(event)) {
|
|
545
|
+
warnOnce("name", `"${event}" is not snake_case \u2264100 chars; tracked as-is (further occurrences are silent)`);
|
|
546
|
+
}
|
|
547
|
+
runtime.emit(event, { meta: props });
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function trackElement(props) {
|
|
551
|
+
guard(() => {
|
|
552
|
+
if (!runtime) return;
|
|
553
|
+
const { element_id, segment_id, ...extra } = props ?? {};
|
|
554
|
+
const id = toId(element_id);
|
|
555
|
+
if (!id) {
|
|
556
|
+
warnOnce("element", `trackElement() ignored a non-finite or zero element_id (${element_id})`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const segmentRaw = Number(segment_id);
|
|
560
|
+
const segmentFinite = Number.isFinite(segmentRaw);
|
|
561
|
+
if (segment_id !== void 0 && !segmentFinite) {
|
|
562
|
+
warnOnce("segment", `trackElement() got a non-finite segment_id (${segment_id}); recorded as 0`);
|
|
563
|
+
}
|
|
564
|
+
const meta = {
|
|
565
|
+
...extra,
|
|
566
|
+
element_id: id,
|
|
567
|
+
segment_id: segmentFinite ? Math.trunc(segmentRaw) : 0
|
|
568
|
+
};
|
|
569
|
+
runtime.emit("element_click", { elementId: id, meta });
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function trackPageView(path) {
|
|
573
|
+
guard(() => runtime?.pageView.emitNow(path));
|
|
574
|
+
}
|
|
575
|
+
function shutdown() {
|
|
576
|
+
guard(() => teardown(true));
|
|
577
|
+
}
|
|
578
|
+
function isActive() {
|
|
579
|
+
return runtime !== null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/react/useAnalytics.ts
|
|
583
|
+
var api = { track, trackElement, trackPageView };
|
|
584
|
+
function useAnalytics() {
|
|
585
|
+
return api;
|
|
586
|
+
}
|
|
587
|
+
export {
|
|
588
|
+
ENTITY_NAME,
|
|
589
|
+
VERSION,
|
|
590
|
+
init,
|
|
591
|
+
isActive,
|
|
592
|
+
shutdown,
|
|
593
|
+
track,
|
|
594
|
+
trackElement,
|
|
595
|
+
trackPageView,
|
|
596
|
+
useAnalytics
|
|
597
|
+
};
|
|
598
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/constants.ts","../src/core/config.ts","../src/core/event.ts","../src/core/transport.ts","../src/core/queue.ts","../src/core/session.ts","../src/core/signals/heartbeat.ts","../src/core/signals/miRender.ts","../src/core/signals/pageview.ts","../src/core/analytics.ts","../src/react/useAnalytics.ts"],"sourcesContent":["/** Entity name bound on every tracked app's portal page. Not configurable. */\nexport const ENTITY_NAME = 'analytics_events';\n\n/** From package.json at build time. */\nexport const VERSION = __VERSION__;\n\n/** MI serves portal pages at /p/<name>, /pl/<name> or /pt/<name>. */\nexport const MI_PAGE_PATH_RE = /^\\/(p[tl]?)\\/([^/]+)(.*)$/;\n\n/** Per-field payload caps that keep a batch within sendBeacon's 64 KiB quota, not schema limits. */\nexport const WIDTHS = {\n app: 100,\n event: 100,\n page_path: 400,\n version: 20,\n meta: 2000,\n} as const;\n\n/** Emitted by ca-analytics. track() rejects them so app code cannot forge them. */\nexport const RESERVED_EVENTS = ['page_view', 'heartbeat', 'component_render', 'element_click'] as const;\n\n/** Custom event names must match this or track() warns once. Silent drift creates permanent duplicate series. */\nexport const EVENT_NAME_RE = /^[a-z][a-z0-9_]{0,99}$/;\n\n/** Per-key meta caps, applied before serializeMeta so one long value can't push another key out. */\nexport const LIMITS = { referrer: 300, query: 300 } as const;\n","import { ENTITY_NAME, MI_PAGE_PATH_RE, WIDTHS } from './constants';\nimport type { Options, ResolvedConfig } from './types';\n\n/** Unsubstituted PP variable, e.g. '[Custom App Analytics Enabled]'. Treated as unset. */\nconst UNSUBSTITUTED_RE = /^\\[[A-Za-z0-9][A-Za-z0-9 _-]*\\]$/;\n\nfunction portalVariables(): Record<string, unknown> {\n if (typeof window === 'undefined') return {};\n\n const block = (window as unknown as Record<string, unknown>).PP_VARIABLES;\n\n return block && typeof block === 'object' && !Array.isArray(block) ? (block as Record<string, unknown>) : {};\n}\n\n/** PP variables arrive as strings, so '0' and 'false' must not read truthy. */\nfunction readFlag(key: string): boolean | undefined {\n const raw = portalVariables()[key];\n\n if (typeof raw === 'boolean') return raw;\n if (typeof raw !== 'string') return undefined;\n\n const value = raw.trim().toLowerCase();\n\n if (value === '' || UNSUBSTITUTED_RE.test(raw.trim())) return undefined;\n\n return value !== '0' && value !== 'false' && value !== 'n' && value !== 'no';\n}\n\nfunction matchPage(): RegExpExecArray | null {\n if (typeof window === 'undefined' || !window.location) return null;\n\n return MI_PAGE_PATH_RE.exec(window.location.pathname);\n}\n\n/** Route below the portal page. '/' at the page root. */\nexport function currentPagePath(): string {\n const match = matchPage();\n\n if (!match) return '/';\n\n const rest = match[3];\n\n return (rest && rest !== '' && rest !== '/' ? rest : '/').slice(0, WIDTHS.page_path);\n}\n\n/** Runtime config, or null when ca-analytics must stay silent: disabled, or not on a portal page. */\nexport function resolveConfig(options: Options = {}): ResolvedConfig | null {\n const enabled = options.enabled ?? readFlag('CUSTOM_APP_ANALYTICS_ENABLED') ?? true;\n\n if (!enabled) return null;\n\n const app = (options.app ?? matchPage()?.[2] ?? '').slice(0, WIDTHS.app);\n\n if (!app) return null;\n\n return { app, endpoint: `/data/page/${encodeURIComponent(app)}/${ENTITY_NAME}` };\n}\n","import { VERSION, WIDTHS } from './constants';\nimport type { EventRow } from './types';\n\nexport interface RowInput {\n app: string;\n event: string;\n sessionId: string;\n pagePath: string;\n elementId?: number;\n meta?: Record<string, unknown>;\n}\n\nconst pad2 = (n: number) => String(n).padStart(2, '0');\n\n/** Integer only: a float or string value widens the `int` column permanently. */\nexport function toId(value: unknown): number {\n const n = Number(value);\n\n return Number.isFinite(n) ? Math.trunc(n) : 0;\n}\n\n/** Space-separated, never ISO — a `T`/`Z` sniffs as text and the column stops being `datetime`. */\nexport function formatTs(date: Date): string {\n return (\n `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ` +\n `${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`\n );\n}\n\nfunction stringify(value: unknown): string | null {\n try {\n const out = JSON.stringify(value);\n\n return typeof out === 'string' ? out : null;\n } catch {\n return null;\n }\n}\n\n/** JSON string capped at WIDTHS.meta. Drops whole keys instead of slicing — a sliced payload is invalid JSON. */\nexport function serializeMeta(meta?: Record<string, unknown>): string {\n if (!meta) return '{}';\n\n const full = stringify(meta);\n\n if (full !== null && full.length <= WIDTHS.meta) return full;\n\n const kept: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(meta)) {\n const candidate = stringify({ ...kept, [key]: value, _truncated: 1 });\n\n if (candidate === null || candidate.length > WIDTHS.meta) continue;\n\n kept[key] = value;\n }\n\n return stringify({ ...kept, _truncated: 1 }) ?? '{}';\n}\n\nexport function uuid(): string {\n const c = typeof crypto !== 'undefined' ? crypto : undefined;\n\n if (c && typeof c.randomUUID === 'function') return c.randomUUID();\n\n // Older Safari has getRandomValues but not randomUUID.\n const bytes = new Uint8Array(16);\n\n if (c && typeof c.getRandomValues === 'function') {\n c.getRandomValues(bytes);\n } else {\n for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);\n }\n\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/** Only place a row is built. `ts`/`element_id` type mistakes here lock the shared dataset's schema permanently. */\nexport function createRow(input: RowInput): EventRow {\n return {\n id: uuid(),\n ts: formatTs(new Date()),\n app: input.app.slice(0, WIDTHS.app),\n event: input.event.slice(0, WIDTHS.event),\n session_id: input.sessionId.slice(0, 36),\n page_path: input.pagePath.slice(0, WIDTHS.page_path),\n element_id: toId(input.elementId),\n version: VERSION.slice(0, WIDTHS.version),\n meta: serializeMeta(input.meta),\n };\n}\n","export type SendOutcome = 'ok' | 'failed';\n\nconst RETRY_DELAY_MS = 2000;\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/** Rows arrive pre-serialized; joining as `[a,b,c]` matches JSON.stringify(rows) byte-for-byte without a second pass. */\nconst toArrayBody = (rows: string[]): string => `[${rows.join(',')}]`;\n\nasync function post(endpoint: string, body: string): Promise<{ ok: boolean; retryable: boolean }> {\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n return { ok: false, retryable: response.status === 429 || response.status >= 500 };\n }\n\n const payload = (await response.json().catch(() => null)) as { resultCode?: number } | null;\n\n // 200 with a non-zero resultCode is a rejected insert, not a success.\n if (payload && typeof payload.resultCode === 'number' && payload.resultCode !== 0) {\n return { ok: false, retryable: false };\n }\n\n return { ok: true, retryable: false };\n } catch {\n return { ok: false, retryable: true };\n }\n}\n\n/** One retry max, same rows — a lost-response double-insert is caught by readers using COUNT(DISTINCT id). */\nexport async function send(endpoint: string, rows: string[]): Promise<SendOutcome> {\n if (rows.length === 0) return 'ok';\n\n const body = toArrayBody(rows);\n const first = await post(endpoint, body);\n\n if (first.ok) return 'ok';\n if (!first.retryable) return 'failed';\n\n await sleep(RETRY_DELAY_MS);\n\n return (await post(endpoint, body)).ok ? 'ok' : 'failed';\n}\n\n/** Unload path. No retry possible — the page is going away. */\nexport function sendOnUnload(endpoint: string, rows: string[]): void {\n if (rows.length === 0) return;\n\n const body = toArrayBody(rows);\n\n try {\n const blob = new Blob([body], { type: 'application/json' });\n\n if (navigator.sendBeacon && navigator.sendBeacon(endpoint, blob)) return;\n } catch {\n // fall through to keepalive fetch\n }\n\n try {\n void fetch(endpoint, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true,\n });\n } catch {\n // Dropped. Nothing further possible at unload.\n }\n}\n","import { send, sendOnUnload } from './transport';\nimport type { SendOutcome } from './transport';\nimport type { EventRow } from './types';\n\n/** A row's JSON already computed once in push(); send() joins these into the array body instead of re-stringifying. */\ntype Buffered = { json: string; size: number };\n\nconst FLUSH_INTERVAL_MS = 15_000;\nconst MAX_FLUSH_ROWS = 50;\n/** Half of sendBeacon's 64 KiB cumulative quota. The server has no practical ceiling. */\nconst MAX_FLUSH_BYTES = 30_000;\nconst MAX_CONSECUTIVE_FAILURES = 3;\n\n// Counts encoded UTF-8 bytes, not UTF-16 code units — what sendBeacon actually charges against its quota.\nconst textEncoder = new TextEncoder();\n\nexport interface Queue {\n push(row: EventRow): void;\n flush(): void;\n flushOnUnload(): void;\n stop(): void;\n}\n\nexport function createQueue(endpoint: string, onKilled?: () => void): Queue {\n let buffer: Buffered[] = [];\n let bytes = 0;\n let failures = 0;\n let stopped = false;\n let inFlight = false;\n // Guards re-entrancy: onKilled calls shutdown() -> stop() again, but the kill must fire once.\n let killed = false;\n\n const timer = setInterval(() => {\n void flush();\n }, FLUSH_INTERVAL_MS);\n\n function stop(): void {\n stopped = true;\n buffer = [];\n bytes = 0;\n clearInterval(timer);\n }\n\n async function flush(): Promise<void> {\n if (stopped || inFlight || buffer.length === 0) return;\n\n const batch = buffer;\n\n buffer = [];\n bytes = 0;\n inFlight = true;\n\n try {\n let outcome: SendOutcome;\n\n // send() isn't expected to reject, but a rejection must not become an unhandled promise rejection.\n try {\n outcome = await send(\n endpoint,\n batch.map((b) => b.json),\n );\n } catch {\n outcome = 'failed';\n }\n\n if (outcome === 'ok') {\n failures = 0;\n return;\n }\n\n failures += 1;\n\n // Batch dropped, not re-buffered: transport already retried once, and re-sending a batch whose\n // response was merely lost is what creates duplicates.\n if (failures >= MAX_CONSECUTIVE_FAILURES && !killed) {\n killed = true;\n stop();\n\n // Hand the kill up to the owner so it can stop the timers/listeners still producing rows.\n try {\n onKilled?.();\n } catch {\n // swallowed — a host callback must never escape\n }\n }\n } finally {\n inFlight = false;\n }\n }\n\n return {\n push(row: EventRow): void {\n if (stopped) return;\n\n let json: string;\n\n try {\n json = JSON.stringify(row);\n } catch {\n return;\n }\n\n const size = textEncoder.encode(json).length;\n\n buffer.push({ json, size });\n bytes += size;\n\n if (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES) void flush();\n },\n flush(): void {\n void flush();\n },\n flushOnUnload(): void {\n if (stopped || buffer.length === 0) return;\n\n const batch = buffer;\n\n buffer = [];\n bytes = 0;\n sendOnUnload(\n endpoint,\n batch.map((b) => b.json),\n );\n },\n stop,\n };\n}\n","const SID_KEY = 'ca_pa_sid';\nconst LAST_KEY = 'ca_pa_last';\nconst IDLE_MS = 30 * 60 * 1000;\nconst TOUCH_THROTTLE_MS = 5000;\n\nlet memoryId: string | null = null;\nlet lastWrittenAt = 0;\n\n/** Safari private mode and blocked-storage policies throw on access, not just on write. */\nfunction read(key: string): string | null {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction write(key: string, value: string): void {\n try {\n localStorage.setItem(key, value);\n } catch {\n // In-memory session for the page lifetime beats no tracking.\n }\n}\n\nfunction newId(): string {\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();\n } catch {}\n\n return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/** Clears in-memory state only. Storage stays, so a reload still finds the session. */\nexport function resetSession(): void {\n memoryId = null;\n lastWrittenAt = 0;\n}\n\n/** Session id, minting a new one after 30 min idle. Extends the window on every call. */\nexport function getSessionId(): string {\n const now = Date.now();\n const storedId = read(SID_KEY);\n const rawLast = read(LAST_KEY);\n const last = rawLast === null ? Number.NaN : Number(rawLast);\n const alive = Number.isFinite(last) && now - last <= IDLE_MS;\n\n let id: string;\n\n if (storedId && alive) {\n // Trusting storage here is what survives a reload.\n id = storedId;\n } else if (!storedId && memoryId) {\n // Blocked localStorage always reads null; reusing the memory id is the only stable fallback.\n id = memoryId;\n } else {\n id = newId();\n write(SID_KEY, id);\n lastWrittenAt = 0;\n }\n\n memoryId = id;\n\n if (now - lastWrittenAt >= TOUCH_THROTTLE_MS) {\n write(LAST_KEY, String(now));\n lastWrittenAt = now;\n }\n\n return id;\n}\n","export const HEARTBEAT_FAST_MS = 60_000;\nexport const HEARTBEAT_SLOW_MS = 300_000;\nexport const HEARTBEAT_FAST_COUNT = 5;\n\n/** Tapered, visible-only — readers derive engaged time as beats x interval, so a hidden beat would count idle time. */\nexport function startHeartbeat(emit: () => void): { stop(): void } {\n let beats = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let stopped = false;\n\n const interval = () => (beats < HEARTBEAT_FAST_COUNT ? HEARTBEAT_FAST_MS : HEARTBEAT_SLOW_MS);\n\n function schedule(): void {\n if (stopped || timer !== undefined) return;\n\n timer = setTimeout(() => {\n timer = undefined;\n\n if (stopped || document.visibilityState !== 'visible') return;\n\n beats += 1;\n\n try {\n emit();\n } catch {\n // A failed emit must not stop the timer.\n }\n\n schedule();\n }, interval());\n }\n\n function clear(): void {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n }\n\n const onVisibilityChange = () => {\n if (stopped) return;\n\n if (document.visibilityState === 'visible') {\n schedule();\n } else {\n // Discard the partial interval rather than credit time the user was away.\n clear();\n }\n };\n\n document.addEventListener('visibilitychange', onVisibilityChange);\n\n if (document.visibilityState === 'visible') schedule();\n\n return {\n stop(): void {\n stopped = true;\n clear();\n document.removeEventListener('visibilitychange', onVisibilityChange);\n },\n };\n}\n","import { toId } from '../event';\n\nexport const RENDER_CAP = 50;\n\nexport interface RenderPayload {\n elementId: number;\n meta: Record<string, unknown>;\n}\n\n/** Listens on `document` only: `MiNamespace.render()` dispatches a bubbling CustomEvent, so load order doesn't matter. */\nexport function startMiRender(emit: (payload: RenderPayload) => void): { stop(): void } {\n let stopped = false;\n const counts = new Map<string, number>();\n\n const onRender = (event: Event) => {\n if (stopped) return;\n\n try {\n const detail = (event as CustomEvent).detail as { component?: unknown; props?: unknown } | undefined;\n\n if (!detail || typeof detail !== 'object') return;\n\n const component = typeof detail.component === 'string' ? detail.component : '';\n\n if (!component) return;\n\n const props = (detail.props && typeof detail.props === 'object' ? detail.props : {}) as Record<string, unknown>;\n // Apps send either {element, segment} or {element_id, segment_value_id}.\n const elementId = toId(props.element ?? props.element_id ?? 0);\n const segmentId = toId(props.segment ?? props.segment_value_id ?? 0);\n\n const key = `${component}|${elementId}|${segmentId}`;\n const seq = (counts.get(key) ?? 0) + 1;\n\n // Some apps re-render on every filter change; uncapped, one page burns the throttle budget.\n if (seq > RENDER_CAP) return;\n\n counts.set(key, seq);\n\n const meta: Record<string, unknown> = {\n component,\n element_id: elementId,\n segment_id: segmentId,\n };\n\n if (seq === RENDER_CAP) meta.capped = 1;\n\n emit({ elementId, meta });\n } catch {\n // Never throw into the host render path.\n }\n };\n\n document.addEventListener('mi-render', onRender);\n\n return {\n stop(): void {\n stopped = true;\n document.removeEventListener('mi-render', onRender);\n counts.clear();\n },\n };\n}\n","export interface PageViewSignal {\n stop(): void;\n emitNow(path?: string): void;\n}\n\n/** Absolute for an inbound hop, bare route after — a route in another app is not comparable here. */\nconst inboundReferrer = (): string => {\n try {\n const raw = document.referrer;\n\n if (!raw) return '';\n\n const url = new URL(raw);\n\n return `${url.origin}${url.pathname}`;\n } catch {\n return '';\n }\n};\n\n/** Emits once on start, then on every history navigation. The original history method always still runs. */\nexport function startPageView(\n emit: (path: string, referrer: string) => void,\n currentPath: () => string,\n): PageViewSignal {\n let stopped = false;\n let lastPath: string | null = null;\n let lastLocation: string | null = null;\n\n // Dedupe key: path alone can't tell a `?tab=` change from a no-op replaceState.\n const locationKey = (path: string): string => {\n try {\n return `${path}${window.location.search}`;\n } catch {\n return path;\n }\n };\n\n // resolvePath is a thunk so a throwing host currentPath() is caught here too, not in the caller.\n // dedupe is off for emitNow(): an explicit trackPageView() must always send.\n const safeEmit = (resolvePath: () => string, dedupe: boolean) => {\n if (stopped) return;\n\n try {\n const path = resolvePath();\n const location = locationKey(path);\n\n // Routers replaceState for scroll and filter sync without moving. Not a view.\n if (dedupe && location === lastLocation) return;\n\n // GA4 semantics: referrer is the previous page_view's path, not document.referrer, except first.\n const referrer = lastPath === null ? inboundReferrer() : lastPath;\n\n lastPath = path;\n lastLocation = location;\n emit(path, referrer);\n } catch {\n // swallowed\n }\n };\n\n const originalPush = window.history.pushState;\n const originalReplace = window.history.replaceState;\n\n const patch = (original: typeof window.history.pushState) =>\n function patched(this: History, ...args: Parameters<typeof window.history.pushState>) {\n const result = original.apply(this, args);\n\n safeEmit(currentPath, true);\n\n return result;\n };\n\n const patchedPush = patch(originalPush);\n const patchedReplace = patch(originalReplace);\n\n window.history.pushState = patchedPush;\n window.history.replaceState = patchedReplace;\n\n const onPopState = () => safeEmit(currentPath, true);\n\n window.addEventListener('popstate', onPopState);\n\n safeEmit(currentPath, true);\n\n return {\n stop(): void {\n stopped = true;\n lastPath = null;\n lastLocation = null;\n\n // Restore only if this signal still owns the patch — another startPageView may have patched over it.\n if (window.history.pushState === patchedPush) {\n window.history.pushState = originalPush;\n }\n\n if (window.history.replaceState === patchedReplace) {\n window.history.replaceState = originalReplace;\n }\n\n window.removeEventListener('popstate', onPopState);\n },\n emitNow(path?: string): void {\n safeEmit(() => path ?? currentPath(), false);\n },\n };\n}\n","import { EVENT_NAME_RE, LIMITS, RESERVED_EVENTS } from './constants';\nimport { currentPagePath, resolveConfig } from './config';\nimport { createRow, toId } from './event';\nimport { createQueue, type Queue } from './queue';\nimport { getSessionId, resetSession } from './session';\nimport { startHeartbeat } from './signals/heartbeat';\nimport { startMiRender } from './signals/miRender';\nimport { startPageView, type PageViewSignal } from './signals/pageview';\nimport type { JsonValue, Options } from './types';\n\ntype Emit = (\n event: string,\n extra?: { pagePath?: string; elementId?: number; meta?: Record<string, unknown> },\n) => void;\n\ninterface Runtime {\n queue: Queue;\n emit: Emit;\n detachUnload: () => void;\n pageView: PageViewSignal;\n stopHeartbeat: () => void;\n stopMiRender: () => void;\n}\n\nlet runtime: Runtime | null = null;\nconst warnedOnce = new Set<string>();\n\n/** Nothing in ca-analytics may throw into the host app. */\nfunction guard(fn: () => void): void {\n try {\n fn();\n } catch {\n // swallowed\n }\n}\n\n/** Warns about caller misuse only the first time `key` is seen. */\nfunction warnOnce(key: string, message: string): void {\n if (warnedOnce.has(key)) return;\n\n warnedOnce.add(key);\n console.warn(`[ca-analytics] ${message}`);\n}\n\nfunction pageViewMeta(referrer: string): Record<string, unknown> {\n const query = window.location.search.replace(/^\\?/, '');\n\n return { referrer: referrer.slice(0, LIMITS.referrer), query: query.slice(0, LIMITS.query) };\n}\n\n/** Stops whatever of init()'s parts already started, used both for teardown and a failed startup. */\nfunction stopStarted(\n parts: {\n queue: Queue;\n detachUnload: () => void;\n pageView?: PageViewSignal;\n stopHeartbeat?: () => void;\n stopMiRender?: () => void;\n },\n flushFirst: boolean,\n): void {\n if (flushFirst) guard(() => parts.queue.flushOnUnload());\n\n guard(() => parts.pageView?.stop());\n guard(() => parts.stopHeartbeat?.());\n guard(() => parts.stopMiRender?.());\n guard(() => parts.detachUnload());\n guard(() => parts.queue.stop());\n}\n\n/** Unwinds everything `init()` started; `flushFirst` also drains the buffer via the unload path. `runtime` is cleared first so a re-entrant call is a no-op. */\nfunction teardown(flushFirst: boolean): void {\n const current = runtime;\n\n if (!current) return;\n\n runtime = null;\n warnedOnce.clear();\n\n stopStarted(current, flushFirst);\n resetSession();\n}\n\n/** Idempotent. A second call while active is ignored rather than doubling listeners. */\nexport function init(options: Options = {}): void {\n guard(() => {\n if (runtime) return;\n\n const config = resolveConfig(options);\n\n if (!config) return;\n\n // A tripped kill-switch tears down ca-analytics entirely, not just the queue.\n const queue = createQueue(config.endpoint, () => shutdown());\n\n const emit: Emit = (event, extra = {}) => {\n queue.push(\n createRow({\n app: config.app,\n event,\n sessionId: getSessionId(),\n pagePath: extra.pagePath ?? currentPagePath(),\n elementId: extra.elementId,\n meta: extra.meta,\n }),\n );\n };\n\n const onUnload = () => guard(() => queue.flushOnUnload());\n // pagehide alone is unreliable on iOS; visibilitychange is what fires there.\n const onVisibility = () => {\n if (document.visibilityState === 'hidden') onUnload();\n };\n const detachUnload = () => {\n window.removeEventListener('pagehide', onUnload);\n document.removeEventListener('visibilitychange', onVisibility);\n };\n\n let pageView: PageViewSignal | undefined;\n let stopHeartbeat: (() => void) | undefined;\n let stopMiRender: (() => void) | undefined;\n\n // A later step throwing must unwind what already started, or its timers/listeners leak.\n try {\n window.addEventListener('pagehide', onUnload);\n document.addEventListener('visibilitychange', onVisibility);\n\n stopHeartbeat = startHeartbeat(() => emit('heartbeat')).stop;\n stopMiRender = startMiRender((payload) =>\n emit('component_render', { elementId: payload.elementId, meta: payload.meta }),\n ).stop;\n pageView = startPageView(\n (path, referrer) => emit('page_view', { pagePath: path, meta: pageViewMeta(referrer) }),\n currentPagePath,\n );\n } catch (error) {\n stopStarted({ queue, detachUnload, pageView, stopHeartbeat, stopMiRender }, false);\n throw error;\n }\n\n runtime = { queue, emit, detachUnload, pageView, stopHeartbeat, stopMiRender };\n });\n}\n\nexport function track(event: string, props?: Record<string, JsonValue>): void {\n guard(() => {\n if (!runtime) return;\n if (!event) return;\n\n const normalized = event.toLowerCase();\n\n if ((RESERVED_EVENTS as readonly string[]).includes(normalized)) {\n warnOnce('reserved', `\"${event}\" is reserved and was not tracked (further occurrences are silent)`);\n\n return;\n }\n\n if (!EVENT_NAME_RE.test(event)) {\n warnOnce('name', `\"${event}\" is not snake_case ≤100 chars; tracked as-is (further occurrences are silent)`);\n }\n\n runtime.emit(event, { meta: props });\n });\n}\n\n/** `element_id`/`segment_id` resolve identity via `/api/element_info`; everything else is free-form. */\nexport function trackElement(props: { element_id: number; segment_id?: number; [key: string]: JsonValue }): void {\n guard(() => {\n if (!runtime) return;\n\n const { element_id, segment_id, ...extra } = props ?? {};\n const id = toId(element_id);\n\n if (!id) {\n warnOnce('element', `trackElement() ignored a non-finite or zero element_id (${element_id})`);\n\n return;\n }\n\n const segmentRaw = Number(segment_id);\n const segmentFinite = Number.isFinite(segmentRaw);\n\n // Omitting segment_id is a supported call; only a supplied-but-bad value is worth a warning.\n if (segment_id !== undefined && !segmentFinite) {\n warnOnce('segment', `trackElement() got a non-finite segment_id (${segment_id}); recorded as 0`);\n }\n\n // Reserved keys last: a caller's own element_id/segment_id in extra must not win the spread.\n const meta: Record<string, unknown> = {\n ...extra,\n element_id: id,\n segment_id: segmentFinite ? Math.trunc(segmentRaw) : 0,\n };\n\n runtime.emit('element_click', { elementId: id, meta });\n });\n}\n\n/** For routers whose navigation the history patch cannot observe. */\nexport function trackPageView(path?: string): void {\n guard(() => runtime?.pageView.emitNow(path));\n}\n\n/** Flushes what is buffered before stopping — up to 15s of events would otherwise drop. */\nexport function shutdown(): void {\n guard(() => teardown(true));\n}\n\nexport function isActive(): boolean {\n return runtime !== null;\n}\n","import { track, trackElement, trackPageView } from '../core/analytics';\n\n/** Thin wrapper over the module singleton. No provider, no context — ca-analytics renders nothing. */\nconst api = { track, trackElement, trackPageView } as const;\n\nexport function useAnalytics(): typeof api {\n return api;\n}\n"],"mappings":"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","names":[]}
|