@mushi-mushi/web 0.1.0 → 0.2.1

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
@@ -1,4 +1,4 @@
1
- import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, getReporterToken, getSessionId, captureEnvironment } from '@mushi-mushi/core';
1
+ import { createLogger, noopLogger, createApiClient, createPreFilter, createOfflineQueue, createRateLimiter, createPiiScrubber, getDeviceFingerprintHash, getReporterToken, getSessionId, captureEnvironment } from '@mushi-mushi/core';
2
2
 
3
3
  // src/mushi.ts
4
4
 
@@ -549,15 +549,21 @@ var MushiWidget = class {
549
549
  document.body.appendChild(this.host);
550
550
  this.render();
551
551
  }
552
- open() {
552
+ open(options) {
553
553
  if (this.isOpen) return;
554
554
  this.isOpen = true;
555
- this.step = "category";
556
- this.selectedCategory = null;
557
- this.selectedIntent = null;
558
555
  this.screenshotAttached = false;
559
556
  this.elementSelected = false;
560
557
  this.submitting = false;
558
+ if (options?.category) {
559
+ this.selectedCategory = options.category;
560
+ this.selectedIntent = null;
561
+ this.step = "intent";
562
+ } else {
563
+ this.selectedCategory = null;
564
+ this.selectedIntent = null;
565
+ this.step = "category";
566
+ }
561
567
  this.render();
562
568
  this.callbacks.onOpen();
563
569
  }
@@ -773,9 +779,10 @@ var MushiWidget = class {
773
779
  const textarea = panel.querySelector(".mushi-textarea");
774
780
  const description = textarea?.value?.trim() ?? "";
775
781
  const errorEl = panel.querySelector(".mushi-error");
776
- if (description.length < 5) {
782
+ const MIN_DESCRIPTION_LENGTH = 20;
783
+ if (description.length < MIN_DESCRIPTION_LENGTH) {
777
784
  if (errorEl) {
778
- errorEl.textContent = t.widget.error;
785
+ errorEl.textContent = `${t.widget.error} (${description.length}/${MIN_DESCRIPTION_LENGTH})`;
779
786
  errorEl.style.display = "block";
780
787
  }
781
788
  return;
@@ -1197,6 +1204,167 @@ function captureSentryContext(_config) {
1197
1204
  return context;
1198
1205
  }
1199
1206
 
1207
+ // src/proactive-triggers.ts
1208
+ function setupProactiveTriggers(callbacks, config = {}) {
1209
+ const cleanups = [];
1210
+ if (config.rageClick !== false) {
1211
+ let handleClick2 = function(e) {
1212
+ const now = Date.now();
1213
+ if (e.target === lastClickTarget) {
1214
+ clickTimes.push(now);
1215
+ clickTimes = clickTimes.filter((t) => now - t < 500);
1216
+ if (clickTimes.length >= 3) {
1217
+ const el = e.target;
1218
+ callbacks.onTrigger("rage_click", {
1219
+ element: el.tagName,
1220
+ id: el.id,
1221
+ text: el.textContent?.slice(0, 50)
1222
+ });
1223
+ clickTimes = [];
1224
+ }
1225
+ } else {
1226
+ lastClickTarget = e.target;
1227
+ clickTimes = [now];
1228
+ }
1229
+ };
1230
+ let clickTimes = [];
1231
+ let lastClickTarget = null;
1232
+ document.addEventListener("click", handleClick2, true);
1233
+ cleanups.push(() => document.removeEventListener("click", handleClick2, true));
1234
+ }
1235
+ if (config.longTask !== false && typeof PerformanceObserver !== "undefined") {
1236
+ try {
1237
+ const observer = new PerformanceObserver((list) => {
1238
+ for (const entry of list.getEntries()) {
1239
+ if (entry.duration > 5e3) {
1240
+ callbacks.onTrigger("long_task", {
1241
+ duration: Math.round(entry.duration),
1242
+ startTime: Math.round(entry.startTime)
1243
+ });
1244
+ }
1245
+ }
1246
+ });
1247
+ observer.observe({ entryTypes: ["longtask"] });
1248
+ cleanups.push(() => observer.disconnect());
1249
+ } catch {
1250
+ }
1251
+ }
1252
+ if (config.apiCascade !== false) {
1253
+ const failedRequests = [];
1254
+ const origFetch = globalThis.fetch;
1255
+ globalThis.fetch = async function(...args) {
1256
+ try {
1257
+ const res = await origFetch.apply(this, args);
1258
+ if (!res.ok && res.status >= 400) {
1259
+ const now = Date.now();
1260
+ failedRequests.push(now);
1261
+ const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1262
+ if (recentFailures.length >= 3) {
1263
+ callbacks.onTrigger("api_cascade", {
1264
+ failureCount: recentFailures.length,
1265
+ windowMs: 1e4
1266
+ });
1267
+ failedRequests.length = 0;
1268
+ }
1269
+ }
1270
+ return res;
1271
+ } catch (err) {
1272
+ const now = Date.now();
1273
+ failedRequests.push(now);
1274
+ const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1275
+ if (recentFailures.length >= 3) {
1276
+ callbacks.onTrigger("api_cascade", {
1277
+ failureCount: recentFailures.length,
1278
+ windowMs: 1e4
1279
+ });
1280
+ failedRequests.length = 0;
1281
+ }
1282
+ throw err;
1283
+ }
1284
+ };
1285
+ cleanups.push(() => {
1286
+ globalThis.fetch = origFetch;
1287
+ });
1288
+ }
1289
+ if (config.errorBoundary) {
1290
+ let handleError2 = function(event) {
1291
+ callbacks.onTrigger("error_boundary", {
1292
+ message: event.message,
1293
+ filename: event.filename,
1294
+ lineno: event.lineno,
1295
+ colno: event.colno
1296
+ });
1297
+ }, handleUnhandledRejection2 = function(event) {
1298
+ callbacks.onTrigger("error_boundary", {
1299
+ message: event.reason instanceof Error ? event.reason.message : String(event.reason),
1300
+ type: "unhandled_rejection"
1301
+ });
1302
+ };
1303
+ window.addEventListener("error", handleError2);
1304
+ window.addEventListener("unhandledrejection", handleUnhandledRejection2);
1305
+ cleanups.push(() => {
1306
+ window.removeEventListener("error", handleError2);
1307
+ window.removeEventListener("unhandledrejection", handleUnhandledRejection2);
1308
+ });
1309
+ }
1310
+ return {
1311
+ destroy() {
1312
+ cleanups.forEach((fn) => fn());
1313
+ }
1314
+ };
1315
+ }
1316
+
1317
+ // src/proactive-manager.ts
1318
+ var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
1319
+ var STORAGE_KEY_CONSEC_DISMISS = "mushi:consecDismiss";
1320
+ function readStorage(key) {
1321
+ try {
1322
+ return localStorage.getItem(key);
1323
+ } catch {
1324
+ return null;
1325
+ }
1326
+ }
1327
+ function writeStorage(key, value) {
1328
+ try {
1329
+ localStorage.setItem(key, value);
1330
+ } catch {
1331
+ }
1332
+ }
1333
+ function createProactiveManager(config = {}) {
1334
+ const maxPerSession = config.maxProactivePerSession ?? 2;
1335
+ const cooldownHours = config.dismissCooldownHours ?? 24;
1336
+ const suppressThreshold = config.suppressAfterDismissals ?? 3;
1337
+ let sessionPromptCount = 0;
1338
+ const sessionTriggerTypes = /* @__PURE__ */ new Set();
1339
+ function shouldShow(triggerType) {
1340
+ const consecDismissals = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
1341
+ if (consecDismissals >= suppressThreshold) return false;
1342
+ const lastDismiss = readStorage(STORAGE_KEY_LAST_DISMISS);
1343
+ if (lastDismiss) {
1344
+ const elapsed = Date.now() - parseInt(lastDismiss, 10);
1345
+ if (elapsed < cooldownHours * 60 * 60 * 1e3) return false;
1346
+ }
1347
+ if (sessionPromptCount >= maxPerSession) return false;
1348
+ if (sessionTriggerTypes.has(triggerType)) return false;
1349
+ sessionTriggerTypes.add(triggerType);
1350
+ sessionPromptCount++;
1351
+ return true;
1352
+ }
1353
+ function recordDismissal() {
1354
+ writeStorage(STORAGE_KEY_LAST_DISMISS, String(Date.now()));
1355
+ const current = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
1356
+ writeStorage(STORAGE_KEY_CONSEC_DISMISS, String(current + 1));
1357
+ }
1358
+ function recordSubmission() {
1359
+ writeStorage(STORAGE_KEY_CONSEC_DISMISS, "0");
1360
+ }
1361
+ function reset() {
1362
+ sessionPromptCount = 0;
1363
+ sessionTriggerTypes.clear();
1364
+ }
1365
+ return { shouldShow, recordDismissal, recordSubmission, reset };
1366
+ }
1367
+
1200
1368
  // src/mushi.ts
1201
1369
  var instance = null;
1202
1370
  var Mushi = class {
@@ -1232,7 +1400,7 @@ function createInstance(config) {
1232
1400
  const apiClient = createApiClient({
1233
1401
  projectId: config.projectId,
1234
1402
  apiKey: config.apiKey,
1235
- apiEndpoint: config.apiEndpoint ?? "https://api.mushimushi.dev"
1403
+ ...config.apiEndpoint ? { apiEndpoint: config.apiEndpoint } : {}
1236
1404
  });
1237
1405
  const preFilter = createPreFilter(config.preFilter);
1238
1406
  const offlineQueue = createOfflineQueue(config.offline);
@@ -1251,9 +1419,11 @@ function createInstance(config) {
1251
1419
  const customMetadata = {};
1252
1420
  let pendingScreenshot = null;
1253
1421
  let pendingElement = null;
1422
+ let pendingProactiveTrigger = null;
1254
1423
  const widget = new MushiWidget(config.widget, {
1255
1424
  onSubmit: async ({ category, description, intent }) => {
1256
1425
  log.info("Report submitted", { category, intent });
1426
+ proactiveManager?.recordSubmission();
1257
1427
  await submitReport(category, description, intent);
1258
1428
  },
1259
1429
  onOpen: () => {
@@ -1262,8 +1432,13 @@ function createInstance(config) {
1262
1432
  },
1263
1433
  onClose: () => {
1264
1434
  log.debug("Widget closed");
1435
+ if (pendingProactiveTrigger) {
1436
+ proactiveManager?.recordDismissal();
1437
+ emit("proactive:dismissed", { type: pendingProactiveTrigger });
1438
+ }
1265
1439
  pendingScreenshot = null;
1266
1440
  pendingElement = null;
1441
+ pendingProactiveTrigger = null;
1267
1442
  emit("widget:closed");
1268
1443
  },
1269
1444
  onScreenshotRequest: async () => {
@@ -1290,6 +1465,39 @@ function createInstance(config) {
1290
1465
  widget.mount();
1291
1466
  }
1292
1467
  }
1468
+ let proactiveTriggers = null;
1469
+ let proactiveManager = null;
1470
+ const proactiveCfg = config.proactive;
1471
+ const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
1472
+ if (hasAnyProactive && typeof document !== "undefined") {
1473
+ proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
1474
+ proactiveTriggers = setupProactiveTriggers(
1475
+ {
1476
+ onTrigger: (type, context) => {
1477
+ if (!proactiveManager.shouldShow(type)) {
1478
+ log.debug("Proactive trigger suppressed by fatigue prevention", { type });
1479
+ return;
1480
+ }
1481
+ log.info("Proactive trigger fired", { type, context });
1482
+ pendingProactiveTrigger = type;
1483
+ emit("proactive:triggered", { type, context });
1484
+ widget.open();
1485
+ }
1486
+ },
1487
+ {
1488
+ rageClick: proactiveCfg?.rageClick,
1489
+ longTask: proactiveCfg?.longTask,
1490
+ apiCascade: proactiveCfg?.apiCascade,
1491
+ errorBoundary: proactiveCfg?.errorBoundary
1492
+ }
1493
+ );
1494
+ log.debug("Proactive triggers enabled", {
1495
+ rageClick: proactiveCfg?.rageClick !== false,
1496
+ longTask: proactiveCfg?.longTask !== false,
1497
+ apiCascade: proactiveCfg?.apiCascade !== false,
1498
+ errorBoundary: proactiveCfg?.errorBoundary === true
1499
+ });
1500
+ }
1293
1501
  offlineQueue.startAutoSync(apiClient);
1294
1502
  offlineQueue.flush(apiClient).then((result) => {
1295
1503
  if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
@@ -1301,12 +1509,41 @@ function createInstance(config) {
1301
1509
  log.info("Report blocked by pre-filter", { reason: filterResult.reason });
1302
1510
  return;
1303
1511
  }
1512
+ const wasm = config.preFilter?.wasmClassifier;
1513
+ if (wasm) {
1514
+ try {
1515
+ const verdict = await wasm.classify({
1516
+ description,
1517
+ category,
1518
+ url: typeof location !== "undefined" ? location.href : void 0,
1519
+ hasScreenshot: pendingScreenshot !== null,
1520
+ hasSelectedElement: pendingElement !== null,
1521
+ hasNetworkErrors: networkCap?.getEntries()?.some((e) => e.status >= 400 || !!e.error) ?? false,
1522
+ hasConsoleErrors: consoleCap?.getEntries()?.some((e) => e.level === "error") ?? false,
1523
+ proactiveTrigger: pendingProactiveTrigger ?? void 0
1524
+ });
1525
+ if (verdict.verdict === "block") {
1526
+ log.info("Report blocked by on-device classifier", {
1527
+ modelId: verdict.modelId,
1528
+ confidence: verdict.confidence,
1529
+ reason: verdict.reason
1530
+ });
1531
+ return;
1532
+ }
1533
+ log.debug("On-device classifier verdict", { ...verdict });
1534
+ } catch (err) {
1535
+ log.warn("On-device classifier threw \u2014 falling through to server", {
1536
+ error: err instanceof Error ? err.message : String(err)
1537
+ });
1538
+ }
1539
+ }
1304
1540
  if (!rateLimiter.tryConsume()) {
1305
1541
  log.warn("Report throttled \u2014 rate limit exceeded");
1306
1542
  return;
1307
1543
  }
1308
1544
  const scrubbedDescription = piiScrubber.scrub(preFilter.truncate(description));
1309
1545
  const sentryCtx = config.sentry ? captureSentryContext(config.sentry) : void 0;
1546
+ const fingerprintHash = await getDeviceFingerprintHash().catch(() => null);
1310
1547
  const report = {
1311
1548
  id: crypto.randomUUID?.() ?? `mushi_${Date.now()}_${Math.random().toString(36).slice(2)}`,
1312
1549
  projectId: config.projectId,
@@ -1326,7 +1563,9 @@ function createInstance(config) {
1326
1563
  },
1327
1564
  sessionId: getSessionId(),
1328
1565
  reporterToken: getReporterToken(),
1566
+ ...fingerprintHash ? { fingerprintHash } : {},
1329
1567
  appVersion: config.integrations?.vercel?.analyticsId,
1568
+ proactiveTrigger: pendingProactiveTrigger ?? void 0,
1330
1569
  sentryEventId: sentryCtx?.eventId,
1331
1570
  sentryReplayId: sentryCtx?.replayId,
1332
1571
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1363,10 +1602,11 @@ function createInstance(config) {
1363
1602
  }
1364
1603
  pendingScreenshot = null;
1365
1604
  pendingElement = null;
1605
+ pendingProactiveTrigger = null;
1366
1606
  }
1367
1607
  const sdk = {
1368
- report() {
1369
- widget.open();
1608
+ report(options) {
1609
+ widget.open(options);
1370
1610
  },
1371
1611
  on(event, handler) {
1372
1612
  if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
@@ -1389,6 +1629,8 @@ function createInstance(config) {
1389
1629
  widget.close();
1390
1630
  },
1391
1631
  destroy() {
1632
+ proactiveTriggers?.destroy();
1633
+ proactiveManager?.reset();
1392
1634
  widget.destroy();
1393
1635
  consoleCap?.destroy();
1394
1636
  networkCap?.destroy();
@@ -1423,142 +1665,6 @@ function createNoopInstance() {
1423
1665
  };
1424
1666
  }
1425
1667
 
1426
- // src/proactive-manager.ts
1427
- var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
1428
- var STORAGE_KEY_CONSEC_DISMISS = "mushi:consecDismiss";
1429
- function readStorage(key) {
1430
- try {
1431
- return localStorage.getItem(key);
1432
- } catch {
1433
- return null;
1434
- }
1435
- }
1436
- function writeStorage(key, value) {
1437
- try {
1438
- localStorage.setItem(key, value);
1439
- } catch {
1440
- }
1441
- }
1442
- function createProactiveManager(config = {}) {
1443
- const maxPerSession = config.maxProactivePerSession ?? 2;
1444
- const cooldownHours = config.dismissCooldownHours ?? 24;
1445
- const suppressThreshold = config.suppressAfterDismissals ?? 3;
1446
- let sessionPromptCount = 0;
1447
- const sessionTriggerTypes = /* @__PURE__ */ new Set();
1448
- function shouldShow(triggerType) {
1449
- const consecDismissals = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
1450
- if (consecDismissals >= suppressThreshold) return false;
1451
- const lastDismiss = readStorage(STORAGE_KEY_LAST_DISMISS);
1452
- if (lastDismiss) {
1453
- const elapsed = Date.now() - parseInt(lastDismiss, 10);
1454
- if (elapsed < cooldownHours * 60 * 60 * 1e3) return false;
1455
- }
1456
- if (sessionPromptCount >= maxPerSession) return false;
1457
- if (sessionTriggerTypes.has(triggerType)) return false;
1458
- sessionTriggerTypes.add(triggerType);
1459
- sessionPromptCount++;
1460
- return true;
1461
- }
1462
- function recordDismissal() {
1463
- writeStorage(STORAGE_KEY_LAST_DISMISS, String(Date.now()));
1464
- const current = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
1465
- writeStorage(STORAGE_KEY_CONSEC_DISMISS, String(current + 1));
1466
- }
1467
- function recordSubmission() {
1468
- writeStorage(STORAGE_KEY_CONSEC_DISMISS, "0");
1469
- }
1470
- function reset() {
1471
- sessionPromptCount = 0;
1472
- sessionTriggerTypes.clear();
1473
- }
1474
- return { shouldShow, recordDismissal, recordSubmission, reset };
1475
- }
1476
-
1477
- // src/proactive-triggers.ts
1478
- function setupProactiveTriggers(callbacks) {
1479
- const cleanups = [];
1480
- let clickTimes = [];
1481
- let lastClickTarget = null;
1482
- function handleClick(e) {
1483
- const now = Date.now();
1484
- if (e.target === lastClickTarget) {
1485
- clickTimes.push(now);
1486
- clickTimes = clickTimes.filter((t) => now - t < 500);
1487
- if (clickTimes.length >= 3) {
1488
- const el = e.target;
1489
- callbacks.onTrigger("rage_click", {
1490
- element: el.tagName,
1491
- id: el.id,
1492
- text: el.textContent?.slice(0, 50)
1493
- });
1494
- clickTimes = [];
1495
- }
1496
- } else {
1497
- lastClickTarget = e.target;
1498
- clickTimes = [now];
1499
- }
1500
- }
1501
- document.addEventListener("click", handleClick, true);
1502
- cleanups.push(() => document.removeEventListener("click", handleClick, true));
1503
- if (typeof PerformanceObserver !== "undefined") {
1504
- try {
1505
- const observer = new PerformanceObserver((list) => {
1506
- for (const entry of list.getEntries()) {
1507
- if (entry.duration > 5e3) {
1508
- callbacks.onTrigger("long_task", {
1509
- duration: Math.round(entry.duration),
1510
- startTime: Math.round(entry.startTime)
1511
- });
1512
- }
1513
- }
1514
- });
1515
- observer.observe({ entryTypes: ["longtask"] });
1516
- cleanups.push(() => observer.disconnect());
1517
- } catch {
1518
- }
1519
- }
1520
- const failedRequests = [];
1521
- const origFetch = globalThis.fetch;
1522
- globalThis.fetch = async function(...args) {
1523
- try {
1524
- const res = await origFetch.apply(this, args);
1525
- if (!res.ok && res.status >= 400) {
1526
- const now = Date.now();
1527
- failedRequests.push(now);
1528
- const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1529
- if (recentFailures.length >= 3) {
1530
- callbacks.onTrigger("api_cascade", {
1531
- failureCount: recentFailures.length,
1532
- windowMs: 1e4
1533
- });
1534
- failedRequests.length = 0;
1535
- }
1536
- }
1537
- return res;
1538
- } catch (err) {
1539
- const now = Date.now();
1540
- failedRequests.push(now);
1541
- const recentFailures = failedRequests.filter((t) => now - t < 1e4);
1542
- if (recentFailures.length >= 3) {
1543
- callbacks.onTrigger("api_cascade", {
1544
- failureCount: recentFailures.length,
1545
- windowMs: 1e4
1546
- });
1547
- failedRequests.length = 0;
1548
- }
1549
- throw err;
1550
- }
1551
- };
1552
- cleanups.push(() => {
1553
- globalThis.fetch = origFetch;
1554
- });
1555
- return {
1556
- destroy() {
1557
- cleanups.forEach((fn) => fn());
1558
- }
1559
- };
1560
- }
1561
-
1562
1668
  export { Mushi, MushiWidget, createConsoleCapture, createElementSelector, createNetworkCapture, createPerformanceCapture, createProactiveManager, createScreenshotCapture, getAvailableLocales, getLocale, setupProactiveTriggers };
1563
1669
  //# sourceMappingURL=index.js.map
1564
1670
  //# sourceMappingURL=index.js.map