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