@mushi-mushi/core 1.22.5 → 1.23.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/README.md CHANGED
@@ -23,7 +23,7 @@ Core types, API client, and shared utilities for every Mushi SDK package.
23
23
 
24
24
  | Type | Purpose |
25
25
  |----------------------------|-----------------------------------------------------------------------------------------------|
26
- | `MushiPreset` | `'production-calm' \| 'beta-loud' \| 'internal-debug' \| 'manual-only'` posture bundles. |
26
+ | `MushiPreset` | Posture bundles: `'production-calm' \| 'beta-loud' \| 'internal-debug' \| 'manual-only'` plus the tiers `'minimal' \| 'standard' \| 'full'`. See [Config presets](#config-presets--precedence). |
27
27
  | `MushiWidgetAnchor` | Raw-CSS positioning (`top` / `right` / `bottom` / `left`) for the widget launcher. |
28
28
  | `MushiPrivacyConfig` | `maskSelectors`, `blockSelectors`, `allowUserRemoveScreenshot` for screenshot redaction. |
29
29
  | `MushiUrlMatcher` | `string \| RegExp` element used by `capture.ignoreUrls` and `apiCascade.ignoreUrls`. |
@@ -46,6 +46,61 @@ Constants: `MUSHI_INTERNAL_HEADER` (`'X-Mushi-Internal'`),
46
46
  `MUSHI_INTERNAL_INIT_MARKER`, and the `MushiInternalRequestKind` literal union
47
47
  are re-exported so framework adapters can build their own self-noise filters.
48
48
 
49
+ ## Config presets & precedence
50
+
51
+ Pass `preset` to pick a posture bundle instead of hand-assembling
52
+ `widget` / `capture` / `proactive` flags. `expandPreset(config)` expands the
53
+ preset into those nested objects; **your explicit config always wins** on every
54
+ key (the merge is per-sub-object, so overriding one `capture` flag keeps the
55
+ rest of the preset's defaults).
56
+
57
+ | Field | `minimal` | `standard` | `full` |
58
+ |----------------------|----------------|--------------------|------------------|
59
+ | Widget | on (`auto`) | on (SDK default) | on (`auto`) |
60
+ | `capture.console` | ✅ | default | ✅ |
61
+ | `capture.network` | ❌ | default | ✅ |
62
+ | `capture.performance`| ❌ | default | ✅ |
63
+ | `capture.screenshot` | `on-report` | default | `auto` |
64
+ | `capture.replay` | `off` | default | `lite` |
65
+ | Proactive triggers | none | default | all on |
66
+
67
+ - **`minimal`** — widget + console capture only, screenshot on report; no
68
+ network/performance/replay, no proactive nudges. Leanest footprint.
69
+ - **`standard`** — today's SDK defaults. Expansion is a **no-op** (the config
70
+ is returned untouched); the preset exists so the intent is explicit and
71
+ documented.
72
+ - **`full`** — every capture channel on (including a self-contained `lite`
73
+ session replay) and all proactive triggers. Loudest / most data.
74
+
75
+ The four legacy postures — `production-calm`, `beta-loud`, `internal-debug`,
76
+ `manual-only` — are still supported and expand the same way.
77
+
78
+ ### Precedence
79
+
80
+ Highest wins. `resolveEnvConfig()` only ever supplies
81
+ `projectId` / `apiKey` / `apiEndpoint`, so it never competes with a preset for
82
+ the nested option objects.
83
+
84
+ | Priority | Source | Example |
85
+ |----------|-------------------------------------|-------------------------------------------|
86
+ | 1 (wins) | Explicit config | `capture: { network: true }` |
87
+ | 2 | `preset` | `preset: 'minimal'` → `network: false` |
88
+ | 3 | Environment variables | `NEXT_PUBLIC_MUSHI_*` / `VITE_MUSHI_*` |
89
+ | 4 | SDK defaults | built-in fallbacks |
90
+
91
+ ### Validation
92
+
93
+ `validateConfig(config)` runs at init and **fails loud but never throws**: it
94
+ `console.error`s on unknown top-level keys (usually a typo) and on an invalid
95
+ `preset` value, then continues. Set `MUSHI_SILENT=1` to suppress the warnings.
96
+
97
+ ```typescript
98
+ import { expandPreset, validateConfig } from '@mushi-mushi/core';
99
+
100
+ validateConfig(config); // warns on typos / bad preset (never throws)
101
+ const resolved = expandPreset(config); // preset → nested objects, explicit wins
102
+ ```
103
+
49
104
  ## Usage
50
105
 
51
106
  ```typescript
package/dist/index.cjs CHANGED
@@ -601,6 +601,219 @@ function resolveEnvConfig() {
601
601
  if (apiEndpoint) result.apiEndpoint = apiEndpoint;
602
602
  return result;
603
603
  }
604
+ var ENV_PREFIXES = ["VITE_", "NEXT_PUBLIC_", "NUXT_PUBLIC_", "EXPO_PUBLIC_", ""];
605
+ var FIELD_SUFFIX = {
606
+ projectId: "MUSHI_PROJECT_ID",
607
+ apiKey: "MUSHI_API_KEY"
608
+ };
609
+ function diagnoseEnvConfig() {
610
+ const resolved = resolveEnvConfig();
611
+ const missing = [];
612
+ if (!resolved.projectId) missing.push("projectId");
613
+ if (!resolved.apiKey) missing.push("apiKey");
614
+ if (missing.length === 0) {
615
+ return { resolved, missing, nearMisses: [], message: null };
616
+ }
617
+ const importMetaEnv = (() => {
618
+ try {
619
+ const env = undefined;
620
+ return typeof env === "object" && env !== null ? env : null;
621
+ } catch {
622
+ return null;
623
+ }
624
+ })();
625
+ const processEnv = (() => {
626
+ try {
627
+ return typeof process !== "undefined" && process.env ? process.env : null;
628
+ } catch {
629
+ return null;
630
+ }
631
+ })();
632
+ const nearMisses = [];
633
+ for (const field of missing) {
634
+ for (const prefix of ENV_PREFIXES) {
635
+ const key = `${prefix}${FIELD_SUFFIX[field]}`;
636
+ if (importMetaEnv && importMetaEnv[key]) {
637
+ nearMisses.push({ field, key, via: "import.meta.env" });
638
+ } else if (processEnv && processEnv[key]) {
639
+ nearMisses.push({ field, key, via: "process.env" });
640
+ }
641
+ }
642
+ }
643
+ const expectedPrefix = importMetaEnv ? "VITE_MUSHI_*" : "NEXT_PUBLIC_MUSHI_* (Next.js), EXPO_PUBLIC_MUSHI_* (Expo), or MUSHI_* (Node / React Native)";
644
+ const lines = [`[mushi] Missing ${missing.join(" and ")} \u2014 the SDK will not initialize.`];
645
+ for (const m of nearMisses) {
646
+ lines.push(
647
+ `[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.`
648
+ );
649
+ }
650
+ lines.push(`[mushi] This runtime reads: ${expectedPrefix}. Set the matching variable(s) and restart your dev server.`);
651
+ if (nearMisses.length === 0) {
652
+ lines.push(
653
+ `[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.`
654
+ );
655
+ }
656
+ return { resolved, missing, nearMisses, message: lines.join("\n") };
657
+ }
658
+
659
+ // src/presets.ts
660
+ var KNOWN_PRESETS = [
661
+ "production-calm",
662
+ "beta-loud",
663
+ "internal-debug",
664
+ "manual-only",
665
+ "minimal",
666
+ "standard",
667
+ "full"
668
+ ];
669
+ var KNOWN_CONFIG_KEYS = [
670
+ "projectId",
671
+ "apiKey",
672
+ "apiEndpoint",
673
+ "timeout",
674
+ "maxRetries",
675
+ "circuitBreaker",
676
+ "preset",
677
+ "runtimeConfig",
678
+ "sentry",
679
+ "widget",
680
+ "capture",
681
+ "privacy",
682
+ "proactive",
683
+ "preFilter",
684
+ "integrations",
685
+ "offline",
686
+ "rewards",
687
+ "assistant",
688
+ "debug",
689
+ "enabled",
690
+ "appVersion",
691
+ "beforeSendFeedback",
692
+ "onCrashedLastRun"
693
+ ];
694
+ function presetDefaults(preset) {
695
+ switch (preset) {
696
+ case "manual-only":
697
+ return {
698
+ widget: { trigger: "manual", outdatedBanner: "console-only" },
699
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
700
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
701
+ };
702
+ case "beta-loud":
703
+ return {
704
+ widget: { trigger: "auto", outdatedBanner: "banner" },
705
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
706
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
707
+ };
708
+ case "internal-debug":
709
+ return {
710
+ widget: { trigger: "auto", outdatedBanner: "banner", brandFooter: true },
711
+ capture: { console: true, network: true, performance: true, screenshot: "auto", elementSelector: true },
712
+ proactive: {
713
+ rageClick: true,
714
+ longTask: true,
715
+ apiCascade: true,
716
+ errorBoundary: true,
717
+ cooldown: { maxProactivePerSession: 10, dismissCooldownHours: 0, suppressAfterDismissals: 99 }
718
+ }
719
+ };
720
+ case "production-calm":
721
+ return {
722
+ widget: { trigger: "auto", outdatedBanner: "console-only" },
723
+ capture: { console: true, network: true, performance: false, screenshot: "on-report", elementSelector: false },
724
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
725
+ };
726
+ // Lean posture: widget on, console capture only, screenshot on-report,
727
+ // no network/performance/replay, no proactive nudges.
728
+ case "minimal":
729
+ return {
730
+ widget: { trigger: "auto" },
731
+ capture: {
732
+ console: true,
733
+ network: false,
734
+ performance: false,
735
+ screenshot: "on-report",
736
+ replay: "off",
737
+ elementSelector: false
738
+ },
739
+ proactive: { rageClick: false, longTask: false, apiCascade: false, errorBoundary: false }
740
+ };
741
+ // Everything-on posture: full capture (incl. self-contained `lite` replay)
742
+ // and every proactive trigger.
743
+ case "full":
744
+ return {
745
+ widget: { trigger: "auto" },
746
+ capture: {
747
+ console: true,
748
+ network: true,
749
+ performance: true,
750
+ screenshot: "auto",
751
+ replay: "lite",
752
+ elementSelector: true
753
+ },
754
+ proactive: { rageClick: true, longTask: true, apiCascade: true, errorBoundary: true }
755
+ };
756
+ // 'standard' and anything unrecognised → no expansion.
757
+ default:
758
+ return void 0;
759
+ }
760
+ }
761
+ function expandPreset(config) {
762
+ const defaults = presetDefaults(config.preset);
763
+ if (!defaults) return config;
764
+ return {
765
+ ...config,
766
+ widget: {
767
+ ...defaults.widget,
768
+ ...config.widget
769
+ },
770
+ capture: {
771
+ ...defaults.capture,
772
+ ...config.capture
773
+ },
774
+ proactive: {
775
+ ...defaults.proactive,
776
+ ...config.proactive,
777
+ cooldown: {
778
+ ...defaults.proactive?.cooldown,
779
+ ...config.proactive?.cooldown
780
+ }
781
+ }
782
+ };
783
+ }
784
+ function isSilenced() {
785
+ const truthy = (v) => v === "1" || v === "true";
786
+ try {
787
+ if (typeof process !== "undefined" && process.env && truthy(process.env.MUSHI_SILENT)) return true;
788
+ } catch {
789
+ }
790
+ try {
791
+ const env = undefined;
792
+ if (env && typeof env === "object" && (truthy(env.MUSHI_SILENT) || truthy(env.VITE_MUSHI_SILENT))) return true;
793
+ } catch {
794
+ }
795
+ return false;
796
+ }
797
+ function validateConfig(config) {
798
+ try {
799
+ if (isSilenced()) return;
800
+ if (!config || typeof config !== "object") return;
801
+ const unknownKeys = Object.keys(config).filter((key) => !KNOWN_CONFIG_KEYS.includes(key));
802
+ if (unknownKeys.length > 0) {
803
+ console.error(
804
+ `[mushi] Unknown config key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.map((k) => `\`${k}\``).join(", ")} \u2014 check for typos (see MushiConfig). Ignored.`
805
+ );
806
+ }
807
+ if (config.preset !== void 0 && !KNOWN_PRESETS.includes(config.preset)) {
808
+ console.error(
809
+ `[mushi] Unknown preset \`${String(config.preset)}\` \u2014 expected one of ${KNOWN_PRESETS.map(
810
+ (p) => `'${p}'`
811
+ ).join(", ")}. Preset ignored.`
812
+ );
813
+ }
814
+ } catch {
815
+ }
816
+ }
604
817
 
605
818
  // src/pre-filter.ts
606
819
  var DEFAULT_MIN_LENGTH = 10;
@@ -2206,7 +2419,9 @@ exports.createOfflineQueue = createOfflineQueue;
2206
2419
  exports.createPiiScrubber = createPiiScrubber;
2207
2420
  exports.createPreFilter = createPreFilter;
2208
2421
  exports.createRateLimiter = createRateLimiter;
2422
+ exports.diagnoseEnvConfig = diagnoseEnvConfig;
2209
2423
  exports.estimateJsonBytes = estimateJsonBytes;
2424
+ exports.expandPreset = expandPreset;
2210
2425
  exports.faviconUrlCandidates = faviconUrlCandidates;
2211
2426
  exports.formatBytes = formatBytes;
2212
2427
  exports.getDeviceFingerprintHash = getDeviceFingerprintHash;
@@ -2235,5 +2450,6 @@ exports.resolveWidgetAccent = resolveWidgetAccent;
2235
2450
  exports.safeWidgetHex = safeWidgetHex;
2236
2451
  exports.scrubPii = scrubPii;
2237
2452
  exports.sha256Hex = sha256Hex;
2453
+ exports.validateConfig = validateConfig;
2238
2454
  //# sourceMappingURL=index.cjs.map
2239
2455
  //# sourceMappingURL=index.cjs.map