@mushi-mushi/web 0.9.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
@@ -2468,42 +2468,211 @@ function createDiscoveryCapture(opts) {
2468
2468
  // src/sentry.ts
2469
2469
  function getSentryGlobal() {
2470
2470
  try {
2471
- const win = globalThis;
2472
- if (win.__SENTRY__) {
2473
- const sentry = win.__SENTRY__;
2474
- const hub = sentry.hub;
2475
- 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;
2476
2515
  }
2477
- if (win.Sentry) {
2478
- return win.Sentry;
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?.();
2479
2526
  }
2480
2527
  } catch {
2481
2528
  }
2482
- return void 0;
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 {
2562
+ }
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?.();
2609
+ }
2610
+ } catch {
2611
+ }
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;
2483
2635
  }
2484
- function captureSentryContext(_config) {
2485
- const context = {};
2636
+ function tagSentryScope(reportId, options = {}) {
2637
+ const sentry = getSentryGlobal();
2638
+ if (!sentry) return;
2486
2639
  try {
2487
- const hub = getSentryGlobal();
2488
- if (!hub) return context;
2489
- const scope = hub.getScope?.();
2490
- if (scope) {
2491
- 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
+ });
2492
2651
  }
2493
- const client = hub.getClient?.();
2494
- if (client) {
2495
- const options = client.getOptions?.();
2496
- context.release = options?.release;
2497
- 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
+ });
2498
2660
  }
2499
- const win = globalThis;
2500
- if (win.__SENTRY_REPLAY__) {
2501
- const replay = win.__SENTRY_REPLAY__;
2502
- 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
+ });
2503
2673
  }
2504
2674
  } catch {
2505
2675
  }
2506
- return context;
2507
2676
  }
2508
2677
 
2509
2678
  // src/proactive-triggers.ts
@@ -2680,7 +2849,7 @@ function createProactiveManager(config = {}) {
2680
2849
 
2681
2850
  // src/version.ts
2682
2851
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
2683
- var MUSHI_SDK_VERSION = "0.9.0" ;
2852
+ var MUSHI_SDK_VERSION = "1.0.0" ;
2684
2853
 
2685
2854
  // src/mushi.ts
2686
2855
  var instance = null;
@@ -2728,6 +2897,30 @@ function createInstance(config) {
2728
2897
  const offlineQueue = core.createOfflineQueue(bootstrapConfig.offline);
2729
2898
  const rateLimiter = core.createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
2730
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
+ }
2731
2924
  let consoleCap = null;
2732
2925
  let networkCap = null;
2733
2926
  let perfCap = null;
@@ -2827,6 +3020,16 @@ function createInstance(config) {
2827
3020
  let runtimeConfigLoaded = false;
2828
3021
  let userInfo = null;
2829
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);
2830
3033
  widget = new MushiWidget(bootstrapConfig.widget, {
2831
3034
  onSubmit: async ({ category, description, intent }) => {
2832
3035
  log.info("Report submitted", { category, intent });
@@ -3036,6 +3239,15 @@ function createInstance(config) {
3036
3239
  const fingerprintHash = await core.getDeviceFingerprintHash().catch(() => null);
3037
3240
  const consoleLogs = activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries();
3038
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;
3039
3251
  const report = {
3040
3252
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
3041
3253
  projectId: config.projectId,
@@ -3061,10 +3273,24 @@ function createInstance(config) {
3061
3273
  sdkPackage: MUSHI_SDK_PACKAGE,
3062
3274
  sdkVersion: MUSHI_SDK_VERSION,
3063
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 } : {},
3064
3284
  sentryEventId: sentryCtx?.eventId,
3065
3285
  sentryReplayId: sentryCtx?.replayId,
3066
3286
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3067
3287
  };
3288
+ breadcrumbs.add({
3289
+ category: "lifecycle",
3290
+ level: "info",
3291
+ message: `Mushi report submitting (${category})`,
3292
+ data: { reportId: report.id, category }
3293
+ });
3068
3294
  if (config.integrations?.custom) {
3069
3295
  const builder = {
3070
3296
  addMetadata(key, value) {
@@ -3090,10 +3316,26 @@ function createInstance(config) {
3090
3316
  if (result.ok) {
3091
3317
  log.info("Report sent", { reportId: result.data?.reportId });
3092
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
+ }
3093
3330
  } else {
3094
3331
  log.warn("Report failed, queuing for retry", { reportId: report.id, error: result.error });
3095
3332
  await offlineQueue.enqueue(report);
3096
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
+ });
3097
3339
  }
3098
3340
  pendingScreenshot = null;
3099
3341
  pendingElement = null;
@@ -3165,6 +3407,9 @@ function createInstance(config) {
3165
3407
  discoveryCap?.destroy();
3166
3408
  discoveryCap = null;
3167
3409
  offlineQueue.stopAutoSync();
3410
+ detachAutoBreadcrumbs?.();
3411
+ detachAutoBreadcrumbs = null;
3412
+ breadcrumbs.clear();
3168
3413
  listeners.clear();
3169
3414
  instance = null;
3170
3415
  log.debug("Destroyed");
@@ -3179,6 +3424,16 @@ function createInstance(config) {
3179
3424
  }
3180
3425
  const description = piiScrubber.scrub(preFilter.truncate(input.description));
3181
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;
3182
3437
  const report = {
3183
3438
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
3184
3439
  projectId: config.projectId,
@@ -3189,16 +3444,20 @@ function createInstance(config) {
3189
3444
  metadata: {
3190
3445
  ...input.metadata ?? {},
3191
3446
  ...userInfo ? { user: userInfo } : {},
3192
- ...input.tags ? { tags: input.tags } : {},
3193
3447
  ...input.error ? { error: input.error } : {},
3194
3448
  ...input.severity ? { severity: input.severity } : {},
3195
3449
  ...input.component ? { component: input.component } : {},
3196
3450
  ...input.source ? { source: input.source } : { source: "captureEvent" }
3197
3451
  },
3452
+ ...captureBreadcrumbs.length > 0 ? { breadcrumbs: captureBreadcrumbs } : {},
3453
+ ...mergedTags && Object.keys(mergedTags).length > 0 ? { tags: mergedTags } : {},
3454
+ ...sentryCtxScrubbed ? { sentryContext: sentryCtxScrubbed } : {},
3198
3455
  sessionId: core.getSessionId(),
3199
3456
  reporterToken: core.getReporterToken(),
3200
3457
  sdkPackage: MUSHI_SDK_PACKAGE,
3201
3458
  sdkVersion: MUSHI_SDK_VERSION,
3459
+ sentryEventId: sentryCtx?.eventId,
3460
+ sentryReplayId: sentryCtx?.replayId,
3202
3461
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3203
3462
  };
3204
3463
  emit("report:submitted", { reportId: report.id });
@@ -3210,12 +3469,43 @@ function createInstance(config) {
3210
3469
  const res = await apiClient.submitReport(report);
3211
3470
  if (res.ok) {
3212
3471
  emit("report:sent", { reportId: res.data?.reportId });
3472
+ try {
3473
+ if (config.sentry && res.data?.reportId) tagSentryScope(res.data.reportId);
3474
+ } catch {
3475
+ }
3213
3476
  return res.data?.reportId ?? null;
3214
3477
  }
3215
3478
  await offlineQueue.enqueue(report);
3216
3479
  emit("report:failed", { reportId: report.id, error: res.error });
3217
3480
  return null;
3218
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
+ },
3219
3509
  identify(userId, traits) {
3220
3510
  userInfo = { id: userId, ...traits?.email ? { email: traits.email } : {}, ...traits?.name ? { name: traits.name } : {} };
3221
3511
  if (traits) {
@@ -3223,6 +3513,36 @@ function createInstance(config) {
3223
3513
  if (k !== "email" && k !== "name") customMetadata[`user.${k}`] = v;
3224
3514
  }
3225
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];
3226
3546
  }
3227
3547
  };
3228
3548
  return sdk;
@@ -3460,10 +3780,144 @@ function createNoopInstance() {
3460
3780
  instance = null;
3461
3781
  },
3462
3782
  captureEvent: async () => null,
3783
+ captureException: async () => null,
3463
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
+ }
3464
3903
  }
3465
3904
  };
3466
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
+ }
3467
3921
 
3468
3922
  exports.Mushi = Mushi;
3469
3923
  exports.MushiWidget = MushiWidget;