@mushi-mushi/core 1.28.0 → 1.30.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/dist/index.cjs CHANGED
@@ -232,7 +232,8 @@ function createApiClient(options) {
232
232
  if (cbFailures >= cbThreshold) cbOpenUntil = Date.now() + cbCooldownMs;
233
233
  };
234
234
  async function request(method, path, body, retries = maxRetries, internalKind, extraHeaders) {
235
- if (cbIsOpen()) {
235
+ const tracked = internalKind !== "reporter-poll";
236
+ if (tracked && cbIsOpen()) {
236
237
  return { ok: false, error: { code: "CIRCUIT_OPEN", message: "Endpoint temporarily unavailable; retrying later." } };
237
238
  }
238
239
  const url = `${baseUrl}${path}`;
@@ -251,7 +252,7 @@ function createApiClient(options) {
251
252
  ...extraHeaders
252
253
  };
253
254
  const serialized = body ? JSON.stringify(body) : void 0;
254
- if (serialized && method !== "GET") {
255
+ if (serialized && method !== "GET" && internalKind !== "reporter-poll") {
255
256
  lastOutbound = { url, headers, body: serialized, path };
256
257
  }
257
258
  try {
@@ -291,19 +292,22 @@ function createApiClient(options) {
291
292
  await sleep(retryAfterMs ?? getBackoffDelay(maxRetries - retries));
292
293
  return request(method, path, body, retries - 1, internalKind, extraHeaders);
293
294
  }
294
- if (unusable) cbRecordUnreachable();
295
- else cbRecordReachable();
295
+ if (tracked) {
296
+ if (unusable) cbRecordUnreachable();
297
+ else cbRecordReachable();
298
+ }
296
299
  return {
297
300
  ok: false,
298
301
  error: {
299
302
  code: errorBody.error?.code || `HTTP_${response.status}`,
300
- message: errorBody.error?.message || errorBody.message || `HTTP ${response.status} error`
303
+ message: errorBody.error?.message || errorBody.message || `HTTP ${response.status} error`,
304
+ status: response.status
301
305
  }
302
306
  };
303
307
  }
304
308
  const payload = await response.json();
305
309
  const data = payload && typeof payload === "object" && "ok" in payload && "data" in payload ? payload.data : payload;
306
- cbRecordReachable();
310
+ if (tracked) cbRecordReachable();
307
311
  return { ok: true, data };
308
312
  } catch (error) {
309
313
  clearTimeout(timer);
@@ -311,7 +315,7 @@ function createApiClient(options) {
311
315
  await sleep(getBackoffDelay(maxRetries - retries));
312
316
  return request(method, path, body, retries - 1, internalKind, extraHeaders);
313
317
  }
314
- cbRecordUnreachable();
318
+ if (tracked) cbRecordUnreachable();
315
319
  return {
316
320
  ok: false,
317
321
  error: {
@@ -322,39 +326,17 @@ function createApiClient(options) {
322
326
  }
323
327
  }
324
328
  async function requestForReporter(method, path, reporterToken, body) {
325
- const tokenHash = await sha256Hex(reporterToken);
326
- const ts = String(Date.now());
327
- const hmac = await hmacSha256Hex(apiKey, `${projectId}.${ts}.${tokenHash}`);
328
- const url = `${baseUrl}${path}`;
329
- const response = await fetch(url, {
330
- method,
331
- headers: {
332
- "Content-Type": "application/json",
333
- "X-Mushi-Api-Key": apiKey,
334
- "X-Mushi-Project": projectId,
335
- ...sdkPackage ? { [MUSHI_SDK_PACKAGE_HEADER]: sdkPackage } : {},
336
- ...sdkVersion ? { [MUSHI_SDK_VERSION_HEADER]: sdkVersion } : {},
337
- [MUSHI_INTERNAL_HEADER]: "reporter-poll",
329
+ try {
330
+ const tokenHash = await sha256Hex(reporterToken);
331
+ const ts = String(Date.now());
332
+ return await request(method, path, body, maxRetries, "reporter-poll", {
338
333
  "X-Reporter-Token-Hash": tokenHash,
339
334
  "X-Reporter-Ts": ts,
340
- "X-Reporter-Hmac": hmac
341
- },
342
- body: body ? JSON.stringify(body) : void 0,
343
- keepalive: isPageUnloading(),
344
- [MUSHI_INTERNAL_INIT_MARKER]: "reporter-poll"
345
- });
346
- if (!response.ok) {
347
- const errorBody = await response.json().catch(() => ({}));
348
- return {
349
- ok: false,
350
- error: {
351
- code: `HTTP_${response.status}`,
352
- message: errorBody.error?.message ?? errorBody.message ?? `HTTP ${response.status} error`
353
- }
354
- };
335
+ "X-Reporter-Hmac": await hmacSha256Hex(apiKey, `${projectId}.${ts}.${tokenHash}`)
336
+ });
337
+ } catch (error) {
338
+ return { ok: false, error: { code: "NETWORK_ERROR", message: error instanceof Error ? error.message : "Unknown error" } };
355
339
  }
356
- const payload = await response.json();
357
- return { ok: true, data: payload.data ?? payload };
358
340
  }
359
341
  return {
360
342
  async submitReport(report) {
@@ -474,6 +456,18 @@ function createApiClient(options) {
474
456
  "discovery"
475
457
  );
476
458
  },
459
+ /** POST /v1/sdk/events — batched product-analytics events (Mushi.track()).
460
+ * Best-effort (1 retry); the event tracker spills unsent batches to
461
+ * localStorage and replays them on the next page load. */
462
+ async postProductEvents(payload) {
463
+ return request(
464
+ "POST",
465
+ "/v1/sdk/events",
466
+ payload,
467
+ 1,
468
+ "discovery"
469
+ );
470
+ },
477
471
  async submitActivity(userId, events, opts) {
478
472
  return request(
479
473
  "POST",
@@ -600,7 +594,7 @@ function sleep(ms) {
600
594
  function getBackoffDelay(attempt) {
601
595
  return Math.min(1e3 * 2 ** attempt + Math.random() * 500, 1e4);
602
596
  }
603
- function parseRetryAfter(value, now2 = Date.now()) {
597
+ function parseRetryAfter(value, now3 = Date.now()) {
604
598
  if (!value) return null;
605
599
  const raw = value.trim();
606
600
  if (!raw) return null;
@@ -608,7 +602,7 @@ function parseRetryAfter(value, now2 = Date.now()) {
608
602
  if (Number.isFinite(seconds)) return seconds < 0 ? null : seconds * 1e3;
609
603
  const at = Date.parse(raw);
610
604
  if (Number.isNaN(at)) return null;
611
- return Math.max(at - now2, 0);
605
+ return Math.max(at - now3, 0);
612
606
  }
613
607
  function isRetryable(error) {
614
608
  if (error instanceof DOMException && error.name === "AbortError") return true;
@@ -826,6 +820,8 @@ var KNOWN_CONFIG_KEYS = [
826
820
  "circuitBreaker",
827
821
  "preset",
828
822
  "runtimeConfig",
823
+ "analytics",
824
+ "trackSessions",
829
825
  "sentry",
830
826
  "widget",
831
827
  "capture",
@@ -917,23 +913,23 @@ function presetDefaults(preset) {
917
913
  }
918
914
  }
919
915
  function expandPreset(config) {
920
- const defaults = presetDefaults(config.preset);
921
- if (!defaults) return config;
916
+ const defaults2 = presetDefaults(config.preset);
917
+ if (!defaults2) return config;
922
918
  return {
923
919
  ...config,
924
920
  widget: {
925
- ...defaults.widget,
921
+ ...defaults2.widget,
926
922
  ...config.widget
927
923
  },
928
924
  capture: {
929
- ...defaults.capture,
925
+ ...defaults2.capture,
930
926
  ...config.capture
931
927
  },
932
928
  proactive: {
933
- ...defaults.proactive,
929
+ ...defaults2.proactive,
934
930
  ...config.proactive,
935
931
  cooldown: {
936
- ...defaults.proactive?.cooldown,
932
+ ...defaults2.proactive?.cooldown,
937
933
  ...config.proactive?.cooldown
938
934
  }
939
935
  }
@@ -1344,12 +1340,12 @@ function createOfflineQueue(config = {}) {
1344
1340
  const n = row.attempts;
1345
1341
  return typeof n === "number" && Number.isFinite(n) ? n : 0;
1346
1342
  }
1347
- function isExpired(row, now2) {
1343
+ function isExpired(row, now3) {
1348
1344
  const queuedAt = row.queuedAt;
1349
1345
  if (!queuedAt) return false;
1350
1346
  const ts = Date.parse(queuedAt);
1351
1347
  if (Number.isNaN(ts)) return false;
1352
- return now2 - ts > MAX_QUEUE_AGE_MS;
1348
+ return now3 - ts > MAX_QUEUE_AGE_MS;
1353
1349
  }
1354
1350
  function detectBackend() {
1355
1351
  if (backendType) return backendType;
@@ -1536,7 +1532,7 @@ function createOfflineQueue(config = {}) {
1536
1532
  await lsEnqueue(report);
1537
1533
  return "localstorage";
1538
1534
  }
1539
- async function enqueue(report) {
1535
+ async function enqueue2(report) {
1540
1536
  if (!enabled) return;
1541
1537
  const currentSize = await size();
1542
1538
  if (currentSize >= maxQueueSize) {
@@ -1554,10 +1550,10 @@ function createOfflineQueue(config = {}) {
1554
1550
  async function runFlush(client) {
1555
1551
  const { backend, rows: storedRows } = await readRows();
1556
1552
  let rows = storedRows;
1557
- const now2 = Date.now();
1553
+ const now3 = Date.now();
1558
1554
  const fresh = [];
1559
1555
  for (const row of rows) {
1560
- if (isExpired(row, now2)) {
1556
+ if (isExpired(row, now3)) {
1561
1557
  const rowId = row.id;
1562
1558
  await removeRow(backend, rowId);
1563
1559
  queueLog.debug("Offline queue: evicting stale report", { id: rowId });
@@ -1705,7 +1701,7 @@ function createOfflineQueue(config = {}) {
1705
1701
  syncCleanup?.();
1706
1702
  syncCleanup = null;
1707
1703
  }
1708
- return { enqueue, flush, size, clear, startAutoSync, stopAutoSync };
1704
+ return { enqueue: enqueue2, flush, size, clear, startAutoSync, stopAutoSync };
1709
1705
  }
1710
1706
 
1711
1707
  // src/environment.ts
@@ -2047,21 +2043,94 @@ function randomSuffix() {
2047
2043
  return n.toString(36).padStart(6, "0").slice(0, 6);
2048
2044
  }
2049
2045
 
2046
+ // src/analytics-gate.ts
2047
+ var AUTOMATED_UA_RE = /HeadlessChrome|PhantomJS|Lighthouse|\bbot\b|bot[/-]|crawler|spider/i;
2048
+ function consentKey(projectId) {
2049
+ return "mushi_events_consent_" + projectId;
2050
+ }
2051
+ var listeners = /* @__PURE__ */ new Set();
2052
+ function dntActive() {
2053
+ if (typeof navigator === "undefined") return false;
2054
+ const nav = navigator;
2055
+ if (nav.globalPrivacyControl === true) return true;
2056
+ const winDnt = typeof window !== "undefined" ? window.doNotTrack : void 0;
2057
+ const dnt = nav.doNotTrack ?? nav.msDoNotTrack ?? winDnt;
2058
+ return dnt === "1" || dnt === "yes";
2059
+ }
2060
+ function isAutomatedBrowser() {
2061
+ if (typeof navigator === "undefined") return false;
2062
+ if (navigator.webdriver === true) return true;
2063
+ return AUTOMATED_UA_RE.test(navigator.userAgent ?? "");
2064
+ }
2065
+ function analyticsBlockReason(config) {
2066
+ if (config?.enabled === false) return "disabled";
2067
+ if (config?.respectDoNotTrack !== false && dntActive()) return "dnt";
2068
+ if (config?.excludeBots !== false && isAutomatedBrowser()) return "automation";
2069
+ return null;
2070
+ }
2071
+ function readStoredConsent(projectId) {
2072
+ if (!projectId) return null;
2073
+ try {
2074
+ const v = localStorage.getItem(consentKey(projectId));
2075
+ return v === "granted" || v === "denied" ? v : null;
2076
+ } catch {
2077
+ return null;
2078
+ }
2079
+ }
2080
+ function writeStoredConsent(projectId, state) {
2081
+ if (!projectId) return;
2082
+ try {
2083
+ localStorage.setItem(consentKey(projectId), state);
2084
+ } catch {
2085
+ }
2086
+ }
2087
+ function resolveAnalyticsConsent(projectId, config) {
2088
+ const stored = readStoredConsent(projectId);
2089
+ if (config?.consent === "required") return stored ?? "pending";
2090
+ return stored === "denied" ? "denied" : "granted";
2091
+ }
2092
+ function setAnalyticsConsent(projectId, state) {
2093
+ writeStoredConsent(projectId, state);
2094
+ for (const listener of [...listeners]) {
2095
+ try {
2096
+ listener(state);
2097
+ } catch {
2098
+ }
2099
+ }
2100
+ }
2101
+ function onAnalyticsConsentChange(listener) {
2102
+ listeners.add(listener);
2103
+ return () => {
2104
+ listeners.delete(listener);
2105
+ };
2106
+ }
2107
+
2050
2108
  // src/session-tracker.ts
2051
2109
  var HEARTBEAT_INTERVAL_MS = 6e4;
2052
2110
  var _client = null;
2053
2111
  var _sdkVersion;
2054
2112
  var _userIdHash = null;
2055
- var _reporterTokenHash = null;
2113
+ var _reporterToken = null;
2056
2114
  var _pageViewCount = 0;
2057
2115
  var _heartbeatTimer = null;
2058
2116
  var _initialized = false;
2117
+ var _active = false;
2118
+ var _started = false;
2119
+ var _unsubscribeConsent = null;
2059
2120
  function now() {
2060
2121
  return (/* @__PURE__ */ new Date()).toISOString();
2061
2122
  }
2062
2123
  function currentRoute() {
2063
2124
  if (typeof window === "undefined") return "";
2064
- return window.location.pathname + window.location.search;
2125
+ return window.location.pathname;
2126
+ }
2127
+ function referrerOrigin() {
2128
+ if (typeof document === "undefined" || !document.referrer) return null;
2129
+ try {
2130
+ return new URL(document.referrer).origin;
2131
+ } catch {
2132
+ return null;
2133
+ }
2065
2134
  }
2066
2135
  function userAgent() {
2067
2136
  if (typeof navigator === "undefined") return null;
@@ -2073,7 +2142,8 @@ function buildPayload(kind, extra) {
2073
2142
  session_id: getSessionId(),
2074
2143
  ts: now(),
2075
2144
  page_view_count: _pageViewCount,
2076
- reporter_token_hash: _reporterTokenHash,
2145
+ // Wire name kept for older servers; the value is the raw token (hashed server-side).
2146
+ reporter_token_hash: _reporterToken,
2077
2147
  user_id_hash: _userIdHash,
2078
2148
  user_agent: userAgent(),
2079
2149
  sdk_version: _sdkVersion,
@@ -2081,26 +2151,42 @@ function buildPayload(kind, extra) {
2081
2151
  };
2082
2152
  }
2083
2153
  function send(payload) {
2084
- if (!_client) return;
2154
+ if (!_client || !_active) return;
2085
2155
  _client.postSessionEvent(payload).catch(() => {
2086
2156
  });
2087
2157
  }
2158
+ function recordPageView(route) {
2159
+ if (!_active) return;
2160
+ _pageViewCount += 1;
2161
+ send(buildPayload("page_view", { route }));
2162
+ }
2163
+ function activate() {
2164
+ if (_active || !_initialized) return;
2165
+ _active = true;
2166
+ if (!_started) {
2167
+ _started = true;
2168
+ _pageViewCount = 1;
2169
+ send(buildPayload("session_start", { route: currentRoute(), referrer: referrerOrigin() }));
2170
+ }
2171
+ _heartbeatTimer = setInterval(() => {
2172
+ send(buildPayload("session_heartbeat", { route: currentRoute() }));
2173
+ }, HEARTBEAT_INTERVAL_MS);
2174
+ }
2175
+ function deactivate() {
2176
+ _active = false;
2177
+ if (_heartbeatTimer != null) {
2178
+ clearInterval(_heartbeatTimer);
2179
+ _heartbeatTimer = null;
2180
+ }
2181
+ }
2088
2182
  function initSessionTracker(opts) {
2089
2183
  if (_initialized || typeof window === "undefined") return;
2184
+ if (analyticsBlockReason(opts.analytics) !== null) return;
2090
2185
  _initialized = true;
2091
2186
  _client = opts.client;
2092
2187
  _sdkVersion = opts.sdkVersion;
2093
- _reporterTokenHash = opts.reporterTokenHash ?? null;
2188
+ _reporterToken = opts.reporterToken ?? opts.reporterTokenHash ?? null;
2094
2189
  _userIdHash = opts.userIdHash ?? null;
2095
- const entryRoute = currentRoute();
2096
- _pageViewCount = 1;
2097
- send(buildPayload("session_start", {
2098
- route: entryRoute,
2099
- referrer: typeof document !== "undefined" ? document.referrer ?? null : null
2100
- }));
2101
- _heartbeatTimer = setInterval(() => {
2102
- send(buildPayload("session_heartbeat", { route: currentRoute() }));
2103
- }, HEARTBEAT_INTERVAL_MS);
2104
2190
  const onHide = () => {
2105
2191
  if (typeof document !== "undefined" && document.visibilityState === "hidden") {
2106
2192
  send(buildPayload("session_end", { route: currentRoute() }));
@@ -2109,46 +2195,423 @@ function initSessionTracker(opts) {
2109
2195
  if (typeof document !== "undefined") {
2110
2196
  document.addEventListener("visibilitychange", onHide, { passive: true });
2111
2197
  }
2112
- if (typeof window !== "undefined") {
2113
- window.addEventListener("pagehide", () => {
2114
- send(buildPayload("session_end", { route: currentRoute() }));
2115
- }, { passive: true });
2198
+ window.addEventListener("pagehide", () => {
2199
+ send(buildPayload("session_end", { route: currentRoute() }));
2200
+ }, { passive: true });
2201
+ if (opts.patchHistory !== false) {
2202
+ patchHistoryForPageViews();
2116
2203
  }
2117
- patchHistoryForPageViews();
2204
+ _unsubscribeConsent = onAnalyticsConsentChange((state) => {
2205
+ if (state === "granted") activate();
2206
+ else deactivate();
2207
+ });
2208
+ if (resolveAnalyticsConsent(opts.projectId ?? "", opts.analytics) === "granted") activate();
2118
2209
  }
2119
2210
  function trackPageView(route) {
2120
2211
  if (!_initialized || !_client) return;
2121
- _pageViewCount += 1;
2122
- send(buildPayload("page_view", { route: route ?? currentRoute() }));
2212
+ recordPageView(route ?? currentRoute());
2123
2213
  }
2124
2214
  function updateSessionIdentity(userIdHash) {
2125
2215
  _userIdHash = userIdHash;
2126
2216
  }
2127
2217
  function destroySessionTracker() {
2128
- if (_heartbeatTimer != null) {
2129
- clearInterval(_heartbeatTimer);
2130
- _heartbeatTimer = null;
2131
- }
2218
+ deactivate();
2219
+ _unsubscribeConsent?.();
2220
+ _unsubscribeConsent = null;
2132
2221
  _initialized = false;
2222
+ _started = false;
2133
2223
  _client = null;
2134
2224
  }
2135
2225
  var _historyPatched = false;
2136
2226
  function patchHistoryForPageViews() {
2137
2227
  if (_historyPatched || typeof history === "undefined") return;
2138
2228
  _historyPatched = true;
2139
- const wrap = (original) => function(...args) {
2229
+ const wrap = (original, countsAsPageView) => function(...args) {
2140
2230
  const result = original.apply(this, args);
2141
- if (original === history.pushState) {
2142
- _pageViewCount += 1;
2143
- send(buildPayload("page_view", { route: currentRoute() }));
2231
+ if (countsAsPageView) recordPageView(currentRoute());
2232
+ return result;
2233
+ };
2234
+ history.pushState = wrap(history.pushState, true);
2235
+ history.replaceState = wrap(history.replaceState, false);
2236
+ window.addEventListener("popstate", () => {
2237
+ recordPageView(currentRoute());
2238
+ }, { passive: true });
2239
+ }
2240
+
2241
+ // src/analytics-taxonomy.ts
2242
+ var EVENT_NAME_RE = /^[a-z][a-z0-9_]{1,63}$/;
2243
+ var EVENT_PROPERTY_LIMITS = {
2244
+ maxKeys: 40,
2245
+ maxKeyLength: 64,
2246
+ maxValueLength: 256,
2247
+ maxBytes: 8192,
2248
+ /** Client batch size before an immediate flush. */
2249
+ maxClientBatch: 20,
2250
+ /** Server-side per-request ceiling. */
2251
+ maxServerBatch: 50,
2252
+ /** Distinct event names per project before the server returns 422. */
2253
+ maxEventNamesPerProject: 200
2254
+ };
2255
+ var RESERVED_PROPERTY_PREFIX = "$";
2256
+ var PII_PROPERTY_KEY_RE = /(email|phone|password|passwd|token|secret|ssn|address|credit|card|iban)/i;
2257
+ var MUSHI_SURFACES = ["web", "console", "docs", "cli", "mcp", "server", "mobile"];
2258
+ var MUSHI_EVENTS = {
2259
+ // ── Any surface (SDK `analytics.autoPageviews`) ──────────────────────────
2260
+ page_view: { surface: ["web", "docs", "console"], required: [] },
2261
+ // ── Landing + docs (apps/docs) ───────────────────────────────────────────
2262
+ // Every docs route; `route` repeats the reserved `$route` as a host prop so
2263
+ // the requirement is checkable. company_funnel_weekly counts visits from
2264
+ // this and landing_view.
2265
+ docs_page_view: { surface: "docs", required: ["route"] },
2266
+ landing_view: { surface: "docs", required: [] },
2267
+ cta_click: { surface: "docs", required: ["cta_id", "location"] },
2268
+ quickstart_view: { surface: "docs", required: [] },
2269
+ pricing_view: { surface: "docs", required: [] },
2270
+ connect_demo_click: { surface: "docs", required: ["href"] },
2271
+ signup_click: { surface: "docs", required: ["href"] },
2272
+ // ── Console (apps/admin) ─────────────────────────────────────────────────
2273
+ signup_completed: { surface: "console", required: ["signup_source"] },
2274
+ report_opened: { surface: ["console", "mcp"], required: ["report_id"] },
2275
+ fix_context_pulled: { surface: ["console", "mcp"], required: ["report_id"] },
2276
+ fix_dispatched: { surface: ["console", "mcp"], required: ["report_id", "agent"] },
2277
+ invite_sent: { surface: "console", required: [] },
2278
+ upgrade_clicked: { surface: "console", required: ["plan"] },
2279
+ test_report_sent: { surface: "console", required: ["project_id"] },
2280
+ diagnosis_viewed: { surface: "console", required: ["report_id", "project_id", "surface"] },
2281
+ // ── Server-emitted (edge functions) ──────────────────────────────────────
2282
+ project_created: { surface: "server", required: ["project_id"] },
2283
+ key_minted: { surface: "server", required: ["project_id"] },
2284
+ first_report_received: { surface: "server", required: ["project_id"] },
2285
+ first_diagnosis_ready: { surface: "server", required: ["project_id"] },
2286
+ fix_merged: { surface: "server", required: ["project_id"] },
2287
+ upgrade_completed: { surface: "server", required: ["project_id", "plan"] },
2288
+ // ── Growth loop (widget "Bug reports by Mushi" mark) ─────────────────────
2289
+ loop_impression: { surface: "web", required: [] },
2290
+ loop_click: { surface: "web", required: [] },
2291
+ loop_signup: { surface: "console", required: ["ref"] }
2292
+ };
2293
+ var MUSHI_EVENT_NAMES = Object.keys(MUSHI_EVENTS);
2294
+ var ACTIVATION_EVENT = "first_report_received";
2295
+ var HABIT_EVENTS = ["report_opened", "fix_context_pulled", "fix_dispatched"];
2296
+ function isValidEventName(name) {
2297
+ return typeof name === "string" && EVENT_NAME_RE.test(name);
2298
+ }
2299
+ function sanitizeEventProperties(input, opts = {}) {
2300
+ const out = {};
2301
+ const dropped = [];
2302
+ if (!input || typeof input !== "object" || Array.isArray(input)) return { properties: out, dropped };
2303
+ const allow = new Set(opts.allowlist ?? []);
2304
+ let keys = 0;
2305
+ for (const [rawKey, rawValue] of Object.entries(input)) {
2306
+ if (keys >= EVENT_PROPERTY_LIMITS.maxKeys) {
2307
+ dropped.push(rawKey);
2308
+ continue;
2309
+ }
2310
+ const key = rawKey.slice(0, EVENT_PROPERTY_LIMITS.maxKeyLength);
2311
+ if (key.startsWith(RESERVED_PROPERTY_PREFIX) && !opts.allowReserved) {
2312
+ dropped.push(key);
2313
+ continue;
2314
+ }
2315
+ if (PII_PROPERTY_KEY_RE.test(key) && !allow.has(key)) {
2316
+ dropped.push(key);
2317
+ continue;
2318
+ }
2319
+ let value;
2320
+ if (rawValue === null || typeof rawValue === "boolean") value = rawValue;
2321
+ else if (typeof rawValue === "number") value = Number.isFinite(rawValue) ? rawValue : null;
2322
+ else if (typeof rawValue === "string") {
2323
+ const s = rawValue.slice(0, EVENT_PROPERTY_LIMITS.maxValueLength);
2324
+ value = opts.scrub ? opts.scrub(s) : s;
2325
+ } else if (rawValue instanceof Date) value = rawValue.toISOString();
2326
+ else {
2327
+ dropped.push(key);
2328
+ continue;
2329
+ }
2330
+ out[key] = value;
2331
+ keys += 1;
2332
+ }
2333
+ return { properties: out, dropped };
2334
+ }
2335
+ function propertiesWithinByteLimit(properties) {
2336
+ try {
2337
+ return JSON.stringify(properties).length <= EVENT_PROPERTY_LIMITS.maxBytes;
2338
+ } catch {
2339
+ return false;
2340
+ }
2341
+ }
2342
+
2343
+ // src/event-tracker.ts
2344
+ var _client2 = null;
2345
+ var _projectId = "";
2346
+ var _anonId = null;
2347
+ var _userId = null;
2348
+ var _userTraits = null;
2349
+ var _sdkVersion2;
2350
+ var _config = defaults();
2351
+ var _scrub;
2352
+ var _getSessionId;
2353
+ var _buffer = [];
2354
+ var _consentBuffer = [];
2355
+ var _consent = "granted";
2356
+ var _sampledIn = true;
2357
+ var _flushTimer = null;
2358
+ var _initialized2 = false;
2359
+ var _flushing = false;
2360
+ var _pendingIdentify = false;
2361
+ var _unsubscribeConsent2 = null;
2362
+ var CONSENT_BUFFER_MAX = 50;
2363
+ var SPILL_MAX = 200;
2364
+ var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
2365
+ function defaults() {
2366
+ return {
2367
+ sampleRate: 1,
2368
+ autoPageviews: false,
2369
+ flushIntervalMs: 5e3,
2370
+ surface: "web",
2371
+ propertyAllowlist: []
2372
+ };
2373
+ }
2374
+ function now2() {
2375
+ return (/* @__PURE__ */ new Date()).toISOString();
2376
+ }
2377
+ function spillKey() {
2378
+ return "mushi_events_spill_" + _projectId;
2379
+ }
2380
+ function isRetryableStatus(status) {
2381
+ return status === void 0 || status === 429 || status >= 500;
2382
+ }
2383
+ function analyticsIdKey() {
2384
+ return "mushi_analytics_id_" + _projectId;
2385
+ }
2386
+ function loadOrMintAnalyticsId() {
2387
+ try {
2388
+ const stored = localStorage.getItem(analyticsIdKey());
2389
+ if (stored) return stored;
2390
+ } catch {
2391
+ }
2392
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : "a-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
2393
+ }
2394
+ function persistAnalyticsId() {
2395
+ if (!_anonId) return;
2396
+ try {
2397
+ if (localStorage.getItem(analyticsIdKey()) !== _anonId) localStorage.setItem(analyticsIdKey(), _anonId);
2398
+ } catch {
2399
+ }
2400
+ }
2401
+ function hashToUnit(s) {
2402
+ let h = 2166136261;
2403
+ for (let i = 0; i < s.length; i += 1) {
2404
+ h ^= s.charCodeAt(i);
2405
+ h = Math.imul(h, 16777619) >>> 0;
2406
+ }
2407
+ return (h >>> 0) / 4294967296;
2408
+ }
2409
+ function readSpill() {
2410
+ try {
2411
+ const raw = localStorage.getItem(spillKey());
2412
+ if (!raw) return [];
2413
+ const parsed = JSON.parse(raw);
2414
+ if (!parsed || Date.now() - parsed.at > SPILL_TTL_MS) {
2415
+ localStorage.removeItem(spillKey());
2416
+ return [];
2417
+ }
2418
+ return Array.isArray(parsed.events) ? parsed.events.slice(0, SPILL_MAX) : [];
2419
+ } catch {
2420
+ return [];
2421
+ }
2422
+ }
2423
+ function writeSpill(events) {
2424
+ try {
2425
+ if (events.length === 0) {
2426
+ localStorage.removeItem(spillKey());
2427
+ return;
2144
2428
  }
2429
+ localStorage.setItem(spillKey(), JSON.stringify({ at: Date.now(), events: events.slice(-SPILL_MAX) }));
2430
+ } catch {
2431
+ }
2432
+ }
2433
+ function takeSpill() {
2434
+ const events = readSpill();
2435
+ if (events.length > 0) writeSpill([]);
2436
+ return events;
2437
+ }
2438
+ function buildPayload2(events) {
2439
+ return {
2440
+ anon_id: _anonId,
2441
+ user_id: _userId,
2442
+ user_traits: _userTraits,
2443
+ session_id: _getSessionId ? _getSessionId() : null,
2444
+ sdk_version: _sdkVersion2,
2445
+ surface: _config.surface,
2446
+ events
2447
+ };
2448
+ }
2449
+ async function flushNow() {
2450
+ if (!_client2 || _flushing || _consent !== "granted") return;
2451
+ if (_buffer.length === 0 && !_pendingIdentify) return;
2452
+ _flushing = true;
2453
+ persistAnalyticsId();
2454
+ const batch = _buffer.splice(0, EVENT_PROPERTY_LIMITS.maxServerBatch);
2455
+ if (_pendingIdentify) {
2456
+ batch.unshift({ name: "identify", ts: now2(), properties: {} });
2457
+ _pendingIdentify = false;
2458
+ }
2459
+ try {
2460
+ const res = await _client2.postProductEvents(buildPayload2(batch));
2461
+ if (res.ok) {
2462
+ writeSpill([]);
2463
+ } else if (isRetryableStatus(res.error?.status)) {
2464
+ writeSpill([...readSpill(), ...batch.filter((e) => e.name !== "identify")]);
2465
+ }
2466
+ } catch {
2467
+ writeSpill([...readSpill(), ...batch.filter((e) => e.name !== "identify")]);
2468
+ } finally {
2469
+ _flushing = false;
2470
+ }
2471
+ if (_buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
2472
+ }
2473
+ function enqueue(ev) {
2474
+ if (_consent === "denied") return false;
2475
+ if (_consent === "pending") {
2476
+ if (_consentBuffer.length >= CONSENT_BUFFER_MAX) return false;
2477
+ _consentBuffer.push(ev);
2478
+ return true;
2479
+ }
2480
+ _buffer.push(ev);
2481
+ if (_buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
2482
+ return true;
2483
+ }
2484
+ function applyConsent(state) {
2485
+ if (state === "denied") {
2486
+ _consent = "denied";
2487
+ _buffer = [];
2488
+ _consentBuffer = [];
2489
+ _pendingIdentify = false;
2490
+ writeSpill([]);
2491
+ return;
2492
+ }
2493
+ const wasPending = _consent === "pending";
2494
+ _consent = "granted";
2495
+ if (wasPending) {
2496
+ _buffer.push(...takeSpill(), ..._consentBuffer);
2497
+ _consentBuffer = [];
2498
+ }
2499
+ if (_buffer.length > 0 || _pendingIdentify) void flushNow();
2500
+ }
2501
+ function stripUndefined(o) {
2502
+ const out = {};
2503
+ for (const [k, v] of Object.entries(o)) {
2504
+ if (v !== void 0) out[k] = v;
2505
+ }
2506
+ return out;
2507
+ }
2508
+ function isEventTrackingActive() {
2509
+ return _initialized2 && _consent === "granted" && _sampledIn;
2510
+ }
2511
+ function getEventAnonymousId() {
2512
+ if (!_initialized2 || _consent === "denied") return null;
2513
+ return _anonId;
2514
+ }
2515
+ function initEventTracker(opts) {
2516
+ if (_initialized2) return;
2517
+ if (typeof window === "undefined") return;
2518
+ const cfg = opts.config ?? {};
2519
+ _config = { ...defaults(), ...stripUndefined(cfg), propertyAllowlist: cfg.propertyAllowlist ?? [] };
2520
+ _projectId = opts.projectId;
2521
+ if (analyticsBlockReason(cfg) !== null) return;
2522
+ _initialized2 = true;
2523
+ _client2 = opts.client;
2524
+ _anonId = opts.anonId ?? loadOrMintAnalyticsId();
2525
+ _userId = opts.userId ?? null;
2526
+ _sdkVersion2 = opts.sdkVersion;
2527
+ _scrub = opts.scrub;
2528
+ _getSessionId = opts.getSessionId;
2529
+ const rate = Math.min(1, Math.max(0, _config.sampleRate));
2530
+ _sampledIn = rate >= 1 ? true : rate <= 0 ? false : hashToUnit(_anonId ?? "anon") < rate;
2531
+ if (!_sampledIn) return;
2532
+ _consent = resolveAnalyticsConsent(_projectId, cfg);
2533
+ if (_consent === "granted") _buffer.push(...takeSpill());
2534
+ else if (_consent === "denied") writeSpill([]);
2535
+ _unsubscribeConsent2 = onAnalyticsConsentChange(applyConsent);
2536
+ const interval = Math.max(1e3, _config.flushIntervalMs);
2537
+ _flushTimer = setInterval(() => {
2538
+ void flushNow();
2539
+ }, interval);
2540
+ const onHide = () => {
2541
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") void flushNow();
2542
+ };
2543
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onHide, { passive: true });
2544
+ window.addEventListener("pagehide", () => {
2545
+ void flushNow();
2546
+ }, { passive: true });
2547
+ if (_config.autoPageviews) patchHistory();
2548
+ }
2549
+ function trackEvent(name, properties, opts) {
2550
+ if (!_initialized2 || !_sampledIn) return false;
2551
+ if (!isValidEventName(name)) return false;
2552
+ const { properties: props } = sanitizeEventProperties(properties, {
2553
+ allowlist: _config.propertyAllowlist,
2554
+ scrub: _scrub
2555
+ });
2556
+ let merged = props;
2557
+ if (opts?.reserved) {
2558
+ const { properties: res } = sanitizeEventProperties(opts.reserved, { allowReserved: true, scrub: _scrub });
2559
+ merged = { ...props, ...res };
2560
+ }
2561
+ merged.$surface = _config.surface;
2562
+ if (typeof location !== "undefined" && merged.$route === void 0) merged.$route = location.pathname;
2563
+ if (!propertiesWithinByteLimit(merged)) return false;
2564
+ return enqueue({
2565
+ name,
2566
+ ts: opts?.ts ?? now2(),
2567
+ properties: merged,
2568
+ ...opts?.dedupKey ? { dedup_key: opts.dedupKey } : {}
2569
+ });
2570
+ }
2571
+ function setEventConsent(state) {
2572
+ if (!_initialized2) _consent = state;
2573
+ setAnalyticsConsent(_projectId, state);
2574
+ }
2575
+ function updateEventIdentity(userId, traits) {
2576
+ _userId = userId;
2577
+ _userTraits = traits ?? null;
2578
+ if (!_initialized2 || !_sampledIn || !userId || _consent === "denied") return;
2579
+ _pendingIdentify = true;
2580
+ if (_consent === "granted") void flushNow();
2581
+ }
2582
+ function flushEvents() {
2583
+ return flushNow();
2584
+ }
2585
+ function destroyEventTracker() {
2586
+ if (_flushTimer != null) {
2587
+ clearInterval(_flushTimer);
2588
+ _flushTimer = null;
2589
+ }
2590
+ _unsubscribeConsent2?.();
2591
+ _unsubscribeConsent2 = null;
2592
+ _initialized2 = false;
2593
+ _client2 = null;
2594
+ _buffer = [];
2595
+ _consentBuffer = [];
2596
+ _consent = "granted";
2597
+ _sampledIn = true;
2598
+ _pendingIdentify = false;
2599
+ _userId = null;
2600
+ _userTraits = null;
2601
+ _config = defaults();
2602
+ }
2603
+ var _historyPatched2 = false;
2604
+ function patchHistory() {
2605
+ if (_historyPatched2 || typeof history === "undefined") return;
2606
+ _historyPatched2 = true;
2607
+ const original = history.pushState;
2608
+ history.pushState = function(...args) {
2609
+ const result = original.apply(this, args);
2610
+ trackEvent("page_view");
2145
2611
  return result;
2146
2612
  };
2147
- history.pushState = wrap(history.pushState);
2148
- history.replaceState = wrap(history.replaceState);
2149
2613
  window.addEventListener("popstate", () => {
2150
- _pageViewCount += 1;
2151
- send(buildPayload("page_view", { route: currentRoute() }));
2614
+ trackEvent("page_view");
2152
2615
  }, { passive: true });
2153
2616
  }
2154
2617
 
@@ -2165,12 +2628,12 @@ function createRateLimiter(config = {}) {
2165
2628
  let tokens = maxBurst;
2166
2629
  let lastRefill = Date.now();
2167
2630
  function refill() {
2168
- const now2 = Date.now();
2169
- const elapsed = now2 - lastRefill;
2631
+ const now3 = Date.now();
2632
+ const elapsed = now3 - lastRefill;
2170
2633
  const refills = Math.floor(elapsed / refillIntervalMs);
2171
2634
  if (refills > 0) {
2172
2635
  tokens = Math.min(maxBurst, tokens + refills * refillRate);
2173
- lastRefill = now2;
2636
+ lastRefill = now3;
2174
2637
  }
2175
2638
  }
2176
2639
  function tryConsume() {
@@ -2568,15 +3031,15 @@ function mushiTokens(mode) {
2568
3031
  // src/identity.ts
2569
3032
  var MUSHI_IDENTITY_TOKEN_PREFIX = "mushi.identity";
2570
3033
  function buildIdentityClaims(input) {
2571
- const now2 = Math.floor(Date.now() / 1e3);
3034
+ const now3 = Math.floor(Date.now() / 1e3);
2572
3035
  const ttl = input.ttlSeconds && input.ttlSeconds > 0 ? Math.min(input.ttlSeconds, 3600) : 600;
2573
3036
  return {
2574
3037
  projectId: input.projectId,
2575
3038
  sub: input.userId,
2576
3039
  ...input.email ? { email: input.email } : {},
2577
3040
  ...input.name ? { name: input.name } : {},
2578
- iat: now2,
2579
- exp: now2 + ttl
3041
+ iat: now3,
3042
+ exp: now3 + ttl
2580
3043
  };
2581
3044
  }
2582
3045
  function base64UrlDecode(segment) {
@@ -2803,9 +3266,13 @@ function isSafeFaviconHref(href) {
2803
3266
  }
2804
3267
  }
2805
3268
 
3269
+ exports.ACTIVATION_EVENT = ACTIVATION_EVENT;
2806
3270
  exports.DEFAULT_API_ENDPOINT = DEFAULT_API_ENDPOINT;
2807
3271
  exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES;
2808
3272
  exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
3273
+ exports.EVENT_NAME_RE = EVENT_NAME_RE;
3274
+ exports.EVENT_PROPERTY_LIMITS = EVENT_PROPERTY_LIMITS;
3275
+ exports.HABIT_EVENTS = HABIT_EVENTS;
2809
3276
  exports.MAX_REPORT_PAYLOAD_BYTES = MAX_REPORT_PAYLOAD_BYTES;
2810
3277
  exports.MAX_SCREENSHOT_DATA_URL_BYTES = MAX_SCREENSHOT_DATA_URL_BYTES;
2811
3278
  exports.MUSHI_ACCENT_SHADOW = MUSHI_ACCENT_SHADOW;
@@ -2818,6 +3285,8 @@ exports.MUSHI_COLORS_LIGHT = MUSHI_COLORS_LIGHT;
2818
3285
  exports.MUSHI_CONTROL_DISABLED = MUSHI_CONTROL_DISABLED;
2819
3286
  exports.MUSHI_COPY = MUSHI_COPY;
2820
3287
  exports.MUSHI_DURATION = MUSHI_DURATION;
3288
+ exports.MUSHI_EVENTS = MUSHI_EVENTS;
3289
+ exports.MUSHI_EVENT_NAMES = MUSHI_EVENT_NAMES;
2821
3290
  exports.MUSHI_GEOMETRY = MUSHI_GEOMETRY;
2822
3291
  exports.MUSHI_IDENTITY_TOKEN_PREFIX = MUSHI_IDENTITY_TOKEN_PREFIX;
2823
3292
  exports.MUSHI_INTERNAL_HEADER = MUSHI_INTERNAL_HEADER;
@@ -2834,11 +3303,13 @@ exports.MUSHI_SDK_PACKAGE_HEADER = MUSHI_SDK_PACKAGE_HEADER;
2834
3303
  exports.MUSHI_SDK_VERSION_HEADER = MUSHI_SDK_VERSION_HEADER;
2835
3304
  exports.MUSHI_SHADOW_INK = MUSHI_SHADOW_INK;
2836
3305
  exports.MUSHI_SPACING = MUSHI_SPACING;
3306
+ exports.MUSHI_SURFACES = MUSHI_SURFACES;
2837
3307
  exports.MUSHI_TIER_COLORS = MUSHI_TIER_COLORS;
2838
3308
  exports.MUSHI_TYPE = MUSHI_TYPE;
2839
3309
  exports.MUSHI_USER_TOKEN_HEADER = MUSHI_USER_TOKEN_HEADER;
2840
3310
  exports.MUSHI_Z = MUSHI_Z;
2841
3311
  exports.REGION_ENDPOINTS = REGION_ENDPOINTS;
3312
+ exports.analyticsBlockReason = analyticsBlockReason;
2842
3313
  exports.buildBeaconEnvelope = buildBeaconEnvelope;
2843
3314
  exports.buildIdentityClaims = buildIdentityClaims;
2844
3315
  exports.buildSdkIngestHeaders = buildSdkIngestHeaders;
@@ -2851,26 +3322,34 @@ exports.createOfflineQueue = createOfflineQueue;
2851
3322
  exports.createPiiScrubber = createPiiScrubber;
2852
3323
  exports.createPreFilter = createPreFilter;
2853
3324
  exports.createRateLimiter = createRateLimiter;
3325
+ exports.destroyEventTracker = destroyEventTracker;
2854
3326
  exports.destroySessionTracker = destroySessionTracker;
2855
3327
  exports.diagnoseEnvConfig = diagnoseEnvConfig;
3328
+ exports.dntActive = dntActive;
2856
3329
  exports.estimateJsonBytes = estimateJsonBytes;
2857
3330
  exports.expandPreset = expandPreset;
2858
3331
  exports.extractTunnelPath = extractTunnelPath;
2859
3332
  exports.faviconUrlCandidates = faviconUrlCandidates;
3333
+ exports.flushEvents = flushEvents;
2860
3334
  exports.flushLastOutboundOnUnload = flushLastOutboundOnUnload;
2861
3335
  exports.formatBytes = formatBytes;
2862
3336
  exports.getBackoffDelay = getBackoffDelay;
2863
3337
  exports.getDeviceFingerprintHash = getDeviceFingerprintHash;
3338
+ exports.getEventAnonymousId = getEventAnonymousId;
2864
3339
  exports.getReporterToken = getReporterToken;
2865
3340
  exports.getSessionId = getSessionId;
2866
3341
  exports.githubRepoDomainHint = githubRepoDomainHint;
2867
3342
  exports.hmacSha256Hex = hmacSha256Hex;
3343
+ exports.initEventTracker = initEventTracker;
2868
3344
  exports.initSessionTracker = initSessionTracker;
3345
+ exports.isAutomatedBrowser = isAutomatedBrowser;
3346
+ exports.isEventTrackingActive = isEventTrackingActive;
2869
3347
  exports.isLikelyGenericFavicon = isLikelyGenericFavicon;
2870
3348
  exports.isMushiBeaconEnvelope = isMushiBeaconEnvelope;
2871
3349
  exports.isPageUnloading = isPageUnloading;
2872
3350
  exports.isSameOriginOrRelative = isSameOriginOrRelative;
2873
3351
  exports.isUntrustedFaviconUrl = isUntrustedFaviconUrl;
3352
+ exports.isValidEventName = isValidEventName;
2874
3353
  exports.markPageUnloading = markPageUnloading;
2875
3354
  exports.matchesErrorFilter = matchesErrorFilter;
2876
3355
  exports.mushiPalette = mushiPalette;
@@ -2878,13 +3357,16 @@ exports.mushiTokens = mushiTokens;
2878
3357
  exports.newUuid = newUuid;
2879
3358
  exports.noopLogger = noopLogger;
2880
3359
  exports.normaliseThrown = normaliseThrown;
3360
+ exports.onAnalyticsConsentChange = onAnalyticsConsentChange;
2881
3361
  exports.originToDomain = originToDomain;
2882
3362
  exports.parseIdentityToken = parseIdentityToken;
2883
3363
  exports.parseRetryAfter = parseRetryAfter;
2884
3364
  exports.projectFaviconUrlCandidates = projectFaviconUrlCandidates;
2885
3365
  exports.projectInitials = projectInitials;
2886
3366
  exports.projectInitialsThemeIndex = projectInitialsThemeIndex;
3367
+ exports.propertiesWithinByteLimit = propertiesWithinByteLimit;
2887
3368
  exports.readPageFaviconHref = readPageFaviconHref;
3369
+ exports.resolveAnalyticsConsent = resolveAnalyticsConsent;
2888
3370
  exports.resolveEnvConfig = resolveEnvConfig;
2889
3371
  exports.resolveProjectDomain = resolveProjectDomain;
2890
3372
  exports.resolveProjectFaviconDomains = resolveProjectFaviconDomains;
@@ -2892,12 +3374,17 @@ exports.resolveRegionEndpoint = resolveRegionEndpoint;
2892
3374
  exports.resolveRequestBaseUrl = resolveRequestBaseUrl;
2893
3375
  exports.resolveWidgetAccent = resolveWidgetAccent;
2894
3376
  exports.safeWidgetHex = safeWidgetHex;
3377
+ exports.sanitizeEventProperties = sanitizeEventProperties;
2895
3378
  exports.scrubPii = scrubPii;
2896
3379
  exports.scrubUrl = scrubUrl;
2897
3380
  exports.sendOnUnload = sendOnUnload;
3381
+ exports.setAnalyticsConsent = setAnalyticsConsent;
3382
+ exports.setEventConsent = setEventConsent;
2898
3383
  exports.sha256Hex = sha256Hex;
2899
3384
  exports.shouldDropCapturedError = shouldDropCapturedError;
3385
+ exports.trackEvent = trackEvent;
2900
3386
  exports.trackPageView = trackPageView;
3387
+ exports.updateEventIdentity = updateEventIdentity;
2901
3388
  exports.updateSessionIdentity = updateSessionIdentity;
2902
3389
  exports.validateConfig = validateConfig;
2903
3390
  //# sourceMappingURL=index.cjs.map