@mushi-mushi/core 1.22.5 → 1.25.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
@@ -358,6 +358,17 @@ function createApiClient(options) {
358
358
  );
359
359
  },
360
360
  // ─── Rewards program (P1) ──────────────────────────────────
361
+ /** POST /v1/sdk/session — lightweight session lifecycle events.
362
+ * Best-effort (1 retry, no offline queue) — mirrors postDiscoveryEvent. */
363
+ async postSessionEvent(payload) {
364
+ return request(
365
+ "POST",
366
+ "/v1/sdk/session",
367
+ payload,
368
+ 1,
369
+ "discovery"
370
+ );
371
+ },
361
372
  async submitActivity(userId, events, opts) {
362
373
  return request(
363
374
  "POST",
@@ -599,6 +610,219 @@ function resolveEnvConfig() {
599
610
  if (apiEndpoint) result.apiEndpoint = apiEndpoint;
600
611
  return result;
601
612
  }
613
+ var ENV_PREFIXES = ["VITE_", "NEXT_PUBLIC_", "NUXT_PUBLIC_", "EXPO_PUBLIC_", ""];
614
+ var FIELD_SUFFIX = {
615
+ projectId: "MUSHI_PROJECT_ID",
616
+ apiKey: "MUSHI_API_KEY"
617
+ };
618
+ function diagnoseEnvConfig() {
619
+ const resolved = resolveEnvConfig();
620
+ const missing = [];
621
+ if (!resolved.projectId) missing.push("projectId");
622
+ if (!resolved.apiKey) missing.push("apiKey");
623
+ if (missing.length === 0) {
624
+ return { resolved, missing, nearMisses: [], message: null };
625
+ }
626
+ const importMetaEnv = (() => {
627
+ try {
628
+ const env = import.meta.env;
629
+ return typeof env === "object" && env !== null ? env : null;
630
+ } catch {
631
+ return null;
632
+ }
633
+ })();
634
+ const processEnv = (() => {
635
+ try {
636
+ return typeof process !== "undefined" && process.env ? process.env : null;
637
+ } catch {
638
+ return null;
639
+ }
640
+ })();
641
+ const nearMisses = [];
642
+ for (const field of missing) {
643
+ for (const prefix of ENV_PREFIXES) {
644
+ const key = `${prefix}${FIELD_SUFFIX[field]}`;
645
+ if (importMetaEnv && importMetaEnv[key]) {
646
+ nearMisses.push({ field, key, via: "import.meta.env" });
647
+ } else if (processEnv && processEnv[key]) {
648
+ nearMisses.push({ field, key, via: "process.env" });
649
+ }
650
+ }
651
+ }
652
+ const expectedPrefix = importMetaEnv ? "VITE_MUSHI_*" : "NEXT_PUBLIC_MUSHI_* (Next.js), EXPO_PUBLIC_MUSHI_* (Expo), or MUSHI_* (Node / React Native)";
653
+ const lines = [`[mushi] Missing ${missing.join(" and ")} \u2014 the SDK will not initialize.`];
654
+ for (const m of nearMisses) {
655
+ lines.push(
656
+ `[mushi] Found ${m.key} in ${m.via}, but this runtime did not resolve it \u2014 it is either the wrong prefix for your bundler or not exposed to the client bundle.`
657
+ );
658
+ }
659
+ lines.push(`[mushi] This runtime reads: ${expectedPrefix}. Set the matching variable(s) and restart your dev server.`);
660
+ if (nearMisses.length === 0) {
661
+ lines.push(
662
+ `[mushi] If you already set the variable, check the prefix: a mis-prefixed var is stripped from client bundles and resolves to undefined with no error.`
663
+ );
664
+ }
665
+ return { resolved, missing, nearMisses, message: lines.join("\n") };
666
+ }
667
+
668
+ // src/presets.ts
669
+ var KNOWN_PRESETS = [
670
+ "production-calm",
671
+ "beta-loud",
672
+ "internal-debug",
673
+ "manual-only",
674
+ "minimal",
675
+ "standard",
676
+ "full"
677
+ ];
678
+ var KNOWN_CONFIG_KEYS = [
679
+ "projectId",
680
+ "apiKey",
681
+ "apiEndpoint",
682
+ "timeout",
683
+ "maxRetries",
684
+ "circuitBreaker",
685
+ "preset",
686
+ "runtimeConfig",
687
+ "sentry",
688
+ "widget",
689
+ "capture",
690
+ "privacy",
691
+ "proactive",
692
+ "preFilter",
693
+ "integrations",
694
+ "offline",
695
+ "rewards",
696
+ "assistant",
697
+ "debug",
698
+ "enabled",
699
+ "appVersion",
700
+ "beforeSendFeedback",
701
+ "onCrashedLastRun"
702
+ ];
703
+ function presetDefaults(preset) {
704
+ switch (preset) {
705
+ case "manual-only":
706
+ return {
707
+ widget: { trigger: "manual", outdatedBanner: "console-only" },
708
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
709
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
710
+ };
711
+ case "beta-loud":
712
+ return {
713
+ widget: { trigger: "auto", outdatedBanner: "banner" },
714
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
715
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
716
+ };
717
+ case "internal-debug":
718
+ return {
719
+ widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
720
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
721
+ proactive: {
722
+ rageClick: true,
723
+ longTask: true,
724
+ apiCascade: true,
725
+ errorBoundary: true,
726
+ cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
727
+ }
728
+ };
729
+ case "production-calm":
730
+ return {
731
+ widget: { trigger: "auto", outdatedBanner: "console-only" },
732
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
733
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
734
+ };
735
+ // Lean posture: widget on, console capture only, screenshot on-report,
736
+ // no network/performance/replay, no proactive nudges.
737
+ case "minimal":
738
+ return {
739
+ widget: { trigger: "auto" },
740
+ capture: {
741
+ console: true,
742
+ network: false,
743
+ performance: false,
744
+ screenshot: "on-report",
745
+ replay: "off",
746
+ elementSelector: false
747
+ },
748
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
749
+ };
750
+ // Everything-on posture: full capture (incl. self-contained `lite` replay)
751
+ // and every proactive trigger.
752
+ case "full":
753
+ return {
754
+ widget: { trigger: "auto" },
755
+ capture: {
756
+ console: true,
757
+ network: true,
758
+ performance: true,
759
+ screenshot: "auto",
760
+ replay: "lite",
761
+ elementSelector: true
762
+ },
763
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
764
+ };
765
+ // 'standard' and anything unrecognised → no expansion.
766
+ default:
767
+ return void 0;
768
+ }
769
+ }
770
+ function expandPreset(config) {
771
+ const defaults = presetDefaults(config.preset);
772
+ if (!defaults) return config;
773
+ return {
774
+ ...config,
775
+ widget: {
776
+ ...defaults.widget,
777
+ ...config.widget
778
+ },
779
+ capture: {
780
+ ...defaults.capture,
781
+ ...config.capture
782
+ },
783
+ proactive: {
784
+ ...defaults.proactive,
785
+ ...config.proactive,
786
+ cooldown: {
787
+ ...defaults.proactive?.cooldown,
788
+ ...config.proactive?.cooldown
789
+ }
790
+ }
791
+ };
792
+ }
793
+ function isSilenced() {
794
+ const truthy = (v) => v === "1" || v === "true";
795
+ try {
796
+ if (typeof process !== "undefined" && process.env && truthy(process.env.MUSHI_SILENT)) return true;
797
+ } catch {
798
+ }
799
+ try {
800
+ const env = import.meta.env;
801
+ if (env && typeof env === "object" && (truthy(env.MUSHI_SILENT) || truthy(env.VITE_MUSHI_SILENT))) return true;
802
+ } catch {
803
+ }
804
+ return false;
805
+ }
806
+ function validateConfig(config) {
807
+ try {
808
+ if (isSilenced()) return;
809
+ if (!config || typeof config !== "object") return;
810
+ const unknownKeys = Object.keys(config).filter((key) => !KNOWN_CONFIG_KEYS.includes(key));
811
+ if (unknownKeys.length > 0) {
812
+ console.error(
813
+ `[mushi] Unknown config key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.map((k) => `\`${k}\``).join(", ")} \u2014 check for typos (see MushiConfig). Ignored.`
814
+ );
815
+ }
816
+ if (config.preset !== void 0 && !KNOWN_PRESETS.includes(config.preset)) {
817
+ console.error(
818
+ `[mushi] Unknown preset \`${String(config.preset)}\` \u2014 expected one of ${KNOWN_PRESETS.map(
819
+ (p) => `'${p}'`
820
+ ).join(", ")}. Preset ignored.`
821
+ );
822
+ }
823
+ } catch {
824
+ }
825
+ }
602
826
 
603
827
  // src/pre-filter.ts
604
828
  var DEFAULT_MIN_LENGTH = 10;
@@ -954,12 +1178,12 @@ function createOfflineQueue(config = {}) {
954
1178
  const n = row.attempts;
955
1179
  return typeof n === "number" && Number.isFinite(n) ? n : 0;
956
1180
  }
957
- function isExpired(row, now) {
1181
+ function isExpired(row, now2) {
958
1182
  const queuedAt = row.queuedAt;
959
1183
  if (!queuedAt) return false;
960
1184
  const ts = Date.parse(queuedAt);
961
1185
  if (Number.isNaN(ts)) return false;
962
- return now - ts > MAX_QUEUE_AGE_MS;
1186
+ return now2 - ts > MAX_QUEUE_AGE_MS;
963
1187
  }
964
1188
  function detectBackend() {
965
1189
  if (backendType) return backendType;
@@ -1138,10 +1362,10 @@ function createOfflineQueue(config = {}) {
1138
1362
  } else {
1139
1363
  rows = lsRead();
1140
1364
  }
1141
- const now = Date.now();
1365
+ const now2 = Date.now();
1142
1366
  const fresh = [];
1143
1367
  for (const row of rows) {
1144
- if (isExpired(row, now)) {
1368
+ if (isExpired(row, now2)) {
1145
1369
  const rowId = row.id;
1146
1370
  await removeRow(backend, rowId);
1147
1371
  queueLog.debug("Offline queue: evicting stale report", { id: rowId });
@@ -1606,6 +1830,111 @@ function randomSuffix() {
1606
1830
  return n.toString(36).padStart(6, "0").slice(0, 6);
1607
1831
  }
1608
1832
 
1833
+ // src/session-tracker.ts
1834
+ var HEARTBEAT_INTERVAL_MS = 6e4;
1835
+ var _client = null;
1836
+ var _sdkVersion;
1837
+ var _userIdHash = null;
1838
+ var _reporterTokenHash = null;
1839
+ var _pageViewCount = 0;
1840
+ var _heartbeatTimer = null;
1841
+ var _initialized = false;
1842
+ function now() {
1843
+ return (/* @__PURE__ */ new Date()).toISOString();
1844
+ }
1845
+ function currentRoute() {
1846
+ if (typeof window === "undefined") return "";
1847
+ return window.location.pathname + window.location.search;
1848
+ }
1849
+ function userAgent() {
1850
+ if (typeof navigator === "undefined") return null;
1851
+ return navigator.userAgent ?? null;
1852
+ }
1853
+ function buildPayload(kind, extra) {
1854
+ return {
1855
+ kind,
1856
+ session_id: getSessionId(),
1857
+ ts: now(),
1858
+ page_view_count: _pageViewCount,
1859
+ reporter_token_hash: _reporterTokenHash,
1860
+ user_id_hash: _userIdHash,
1861
+ user_agent: userAgent(),
1862
+ sdk_version: _sdkVersion,
1863
+ ...extra
1864
+ };
1865
+ }
1866
+ function send(payload) {
1867
+ if (!_client) return;
1868
+ _client.postSessionEvent(payload).catch(() => {
1869
+ });
1870
+ }
1871
+ function initSessionTracker(opts) {
1872
+ if (_initialized || typeof window === "undefined") return;
1873
+ _initialized = true;
1874
+ _client = opts.client;
1875
+ _sdkVersion = opts.sdkVersion;
1876
+ _reporterTokenHash = opts.reporterTokenHash ?? null;
1877
+ _userIdHash = opts.userIdHash ?? null;
1878
+ const entryRoute = currentRoute();
1879
+ _pageViewCount = 1;
1880
+ send(buildPayload("session_start", {
1881
+ route: entryRoute,
1882
+ referrer: typeof document !== "undefined" ? document.referrer ?? null : null
1883
+ }));
1884
+ _heartbeatTimer = setInterval(() => {
1885
+ send(buildPayload("session_heartbeat", { route: currentRoute() }));
1886
+ }, HEARTBEAT_INTERVAL_MS);
1887
+ const onHide = () => {
1888
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1889
+ send(buildPayload("session_end", { route: currentRoute() }));
1890
+ }
1891
+ };
1892
+ if (typeof document !== "undefined") {
1893
+ document.addEventListener("visibilitychange", onHide, { passive: true });
1894
+ }
1895
+ if (typeof window !== "undefined") {
1896
+ window.addEventListener("pagehide", () => {
1897
+ send(buildPayload("session_end", { route: currentRoute() }));
1898
+ }, { passive: true });
1899
+ }
1900
+ patchHistoryForPageViews();
1901
+ }
1902
+ function trackPageView(route) {
1903
+ if (!_initialized || !_client) return;
1904
+ _pageViewCount += 1;
1905
+ send(buildPayload("page_view", { route: route ?? currentRoute() }));
1906
+ }
1907
+ function updateSessionIdentity(userIdHash) {
1908
+ _userIdHash = userIdHash;
1909
+ }
1910
+ function destroySessionTracker() {
1911
+ if (_heartbeatTimer != null) {
1912
+ clearInterval(_heartbeatTimer);
1913
+ _heartbeatTimer = null;
1914
+ }
1915
+ _initialized = false;
1916
+ _client = null;
1917
+ }
1918
+ var _historyPatched = false;
1919
+ function patchHistoryForPageViews() {
1920
+ if (_historyPatched || typeof history === "undefined") return;
1921
+ _historyPatched = true;
1922
+ const wrap = (original) => function(...args) {
1923
+ const result = original.apply(this, args);
1924
+ if (original === history.pushState) {
1925
+ _pageViewCount += 1;
1926
+ send(buildPayload("page_view", { route: currentRoute() }));
1927
+ }
1928
+ return result;
1929
+ };
1930
+ history.pushState = wrap(history.pushState);
1931
+ history.replaceState = wrap(history.replaceState);
1932
+ window.addEventListener("popstate", () => {
1933
+ _pageViewCount += 1;
1934
+ send(buildPayload("page_view", { route: currentRoute() }));
1935
+ }, { passive: true });
1936
+ }
1937
+
1609
1938
  // src/rate-limiter.ts
1610
1939
  var DEFAULT_MAX_BURST = 10;
1611
1940
  var DEFAULT_REFILL_RATE = 1;
@@ -1619,12 +1948,12 @@ function createRateLimiter(config = {}) {
1619
1948
  let tokens = maxBurst;
1620
1949
  let lastRefill = Date.now();
1621
1950
  function refill() {
1622
- const now = Date.now();
1623
- const elapsed = now - lastRefill;
1951
+ const now2 = Date.now();
1952
+ const elapsed = now2 - lastRefill;
1624
1953
  const refills = Math.floor(elapsed / refillIntervalMs);
1625
1954
  if (refills > 0) {
1626
1955
  tokens = Math.min(maxBurst, tokens + refills * refillRate);
1627
- lastRefill = now;
1956
+ lastRefill = now2;
1628
1957
  }
1629
1958
  }
1630
1959
  function tryConsume() {
@@ -1938,15 +2267,15 @@ function mushiTokens(mode) {
1938
2267
  // src/identity.ts
1939
2268
  var MUSHI_IDENTITY_TOKEN_PREFIX = "mushi.identity";
1940
2269
  function buildIdentityClaims(input) {
1941
- const now = Math.floor(Date.now() / 1e3);
2270
+ const now2 = Math.floor(Date.now() / 1e3);
1942
2271
  const ttl = input.ttlSeconds && input.ttlSeconds > 0 ? Math.min(input.ttlSeconds, 3600) : 600;
1943
2272
  return {
1944
2273
  projectId: input.projectId,
1945
2274
  sub: input.userId,
1946
2275
  ...input.email ? { email: input.email } : {},
1947
2276
  ...input.name ? { name: input.name } : {},
1948
- iat: now,
1949
- exp: now + ttl
2277
+ iat: now2,
2278
+ exp: now2 + ttl
1950
2279
  };
1951
2280
  }
1952
2281
  function base64UrlDecode(segment) {
@@ -1985,12 +2314,12 @@ var MULTI_TENANT_PARENT_HOSTS = /* @__PURE__ */ new Set(["kensaur.us", "www.kens
1985
2314
  var SLUG_DOMAIN_HINTS = {
1986
2315
  "glot-it": "glot.it",
1987
2316
  glotit: "glot.it",
1988
- "solo-boss-cloud": "sbc-front.vercel.app",
2317
+ "solo-boss-cloud": "solo-boss.kensaur.us",
1989
2318
  "mushi-mushi": "kensaur.us"
1990
2319
  };
1991
2320
  var SLUG_ICON_URL_HINTS = {
1992
2321
  "mushi-mushi": "https://kensaur.us/mushi-mushi/admin/favicon.svg",
1993
- "solo-boss-cloud": "https://sbc-front.vercel.app/favicon.svg",
2322
+ "solo-boss-cloud": "https://solo-boss.kensaur.us/favicon.svg",
1994
2323
  "glot-it": "https://kensaur.us/glot-it/apple-touch-icon.png",
1995
2324
  glotit: "https://kensaur.us/glot-it/apple-touch-icon.png"
1996
2325
  };
@@ -2164,6 +2493,6 @@ function isSafeFaviconHref(href) {
2164
2493
  }
2165
2494
  }
2166
2495
 
2167
- 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_COLORS_DARK, MUSHI_COLORS_LIGHT, MUSHI_COPY, MUSHI_GEOMETRY, MUSHI_IDENTITY_TOKEN_PREFIX, MUSHI_INTERNAL_HEADER, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INVERSE, MUSHI_MOTION, MUSHI_ON_ACCENT, MUSHI_PROJECT_HEADER, MUSHI_RADIUS, MUSHI_REPORTER_STATUS, MUSHI_SDK_PACKAGE_HEADER, MUSHI_SDK_VERSION_HEADER, MUSHI_SPACING, MUSHI_TIER_COLORS, MUSHI_TYPE, MUSHI_USER_TOKEN_HEADER, MUSHI_Z, REGION_ENDPOINTS, buildIdentityClaims, buildSdkIngestHeaders, captureEnvironment, checkReportPayloadSize, createApiClient, createBreadcrumbBuffer, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, estimateJsonBytes, faviconUrlCandidates, formatBytes, getDeviceFingerprintHash, getReporterToken, getSessionId, githubRepoDomainHint, hmacSha256Hex, isLikelyGenericFavicon, isUntrustedFaviconUrl, mushiPalette, mushiTokens, newUuid, noopLogger, normaliseThrown, originToDomain, parseIdentityToken, projectFaviconUrlCandidates, projectInitials, projectInitialsThemeIndex, readPageFaviconHref, resolveEnvConfig, resolveProjectDomain, resolveProjectFaviconDomains, resolveRegionEndpoint, resolveWidgetAccent, safeWidgetHex, scrubPii, sha256Hex };
2496
+ 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_COLORS_DARK, MUSHI_COLORS_LIGHT, MUSHI_COPY, MUSHI_GEOMETRY, MUSHI_IDENTITY_TOKEN_PREFIX, MUSHI_INTERNAL_HEADER, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INVERSE, MUSHI_MOTION, MUSHI_ON_ACCENT, MUSHI_PROJECT_HEADER, MUSHI_RADIUS, MUSHI_REPORTER_STATUS, MUSHI_SDK_PACKAGE_HEADER, MUSHI_SDK_VERSION_HEADER, MUSHI_SPACING, MUSHI_TIER_COLORS, MUSHI_TYPE, MUSHI_USER_TOKEN_HEADER, MUSHI_Z, REGION_ENDPOINTS, buildIdentityClaims, buildSdkIngestHeaders, captureEnvironment, checkReportPayloadSize, createApiClient, createBreadcrumbBuffer, createLogger, createOfflineQueue, createPiiScrubber, createPreFilter, createRateLimiter, destroySessionTracker, diagnoseEnvConfig, estimateJsonBytes, expandPreset, faviconUrlCandidates, formatBytes, getDeviceFingerprintHash, getReporterToken, getSessionId, githubRepoDomainHint, hmacSha256Hex, initSessionTracker, isLikelyGenericFavicon, isUntrustedFaviconUrl, mushiPalette, mushiTokens, newUuid, noopLogger, normaliseThrown, originToDomain, parseIdentityToken, projectFaviconUrlCandidates, projectInitials, projectInitialsThemeIndex, readPageFaviconHref, resolveEnvConfig, resolveProjectDomain, resolveProjectFaviconDomains, resolveRegionEndpoint, resolveWidgetAccent, safeWidgetHex, scrubPii, sha256Hex, trackPageView, updateSessionIdentity, validateConfig };
2168
2497
  //# sourceMappingURL=index.js.map
2169
2498
  //# sourceMappingURL=index.js.map