@metricinsights/ca-analytics 0.2.0 → 0.3.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/README.md +49 -21
- package/dist/index.cjs +59 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.js +59 -14
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -33,19 +33,22 @@ npm i @metricinsights/ca-analytics
|
|
|
33
33
|
|
|
34
34
|
## Usage
|
|
35
35
|
|
|
36
|
-
```
|
|
36
|
+
```tsx
|
|
37
37
|
import { init, useAnalytics } from '@metricinsights/ca-analytics';
|
|
38
38
|
|
|
39
39
|
// app entry, once
|
|
40
40
|
init();
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// in any component
|
|
43
|
+
function RevenueTile() {
|
|
44
|
+
const { track } = useAnalytics();
|
|
45
|
+
|
|
46
|
+
return <button onClick={() => track('tile_clicked', { tile: 'revenue' })}>Revenue</button>;
|
|
47
|
+
}
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
`useAnalytics()` is a thin wrapper over the module singleton — no provider, no context, callable
|
|
48
|
-
from any component.
|
|
51
|
+
from any component. Outside React, import `track`, `trackElement` and `trackPageView` directly.
|
|
49
52
|
|
|
50
53
|
## API
|
|
51
54
|
|
|
@@ -64,11 +67,15 @@ later `init()` inside the 30-minute idle window resumes the *same* `session_id`.
|
|
|
64
67
|
|
|
65
68
|
| Export | Signature | Notes |
|
|
66
69
|
| --- | --- | --- |
|
|
67
|
-
| `track` | `(event: string, props?:
|
|
68
|
-
| `trackElement` |
|
|
70
|
+
| `track` | `(event: string, props?: object) => void` | Empty and [reserved](#events) names are ignored. |
|
|
71
|
+
| `trackElement` | `<T extends { element_id: number; segment_id?: number }>(props: T) => void` | Emits `element_click`. |
|
|
69
72
|
| `trackPageView` | `(path?: string) => void` | For routers the `history` patch cannot observe. Skips dedupe — always emits. |
|
|
70
73
|
| `useAnalytics` | `() => { track, trackElement, trackPageView }` | React hook; no provider needed. |
|
|
71
74
|
|
|
75
|
+
Props can be any object, including interface-typed ones, with values of any type. They are
|
|
76
|
+
serialized with `JSON.stringify` semantics: a `Date` becomes its ISO string, and `undefined` and
|
|
77
|
+
functions are dropped.
|
|
78
|
+
|
|
72
79
|
### Constants
|
|
73
80
|
|
|
74
81
|
| Export | Value | Notes |
|
|
@@ -93,7 +100,7 @@ Analytics Enabled]`) counts as *not set* and falls through to the option or defa
|
|
|
93
100
|
|
|
94
101
|
| Event | Fires when | `meta` |
|
|
95
102
|
| --- | --- | --- |
|
|
96
|
-
| `page_view` | Once on `init()`, then on every `pushState` / `replaceState` / `popstate`, or on demand via `trackPageView()`. | `referrer`, `query` — always both, always strings |
|
|
103
|
+
| `page_view` | Once on `init()`, then on every `pushState` / `replaceState` / `popstate` / `hashchange`, or on demand via `trackPageView()`. | `referrer`, `query` — always both, always strings |
|
|
97
104
|
| `heartbeat` | Tab visible: every 60s for 5 beats, then every 300s. | `{}` |
|
|
98
105
|
| `component_render` | The `mi-render` `CustomEvent` MI dispatches on `document`. | `component`, `element_id`, `segment_id`, plus each prop as `prop.<path>` |
|
|
99
106
|
| `element_click` | `trackElement()`. | `element_id`, `segment_id`, plus your own keys |
|
|
@@ -107,15 +114,22 @@ name that does not match is still tracked verbatim but logs one console warning,
|
|
|
107
114
|
dataset is append-only with no rename — `'Tile Click'`, `'tile_click '` and `'tileClick'` would
|
|
108
115
|
become three permanent, unmergeable series. Reserved-name matching is case-insensitive.
|
|
109
116
|
|
|
110
|
-
Two meta keys are reserved: `_truncated` is stamped only when meta
|
|
111
|
-
|
|
117
|
+
Two meta keys are reserved: `_truncated` is stamped only when meta exceeds 2000 characters or
|
|
118
|
+
cannot be serialized as a whole (for example a `BigInt` or a circular value), and the dashboard hides both `_truncated` and `capped` from its property view, so do
|
|
112
119
|
not use either as your own prop name.
|
|
113
120
|
|
|
114
|
-
**`page_view`** dedupes on path + search, so a router calling
|
|
115
|
-
sync does not emit. `meta.referrer` follows GA4 semantics: the
|
|
116
|
-
`document.referrer` normalized to origin + pathname; every later
|
|
117
|
-
previous view. `meta.query` is `location.search` without the
|
|
118
|
-
characters; either can be `''`.
|
|
121
|
+
**`page_view`** dedupes on path + search (+ hash route, see below), so a router calling
|
|
122
|
+
`replaceState` for scroll or filter sync does not emit. `meta.referrer` follows GA4 semantics: the
|
|
123
|
+
first view of the document uses `document.referrer` normalized to origin + pathname; every later
|
|
124
|
+
view uses the `page_path` of the previous view. `meta.query` is `location.search` without the
|
|
125
|
+
leading `?`. Both are capped at 300 characters; either can be `''`.
|
|
126
|
+
|
|
127
|
+
**Hash routing.** Hash-routed apps (for example React Router's `HashRouter`) are tracked without
|
|
128
|
+
extra setup. When `location.hash` starts with `#/`, the fragment is the route: `#/reports/42?tab=1`
|
|
129
|
+
records `page_path` `/reports/42` (it replaces any path below the portal page), and the fragment's
|
|
130
|
+
own query is appended to `meta.query` with `&` (`?embed=1#/reports?tab=1` → `embed=1&tab=1`). A
|
|
131
|
+
change of the hash route is a new view. A plain in-page anchor (`#section`, anything not starting
|
|
132
|
+
with `#/`) is not a route: it never changes `page_path` and never emits a `page_view`.
|
|
119
133
|
|
|
120
134
|
**`component_render`** reads `detail.component` plus `detail.props`. `MiNamespace.render()` is a
|
|
121
135
|
pure event emitter, so `document` sees every shared component render — no patching.
|
|
@@ -126,15 +140,18 @@ the spellings MI's own components use. No other key feeds `element_id`, which jo
|
|
|
126
140
|
|
|
127
141
|
Every other prop flattens to `prop.<dotted.path>` — flat, because the dashboard summarizes per
|
|
128
142
|
key and a nested object collapses to one opaque string. Scalars verbatim, strings capped at 40,
|
|
129
|
-
arrays as `arr:<length>`, 4 levels deep,
|
|
143
|
+
arrays as `arr:<length>`, 4 levels deep, 40 keys. When the depth or key cap drops a prop, the row
|
|
144
|
+
carries `props_dropped: 1`.
|
|
130
145
|
|
|
131
146
|
Props are stored verbatim and readable by anyone with dashboard access. Keep record-specific text
|
|
132
147
|
out of props you render with.
|
|
133
148
|
|
|
134
149
|
Capped at 50 emits per distinct component + element + segment + props; the capping emit carries
|
|
135
|
-
`capped: 1`. Past 200 buckets a
|
|
150
|
+
`capped: 1`. Past 200 distinct buckets per `init()`, a render that would open a new bucket goes into
|
|
151
|
+
one shared overflow bucket per component instead, which has the same cap of 50.
|
|
136
152
|
|
|
137
|
-
**`trackElement`** needs `element_id` to be
|
|
153
|
+
**`trackElement`** needs `element_id` to be a non-zero number within the `int` column's range
|
|
154
|
+
(−2³¹ … 2³¹−1; a fraction is truncated) — anything else drops the event
|
|
138
155
|
(one `console.warn` per runtime). `segment_id` is optional and defaults to `0` when omitted, with
|
|
139
156
|
no warning; a supplied non-finite value also records `0` but logs one `console.warn` per runtime.
|
|
140
157
|
Extra props pass through to `meta` untouched, and
|
|
@@ -156,7 +173,7 @@ stamps itself — the client never sends it.
|
|
|
156
173
|
| `app` | text | Portal page internal name, read from `location.pathname` against `/^\/(p[tl]?)\/([^/]+)(.*)$/`. Capped at 100. |
|
|
157
174
|
| `event` | text | Capped at 100. |
|
|
158
175
|
| `session_id` | text | New session after 30 minutes idle. Survives reloads via `localStorage`. |
|
|
159
|
-
| `page_path` | text | Route below the portal page, `/` at the root. Capped at 400. |
|
|
176
|
+
| `page_path` | text | Route below the portal page, `/` at the root; a `#/` hash route when present (see [Hash routing](#events)). Capped at 400. |
|
|
160
177
|
| `element_id` | int | `0` when not applicable. Integers only. |
|
|
161
178
|
| `version` | text | This package's version, capped at 20. |
|
|
162
179
|
| `meta` | text | JSON string, capped at 2000. |
|
|
@@ -168,13 +185,15 @@ Text columns are wide (10500) and the caps above are client-side payload budget,
|
|
|
168
185
|
inside `sendBeacon`'s 64 KiB quota — they are not schema limits.
|
|
169
186
|
|
|
170
187
|
`meta` over 2000 characters drops whole keys rather than slicing, so the stored value always
|
|
171
|
-
parses, and stamps `_truncated: 1`.
|
|
188
|
+
parses, and stamps `_truncated: 1`. Meta that `JSON.stringify` cannot serialize as a whole takes the
|
|
189
|
+
same path: keys that fail on their own are dropped, and `_truncated: 1` is stamped.
|
|
172
190
|
|
|
173
191
|
## Delivery
|
|
174
192
|
|
|
175
193
|
Rows buffer and flush on whichever comes first: 15 seconds, 50 rows, or 30 KB encoded. On
|
|
176
194
|
`pagehide`, and on `visibilitychange` to hidden (which is what actually fires on iOS), the buffer
|
|
177
|
-
goes out via `sendBeacon`, falling back to `fetch(…, { keepalive: true })
|
|
195
|
+
goes out via `sendBeacon`, falling back to `fetch(…, { keepalive: true })`, in chunks of at most
|
|
196
|
+
30 KB each.
|
|
178
197
|
|
|
179
198
|
A failed batch retries once after 2 seconds — network errors, 429 and 5xx are retryable; a `200`
|
|
180
199
|
carrying a non-zero `resultCode` is a rejected insert and is not. A failed batch is dropped, not
|
|
@@ -204,6 +223,15 @@ wrapped.
|
|
|
204
223
|
> (`int → float → text`). One float or string converts the column permanently.
|
|
205
224
|
> - **Never send `owner_user_id`.** The server stamps it and creates the column itself.
|
|
206
225
|
|
|
226
|
+
## Releases
|
|
227
|
+
|
|
228
|
+
Releases use the shared workflows from [mi-examples-workflows](https://github.com/mi-examples/mi-examples-workflows) ([release flow](https://github.com/mi-examples/mi-examples-workflows/blob/main/docs/workflows.md#release-workflows)). This repository has no `develop` branch, so there are no betas:
|
|
229
|
+
|
|
230
|
+
1. Run **Actions → Release → Run workflow**. It opens a release pull request `release/vX.Y.Z → main` with the version bump and the new `CHANGELOG.md` entry.
|
|
231
|
+
2. Review and edit the entry in the pull request, then merge it.
|
|
232
|
+
3. Merging publishes to npm under `latest` and creates the tag and the GitHub release.
|
|
233
|
+
|
|
234
|
+
Versions come from [Conventional Commits](https://www.conventionalcommits.org/) (`feat` → minor; `fix`, `perf`, `revert` → patch; `!` or `BREAKING CHANGE:` → major). Publishing uses npm Trusted Publishing (OIDC) from `.github/workflows/release.yml`, so don't rename that file.
|
|
207
235
|
## License
|
|
208
236
|
|
|
209
237
|
MIT © 2026 Metric Insights
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
34
34
|
|
|
35
35
|
// src/core/constants.ts
|
|
36
36
|
var ENTITY_NAME = "analytics_events";
|
|
37
|
-
var VERSION = "0.
|
|
37
|
+
var VERSION = "0.3.0";
|
|
38
38
|
var MI_PAGE_PATH_RE = /^\/(p[tl]?)\/([^/]+)(.*)$/;
|
|
39
39
|
var WIDTHS = {
|
|
40
40
|
app: 100,
|
|
@@ -66,12 +66,29 @@ function matchPage() {
|
|
|
66
66
|
if (typeof window === "undefined" || !window.location) return null;
|
|
67
67
|
return MI_PAGE_PATH_RE.exec(window.location.pathname);
|
|
68
68
|
}
|
|
69
|
+
function isHashRoute(hash2) {
|
|
70
|
+
return hash2.startsWith("#/");
|
|
71
|
+
}
|
|
72
|
+
function hashRoute() {
|
|
73
|
+
const hash2 = window.location.hash;
|
|
74
|
+
if (!isHashRoute(hash2)) return null;
|
|
75
|
+
const route = hash2.slice(1);
|
|
76
|
+
const pathEnd = route.search(/[?#]/);
|
|
77
|
+
const path = pathEnd === -1 ? route : route.slice(0, pathEnd);
|
|
78
|
+
const queryMatch = /\?([^#]*)/.exec(route);
|
|
79
|
+
return { path, query: queryMatch ? queryMatch[1] : "" };
|
|
80
|
+
}
|
|
69
81
|
function currentPagePath() {
|
|
70
82
|
const match = matchPage();
|
|
71
83
|
if (!match) return "/";
|
|
72
|
-
const rest = match[3];
|
|
84
|
+
const rest = hashRoute()?.path ?? match[3];
|
|
73
85
|
return (rest && rest !== "" && rest !== "/" ? rest : "/").slice(0, WIDTHS.page_path);
|
|
74
86
|
}
|
|
87
|
+
function currentQuery() {
|
|
88
|
+
const outer = window.location.search.replace(/^\?/, "");
|
|
89
|
+
const inner = hashRoute()?.query ?? "";
|
|
90
|
+
return outer && inner ? `${outer}&${inner}` : outer || inner;
|
|
91
|
+
}
|
|
75
92
|
function resolveConfig(options = {}) {
|
|
76
93
|
const enabled = options.enabled ?? readFlag("CUSTOM_APP_ANALYTICS_ENABLED") ?? true;
|
|
77
94
|
if (!enabled) return null;
|
|
@@ -82,9 +99,11 @@ function resolveConfig(options = {}) {
|
|
|
82
99
|
|
|
83
100
|
// src/core/event.ts
|
|
84
101
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
102
|
+
var INT32_MIN = -(2 ** 31);
|
|
103
|
+
var INT32_MAX = 2 ** 31 - 1;
|
|
85
104
|
function toId(value) {
|
|
86
|
-
const n = Number(value);
|
|
87
|
-
return Number.
|
|
105
|
+
const n = Math.trunc(Number(value));
|
|
106
|
+
return Number.isSafeInteger(n) && n >= INT32_MIN && n <= INT32_MAX ? n : 0;
|
|
88
107
|
}
|
|
89
108
|
function formatTs(date) {
|
|
90
109
|
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
|
|
@@ -179,12 +198,13 @@ function sendOnUnload(endpoint, rows) {
|
|
|
179
198
|
} catch {
|
|
180
199
|
}
|
|
181
200
|
try {
|
|
182
|
-
|
|
201
|
+
fetch(endpoint, {
|
|
183
202
|
method: "POST",
|
|
184
203
|
credentials: "same-origin",
|
|
185
204
|
headers: { "Content-Type": "application/json" },
|
|
186
205
|
body,
|
|
187
206
|
keepalive: true
|
|
207
|
+
}).catch(() => {
|
|
188
208
|
});
|
|
189
209
|
} catch {
|
|
190
210
|
}
|
|
@@ -196,6 +216,22 @@ var MAX_FLUSH_ROWS = 50;
|
|
|
196
216
|
var MAX_FLUSH_BYTES = 3e4;
|
|
197
217
|
var MAX_CONSECUTIVE_FAILURES = 3;
|
|
198
218
|
var textEncoder = new TextEncoder();
|
|
219
|
+
function chunkBySize(rows, maxBytes) {
|
|
220
|
+
const chunks = [];
|
|
221
|
+
let current = [];
|
|
222
|
+
let size = 2;
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
if (current.length > 0 && size + 1 + row.size > maxBytes) {
|
|
225
|
+
chunks.push(current);
|
|
226
|
+
current = [];
|
|
227
|
+
size = 2;
|
|
228
|
+
}
|
|
229
|
+
size += (current.length > 0 ? 1 : 0) + row.size;
|
|
230
|
+
current.push(row);
|
|
231
|
+
}
|
|
232
|
+
if (current.length > 0) chunks.push(current);
|
|
233
|
+
return chunks;
|
|
234
|
+
}
|
|
199
235
|
function createQueue(endpoint, onKilled) {
|
|
200
236
|
let buffer = [];
|
|
201
237
|
let bytes = 0;
|
|
@@ -218,8 +254,8 @@ function createQueue(endpoint, onKilled) {
|
|
|
218
254
|
buffer = [];
|
|
219
255
|
bytes = 0;
|
|
220
256
|
inFlight = true;
|
|
257
|
+
let outcome = "failed";
|
|
221
258
|
try {
|
|
222
|
-
let outcome;
|
|
223
259
|
try {
|
|
224
260
|
outcome = await send(
|
|
225
261
|
endpoint,
|
|
@@ -228,6 +264,7 @@ function createQueue(endpoint, onKilled) {
|
|
|
228
264
|
} catch {
|
|
229
265
|
outcome = "failed";
|
|
230
266
|
}
|
|
267
|
+
if (stopped) return;
|
|
231
268
|
if (outcome === "ok") {
|
|
232
269
|
failures = 0;
|
|
233
270
|
return;
|
|
@@ -243,6 +280,7 @@ function createQueue(endpoint, onKilled) {
|
|
|
243
280
|
}
|
|
244
281
|
} finally {
|
|
245
282
|
inFlight = false;
|
|
283
|
+
if (!stopped && outcome === "ok" && (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES)) void flush();
|
|
246
284
|
}
|
|
247
285
|
}
|
|
248
286
|
return {
|
|
@@ -267,10 +305,12 @@ function createQueue(endpoint, onKilled) {
|
|
|
267
305
|
const batch = buffer;
|
|
268
306
|
buffer = [];
|
|
269
307
|
bytes = 0;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
308
|
+
for (const chunk of chunkBySize(batch, MAX_FLUSH_BYTES)) {
|
|
309
|
+
sendOnUnload(
|
|
310
|
+
endpoint,
|
|
311
|
+
chunk.map((b) => b.json)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
274
314
|
},
|
|
275
315
|
stop
|
|
276
316
|
};
|
|
@@ -486,7 +526,8 @@ function startPageView(emit, currentPath) {
|
|
|
486
526
|
let lastLocation = null;
|
|
487
527
|
const locationKey = (path) => {
|
|
488
528
|
try {
|
|
489
|
-
|
|
529
|
+
const { search, hash: hash2 } = window.location;
|
|
530
|
+
return `${path}${search}${isHashRoute(hash2) ? hash2 : ""}`;
|
|
490
531
|
} catch {
|
|
491
532
|
return path;
|
|
492
533
|
}
|
|
@@ -517,6 +558,7 @@ function startPageView(emit, currentPath) {
|
|
|
517
558
|
window.history.replaceState = patchedReplace;
|
|
518
559
|
const onPopState = () => safeEmit(currentPath, true);
|
|
519
560
|
window.addEventListener("popstate", onPopState);
|
|
561
|
+
window.addEventListener("hashchange", onPopState);
|
|
520
562
|
safeEmit(currentPath, true);
|
|
521
563
|
return {
|
|
522
564
|
stop() {
|
|
@@ -530,6 +572,7 @@ function startPageView(emit, currentPath) {
|
|
|
530
572
|
window.history.replaceState = originalReplace;
|
|
531
573
|
}
|
|
532
574
|
window.removeEventListener("popstate", onPopState);
|
|
575
|
+
window.removeEventListener("hashchange", onPopState);
|
|
533
576
|
},
|
|
534
577
|
emitNow(path) {
|
|
535
578
|
safeEmit(() => path ?? currentPath(), false);
|
|
@@ -552,7 +595,7 @@ function warnOnce(key, message) {
|
|
|
552
595
|
console.warn(`[ca-analytics] ${message}`);
|
|
553
596
|
}
|
|
554
597
|
function pageViewMeta(referrer) {
|
|
555
|
-
const query =
|
|
598
|
+
const query = currentQuery();
|
|
556
599
|
return { referrer: referrer.slice(0, LIMITS.referrer), query: query.slice(0, LIMITS.query) };
|
|
557
600
|
}
|
|
558
601
|
function stopStarted(parts, flushFirst) {
|
|
@@ -576,7 +619,9 @@ function init(options = {}) {
|
|
|
576
619
|
if (runtime) return;
|
|
577
620
|
const config = resolveConfig(options);
|
|
578
621
|
if (!config) return;
|
|
579
|
-
const queue = createQueue(config.endpoint, () =>
|
|
622
|
+
const queue = createQueue(config.endpoint, () => {
|
|
623
|
+
if (runtime?.queue === queue) shutdown();
|
|
624
|
+
});
|
|
580
625
|
const emit = (event, extra = {}) => {
|
|
581
626
|
queue.push(
|
|
582
627
|
createRow({
|
|
@@ -639,7 +684,7 @@ function trackElement(props) {
|
|
|
639
684
|
const { element_id, segment_id, ...extra } = props ?? {};
|
|
640
685
|
const id = toId(element_id);
|
|
641
686
|
if (!id) {
|
|
642
|
-
warnOnce("element", `trackElement() ignored a non-finite or
|
|
687
|
+
warnOnce("element", `trackElement() ignored a zero, non-finite or out-of-range element_id (${element_id})`);
|
|
643
688
|
return;
|
|
644
689
|
}
|
|
645
690
|
const segmentRaw = Number(segment_id);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../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":["export { init, isActive, shutdown, track, trackElement, trackPageView } from './core/analytics';\nexport { ENTITY_NAME, VERSION } from './core/constants';\nexport type { EventRow, JsonValue, Options } from './core/types';\nexport { useAnalytics } from './react/useAnalytics';\n","/** 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 // Page internal_name: both the row's attribution key and the write endpoint's page.\n const 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/** Distinct cap buckets per page load. Past this every render of a component shares one bucket, so per-render props can't leak the map. */\nconst MAX_KEYS = 200;\n/** `visibility.preview.control_buttons.lineage` is 4 levels, the deepest shape MI documents. */\nconst PROP_DEPTH = 4;\n/** A `viewer` render is 38 leaves / 1672 chars, so this must not bind before serializeMeta's own cap does. */\nconst PROP_KEYS = 40;\nconst PROP_STRING = 40;\n\n/** Already read into element_id/segment_id above; re-emitting them as props is noise. */\nconst IDENTITY_KEYS = new Set(['element', 'element_id', 'segment', 'segment_id', 'segment_value_id']);\n\nconst SKIP = Symbol('skip');\n\nexport interface RenderPayload {\n elementId: number;\n meta: Record<string, unknown>;\n}\n\nfunction leafValue(value: unknown): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case 'boolean':\n return value;\n case 'number':\n return Number.isFinite(value) ? value : SKIP;\n case 'string':\n return value.slice(0, PROP_STRING);\n default:\n return SKIP;\n }\n}\n\n/** Dotted paths, flat output: the dashboard stringifies a nested object into one opaque value, so its per-key distribution would be noise. Returns true when a cap dropped something. */\nfunction flattenProps(source: Record<string, unknown>, out: Record<string, unknown>, prefix: string, depth: number): boolean {\n let dropped = false;\n\n for (const [key, value] of Object.entries(source)) {\n if (Object.keys(out).length >= PROP_KEYS) return true;\n if (depth === 0 && IDENTITY_KEYS.has(key)) continue;\n\n const path = prefix === '' ? key : `${prefix}.${key}`;\n\n // Summarized, not expanded — index-keyed paths would make every array length its own set of keys.\n if (Array.isArray(value)) {\n out[`prop.${path}`] = `arr:${value.length}`;\n continue;\n }\n\n if (value !== null && typeof value === 'object') {\n if (depth + 1 < PROP_DEPTH) dropped = flattenProps(value as Record<string, unknown>, out, path, depth + 1) || dropped;\n else dropped = true;\n\n continue;\n }\n\n const leaf = leafValue(value);\n\n if (leaf !== SKIP) out[`prop.${path}`] = leaf;\n }\n\n return dropped;\n}\n\n/** FNV-1a. Keeps the bucket key short no matter how large the props are. */\nfunction hash(text: string): string {\n let h = 0x811c9dc5;\n\n for (let i = 0; i < text.length; i += 1) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n\n return (h >>> 0).toString(36);\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 // Three spellings in the wild: preview sends {element, segment}, viewer {element_id, segment_id}, access_denied_popup {element_id, segment_value_id}.\n const elementId = toId(props.element ?? props.element_id ?? 0);\n const segmentId = toId(props.segment ?? props.segment_id ?? props.segment_value_id ?? 0);\n\n const flat: Record<string, unknown> = {};\n const propsDropped = flattenProps(props, flat, '', 0);\n\n // Props are in the key: a component with no element (a popup) is otherwise one bucket for every variant it renders.\n const bucket = `${component}|${elementId}|${segmentId}|${hash(JSON.stringify(flat))}`;\n const key = counts.has(bucket) || counts.size < MAX_KEYS ? bucket : `${component}|*`;\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 // Reserved keys first: serializeMeta drops trailing keys at the 2000-char cap, so props go before identity does.\n const meta: Record<string, unknown> = {\n component,\n element_id: elementId,\n segment_id: segmentId,\n ...flat,\n };\n\n if (propsDropped) meta.props_dropped = 1;\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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../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":["export { init, isActive, shutdown, track, trackElement, trackPageView } from './core/analytics';\nexport { ENTITY_NAME, VERSION } from './core/constants';\nexport type { EventRow, JsonValue, Options } from './core/types';\nexport { useAnalytics } from './react/useAnalytics';\n","/** 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/** A hash router's fragment (`#/reports`). A plain in-page anchor (`#section`) is not a route. */\nexport function isHashRoute(hash: string): boolean {\n return hash.startsWith('#/');\n}\n\n/** Path and query a hash router keeps in the fragment: `#/reports?tab=1` → `/reports`, `tab=1`. Null without one. */\nfunction hashRoute(): { path: string; query: string } | null {\n const hash = window.location.hash;\n\n if (!isHashRoute(hash)) return null;\n\n const route = hash.slice(1);\n const pathEnd = route.search(/[?#]/);\n const path = pathEnd === -1 ? route : route.slice(0, pathEnd);\n const queryMatch = /\\?([^#]*)/.exec(route);\n\n return { path, query: queryMatch ? queryMatch[1] : '' };\n}\n\n/** Route below the portal page. '/' at the page root. A hash route, when present, is the route. */\nexport function currentPagePath(): string {\n const match = matchPage();\n\n if (!match) return '/';\n\n const rest = hashRoute()?.path ?? match[3];\n\n return (rest && rest !== '' && rest !== '/' ? rest : '/').slice(0, WIDTHS.page_path);\n}\n\n/** `location.search` without the `?`, joined with `&` to a hash route's own query. */\nexport function currentQuery(): string {\n const outer = window.location.search.replace(/^\\?/, '');\n const inner = hashRoute()?.query ?? '';\n\n return outer && inner ? `${outer}&${inner}` : outer || inner;\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 // Page internal_name: both the row's attribution key and the write endpoint's page.\n const 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\nconst INT32_MIN = -(2 ** 31);\nconst INT32_MAX = 2 ** 31 - 1;\n\n/**\n * Integer only: a float or string value widens the `int` column permanently. Out-of-int32 values are 0,\n * since past 2^31-1 the column overflows and past 1e21 the number serializes as `1e+21`.\n */\nexport function toId(value: unknown): number {\n const n = Math.trunc(Number(value));\n\n return Number.isSafeInteger(n) && n >= INT32_MIN && n <= INT32_MAX ? 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/catch only sees a synchronous throw; the .catch keeps a rejected request (e.g. over the\n // keepalive quota) from surfacing as an unhandled rejection in the host app.\n try {\n fetch(endpoint, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true,\n }).catch(() => {\n // Dropped. Nothing further possible at unload.\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\n/** Splits rows into batches whose `[a,b,c]` body stays within maxBytes. A lone row over the limit still goes, alone. */\nfunction chunkBySize(rows: Buffered[], maxBytes: number): Buffered[][] {\n const chunks: Buffered[][] = [];\n let current: Buffered[] = [];\n let size = 2;\n\n for (const row of rows) {\n if (current.length > 0 && size + 1 + row.size > maxBytes) {\n chunks.push(current);\n current = [];\n size = 2;\n }\n\n size += (current.length > 0 ? 1 : 0) + row.size;\n current.push(row);\n }\n\n if (current.length > 0) chunks.push(current);\n\n return chunks;\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 let outcome: SendOutcome = 'failed';\n\n try {\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 // Stopped while the request was out: a stale result must not count toward, or trip, the kill-switch.\n if (stopped) return;\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 // push() only flushes on its own threshold check, which is a no-op while a request is out; rows\n // that piled up meanwhile would otherwise wait for the timer and could outgrow the beacon quota.\n // Only after a success: re-flushing a failure straight away would spend the kill-switch's three\n // strikes within seconds of a brief outage, instead of across timer ticks.\n if (!stopped && outcome === 'ok' && (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES)) void flush();\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\n // One body over the 64 KiB quota fails in both sendBeacon and keepalive fetch and drops every row;\n // flush-sized chunks let at least the ones that fit through.\n for (const chunk of chunkBySize(batch, MAX_FLUSH_BYTES)) {\n sendOnUnload(\n endpoint,\n chunk.map((b) => b.json),\n );\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/** Distinct cap buckets per page load. Past this every render of a component shares one bucket, so per-render props can't leak the map. */\nconst MAX_KEYS = 200;\n/** `visibility.preview.control_buttons.lineage` is 4 levels, the deepest shape MI documents. */\nconst PROP_DEPTH = 4;\n/** A `viewer` render is 38 leaves / 1672 chars, so this must not bind before serializeMeta's own cap does. */\nconst PROP_KEYS = 40;\nconst PROP_STRING = 40;\n\n/** Already read into element_id/segment_id above; re-emitting them as props is noise. */\nconst IDENTITY_KEYS = new Set(['element', 'element_id', 'segment', 'segment_id', 'segment_value_id']);\n\nconst SKIP = Symbol('skip');\n\nexport interface RenderPayload {\n elementId: number;\n meta: Record<string, unknown>;\n}\n\nfunction leafValue(value: unknown): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case 'boolean':\n return value;\n case 'number':\n return Number.isFinite(value) ? value : SKIP;\n case 'string':\n return value.slice(0, PROP_STRING);\n default:\n return SKIP;\n }\n}\n\n/** Dotted paths, flat output: the dashboard stringifies a nested object into one opaque value, so its per-key distribution would be noise. Returns true when a cap dropped something. */\nfunction flattenProps(source: Record<string, unknown>, out: Record<string, unknown>, prefix: string, depth: number): boolean {\n let dropped = false;\n\n for (const [key, value] of Object.entries(source)) {\n if (Object.keys(out).length >= PROP_KEYS) return true;\n if (depth === 0 && IDENTITY_KEYS.has(key)) continue;\n\n const path = prefix === '' ? key : `${prefix}.${key}`;\n\n // Summarized, not expanded — index-keyed paths would make every array length its own set of keys.\n if (Array.isArray(value)) {\n out[`prop.${path}`] = `arr:${value.length}`;\n continue;\n }\n\n if (value !== null && typeof value === 'object') {\n if (depth + 1 < PROP_DEPTH) dropped = flattenProps(value as Record<string, unknown>, out, path, depth + 1) || dropped;\n else dropped = true;\n\n continue;\n }\n\n const leaf = leafValue(value);\n\n if (leaf !== SKIP) out[`prop.${path}`] = leaf;\n }\n\n return dropped;\n}\n\n/** FNV-1a. Keeps the bucket key short no matter how large the props are. */\nfunction hash(text: string): string {\n let h = 0x811c9dc5;\n\n for (let i = 0; i < text.length; i += 1) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n\n return (h >>> 0).toString(36);\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 // Three spellings in the wild: preview sends {element, segment}, viewer {element_id, segment_id}, access_denied_popup {element_id, segment_value_id}.\n const elementId = toId(props.element ?? props.element_id ?? 0);\n const segmentId = toId(props.segment ?? props.segment_id ?? props.segment_value_id ?? 0);\n\n const flat: Record<string, unknown> = {};\n const propsDropped = flattenProps(props, flat, '', 0);\n\n // Props are in the key: a component with no element (a popup) is otherwise one bucket for every variant it renders.\n const bucket = `${component}|${elementId}|${segmentId}|${hash(JSON.stringify(flat))}`;\n const key = counts.has(bucket) || counts.size < MAX_KEYS ? bucket : `${component}|*`;\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 // Reserved keys first: serializeMeta drops trailing keys at the 2000-char cap, so props go before identity does.\n const meta: Record<string, unknown> = {\n component,\n element_id: elementId,\n segment_id: segmentId,\n ...flat,\n };\n\n if (propsDropped) meta.props_dropped = 1;\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","import { isHashRoute } from '../config';\n\nexport 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 or hash 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. A hash route\n // (`#/reports?tab=1`) is part of it; a plain in-page anchor (`#section`) is not a new view.\n const locationKey = (path: string): string => {\n try {\n const { search, hash } = window.location;\n\n return `${path}${search}${isHashRoute(hash) ? hash : ''}`;\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 // Hash routers and `location.hash = …` navigate without pushState; dedupe drops the popstate twin.\n window.addEventListener('hashchange', 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 window.removeEventListener('hashchange', 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, currentQuery, 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 { 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 = currentQuery();\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 — but only while this\n // queue's runtime is still the live one, so a stale queue can't kill a later init().\n const queue: Queue = createQueue(config.endpoint, () => {\n if (runtime?.queue === queue) shutdown();\n });\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\n/** `props` is any object, interface-typed included; it is serialized like JSON.stringify (a Date becomes its ISO string). */\nexport function track(event: string, props?: object): 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 as Record<string, unknown> | undefined });\n });\n}\n\n/**\n * `element_id`/`segment_id` resolve identity via `/api/element_info`; everything else is free-form. Generic so\n * interface-typed props with extra keys are accepted — an index signature would reject them.\n */\nexport function trackElement<T extends { element_id: number; segment_id?: number }>(props: T): 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 zero, non-finite or out-of-range 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":["hash","hash"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** No longer used by the tracking signatures, which accept any value; still exported so existing imports keep compiling. */
|
|
1
2
|
type JsonValue = string | number | boolean | null | undefined | JsonValue[] | {
|
|
2
3
|
[key: string]: JsonValue;
|
|
3
4
|
};
|
|
@@ -20,13 +21,16 @@ interface EventRow {
|
|
|
20
21
|
|
|
21
22
|
/** Idempotent. A second call while active is ignored rather than doubling listeners. */
|
|
22
23
|
declare function init(options?: Options): void;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** `props` is any object, interface-typed included; it is serialized like JSON.stringify (a Date becomes its ISO string). */
|
|
25
|
+
declare function track(event: string, props?: object): void;
|
|
26
|
+
/**
|
|
27
|
+
* `element_id`/`segment_id` resolve identity via `/api/element_info`; everything else is free-form. Generic so
|
|
28
|
+
* interface-typed props with extra keys are accepted — an index signature would reject them.
|
|
29
|
+
*/
|
|
30
|
+
declare function trackElement<T extends {
|
|
26
31
|
element_id: number;
|
|
27
32
|
segment_id?: number;
|
|
28
|
-
|
|
29
|
-
}): void;
|
|
33
|
+
}>(props: T): void;
|
|
30
34
|
/** For routers whose navigation the history patch cannot observe. */
|
|
31
35
|
declare function trackPageView(path?: string): void;
|
|
32
36
|
/** Flushes what is buffered before stopping — up to 15s of events would otherwise drop. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** No longer used by the tracking signatures, which accept any value; still exported so existing imports keep compiling. */
|
|
1
2
|
type JsonValue = string | number | boolean | null | undefined | JsonValue[] | {
|
|
2
3
|
[key: string]: JsonValue;
|
|
3
4
|
};
|
|
@@ -20,13 +21,16 @@ interface EventRow {
|
|
|
20
21
|
|
|
21
22
|
/** Idempotent. A second call while active is ignored rather than doubling listeners. */
|
|
22
23
|
declare function init(options?: Options): void;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/** `props` is any object, interface-typed included; it is serialized like JSON.stringify (a Date becomes its ISO string). */
|
|
25
|
+
declare function track(event: string, props?: object): void;
|
|
26
|
+
/**
|
|
27
|
+
* `element_id`/`segment_id` resolve identity via `/api/element_info`; everything else is free-form. Generic so
|
|
28
|
+
* interface-typed props with extra keys are accepted — an index signature would reject them.
|
|
29
|
+
*/
|
|
30
|
+
declare function trackElement<T extends {
|
|
26
31
|
element_id: number;
|
|
27
32
|
segment_id?: number;
|
|
28
|
-
|
|
29
|
-
}): void;
|
|
33
|
+
}>(props: T): void;
|
|
30
34
|
/** For routers whose navigation the history patch cannot observe. */
|
|
31
35
|
declare function trackPageView(path?: string): void;
|
|
32
36
|
/** Flushes what is buffered before stopping — up to 15s of events would otherwise drop. */
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/core/constants.ts
|
|
2
2
|
var ENTITY_NAME = "analytics_events";
|
|
3
|
-
var VERSION = "0.
|
|
3
|
+
var VERSION = "0.3.0";
|
|
4
4
|
var MI_PAGE_PATH_RE = /^\/(p[tl]?)\/([^/]+)(.*)$/;
|
|
5
5
|
var WIDTHS = {
|
|
6
6
|
app: 100,
|
|
@@ -32,12 +32,29 @@ function matchPage() {
|
|
|
32
32
|
if (typeof window === "undefined" || !window.location) return null;
|
|
33
33
|
return MI_PAGE_PATH_RE.exec(window.location.pathname);
|
|
34
34
|
}
|
|
35
|
+
function isHashRoute(hash2) {
|
|
36
|
+
return hash2.startsWith("#/");
|
|
37
|
+
}
|
|
38
|
+
function hashRoute() {
|
|
39
|
+
const hash2 = window.location.hash;
|
|
40
|
+
if (!isHashRoute(hash2)) return null;
|
|
41
|
+
const route = hash2.slice(1);
|
|
42
|
+
const pathEnd = route.search(/[?#]/);
|
|
43
|
+
const path = pathEnd === -1 ? route : route.slice(0, pathEnd);
|
|
44
|
+
const queryMatch = /\?([^#]*)/.exec(route);
|
|
45
|
+
return { path, query: queryMatch ? queryMatch[1] : "" };
|
|
46
|
+
}
|
|
35
47
|
function currentPagePath() {
|
|
36
48
|
const match = matchPage();
|
|
37
49
|
if (!match) return "/";
|
|
38
|
-
const rest = match[3];
|
|
50
|
+
const rest = hashRoute()?.path ?? match[3];
|
|
39
51
|
return (rest && rest !== "" && rest !== "/" ? rest : "/").slice(0, WIDTHS.page_path);
|
|
40
52
|
}
|
|
53
|
+
function currentQuery() {
|
|
54
|
+
const outer = window.location.search.replace(/^\?/, "");
|
|
55
|
+
const inner = hashRoute()?.query ?? "";
|
|
56
|
+
return outer && inner ? `${outer}&${inner}` : outer || inner;
|
|
57
|
+
}
|
|
41
58
|
function resolveConfig(options = {}) {
|
|
42
59
|
const enabled = options.enabled ?? readFlag("CUSTOM_APP_ANALYTICS_ENABLED") ?? true;
|
|
43
60
|
if (!enabled) return null;
|
|
@@ -48,9 +65,11 @@ function resolveConfig(options = {}) {
|
|
|
48
65
|
|
|
49
66
|
// src/core/event.ts
|
|
50
67
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
68
|
+
var INT32_MIN = -(2 ** 31);
|
|
69
|
+
var INT32_MAX = 2 ** 31 - 1;
|
|
51
70
|
function toId(value) {
|
|
52
|
-
const n = Number(value);
|
|
53
|
-
return Number.
|
|
71
|
+
const n = Math.trunc(Number(value));
|
|
72
|
+
return Number.isSafeInteger(n) && n >= INT32_MIN && n <= INT32_MAX ? n : 0;
|
|
54
73
|
}
|
|
55
74
|
function formatTs(date) {
|
|
56
75
|
return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}:${pad2(date.getUTCSeconds())}`;
|
|
@@ -145,12 +164,13 @@ function sendOnUnload(endpoint, rows) {
|
|
|
145
164
|
} catch {
|
|
146
165
|
}
|
|
147
166
|
try {
|
|
148
|
-
|
|
167
|
+
fetch(endpoint, {
|
|
149
168
|
method: "POST",
|
|
150
169
|
credentials: "same-origin",
|
|
151
170
|
headers: { "Content-Type": "application/json" },
|
|
152
171
|
body,
|
|
153
172
|
keepalive: true
|
|
173
|
+
}).catch(() => {
|
|
154
174
|
});
|
|
155
175
|
} catch {
|
|
156
176
|
}
|
|
@@ -162,6 +182,22 @@ var MAX_FLUSH_ROWS = 50;
|
|
|
162
182
|
var MAX_FLUSH_BYTES = 3e4;
|
|
163
183
|
var MAX_CONSECUTIVE_FAILURES = 3;
|
|
164
184
|
var textEncoder = new TextEncoder();
|
|
185
|
+
function chunkBySize(rows, maxBytes) {
|
|
186
|
+
const chunks = [];
|
|
187
|
+
let current = [];
|
|
188
|
+
let size = 2;
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
if (current.length > 0 && size + 1 + row.size > maxBytes) {
|
|
191
|
+
chunks.push(current);
|
|
192
|
+
current = [];
|
|
193
|
+
size = 2;
|
|
194
|
+
}
|
|
195
|
+
size += (current.length > 0 ? 1 : 0) + row.size;
|
|
196
|
+
current.push(row);
|
|
197
|
+
}
|
|
198
|
+
if (current.length > 0) chunks.push(current);
|
|
199
|
+
return chunks;
|
|
200
|
+
}
|
|
165
201
|
function createQueue(endpoint, onKilled) {
|
|
166
202
|
let buffer = [];
|
|
167
203
|
let bytes = 0;
|
|
@@ -184,8 +220,8 @@ function createQueue(endpoint, onKilled) {
|
|
|
184
220
|
buffer = [];
|
|
185
221
|
bytes = 0;
|
|
186
222
|
inFlight = true;
|
|
223
|
+
let outcome = "failed";
|
|
187
224
|
try {
|
|
188
|
-
let outcome;
|
|
189
225
|
try {
|
|
190
226
|
outcome = await send(
|
|
191
227
|
endpoint,
|
|
@@ -194,6 +230,7 @@ function createQueue(endpoint, onKilled) {
|
|
|
194
230
|
} catch {
|
|
195
231
|
outcome = "failed";
|
|
196
232
|
}
|
|
233
|
+
if (stopped) return;
|
|
197
234
|
if (outcome === "ok") {
|
|
198
235
|
failures = 0;
|
|
199
236
|
return;
|
|
@@ -209,6 +246,7 @@ function createQueue(endpoint, onKilled) {
|
|
|
209
246
|
}
|
|
210
247
|
} finally {
|
|
211
248
|
inFlight = false;
|
|
249
|
+
if (!stopped && outcome === "ok" && (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES)) void flush();
|
|
212
250
|
}
|
|
213
251
|
}
|
|
214
252
|
return {
|
|
@@ -233,10 +271,12 @@ function createQueue(endpoint, onKilled) {
|
|
|
233
271
|
const batch = buffer;
|
|
234
272
|
buffer = [];
|
|
235
273
|
bytes = 0;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
274
|
+
for (const chunk of chunkBySize(batch, MAX_FLUSH_BYTES)) {
|
|
275
|
+
sendOnUnload(
|
|
276
|
+
endpoint,
|
|
277
|
+
chunk.map((b) => b.json)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
240
280
|
},
|
|
241
281
|
stop
|
|
242
282
|
};
|
|
@@ -452,7 +492,8 @@ function startPageView(emit, currentPath) {
|
|
|
452
492
|
let lastLocation = null;
|
|
453
493
|
const locationKey = (path) => {
|
|
454
494
|
try {
|
|
455
|
-
|
|
495
|
+
const { search, hash: hash2 } = window.location;
|
|
496
|
+
return `${path}${search}${isHashRoute(hash2) ? hash2 : ""}`;
|
|
456
497
|
} catch {
|
|
457
498
|
return path;
|
|
458
499
|
}
|
|
@@ -483,6 +524,7 @@ function startPageView(emit, currentPath) {
|
|
|
483
524
|
window.history.replaceState = patchedReplace;
|
|
484
525
|
const onPopState = () => safeEmit(currentPath, true);
|
|
485
526
|
window.addEventListener("popstate", onPopState);
|
|
527
|
+
window.addEventListener("hashchange", onPopState);
|
|
486
528
|
safeEmit(currentPath, true);
|
|
487
529
|
return {
|
|
488
530
|
stop() {
|
|
@@ -496,6 +538,7 @@ function startPageView(emit, currentPath) {
|
|
|
496
538
|
window.history.replaceState = originalReplace;
|
|
497
539
|
}
|
|
498
540
|
window.removeEventListener("popstate", onPopState);
|
|
541
|
+
window.removeEventListener("hashchange", onPopState);
|
|
499
542
|
},
|
|
500
543
|
emitNow(path) {
|
|
501
544
|
safeEmit(() => path ?? currentPath(), false);
|
|
@@ -518,7 +561,7 @@ function warnOnce(key, message) {
|
|
|
518
561
|
console.warn(`[ca-analytics] ${message}`);
|
|
519
562
|
}
|
|
520
563
|
function pageViewMeta(referrer) {
|
|
521
|
-
const query =
|
|
564
|
+
const query = currentQuery();
|
|
522
565
|
return { referrer: referrer.slice(0, LIMITS.referrer), query: query.slice(0, LIMITS.query) };
|
|
523
566
|
}
|
|
524
567
|
function stopStarted(parts, flushFirst) {
|
|
@@ -542,7 +585,9 @@ function init(options = {}) {
|
|
|
542
585
|
if (runtime) return;
|
|
543
586
|
const config = resolveConfig(options);
|
|
544
587
|
if (!config) return;
|
|
545
|
-
const queue = createQueue(config.endpoint, () =>
|
|
588
|
+
const queue = createQueue(config.endpoint, () => {
|
|
589
|
+
if (runtime?.queue === queue) shutdown();
|
|
590
|
+
});
|
|
546
591
|
const emit = (event, extra = {}) => {
|
|
547
592
|
queue.push(
|
|
548
593
|
createRow({
|
|
@@ -605,7 +650,7 @@ function trackElement(props) {
|
|
|
605
650
|
const { element_id, segment_id, ...extra } = props ?? {};
|
|
606
651
|
const id = toId(element_id);
|
|
607
652
|
if (!id) {
|
|
608
|
-
warnOnce("element", `trackElement() ignored a non-finite or
|
|
653
|
+
warnOnce("element", `trackElement() ignored a zero, non-finite or out-of-range element_id (${element_id})`);
|
|
609
654
|
return;
|
|
610
655
|
}
|
|
611
656
|
const segmentRaw = Number(segment_id);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 // Page internal_name: both the row's attribution key and the write endpoint's page.\n const 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/** Distinct cap buckets per page load. Past this every render of a component shares one bucket, so per-render props can't leak the map. */\nconst MAX_KEYS = 200;\n/** `visibility.preview.control_buttons.lineage` is 4 levels, the deepest shape MI documents. */\nconst PROP_DEPTH = 4;\n/** A `viewer` render is 38 leaves / 1672 chars, so this must not bind before serializeMeta's own cap does. */\nconst PROP_KEYS = 40;\nconst PROP_STRING = 40;\n\n/** Already read into element_id/segment_id above; re-emitting them as props is noise. */\nconst IDENTITY_KEYS = new Set(['element', 'element_id', 'segment', 'segment_id', 'segment_value_id']);\n\nconst SKIP = Symbol('skip');\n\nexport interface RenderPayload {\n elementId: number;\n meta: Record<string, unknown>;\n}\n\nfunction leafValue(value: unknown): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case 'boolean':\n return value;\n case 'number':\n return Number.isFinite(value) ? value : SKIP;\n case 'string':\n return value.slice(0, PROP_STRING);\n default:\n return SKIP;\n }\n}\n\n/** Dotted paths, flat output: the dashboard stringifies a nested object into one opaque value, so its per-key distribution would be noise. Returns true when a cap dropped something. */\nfunction flattenProps(source: Record<string, unknown>, out: Record<string, unknown>, prefix: string, depth: number): boolean {\n let dropped = false;\n\n for (const [key, value] of Object.entries(source)) {\n if (Object.keys(out).length >= PROP_KEYS) return true;\n if (depth === 0 && IDENTITY_KEYS.has(key)) continue;\n\n const path = prefix === '' ? key : `${prefix}.${key}`;\n\n // Summarized, not expanded — index-keyed paths would make every array length its own set of keys.\n if (Array.isArray(value)) {\n out[`prop.${path}`] = `arr:${value.length}`;\n continue;\n }\n\n if (value !== null && typeof value === 'object') {\n if (depth + 1 < PROP_DEPTH) dropped = flattenProps(value as Record<string, unknown>, out, path, depth + 1) || dropped;\n else dropped = true;\n\n continue;\n }\n\n const leaf = leafValue(value);\n\n if (leaf !== SKIP) out[`prop.${path}`] = leaf;\n }\n\n return dropped;\n}\n\n/** FNV-1a. Keeps the bucket key short no matter how large the props are. */\nfunction hash(text: string): string {\n let h = 0x811c9dc5;\n\n for (let i = 0; i < text.length; i += 1) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n\n return (h >>> 0).toString(36);\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 // Three spellings in the wild: preview sends {element, segment}, viewer {element_id, segment_id}, access_denied_popup {element_id, segment_value_id}.\n const elementId = toId(props.element ?? props.element_id ?? 0);\n const segmentId = toId(props.segment ?? props.segment_id ?? props.segment_value_id ?? 0);\n\n const flat: Record<string, unknown> = {};\n const propsDropped = flattenProps(props, flat, '', 0);\n\n // Props are in the key: a component with no element (a popup) is otherwise one bucket for every variant it renders.\n const bucket = `${component}|${elementId}|${segmentId}|${hash(JSON.stringify(flat))}`;\n const key = counts.has(bucket) || counts.size < MAX_KEYS ? bucket : `${component}|*`;\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 // Reserved keys first: serializeMeta drops trailing keys at the 2000-char cap, so props go before identity does.\n const meta: Record<string, unknown> = {\n component,\n element_id: elementId,\n segment_id: segmentId,\n ...flat,\n };\n\n if (propsDropped) meta.props_dropped = 1;\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":[]}
|
|
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/** A hash router's fragment (`#/reports`). A plain in-page anchor (`#section`) is not a route. */\nexport function isHashRoute(hash: string): boolean {\n return hash.startsWith('#/');\n}\n\n/** Path and query a hash router keeps in the fragment: `#/reports?tab=1` → `/reports`, `tab=1`. Null without one. */\nfunction hashRoute(): { path: string; query: string } | null {\n const hash = window.location.hash;\n\n if (!isHashRoute(hash)) return null;\n\n const route = hash.slice(1);\n const pathEnd = route.search(/[?#]/);\n const path = pathEnd === -1 ? route : route.slice(0, pathEnd);\n const queryMatch = /\\?([^#]*)/.exec(route);\n\n return { path, query: queryMatch ? queryMatch[1] : '' };\n}\n\n/** Route below the portal page. '/' at the page root. A hash route, when present, is the route. */\nexport function currentPagePath(): string {\n const match = matchPage();\n\n if (!match) return '/';\n\n const rest = hashRoute()?.path ?? match[3];\n\n return (rest && rest !== '' && rest !== '/' ? rest : '/').slice(0, WIDTHS.page_path);\n}\n\n/** `location.search` without the `?`, joined with `&` to a hash route's own query. */\nexport function currentQuery(): string {\n const outer = window.location.search.replace(/^\\?/, '');\n const inner = hashRoute()?.query ?? '';\n\n return outer && inner ? `${outer}&${inner}` : outer || inner;\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 // Page internal_name: both the row's attribution key and the write endpoint's page.\n const 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\nconst INT32_MIN = -(2 ** 31);\nconst INT32_MAX = 2 ** 31 - 1;\n\n/**\n * Integer only: a float or string value widens the `int` column permanently. Out-of-int32 values are 0,\n * since past 2^31-1 the column overflows and past 1e21 the number serializes as `1e+21`.\n */\nexport function toId(value: unknown): number {\n const n = Math.trunc(Number(value));\n\n return Number.isSafeInteger(n) && n >= INT32_MIN && n <= INT32_MAX ? 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/catch only sees a synchronous throw; the .catch keeps a rejected request (e.g. over the\n // keepalive quota) from surfacing as an unhandled rejection in the host app.\n try {\n fetch(endpoint, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true,\n }).catch(() => {\n // Dropped. Nothing further possible at unload.\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\n/** Splits rows into batches whose `[a,b,c]` body stays within maxBytes. A lone row over the limit still goes, alone. */\nfunction chunkBySize(rows: Buffered[], maxBytes: number): Buffered[][] {\n const chunks: Buffered[][] = [];\n let current: Buffered[] = [];\n let size = 2;\n\n for (const row of rows) {\n if (current.length > 0 && size + 1 + row.size > maxBytes) {\n chunks.push(current);\n current = [];\n size = 2;\n }\n\n size += (current.length > 0 ? 1 : 0) + row.size;\n current.push(row);\n }\n\n if (current.length > 0) chunks.push(current);\n\n return chunks;\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 let outcome: SendOutcome = 'failed';\n\n try {\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 // Stopped while the request was out: a stale result must not count toward, or trip, the kill-switch.\n if (stopped) return;\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 // push() only flushes on its own threshold check, which is a no-op while a request is out; rows\n // that piled up meanwhile would otherwise wait for the timer and could outgrow the beacon quota.\n // Only after a success: re-flushing a failure straight away would spend the kill-switch's three\n // strikes within seconds of a brief outage, instead of across timer ticks.\n if (!stopped && outcome === 'ok' && (buffer.length >= MAX_FLUSH_ROWS || bytes >= MAX_FLUSH_BYTES)) void flush();\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\n // One body over the 64 KiB quota fails in both sendBeacon and keepalive fetch and drops every row;\n // flush-sized chunks let at least the ones that fit through.\n for (const chunk of chunkBySize(batch, MAX_FLUSH_BYTES)) {\n sendOnUnload(\n endpoint,\n chunk.map((b) => b.json),\n );\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/** Distinct cap buckets per page load. Past this every render of a component shares one bucket, so per-render props can't leak the map. */\nconst MAX_KEYS = 200;\n/** `visibility.preview.control_buttons.lineage` is 4 levels, the deepest shape MI documents. */\nconst PROP_DEPTH = 4;\n/** A `viewer` render is 38 leaves / 1672 chars, so this must not bind before serializeMeta's own cap does. */\nconst PROP_KEYS = 40;\nconst PROP_STRING = 40;\n\n/** Already read into element_id/segment_id above; re-emitting them as props is noise. */\nconst IDENTITY_KEYS = new Set(['element', 'element_id', 'segment', 'segment_id', 'segment_value_id']);\n\nconst SKIP = Symbol('skip');\n\nexport interface RenderPayload {\n elementId: number;\n meta: Record<string, unknown>;\n}\n\nfunction leafValue(value: unknown): unknown {\n if (value === null) return null;\n\n switch (typeof value) {\n case 'boolean':\n return value;\n case 'number':\n return Number.isFinite(value) ? value : SKIP;\n case 'string':\n return value.slice(0, PROP_STRING);\n default:\n return SKIP;\n }\n}\n\n/** Dotted paths, flat output: the dashboard stringifies a nested object into one opaque value, so its per-key distribution would be noise. Returns true when a cap dropped something. */\nfunction flattenProps(source: Record<string, unknown>, out: Record<string, unknown>, prefix: string, depth: number): boolean {\n let dropped = false;\n\n for (const [key, value] of Object.entries(source)) {\n if (Object.keys(out).length >= PROP_KEYS) return true;\n if (depth === 0 && IDENTITY_KEYS.has(key)) continue;\n\n const path = prefix === '' ? key : `${prefix}.${key}`;\n\n // Summarized, not expanded — index-keyed paths would make every array length its own set of keys.\n if (Array.isArray(value)) {\n out[`prop.${path}`] = `arr:${value.length}`;\n continue;\n }\n\n if (value !== null && typeof value === 'object') {\n if (depth + 1 < PROP_DEPTH) dropped = flattenProps(value as Record<string, unknown>, out, path, depth + 1) || dropped;\n else dropped = true;\n\n continue;\n }\n\n const leaf = leafValue(value);\n\n if (leaf !== SKIP) out[`prop.${path}`] = leaf;\n }\n\n return dropped;\n}\n\n/** FNV-1a. Keeps the bucket key short no matter how large the props are. */\nfunction hash(text: string): string {\n let h = 0x811c9dc5;\n\n for (let i = 0; i < text.length; i += 1) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n\n return (h >>> 0).toString(36);\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 // Three spellings in the wild: preview sends {element, segment}, viewer {element_id, segment_id}, access_denied_popup {element_id, segment_value_id}.\n const elementId = toId(props.element ?? props.element_id ?? 0);\n const segmentId = toId(props.segment ?? props.segment_id ?? props.segment_value_id ?? 0);\n\n const flat: Record<string, unknown> = {};\n const propsDropped = flattenProps(props, flat, '', 0);\n\n // Props are in the key: a component with no element (a popup) is otherwise one bucket for every variant it renders.\n const bucket = `${component}|${elementId}|${segmentId}|${hash(JSON.stringify(flat))}`;\n const key = counts.has(bucket) || counts.size < MAX_KEYS ? bucket : `${component}|*`;\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 // Reserved keys first: serializeMeta drops trailing keys at the 2000-char cap, so props go before identity does.\n const meta: Record<string, unknown> = {\n component,\n element_id: elementId,\n segment_id: segmentId,\n ...flat,\n };\n\n if (propsDropped) meta.props_dropped = 1;\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","import { isHashRoute } from '../config';\n\nexport 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 or hash 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. A hash route\n // (`#/reports?tab=1`) is part of it; a plain in-page anchor (`#section`) is not a new view.\n const locationKey = (path: string): string => {\n try {\n const { search, hash } = window.location;\n\n return `${path}${search}${isHashRoute(hash) ? hash : ''}`;\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 // Hash routers and `location.hash = …` navigate without pushState; dedupe drops the popstate twin.\n window.addEventListener('hashchange', 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 window.removeEventListener('hashchange', 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, currentQuery, 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 { 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 = currentQuery();\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 — but only while this\n // queue's runtime is still the live one, so a stale queue can't kill a later init().\n const queue: Queue = createQueue(config.endpoint, () => {\n if (runtime?.queue === queue) shutdown();\n });\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\n/** `props` is any object, interface-typed included; it is serialized like JSON.stringify (a Date becomes its ISO string). */\nexport function track(event: string, props?: object): 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 as Record<string, unknown> | undefined });\n });\n}\n\n/**\n * `element_id`/`segment_id` resolve identity via `/api/element_info`; everything else is free-form. Generic so\n * interface-typed props with extra keys are accepted — an index signature would reject them.\n */\nexport function trackElement<T extends { element_id: number; segment_id?: number }>(props: T): 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 zero, non-finite or out-of-range 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":["hash","hash"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metricinsights/ca-analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Usage analytics for Metric Insights Custom Apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
31
32
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
@@ -43,9 +44,11 @@
|
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@types/react": "^18.3.12",
|
|
47
|
+
"jsdom": "^30.1.1",
|
|
46
48
|
"react": "^18.3.1",
|
|
47
49
|
"tsup": "^8.3.5",
|
|
48
|
-
"typescript": "^5.7.2"
|
|
50
|
+
"typescript": "^5.7.2",
|
|
51
|
+
"vitest": "^5.0.2"
|
|
49
52
|
},
|
|
50
53
|
"keywords": [
|
|
51
54
|
"metricinsights",
|
|
@@ -62,5 +65,8 @@
|
|
|
62
65
|
"url": "https://github.com/mi-examples/ca-analytics/issues"
|
|
63
66
|
},
|
|
64
67
|
"homepage": "https://github.com/mi-examples/ca-analytics#readme",
|
|
65
|
-
"license": "MIT"
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=22"
|
|
71
|
+
}
|
|
66
72
|
}
|