@mushi-mushi/core 1.30.0 → 1.31.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.
@@ -0,0 +1,2100 @@
1
+ /**
2
+ * FILE: reporter-channels.ts
3
+ * PURPOSE: Typed calls for the reporter loop v2 routes (Plan 018 §4, §6.2):
4
+ * the badge / toast feed, mark-read, the report timeline, and the
5
+ * reporter's own email / push opt-in.
6
+ *
7
+ * USAGE:
8
+ * import { reporterChannels } from '@mushi-mushi/core/reporter-channels'
9
+ * const rc = reporterChannels(apiClient)
10
+ * const res = await rc.getUpdates(reporterToken)
11
+ *
12
+ * Shipped as a subpath so the main `@mushi-mushi/core` entry stays inside
13
+ * its size budget. Every call goes through `apiClient.reporterRequest`, which
14
+ * signs the reporter digest headers, times out, retries once and never
15
+ * rejects: failures resolve as `{ ok: false }`.
16
+ *
17
+ * Email and push are opt-in. The server refuses a channel the project has
18
+ * not turned on, or cannot send (`EMAIL_NOT_AVAILABLE` / `PUSH_NOT_AVAILABLE`
19
+ * with `reason: 'not_configured' | 'project_disabled'`), so never show an
20
+ * opt-in control unless `GET /v1/sdk/config` → `reporter.emailEnabled` /
21
+ * `reporter.pushEnabled` is true.
22
+ */
23
+
24
+ /** `GET /v1/reporter/updates` — the unread badge and the next-visit toast. */
25
+ interface MushiReporterUpdates {
26
+ unread_total: number;
27
+ /** Up to 3 newest unread events, `report_id` is always the reporter's own report. */
28
+ latest: Array<{
29
+ report_id: string;
30
+ kind: string | null;
31
+ preview: string;
32
+ at: string;
33
+ }>;
34
+ server_time: string;
35
+ }
36
+ type MushiReporterChannelUnavailable = 'not_configured' | 'project_disabled';
37
+ /** `GET|PUT /v1/reporter/notification-prefs`. The address is always masked. */
38
+ interface MushiReporterNotificationPrefs {
39
+ email: string | null;
40
+ email_verified: boolean;
41
+ /** An address was given; the confirmation link has not been clicked yet. */
42
+ email_pending: boolean;
43
+ unsubscribed: boolean;
44
+ channels: {
45
+ in_app: boolean;
46
+ email: boolean;
47
+ push: boolean;
48
+ };
49
+ push_subscriptions: number;
50
+ available: {
51
+ email: boolean;
52
+ push: boolean;
53
+ };
54
+ unavailable_reason: {
55
+ email: MushiReporterChannelUnavailable | null;
56
+ push: MushiReporterChannelUnavailable | null;
57
+ };
58
+ /** PUT only: a confirmation email went out. */
59
+ verification_sent?: boolean;
60
+ }
61
+ /** `email`: a new address starts double opt-in; `null` forgets the address. */
62
+ interface MushiReporterPrefsUpdate {
63
+ email?: string | null;
64
+ channels?: {
65
+ email?: boolean;
66
+ push?: boolean;
67
+ };
68
+ }
69
+ /** `PushSubscription.toJSON()` shape. */
70
+ interface MushiPushSubscriptionJSON {
71
+ endpoint: string;
72
+ keys: {
73
+ p256dh: string;
74
+ auth: string;
75
+ };
76
+ }
77
+ /** One entry of `GET /v1/reporter/reports/:id` → `timeline`. */
78
+ interface MushiReporterTimelineItem {
79
+ kind: string;
80
+ at: string;
81
+ /** English text; pipeline kinds can be re-rendered per locale unless `custom`. */
82
+ text: string;
83
+ /** Developer- or project-written text: show `text` as is. */
84
+ custom?: boolean;
85
+ body?: string;
86
+ version?: string;
87
+ closed_reason?: string;
88
+ comment_id?: number | string;
89
+ }
90
+ interface MushiReporterReportDetail {
91
+ report: Record<string, unknown> & {
92
+ id: string;
93
+ status: string;
94
+ };
95
+ timeline: MushiReporterTimelineItem[];
96
+ can_verify: boolean;
97
+ }
98
+ interface MushiReporterChannels {
99
+ getUpdates(reporterToken: string, since?: string): Promise<MushiApiResponse<MushiReporterUpdates>>;
100
+ getReport(reportId: string, reporterToken: string): Promise<MushiApiResponse<MushiReporterReportDetail>>;
101
+ /** Mark one report's updates read; resolves the new unread total. */
102
+ markReportRead(reportId: string, reporterToken: string): Promise<MushiApiResponse<{
103
+ marked_read: number;
104
+ unread_total: number | null;
105
+ }>>;
106
+ markAllRead(reporterToken: string): Promise<MushiApiResponse<{
107
+ marked_read: number;
108
+ unread_total: number;
109
+ }>>;
110
+ getPrefs(reporterToken: string): Promise<MushiApiResponse<MushiReporterNotificationPrefs>>;
111
+ setPrefs(reporterToken: string, update: MushiReporterPrefsUpdate): Promise<MushiApiResponse<MushiReporterNotificationPrefs>>;
112
+ subscribePush(reporterToken: string, subscription: MushiPushSubscriptionJSON): Promise<MushiApiResponse<{
113
+ subscribed: boolean;
114
+ }>>;
115
+ unsubscribePush(reporterToken: string, endpoint: string): Promise<MushiApiResponse<{
116
+ removed: number;
117
+ }>>;
118
+ }
119
+ declare function reporterChannels(client: Pick<MushiApiClient, 'reporterRequest'>): MushiReporterChannels;
120
+ /**
121
+ * Browser push for a reporter: permission prompt, service worker, push
122
+ * subscription, then `subscribePush`. Call it from a click ("Notify me"):
123
+ * browsers only show the permission prompt after a user action. Never throws.
124
+ *
125
+ * `serviceWorkerPath` is a worker on the host app's own origin that shows
126
+ * the push payload (`{ title, body, tag, url? }`); `vapidPublicKey` comes
127
+ * from `GET /v1/sdk/config` → `reporter.vapidPublicKey` (null = not offered).
128
+ */
129
+ declare function subscribeBrowserPush(channels: Pick<MushiReporterChannels, 'subscribePush'>, reporterToken: string, serviceWorkerPath: string | null | undefined, vapidPublicKey: string | null | undefined): Promise<{
130
+ ok: true;
131
+ } | {
132
+ ok: false;
133
+ reason: string;
134
+ }>;
135
+
136
+ interface MushiConfig {
137
+ projectId: string;
138
+ apiKey: string;
139
+ apiEndpoint?: string;
140
+ /** Per-request timeout in ms (default 10000). Raise on slow mobile networks. */
141
+ timeout?: number;
142
+ /** Max retries for transient (network / 5xx) failures (default 2). */
143
+ maxRetries?: number;
144
+ /**
145
+ * Endpoint circuit breaker. After `threshold` consecutive unreachable
146
+ * failures the client fast-fails for `cooldownMs` (then half-opens) instead
147
+ * of hammering a down endpoint; fast-failed reports still hit the offline
148
+ * queue. Set `enabled: false` to opt out. Defaults: enabled, threshold 4,
149
+ * cooldown 30000ms.
150
+ */
151
+ circuitBreaker?: {
152
+ enabled?: boolean;
153
+ threshold?: number;
154
+ cooldownMs?: number;
155
+ };
156
+ /** Opinionated defaults for common environments. Explicit config wins. */
157
+ preset?: MushiPreset;
158
+ /**
159
+ * Fetch non-secret widget/capture settings from the Mushi project at
160
+ * startup. Defaults to true so console changes apply without rebuilding
161
+ * host apps. Set false for fully static/offline deployments.
162
+ */
163
+ runtimeConfig?: boolean | 'auto';
164
+ /**
165
+ * Product analytics (`Mushi.track()`, Users & Funnels). Enabled by default
166
+ * and keyed on the same opaque per-project reporter token as sessions;
167
+ * honours DNT / GPC. Set `enabled: false` to opt out, or
168
+ * `consent: 'required'` to buffer until `setConsent('granted')`.
169
+ * Session tracking passes the same gate: `enabled`, DNT / GPC, bot
170
+ * exclusion and consent all apply to it too.
171
+ */
172
+ analytics?: MushiAnalyticsConfig;
173
+ /**
174
+ * Session lifecycle tracking for the console's Activity and Users views
175
+ * (session start / heartbeat / end, page views). Default true. Runs only
176
+ * while the `analytics` gate allows tracking; `false` turns it off even
177
+ * then.
178
+ */
179
+ trackSessions?: boolean;
180
+ sentry?: MushiSentryConfig;
181
+ widget?: MushiWidgetConfig;
182
+ /** Reporter updates beyond the in-app badge (Plan 018 §4). */
183
+ notifications?: MushiNotificationsConfig;
184
+ capture?: MushiCaptureConfig;
185
+ privacy?: MushiPrivacyConfig;
186
+ proactive?: MushiProactiveConfig;
187
+ preFilter?: MushiPreFilterConfig;
188
+ integrations?: MushiIntegrationsConfig;
189
+ offline?: MushiOfflineConfig;
190
+ rewards?: MushiRewardsConfig;
191
+ /** Page-aware, user-data-aware in-SDK assistant (P5). */
192
+ assistant?: MushiAssistantConfig;
193
+ debug?: boolean;
194
+ enabled?: boolean;
195
+ /**
196
+ * Host app release identifier stored on `reports.app_version` at ingest.
197
+ * Pairs with `environment.buildId` (from `<meta name="mushi:build">`) for
198
+ * deploy-level provenance. Falls back to `integrations.vercel.analyticsId`
199
+ * when omitted (legacy Vercel Analytics wiring).
200
+ */
201
+ appVersion?: string;
202
+ /**
203
+ * Same-origin (or absolute) ingest proxy. When set, every SDK HTTP call
204
+ * goes here instead of `apiEndpoint` so ad blockers that drop third-party
205
+ * `*.supabase.co/functions` hosts cannot silently eat reports.
206
+ * Relative URLs resolve against the page origin — that is the recommended
207
+ * form (no CORS preflight; matches Sentry `tunnel`).
208
+ *
209
+ * @example
210
+ * tunnel: '/api/mushi-tunnel'
211
+ */
212
+ tunnel?: string;
213
+ /**
214
+ * Drop automatic error reports whose message matches any string (substring)
215
+ * or RegExp. Never applied to user-submitted widget feedback.
216
+ */
217
+ ignoreErrors?: Array<string | RegExp>;
218
+ /**
219
+ * Drop automatic error reports whose script URL / stack filename matches.
220
+ */
221
+ denyUrls?: Array<string | RegExp>;
222
+ /**
223
+ * When set, drop automatic error reports unless the script URL matches
224
+ * at least one entry.
225
+ */
226
+ allowUrls?: Array<string | RegExp>;
227
+ /**
228
+ * Probabilistic sampling rate for session-replay recording (rrweb/lite).
229
+ * Range 0–1, default 1 (record all sessions). The sampling decision is made
230
+ * once at session init — a sampled-out session never loads the rrweb chunk,
231
+ * reducing bundle cost and CPU for excluded users.
232
+ *
233
+ * Independent of `sampleRate` (error reporting). A session can have replay
234
+ * on but errors sampled, or vice versa.
235
+ *
236
+ * @example
237
+ * replaySampleRate: 0.2 // record replay for ~20% of sessions
238
+ */
239
+ replaySampleRate?: number;
240
+ /**
241
+ * When session replay was sampled out, still start the rolling buffer for
242
+ * this fraction of sessions (0–1, default 0) so an error/feedback report
243
+ * can attach replay. Keep at 0 unless you have accepted the CPU/egress
244
+ * cost — do not set to 1 with `capture.replay: 'rrweb'`.
245
+ */
246
+ replaysOnErrorSampleRate?: number;
247
+ /**
248
+ * Probabilistic sampling rate for automatic (non-user-initiated) error
249
+ * reports. Range 0–1, default 1 (send all). User-initiated feedback reports
250
+ * are always sent regardless of this value.
251
+ *
252
+ * Examples:
253
+ * sampleRate: 0.1 → send ~10% of auto-captured errors
254
+ * sampleRate: 1 → send all (default)
255
+ * sampleRate: 0 → send none (disables automatic capture; feedback unaffected)
256
+ *
257
+ * When rate limiting or circuit breaker is also active, the stricter of the
258
+ * two gates applies. Use sampleRate for sustained high-volume apps; use
259
+ * rate limiting for burst protection.
260
+ */
261
+ sampleRate?: number;
262
+ /**
263
+ * Hook called before ANY report (error, exception, or user feedback) is
264
+ * sent. Return null to cancel the report; return the (possibly modified)
265
+ * report to proceed. Async version supported — resolution is awaited before
266
+ * submission. Runs AFTER built-in PII scrubbing.
267
+ *
268
+ * @example
269
+ * beforeSend: (report) => {
270
+ * if (report.category === 'bug' && report.description.includes('internal')) return null
271
+ * return { ...report, description: sanitize(report.description) }
272
+ * }
273
+ */
274
+ beforeSend?: (report: MushiReport) => MushiReport | null | Promise<MushiReport | null>;
275
+ /**
276
+ * @deprecated Use `beforeSend` instead. `beforeSendFeedback` is kept for
277
+ * backwards compatibility and applies only to user-submitted feedback reports.
278
+ * `beforeSend` covers all report types and takes precedence when both are set.
279
+ */
280
+ beforeSendFeedback?: (report: MushiReport) => MushiReport | null | Promise<MushiReport | null>;
281
+ /** Called once if the app crashed during the previous session. */
282
+ onCrashedLastRun?: (crashed: boolean) => void;
283
+ }
284
+ interface MushiSentryConfig {
285
+ dsn?: string;
286
+ consumeUserFeedback?: boolean;
287
+ enrichWithSeer?: boolean;
288
+ useReplay?: boolean;
289
+ }
290
+ interface MushiWidgetConfig {
291
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
292
+ /**
293
+ * Raw CSS anchors for app shells with bottom navs/chat composers/cookie
294
+ * banners. When set, these values win over `position` + numeric `inset`.
295
+ */
296
+ anchor?: MushiWidgetAnchor;
297
+ theme?: 'auto' | 'light' | 'dark' | 'inherit';
298
+ /**
299
+ * Override the widget's primary accent colour (default: vermillion #E03C2C).
300
+ * Must be a CSS colour value that has sufficient contrast against both light
301
+ * and dark widget backgrounds. The widget derives a wash + accessible ink
302
+ * colour automatically, so only the base colour is required.
303
+ */
304
+ accent?: string;
305
+ /** Explicit text colour on the accent surface. Auto-computed when omitted. */
306
+ accentText?: string;
307
+ triggerText?: string;
308
+ expandedTitle?: string;
309
+ mode?: 'simple' | 'conversational';
310
+ locale?: string;
311
+ zIndex?: number;
312
+ /**
313
+ * Controls how, or whether, the default trigger is injected.
314
+ * `auto` — historical floating stamp FAB.
315
+ * `banner` — slim full-width header strip (recommended default; less obtrusive
316
+ * than a FAB and visible even when the SDK launcher is `hide()`'d).
317
+ * Pair with `bannerConfig` to customise appearance.
318
+ */
319
+ trigger?: 'auto' | 'banner' | 'edge-tab' | 'attach' | 'manual' | 'hidden';
320
+ /**
321
+ * Configuration for the header-banner launcher mode.
322
+ * Only applies when `trigger === 'banner'`.
323
+ */
324
+ bannerConfig?: MushiBannerConfig;
325
+ /** CSS selector used when `trigger` is `attach`. */
326
+ attachToSelector?: string;
327
+ /**
328
+ * Per-edge trigger offset in pixels. `auto` clears the corresponding edge.
329
+ * Defaults to the historical 24px gutter plus safe-area insets.
330
+ */
331
+ inset?: MushiWidgetInset;
332
+ respectSafeArea?: boolean;
333
+ /** Hide the launcher while any matching element exists in the document. */
334
+ hideOnSelector?: string;
335
+ /** Hide the launcher on matching pathnames. Strings are substring matches. */
336
+ hideOnRoutes?: string[];
337
+ environments?: Partial<Record<'production' | 'staging' | 'development', 'always' | 'never' | 'manual'>>;
338
+ /** Opt-in smart trigger behavior; planned to become the default in a later minor. */
339
+ smartHide?: boolean | MushiWidgetSmartHideConfig;
340
+ /**
341
+ * Allow the FAB to be dragged and repositioned by the user.
342
+ * - `true` — drag enabled, persisted to localStorage, snaps to nearest edge on release.
343
+ * - `false` — fixed position (default).
344
+ * - Object — fine-grained control:
345
+ * - `persist` save position per projectId (default true when draggable).
346
+ * - `snapToEdge` snap to the nearest left/right edge on release (default true).
347
+ * - `axis` constrain movement to one axis (`'x'`, `'y'`, or `'both'` default).
348
+ */
349
+ draggable?: boolean | {
350
+ persist?: boolean;
351
+ snapToEdge?: boolean;
352
+ axis?: 'both' | 'x' | 'y';
353
+ };
354
+ /**
355
+ * Show the "Bug reports by Mushi" mark at the bottom of the widget panel.
356
+ * It links to the Mushi site (`utm_source=widget&utm_medium=powered-by` plus
357
+ * an anonymous `ref` derived from a SHA-256 prefix of the project id) and
358
+ * feeds the `loop_impression` / `loop_click` analytics events.
359
+ *
360
+ * Precedence (most specific wins):
361
+ * 1. An explicit value here — the MIT SDK config is a hard override and
362
+ * beats anything the runtime config sends.
363
+ * 2. `widget.brandFooter` from the runtime config (`GET /v1/sdk/config`):
364
+ * Mushi Cloud sends `true` for Free Cloud projects and `false` for paid
365
+ * plans; self-hosted servers send nothing. Opt out with one toggle in the
366
+ * console project settings.
367
+ * 3. Default `false`.
368
+ */
369
+ brandFooter?: boolean;
370
+ /**
371
+ * How the widget surfaces the "update @mushi-mushi/web" notice. It is a
372
+ * developer instruction, so `'auto'` (default) shows it in the widget only on
373
+ * a dev host (localhost, loopback, `*.localhost`, `*.local`, `file:`) or with
374
+ * `debug: true` — never to an app's end users. `'banner'` always shows it;
375
+ * `'console-only'` and `'off'` never render it in the widget.
376
+ */
377
+ outdatedBanner?: 'auto' | 'banner' | 'console-only' | 'off';
378
+ /**
379
+ * Privacy nudge shown beside an attached screenshot preview, reminding the
380
+ * reporter to remove anything sensitive (balances, PII, tokens) before they
381
+ * submit. The widget always renders the captured image as a visible preview
382
+ * so the user can see exactly what will be sent and remove it if needed; this
383
+ * flag only controls the accompanying caption.
384
+ * - `true` (default) — show the localized default caption.
385
+ * - string — show this custom caption verbatim.
386
+ * - `false` — hide the caption (the preview + remove control still show).
387
+ *
388
+ * Settable per-host via the SDK config and remotely via the Mushi console
389
+ * runtime config (it travels in the `widget` block of `GET /v1/sdk/config`).
390
+ */
391
+ screenshotSensitiveHint?: boolean | string;
392
+ /**
393
+ * Beta mode: injects discreet "early access" messaging into the widget panel.
394
+ * Shows a beta strip on the report screen and a contact footer on the
395
+ * receipt. Designed to reduce user frustration with in-progress apps while
396
+ * actively inviting feedback.
397
+ */
398
+ betaMode?: MushiBetaModeConfig;
399
+ /**
400
+ * Absolute base URL of the Mushi admin console (e.g. `https://mushi.example.com`).
401
+ * When set, the success step surfaces a one-tap link to the user's own report
402
+ * on the console so they can watch the status change in real time. Without
403
+ * this the success step still confirms submission but cannot deep-link.
404
+ *
405
+ * This is intentionally separate from the API endpoint — production apps
406
+ * usually have the API on `api.mushi.example.com` and the console on
407
+ * `app.mushi.example.com`.
408
+ */
409
+ dashboardUrl?: string;
410
+ /**
411
+ * Override the SLA copy shown in the success step's "what happens next"
412
+ * line. Defaults to "We aim to review within 48h". Set to an empty string
413
+ * to hide the line entirely (e.g. internal-only deployments where SLA
414
+ * messaging would be over-promising).
415
+ */
416
+ responseSlaLabel?: string;
417
+ /**
418
+ * Show the "Idea" chip on the report screen. Defaults to true. Set to false
419
+ * for production-only deployments where you don't want to invite feature
420
+ * ideas through the widget. An idea is sent as `category='other'` with
421
+ * `user_category='feature'`, so triage never files it as a defect.
422
+ */
423
+ featureRequestCard?: boolean;
424
+ /** Override the localised label of the Idea chip. */
425
+ featureRequestLabel?: string;
426
+ /**
427
+ * @deprecated The one-screen report (Plan 018) has no helper text under the
428
+ * Idea chip; the web widget ignores this.
429
+ */
430
+ featureRequestDescription?: string;
431
+ /**
432
+ * Minimum description character count before Send enables. Default 8
433
+ * (Plan 018 §1.1), halved for CJK text on web; an attached screenshot or
434
+ * element lowers it to 0.
435
+ */
436
+ minDescriptionLength?: number;
437
+ /**
438
+ * CSS selectors of host-app elements that the widget trigger and panel must
439
+ * never visually overlap. At render time the widget queries each selector,
440
+ * measures the union bounding rect, and nudges `--mushi-top` / `--mushi-bottom`
441
+ * so the panel clears every avoided element by at least 8px.
442
+ *
443
+ * Typical use: avoid a sticky mobile header or a fixed sign-in CTA.
444
+ *
445
+ * @example
446
+ * avoidSelectors: ['[data-mobile-header]', '#sign-in-cta']
447
+ */
448
+ avoidSelectors?: string[];
449
+ /**
450
+ * Add host-specific categories. The report screen shows them under the
451
+ * "More…" chip, after the built-in Bug / Slow / Looks wrong / Confusing /
452
+ * Idea chips.
453
+ *
454
+ * Each custom category maps onto one of the five built-in `MushiReportCategory`
455
+ * values via `baseCategory` (defaults to `'other'`). The custom id is
456
+ * preserved as `userCategory` on the report so the Mushi console can filter
457
+ * by it independently of the LLM-assigned `category`.
458
+ *
459
+ * Deep-link helpers (`open`, `openWith`, `report`) accept the custom `id`
460
+ * in addition to the built-in enum values when this option is set.
461
+ *
462
+ * When absent, behaviour is identical to the historical defaults (backward-compatible).
463
+ */
464
+ categories?: MushiCustomCategory[];
465
+ }
466
+ /**
467
+ * Descriptor for a single entry in a host-app custom category list.
468
+ * Passed as `widget.categories` to `Mushi.init()`.
469
+ */
470
+ interface MushiCustomCategory {
471
+ /** Unique identifier used in `openWith(id)` / `report({ category: id })`. */
472
+ id: string;
473
+ /** Human-readable label shown under the "More…" chip. */
474
+ label: string;
475
+ /** Optional helper text shown beneath the label. */
476
+ description?: string;
477
+ /**
478
+ * Optional sub-chips shown after this category is picked (e.g. "Crash",
479
+ * "Wrong result"). Picking one is never required.
480
+ */
481
+ intents?: string[];
482
+ /**
483
+ * Emoji or single-character icon rendered next to the label in the picker.
484
+ * Falls back to a neutral bubble when absent.
485
+ */
486
+ icon?: string;
487
+ /**
488
+ * Which built-in `MushiReportCategory` value this custom category maps to
489
+ * for server-side classification. Defaults to `'other'`.
490
+ * The custom `id` is always preserved in `report.userCategory`.
491
+ */
492
+ baseCategory?: MushiReportCategory;
493
+ }
494
+ /** Optional flat link in the rich banner action row (admin-console BetaBanner style). */
495
+ interface MushiBannerLink {
496
+ /** Link label shown in the action row. */
497
+ label: string;
498
+ /** External URL — opens in a new tab when set. */
499
+ href?: string;
500
+ /** When `href` is absent, opens the widget in feature-request mode. */
501
+ featureRequest?: boolean;
502
+ }
503
+ /**
504
+ * Configuration for the `trigger: 'banner'` header-strip launcher.
505
+ *
506
+ * The banner renders as a slim, full-width strip pinned to the top of the
507
+ * viewport (or bottom if `position === 'bottom'`). It is styled to match the
508
+ * app's brand accent and dismissed per-session via a ✕ button.
509
+ *
510
+ * Variants
511
+ * --------
512
+ * `neon` — lime / electric-green strip (high contrast, dev / beta tool feel).
513
+ * `brand` — uses the Mushi vermillion accent (editorial, app-quality feel).
514
+ * `subtle` — near-invisible hairline with muted text (least disruptive).
515
+ */
516
+ interface MushiBannerConfig {
517
+ /** Visual style of the banner strip. Defaults to `'subtle'` (Plan 018 §1.4); `neon` is opt-in. */
518
+ variant?: 'neon' | 'brand' | 'subtle';
519
+ /** 'top' pins the banner below any existing sticky headers; 'bottom' pins above bottom navs. Defaults to 'top'. */
520
+ position?: 'top' | 'bottom';
521
+ /**
522
+ * Body copy on the strip — the lime "Beta" announcement line users see in
523
+ * the Mushi admin console. When set, the banner switches to the rich layout
524
+ * (pill + message + flat text actions) instead of button-only CTAs.
525
+ */
526
+ message?: string;
527
+ /**
528
+ * Pill label shown before `message` (e.g. "Beta"). Defaults to `"Beta"` when
529
+ * `message` is set. Pass `false` to hide the pill.
530
+ */
531
+ label?: string | false;
532
+ /** Override the call-to-action text in the banner. Defaults to '🐛 Report a bug'. */
533
+ bugCta?: string;
534
+ /** Show a "Request a feature" button alongside the bug button. Defaults to true. */
535
+ featureCta?: boolean;
536
+ /** Override the feature-request button label. */
537
+ featureCtaLabel?: string;
538
+ /** Extra flat links after the bug/feature CTAs (e.g. "My submissions"). */
539
+ links?: MushiBannerLink[];
540
+ /** CSS z-index of the banner element. Defaults to the widget's configured zIndex. */
541
+ zIndex?: number;
542
+ }
543
+ interface MushiBetaModeConfig {
544
+ enabled?: boolean;
545
+ /** Display name of the app shown in the beta strip. Defaults to 'This app'. */
546
+ appName?: string;
547
+ /** Contact email shown on success step for direct founder/dev reach. */
548
+ contactEmail?: string;
549
+ /** Override the default beta strip message. */
550
+ message?: string;
551
+ /**
552
+ * Optional perks shown to the user for opting in as a beta tester.
553
+ * e.g. ['Early access to new lessons', 'Priority bug fix queue'].
554
+ */
555
+ perks?: string[];
556
+ /**
557
+ * Version-tied release notes rendered as a collapsible "What's new in this build"
558
+ * row in the widget. Closes the feedback loop — "did you fix what I reported?"
559
+ * Only the first entry (latest) is shown by default.
560
+ */
561
+ changelogItems?: MushiBetaChangelogEntry[];
562
+ }
563
+ interface MushiBetaChangelogEntry {
564
+ version: string;
565
+ date?: string;
566
+ items: string[];
567
+ }
568
+ interface MushiWidgetInset {
569
+ top?: number | 'auto';
570
+ right?: number | 'auto';
571
+ bottom?: number | 'auto';
572
+ left?: number | 'auto';
573
+ }
574
+ interface MushiWidgetAnchor {
575
+ top?: string;
576
+ right?: string;
577
+ bottom?: string;
578
+ left?: string;
579
+ }
580
+ type MushiPreset = 'production-calm' | 'beta-loud' | 'internal-debug' | 'manual-only' | 'minimal' | 'standard' | 'full';
581
+ interface MushiWidgetSmartHideConfig {
582
+ onMobile?: 'edge-tab' | 'hide' | false;
583
+ onScroll?: 'shrink' | 'hide' | false;
584
+ onIdleMs?: number;
585
+ }
586
+ interface MushiCaptureConfig {
587
+ console?: boolean;
588
+ network?: boolean;
589
+ /**
590
+ * URLs that should never be captured as host-app traffic. Strings are
591
+ * substring matches; RegExp values are tested against the fully resolved URL.
592
+ */
593
+ ignoreUrls?: MushiUrlMatcher[];
594
+ performance?: boolean;
595
+ screenshot?: 'on-report' | 'auto' | 'off';
596
+ /**
597
+ * Custom screenshot capture function. When provided, the SDK calls this
598
+ * instead of its built-in DOM-snapshot capturer. Return a JPEG/PNG data URI
599
+ * on success, or `null` to skip attachment. If the function throws the SDK
600
+ * falls back to the built-in capturer.
601
+ *
602
+ * Primary use-case: Capacitor / WebView hosts that route through a native
603
+ * plugin (e.g. `@capawesome/capacitor-screenshot`) to obtain a real pixel-
604
+ * accurate screen grab instead of the SVG-foreignObject DOM reconstruction.
605
+ *
606
+ * @example
607
+ * capture: {
608
+ * screenshot: 'auto',
609
+ * screenshotProvider: async () => {
610
+ * const { Screenshot } = await import('@capawesome/capacitor-screenshot');
611
+ * const { uri } = await Screenshot.take();
612
+ * return uri; // data URI on web, file:// on native (SDK handles both)
613
+ * },
614
+ * }
615
+ */
616
+ screenshotProvider?: () => Promise<string | null>;
617
+ elementSelector?: boolean;
618
+ replay?: 'sentry' | 'rrweb' | 'lite' | 'off';
619
+ /**
620
+ * How to load rrweb for `replay: 'rrweb'`. A published SDK cannot import
621
+ * rrweb itself: its bare `import('rrweb')` is invisible to your bundler, so
622
+ * rrweb never makes it into your build. Hand the import over and your
623
+ * bundler code-splits it like any other dynamic import. Install `rrweb`
624
+ * yourself; the chunk loads only for sessions sampled into replay.
625
+ *
626
+ * Without a loader the SDK uses a global `rrweb` (the UMD build from a
627
+ * script tag) when one exists, and otherwise records lite replay (clicks)
628
+ * and warns once in the console. Recording masks every input and every
629
+ * text node.
630
+ *
631
+ * @example
632
+ * capture: { replay: 'rrweb', rrweb: () => import('rrweb') }
633
+ */
634
+ rrweb?: () => Promise<{
635
+ record?: unknown;
636
+ }>;
637
+ /**
638
+ * Mushi Mushi v2.1 (whitepaper §6 hybrid mode): passive inventory
639
+ * discovery. When enabled the SDK observes navigations and emits a
640
+ * tiny payload — `(route, title, testids[], outbound api paths[],
641
+ * dom_summary[≤200 chars])` — back to `/v1/sdk/discovery`. The
642
+ * server aggregates this over a 30-day rolling window and the
643
+ * admin can ask Claude to propose an `inventory.yaml` from it.
644
+ *
645
+ * Default: off. Recommended: enable in dev/preview/staging from the
646
+ * day you install the SDK so the proposer has data when you go to
647
+ * generate your first inventory.
648
+ *
649
+ * What is sent (per navigation, throttled to ≤1/route/min):
650
+ * - `location.pathname` template-normalized (`/practice/abc-123`
651
+ * → `/practice/[id]` via uuid/numeric/hex heuristics + an
652
+ * optional framework hint if the host app sets
653
+ * `discoverInventory.routeTemplates`)
654
+ * - `document.title`
655
+ * - All `[data-testid]` values currently in the DOM
656
+ * - Recent fetch/XHR paths the existing network capturer saw
657
+ * - The first text run of `<h1>` / `<title>` / `<main>` truncated
658
+ * to 200 chars (helps Claude name stories well)
659
+ * - Sanitized query-param keys (key names only, never values)
660
+ * - A SHA-256 of `userId || sessionId` so the server can dedupe
661
+ * distinct users without ever seeing identity
662
+ *
663
+ * Nothing else. No DOM beyond the summary, no query values, no PII.
664
+ */
665
+ discoverInventory?: boolean | MushiDiscoverInventoryConfig;
666
+ /**
667
+ * W3C trace-context propagation for OTel-style frontend→backend correlation.
668
+ *
669
+ * When enabled, the SDK injects a W3C `traceparent` header and an
670
+ * `x-mushi-session` header into every fetch/XHR request whose URL matches
671
+ * the `corsUrls` allowlist. The generated trace_id is recorded on each
672
+ * captured network entry so a bug report can be correlated with the
673
+ * backend span that handled the failing request.
674
+ *
675
+ * Default: disabled. Enable only for origins you control to avoid CORS issues.
676
+ * The backend (node SDK or Supabase edge function) must include
677
+ * `traceparent` and `x-mushi-session` in `Access-Control-Allow-Headers`.
678
+ *
679
+ * @example
680
+ * capture: {
681
+ * tracePropagation: {
682
+ * enabled: true,
683
+ * corsUrls: [/api\.myapp\.com/, /localhost:3000/],
684
+ * }
685
+ * }
686
+ */
687
+ tracePropagation?: MushiTracePropagationConfig;
688
+ }
689
+ interface MushiTracePropagationConfig {
690
+ /** Enable W3C traceparent injection. Defaults to false. */
691
+ enabled?: boolean;
692
+ /**
693
+ * URL patterns for which the SDK injects `traceparent` + `x-mushi-session`
694
+ * headers. Only requests matching at least one pattern are instrumented.
695
+ * Strings are substring-matched; RegExp values are tested against the full URL.
696
+ * Required when `enabled` is true — an empty allowlist silently disables propagation.
697
+ */
698
+ corsUrls?: Array<string | RegExp>;
699
+ }
700
+ /**
701
+ * Fine-grained controls for `discoverInventory`. Defaults are tuned
702
+ * to be quiet enough for production but dense enough for the proposer
703
+ * to produce useful drafts.
704
+ */
705
+ interface MushiDiscoverInventoryConfig {
706
+ enabled?: boolean;
707
+ /**
708
+ * Minimum gap between two emissions for the same route (ms). Defaults
709
+ * to 60_000. Set to a larger value for high-traffic SPAs to keep the
710
+ * ingest volume manageable.
711
+ */
712
+ throttleMs?: number;
713
+ /**
714
+ * Static route templates the host framework knows about. When set,
715
+ * a visit to `/practice/abc-123` matches `/practice/[id]` and gets
716
+ * normalized to that template. Without this we fall back to a
717
+ * heuristic (uuid / numeric / 24-char-hex segments collapse to
718
+ * `[id]`). Provide this when you have a static manifest — Next.js'
719
+ * `next.config` route export, React Router's route config, etc.
720
+ */
721
+ routeTemplates?: string[];
722
+ /**
723
+ * Override the SHA-256 user-id-hash input. Defaults to
724
+ * `mushi.userId || sessionId`. Set to `null` to opt out of distinct-
725
+ * user counting entirely.
726
+ */
727
+ userIdSource?: 'auto' | 'session-only' | 'none';
728
+ /**
729
+ * If false, the DOM summary (≤200 chars from h1/title/main) is not
730
+ * captured. Default true.
731
+ */
732
+ captureDomSummary?: boolean;
733
+ }
734
+ interface MushiPrivacyConfig {
735
+ /** DOM nodes to visually mask in screenshots before upload. */
736
+ maskSelectors?: string[];
737
+ /** DOM subtrees to remove from screenshots before upload. */
738
+ blockSelectors?: string[];
739
+ /**
740
+ * CSS selectors whose matching elements are blacked-out (filled with an
741
+ * opaque black rectangle) in screenshots before upload. Intended for
742
+ * sensitive fields that should never appear in any form — passwords, PII,
743
+ * financial data. Applied in addition to `maskSelectors`.
744
+ *
745
+ * Always redacted, whatever this is set to: `input[type="password"]`,
746
+ * `input[autocomplete^="cc-"]`, `[data-private]`, `[data-mushi-mask]`.
747
+ * This list is added on top; it defaults to `['[data-mushi-redact]']`,
748
+ * and an empty array drops only that default.
749
+ */
750
+ redactSelectors?: string[];
751
+ /** Let reporters remove an attached screenshot before submitting. Defaults to true. */
752
+ allowUserRemoveScreenshot?: boolean;
753
+ }
754
+ interface MushiProactiveConfig {
755
+ rageClick?: boolean;
756
+ errorBoundary?: boolean;
757
+ longTask?: boolean;
758
+ apiCascade?: boolean | MushiApiCascadeConfig;
759
+ cooldown?: MushiCooldownConfig;
760
+ /**
761
+ * Beta-mode nudge: fire after the user has been on the same route for
762
+ * `thresholdMs` continuous milliseconds (default 5min). Pass `true` to
763
+ * accept the default threshold, or a config object to override. Use
764
+ * conservatively — set the per-session cap in `cooldown` to avoid
765
+ * nag fatigue.
766
+ */
767
+ pageDwell?: boolean | MushiPageDwellConfig;
768
+ /**
769
+ * One-shot welcome prompt for first-time visitors. Fires `delayMs` after
770
+ * `Mushi.init` (default 45s) and is suppressed permanently after the
771
+ * first fire via localStorage. Recommended for beta deployments.
772
+ */
773
+ firstSession?: boolean | MushiFirstSessionConfig;
774
+ }
775
+ interface MushiPageDwellConfig {
776
+ /** Continuous dwell time before firing. Defaults to 5 minutes. */
777
+ thresholdMs?: number;
778
+ /**
779
+ * Route path prefixes (or glob-style patterns with `*`) that suppress the
780
+ * dwell nudge. Auth routes are excluded by default so users aren't prompted
781
+ * during login/signup flows.
782
+ *
783
+ * Default: `['/login', '/logout', '/signup', '/sso/*', '/auth/*']`
784
+ */
785
+ excludeRoutes?: string[];
786
+ }
787
+ interface MushiFirstSessionConfig {
788
+ /** Delay before firing the welcome nudge. Defaults to 45 seconds. */
789
+ delayMs?: number;
790
+ /** Override the localStorage key used to mark the user as welcomed. */
791
+ storageKey?: string;
792
+ }
793
+ type MushiUrlMatcher = string | RegExp;
794
+ interface MushiApiCascadeConfig {
795
+ enabled?: boolean;
796
+ /**
797
+ * URLs ignored by the API cascade detector. The SDK always ignores its own
798
+ * gateway endpoints as well; this hook lets host apps exclude analytics,
799
+ * health probes, or third-party scripts that are noisy by design.
800
+ */
801
+ ignoreUrls?: MushiUrlMatcher[];
802
+ }
803
+ interface MushiCooldownConfig {
804
+ maxProactivePerSession?: number;
805
+ dismissCooldownHours?: number;
806
+ suppressAfterDismissals?: number;
807
+ /**
808
+ * Cross-reload re-show cooldown, in minutes (default 30; `0` disables).
809
+ *
810
+ * The `dismissCooldownHours` window only starts after a clean dismissal is
811
+ * recorded (widget `onClose`). A page reload or crash tears down the JS
812
+ * context before that, so on a broken/reloading page the proactive panel
813
+ * would otherwise re-open on every load. When a prompt is shown the SDK
814
+ * persists a timestamp; a fresh session (new JS context) suppresses prompts
815
+ * shown within this window. Within a live session the per-session limit
816
+ * governs instead, so this never blocks a legitimate second trigger.
817
+ */
818
+ reshowCooldownMinutes?: number;
819
+ }
820
+ /**
821
+ * How a reporter hears back outside the widget (Plan 018 §4). Email and push
822
+ * are opt-in by the reporter AND need the project switch in the console;
823
+ * `/v1/sdk/config` → `reporter` says what the project offers.
824
+ */
825
+ interface MushiNotificationsConfig {
826
+ /** Show one "the developer replied" / "fixed in vX" toast on the next visit. Default true. */
827
+ toast?: boolean;
828
+ /** Offer "Get updates by email" on the receipt and report detail. Default true (when the project offers email). */
829
+ email?: boolean;
830
+ /**
831
+ * Prefill the email box from `identify({ email })`. Default false. The box
832
+ * is never pre-ticked either way.
833
+ */
834
+ emailFromIdentity?: boolean;
835
+ /**
836
+ * Browser push: a service worker on YOUR origin that shows the push
837
+ * payload `{ title, body, tag, url? }`; required for
838
+ * `subscribeReporterPush()`. Off by default.
839
+ */
840
+ webPush?: false | {
841
+ serviceWorkerPath: string;
842
+ };
843
+ }
844
+ interface MushiPreFilterConfig {
845
+ enabled?: boolean;
846
+ blockObviousSpam?: boolean;
847
+ minDescriptionLength?: number;
848
+ maxDescriptionLength?: number;
849
+ /**
850
+ * V5.3 §2.12: optional on-device classifier (typically from
851
+ * `@mushi-mushi/wasm-classifier`). Consulted before the pattern pre-filter.
852
+ * Must conform to the `MushiOnDeviceClassifier` shape.
853
+ */
854
+ wasmClassifier?: MushiOnDeviceClassifier;
855
+ }
856
+ interface MushiOnDeviceClassifierInput {
857
+ description: string;
858
+ category?: string;
859
+ url?: string;
860
+ hasScreenshot?: boolean;
861
+ hasSelectedElement?: boolean;
862
+ hasNetworkErrors?: boolean;
863
+ hasConsoleErrors?: boolean;
864
+ proactiveTrigger?: string;
865
+ }
866
+ interface MushiOnDeviceClassifierResult {
867
+ verdict: 'pass' | 'block' | 'unsure';
868
+ confidence: number;
869
+ reason: string;
870
+ modelId: string;
871
+ durationMs: number;
872
+ }
873
+ interface MushiOnDeviceClassifier {
874
+ readonly modelId: string;
875
+ classify(input: MushiOnDeviceClassifierInput): Promise<MushiOnDeviceClassifierResult>;
876
+ destroy(): void;
877
+ }
878
+ interface MushiIntegrationsConfig {
879
+ opentelemetry?: {
880
+ traceContext?: boolean;
881
+ };
882
+ vercel?: {
883
+ analyticsId?: string;
884
+ };
885
+ custom?: (report: MushiReportBuilder) => void;
886
+ }
887
+ interface MushiOfflineConfig {
888
+ enabled?: boolean;
889
+ maxQueueSize?: number;
890
+ syncOnReconnect?: boolean;
891
+ /**
892
+ * Encrypt queued reports at rest (IndexedDB + localStorage) with AES-GCM.
893
+ *
894
+ * Wave S1 / D-16: on a shared device (kiosks, iPads, support-agent
895
+ * laptops) any queued report sits in plaintext until the next online flush.
896
+ * With this flag set we generate a non-extractable AES-GCM key at first use,
897
+ * stash it in a single tightly-scoped IndexedDB record, and wrap every
898
+ * queued report payload under it. The key never leaves the origin's
899
+ * IndexedDB; stealing the DB file on disk still requires the OS-level
900
+ * Web Crypto keystore to decrypt.
901
+ *
902
+ * Defaults to `true`. Set false only if you need to inspect raw queue
903
+ * contents during local dev.
904
+ */
905
+ encryptAtRest?: boolean;
906
+ }
907
+ interface MushiRewardsConfig {
908
+ enabled?: boolean;
909
+ /**
910
+ * P1: when true the SDK auto-tracks route navigations, [data-testid] clicks,
911
+ * and session dwell time and flushes them to POST /v1/sdk/activity.
912
+ * Default false. Requires opted_in_to_rewards = true from the end user.
913
+ */
914
+ trackActivity?: boolean;
915
+ /**
916
+ * Activity batch flush interval in ms. Default 300_000 (5 min).
917
+ * Minimum 30_000 (30s) — smaller values are clamped.
918
+ */
919
+ flushIntervalMs?: number;
920
+ /**
921
+ * Show a tier + points footer in the SDK widget.
922
+ * Default false.
923
+ */
924
+ showInWidget?: boolean;
925
+ /**
926
+ * Whether to show a "+X pts" toast notification on each award.
927
+ * Default true when enabled.
928
+ */
929
+ showNotifications?: boolean;
930
+ /**
931
+ * 'auto' — immediately opt the user in when identify() is called.
932
+ * 'explicit' — SDK surfaces a one-time consent prompt. Default 'explicit'.
933
+ */
934
+ consentMode?: 'auto' | 'explicit';
935
+ /**
936
+ * P2: callback that returns a host-app JWT for server-side identity
937
+ * verification before monetary rewards are issued. Return null to skip
938
+ * verification for non-monetary actions.
939
+ */
940
+ verifyUserToken?: () => Promise<string | null>;
941
+ /**
942
+ * @deprecated Superseded by server-side reward_webhooks table.
943
+ * Kept for backwards compatibility; has no effect from v0.10.0.
944
+ */
945
+ webhookOnReward?: string;
946
+ }
947
+ interface MushiReputationResult {
948
+ totalPoints: number;
949
+ points30d: number;
950
+ reputation: number;
951
+ confirmedBugs: number;
952
+ totalReports: number;
953
+ }
954
+ interface MushiTierResult {
955
+ id: string;
956
+ slug: string;
957
+ displayName: string;
958
+ pointsThreshold: number;
959
+ perks: Record<string, unknown>;
960
+ }
961
+ interface MushiActivityEvent {
962
+ action: string;
963
+ metadata?: Record<string, unknown>;
964
+ }
965
+ type MushiReportCategory = 'bug' | 'slow' | 'visual' | 'confusing' | 'other';
966
+ type MushiReportStatus = 'pending' | 'submitted' | 'classified' | 'grouped' | 'fixing' | 'fixed' | 'dismissed';
967
+ interface MushiReport {
968
+ id: string;
969
+ projectId: string;
970
+ category: MushiReportCategory;
971
+ /**
972
+ * Custom category identifier set by the host app via `widget.categories`.
973
+ * Preserved as `reports.user_category` on the server so the Mushi console
974
+ * can filter by it independently of the LLM-assigned `category`.
975
+ * Not set when the host uses the default built-in category set.
976
+ */
977
+ userCategory?: string;
978
+ description: string;
979
+ userIntent?: string;
980
+ environment: MushiEnvironment;
981
+ consoleLogs?: MushiConsoleEntry[];
982
+ networkLogs?: MushiNetworkEntry[];
983
+ performanceMetrics?: MushiPerformanceMetrics;
984
+ timeline?: MushiTimelineEntry[];
985
+ screenshotDataUrl?: string;
986
+ selectedElement?: MushiSelectedElement;
987
+ /** rrweb / lite rolling-buffer events attached on submit when capture.replay is enabled. */
988
+ replayEvents?: unknown[];
989
+ metadata?: Record<string, unknown>;
990
+ sessionId?: string;
991
+ reporterToken: string;
992
+ /**
993
+ * §3c — stable per-device hash from `getDeviceFingerprintHash()`.
994
+ * Sent so the server can run the cross-account anti-gaming check; falls
995
+ * back to IP+UA fingerprinting when omitted.
996
+ */
997
+ fingerprintHash?: string;
998
+ appVersion?: string;
999
+ /** SDK package that submitted the report, e.g. `@mushi-mushi/web`. */
1000
+ sdkPackage?: string;
1001
+ /** npm package version that submitted the report, e.g. `0.8.0`. */
1002
+ sdkVersion?: string;
1003
+ proactiveTrigger?: string;
1004
+ /**
1005
+ * Mushi-side breadcrumbs maintained by the SDK in a 50-entry ring buffer.
1006
+ * Captured automatically (SDK lifecycle events, console errors, route
1007
+ * changes, `[data-testid]` clicks) and via `Mushi.addBreadcrumb()`.
1008
+ * Independent of Sentry's breadcrumbs (those land in
1009
+ * `sentryContext.breadcrumbs`) so a host using Mushi without Sentry
1010
+ * still gets a timeline.
1011
+ */
1012
+ breadcrumbs?: MushiBreadcrumb[];
1013
+ /**
1014
+ * Sticky tags set via `Mushi.setTag()` / `Mushi.setTags()`. Mirrors
1015
+ * the Sentry / DataDog "tag" vocabulary — short string keys with
1016
+ * scalar values. The Triage LLM treats these as high-signal hints
1017
+ * ("checkout-flow=redesign-v2" tells the LLM which feature flag the
1018
+ * report came from). Plumbed through to `reports.metadata.tags`
1019
+ * server-side until a dedicated `tags` column is added.
1020
+ */
1021
+ tags?: Record<string, string | number | boolean>;
1022
+ /**
1023
+ * Rich Sentry context captured at report time. Replaces the legacy
1024
+ * `sentryEventId`/`sentryReplayId` pair (those are kept for
1025
+ * back-compat — server unifies them). When the host has Sentry
1026
+ * installed (v7, v8, or v9), every Mushi report carries enough trace
1027
+ * data for the admin to pivot into Sentry's MCP / web UI without
1028
+ * a manual paste.
1029
+ */
1030
+ sentryContext?: MushiSentryContext;
1031
+ /** @deprecated use `sentryContext.eventId` — kept for back-compat. */
1032
+ sentryEventId?: string;
1033
+ /** @deprecated use `sentryContext.replayId` — kept for back-compat. */
1034
+ sentryReplayId?: string;
1035
+ queuedAt?: string;
1036
+ createdAt: string;
1037
+ }
1038
+ /**
1039
+ * Single breadcrumb entry. Shape follows the Sentry breadcrumb schema
1040
+ * 1:1 so an admin tooling layer (or the Triage LLM) can treat Mushi
1041
+ * breadcrumbs and Sentry breadcrumbs interchangeably without a
1042
+ * normalisation pass.
1043
+ */
1044
+ interface MushiBreadcrumb {
1045
+ /** Unix epoch ms when the breadcrumb fired. */
1046
+ timestamp: number;
1047
+ /**
1048
+ * Coarse bucket for filtering / coloring in the report drawer.
1049
+ * - `navigation` — route or url change
1050
+ * - `ui.click` — user clicked a `[data-testid]` element (web SDK)
1051
+ * - `ui.tap` — user tapped a `UIView` / Android `View` (native SDKs)
1052
+ * - `console` — `console.error` / `console.warn` callsite
1053
+ * - `xhr` / `fetch` — network request that errored or 4xx/5xx (web SDK)
1054
+ * - `network` — network failure on iOS / Android (native SDKs)
1055
+ * - `lifecycle` — Mushi SDK init / open / submit / queue
1056
+ * - `custom` — host called `Mushi.addBreadcrumb()`
1057
+ *
1058
+ * Admin tooling that filters by interaction should treat
1059
+ * `ui.click` ∪ `ui.tap` as one bucket and `xhr` ∪ `fetch` ∪ `network`
1060
+ * as one bucket. Native enums are platform-idiomatic (`tap` on touch
1061
+ * devices, `network` because iOS/Android don't expose `XHR` / `fetch`
1062
+ * separately) — every wire string is documented in the native READMEs.
1063
+ */
1064
+ category: 'navigation' | 'ui.click' | 'ui.tap' | 'console' | 'xhr' | 'fetch' | 'network' | 'lifecycle' | 'custom';
1065
+ /**
1066
+ * Severity — `info` is the default. `warning` / `error` map to a
1067
+ * coloured pill in the drawer; the Triage LLM uses these to decide
1068
+ * which breadcrumbs to feature in the "what happened" summary.
1069
+ */
1070
+ level: 'debug' | 'info' | 'warning' | 'error';
1071
+ /** Free-form short summary, capped at 500 chars at submit time. */
1072
+ message: string;
1073
+ /** Optional structured payload — kept small to keep ingest cheap. */
1074
+ data?: Record<string, unknown>;
1075
+ /**
1076
+ * Correlation ID linking this breadcrumb to the `MushiNetworkEntry` and
1077
+ * `MushiConsoleEntry` entries emitted during the same network request's
1078
+ * lifetime (Phase 3b). Set for `fetch` / `xhr` category crumbs and for
1079
+ * console entries captured while a request was in-flight.
1080
+ */
1081
+ correlationId?: string;
1082
+ }
1083
+ /**
1084
+ * Snapshot of Sentry's current scope at report submission. Captured by
1085
+ * `captureSentryContext()` in the web SDK; designed to be cheap to
1086
+ * serialise and exhaustively useful when the admin pivots into Sentry
1087
+ * MCP via `find_organizations` → `search_issues` → `get_event_attachment`.
1088
+ *
1089
+ * Every field is optional so a host without Sentry installed (and a
1090
+ * Sentry SDK that exposes only a subset of these globals) still
1091
+ * produces a partial — but useful — payload.
1092
+ */
1093
+ interface MushiSentryContext {
1094
+ /** Sentry SDK version family detected at capture time. */
1095
+ sdk?: 'v7' | 'v8' | 'v9' | 'unknown';
1096
+ /** `Sentry.lastEventId()` (v8+) / `getLastEventId()` (v7). */
1097
+ eventId?: string;
1098
+ /** Replay session id from `Sentry.getReplay()?.getReplayId()`. */
1099
+ replayId?: string;
1100
+ /** Distributed-tracing trace id (32 hex). */
1101
+ traceId?: string;
1102
+ /** Active span id at capture time (16 hex). */
1103
+ spanId?: string;
1104
+ /** `transaction` field from the active scope, e.g. `GET /checkout`. */
1105
+ transactionName?: string;
1106
+ /** Build identifier set via `Sentry.init({ release })`. */
1107
+ release?: string;
1108
+ /** Logical environment, e.g. `production`. */
1109
+ environment?: string;
1110
+ /** Session id when Sentry session-tracking is enabled. */
1111
+ sessionId?: string;
1112
+ /** User context from Sentry's scope (id/email/username/ip). */
1113
+ user?: {
1114
+ id?: string;
1115
+ email?: string;
1116
+ username?: string;
1117
+ ip_address?: string;
1118
+ };
1119
+ /** Tags attached to Sentry's current scope. */
1120
+ tags?: Record<string, string | number | boolean>;
1121
+ /**
1122
+ * Last N Sentry breadcrumbs (default cap = 30). Already-formatted
1123
+ * Sentry breadcrumbs, not Mushi's — the two are surfaced side by
1124
+ * side in the admin drawer so users can correlate.
1125
+ */
1126
+ breadcrumbs?: Array<{
1127
+ timestamp?: number;
1128
+ category?: string;
1129
+ level?: string;
1130
+ message?: string;
1131
+ type?: string;
1132
+ data?: Record<string, unknown>;
1133
+ }>;
1134
+ /**
1135
+ * Issue url (deeplink) when the SDK can derive it from the event id
1136
+ * + DSN. Lets the admin jump straight to the Sentry issue page
1137
+ * without the user pasting a link.
1138
+ */
1139
+ issueUrl?: string;
1140
+ }
1141
+ interface MushiReportBuilder {
1142
+ addMetadata(key: string, value: unknown): void;
1143
+ setCategory(category: MushiReportCategory): void;
1144
+ setDescription(description: string): void;
1145
+ }
1146
+ interface MushiEnvironment {
1147
+ userAgent: string;
1148
+ platform: string;
1149
+ language: string;
1150
+ viewport: {
1151
+ width: number;
1152
+ height: number;
1153
+ };
1154
+ url: string;
1155
+ referrer: string;
1156
+ timestamp: string;
1157
+ timezone: string;
1158
+ connection?: {
1159
+ effectiveType?: string;
1160
+ downlink?: number;
1161
+ rtt?: number;
1162
+ };
1163
+ deviceMemory?: number;
1164
+ hardwareConcurrency?: number;
1165
+ /**
1166
+ * v2 inventory hints (whitepaper §4.7).
1167
+ *
1168
+ * `route` — the bare pathname (no query / hash). Pinned to the
1169
+ * inventory's `Page.path` so the Triage LLM can shortcut from a
1170
+ * freeform "the streak counter is broken" report to the right page.
1171
+ *
1172
+ * `nearestTestid` — the closest ancestor of the active element with
1173
+ * a `data-testid`, captured at widget-open time. This pins the
1174
+ * report to one Action node when more than one page has the same
1175
+ * page path (e.g. a shared "Buy Pro" CTA on landing + dashboard).
1176
+ *
1177
+ * Both are best-effort; freeform reports with no active element will
1178
+ * have `route` only.
1179
+ */
1180
+ route?: string;
1181
+ nearestTestid?: string;
1182
+ /**
1183
+ * Native-shell detection (best-effort, present only when a native bridge is
1184
+ * detected). Lets triage distinguish a Capacitor/Cordova/React-Native WebView
1185
+ * report from a plain browser one, and lets the SDK gate DOM-only behaviour.
1186
+ */
1187
+ native?: {
1188
+ capacitor?: boolean;
1189
+ cordova?: boolean;
1190
+ reactNative?: boolean;
1191
+ };
1192
+ /**
1193
+ * SDK boost (2026-05-07): richer per-report context so the Triage LLM
1194
+ * and the admin /reports detail drawer have something to reason about
1195
+ * beyond `userAgent`. Every field is optional so legacy SDKs and
1196
+ * non-browser callers (CLI, server-to-server) keep validating.
1197
+ */
1198
+ /**
1199
+ * Parsed UA-CH (User-Agent Client Hints) high-entropy values when the
1200
+ * browser supports `navigator.userAgentData.getHighEntropyValues`.
1201
+ * This is the modern, reliable way to identify Chromium browsers — UA
1202
+ * sniffing is unreliable post-Chrome 100 because the UA string was
1203
+ * frozen for privacy. Safari + Firefox don't expose UA-CH so we still
1204
+ * fall back to UA parsing for those (handled server-side).
1205
+ */
1206
+ userAgentData?: {
1207
+ /** Best-effort browser brand (e.g. "Chrome", "Edge", "Brave"). */
1208
+ browser?: string;
1209
+ /** Browser version (full semver where available, e.g. "131.0.6778.86"). */
1210
+ browserVersion?: string;
1211
+ /** OS family (e.g. "macOS", "Windows", "Android", "iOS"). */
1212
+ os?: string;
1213
+ /** OS version when the browser exposes it (e.g. "14.5.0"). */
1214
+ osVersion?: string;
1215
+ /** Whether the device self-identifies as mobile (UA-CH `mobile`). */
1216
+ mobile?: boolean;
1217
+ /** Device model when the OS exposes it (Android only, e.g. "Pixel 8"). */
1218
+ model?: string;
1219
+ /** CPU architecture (e.g. "x86", "arm"). */
1220
+ architecture?: string;
1221
+ /** CPU bitness ("32" or "64"). */
1222
+ bitness?: string;
1223
+ };
1224
+ /**
1225
+ * Physical device pixels behind the viewport. `viewport` already
1226
+ * captures the CSS pixel box; this lets us tell a 1080p MacBook from a
1227
+ * Retina iPhone and explain "looks fine on my screen, broken on
1228
+ * theirs" by surfacing devicePixelRatio mismatches.
1229
+ */
1230
+ screen?: {
1231
+ /** `screen.width` — outer device width in CSS px. */
1232
+ width?: number;
1233
+ /** `screen.height` — outer device height in CSS px. */
1234
+ height?: number;
1235
+ /** `window.devicePixelRatio` — physical / CSS px ratio. */
1236
+ devicePixelRatio?: number;
1237
+ /** `screen.colorDepth` — bits per pixel (typically 24/30). */
1238
+ colorDepth?: number;
1239
+ /** Active orientation type from `screen.orientation`. */
1240
+ orientation?: 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary' | string;
1241
+ };
1242
+ /**
1243
+ * Accessibility / display preferences resolved via media queries.
1244
+ * These are reproduction hints — a bug that only repros under
1245
+ * `prefers-reduced-motion: reduce` or `forced-colors: active` is one
1246
+ * the developer would otherwise spend hours hunting for.
1247
+ */
1248
+ prefersColorScheme?: 'dark' | 'light' | 'no-preference';
1249
+ prefersReducedMotion?: boolean;
1250
+ prefersReducedData?: boolean;
1251
+ prefersContrast?: 'more' | 'less' | 'no-preference' | 'custom';
1252
+ forcedColors?: boolean;
1253
+ /** `navigator.onLine` at capture time. False = the report was filed offline. */
1254
+ online?: boolean;
1255
+ /**
1256
+ * `(display-mode: standalone | minimal-ui | fullscreen | browser)`
1257
+ * resolved via media queries. Tells us whether the user filed the
1258
+ * report from a regular browser tab, an installed PWA, or a TWA — a
1259
+ * different code path on iOS Safari for each.
1260
+ */
1261
+ displayMode?: 'browser' | 'minimal-ui' | 'standalone' | 'fullscreen';
1262
+ /** `document.title` at capture time. Surface for "what page were they on?". */
1263
+ documentTitle?: string;
1264
+ /**
1265
+ * Optional opt-in build identifier from `<meta name="mushi:build" content="...">`.
1266
+ * Hosts that already expose a git SHA / build number to their HTML
1267
+ * (e.g. `<meta name="mushi:build" content="abc123def">`) get it
1268
+ * threaded through automatically — no SDK config required. Pairs with
1269
+ * `appVersion` to pin reports to a specific deploy.
1270
+ */
1271
+ buildId?: string;
1272
+ /**
1273
+ * Snapshot of the Navigation Timing entry. Reports that come in
1274
+ * during a slow page load look very different from steady-state
1275
+ * reports, and the LLM can't tell the difference from a stack alone.
1276
+ */
1277
+ pageLoadTiming?: {
1278
+ /** `domContentLoadedEventEnd - startTime` in ms. */
1279
+ domContentLoadedMs?: number;
1280
+ /** `loadEventEnd - startTime` in ms. */
1281
+ loadCompleteMs?: number;
1282
+ /** `responseStart - startTime` in ms (TTFB). */
1283
+ timeToFirstByteMs?: number;
1284
+ /** Navigation type (`navigate`, `reload`, `back_forward`, `prerender`). */
1285
+ navigationType?: string;
1286
+ };
1287
+ }
1288
+ interface MushiConsoleEntry {
1289
+ level: 'log' | 'warn' | 'error' | 'info' | 'debug';
1290
+ message: string;
1291
+ timestamp: number;
1292
+ stack?: string;
1293
+ /**
1294
+ * Correlation ID linking this console entry to the network request that was
1295
+ * active when this log was emitted (e.g. the fetch whose catch block called
1296
+ * console.error). Matches `MushiNetworkEntry.correlationId`. Set by the SDK's
1297
+ * active-request tracker (Phase 3b).
1298
+ */
1299
+ correlationId?: string;
1300
+ }
1301
+ interface MushiNetworkEntry {
1302
+ method: string;
1303
+ url: string;
1304
+ status: number;
1305
+ duration: number;
1306
+ timestamp: number;
1307
+ requestHeaders?: Record<string, string>;
1308
+ responseHeaders?: Record<string, string>;
1309
+ error?: string;
1310
+ /**
1311
+ * W3C trace ID (32-char lowercase hex) injected by the SDK's trace propagation
1312
+ * feature. Present only when `capture.tracePropagation.enabled` is true AND
1313
+ * the request URL matched the `corsUrls` allowlist.
1314
+ * Used to correlate this network entry with a backend span in the admin console.
1315
+ */
1316
+ traceId?: string;
1317
+ /**
1318
+ * Whether this entry was captured by the fetch interceptor or the XHR interceptor
1319
+ * (Phase 3a). Used for row-level provenance badges in the console UI.
1320
+ */
1321
+ captureMethod?: 'fetch' | 'xhr';
1322
+ /**
1323
+ * Correlation ID shared with `MushiConsoleEntry.correlationId` and
1324
+ * `MushiBreadcrumb.correlationId` for entries emitted during this request's
1325
+ * lifetime (Phase 3b). Enables "highlight all logs/crumbs from this request."
1326
+ */
1327
+ correlationId?: string;
1328
+ }
1329
+ interface MushiPerformanceMetrics {
1330
+ fcp?: number;
1331
+ lcp?: number;
1332
+ cls?: number;
1333
+ fid?: number;
1334
+ inp?: number;
1335
+ ttfb?: number;
1336
+ longTasks?: number;
1337
+ inpAttribution?: {
1338
+ eventType?: string;
1339
+ targetSelector?: string;
1340
+ inputDelay?: number;
1341
+ processingDuration?: number;
1342
+ presentationDelay?: number;
1343
+ };
1344
+ }
1345
+ interface MushiSelectedElement {
1346
+ tagName: string;
1347
+ id?: string;
1348
+ className?: string;
1349
+ textContent?: string;
1350
+ xpath?: string;
1351
+ rect?: {
1352
+ x: number;
1353
+ y: number;
1354
+ width: number;
1355
+ height: number;
1356
+ };
1357
+ /**
1358
+ * `data-testid` of the closest ancestor that has one. Mushi v2 uses this
1359
+ * to map a report → Action node in the bidirectional graph (whitepaper §4.7).
1360
+ * Falls back to `undefined` when no ancestor declares a testid — the v2
1361
+ * Triage LLM still classifies these reports, just without the inventory
1362
+ * grounding shortcut.
1363
+ */
1364
+ nearestTestid?: string;
1365
+ /** Path of the page the user reported from (`window.location.pathname`).
1366
+ * Combined with `nearestTestid` it pins a report to one Action even when
1367
+ * the same testid exists on multiple pages. */
1368
+ route?: string;
1369
+ }
1370
+ type MushiTimelineKind = 'route' | 'click' | 'request' | 'log' | 'screen';
1371
+ interface MushiTimelineEntry {
1372
+ ts: number;
1373
+ kind: MushiTimelineKind;
1374
+ payload: Record<string, unknown>;
1375
+ }
1376
+ type MushiEventType = 'report:submitted' | 'report:queued' | 'report:sent' | 'report:failed' | 'widget:opened' | 'widget:closed' | 'proactive:triggered' | 'proactive:dismissed' | 'report:dispatched';
1377
+ type MushiEventHandler = (event: {
1378
+ type: MushiEventType;
1379
+ data?: unknown;
1380
+ }) => void;
1381
+ interface MushiDiagnosticsResult {
1382
+ apiEndpointReachable: boolean;
1383
+ cspAllowsEndpoint: boolean;
1384
+ widgetMounted: boolean;
1385
+ shadowDomAvailable: boolean;
1386
+ dialogSupported: boolean;
1387
+ runtimeConfigLoaded: boolean;
1388
+ captureScreenshotAvailable: boolean;
1389
+ captureNetworkIntercepting: boolean;
1390
+ sdkVersion: string;
1391
+ /**
1392
+ * True when the widget host element has `pointer-events: none` and
1393
+ * zero width/height — i.e. it cannot act as a touch blocker over host UI.
1394
+ * False when the SDK has not yet mounted, or when a consumer has overridden
1395
+ * the host styles without restoring the pass-through contract.
1396
+ */
1397
+ widgetHostPointerSafe: boolean;
1398
+ /**
1399
+ * Bounding rect of the widget host element (`offsetWidth × offsetHeight`).
1400
+ * Should be `{ width: 0, height: 0 }` for a healthy SDK — the host is
1401
+ * zero-sized with `overflow: visible` so the shadow internals extend
1402
+ * outward without creating a hit-test surface. `null` when not mounted.
1403
+ */
1404
+ widgetHostBounds: {
1405
+ width: number;
1406
+ height: number;
1407
+ } | null;
1408
+ /**
1409
+ * True when the widget is currently suppressed: `hideOnSelector` matched an
1410
+ * element, `hideOnRoutes` matched the current pathname, or `hide()` was
1411
+ * called. A suppressed widget renders nothing and removes the body nudge.
1412
+ */
1413
+ widgetSuppressed: boolean;
1414
+ /**
1415
+ * True when `trigger: 'banner'` is active and the banner is currently
1416
+ * rendered in the shadow DOM (not dismissed, not suppressed).
1417
+ */
1418
+ bannerRendered: boolean;
1419
+ }
1420
+ /**
1421
+ * Page-aware assistant (P5). The host app publishes a structured snapshot of
1422
+ * what the current screen is showing; the SDK forwards only this narrow,
1423
+ * server-validated subset so a malicious page cannot inject arbitrary prompt
1424
+ * fields. Ported from the admin console's page-context registry.
1425
+ */
1426
+ interface MushiPageContext {
1427
+ /** Current route/path (e.g. '/activity'). */
1428
+ route: string;
1429
+ /** Human title of the screen. */
1430
+ title?: string;
1431
+ /** One-line description of what is on screen. */
1432
+ summary?: string;
1433
+ /** Active filters as a flat key/value map. */
1434
+ filters?: Record<string, string | number | boolean | null | undefined>;
1435
+ /** The focused/selected entity, if any. */
1436
+ selection?: {
1437
+ kind: string;
1438
+ id: string;
1439
+ label?: string;
1440
+ } | null;
1441
+ }
1442
+ interface MushiAssistantConfig {
1443
+ /**
1444
+ * Enable the in-SDK assistant tab. Defaults to false; the console can also
1445
+ * enable it via runtime config so it can be turned on without a rebuild.
1446
+ */
1447
+ enabled?: boolean;
1448
+ /** Tab label (default: "Ask"). */
1449
+ label?: string;
1450
+ /** Greeting shown on an empty assistant thread. */
1451
+ greeting?: string;
1452
+ /** Suggested starter questions shown on an empty thread. */
1453
+ suggestions?: string[];
1454
+ }
1455
+ interface MushiAssistantStep {
1456
+ label: string;
1457
+ detail?: string;
1458
+ }
1459
+ interface MushiAssistantReply {
1460
+ /** 'answer' resolves the turn; 'clarify' asks the user a follow-up. */
1461
+ kind: 'answer' | 'clarify';
1462
+ text?: string;
1463
+ steps?: MushiAssistantStep[];
1464
+ /** For 'clarify' replies. */
1465
+ question?: string;
1466
+ options?: string[];
1467
+ /** Server-assigned thread id so the next turn continues the conversation. */
1468
+ threadId?: string;
1469
+ }
1470
+ interface MushiSDKInstance {
1471
+ /**
1472
+ * Open the reporter widget. With no options, opens to the category
1473
+ * picker so the user can choose between bug categories and the
1474
+ * feature-request shortcut. Pass `{ category }` to deep-link into a
1475
+ * specific bug intent, or `{ featureRequest: true }` to deep-link into
1476
+ * the feature-request description step (skips intent picker).
1477
+ *
1478
+ * When `widget.categories` is configured, `category` also accepts a
1479
+ * custom category `id` from that list.
1480
+ */
1481
+ report(options?: {
1482
+ category?: MushiReportCategory | string;
1483
+ featureRequest?: boolean;
1484
+ }): void;
1485
+ on(event: MushiEventType, handler: MushiEventHandler): () => void;
1486
+ setUser(user: {
1487
+ id: string;
1488
+ email?: string;
1489
+ name?: string;
1490
+ }): void;
1491
+ setMetadata(key: string, value: unknown): void;
1492
+ setScreen(screen: {
1493
+ name: string;
1494
+ route?: string;
1495
+ feature?: string;
1496
+ }): void;
1497
+ isOpen(): boolean;
1498
+ open(): void;
1499
+ /**
1500
+ * Open the widget deep-linked to a specific category. Accepts a built-in
1501
+ * `MushiReportCategory` value or a custom category `id` when
1502
+ * `widget.categories` is configured.
1503
+ */
1504
+ openWith(category: MushiReportCategory | string): void;
1505
+ show(): void;
1506
+ hide(): void;
1507
+ attachTo(selectorOrElement: string | Element, options?: MushiWidgetConfig): () => void;
1508
+ setTrigger(trigger: NonNullable<MushiWidgetConfig['trigger']>): void;
1509
+ /** Open the widget directly to the reporter's "My reports" history view. */
1510
+ openReporter(): void;
1511
+ close(): void;
1512
+ destroy(): void;
1513
+ updateConfig(config: MushiRuntimeSdkConfig): void;
1514
+ diagnose(): Promise<MushiDiagnosticsResult>;
1515
+ /**
1516
+ * Wave G4 — unified `captureEvent` API. Submits a bug report
1517
+ * programmatically without opening the widget. Useful for adapters
1518
+ * that translate errors from Datadog / Honeycomb / Sentry /
1519
+ * Grafana into Mushi reports.
1520
+ *
1521
+ * Returns the server-assigned report id when the submit succeeds.
1522
+ */
1523
+ captureEvent(event: MushiCaptureEventInput): Promise<string | null>;
1524
+ /**
1525
+ * 2026-05-07 — `try/catch`-friendly sugar over `captureEvent`. Pass
1526
+ * a thrown value directly and Mushi normalises it (any exotic
1527
+ * throw shape: `Error`, string, plain object, `null`) into a report
1528
+ * with category `bug`, severity `high` (overridable), and the
1529
+ * stack trace folded into both `description` (compact) and
1530
+ * `metadata.error.stack` (full).
1531
+ *
1532
+ * Pairs with Sentry: when `config.sentry` is present, the same
1533
+ * call-site can flush to Sentry (`Sentry.captureException(err)`)
1534
+ * and Mushi (`Mushi.captureException(err)`) — the two reports get
1535
+ * cross-linked via `sentryContext.eventId` automatically.
1536
+ */
1537
+ captureException(error: unknown, options?: MushiCaptureExceptionOptions): Promise<string | null>;
1538
+ /**
1539
+ * Wave G4 — sugar alias for `setUser()`. Name mirrors the
1540
+ * identify/track/capture vocabulary that PostHog/Segment/Mixpanel
1541
+ * users already know.
1542
+ */
1543
+ identify(userId: string, traits?: {
1544
+ email?: string;
1545
+ name?: string;
1546
+ [k: string]: unknown;
1547
+ }): void;
1548
+ /**
1549
+ * Identify the current end user with a signed Mushi identity JWT minted by
1550
+ * the host app's server (see `@mushi-mushi/node`'s `mintMushiIdentityToken`).
1551
+ * This is the trust anchor for secure "My Reports", rewards-to-membership
1552
+ * grants, and the per-user assistant data index — the backend re-verifies
1553
+ * the signature against the project's identity secret. Unlike `identify()`,
1554
+ * which is spoofable, claims from this token are trusted server-side.
1555
+ *
1556
+ * Pass `null` (or call on logout) to clear the identity and fall back to the
1557
+ * anonymous reporter token.
1558
+ */
1559
+ identifyWithToken(token: string | null): void;
1560
+ /**
1561
+ * Publish the current screen's context so the in-SDK assistant can answer
1562
+ * page-aware questions. Call on every route/selection change. Pass `null`
1563
+ * to clear (e.g. on unmount). Only a narrow subset is sent to the server.
1564
+ */
1565
+ publishPageContext(context: MushiPageContext | null): void;
1566
+ /** Open the widget on the assistant ("Ask") tab. */
1567
+ openAssistant(): void;
1568
+ /**
1569
+ * Sentry-grade observability surface (2026-05-07).
1570
+ *
1571
+ * Hosts can drop a breadcrumb on every meaningful state change in
1572
+ * their app — feature-flag toggles, route transitions, optimistic
1573
+ * UI commits, server reconciliation — and the buffer's last 50
1574
+ * entries automatically attach to every report. Useful even when
1575
+ * Sentry isn't installed; when it is, Mushi reports also carry
1576
+ * Sentry's breadcrumbs alongside Mushi's own.
1577
+ */
1578
+ addBreadcrumb(crumb: Omit<MushiBreadcrumb, 'timestamp'> & {
1579
+ timestamp?: number;
1580
+ }): void;
1581
+ /** Snapshot of the current breadcrumb ring buffer (oldest first). */
1582
+ getBreadcrumbs(): MushiBreadcrumb[];
1583
+ /**
1584
+ * Set a sticky tag that lands on every subsequent report. Numeric
1585
+ * and boolean values are accepted; they're coerced to strings on
1586
+ * the wire so the Triage LLM can read them without type juggling.
1587
+ */
1588
+ setTag(key: string, value: string | number | boolean): void;
1589
+ /** Bulk variant of `setTag`. Replaces existing values for shared keys. */
1590
+ setTags(tags: Record<string, string | number | boolean>): void;
1591
+ /** Remove a single tag, or all tags when called with no argument. */
1592
+ clearTag(key?: string): void;
1593
+ /**
1594
+ * Returns the current user's reputation + legacy point totals from the
1595
+ * reporter_reputation table. Available even when the rewards program is
1596
+ * disabled (legacy surface).
1597
+ */
1598
+ getReputation(): Promise<MushiReputationResult | null>;
1599
+ /**
1600
+ * Returns the current user's tier from the rewards program.
1601
+ * Returns null when the user has not yet been identified or the project
1602
+ * has rewards_enabled = false.
1603
+ */
1604
+ getTier(): Promise<MushiTierResult | null>;
1605
+ /**
1606
+ * Manually record a host-defined activity event (e.g. 'lesson_completed').
1607
+ * The SDK batches these and flushes to POST /v1/sdk/activity.
1608
+ * No-op when rewards are disabled or the user has not opted in.
1609
+ * For product analytics (funnels, paths, people) use `track()` instead.
1610
+ */
1611
+ recordActivity(action: string, metadata?: Record<string, unknown>): void;
1612
+ /**
1613
+ * Record a product-analytics event, e.g. `track('checkout_started', { plan: 'pro' })`.
1614
+ * Names must match `^[a-z][a-z0-9_]{1,63}$`; properties are flat
1615
+ * (string | number | boolean | null), PII-looking keys are dropped, and
1616
+ * every string value runs through the PII scrubber. Batched to
1617
+ * POST /v1/sdk/events. No-op under DNT/GPC, `analytics.enabled: false`,
1618
+ * or before consent when `analytics.consent === 'required'`.
1619
+ * Returns true when the event was queued.
1620
+ */
1621
+ track(event: string, properties?: Record<string, MushiPropertyValue>): boolean;
1622
+ /** Grant or deny analytics consent (persisted per project in localStorage). */
1623
+ setConsent(state: 'granted' | 'denied'): void;
1624
+ /** The anonymous id analytics events are keyed on, or null when tracking is off. */
1625
+ getAnonymousId(): string | null;
1626
+ /**
1627
+ * Briefly animate the bug-report trigger button to draw the user's
1628
+ * attention without opening the full widget. Ideal for subtle "feedback
1629
+ * welcome" nudges (first-session, beta-onboarding).
1630
+ */
1631
+ pulseTrigger(): void;
1632
+ /**
1633
+ * Returns the signed-in reporter's own report history.
1634
+ * Keyed to the persistent `reporterToken` stored in localStorage /
1635
+ * AsyncStorage. Returns an empty array when no token exists yet.
1636
+ */
1637
+ listMyReports(): Promise<MushiReporterReport[]>;
1638
+ /**
1639
+ * Returns the comment thread for a given report. Only returns comments
1640
+ * visible to the reporter (their own comments + team replies).
1641
+ */
1642
+ listMyComments(reportId: string): Promise<MushiReporterComment[]>;
1643
+ /**
1644
+ * Post a follow-up comment on one of the reporter's own reports.
1645
+ * Returns the newly created comment, or null on failure.
1646
+ */
1647
+ replyToReport(reportId: string, body: string): Promise<MushiReporterComment | null>;
1648
+ /**
1649
+ * Submit a structured feedback chip (confirms / not_fixed / …) on a report.
1650
+ * Drives the verify/reopen lifecycle when the report is in a fixed state.
1651
+ */
1652
+ submitFeedbackSignal(reportId: string, signal: string, note?: string): Promise<Record<string, unknown> | null>;
1653
+ /**
1654
+ * Reporter-initiated regression reopen with an optional note.
1655
+ */
1656
+ reopenReport(reportId: string, note?: string): Promise<Record<string, unknown> | null>;
1657
+ /**
1658
+ * Open the reporter inbox ("my reports") view in the widget.
1659
+ */
1660
+ openMyReports(): void;
1661
+ /** Unread count and the newest unread updates on this device's reports (null on failure). */
1662
+ getReporterUpdates(): Promise<MushiReporterUpdates | null>;
1663
+ /** Mark one report's updates read; resolves the new unread total (null on failure). */
1664
+ markReportRead(reportId: string): Promise<number | null>;
1665
+ /**
1666
+ * Fires with the unread count and newest updates whenever the SDK checks
1667
+ * for updates, so a host can draw its own badge: right away on subscribe,
1668
+ * then every 5 minutes while the page is visible and on tab focus, and
1669
+ * only from a device that has filed a report (Plan 018 §4.4). Returns an
1670
+ * unsubscribe function.
1671
+ */
1672
+ onReporterUpdate(cb: (updates: MushiReporterUpdates) => void): () => void;
1673
+ /** This reporter's email / push choices; the address comes back masked. */
1674
+ getNotificationPrefs(): Promise<MushiReporterNotificationPrefs | null>;
1675
+ /**
1676
+ * Ask for email updates (`{ email }` sends a confirmation email first —
1677
+ * nothing else is mailed until it is clicked) or change channels. Show this
1678
+ * only when the reporter asked: never pre-tick an opt-in. Resolves the
1679
+ * server's answer, including `EMAIL_NOT_AVAILABLE` when this app does not
1680
+ * offer email.
1681
+ */
1682
+ setNotificationPrefs(update: MushiReporterPrefsUpdate): Promise<MushiApiResponse<MushiReporterNotificationPrefs>>;
1683
+ /**
1684
+ * Turn on browser push for this reporter. Call it from a click ("Notify
1685
+ * me"): it asks the browser for permission. Needs
1686
+ * `notifications.webPush.serviceWorkerPath` and the project's push switch.
1687
+ */
1688
+ subscribeReporterPush(): Promise<{
1689
+ ok: true;
1690
+ } | {
1691
+ ok: false;
1692
+ reason: string;
1693
+ }>;
1694
+ /**
1695
+ * Returns the global contributor hall-of-fame ranked by total points.
1696
+ * Safe to call without an authenticated user; uses public endpoint.
1697
+ * @param limit Maximum entries to return (default 20).
1698
+ */
1699
+ getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
1700
+ }
1701
+ interface MushiCaptureExceptionOptions {
1702
+ /** Override the default `'bug'` category (e.g. `'slow'` for timeouts). */
1703
+ category?: MushiReportCategory;
1704
+ /** Default `'high'`. Use `'critical'` for boot-time errors, `'low'` for known recoverables. */
1705
+ severity?: 'critical' | 'high' | 'medium' | 'low';
1706
+ /** Affected component / page area, surfaces in the admin reports list. */
1707
+ component?: string;
1708
+ /** Optional human-readable summary that overrides the auto-derived one. */
1709
+ description?: string;
1710
+ /** Per-call tags merged with sticky tags. */
1711
+ tags?: Record<string, string | number | boolean>;
1712
+ /** Free-form metadata folded into `reports.metadata`. */
1713
+ metadata?: Record<string, unknown>;
1714
+ /** Source label — defaults to `'captureException'`. */
1715
+ source?: string;
1716
+ }
1717
+ interface MushiCaptureEventInput {
1718
+ /** Human-readable summary; becomes `reports.description`. */
1719
+ description: string;
1720
+ category?: MushiReportCategory;
1721
+ severity?: 'critical' | 'high' | 'medium' | 'low';
1722
+ component?: string;
1723
+ /** Arbitrary tags merged into `reports.metadata.tags`. */
1724
+ tags?: Record<string, string | number | boolean>;
1725
+ /** Source-of-truth adapter that produced this event (e.g. `'datadog'`). */
1726
+ source?: string;
1727
+ /** Optional error payload (name/message/stack) captured from the host app. */
1728
+ error?: {
1729
+ name?: string;
1730
+ message?: string;
1731
+ stack?: string;
1732
+ };
1733
+ metadata?: Record<string, unknown>;
1734
+ }
1735
+ interface MushiApiClient {
1736
+ submitReport(report: MushiReport): Promise<MushiApiResponse<{
1737
+ reportId: string;
1738
+ }>>;
1739
+ getReportStatus(reportId: string): Promise<MushiApiResponse<{
1740
+ status: MushiReportStatus;
1741
+ }>>;
1742
+ getSdkConfig(): Promise<MushiApiResponse<MushiRuntimeSdkConfig>>;
1743
+ /**
1744
+ * Send one assistant turn. Forwards the page context + thread id; the
1745
+ * end-user identity token (when set) travels on the X-Mushi-User-Token
1746
+ * header so the backend can scope retrieval to the verified user.
1747
+ */
1748
+ askAssistant(input: {
1749
+ message: string;
1750
+ threadId?: string | null;
1751
+ context?: MushiPageContext | null;
1752
+ }): Promise<MushiApiResponse<MushiAssistantReply>>;
1753
+ getLatestSdkVersion(packageName: string): Promise<MushiApiResponse<MushiSdkVersionInfo>>;
1754
+ /**
1755
+ * Mushi v2.1: ship a single passive-discovery observation (route +
1756
+ * testids + outbound APIs + DOM summary). Best-effort fire-and-forget;
1757
+ * the caller should not block on the response. The server rate-limits
1758
+ * per (project, route) to keep ingest cheap, so it's fine for clients
1759
+ * to over-emit on the throttle window — the server picks the freshest.
1760
+ */
1761
+ postDiscoveryEvent(event: MushiDiscoveryEventPayload): Promise<MushiApiResponse<{
1762
+ accepted: boolean;
1763
+ }>>;
1764
+ /** POST /v1/sdk/session — lightweight session lifecycle event (best-effort). */
1765
+ postSessionEvent(payload: MushiSessionEventPayload): Promise<MushiApiResponse<{
1766
+ accepted: boolean;
1767
+ }>>;
1768
+ /** POST /v1/sdk/events — batched product-analytics events (Mushi.track()). */
1769
+ postProductEvents(payload: MushiProductEventPayload): Promise<MushiApiResponse<{
1770
+ accepted: number;
1771
+ dropped: number;
1772
+ }>>;
1773
+ listReporterReports(reporterToken: string): Promise<MushiApiResponse<{
1774
+ reports: MushiReporterReport[];
1775
+ }>>;
1776
+ /**
1777
+ * A signed reporter call (digest headers, timeout, one retry; never rejects).
1778
+ * The typed reporter-loop v2 calls are in `@mushi-mushi/core/reporter-channels`.
1779
+ */
1780
+ reporterRequest<T>(method: string, path: string, reporterToken: string, body?: unknown): Promise<MushiApiResponse<T>>;
1781
+ listReporterComments(reportId: string, reporterToken: string): Promise<MushiApiResponse<{
1782
+ comments: MushiReporterComment[];
1783
+ }>>;
1784
+ replyToReporterReport(reportId: string, reporterToken: string, body: string, feedbackSignal?: string): Promise<MushiApiResponse<{
1785
+ comment: MushiReporterComment;
1786
+ feedback?: Record<string, unknown>;
1787
+ }>>;
1788
+ reopenReporterReport(reportId: string, reporterToken: string, note?: string): Promise<MushiApiResponse<{
1789
+ outcome: Record<string, unknown>;
1790
+ }>>;
1791
+ /** List in-app notifications for the authenticated reporter. */
1792
+ listNotifications(reporterToken: string, opts?: {
1793
+ since?: string;
1794
+ limit?: number;
1795
+ }): Promise<MushiApiResponse<{
1796
+ notifications: Array<Record<string, unknown>>;
1797
+ }>>;
1798
+ /** Mark a single reporter notification as read. */
1799
+ markNotificationRead(notificationId: string, reporterToken: string): Promise<MushiApiResponse<{
1800
+ ok: boolean;
1801
+ }>>;
1802
+ /** List the reporter-facing feature board (public roadmap tickets). */
1803
+ listReporterFeatureBoard(reporterToken: string): Promise<MushiApiResponse<{
1804
+ tickets: Array<Record<string, unknown>>;
1805
+ }>>;
1806
+ /** Toggle the reporter's vote on a feature-board ticket. */
1807
+ voteReporterFeatureBoard(requestId: string, reporterToken: string): Promise<MushiApiResponse<{
1808
+ voted: boolean;
1809
+ action: string;
1810
+ }>>;
1811
+ /**
1812
+ * Submit a batch of activity events for the current user.
1813
+ * Requires rewards to be enabled on the project.
1814
+ */
1815
+ submitActivity(userId: string, events: MushiActivityEvent[], opts?: {
1816
+ userTraits?: {
1817
+ email?: string;
1818
+ name?: string;
1819
+ provider?: string;
1820
+ };
1821
+ reporterTokenHash?: string;
1822
+ optedIn?: boolean;
1823
+ /** P2: host-app JWT; when present the server attempts verifyHostJwt
1824
+ * and updates end_users.jwt_verified_at if valid. Required for
1825
+ * monetary payout eligibility. */
1826
+ hostJwt?: string;
1827
+ }): Promise<MushiApiResponse<{
1828
+ accepted: number;
1829
+ total: number;
1830
+ }>>;
1831
+ /** Fetch the current user's point totals (requires userId). */
1832
+ getMyPoints(userId: string): Promise<MushiApiResponse<{
1833
+ total_points: number;
1834
+ points_30d: number;
1835
+ points_lifetime: number;
1836
+ tier: MushiTierResult | null;
1837
+ }>>;
1838
+ /** Fetch the current user's tier (requires userId). */
1839
+ getMyTier(userId: string): Promise<MushiApiResponse<MushiTierResult | null>>;
1840
+ /** Fetch the current user's activity history (requires userId). */
1841
+ getMyHistory(userId: string, opts?: {
1842
+ limit?: number;
1843
+ }): Promise<MushiApiResponse<{
1844
+ items: unknown[];
1845
+ total: number;
1846
+ }>>;
1847
+ /** Fetch the project's public leaderboard (top contributors). */
1848
+ getHallOfFame(limit?: number): Promise<MushiApiResponse<{
1849
+ data: Array<{
1850
+ display_name: string;
1851
+ email_hash: string | null;
1852
+ tier_slug: string | null;
1853
+ tier_name: string | null;
1854
+ points_30d: number;
1855
+ total_points: number;
1856
+ }>;
1857
+ meta: {
1858
+ project_name: string;
1859
+ };
1860
+ }>>;
1861
+ /** Link the anonymous reporter_token_hash to the caller's mushi_testers row. */
1862
+ sendMagicLink(email: string): Promise<MushiApiResponse<{
1863
+ ok: boolean;
1864
+ }>>;
1865
+ linkReporterToken(reporterTokenHash: string, jwt: string): Promise<MushiApiResponse<{
1866
+ ok: boolean;
1867
+ linked: number;
1868
+ }>>;
1869
+ /** All reports filed by the signed-in tester, across all projects. */
1870
+ getCrossAppReports(jwt: string, opts?: {
1871
+ limit?: number;
1872
+ offset?: number;
1873
+ }): Promise<MushiApiResponse<{
1874
+ reports: MushiCrossAppReport[];
1875
+ }>>;
1876
+ /** The signed-in tester's global rank + points. */
1877
+ getMyReputation(jwt: string): Promise<MushiApiResponse<{
1878
+ reputation: MushiTesterReputation | null;
1879
+ }>>;
1880
+ /** Top-N global tester leaderboard (no auth required). */
1881
+ getPublicLeaderboard(limit?: number): Promise<MushiApiResponse<{
1882
+ leaderboard: MushiLeaderboardEntry[];
1883
+ }>>;
1884
+ /** Whether the caller has a mushi_testers row. */
1885
+ getTesterStatus(jwt: string): Promise<MushiApiResponse<{
1886
+ is_tester: boolean;
1887
+ tester_id: string | null;
1888
+ public_handle: string | null;
1889
+ display_name: string | null;
1890
+ }>>;
1891
+ }
1892
+ /**
1893
+ * Wire shape of a single discovery event sent by the SDK to
1894
+ * `POST /v1/sdk/discovery`. Mirrored server-side in
1895
+ * `_shared/schemas.ts::discoveryEventSchema`.
1896
+ */
1897
+ interface MushiDiscoveryEventPayload {
1898
+ route: string;
1899
+ page_title?: string | null;
1900
+ dom_summary?: string | null;
1901
+ testids: string[];
1902
+ network_paths: string[];
1903
+ query_param_keys: string[];
1904
+ user_id_hash?: string | null;
1905
+ sdk_version?: string;
1906
+ observed_at: string;
1907
+ }
1908
+ /** Payload for /v1/sdk/session — lightweight session lifecycle tracking.
1909
+ * Best-effort, no offline queue. Privacy-safe: no PII by default;
1910
+ * `user_id_hash` is the same device-fingerprint hash used elsewhere. */
1911
+ interface MushiSessionEventPayload {
1912
+ /** Lifecycle event kind. */
1913
+ kind: 'session_start' | 'session_heartbeat' | 'session_end' | 'page_view';
1914
+ session_id: string;
1915
+ /** ISO timestamp of the event. */
1916
+ ts: string;
1917
+ /** Entry route for session_start; current route for page_view. */
1918
+ route?: string | null;
1919
+ /** document.referrer on session_start. */
1920
+ referrer?: string | null;
1921
+ /** Total page views in this session so far (updated on heartbeat/end). */
1922
+ page_view_count?: number;
1923
+ /** Device fingerprint hash — same token used in reports. */
1924
+ reporter_token_hash?: string | null;
1925
+ /** user_id_hash when the host app called Mushi.identify(). */
1926
+ user_id_hash?: string | null;
1927
+ user_agent?: string | null;
1928
+ sdk_version?: string;
1929
+ }
1930
+ type MushiPropertyValue = string | number | boolean | null;
1931
+ /** Configuration for `Mushi.track()` product analytics. */
1932
+ interface MushiAnalyticsConfig {
1933
+ /** Master switch (default true). */
1934
+ enabled?: boolean;
1935
+ /** 'implied' (default) sends immediately; 'required' buffers until setConsent('granted'). */
1936
+ consent?: 'implied' | 'required';
1937
+ /** 0..1, decided once per person (default 1). */
1938
+ sampleRate?: number;
1939
+ /** Honour navigator.doNotTrack / globalPrivacyControl (default true). */
1940
+ respectDoNotTrack?: boolean;
1941
+ /**
1942
+ * Skip tracking in WebDriver-controlled browsers (Playwright, Puppeteer,
1943
+ * Selenium), headless Chrome, Lighthouse and crawler user agents, so test
1944
+ * runs and bots never count as users (default true). Set false to exercise
1945
+ * analytics from an end-to-end test.
1946
+ */
1947
+ excludeBots?: boolean;
1948
+ /** Emit `pageview` on history navigation (default false; sessions already record page views). */
1949
+ autoPageviews?: boolean;
1950
+ /** Flush cadence in ms (default 5000, min 1000). */
1951
+ flushIntervalMs?: number;
1952
+ /** Property keys allowed through the PII key filter. */
1953
+ propertyAllowlist?: string[];
1954
+ /** Which surface these events come from (default 'web'). */
1955
+ surface?: 'web' | 'console' | 'docs' | 'cli' | 'mcp' | 'server' | 'mobile';
1956
+ }
1957
+ /** Wire shape for POST /v1/sdk/events. */
1958
+ interface MushiProductEventPayload {
1959
+ anon_id?: string | null;
1960
+ user_id?: string | null;
1961
+ user_traits?: Record<string, unknown> | null;
1962
+ session_id?: string | null;
1963
+ sdk_version?: string | null;
1964
+ surface?: MushiAnalyticsConfig['surface'];
1965
+ events: Array<{
1966
+ name: string;
1967
+ ts?: string;
1968
+ properties?: Record<string, MushiPropertyValue>;
1969
+ dedup_key?: string;
1970
+ }>;
1971
+ }
1972
+ interface MushiApiResponse<T> {
1973
+ ok: boolean;
1974
+ data?: T;
1975
+ /** `status` is the HTTP status when the server answered; absent for network errors. */
1976
+ error?: {
1977
+ code: string;
1978
+ message: string;
1979
+ status?: number;
1980
+ };
1981
+ }
1982
+ interface MushiRuntimeSdkConfig {
1983
+ enabled?: boolean;
1984
+ version?: string | null;
1985
+ widget?: MushiWidgetConfig;
1986
+ capture?: MushiCaptureConfig;
1987
+ native?: {
1988
+ triggerMode?: 'shake' | 'button' | 'both' | 'none';
1989
+ minDescriptionLength?: number;
1990
+ };
1991
+ /** When false, the widget skips background reporter-inbox polling. Default true. */
1992
+ reporterNotificationsEnabled?: boolean;
1993
+ /**
1994
+ * Opt-in reporter channels this project offers (Plan 018 §4.1). Each is
1995
+ * true only when the project turned it on AND the server can send it.
1996
+ */
1997
+ reporter?: {
1998
+ emailEnabled: boolean;
1999
+ pushEnabled: boolean;
2000
+ vapidPublicKey: string | null;
2001
+ };
2002
+ }
2003
+ interface MushiSdkVersionInfo {
2004
+ package: string;
2005
+ latest: string | null;
2006
+ current?: string;
2007
+ deprecated: boolean;
2008
+ deprecationMessage?: string | null;
2009
+ releasedAt?: string | null;
2010
+ }
2011
+ interface MushiReporterReport {
2012
+ id: string;
2013
+ status: string;
2014
+ /** @deprecated No longer sent to reporters (Plan 018); internal triage only. */
2015
+ category?: string;
2016
+ /** @deprecated No longer sent to reporters (Plan 018); internal triage only. */
2017
+ severity?: string | null;
2018
+ /** Reporter-safe title (the summary, or the first line of the description). */
2019
+ title?: string | null;
2020
+ /** The type the reporter picked (`user_category`), if any. */
2021
+ user_category?: string | null;
2022
+ /** Page path the report was filed on. */
2023
+ page?: string | null;
2024
+ app_version?: string | null;
2025
+ /** Signed, short-lived thumbnail URL; null without a screenshot. */
2026
+ screenshot_thumb_url?: string | null;
2027
+ /** Bucketed duplicate count: never a raw number. */
2028
+ group_bucket?: 'none' | 'few' | 'many';
2029
+ closed_reason?: string | null;
2030
+ fixed_in_version?: string | null;
2031
+ /** The developer asked a question and is waiting on the reporter. */
2032
+ awaiting_reporter?: boolean;
2033
+ /** True when this row is a report the reporter follows (theirs was a duplicate of it). */
2034
+ followed?: boolean;
2035
+ last_event_at?: string | null;
2036
+ /** Template-rendered (or verbatim developer) text of the latest event. */
2037
+ last_event_preview?: string | null;
2038
+ summary?: string | null;
2039
+ description?: string | null;
2040
+ created_at: string;
2041
+ last_admin_reply_at?: string | null;
2042
+ last_reporter_reply_at?: string | null;
2043
+ parent_report_id?: string | null;
2044
+ verified_at?: string | null;
2045
+ reopened_at?: string | null;
2046
+ regression_count?: number;
2047
+ unread_count?: number;
2048
+ }
2049
+ interface MushiReporterComment {
2050
+ id: number;
2051
+ author_kind: 'admin' | 'reporter';
2052
+ author_name?: string | null;
2053
+ body: string;
2054
+ visible_to_reporter?: boolean;
2055
+ created_at: string;
2056
+ }
2057
+ interface MushiHallOfFameEntry {
2058
+ display_name: string;
2059
+ email_hash: string | null;
2060
+ tier_slug: string | null;
2061
+ tier_name: string | null;
2062
+ points_30d: number;
2063
+ total_points: number;
2064
+ }
2065
+ /** A report filed by the current tester, across any project. */
2066
+ interface MushiCrossAppReport {
2067
+ id: string;
2068
+ short_id: string | null;
2069
+ title: string | null;
2070
+ category: string;
2071
+ status: string;
2072
+ created_at: string;
2073
+ updated_at: string;
2074
+ project_id: string | null;
2075
+ app_name: string | null;
2076
+ app_slug: string | null;
2077
+ /** Hostname from latest SDK heartbeat — used for favicon in cross-app list. */
2078
+ app_domain?: string | null;
2079
+ }
2080
+ /** A single entry in the global tester leaderboard. */
2081
+ interface MushiLeaderboardEntry {
2082
+ tester_id: string;
2083
+ public_handle: string | null;
2084
+ display_name: string | null;
2085
+ rank: number;
2086
+ points_30d: number;
2087
+ total_points: number;
2088
+ badge_slug?: string | null;
2089
+ }
2090
+ /** The caller's global reputation snapshot. */
2091
+ interface MushiTesterReputation {
2092
+ tester_id: string;
2093
+ public_handle: string | null;
2094
+ display_name: string | null;
2095
+ rank: number | null;
2096
+ points_30d: number;
2097
+ total_points: number;
2098
+ }
2099
+
2100
+ export { type MushiReporterReport as $, type MushiDiscoverInventoryConfig as A, type MushiDiscoveryEventPayload as B, type MushiEventHandler as C, type MushiEventType as D, type MushiHallOfFameEntry as E, type MushiIntegrationsConfig as F, type MushiLeaderboardEntry as G, type MushiNetworkEntry as H, type MushiNotificationsConfig as I, type MushiOnDeviceClassifier as J, type MushiOnDeviceClassifierInput as K, type MushiOnDeviceClassifierResult as L, type MushiApiClient as M, type MushiPageContext as N, type MushiPerformanceMetrics as O, type MushiPreset as P, type MushiPrivacyConfig as Q, type MushiProactiveConfig as R, type MushiProductEventPayload as S, type MushiPushSubscriptionJSON as T, type MushiReportBuilder as U, type MushiReportCategory as V, type MushiReportStatus as W, type MushiReporterChannelUnavailable as X, type MushiReporterComment as Y, type MushiReporterNotificationPrefs as Z, type MushiReporterPrefsUpdate as _, type MushiConfig as a, type MushiReporterReportDetail as a0, type MushiReporterTimelineItem as a1, type MushiReporterUpdates as a2, type MushiReputationResult as a3, type MushiRewardsConfig as a4, type MushiRuntimeSdkConfig as a5, type MushiSDKInstance as a6, type MushiSdkVersionInfo as a7, type MushiSelectedElement as a8, type MushiSentryConfig as a9, type MushiSentryContext as aa, type MushiSessionEventPayload as ab, type MushiTesterReputation as ac, type MushiTierResult as ad, type MushiTimelineEntry as ae, type MushiTimelineKind as af, type MushiTracePropagationConfig as ag, type MushiUrlMatcher as ah, type MushiWidgetAnchor as ai, type MushiWidgetConfig as aj, type MushiReporterChannels as ak, reporterChannels as al, subscribeBrowserPush as am, type MushiPreFilterConfig as b, type MushiReport as c, type MushiOfflineConfig as d, type MushiEnvironment as e, type MushiAnalyticsConfig as f, type MushiPropertyValue as g, type MushiBreadcrumb as h, type MushiActivityEvent as i, type MushiApiCascadeConfig as j, type MushiApiResponse as k, type MushiAssistantConfig as l, type MushiAssistantReply as m, type MushiAssistantStep as n, type MushiBannerConfig as o, type MushiBannerLink as p, type MushiBetaChangelogEntry as q, type MushiBetaModeConfig as r, type MushiCaptureConfig as s, type MushiCaptureEventInput as t, type MushiCaptureExceptionOptions as u, type MushiConsoleEntry as v, type MushiCooldownConfig as w, type MushiCrossAppReport as x, type MushiCustomCategory as y, type MushiDiagnosticsResult as z };