@mushi-mushi/web 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/index.cjs +244 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +244 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2050,6 +2050,17 @@ function createElementSelector() {
|
|
|
2050
2050
|
let active = false;
|
|
2051
2051
|
let overlay = null;
|
|
2052
2052
|
let resolvePromise = null;
|
|
2053
|
+
function findNearestTestid(el) {
|
|
2054
|
+
let cur = el;
|
|
2055
|
+
let hops = 0;
|
|
2056
|
+
while (cur && hops < 20) {
|
|
2057
|
+
const tid = cur.getAttribute?.("data-testid");
|
|
2058
|
+
if (tid) return tid;
|
|
2059
|
+
cur = cur.parentElement;
|
|
2060
|
+
hops++;
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2053
2064
|
function getXPath(el) {
|
|
2054
2065
|
const parts = [];
|
|
2055
2066
|
let current = el;
|
|
@@ -2079,7 +2090,13 @@ function createElementSelector() {
|
|
|
2079
2090
|
y: Math.round(rect.y),
|
|
2080
2091
|
width: Math.round(rect.width),
|
|
2081
2092
|
height: Math.round(rect.height)
|
|
2082
|
-
}
|
|
2093
|
+
},
|
|
2094
|
+
// v2 (whitepaper §4.7): the closest ancestor's `data-testid` lets the
|
|
2095
|
+
// server map this report → an Action node in the inventory graph
|
|
2096
|
+
// without a fuzzy NLP guess. We walk to the body so a deeply nested
|
|
2097
|
+
// span inside a button-with-testid still resolves correctly.
|
|
2098
|
+
nearestTestid: findNearestTestid(el) || void 0,
|
|
2099
|
+
route: typeof window !== "undefined" ? window.location.pathname : void 0
|
|
2083
2100
|
};
|
|
2084
2101
|
}
|
|
2085
2102
|
function createOverlay() {
|
|
@@ -2258,6 +2275,194 @@ function textSnippet(el) {
|
|
|
2258
2275
|
return text ? text.slice(0, 80) : void 0;
|
|
2259
2276
|
}
|
|
2260
2277
|
|
|
2278
|
+
// src/capture/discovery.ts
|
|
2279
|
+
var DEFAULT_THROTTLE_MS = 6e4;
|
|
2280
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2281
|
+
var HEX24_RE = /^[0-9a-f]{20,}$/i;
|
|
2282
|
+
var NUMERIC_RE = /^\d+$/;
|
|
2283
|
+
var SLUG_HASHY_RE = /^[a-z0-9]{16,}$/i;
|
|
2284
|
+
function normalizeSegment(seg) {
|
|
2285
|
+
if (seg.length === 0) return seg;
|
|
2286
|
+
if (UUID_RE.test(seg)) return "[id]";
|
|
2287
|
+
if (HEX24_RE.test(seg)) return "[id]";
|
|
2288
|
+
if (NUMERIC_RE.test(seg)) return "[id]";
|
|
2289
|
+
if (SLUG_HASHY_RE.test(seg) && /\d/.test(seg)) return "[id]";
|
|
2290
|
+
return seg;
|
|
2291
|
+
}
|
|
2292
|
+
function normalizeRoute(pathname, templates) {
|
|
2293
|
+
const clean = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
2294
|
+
if (templates?.length) {
|
|
2295
|
+
const matched = matchTemplate(clean, templates);
|
|
2296
|
+
if (matched) return matched;
|
|
2297
|
+
}
|
|
2298
|
+
return "/" + clean.split("/").filter((s) => s.length > 0).map(normalizeSegment).join("/");
|
|
2299
|
+
}
|
|
2300
|
+
function matchTemplate(pathname, templates) {
|
|
2301
|
+
const segs = pathname.split("/").filter((s) => s.length > 0);
|
|
2302
|
+
const sorted = [...templates].sort(
|
|
2303
|
+
(a, b) => b.split("/").length - a.split("/").length
|
|
2304
|
+
);
|
|
2305
|
+
for (const tpl of sorted) {
|
|
2306
|
+
const tplSegs = tpl.split("/").filter((s) => s.length > 0);
|
|
2307
|
+
if (tplSegs.length !== segs.length) continue;
|
|
2308
|
+
let ok = true;
|
|
2309
|
+
for (let i = 0; i < tplSegs.length; i++) {
|
|
2310
|
+
const t = tplSegs[i];
|
|
2311
|
+
const s = segs[i];
|
|
2312
|
+
if (t.startsWith("[") && t.endsWith("]")) continue;
|
|
2313
|
+
if (t.startsWith(":")) continue;
|
|
2314
|
+
if (t === s) continue;
|
|
2315
|
+
ok = false;
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
if (ok) return "/" + tplSegs.join("/");
|
|
2319
|
+
}
|
|
2320
|
+
return null;
|
|
2321
|
+
}
|
|
2322
|
+
function readTestids() {
|
|
2323
|
+
if (typeof document === "undefined") return [];
|
|
2324
|
+
const out = /* @__PURE__ */ new Set();
|
|
2325
|
+
const els = document.querySelectorAll("[data-testid]");
|
|
2326
|
+
for (const el of Array.from(els)) {
|
|
2327
|
+
const v = el.getAttribute("data-testid");
|
|
2328
|
+
if (v && v.length > 0 && v.length < 120) out.add(v);
|
|
2329
|
+
}
|
|
2330
|
+
return Array.from(out).sort();
|
|
2331
|
+
}
|
|
2332
|
+
function readQueryParamKeys() {
|
|
2333
|
+
if (typeof window === "undefined") return [];
|
|
2334
|
+
try {
|
|
2335
|
+
const params = new URLSearchParams(window.location.search);
|
|
2336
|
+
const out = /* @__PURE__ */ new Set();
|
|
2337
|
+
params.forEach((_, key) => out.add(key));
|
|
2338
|
+
return Array.from(out).sort();
|
|
2339
|
+
} catch {
|
|
2340
|
+
return [];
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function readDomSummary() {
|
|
2344
|
+
if (typeof document === "undefined") return null;
|
|
2345
|
+
const trim = (s) => (s ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2346
|
+
const h1 = trim(document.querySelector("h1")?.textContent);
|
|
2347
|
+
if (h1) return h1;
|
|
2348
|
+
const title = trim(document.title);
|
|
2349
|
+
if (title) return title;
|
|
2350
|
+
const main = trim(document.querySelector("main")?.textContent);
|
|
2351
|
+
return main || null;
|
|
2352
|
+
}
|
|
2353
|
+
async function hashUserId(input) {
|
|
2354
|
+
if (!input || typeof crypto === "undefined" || !crypto.subtle) return null;
|
|
2355
|
+
try {
|
|
2356
|
+
const data = new TextEncoder().encode(input);
|
|
2357
|
+
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
2358
|
+
const bytes = new Uint8Array(buf);
|
|
2359
|
+
let hex = "";
|
|
2360
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2361
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
2362
|
+
}
|
|
2363
|
+
return hex;
|
|
2364
|
+
} catch {
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function createDiscoveryCapture(opts) {
|
|
2369
|
+
const {
|
|
2370
|
+
config,
|
|
2371
|
+
getRecentNetworkPaths,
|
|
2372
|
+
getUserId,
|
|
2373
|
+
getSessionId: getSessionId2,
|
|
2374
|
+
onEvent
|
|
2375
|
+
} = opts;
|
|
2376
|
+
const throttleMs = config.throttleMs ?? DEFAULT_THROTTLE_MS;
|
|
2377
|
+
const captureSummary = config.captureDomSummary !== false;
|
|
2378
|
+
const userIdSource = config.userIdSource ?? "auto";
|
|
2379
|
+
const lastEmittedAt = /* @__PURE__ */ new Map();
|
|
2380
|
+
let lastPath = null;
|
|
2381
|
+
let pendingTimer = null;
|
|
2382
|
+
async function emitForCurrent() {
|
|
2383
|
+
if (typeof window === "undefined") return;
|
|
2384
|
+
const route = normalizeRoute(window.location.pathname, config.routeTemplates);
|
|
2385
|
+
const now = Date.now();
|
|
2386
|
+
const last = lastEmittedAt.get(route) ?? 0;
|
|
2387
|
+
if (now - last < throttleMs) return;
|
|
2388
|
+
lastEmittedAt.set(route, now);
|
|
2389
|
+
let userIdInput = null;
|
|
2390
|
+
if (userIdSource === "auto") {
|
|
2391
|
+
userIdInput = getUserId() ?? getSessionId2();
|
|
2392
|
+
} else if (userIdSource === "session-only") {
|
|
2393
|
+
userIdInput = getSessionId2();
|
|
2394
|
+
}
|
|
2395
|
+
const event = {
|
|
2396
|
+
route,
|
|
2397
|
+
page_title: typeof document !== "undefined" ? (document.title || "").slice(0, 300) || null : null,
|
|
2398
|
+
dom_summary: captureSummary ? readDomSummary() : null,
|
|
2399
|
+
testids: readTestids(),
|
|
2400
|
+
network_paths: getRecentNetworkPaths().slice(-50),
|
|
2401
|
+
query_param_keys: readQueryParamKeys(),
|
|
2402
|
+
user_id_hash: await hashUserId(userIdInput),
|
|
2403
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2404
|
+
};
|
|
2405
|
+
onEvent(event);
|
|
2406
|
+
}
|
|
2407
|
+
function scheduleEmit() {
|
|
2408
|
+
if (pendingTimer) return;
|
|
2409
|
+
pendingTimer = setTimeout(() => {
|
|
2410
|
+
pendingTimer = null;
|
|
2411
|
+
void emitForCurrent();
|
|
2412
|
+
}, 100);
|
|
2413
|
+
}
|
|
2414
|
+
function onMaybeNavigation() {
|
|
2415
|
+
if (typeof window === "undefined") return;
|
|
2416
|
+
const path = window.location.pathname + window.location.search;
|
|
2417
|
+
if (path === lastPath) return;
|
|
2418
|
+
lastPath = path;
|
|
2419
|
+
scheduleEmit();
|
|
2420
|
+
}
|
|
2421
|
+
if (typeof window === "undefined") {
|
|
2422
|
+
return {
|
|
2423
|
+
destroy: () => void 0,
|
|
2424
|
+
flushNow: () => void 0
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
const originalPush = window.history.pushState.bind(window.history);
|
|
2428
|
+
const originalReplace = window.history.replaceState.bind(window.history);
|
|
2429
|
+
const patchedPush = function patched(...args) {
|
|
2430
|
+
const out = originalPush(...args);
|
|
2431
|
+
onMaybeNavigation();
|
|
2432
|
+
return out;
|
|
2433
|
+
};
|
|
2434
|
+
const patchedReplace = function patched(...args) {
|
|
2435
|
+
const out = originalReplace(...args);
|
|
2436
|
+
onMaybeNavigation();
|
|
2437
|
+
return out;
|
|
2438
|
+
};
|
|
2439
|
+
window.history.pushState = patchedPush;
|
|
2440
|
+
window.history.replaceState = patchedReplace;
|
|
2441
|
+
const onPop = () => onMaybeNavigation();
|
|
2442
|
+
window.addEventListener("popstate", onPop);
|
|
2443
|
+
scheduleEmit();
|
|
2444
|
+
return {
|
|
2445
|
+
destroy() {
|
|
2446
|
+
window.removeEventListener("popstate", onPop);
|
|
2447
|
+
if (window.history.pushState === patchedPush) {
|
|
2448
|
+
window.history.pushState = originalPush;
|
|
2449
|
+
}
|
|
2450
|
+
if (window.history.replaceState === patchedReplace) {
|
|
2451
|
+
window.history.replaceState = originalReplace;
|
|
2452
|
+
}
|
|
2453
|
+
if (pendingTimer) {
|
|
2454
|
+
clearTimeout(pendingTimer);
|
|
2455
|
+
pendingTimer = null;
|
|
2456
|
+
}
|
|
2457
|
+
lastEmittedAt.clear();
|
|
2458
|
+
},
|
|
2459
|
+
flushNow() {
|
|
2460
|
+
lastEmittedAt.clear();
|
|
2461
|
+
void emitForCurrent();
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2261
2466
|
// src/sentry.ts
|
|
2262
2467
|
function getSentryGlobal() {
|
|
2263
2468
|
try {
|
|
@@ -2473,7 +2678,7 @@ function createProactiveManager(config = {}) {
|
|
|
2473
2678
|
|
|
2474
2679
|
// src/version.ts
|
|
2475
2680
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
2476
|
-
var MUSHI_SDK_VERSION = "0.
|
|
2681
|
+
var MUSHI_SDK_VERSION = "0.9.0" ;
|
|
2477
2682
|
|
|
2478
2683
|
// src/mushi.ts
|
|
2479
2684
|
var instance = null;
|
|
@@ -2526,6 +2731,7 @@ function createInstance(config) {
|
|
|
2526
2731
|
let perfCap = null;
|
|
2527
2732
|
let screenshotCap = null;
|
|
2528
2733
|
let elementSelector = null;
|
|
2734
|
+
let discoveryCap = null;
|
|
2529
2735
|
const timelineCap = createTimelineCapture();
|
|
2530
2736
|
let widget;
|
|
2531
2737
|
function syncCaptureModules() {
|
|
@@ -2574,6 +2780,40 @@ function createInstance(config) {
|
|
|
2574
2780
|
elementSelector = null;
|
|
2575
2781
|
pendingElement = null;
|
|
2576
2782
|
}
|
|
2783
|
+
const discoveryRaw = activeConfig.capture?.discoverInventory;
|
|
2784
|
+
const discoveryConfig = discoveryRaw === true ? {} : discoveryRaw && typeof discoveryRaw === "object" ? discoveryRaw : null;
|
|
2785
|
+
const discoveryEnabled = discoveryConfig != null && discoveryConfig.enabled !== false;
|
|
2786
|
+
if (discoveryEnabled) {
|
|
2787
|
+
discoveryCap?.destroy();
|
|
2788
|
+
discoveryCap = createDiscoveryCapture({
|
|
2789
|
+
config: discoveryConfig,
|
|
2790
|
+
getRecentNetworkPaths: () => {
|
|
2791
|
+
if (!networkCap) return [];
|
|
2792
|
+
return networkCap.getEntries().map((e) => {
|
|
2793
|
+
try {
|
|
2794
|
+
const u = new URL(e.url, typeof window !== "undefined" ? window.location.href : "http://localhost");
|
|
2795
|
+
if (u.host && typeof window !== "undefined" && u.host !== window.location.host) return null;
|
|
2796
|
+
return u.pathname;
|
|
2797
|
+
} catch {
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
}).filter((p) => p != null && p.length > 0 && p.length < 200);
|
|
2801
|
+
},
|
|
2802
|
+
getUserId: () => userInfo?.id ?? null,
|
|
2803
|
+
getSessionId,
|
|
2804
|
+
onEvent: (event) => {
|
|
2805
|
+
void apiClient.postDiscoveryEvent({
|
|
2806
|
+
...event,
|
|
2807
|
+
sdk_version: MUSHI_SDK_VERSION
|
|
2808
|
+
}).catch((err) => {
|
|
2809
|
+
log.debug("discovery emit failed", { err: String(err) });
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
} else {
|
|
2814
|
+
discoveryCap?.destroy();
|
|
2815
|
+
discoveryCap = null;
|
|
2816
|
+
}
|
|
2577
2817
|
}
|
|
2578
2818
|
const listeners = /* @__PURE__ */ new Map();
|
|
2579
2819
|
function emit(type, data) {
|
|
@@ -2920,6 +3160,8 @@ function createInstance(config) {
|
|
|
2920
3160
|
perfCap?.destroy();
|
|
2921
3161
|
elementSelector?.deactivate();
|
|
2922
3162
|
timelineCap.destroy();
|
|
3163
|
+
discoveryCap?.destroy();
|
|
3164
|
+
discoveryCap = null;
|
|
2923
3165
|
offlineQueue.stopAutoSync();
|
|
2924
3166
|
listeners.clear();
|
|
2925
3167
|
instance = null;
|