@mushi-mushi/web 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, getReporterToken, getDeviceFingerprintHash, getSessionId, captureEnvironment, DEFAULT_API_ENDPOINT, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INTERNAL_HEADER } from '@mushi-mushi/core';
1
+ import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, createBreadcrumbBuffer, getReporterToken, getDeviceFingerprintHash, getSessionId, captureEnvironment, DEFAULT_API_ENDPOINT, MUSHI_INTERNAL_INIT_MARKER, MUSHI_INTERNAL_HEADER, normaliseThrown } from '@mushi-mushi/core';
2
2
 
3
3
  // src/mushi.ts
4
4
 
@@ -2050,6 +2050,17 @@ function createElementSelector() {
2050
2050
  let active = false;
2051
2051
  let overlay = null;
2052
2052
  let resolvePromise = null;
2053
+ function findNearestTestid(el) {
2054
+ let cur = el;
2055
+ let hops = 0;
2056
+ while (cur && hops < 20) {
2057
+ const tid = cur.getAttribute?.("data-testid");
2058
+ if (tid) return tid;
2059
+ cur = cur.parentElement;
2060
+ hops++;
2061
+ }
2062
+ return null;
2063
+ }
2053
2064
  function getXPath(el) {
2054
2065
  const parts = [];
2055
2066
  let current = el;
@@ -2079,7 +2090,13 @@ function createElementSelector() {
2079
2090
  y: Math.round(rect.y),
2080
2091
  width: Math.round(rect.width),
2081
2092
  height: Math.round(rect.height)
2082
- }
2093
+ },
2094
+ // v2 (whitepaper §4.7): the closest ancestor's `data-testid` lets the
2095
+ // server map this report → an Action node in the inventory graph
2096
+ // without a fuzzy NLP guess. We walk to the body so a deeply nested
2097
+ // span inside a button-with-testid still resolves correctly.
2098
+ nearestTestid: findNearestTestid(el) || void 0,
2099
+ route: typeof window !== "undefined" ? window.location.pathname : void 0
2083
2100
  };
2084
2101
  }
2085
2102
  function createOverlay() {
@@ -2258,45 +2275,402 @@ function textSnippet(el) {
2258
2275
  return text ? text.slice(0, 80) : void 0;
2259
2276
  }
2260
2277
 
2278
+ // src/capture/discovery.ts
2279
+ var DEFAULT_THROTTLE_MS = 6e4;
2280
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2281
+ var HEX24_RE = /^[0-9a-f]{20,}$/i;
2282
+ var NUMERIC_RE = /^\d+$/;
2283
+ var SLUG_HASHY_RE = /^[a-z0-9]{16,}$/i;
2284
+ function normalizeSegment(seg) {
2285
+ if (seg.length === 0) return seg;
2286
+ if (UUID_RE.test(seg)) return "[id]";
2287
+ if (HEX24_RE.test(seg)) return "[id]";
2288
+ if (NUMERIC_RE.test(seg)) return "[id]";
2289
+ if (SLUG_HASHY_RE.test(seg) && /\d/.test(seg)) return "[id]";
2290
+ return seg;
2291
+ }
2292
+ function normalizeRoute(pathname, templates) {
2293
+ const clean = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
2294
+ if (templates?.length) {
2295
+ const matched = matchTemplate(clean, templates);
2296
+ if (matched) return matched;
2297
+ }
2298
+ return "/" + clean.split("/").filter((s) => s.length > 0).map(normalizeSegment).join("/");
2299
+ }
2300
+ function matchTemplate(pathname, templates) {
2301
+ const segs = pathname.split("/").filter((s) => s.length > 0);
2302
+ const sorted = [...templates].sort(
2303
+ (a, b) => b.split("/").length - a.split("/").length
2304
+ );
2305
+ for (const tpl of sorted) {
2306
+ const tplSegs = tpl.split("/").filter((s) => s.length > 0);
2307
+ if (tplSegs.length !== segs.length) continue;
2308
+ let ok = true;
2309
+ for (let i = 0; i < tplSegs.length; i++) {
2310
+ const t = tplSegs[i];
2311
+ const s = segs[i];
2312
+ if (t.startsWith("[") && t.endsWith("]")) continue;
2313
+ if (t.startsWith(":")) continue;
2314
+ if (t === s) continue;
2315
+ ok = false;
2316
+ break;
2317
+ }
2318
+ if (ok) return "/" + tplSegs.join("/");
2319
+ }
2320
+ return null;
2321
+ }
2322
+ function readTestids() {
2323
+ if (typeof document === "undefined") return [];
2324
+ const out = /* @__PURE__ */ new Set();
2325
+ const els = document.querySelectorAll("[data-testid]");
2326
+ for (const el of Array.from(els)) {
2327
+ const v = el.getAttribute("data-testid");
2328
+ if (v && v.length > 0 && v.length < 120) out.add(v);
2329
+ }
2330
+ return Array.from(out).sort();
2331
+ }
2332
+ function readQueryParamKeys() {
2333
+ if (typeof window === "undefined") return [];
2334
+ try {
2335
+ const params = new URLSearchParams(window.location.search);
2336
+ const out = /* @__PURE__ */ new Set();
2337
+ params.forEach((_, key) => out.add(key));
2338
+ return Array.from(out).sort();
2339
+ } catch {
2340
+ return [];
2341
+ }
2342
+ }
2343
+ function readDomSummary() {
2344
+ if (typeof document === "undefined") return null;
2345
+ const trim = (s) => (s ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
2346
+ const h1 = trim(document.querySelector("h1")?.textContent);
2347
+ if (h1) return h1;
2348
+ const title = trim(document.title);
2349
+ if (title) return title;
2350
+ const main = trim(document.querySelector("main")?.textContent);
2351
+ return main || null;
2352
+ }
2353
+ async function hashUserId(input) {
2354
+ if (!input || typeof crypto === "undefined" || !crypto.subtle) return null;
2355
+ try {
2356
+ const data = new TextEncoder().encode(input);
2357
+ const buf = await crypto.subtle.digest("SHA-256", data);
2358
+ const bytes = new Uint8Array(buf);
2359
+ let hex = "";
2360
+ for (let i = 0; i < bytes.length; i++) {
2361
+ hex += bytes[i].toString(16).padStart(2, "0");
2362
+ }
2363
+ return hex;
2364
+ } catch {
2365
+ return null;
2366
+ }
2367
+ }
2368
+ function createDiscoveryCapture(opts) {
2369
+ const {
2370
+ config,
2371
+ getRecentNetworkPaths,
2372
+ getUserId,
2373
+ getSessionId: getSessionId2,
2374
+ onEvent
2375
+ } = opts;
2376
+ const throttleMs = config.throttleMs ?? DEFAULT_THROTTLE_MS;
2377
+ const captureSummary = config.captureDomSummary !== false;
2378
+ const userIdSource = config.userIdSource ?? "auto";
2379
+ const lastEmittedAt = /* @__PURE__ */ new Map();
2380
+ let lastPath = null;
2381
+ let pendingTimer = null;
2382
+ async function emitForCurrent() {
2383
+ if (typeof window === "undefined") return;
2384
+ const route = normalizeRoute(window.location.pathname, config.routeTemplates);
2385
+ const now = Date.now();
2386
+ const last = lastEmittedAt.get(route) ?? 0;
2387
+ if (now - last < throttleMs) return;
2388
+ lastEmittedAt.set(route, now);
2389
+ let userIdInput = null;
2390
+ if (userIdSource === "auto") {
2391
+ userIdInput = getUserId() ?? getSessionId2();
2392
+ } else if (userIdSource === "session-only") {
2393
+ userIdInput = getSessionId2();
2394
+ }
2395
+ const event = {
2396
+ route,
2397
+ page_title: typeof document !== "undefined" ? (document.title || "").slice(0, 300) || null : null,
2398
+ dom_summary: captureSummary ? readDomSummary() : null,
2399
+ testids: readTestids(),
2400
+ network_paths: getRecentNetworkPaths().slice(-50),
2401
+ query_param_keys: readQueryParamKeys(),
2402
+ user_id_hash: await hashUserId(userIdInput),
2403
+ observed_at: (/* @__PURE__ */ new Date()).toISOString()
2404
+ };
2405
+ onEvent(event);
2406
+ }
2407
+ function scheduleEmit() {
2408
+ if (pendingTimer) return;
2409
+ pendingTimer = setTimeout(() => {
2410
+ pendingTimer = null;
2411
+ void emitForCurrent();
2412
+ }, 100);
2413
+ }
2414
+ function onMaybeNavigation() {
2415
+ if (typeof window === "undefined") return;
2416
+ const path = window.location.pathname + window.location.search;
2417
+ if (path === lastPath) return;
2418
+ lastPath = path;
2419
+ scheduleEmit();
2420
+ }
2421
+ if (typeof window === "undefined") {
2422
+ return {
2423
+ destroy: () => void 0,
2424
+ flushNow: () => void 0
2425
+ };
2426
+ }
2427
+ const originalPush = window.history.pushState.bind(window.history);
2428
+ const originalReplace = window.history.replaceState.bind(window.history);
2429
+ const patchedPush = function patched(...args) {
2430
+ const out = originalPush(...args);
2431
+ onMaybeNavigation();
2432
+ return out;
2433
+ };
2434
+ const patchedReplace = function patched(...args) {
2435
+ const out = originalReplace(...args);
2436
+ onMaybeNavigation();
2437
+ return out;
2438
+ };
2439
+ window.history.pushState = patchedPush;
2440
+ window.history.replaceState = patchedReplace;
2441
+ const onPop = () => onMaybeNavigation();
2442
+ window.addEventListener("popstate", onPop);
2443
+ scheduleEmit();
2444
+ return {
2445
+ destroy() {
2446
+ window.removeEventListener("popstate", onPop);
2447
+ if (window.history.pushState === patchedPush) {
2448
+ window.history.pushState = originalPush;
2449
+ }
2450
+ if (window.history.replaceState === patchedReplace) {
2451
+ window.history.replaceState = originalReplace;
2452
+ }
2453
+ if (pendingTimer) {
2454
+ clearTimeout(pendingTimer);
2455
+ pendingTimer = null;
2456
+ }
2457
+ lastEmittedAt.clear();
2458
+ },
2459
+ flushNow() {
2460
+ lastEmittedAt.clear();
2461
+ void emitForCurrent();
2462
+ }
2463
+ };
2464
+ }
2465
+
2261
2466
  // src/sentry.ts
2262
2467
  function getSentryGlobal() {
2263
2468
  try {
2264
- const win = globalThis;
2265
- if (win.__SENTRY__) {
2266
- const sentry = win.__SENTRY__;
2267
- const hub = sentry.hub;
2268
- return hub;
2469
+ const w = globalThis;
2470
+ if (w.Sentry) return w.Sentry;
2471
+ return void 0;
2472
+ } catch {
2473
+ return void 0;
2474
+ }
2475
+ }
2476
+ function getSentryReplayGlobal() {
2477
+ try {
2478
+ const w = globalThis;
2479
+ return w.__SENTRY_REPLAY__;
2480
+ } catch {
2481
+ return void 0;
2482
+ }
2483
+ }
2484
+ function detectSentrySdkFamily() {
2485
+ try {
2486
+ const w = globalThis;
2487
+ const meta = w.__SENTRY__;
2488
+ const sentry = w.Sentry;
2489
+ if (meta?.version === "9" || sentry && typeof sentry.lastEventId === "function") {
2490
+ return meta?.version === "9" ? "v9" : "v8";
2491
+ }
2492
+ if (meta?.version === "8") return "v8";
2493
+ if (sentry && typeof sentry.getCurrentHub === "function") return "v7";
2494
+ return "unknown";
2495
+ } catch {
2496
+ return "unknown";
2497
+ }
2498
+ }
2499
+ function captureSentryContext(_config, options = {}) {
2500
+ const limit = Math.max(0, options.breadcrumbsLimit ?? 30);
2501
+ const out = {};
2502
+ const sentry = getSentryGlobal();
2503
+ if (!sentry) return out;
2504
+ out.sdk = detectSentrySdkFamily();
2505
+ try {
2506
+ const v8 = sentry;
2507
+ if (typeof v8.lastEventId === "function") {
2508
+ out.eventId = v8.lastEventId() ?? void 0;
2509
+ } else {
2510
+ const v7 = sentry;
2511
+ const scope2 = v7.getCurrentHub?.()?.getScope?.();
2512
+ out.eventId = scope2?.getLastEventId?.() ?? void 0;
2513
+ }
2514
+ } catch {
2515
+ }
2516
+ let scope;
2517
+ try {
2518
+ const v8 = sentry;
2519
+ if (typeof v8.getCurrentScope === "function") {
2520
+ scope = v8.getCurrentScope();
2521
+ } else {
2522
+ const v7 = sentry;
2523
+ scope = v7.getCurrentHub?.()?.getScope?.();
2524
+ }
2525
+ } catch {
2526
+ }
2527
+ if (scope) {
2528
+ try {
2529
+ const user = scope.getUser?.();
2530
+ if (user) {
2531
+ out.user = {
2532
+ id: typeof user.id === "string" ? user.id : void 0,
2533
+ email: typeof user.email === "string" ? user.email : void 0,
2534
+ username: typeof user.username === "string" ? user.username : void 0,
2535
+ ip_address: typeof user.ip_address === "string" ? user.ip_address : void 0
2536
+ };
2537
+ }
2538
+ } catch {
2539
+ }
2540
+ try {
2541
+ const tags = scope.getTags?.();
2542
+ if (tags && typeof tags === "object") {
2543
+ const pruned = {};
2544
+ for (const [k, v] of Object.entries(tags)) {
2545
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
2546
+ pruned[k] = v;
2547
+ }
2548
+ }
2549
+ if (Object.keys(pruned).length > 0) out.tags = pruned;
2550
+ }
2551
+ } catch {
2552
+ }
2553
+ try {
2554
+ out.transactionName = scope.getTransactionName?.() ?? scope.getTransaction?.()?.name ?? void 0;
2555
+ } catch {
2556
+ }
2557
+ try {
2558
+ out.sessionId = scope.getSession?.()?.sid ?? void 0;
2559
+ } catch {
2269
2560
  }
2270
- if (win.Sentry) {
2271
- return win.Sentry;
2561
+ try {
2562
+ const raw = scope.getBreadcrumbs?.() ?? scope._breadcrumbs ?? [];
2563
+ if (Array.isArray(raw) && raw.length > 0) {
2564
+ const sliced = raw.slice(-limit);
2565
+ out.breadcrumbs = sliced.map((b) => {
2566
+ const r = b;
2567
+ return {
2568
+ timestamp: typeof r.timestamp === "number" ? (
2569
+ // Sentry stores breadcrumb timestamps in seconds; convert
2570
+ // to ms so the field is comparable to Mushi's own.
2571
+ r.timestamp < 1e12 ? Math.round(r.timestamp * 1e3) : r.timestamp
2572
+ ) : void 0,
2573
+ category: typeof r.category === "string" ? r.category : void 0,
2574
+ level: typeof r.level === "string" ? r.level : void 0,
2575
+ message: typeof r.message === "string" ? r.message : void 0,
2576
+ type: typeof r.type === "string" ? r.type : void 0,
2577
+ data: r.data && typeof r.data === "object" ? r.data : void 0
2578
+ };
2579
+ });
2580
+ }
2581
+ } catch {
2582
+ }
2583
+ }
2584
+ try {
2585
+ const v8 = sentry;
2586
+ let span;
2587
+ if (typeof v8.getActiveSpan === "function") {
2588
+ span = v8.getActiveSpan();
2589
+ } else if (scope?.getSpan) {
2590
+ span = scope.getSpan();
2591
+ }
2592
+ if (span) {
2593
+ const ctx = span.spanContext?.();
2594
+ out.traceId = ctx?.traceId ?? span.traceId ?? void 0;
2595
+ out.spanId = ctx?.spanId ?? span.spanId ?? void 0;
2596
+ }
2597
+ } catch {
2598
+ }
2599
+ let client;
2600
+ try {
2601
+ const v8 = sentry;
2602
+ if (typeof v8.getClient === "function") {
2603
+ client = v8.getClient();
2604
+ } else {
2605
+ const v7 = sentry;
2606
+ client = v7.getCurrentHub?.()?.getClient?.();
2272
2607
  }
2273
2608
  } catch {
2274
2609
  }
2275
- return void 0;
2610
+ if (client) {
2611
+ try {
2612
+ const opts = client.getOptions?.();
2613
+ if (opts?.release) out.release = opts.release;
2614
+ if (opts?.environment) out.environment = opts.environment;
2615
+ } catch {
2616
+ }
2617
+ try {
2618
+ const dsn = client.getDsn?.();
2619
+ if (dsn?.host && dsn?.projectId && out.eventId) {
2620
+ const orgHost = dsn.host.replace(/^o\d+\./, "");
2621
+ out.issueUrl = `https://${orgHost}/issues/?query=${encodeURIComponent(out.eventId)}`;
2622
+ }
2623
+ } catch {
2624
+ }
2625
+ }
2626
+ try {
2627
+ const v8 = sentry;
2628
+ const replay = v8.getReplay?.() ?? getSentryReplayGlobal();
2629
+ out.replayId = replay?.getReplayId?.() ?? void 0;
2630
+ } catch {
2631
+ }
2632
+ return out;
2276
2633
  }
2277
- function captureSentryContext(_config) {
2278
- const context = {};
2634
+ function tagSentryScope(reportId, options = {}) {
2635
+ const sentry = getSentryGlobal();
2636
+ if (!sentry) return;
2279
2637
  try {
2280
- const hub = getSentryGlobal();
2281
- if (!hub) return context;
2282
- const scope = hub.getScope?.();
2283
- if (scope) {
2284
- context.eventId = scope.getLastEventId?.() ?? void 0;
2638
+ const v8 = sentry;
2639
+ if (typeof v8.setTag === "function") {
2640
+ v8.setTag("mushi.report_id", reportId);
2641
+ if (options.reportUrl) v8.setTag("mushi.report_url", options.reportUrl);
2642
+ }
2643
+ if (typeof v8.setContext === "function") {
2644
+ v8.setContext("mushi_report", {
2645
+ id: reportId,
2646
+ ...options.reportUrl ? { url: options.reportUrl } : {},
2647
+ captured_at: (/* @__PURE__ */ new Date()).toISOString()
2648
+ });
2285
2649
  }
2286
- const client = hub.getClient?.();
2287
- if (client) {
2288
- const options = client.getOptions?.();
2289
- context.release = options?.release;
2290
- context.environment = options?.environment;
2650
+ if (typeof v8.addBreadcrumb === "function") {
2651
+ v8.addBreadcrumb({
2652
+ category: "mushi",
2653
+ type: "info",
2654
+ level: "info",
2655
+ message: `Mushi report submitted (${reportId})`,
2656
+ data: { report_id: reportId, ...options.reportUrl ? { url: options.reportUrl } : {} }
2657
+ });
2291
2658
  }
2292
- const win = globalThis;
2293
- if (win.__SENTRY_REPLAY__) {
2294
- const replay = win.__SENTRY_REPLAY__;
2295
- context.replayId = replay.getReplayId?.() ?? void 0;
2659
+ } catch {
2660
+ }
2661
+ try {
2662
+ const v7 = sentry;
2663
+ const scope = v7.getCurrentHub?.()?.getScope?.();
2664
+ if (scope) {
2665
+ scope.setTag?.("mushi.report_id", reportId);
2666
+ if (options.reportUrl) scope.setTag?.("mushi.report_url", options.reportUrl);
2667
+ scope.setContext?.("mushi_report", {
2668
+ id: reportId,
2669
+ ...options.reportUrl ? { url: options.reportUrl } : {}
2670
+ });
2296
2671
  }
2297
2672
  } catch {
2298
2673
  }
2299
- return context;
2300
2674
  }
2301
2675
 
2302
2676
  // src/proactive-triggers.ts
@@ -2473,7 +2847,7 @@ function createProactiveManager(config = {}) {
2473
2847
 
2474
2848
  // src/version.ts
2475
2849
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
2476
- var MUSHI_SDK_VERSION = "0.8.0" ;
2850
+ var MUSHI_SDK_VERSION = "1.0.0" ;
2477
2851
 
2478
2852
  // src/mushi.ts
2479
2853
  var instance = null;
@@ -2521,11 +2895,36 @@ function createInstance(config) {
2521
2895
  const offlineQueue = createOfflineQueue(bootstrapConfig.offline);
2522
2896
  const rateLimiter = createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
2523
2897
  const piiScrubber = createPiiScrubber();
2898
+ function scrubBreadcrumbsForWire(crumbs) {
2899
+ return crumbs.map((c) => {
2900
+ const next = { ...c };
2901
+ if (typeof c.message === "string") {
2902
+ next.message = piiScrubber.scrub(c.message);
2903
+ }
2904
+ if (c.data && typeof c.data === "object") {
2905
+ const cleaned = {};
2906
+ for (const [k, v] of Object.entries(c.data)) {
2907
+ cleaned[k] = typeof v === "string" ? piiScrubber.scrub(v) : v;
2908
+ }
2909
+ next.data = cleaned;
2910
+ }
2911
+ return next;
2912
+ });
2913
+ }
2914
+ function scrubTagsForWire(tags) {
2915
+ if (!tags) return void 0;
2916
+ const out = {};
2917
+ for (const [k, v] of Object.entries(tags)) {
2918
+ out[k] = typeof v === "string" ? piiScrubber.scrub(v) : v;
2919
+ }
2920
+ return out;
2921
+ }
2524
2922
  let consoleCap = null;
2525
2923
  let networkCap = null;
2526
2924
  let perfCap = null;
2527
2925
  let screenshotCap = null;
2528
2926
  let elementSelector = null;
2927
+ let discoveryCap = null;
2529
2928
  const timelineCap = createTimelineCapture();
2530
2929
  let widget;
2531
2930
  function syncCaptureModules() {
@@ -2574,6 +2973,40 @@ function createInstance(config) {
2574
2973
  elementSelector = null;
2575
2974
  pendingElement = null;
2576
2975
  }
2976
+ const discoveryRaw = activeConfig.capture?.discoverInventory;
2977
+ const discoveryConfig = discoveryRaw === true ? {} : discoveryRaw && typeof discoveryRaw === "object" ? discoveryRaw : null;
2978
+ const discoveryEnabled = discoveryConfig != null && discoveryConfig.enabled !== false;
2979
+ if (discoveryEnabled) {
2980
+ discoveryCap?.destroy();
2981
+ discoveryCap = createDiscoveryCapture({
2982
+ config: discoveryConfig,
2983
+ getRecentNetworkPaths: () => {
2984
+ if (!networkCap) return [];
2985
+ return networkCap.getEntries().map((e) => {
2986
+ try {
2987
+ const u = new URL(e.url, typeof window !== "undefined" ? window.location.href : "http://localhost");
2988
+ if (u.host && typeof window !== "undefined" && u.host !== window.location.host) return null;
2989
+ return u.pathname;
2990
+ } catch {
2991
+ return null;
2992
+ }
2993
+ }).filter((p) => p != null && p.length > 0 && p.length < 200);
2994
+ },
2995
+ getUserId: () => userInfo?.id ?? null,
2996
+ getSessionId,
2997
+ onEvent: (event) => {
2998
+ void apiClient.postDiscoveryEvent({
2999
+ ...event,
3000
+ sdk_version: MUSHI_SDK_VERSION
3001
+ }).catch((err) => {
3002
+ log.debug("discovery emit failed", { err: String(err) });
3003
+ });
3004
+ }
3005
+ });
3006
+ } else {
3007
+ discoveryCap?.destroy();
3008
+ discoveryCap = null;
3009
+ }
2577
3010
  }
2578
3011
  const listeners = /* @__PURE__ */ new Map();
2579
3012
  function emit(type, data) {
@@ -2585,6 +3018,16 @@ function createInstance(config) {
2585
3018
  let runtimeConfigLoaded = false;
2586
3019
  let userInfo = null;
2587
3020
  const customMetadata = {};
3021
+ const stickyTags = {};
3022
+ const breadcrumbs = createBreadcrumbBuffer({ max: 50 });
3023
+ breadcrumbs.add({
3024
+ category: "lifecycle",
3025
+ level: "info",
3026
+ message: "Mushi SDK init",
3027
+ data: { projectId: bootstrapConfig.projectId, sdkVersion: MUSHI_SDK_VERSION }
3028
+ });
3029
+ let detachAutoBreadcrumbs = null;
3030
+ detachAutoBreadcrumbs = installAutoBreadcrumbs(breadcrumbs);
2588
3031
  widget = new MushiWidget(bootstrapConfig.widget, {
2589
3032
  onSubmit: async ({ category, description, intent }) => {
2590
3033
  log.info("Report submitted", { category, intent });
@@ -2794,6 +3237,15 @@ function createInstance(config) {
2794
3237
  const fingerprintHash = await getDeviceFingerprintHash().catch(() => null);
2795
3238
  const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
2796
3239
  const networkLogs = activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries();
3240
+ const reportBreadcrumbs = scrubBreadcrumbsForWire(breadcrumbs.getAll());
3241
+ const stickyTagSnapshot = scrubTagsForWire(
3242
+ Object.keys(stickyTags).length > 0 ? { ...stickyTags } : void 0
3243
+ );
3244
+ const sentryCtxScrubbed = sentryCtx ? {
3245
+ ...sentryCtx,
3246
+ ...sentryCtx.breadcrumbs ? { breadcrumbs: scrubBreadcrumbsForWire(sentryCtx.breadcrumbs) } : {},
3247
+ ...sentryCtx.tags ? { tags: scrubTagsForWire(sentryCtx.tags) } : {}
3248
+ } : void 0;
2797
3249
  const report = {
2798
3250
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2799
3251
  projectId: config.projectId,
@@ -2819,10 +3271,24 @@ function createInstance(config) {
2819
3271
  sdkPackage: MUSHI_SDK_PACKAGE,
2820
3272
  sdkVersion: MUSHI_SDK_VERSION,
2821
3273
  proactiveTrigger: pendingProactiveTrigger ?? void 0,
3274
+ // Top-level Sentry-grade observability fields. Breadcrumbs are
3275
+ // surfaced separately from `consoleLogs` because they're the
3276
+ // higher-signal "what just happened" trail (vs. the high-volume
3277
+ // raw console mirror), and the admin /reports drawer shows them
3278
+ // in different panes.
3279
+ ...reportBreadcrumbs.length > 0 ? { breadcrumbs: reportBreadcrumbs } : {},
3280
+ ...stickyTagSnapshot ? { tags: stickyTagSnapshot } : {},
3281
+ ...sentryCtxScrubbed ? { sentryContext: sentryCtxScrubbed } : {},
2822
3282
  sentryEventId: sentryCtx?.eventId,
2823
3283
  sentryReplayId: sentryCtx?.replayId,
2824
3284
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2825
3285
  };
3286
+ breadcrumbs.add({
3287
+ category: "lifecycle",
3288
+ level: "info",
3289
+ message: `Mushi report submitting (${category})`,
3290
+ data: { reportId: report.id, category }
3291
+ });
2826
3292
  if (config.integrations?.custom) {
2827
3293
  const builder = {
2828
3294
  addMetadata(key, value) {
@@ -2848,10 +3314,26 @@ function createInstance(config) {
2848
3314
  if (result.ok) {
2849
3315
  log.info("Report sent", { reportId: result.data?.reportId });
2850
3316
  emit("report:sent", { reportId: result.data?.reportId });
3317
+ breadcrumbs.add({
3318
+ category: "lifecycle",
3319
+ level: "info",
3320
+ message: `Mushi report sent (${result.data?.reportId ?? report.id})`
3321
+ });
3322
+ try {
3323
+ if (config.sentry && result.data?.reportId) {
3324
+ tagSentryScope(result.data.reportId);
3325
+ }
3326
+ } catch {
3327
+ }
2851
3328
  } else {
2852
3329
  log.warn("Report failed, queuing for retry", { reportId: report.id, error: result.error });
2853
3330
  await offlineQueue.enqueue(report);
2854
3331
  emit("report:failed", { reportId: report.id, error: result.error });
3332
+ breadcrumbs.add({
3333
+ category: "lifecycle",
3334
+ level: "warning",
3335
+ message: `Mushi report queued for retry (${report.id})`
3336
+ });
2855
3337
  }
2856
3338
  pendingScreenshot = null;
2857
3339
  pendingElement = null;
@@ -2920,7 +3402,12 @@ function createInstance(config) {
2920
3402
  perfCap?.destroy();
2921
3403
  elementSelector?.deactivate();
2922
3404
  timelineCap.destroy();
3405
+ discoveryCap?.destroy();
3406
+ discoveryCap = null;
2923
3407
  offlineQueue.stopAutoSync();
3408
+ detachAutoBreadcrumbs?.();
3409
+ detachAutoBreadcrumbs = null;
3410
+ breadcrumbs.clear();
2924
3411
  listeners.clear();
2925
3412
  instance = null;
2926
3413
  log.debug("Destroyed");
@@ -2935,6 +3422,16 @@ function createInstance(config) {
2935
3422
  }
2936
3423
  const description = piiScrubber.scrub(preFilter.truncate(input.description));
2937
3424
  const category = input.category ?? "bug";
3425
+ const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
3426
+ const captureBreadcrumbs = scrubBreadcrumbsForWire(breadcrumbs.getAll());
3427
+ const mergedTags = scrubTagsForWire(
3428
+ Object.keys(stickyTags).length === 0 && !input.tags ? void 0 : { ...stickyTags, ...input.tags ?? {} }
3429
+ );
3430
+ const sentryCtxScrubbed = sentryCtx ? {
3431
+ ...sentryCtx,
3432
+ ...sentryCtx.breadcrumbs ? { breadcrumbs: scrubBreadcrumbsForWire(sentryCtx.breadcrumbs) } : {},
3433
+ ...sentryCtx.tags ? { tags: scrubTagsForWire(sentryCtx.tags) } : {}
3434
+ } : void 0;
2938
3435
  const report = {
2939
3436
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2940
3437
  projectId: config.projectId,
@@ -2945,16 +3442,20 @@ function createInstance(config) {
2945
3442
  metadata: {
2946
3443
  ...input.metadata ?? {},
2947
3444
  ...userInfo ? { user: userInfo } : {},
2948
- ...input.tags ? { tags: input.tags } : {},
2949
3445
  ...input.error ? { error: input.error } : {},
2950
3446
  ...input.severity ? { severity: input.severity } : {},
2951
3447
  ...input.component ? { component: input.component } : {},
2952
3448
  ...input.source ? { source: input.source } : { source: "captureEvent" }
2953
3449
  },
3450
+ ...captureBreadcrumbs.length > 0 ? { breadcrumbs: captureBreadcrumbs } : {},
3451
+ ...mergedTags && Object.keys(mergedTags).length > 0 ? { tags: mergedTags } : {},
3452
+ ...sentryCtxScrubbed ? { sentryContext: sentryCtxScrubbed } : {},
2954
3453
  sessionId: getSessionId(),
2955
3454
  reporterToken: getReporterToken(),
2956
3455
  sdkPackage: MUSHI_SDK_PACKAGE,
2957
3456
  sdkVersion: MUSHI_SDK_VERSION,
3457
+ sentryEventId: sentryCtx?.eventId,
3458
+ sentryReplayId: sentryCtx?.replayId,
2958
3459
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2959
3460
  };
2960
3461
  emit("report:submitted", { reportId: report.id });
@@ -2966,12 +3467,43 @@ function createInstance(config) {
2966
3467
  const res = await apiClient.submitReport(report);
2967
3468
  if (res.ok) {
2968
3469
  emit("report:sent", { reportId: res.data?.reportId });
3470
+ try {
3471
+ if (config.sentry && res.data?.reportId) tagSentryScope(res.data.reportId);
3472
+ } catch {
3473
+ }
2969
3474
  return res.data?.reportId ?? null;
2970
3475
  }
2971
3476
  await offlineQueue.enqueue(report);
2972
3477
  emit("report:failed", { reportId: report.id, error: res.error });
2973
3478
  return null;
2974
3479
  },
3480
+ async captureException(error, options) {
3481
+ const normalised = normaliseThrown(error);
3482
+ breadcrumbs.add({
3483
+ category: "lifecycle",
3484
+ level: "error",
3485
+ message: `Mushi.captureException(${normalised.name}): ${normalised.message}`,
3486
+ ...normalised.stack ? { data: { stack: normalised.stack.slice(0, 500) } } : {}
3487
+ });
3488
+ const description = options?.description?.trim() || `${normalised.name}: ${normalised.message}` || "Uncaught exception";
3489
+ return sdk.captureEvent({
3490
+ description,
3491
+ category: options?.category ?? "bug",
3492
+ severity: options?.severity ?? "high",
3493
+ ...options?.component ? { component: options.component } : {},
3494
+ ...options?.tags ? { tags: options.tags } : {},
3495
+ source: options?.source ?? "captureException",
3496
+ error: {
3497
+ name: normalised.name,
3498
+ message: normalised.message,
3499
+ ...normalised.stack ? { stack: normalised.stack } : {}
3500
+ },
3501
+ metadata: {
3502
+ ...options?.metadata ?? {},
3503
+ ...normalised.cause ? { cause: normalised.cause } : {}
3504
+ }
3505
+ });
3506
+ },
2975
3507
  identify(userId, traits) {
2976
3508
  userInfo = { id: userId, ...traits?.email ? { email: traits.email } : {}, ...traits?.name ? { name: traits.name } : {} };
2977
3509
  if (traits) {
@@ -2979,6 +3511,36 @@ function createInstance(config) {
2979
3511
  if (k !== "email" && k !== "name") customMetadata[`user.${k}`] = v;
2980
3512
  }
2981
3513
  }
3514
+ breadcrumbs.add({
3515
+ category: "lifecycle",
3516
+ level: "info",
3517
+ message: `Mushi.identify(${userId})`
3518
+ });
3519
+ },
3520
+ addBreadcrumb(crumb) {
3521
+ breadcrumbs.add(crumb);
3522
+ },
3523
+ getBreadcrumbs() {
3524
+ return breadcrumbs.getAll();
3525
+ },
3526
+ setTag(key, value) {
3527
+ if (typeof key !== "string" || key.length === 0) return;
3528
+ stickyTags[key] = value;
3529
+ },
3530
+ setTags(tags) {
3531
+ if (!tags || typeof tags !== "object") return;
3532
+ for (const [k, v] of Object.entries(tags)) {
3533
+ if (typeof k === "string" && k.length > 0) {
3534
+ stickyTags[k] = v;
3535
+ }
3536
+ }
3537
+ },
3538
+ clearTag(key) {
3539
+ if (typeof key === "string" && key.length > 0) {
3540
+ delete stickyTags[key];
3541
+ return;
3542
+ }
3543
+ for (const k of Object.keys(stickyTags)) delete stickyTags[k];
2982
3544
  }
2983
3545
  };
2984
3546
  return sdk;
@@ -3216,10 +3778,144 @@ function createNoopInstance() {
3216
3778
  instance = null;
3217
3779
  },
3218
3780
  captureEvent: async () => null,
3781
+ captureException: async () => null,
3219
3782
  identify: () => {
3783
+ },
3784
+ addBreadcrumb: () => {
3785
+ },
3786
+ getBreadcrumbs: () => [],
3787
+ setTag: () => {
3788
+ },
3789
+ setTags: () => {
3790
+ },
3791
+ clearTag: () => {
3792
+ }
3793
+ };
3794
+ }
3795
+ function installAutoBreadcrumbs(buffer) {
3796
+ if (typeof window === "undefined") return () => {
3797
+ };
3798
+ const cleanups = [];
3799
+ try {
3800
+ const dispatchRouteChange = (kind) => {
3801
+ buffer.add({
3802
+ category: "navigation",
3803
+ level: "info",
3804
+ message: `${kind}: ${window.location.pathname}`,
3805
+ data: { url: window.location.href, kind }
3806
+ });
3807
+ };
3808
+ const onPop = () => dispatchRouteChange("popstate");
3809
+ window.addEventListener("popstate", onPop, { passive: true });
3810
+ cleanups.push(() => window.removeEventListener("popstate", onPop));
3811
+ const origPush = window.history.pushState;
3812
+ const origReplace = window.history.replaceState;
3813
+ window.history.pushState = function patched(...args) {
3814
+ const ret = origPush.apply(this, args);
3815
+ try {
3816
+ dispatchRouteChange("pushState");
3817
+ } catch {
3818
+ }
3819
+ return ret;
3820
+ };
3821
+ window.history.replaceState = function patched(...args) {
3822
+ const ret = origReplace.apply(this, args);
3823
+ try {
3824
+ dispatchRouteChange("replaceState");
3825
+ } catch {
3826
+ }
3827
+ return ret;
3828
+ };
3829
+ cleanups.push(() => {
3830
+ window.history.pushState = origPush;
3831
+ window.history.replaceState = origReplace;
3832
+ });
3833
+ } catch {
3834
+ }
3835
+ try {
3836
+ const origError = console.error;
3837
+ const origWarn = console.warn;
3838
+ console.error = function(...args) {
3839
+ try {
3840
+ buffer.add({
3841
+ category: "console",
3842
+ level: "error",
3843
+ message: args.map(stringifyConsoleArg).join(" ")
3844
+ });
3845
+ } catch {
3846
+ }
3847
+ return origError.apply(this, args);
3848
+ };
3849
+ console.warn = function(...args) {
3850
+ try {
3851
+ buffer.add({
3852
+ category: "console",
3853
+ level: "warning",
3854
+ message: args.map(stringifyConsoleArg).join(" ")
3855
+ });
3856
+ } catch {
3857
+ }
3858
+ return origWarn.apply(this, args);
3859
+ };
3860
+ cleanups.push(() => {
3861
+ console.error = origError;
3862
+ console.warn = origWarn;
3863
+ });
3864
+ } catch {
3865
+ }
3866
+ try {
3867
+ const onClick = (ev) => {
3868
+ try {
3869
+ const target = ev.target;
3870
+ if (!(target instanceof Element)) return;
3871
+ let cur = target;
3872
+ let hops = 0;
3873
+ while (cur && hops < 10) {
3874
+ const tid = cur.getAttribute("data-testid");
3875
+ if (tid) {
3876
+ const text = (cur.textContent ?? "").trim().slice(0, 80);
3877
+ buffer.add({
3878
+ category: "ui.click",
3879
+ level: "info",
3880
+ message: `clicked ${tid}${text ? ` \u2014 ${text}` : ""}`,
3881
+ data: { testid: tid, tag: cur.tagName.toLowerCase() }
3882
+ });
3883
+ return;
3884
+ }
3885
+ cur = cur.parentElement;
3886
+ hops++;
3887
+ }
3888
+ } catch {
3889
+ }
3890
+ };
3891
+ document.addEventListener("click", onClick, { passive: true, capture: true });
3892
+ cleanups.push(() => document.removeEventListener("click", onClick, true));
3893
+ } catch {
3894
+ }
3895
+ return () => {
3896
+ for (const c of cleanups) {
3897
+ try {
3898
+ c();
3899
+ } catch {
3900
+ }
3220
3901
  }
3221
3902
  };
3222
3903
  }
3904
+ function stringifyConsoleArg(arg) {
3905
+ try {
3906
+ if (arg instanceof Error) {
3907
+ return `${arg.name}: ${arg.message}`;
3908
+ }
3909
+ if (typeof arg === "object" && arg !== null) {
3910
+ const json = JSON.stringify(arg);
3911
+ return json.length > 200 ? `${json.slice(0, 200)}\u2026` : json;
3912
+ }
3913
+ const s = String(arg);
3914
+ return s.length > 200 ? `${s.slice(0, 200)}\u2026` : s;
3915
+ } catch {
3916
+ return `[${typeof arg}]`;
3917
+ }
3918
+ }
3223
3919
 
3224
3920
  export { Mushi, MushiWidget, createConsoleCapture, createElementSelector, createNetworkCapture, createPerformanceCapture, createProactiveManager, createScreenshotCapture, createTimelineCapture, getAvailableLocales, getLocale, setupProactiveTriggers };
3225
3921
  //# sourceMappingURL=index.js.map