@mushi-mushi/core 1.28.0 → 1.29.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
@@ -295,7 +295,8 @@ function createApiClient(options) {
295
295
  ok: false,
296
296
  error: {
297
297
  code: errorBody.error?.code || `HTTP_${response.status}`,
298
- message: errorBody.error?.message || errorBody.message || `HTTP ${response.status} error`
298
+ message: errorBody.error?.message || errorBody.message || `HTTP ${response.status} error`,
299
+ status: response.status
299
300
  }
300
301
  };
301
302
  }
@@ -472,6 +473,18 @@ function createApiClient(options) {
472
473
  "discovery"
473
474
  );
474
475
  },
476
+ /** POST /v1/sdk/events — batched product-analytics events (Mushi.track()).
477
+ * Best-effort (1 retry); the event tracker spills unsent batches to
478
+ * localStorage and replays them on the next page load. */
479
+ async postProductEvents(payload) {
480
+ return request(
481
+ "POST",
482
+ "/v1/sdk/events",
483
+ payload,
484
+ 1,
485
+ "discovery"
486
+ );
487
+ },
475
488
  async submitActivity(userId, events, opts) {
476
489
  return request(
477
490
  "POST",
@@ -598,7 +611,7 @@ function sleep(ms) {
598
611
  function getBackoffDelay(attempt) {
599
612
  return Math.min(1e3 * 2 ** attempt + Math.random() * 500, 1e4);
600
613
  }
601
- function parseRetryAfter(value, now2 = Date.now()) {
614
+ function parseRetryAfter(value, now3 = Date.now()) {
602
615
  if (!value) return null;
603
616
  const raw = value.trim();
604
617
  if (!raw) return null;
@@ -606,7 +619,7 @@ function parseRetryAfter(value, now2 = Date.now()) {
606
619
  if (Number.isFinite(seconds)) return seconds < 0 ? null : seconds * 1e3;
607
620
  const at = Date.parse(raw);
608
621
  if (Number.isNaN(at)) return null;
609
- return Math.max(at - now2, 0);
622
+ return Math.max(at - now3, 0);
610
623
  }
611
624
  function isRetryable(error) {
612
625
  if (error instanceof DOMException && error.name === "AbortError") return true;
@@ -824,6 +837,8 @@ var KNOWN_CONFIG_KEYS = [
824
837
  "circuitBreaker",
825
838
  "preset",
826
839
  "runtimeConfig",
840
+ "analytics",
841
+ "trackSessions",
827
842
  "sentry",
828
843
  "widget",
829
844
  "capture",
@@ -915,23 +930,23 @@ function presetDefaults(preset) {
915
930
  }
916
931
  }
917
932
  function expandPreset(config) {
918
- const defaults = presetDefaults(config.preset);
919
- if (!defaults) return config;
933
+ const defaults2 = presetDefaults(config.preset);
934
+ if (!defaults2) return config;
920
935
  return {
921
936
  ...config,
922
937
  widget: {
923
- ...defaults.widget,
938
+ ...defaults2.widget,
924
939
  ...config.widget
925
940
  },
926
941
  capture: {
927
- ...defaults.capture,
942
+ ...defaults2.capture,
928
943
  ...config.capture
929
944
  },
930
945
  proactive: {
931
- ...defaults.proactive,
946
+ ...defaults2.proactive,
932
947
  ...config.proactive,
933
948
  cooldown: {
934
- ...defaults.proactive?.cooldown,
949
+ ...defaults2.proactive?.cooldown,
935
950
  ...config.proactive?.cooldown
936
951
  }
937
952
  }
@@ -1342,12 +1357,12 @@ function createOfflineQueue(config = {}) {
1342
1357
  const n = row.attempts;
1343
1358
  return typeof n === "number" && Number.isFinite(n) ? n : 0;
1344
1359
  }
1345
- function isExpired(row, now2) {
1360
+ function isExpired(row, now3) {
1346
1361
  const queuedAt = row.queuedAt;
1347
1362
  if (!queuedAt) return false;
1348
1363
  const ts = Date.parse(queuedAt);
1349
1364
  if (Number.isNaN(ts)) return false;
1350
- return now2 - ts > MAX_QUEUE_AGE_MS;
1365
+ return now3 - ts > MAX_QUEUE_AGE_MS;
1351
1366
  }
1352
1367
  function detectBackend() {
1353
1368
  if (backendType) return backendType;
@@ -1534,7 +1549,7 @@ function createOfflineQueue(config = {}) {
1534
1549
  await lsEnqueue(report);
1535
1550
  return "localstorage";
1536
1551
  }
1537
- async function enqueue(report) {
1552
+ async function enqueue2(report) {
1538
1553
  if (!enabled) return;
1539
1554
  const currentSize = await size();
1540
1555
  if (currentSize >= maxQueueSize) {
@@ -1552,10 +1567,10 @@ function createOfflineQueue(config = {}) {
1552
1567
  async function runFlush(client) {
1553
1568
  const { backend, rows: storedRows } = await readRows();
1554
1569
  let rows = storedRows;
1555
- const now2 = Date.now();
1570
+ const now3 = Date.now();
1556
1571
  const fresh = [];
1557
1572
  for (const row of rows) {
1558
- if (isExpired(row, now2)) {
1573
+ if (isExpired(row, now3)) {
1559
1574
  const rowId = row.id;
1560
1575
  await removeRow(backend, rowId);
1561
1576
  queueLog.debug("Offline queue: evicting stale report", { id: rowId });
@@ -1703,7 +1718,7 @@ function createOfflineQueue(config = {}) {
1703
1718
  syncCleanup?.();
1704
1719
  syncCleanup = null;
1705
1720
  }
1706
- return { enqueue, flush, size, clear, startAutoSync, stopAutoSync };
1721
+ return { enqueue: enqueue2, flush, size, clear, startAutoSync, stopAutoSync };
1707
1722
  }
1708
1723
 
1709
1724
  // src/environment.ts
@@ -2045,21 +2060,94 @@ function randomSuffix() {
2045
2060
  return n.toString(36).padStart(6, "0").slice(0, 6);
2046
2061
  }
2047
2062
 
2063
+ // src/analytics-gate.ts
2064
+ var AUTOMATED_UA_RE = /HeadlessChrome|PhantomJS|Lighthouse|\bbot\b|bot[/-]|crawler|spider/i;
2065
+ function consentKey(projectId) {
2066
+ return "mushi_events_consent_" + projectId;
2067
+ }
2068
+ var listeners = /* @__PURE__ */ new Set();
2069
+ function dntActive() {
2070
+ if (typeof navigator === "undefined") return false;
2071
+ const nav = navigator;
2072
+ if (nav.globalPrivacyControl === true) return true;
2073
+ const winDnt = typeof window !== "undefined" ? window.doNotTrack : void 0;
2074
+ const dnt = nav.doNotTrack ?? nav.msDoNotTrack ?? winDnt;
2075
+ return dnt === "1" || dnt === "yes";
2076
+ }
2077
+ function isAutomatedBrowser() {
2078
+ if (typeof navigator === "undefined") return false;
2079
+ if (navigator.webdriver === true) return true;
2080
+ return AUTOMATED_UA_RE.test(navigator.userAgent ?? "");
2081
+ }
2082
+ function analyticsBlockReason(config) {
2083
+ if (config?.enabled === false) return "disabled";
2084
+ if (config?.respectDoNotTrack !== false && dntActive()) return "dnt";
2085
+ if (config?.excludeBots !== false && isAutomatedBrowser()) return "automation";
2086
+ return null;
2087
+ }
2088
+ function readStoredConsent(projectId) {
2089
+ if (!projectId) return null;
2090
+ try {
2091
+ const v = localStorage.getItem(consentKey(projectId));
2092
+ return v === "granted" || v === "denied" ? v : null;
2093
+ } catch {
2094
+ return null;
2095
+ }
2096
+ }
2097
+ function writeStoredConsent(projectId, state) {
2098
+ if (!projectId) return;
2099
+ try {
2100
+ localStorage.setItem(consentKey(projectId), state);
2101
+ } catch {
2102
+ }
2103
+ }
2104
+ function resolveAnalyticsConsent(projectId, config) {
2105
+ const stored = readStoredConsent(projectId);
2106
+ if (config?.consent === "required") return stored ?? "pending";
2107
+ return stored === "denied" ? "denied" : "granted";
2108
+ }
2109
+ function setAnalyticsConsent(projectId, state) {
2110
+ writeStoredConsent(projectId, state);
2111
+ for (const listener of [...listeners]) {
2112
+ try {
2113
+ listener(state);
2114
+ } catch {
2115
+ }
2116
+ }
2117
+ }
2118
+ function onAnalyticsConsentChange(listener) {
2119
+ listeners.add(listener);
2120
+ return () => {
2121
+ listeners.delete(listener);
2122
+ };
2123
+ }
2124
+
2048
2125
  // src/session-tracker.ts
2049
2126
  var HEARTBEAT_INTERVAL_MS = 6e4;
2050
2127
  var _client = null;
2051
2128
  var _sdkVersion;
2052
2129
  var _userIdHash = null;
2053
- var _reporterTokenHash = null;
2130
+ var _reporterToken = null;
2054
2131
  var _pageViewCount = 0;
2055
2132
  var _heartbeatTimer = null;
2056
2133
  var _initialized = false;
2134
+ var _active = false;
2135
+ var _started = false;
2136
+ var _unsubscribeConsent = null;
2057
2137
  function now() {
2058
2138
  return (/* @__PURE__ */ new Date()).toISOString();
2059
2139
  }
2060
2140
  function currentRoute() {
2061
2141
  if (typeof window === "undefined") return "";
2062
- return window.location.pathname + window.location.search;
2142
+ return window.location.pathname;
2143
+ }
2144
+ function referrerOrigin() {
2145
+ if (typeof document === "undefined" || !document.referrer) return null;
2146
+ try {
2147
+ return new URL(document.referrer).origin;
2148
+ } catch {
2149
+ return null;
2150
+ }
2063
2151
  }
2064
2152
  function userAgent() {
2065
2153
  if (typeof navigator === "undefined") return null;
@@ -2071,7 +2159,8 @@ function buildPayload(kind, extra) {
2071
2159
  session_id: getSessionId(),
2072
2160
  ts: now(),
2073
2161
  page_view_count: _pageViewCount,
2074
- reporter_token_hash: _reporterTokenHash,
2162
+ // Wire name kept for older servers; the value is the raw token (hashed server-side).
2163
+ reporter_token_hash: _reporterToken,
2075
2164
  user_id_hash: _userIdHash,
2076
2165
  user_agent: userAgent(),
2077
2166
  sdk_version: _sdkVersion,
@@ -2079,26 +2168,42 @@ function buildPayload(kind, extra) {
2079
2168
  };
2080
2169
  }
2081
2170
  function send(payload) {
2082
- if (!_client) return;
2171
+ if (!_client || !_active) return;
2083
2172
  _client.postSessionEvent(payload).catch(() => {
2084
2173
  });
2085
2174
  }
2175
+ function recordPageView(route) {
2176
+ if (!_active) return;
2177
+ _pageViewCount += 1;
2178
+ send(buildPayload("page_view", { route }));
2179
+ }
2180
+ function activate() {
2181
+ if (_active || !_initialized) return;
2182
+ _active = true;
2183
+ if (!_started) {
2184
+ _started = true;
2185
+ _pageViewCount = 1;
2186
+ send(buildPayload("session_start", { route: currentRoute(), referrer: referrerOrigin() }));
2187
+ }
2188
+ _heartbeatTimer = setInterval(() => {
2189
+ send(buildPayload("session_heartbeat", { route: currentRoute() }));
2190
+ }, HEARTBEAT_INTERVAL_MS);
2191
+ }
2192
+ function deactivate() {
2193
+ _active = false;
2194
+ if (_heartbeatTimer != null) {
2195
+ clearInterval(_heartbeatTimer);
2196
+ _heartbeatTimer = null;
2197
+ }
2198
+ }
2086
2199
  function initSessionTracker(opts) {
2087
2200
  if (_initialized || typeof window === "undefined") return;
2201
+ if (analyticsBlockReason(opts.analytics) !== null) return;
2088
2202
  _initialized = true;
2089
2203
  _client = opts.client;
2090
2204
  _sdkVersion = opts.sdkVersion;
2091
- _reporterTokenHash = opts.reporterTokenHash ?? null;
2205
+ _reporterToken = opts.reporterToken ?? opts.reporterTokenHash ?? null;
2092
2206
  _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
2207
  const onHide = () => {
2103
2208
  if (typeof document !== "undefined" && document.visibilityState === "hidden") {
2104
2209
  send(buildPayload("session_end", { route: currentRoute() }));
@@ -2107,46 +2212,421 @@ function initSessionTracker(opts) {
2107
2212
  if (typeof document !== "undefined") {
2108
2213
  document.addEventListener("visibilitychange", onHide, { passive: true });
2109
2214
  }
2110
- if (typeof window !== "undefined") {
2111
- window.addEventListener("pagehide", () => {
2112
- send(buildPayload("session_end", { route: currentRoute() }));
2113
- }, { passive: true });
2215
+ window.addEventListener("pagehide", () => {
2216
+ send(buildPayload("session_end", { route: currentRoute() }));
2217
+ }, { passive: true });
2218
+ if (opts.patchHistory !== false) {
2219
+ patchHistoryForPageViews();
2114
2220
  }
2115
- patchHistoryForPageViews();
2221
+ _unsubscribeConsent = onAnalyticsConsentChange((state) => {
2222
+ if (state === "granted") activate();
2223
+ else deactivate();
2224
+ });
2225
+ if (resolveAnalyticsConsent(opts.projectId ?? "", opts.analytics) === "granted") activate();
2116
2226
  }
2117
2227
  function trackPageView(route) {
2118
2228
  if (!_initialized || !_client) return;
2119
- _pageViewCount += 1;
2120
- send(buildPayload("page_view", { route: route ?? currentRoute() }));
2229
+ recordPageView(route ?? currentRoute());
2121
2230
  }
2122
2231
  function updateSessionIdentity(userIdHash) {
2123
2232
  _userIdHash = userIdHash;
2124
2233
  }
2125
2234
  function destroySessionTracker() {
2126
- if (_heartbeatTimer != null) {
2127
- clearInterval(_heartbeatTimer);
2128
- _heartbeatTimer = null;
2129
- }
2235
+ deactivate();
2236
+ _unsubscribeConsent?.();
2237
+ _unsubscribeConsent = null;
2130
2238
  _initialized = false;
2239
+ _started = false;
2131
2240
  _client = null;
2132
2241
  }
2133
2242
  var _historyPatched = false;
2134
2243
  function patchHistoryForPageViews() {
2135
2244
  if (_historyPatched || typeof history === "undefined") return;
2136
2245
  _historyPatched = true;
2137
- const wrap = (original) => function(...args) {
2246
+ const wrap = (original, countsAsPageView) => function(...args) {
2138
2247
  const result = original.apply(this, args);
2139
- if (original === history.pushState) {
2140
- _pageViewCount += 1;
2141
- send(buildPayload("page_view", { route: currentRoute() }));
2248
+ if (countsAsPageView) recordPageView(currentRoute());
2249
+ return result;
2250
+ };
2251
+ history.pushState = wrap(history.pushState, true);
2252
+ history.replaceState = wrap(history.replaceState, false);
2253
+ window.addEventListener("popstate", () => {
2254
+ recordPageView(currentRoute());
2255
+ }, { passive: true });
2256
+ }
2257
+
2258
+ // src/analytics-taxonomy.ts
2259
+ var EVENT_NAME_RE = /^[a-z][a-z0-9_]{1,63}$/;
2260
+ var EVENT_PROPERTY_LIMITS = {
2261
+ maxKeys: 40,
2262
+ maxKeyLength: 64,
2263
+ maxValueLength: 256,
2264
+ maxBytes: 8192,
2265
+ /** Client batch size before an immediate flush. */
2266
+ maxClientBatch: 20,
2267
+ /** Server-side per-request ceiling. */
2268
+ maxServerBatch: 50,
2269
+ /** Distinct event names per project before the server returns 422. */
2270
+ maxEventNamesPerProject: 200
2271
+ };
2272
+ var RESERVED_PROPERTY_PREFIX = "$";
2273
+ var PII_PROPERTY_KEY_RE = /(email|phone|password|passwd|token|secret|ssn|address|credit|card|iban)/i;
2274
+ var MUSHI_SURFACES = ["web", "console", "docs", "cli", "mcp", "server", "mobile"];
2275
+ var MUSHI_EVENTS = {
2276
+ // ── Any surface (SDK `analytics.autoPageviews`) ──────────────────────────
2277
+ page_view: { surface: ["web", "docs", "console"], required: [] },
2278
+ // ── Landing + docs (apps/docs) ───────────────────────────────────────────
2279
+ // Every docs route; `route` repeats the reserved `$route` as a host prop so
2280
+ // the requirement is checkable. company_funnel_weekly counts visits from
2281
+ // this and landing_view.
2282
+ docs_page_view: { surface: "docs", required: ["route"] },
2283
+ landing_view: { surface: "docs", required: [] },
2284
+ cta_click: { surface: "docs", required: ["cta_id", "location"] },
2285
+ quickstart_view: { surface: "docs", required: [] },
2286
+ pricing_view: { surface: "docs", required: [] },
2287
+ connect_demo_click: { surface: "docs", required: ["href"] },
2288
+ signup_click: { surface: "docs", required: ["href"] },
2289
+ // ── Console (apps/admin) ─────────────────────────────────────────────────
2290
+ signup_completed: { surface: "console", required: ["signup_source"] },
2291
+ report_opened: { surface: ["console", "mcp"], required: ["report_id"] },
2292
+ fix_context_pulled: { surface: ["console", "mcp"], required: ["report_id"] },
2293
+ fix_dispatched: { surface: ["console", "mcp"], required: ["report_id", "agent"] },
2294
+ invite_sent: { surface: "console", required: [] },
2295
+ upgrade_clicked: { surface: "console", required: ["plan"] },
2296
+ test_report_sent: { surface: "console", required: ["project_id"] },
2297
+ // ── Server-emitted (edge functions) ──────────────────────────────────────
2298
+ project_created: { surface: "server", required: ["project_id"] },
2299
+ key_minted: { surface: "server", required: ["project_id"] },
2300
+ first_report_received: { surface: "server", required: ["project_id"] },
2301
+ fix_merged: { surface: "server", required: ["project_id"] },
2302
+ upgrade_completed: { surface: "server", required: ["project_id", "plan"] },
2303
+ // ── Growth loop (widget "Bug reports by Mushi" mark) ─────────────────────
2304
+ loop_impression: { surface: "web", required: [] },
2305
+ loop_click: { surface: "web", required: [] },
2306
+ loop_signup: { surface: "console", required: ["ref"] }
2307
+ };
2308
+ var MUSHI_EVENT_NAMES = Object.keys(MUSHI_EVENTS);
2309
+ var ACTIVATION_EVENT = "first_report_received";
2310
+ var HABIT_EVENTS = ["report_opened", "fix_context_pulled", "fix_dispatched"];
2311
+ function isValidEventName(name) {
2312
+ return typeof name === "string" && EVENT_NAME_RE.test(name);
2313
+ }
2314
+ function sanitizeEventProperties(input, opts = {}) {
2315
+ const out = {};
2316
+ const dropped = [];
2317
+ if (!input || typeof input !== "object" || Array.isArray(input)) return { properties: out, dropped };
2318
+ const allow = new Set(opts.allowlist ?? []);
2319
+ let keys = 0;
2320
+ for (const [rawKey, rawValue] of Object.entries(input)) {
2321
+ if (keys >= EVENT_PROPERTY_LIMITS.maxKeys) {
2322
+ dropped.push(rawKey);
2323
+ continue;
2324
+ }
2325
+ const key = rawKey.slice(0, EVENT_PROPERTY_LIMITS.maxKeyLength);
2326
+ if (key.startsWith(RESERVED_PROPERTY_PREFIX) && !opts.allowReserved) {
2327
+ dropped.push(key);
2328
+ continue;
2329
+ }
2330
+ if (PII_PROPERTY_KEY_RE.test(key) && !allow.has(key)) {
2331
+ dropped.push(key);
2332
+ continue;
2142
2333
  }
2334
+ let value;
2335
+ if (rawValue === null || typeof rawValue === "boolean") value = rawValue;
2336
+ else if (typeof rawValue === "number") value = Number.isFinite(rawValue) ? rawValue : null;
2337
+ else if (typeof rawValue === "string") {
2338
+ const s = rawValue.slice(0, EVENT_PROPERTY_LIMITS.maxValueLength);
2339
+ value = opts.scrub ? opts.scrub(s) : s;
2340
+ } else if (rawValue instanceof Date) value = rawValue.toISOString();
2341
+ else {
2342
+ dropped.push(key);
2343
+ continue;
2344
+ }
2345
+ out[key] = value;
2346
+ keys += 1;
2347
+ }
2348
+ return { properties: out, dropped };
2349
+ }
2350
+ function propertiesWithinByteLimit(properties) {
2351
+ try {
2352
+ return JSON.stringify(properties).length <= EVENT_PROPERTY_LIMITS.maxBytes;
2353
+ } catch {
2354
+ return false;
2355
+ }
2356
+ }
2357
+
2358
+ // src/event-tracker.ts
2359
+ var _client2 = null;
2360
+ var _projectId = "";
2361
+ var _anonId = null;
2362
+ var _userId = null;
2363
+ var _userTraits = null;
2364
+ var _sdkVersion2;
2365
+ var _config = defaults();
2366
+ var _scrub;
2367
+ var _getSessionId;
2368
+ var _buffer = [];
2369
+ var _consentBuffer = [];
2370
+ var _consent = "granted";
2371
+ var _sampledIn = true;
2372
+ var _flushTimer = null;
2373
+ var _initialized2 = false;
2374
+ var _flushing = false;
2375
+ var _pendingIdentify = false;
2376
+ var _unsubscribeConsent2 = null;
2377
+ var CONSENT_BUFFER_MAX = 50;
2378
+ var SPILL_MAX = 200;
2379
+ var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
2380
+ function defaults() {
2381
+ return {
2382
+ sampleRate: 1,
2383
+ autoPageviews: false,
2384
+ flushIntervalMs: 5e3,
2385
+ surface: "web",
2386
+ propertyAllowlist: []
2387
+ };
2388
+ }
2389
+ function now2() {
2390
+ return (/* @__PURE__ */ new Date()).toISOString();
2391
+ }
2392
+ function spillKey() {
2393
+ return "mushi_events_spill_" + _projectId;
2394
+ }
2395
+ function isRetryableStatus(status) {
2396
+ return status === void 0 || status === 429 || status >= 500;
2397
+ }
2398
+ function analyticsIdKey() {
2399
+ return "mushi_analytics_id_" + _projectId;
2400
+ }
2401
+ function loadOrMintAnalyticsId() {
2402
+ try {
2403
+ const stored = localStorage.getItem(analyticsIdKey());
2404
+ if (stored) return stored;
2405
+ } catch {
2406
+ }
2407
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : "a-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
2408
+ }
2409
+ function persistAnalyticsId() {
2410
+ if (!_anonId) return;
2411
+ try {
2412
+ if (localStorage.getItem(analyticsIdKey()) !== _anonId) localStorage.setItem(analyticsIdKey(), _anonId);
2413
+ } catch {
2414
+ }
2415
+ }
2416
+ function hashToUnit(s) {
2417
+ let h = 2166136261;
2418
+ for (let i = 0; i < s.length; i += 1) {
2419
+ h ^= s.charCodeAt(i);
2420
+ h = Math.imul(h, 16777619) >>> 0;
2421
+ }
2422
+ return (h >>> 0) / 4294967296;
2423
+ }
2424
+ function readSpill() {
2425
+ try {
2426
+ const raw = localStorage.getItem(spillKey());
2427
+ if (!raw) return [];
2428
+ const parsed = JSON.parse(raw);
2429
+ if (!parsed || Date.now() - parsed.at > SPILL_TTL_MS) {
2430
+ localStorage.removeItem(spillKey());
2431
+ return [];
2432
+ }
2433
+ return Array.isArray(parsed.events) ? parsed.events.slice(0, SPILL_MAX) : [];
2434
+ } catch {
2435
+ return [];
2436
+ }
2437
+ }
2438
+ function writeSpill(events) {
2439
+ try {
2440
+ if (events.length === 0) {
2441
+ localStorage.removeItem(spillKey());
2442
+ return;
2443
+ }
2444
+ localStorage.setItem(spillKey(), JSON.stringify({ at: Date.now(), events: events.slice(-SPILL_MAX) }));
2445
+ } catch {
2446
+ }
2447
+ }
2448
+ function takeSpill() {
2449
+ const events = readSpill();
2450
+ if (events.length > 0) writeSpill([]);
2451
+ return events;
2452
+ }
2453
+ function buildPayload2(events) {
2454
+ return {
2455
+ anon_id: _anonId,
2456
+ user_id: _userId,
2457
+ user_traits: _userTraits,
2458
+ session_id: _getSessionId ? _getSessionId() : null,
2459
+ sdk_version: _sdkVersion2,
2460
+ surface: _config.surface,
2461
+ events
2462
+ };
2463
+ }
2464
+ async function flushNow() {
2465
+ if (!_client2 || _flushing || _consent !== "granted") return;
2466
+ if (_buffer.length === 0 && !_pendingIdentify) return;
2467
+ _flushing = true;
2468
+ persistAnalyticsId();
2469
+ const batch = _buffer.splice(0, EVENT_PROPERTY_LIMITS.maxServerBatch);
2470
+ if (_pendingIdentify) {
2471
+ batch.unshift({ name: "identify", ts: now2(), properties: {} });
2472
+ _pendingIdentify = false;
2473
+ }
2474
+ try {
2475
+ const res = await _client2.postProductEvents(buildPayload2(batch));
2476
+ if (res.ok) {
2477
+ writeSpill([]);
2478
+ } else if (isRetryableStatus(res.error?.status)) {
2479
+ writeSpill([...readSpill(), ...batch.filter((e) => e.name !== "identify")]);
2480
+ }
2481
+ } catch {
2482
+ writeSpill([...readSpill(), ...batch.filter((e) => e.name !== "identify")]);
2483
+ } finally {
2484
+ _flushing = false;
2485
+ }
2486
+ if (_buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
2487
+ }
2488
+ function enqueue(ev) {
2489
+ if (_consent === "denied") return false;
2490
+ if (_consent === "pending") {
2491
+ if (_consentBuffer.length >= CONSENT_BUFFER_MAX) return false;
2492
+ _consentBuffer.push(ev);
2493
+ return true;
2494
+ }
2495
+ _buffer.push(ev);
2496
+ if (_buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
2497
+ return true;
2498
+ }
2499
+ function applyConsent(state) {
2500
+ if (state === "denied") {
2501
+ _consent = "denied";
2502
+ _buffer = [];
2503
+ _consentBuffer = [];
2504
+ _pendingIdentify = false;
2505
+ writeSpill([]);
2506
+ return;
2507
+ }
2508
+ const wasPending = _consent === "pending";
2509
+ _consent = "granted";
2510
+ if (wasPending) {
2511
+ _buffer.push(...takeSpill(), ..._consentBuffer);
2512
+ _consentBuffer = [];
2513
+ }
2514
+ if (_buffer.length > 0 || _pendingIdentify) void flushNow();
2515
+ }
2516
+ function stripUndefined(o) {
2517
+ const out = {};
2518
+ for (const [k, v] of Object.entries(o)) {
2519
+ if (v !== void 0) out[k] = v;
2520
+ }
2521
+ return out;
2522
+ }
2523
+ function isEventTrackingActive() {
2524
+ return _initialized2 && _consent === "granted" && _sampledIn;
2525
+ }
2526
+ function getEventAnonymousId() {
2527
+ if (!_initialized2 || _consent === "denied") return null;
2528
+ return _anonId;
2529
+ }
2530
+ function initEventTracker(opts) {
2531
+ if (_initialized2) return;
2532
+ if (typeof window === "undefined") return;
2533
+ const cfg = opts.config ?? {};
2534
+ _config = { ...defaults(), ...stripUndefined(cfg), propertyAllowlist: cfg.propertyAllowlist ?? [] };
2535
+ _projectId = opts.projectId;
2536
+ if (analyticsBlockReason(cfg) !== null) return;
2537
+ _initialized2 = true;
2538
+ _client2 = opts.client;
2539
+ _anonId = opts.anonId ?? loadOrMintAnalyticsId();
2540
+ _userId = opts.userId ?? null;
2541
+ _sdkVersion2 = opts.sdkVersion;
2542
+ _scrub = opts.scrub;
2543
+ _getSessionId = opts.getSessionId;
2544
+ const rate = Math.min(1, Math.max(0, _config.sampleRate));
2545
+ _sampledIn = rate >= 1 ? true : rate <= 0 ? false : hashToUnit(_anonId ?? "anon") < rate;
2546
+ if (!_sampledIn) return;
2547
+ _consent = resolveAnalyticsConsent(_projectId, cfg);
2548
+ if (_consent === "granted") _buffer.push(...takeSpill());
2549
+ else if (_consent === "denied") writeSpill([]);
2550
+ _unsubscribeConsent2 = onAnalyticsConsentChange(applyConsent);
2551
+ const interval = Math.max(1e3, _config.flushIntervalMs);
2552
+ _flushTimer = setInterval(() => {
2553
+ void flushNow();
2554
+ }, interval);
2555
+ const onHide = () => {
2556
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") void flushNow();
2557
+ };
2558
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onHide, { passive: true });
2559
+ window.addEventListener("pagehide", () => {
2560
+ void flushNow();
2561
+ }, { passive: true });
2562
+ if (_config.autoPageviews) patchHistory();
2563
+ }
2564
+ function trackEvent(name, properties, opts) {
2565
+ if (!_initialized2 || !_sampledIn) return false;
2566
+ if (!isValidEventName(name)) return false;
2567
+ const { properties: props } = sanitizeEventProperties(properties, {
2568
+ allowlist: _config.propertyAllowlist,
2569
+ scrub: _scrub
2570
+ });
2571
+ let merged = props;
2572
+ if (opts?.reserved) {
2573
+ const { properties: res } = sanitizeEventProperties(opts.reserved, { allowReserved: true, scrub: _scrub });
2574
+ merged = { ...props, ...res };
2575
+ }
2576
+ merged.$surface = _config.surface;
2577
+ if (typeof location !== "undefined" && merged.$route === void 0) merged.$route = location.pathname;
2578
+ if (!propertiesWithinByteLimit(merged)) return false;
2579
+ return enqueue({
2580
+ name,
2581
+ ts: opts?.ts ?? now2(),
2582
+ properties: merged,
2583
+ ...opts?.dedupKey ? { dedup_key: opts.dedupKey } : {}
2584
+ });
2585
+ }
2586
+ function setEventConsent(state) {
2587
+ if (!_initialized2) _consent = state;
2588
+ setAnalyticsConsent(_projectId, state);
2589
+ }
2590
+ function updateEventIdentity(userId, traits) {
2591
+ _userId = userId;
2592
+ _userTraits = traits ?? null;
2593
+ if (!_initialized2 || !_sampledIn || !userId || _consent === "denied") return;
2594
+ _pendingIdentify = true;
2595
+ if (_consent === "granted") void flushNow();
2596
+ }
2597
+ function flushEvents() {
2598
+ return flushNow();
2599
+ }
2600
+ function destroyEventTracker() {
2601
+ if (_flushTimer != null) {
2602
+ clearInterval(_flushTimer);
2603
+ _flushTimer = null;
2604
+ }
2605
+ _unsubscribeConsent2?.();
2606
+ _unsubscribeConsent2 = null;
2607
+ _initialized2 = false;
2608
+ _client2 = null;
2609
+ _buffer = [];
2610
+ _consentBuffer = [];
2611
+ _consent = "granted";
2612
+ _sampledIn = true;
2613
+ _pendingIdentify = false;
2614
+ _userId = null;
2615
+ _userTraits = null;
2616
+ _config = defaults();
2617
+ }
2618
+ var _historyPatched2 = false;
2619
+ function patchHistory() {
2620
+ if (_historyPatched2 || typeof history === "undefined") return;
2621
+ _historyPatched2 = true;
2622
+ const original = history.pushState;
2623
+ history.pushState = function(...args) {
2624
+ const result = original.apply(this, args);
2625
+ trackEvent("page_view");
2143
2626
  return result;
2144
2627
  };
2145
- history.pushState = wrap(history.pushState);
2146
- history.replaceState = wrap(history.replaceState);
2147
2628
  window.addEventListener("popstate", () => {
2148
- _pageViewCount += 1;
2149
- send(buildPayload("page_view", { route: currentRoute() }));
2629
+ trackEvent("page_view");
2150
2630
  }, { passive: true });
2151
2631
  }
2152
2632
 
@@ -2163,12 +2643,12 @@ function createRateLimiter(config = {}) {
2163
2643
  let tokens = maxBurst;
2164
2644
  let lastRefill = Date.now();
2165
2645
  function refill() {
2166
- const now2 = Date.now();
2167
- const elapsed = now2 - lastRefill;
2646
+ const now3 = Date.now();
2647
+ const elapsed = now3 - lastRefill;
2168
2648
  const refills = Math.floor(elapsed / refillIntervalMs);
2169
2649
  if (refills > 0) {
2170
2650
  tokens = Math.min(maxBurst, tokens + refills * refillRate);
2171
- lastRefill = now2;
2651
+ lastRefill = now3;
2172
2652
  }
2173
2653
  }
2174
2654
  function tryConsume() {
@@ -2566,15 +3046,15 @@ function mushiTokens(mode) {
2566
3046
  // src/identity.ts
2567
3047
  var MUSHI_IDENTITY_TOKEN_PREFIX = "mushi.identity";
2568
3048
  function buildIdentityClaims(input) {
2569
- const now2 = Math.floor(Date.now() / 1e3);
3049
+ const now3 = Math.floor(Date.now() / 1e3);
2570
3050
  const ttl = input.ttlSeconds && input.ttlSeconds > 0 ? Math.min(input.ttlSeconds, 3600) : 600;
2571
3051
  return {
2572
3052
  projectId: input.projectId,
2573
3053
  sub: input.userId,
2574
3054
  ...input.email ? { email: input.email } : {},
2575
3055
  ...input.name ? { name: input.name } : {},
2576
- iat: now2,
2577
- exp: now2 + ttl
3056
+ iat: now3,
3057
+ exp: now3 + ttl
2578
3058
  };
2579
3059
  }
2580
3060
  function base64UrlDecode(segment) {
@@ -2801,6 +3281,6 @@ function isSafeFaviconHref(href) {
2801
3281
  }
2802
3282
  }
2803
3283
 
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 };
3284
+ 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
3285
  //# sourceMappingURL=index.js.map
2806
3286
  //# sourceMappingURL=index.js.map