@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/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.8.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;