@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/README.md +92 -0
- package/SECURITY.md +167 -4
- package/dist/index.cjs +724 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +725 -29
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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
|
|
2267
|
-
if (
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
2273
|
-
|
|
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
|
-
|
|
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
|
|
2280
|
-
const
|
|
2636
|
+
function tagSentryScope(reportId, options = {}) {
|
|
2637
|
+
const sentry = getSentryGlobal();
|
|
2638
|
+
if (!sentry) return;
|
|
2281
2639
|
try {
|
|
2282
|
-
const
|
|
2283
|
-
if (
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
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
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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.
|
|
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;
|