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