@mushi-mushi/web 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,8 +19,17 @@ Browser SDK for Mushi Mushi — embeddable bug reporting widget with Shadow DOM
19
19
  - Light/dark theme with auto-detection (`prefers-color-scheme`)
20
20
  - **Trigger modes** (0.6+) — `auto` / `edge-tab` / `attach` (bring-your-own-button) / `manual` / `hidden`, plus `smartHide`, `hideOnSelector`, `hideOnRoutes`, configurable `inset` and `respectSafeArea`
21
21
  - **Runtime trigger APIs** — `Mushi.show()`, `Mushi.hide()`, `Mushi.attachTo(selector)`, `Mushi.setTrigger(mode)`, `Mushi.openWith(category)`
22
+ - **Widget anchor** (0.9+) — `widget.anchor` accepts raw CSS (including `var()` and `env()`) so the launcher honours your app shell's tab bars, docks, mini-players, and cookie banners without Shadow-DOM patching
23
+ - **Presets** (0.9+) — `preset: 'production-calm' | 'beta-loud' | 'internal-debug' | 'manual-only'` flips a coherent bundle of widget / capture / proactive defaults so prod apps stay quiet and internal builds stay loud
22
24
  - **Proactive triggers** — rage click, long task, API cascade failure detection
23
25
  - **Report fatigue prevention** — session limits, cooldowns, permanent suppression
26
+ - **Privacy controls** (0.9.1+) — `privacy.maskSelectors`, `privacy.blockSelectors`, `privacy.allowUserRemoveScreenshot` for selector-level screenshot redaction and a one-tap "Remove screenshot" button in the panel
27
+ - **Repro timeline** (0.10+) — auto-captures route changes, clicks, and SDK lifecycle into a normalised `MushiReport.timeline`; pair with `Mushi.setScreen({ name, route, feature })` for screen-level grouping in the admin
28
+ - **Two-way replies** (0.11+) — the panel ships a "Your reports" view that polls comments authored by the dev team and lets the reporter reply, all signed with HMAC against the public API key (no auth user required)
29
+ - **Passive inventory discovery** (0.12+) — opt-in `capture.discoverInventory` ships throttled, PII-free observations (route template, page title, `[data-testid]` values, recent fetch paths, query-param **keys** only, sha256 of user/session id) to `POST /v1/sdk/discovery`. The Mushi server aggregates them into a 30-day `discovery_observed_inventory` view and Claude Sonnet drafts a first-pass `inventory.yaml` proposal you can accept on `/inventory ▸ Discovery`. See `MushiDiscoverInventoryConfig` in [`@mushi-mushi/core`](../core)
30
+ - **SDK identity & freshness** (0.8+) — every report ships `sdkPackage` + `sdkVersion`; the widget polls `/v1/sdk/latest-version` and surfaces an outdated banner (configurable via `widget.outdatedBanner`)
31
+ - **Self-noise filters** (0.7.1+) — internal Mushi requests are tagged with `X-Mushi-Internal` and excluded from network capture + `apiCascade`; configurable `capture.ignoreUrls` and `proactive.apiCascade.ignoreUrls` for host-app endpoints you also don't want counted
32
+ - **`Mushi.diagnose()`** (0.7.1+) — one-call CSP / runtime-config / capture / widget health check (also runs without an init for pre-install smoke tests)
24
33
  - Keyboard-first: `Esc` to close, `⌘/Ctrl + Enter` to submit, focus-trapped panel
25
34
  - Honours `prefers-reduced-motion` (animations collapse to instant)
26
35
 
@@ -163,6 +172,159 @@ setupProactiveTriggers({
163
172
  });
164
173
  ```
165
174
 
175
+ ### Self-noise filters and CSP diagnostics
176
+
177
+ Out of the box the SDK tags every request it makes with `X-Mushi-Internal` and skips
178
+ those URLs in `capture.network` + `proactive.apiCascade`, so an unconfigured CSP
179
+ or a flaky local Supabase stack can no longer make Mushi report on Mushi:
180
+
181
+ ```typescript
182
+ Mushi.init({
183
+ projectId: 'proj_xxx',
184
+ apiKey: 'mushi_xxx',
185
+ // 'auto' (default) skips the runtime-config fetch on localhost endpoints —
186
+ // pass `true` to force it everywhere, `false` to disable entirely.
187
+ runtimeConfig: 'auto',
188
+ capture: {
189
+ network: true,
190
+ ignoreUrls: [/\/api\/internal\//, 'https://posthog.example.com'],
191
+ },
192
+ proactive: {
193
+ apiCascade: {
194
+ enabled: true,
195
+ ignoreUrls: ['https://feature-flags.example.com'],
196
+ },
197
+ },
198
+ });
199
+
200
+ const health = await Mushi.diagnose();
201
+ // → { apiEndpointReachable, cspAllowsEndpoint, widgetMounted, shadowDomAvailable,
202
+ // dialogSupported, runtimeConfigLoaded, captureScreenshotAvailable,
203
+ // captureNetworkIntercepting, sdkVersion }
204
+ ```
205
+
206
+ `Mushi.diagnose()` works **before** `Mushi.init()` too — call it from a debug
207
+ console or installer wizard to surface CSP / endpoint problems with zero risk
208
+ of accidentally booting the widget.
209
+
210
+ ### Presets and widget anchor
211
+
212
+ ```typescript
213
+ Mushi.init({
214
+ projectId: 'proj_xxx',
215
+ apiKey: 'mushi_xxx',
216
+ // production-calm = manual trigger, screenshot only on report, no proactive prompts
217
+ // beta-loud = proactive triggers + console + network always on
218
+ // internal-debug = above + verbose debug + always-on screenshot
219
+ // manual-only = trigger only, every proactive surface off
220
+ preset: 'production-calm',
221
+ widget: {
222
+ // Raw CSS strings (including `var()` and `env()`) win over `position` /
223
+ // `inset` so the launcher tracks your app shell's tab bars or mini-player.
224
+ anchor: {
225
+ bottom: 'calc(var(--app-dock-h, 0px) + env(safe-area-inset-bottom))',
226
+ right: 'calc(0.75rem + env(safe-area-inset-right))',
227
+ },
228
+ brandFooter: true,
229
+ outdatedBanner: 'auto',
230
+ },
231
+ });
232
+ ```
233
+
234
+ ### Privacy and screenshot redaction
235
+
236
+ ```typescript
237
+ Mushi.init({
238
+ projectId: 'proj_xxx',
239
+ apiKey: 'mushi_xxx',
240
+ privacy: {
241
+ maskSelectors: ['[data-private]', 'input', '.thai-answer-draft'],
242
+ blockSelectors: ['[data-payment]', '[data-auth-token]'],
243
+ allowUserRemoveScreenshot: true,
244
+ },
245
+ });
246
+ ```
247
+
248
+ `maskSelectors` paints a solid block over matching elements before serialisation;
249
+ `blockSelectors` removes them entirely. `allowUserRemoveScreenshot` adds a
250
+ "Remove screenshot" affordance next to the attachment chip in the panel, so the
251
+ reporter can yank a screenshot they didn't realise contained sensitive data.
252
+
253
+ ### Repro timeline and `setScreen()`
254
+
255
+ ```typescript
256
+ const mushi = Mushi.init({ /* ... */ });
257
+ mushi.setScreen({ name: 'Chat', route: '/chat', feature: 'roleplay' });
258
+ ```
259
+
260
+ The SDK auto-records `route` (initial + `pushState` / `popstate` / `hashchange`),
261
+ `click` (with selector + text snippet), and `screen` events into a 120-entry
262
+ ring buffer. Submissions ship the trail as `MushiReport.timeline` and the admin
263
+ console renders it as a chronological "what happened before the report" card on
264
+ `/reports/:id`.
265
+
266
+ ### Two-way replies (Your reports)
267
+
268
+ The widget mounts a "Your reports" tab that lists this reporter's history,
269
+ unread admin replies (with a count badge on the trigger), and a reply input.
270
+ Calls are signed with an HMAC over `projectId.timestamp.sha256(reporterToken)`
271
+ using the public API key as the secret, so no Supabase auth is required.
272
+
273
+ ```typescript
274
+ Mushi.init({
275
+ projectId: 'proj_xxx',
276
+ apiKey: 'mushi_xxx',
277
+ widget: { brandFooter: true, outdatedBanner: 'auto' },
278
+ });
279
+ ```
280
+
281
+ Endpoints (Edge Function): `GET /v1/reporter/reports`,
282
+ `GET /v1/reporter/reports/:id/comments`, `POST /v1/reporter/reports/:id/reply`.
283
+ The DB-side `report_comments_fanout_to_reporter` trigger creates a
284
+ `reporter_notifications` row whenever a `visible_to_reporter` admin comment
285
+ lands, so the unread count stays in sync without polling.
286
+
287
+ ### Passive inventory discovery (v2.1)
288
+
289
+ ```typescript
290
+ Mushi.init({
291
+ projectId: 'proj_xxx',
292
+ apiKey: 'mushi_xxx',
293
+ capture: {
294
+ // `true` enables defaults (60s per-route throttle, heuristic
295
+ // route normalisation). Pass an object for fine-grained control.
296
+ discoverInventory: {
297
+ enabled: true,
298
+ throttleMs: 60_000,
299
+ // Optional — your framework's known route templates so we don't
300
+ // have to guess `/practice/abc-123` → `/practice/[id]`.
301
+ routeTemplates: ['/practice/[id]', '/lessons/[slug]'],
302
+ },
303
+ },
304
+ });
305
+ ```
306
+
307
+ Each emission is one row on `POST /v1/sdk/discovery`:
308
+
309
+ ```jsonc
310
+ {
311
+ "route": "/practice/[id]",
312
+ "page_title": "Practice — Glot.it",
313
+ "dom_summary": "…≤200 chars…",
314
+ "testids": ["practice-submit", "practice-hint"],
315
+ "network_paths": ["/api/practice/run", "/rest/v1/answers"],
316
+ "query_param_keys": ["lang"],
317
+ "user_id_hash": "sha256(…)",
318
+ "observed_at": "2026-05-04T12:00:00Z"
319
+ }
320
+ ```
321
+
322
+ Open `/inventory ▸ Discovery` in the admin to watch routes accumulate,
323
+ hit **Generate proposal**, then **Accept** to write the LLM-drafted
324
+ `inventory.yaml` into the project. Nothing else changes about the SDK —
325
+ the discovery channel is independent of the bug-report widget and stays
326
+ quiet under `prefers-reduced-motion` / when the tab is hidden.
327
+
166
328
  ## Test utilities (`./test-utils`)
167
329
 
168
330
  Deterministic Playwright / jsdom helpers, published as a separate
@@ -172,11 +334,14 @@ entry-point so production bundles pay nothing for them:
172
334
  import { triggerBug, openReport, waitForQueueDrain } from '@mushi-mushi/web/test-utils';
173
335
  ```
174
336
 
175
- | Export | Purpose |
176
- |---------------------|-------------------------------------------------------------------------|
177
- | `triggerBug(opts?)` | Submit a report bypassing the widget. Returns the server-assigned id. |
178
- | `openReport(cat?)` | Open the widget programmatically without submitting. |
179
- | `waitForQueueDrain` | Resolve once the offline queue is empty (number remaining at timeout). |
337
+ | Export | Purpose |
338
+ |------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
339
+ | `triggerBug(opts?)` | Submit a report bypassing the widget. Returns the server-assigned id. |
340
+ | `openReport(cat?)` | Open the widget programmatically without submitting. |
341
+ | `openMushiWidget(cat?)` | Alias for `openReport` Playwright-friendly name for the dogfood contract suite. |
342
+ | `waitForQueueDrain` | Resolve once the offline queue is empty (number remaining at timeout). |
343
+ | `expectMushiReady(opts?)` | Resolve with a `MushiDiagnosticsResult` once the SDK is initialised and reachable. Fails if `apiEndpointReachable === false`. |
344
+ | `expectNoMushiSelfCascade()` | Run an action and assert no internal Mushi request fired the `api_cascade` proactive trigger. Catches CSP / runtime-config self-noise. |
180
345
 
181
346
  Every helper no-ops when `Mushi.getInstance()` returns `null`, so
182
347
  conditional-wiring tests (e.g. cloud vs local targets) don't need to