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