@mushi-mushi/web 0.1.0 → 0.2.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 +30 -1
- package/dist/index.cjs +249 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +249 -145
- package/dist/index.js.map +1 -1
- package/package.json +78 -78
package/README.md
CHANGED
|
@@ -32,6 +32,13 @@ Auto-detects conditions that should prompt the user:
|
|
|
32
32
|
- **Rage click** — 3+ clicks in < 500ms on same element
|
|
33
33
|
- **Long task** — > 5s main thread block (PerformanceObserver)
|
|
34
34
|
- **API cascade** — 3+ failed requests in 10s window
|
|
35
|
+
- **Error boundary** — global `window.error` and `unhandledrejection` events (opt-in via `errorBoundary: true`)
|
|
36
|
+
|
|
37
|
+
Each trigger respects its config flag — set `rageClick: false` to disable rage click detection, etc.
|
|
38
|
+
|
|
39
|
+
## Known Limitations
|
|
40
|
+
|
|
41
|
+
**Screenshot capture** uses canvas/SVG `foreignObject` serialization. This does not work with cross-origin iframes, tainted `<canvas>` elements, or pages with strict CSP. Best-effort on single-origin SPAs.
|
|
35
42
|
|
|
36
43
|
## Bundle Size
|
|
37
44
|
|
|
@@ -52,6 +59,28 @@ Mushi.init({
|
|
|
52
59
|
|
|
53
60
|
### With Proactive Triggers
|
|
54
61
|
|
|
62
|
+
Proactive triggers are wired into `Mushi.init()` automatically when `config.proactive` is provided. The SDK opens the widget when a trigger fires, gated by fatigue prevention:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
Mushi.init({
|
|
66
|
+
projectId: 'proj_xxx',
|
|
67
|
+
apiKey: 'your-api-key',
|
|
68
|
+
proactive: {
|
|
69
|
+
rageClick: true,
|
|
70
|
+
longTask: true,
|
|
71
|
+
apiCascade: true,
|
|
72
|
+
errorBoundary: true,
|
|
73
|
+
cooldown: {
|
|
74
|
+
maxProactivePerSession: 2,
|
|
75
|
+
dismissCooldownHours: 24,
|
|
76
|
+
suppressAfterDismissals: 3,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For manual composition (advanced), the lower-level APIs are also exported:
|
|
83
|
+
|
|
55
84
|
```typescript
|
|
56
85
|
import { createProactiveManager, setupProactiveTriggers } from '@mushi-mushi/web';
|
|
57
86
|
|
|
@@ -60,7 +89,7 @@ const manager = createProactiveManager({ maxProactivePerSession: 2 });
|
|
|
60
89
|
setupProactiveTriggers({
|
|
61
90
|
onTrigger: (type, context) => {
|
|
62
91
|
if (manager.shouldShow(type)) {
|
|
63
|
-
//
|
|
92
|
+
// Custom handling
|
|
64
93
|
}
|
|
65
94
|
},
|
|
66
95
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -551,15 +551,21 @@ var MushiWidget = class {
|
|
|
551
551
|
document.body.appendChild(this.host);
|
|
552
552
|
this.render();
|
|
553
553
|
}
|
|
554
|
-
open() {
|
|
554
|
+
open(options) {
|
|
555
555
|
if (this.isOpen) return;
|
|
556
556
|
this.isOpen = true;
|
|
557
|
-
this.step = "category";
|
|
558
|
-
this.selectedCategory = null;
|
|
559
|
-
this.selectedIntent = null;
|
|
560
557
|
this.screenshotAttached = false;
|
|
561
558
|
this.elementSelected = false;
|
|
562
559
|
this.submitting = false;
|
|
560
|
+
if (options?.category) {
|
|
561
|
+
this.selectedCategory = options.category;
|
|
562
|
+
this.selectedIntent = null;
|
|
563
|
+
this.step = "intent";
|
|
564
|
+
} else {
|
|
565
|
+
this.selectedCategory = null;
|
|
566
|
+
this.selectedIntent = null;
|
|
567
|
+
this.step = "category";
|
|
568
|
+
}
|
|
563
569
|
this.render();
|
|
564
570
|
this.callbacks.onOpen();
|
|
565
571
|
}
|
|
@@ -775,9 +781,10 @@ var MushiWidget = class {
|
|
|
775
781
|
const textarea = panel.querySelector(".mushi-textarea");
|
|
776
782
|
const description = textarea?.value?.trim() ?? "";
|
|
777
783
|
const errorEl = panel.querySelector(".mushi-error");
|
|
778
|
-
|
|
784
|
+
const MIN_DESCRIPTION_LENGTH = 20;
|
|
785
|
+
if (description.length < MIN_DESCRIPTION_LENGTH) {
|
|
779
786
|
if (errorEl) {
|
|
780
|
-
errorEl.textContent = t.widget.error
|
|
787
|
+
errorEl.textContent = `${t.widget.error} (${description.length}/${MIN_DESCRIPTION_LENGTH})`;
|
|
781
788
|
errorEl.style.display = "block";
|
|
782
789
|
}
|
|
783
790
|
return;
|
|
@@ -1199,6 +1206,167 @@ function captureSentryContext(_config) {
|
|
|
1199
1206
|
return context;
|
|
1200
1207
|
}
|
|
1201
1208
|
|
|
1209
|
+
// src/proactive-triggers.ts
|
|
1210
|
+
function setupProactiveTriggers(callbacks, config = {}) {
|
|
1211
|
+
const cleanups = [];
|
|
1212
|
+
if (config.rageClick !== false) {
|
|
1213
|
+
let handleClick2 = function(e) {
|
|
1214
|
+
const now = Date.now();
|
|
1215
|
+
if (e.target === lastClickTarget) {
|
|
1216
|
+
clickTimes.push(now);
|
|
1217
|
+
clickTimes = clickTimes.filter((t) => now - t < 500);
|
|
1218
|
+
if (clickTimes.length >= 3) {
|
|
1219
|
+
const el = e.target;
|
|
1220
|
+
callbacks.onTrigger("rage_click", {
|
|
1221
|
+
element: el.tagName,
|
|
1222
|
+
id: el.id,
|
|
1223
|
+
text: el.textContent?.slice(0, 50)
|
|
1224
|
+
});
|
|
1225
|
+
clickTimes = [];
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
lastClickTarget = e.target;
|
|
1229
|
+
clickTimes = [now];
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
let clickTimes = [];
|
|
1233
|
+
let lastClickTarget = null;
|
|
1234
|
+
document.addEventListener("click", handleClick2, true);
|
|
1235
|
+
cleanups.push(() => document.removeEventListener("click", handleClick2, true));
|
|
1236
|
+
}
|
|
1237
|
+
if (config.longTask !== false && typeof PerformanceObserver !== "undefined") {
|
|
1238
|
+
try {
|
|
1239
|
+
const observer = new PerformanceObserver((list) => {
|
|
1240
|
+
for (const entry of list.getEntries()) {
|
|
1241
|
+
if (entry.duration > 5e3) {
|
|
1242
|
+
callbacks.onTrigger("long_task", {
|
|
1243
|
+
duration: Math.round(entry.duration),
|
|
1244
|
+
startTime: Math.round(entry.startTime)
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
observer.observe({ entryTypes: ["longtask"] });
|
|
1250
|
+
cleanups.push(() => observer.disconnect());
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (config.apiCascade !== false) {
|
|
1255
|
+
const failedRequests = [];
|
|
1256
|
+
const origFetch = globalThis.fetch;
|
|
1257
|
+
globalThis.fetch = async function(...args) {
|
|
1258
|
+
try {
|
|
1259
|
+
const res = await origFetch.apply(this, args);
|
|
1260
|
+
if (!res.ok && res.status >= 400) {
|
|
1261
|
+
const now = Date.now();
|
|
1262
|
+
failedRequests.push(now);
|
|
1263
|
+
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1264
|
+
if (recentFailures.length >= 3) {
|
|
1265
|
+
callbacks.onTrigger("api_cascade", {
|
|
1266
|
+
failureCount: recentFailures.length,
|
|
1267
|
+
windowMs: 1e4
|
|
1268
|
+
});
|
|
1269
|
+
failedRequests.length = 0;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return res;
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
const now = Date.now();
|
|
1275
|
+
failedRequests.push(now);
|
|
1276
|
+
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1277
|
+
if (recentFailures.length >= 3) {
|
|
1278
|
+
callbacks.onTrigger("api_cascade", {
|
|
1279
|
+
failureCount: recentFailures.length,
|
|
1280
|
+
windowMs: 1e4
|
|
1281
|
+
});
|
|
1282
|
+
failedRequests.length = 0;
|
|
1283
|
+
}
|
|
1284
|
+
throw err;
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
cleanups.push(() => {
|
|
1288
|
+
globalThis.fetch = origFetch;
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
if (config.errorBoundary) {
|
|
1292
|
+
let handleError2 = function(event) {
|
|
1293
|
+
callbacks.onTrigger("error_boundary", {
|
|
1294
|
+
message: event.message,
|
|
1295
|
+
filename: event.filename,
|
|
1296
|
+
lineno: event.lineno,
|
|
1297
|
+
colno: event.colno
|
|
1298
|
+
});
|
|
1299
|
+
}, handleUnhandledRejection2 = function(event) {
|
|
1300
|
+
callbacks.onTrigger("error_boundary", {
|
|
1301
|
+
message: event.reason instanceof Error ? event.reason.message : String(event.reason),
|
|
1302
|
+
type: "unhandled_rejection"
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
window.addEventListener("error", handleError2);
|
|
1306
|
+
window.addEventListener("unhandledrejection", handleUnhandledRejection2);
|
|
1307
|
+
cleanups.push(() => {
|
|
1308
|
+
window.removeEventListener("error", handleError2);
|
|
1309
|
+
window.removeEventListener("unhandledrejection", handleUnhandledRejection2);
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
destroy() {
|
|
1314
|
+
cleanups.forEach((fn) => fn());
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/proactive-manager.ts
|
|
1320
|
+
var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
|
|
1321
|
+
var STORAGE_KEY_CONSEC_DISMISS = "mushi:consecDismiss";
|
|
1322
|
+
function readStorage(key) {
|
|
1323
|
+
try {
|
|
1324
|
+
return localStorage.getItem(key);
|
|
1325
|
+
} catch {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function writeStorage(key, value) {
|
|
1330
|
+
try {
|
|
1331
|
+
localStorage.setItem(key, value);
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function createProactiveManager(config = {}) {
|
|
1336
|
+
const maxPerSession = config.maxProactivePerSession ?? 2;
|
|
1337
|
+
const cooldownHours = config.dismissCooldownHours ?? 24;
|
|
1338
|
+
const suppressThreshold = config.suppressAfterDismissals ?? 3;
|
|
1339
|
+
let sessionPromptCount = 0;
|
|
1340
|
+
const sessionTriggerTypes = /* @__PURE__ */ new Set();
|
|
1341
|
+
function shouldShow(triggerType) {
|
|
1342
|
+
const consecDismissals = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
|
|
1343
|
+
if (consecDismissals >= suppressThreshold) return false;
|
|
1344
|
+
const lastDismiss = readStorage(STORAGE_KEY_LAST_DISMISS);
|
|
1345
|
+
if (lastDismiss) {
|
|
1346
|
+
const elapsed = Date.now() - parseInt(lastDismiss, 10);
|
|
1347
|
+
if (elapsed < cooldownHours * 60 * 60 * 1e3) return false;
|
|
1348
|
+
}
|
|
1349
|
+
if (sessionPromptCount >= maxPerSession) return false;
|
|
1350
|
+
if (sessionTriggerTypes.has(triggerType)) return false;
|
|
1351
|
+
sessionTriggerTypes.add(triggerType);
|
|
1352
|
+
sessionPromptCount++;
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
function recordDismissal() {
|
|
1356
|
+
writeStorage(STORAGE_KEY_LAST_DISMISS, String(Date.now()));
|
|
1357
|
+
const current = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
|
|
1358
|
+
writeStorage(STORAGE_KEY_CONSEC_DISMISS, String(current + 1));
|
|
1359
|
+
}
|
|
1360
|
+
function recordSubmission() {
|
|
1361
|
+
writeStorage(STORAGE_KEY_CONSEC_DISMISS, "0");
|
|
1362
|
+
}
|
|
1363
|
+
function reset() {
|
|
1364
|
+
sessionPromptCount = 0;
|
|
1365
|
+
sessionTriggerTypes.clear();
|
|
1366
|
+
}
|
|
1367
|
+
return { shouldShow, recordDismissal, recordSubmission, reset };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1202
1370
|
// src/mushi.ts
|
|
1203
1371
|
var instance = null;
|
|
1204
1372
|
var Mushi = class {
|
|
@@ -1234,7 +1402,7 @@ function createInstance(config) {
|
|
|
1234
1402
|
const apiClient = core.createApiClient({
|
|
1235
1403
|
projectId: config.projectId,
|
|
1236
1404
|
apiKey: config.apiKey,
|
|
1237
|
-
apiEndpoint: config.apiEndpoint
|
|
1405
|
+
...config.apiEndpoint ? { apiEndpoint: config.apiEndpoint } : {}
|
|
1238
1406
|
});
|
|
1239
1407
|
const preFilter = core.createPreFilter(config.preFilter);
|
|
1240
1408
|
const offlineQueue = core.createOfflineQueue(config.offline);
|
|
@@ -1253,9 +1421,11 @@ function createInstance(config) {
|
|
|
1253
1421
|
const customMetadata = {};
|
|
1254
1422
|
let pendingScreenshot = null;
|
|
1255
1423
|
let pendingElement = null;
|
|
1424
|
+
let pendingProactiveTrigger = null;
|
|
1256
1425
|
const widget = new MushiWidget(config.widget, {
|
|
1257
1426
|
onSubmit: async ({ category, description, intent }) => {
|
|
1258
1427
|
log.info("Report submitted", { category, intent });
|
|
1428
|
+
proactiveManager?.recordSubmission();
|
|
1259
1429
|
await submitReport(category, description, intent);
|
|
1260
1430
|
},
|
|
1261
1431
|
onOpen: () => {
|
|
@@ -1264,8 +1434,13 @@ function createInstance(config) {
|
|
|
1264
1434
|
},
|
|
1265
1435
|
onClose: () => {
|
|
1266
1436
|
log.debug("Widget closed");
|
|
1437
|
+
if (pendingProactiveTrigger) {
|
|
1438
|
+
proactiveManager?.recordDismissal();
|
|
1439
|
+
emit("proactive:dismissed", { type: pendingProactiveTrigger });
|
|
1440
|
+
}
|
|
1267
1441
|
pendingScreenshot = null;
|
|
1268
1442
|
pendingElement = null;
|
|
1443
|
+
pendingProactiveTrigger = null;
|
|
1269
1444
|
emit("widget:closed");
|
|
1270
1445
|
},
|
|
1271
1446
|
onScreenshotRequest: async () => {
|
|
@@ -1292,6 +1467,39 @@ function createInstance(config) {
|
|
|
1292
1467
|
widget.mount();
|
|
1293
1468
|
}
|
|
1294
1469
|
}
|
|
1470
|
+
let proactiveTriggers = null;
|
|
1471
|
+
let proactiveManager = null;
|
|
1472
|
+
const proactiveCfg = config.proactive;
|
|
1473
|
+
const hasAnyProactive = proactiveCfg && (proactiveCfg.rageClick !== false || proactiveCfg.longTask !== false || proactiveCfg.apiCascade !== false || proactiveCfg.errorBoundary === true);
|
|
1474
|
+
if (hasAnyProactive && typeof document !== "undefined") {
|
|
1475
|
+
proactiveManager = createProactiveManager(proactiveCfg?.cooldown);
|
|
1476
|
+
proactiveTriggers = setupProactiveTriggers(
|
|
1477
|
+
{
|
|
1478
|
+
onTrigger: (type, context) => {
|
|
1479
|
+
if (!proactiveManager.shouldShow(type)) {
|
|
1480
|
+
log.debug("Proactive trigger suppressed by fatigue prevention", { type });
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
log.info("Proactive trigger fired", { type, context });
|
|
1484
|
+
pendingProactiveTrigger = type;
|
|
1485
|
+
emit("proactive:triggered", { type, context });
|
|
1486
|
+
widget.open();
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
rageClick: proactiveCfg?.rageClick,
|
|
1491
|
+
longTask: proactiveCfg?.longTask,
|
|
1492
|
+
apiCascade: proactiveCfg?.apiCascade,
|
|
1493
|
+
errorBoundary: proactiveCfg?.errorBoundary
|
|
1494
|
+
}
|
|
1495
|
+
);
|
|
1496
|
+
log.debug("Proactive triggers enabled", {
|
|
1497
|
+
rageClick: proactiveCfg?.rageClick !== false,
|
|
1498
|
+
longTask: proactiveCfg?.longTask !== false,
|
|
1499
|
+
apiCascade: proactiveCfg?.apiCascade !== false,
|
|
1500
|
+
errorBoundary: proactiveCfg?.errorBoundary === true
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1295
1503
|
offlineQueue.startAutoSync(apiClient);
|
|
1296
1504
|
offlineQueue.flush(apiClient).then((result) => {
|
|
1297
1505
|
if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
|
|
@@ -1303,6 +1511,34 @@ function createInstance(config) {
|
|
|
1303
1511
|
log.info("Report blocked by pre-filter", { reason: filterResult.reason });
|
|
1304
1512
|
return;
|
|
1305
1513
|
}
|
|
1514
|
+
const wasm = config.preFilter?.wasmClassifier;
|
|
1515
|
+
if (wasm) {
|
|
1516
|
+
try {
|
|
1517
|
+
const verdict = await wasm.classify({
|
|
1518
|
+
description,
|
|
1519
|
+
category,
|
|
1520
|
+
url: typeof location !== "undefined" ? location.href : void 0,
|
|
1521
|
+
hasScreenshot: pendingScreenshot !== null,
|
|
1522
|
+
hasSelectedElement: pendingElement !== null,
|
|
1523
|
+
hasNetworkErrors: networkCap?.getEntries()?.some((e) => e.status >= 400 || !!e.error) ?? false,
|
|
1524
|
+
hasConsoleErrors: consoleCap?.getEntries()?.some((e) => e.level === "error") ?? false,
|
|
1525
|
+
proactiveTrigger: pendingProactiveTrigger ?? void 0
|
|
1526
|
+
});
|
|
1527
|
+
if (verdict.verdict === "block") {
|
|
1528
|
+
log.info("Report blocked by on-device classifier", {
|
|
1529
|
+
modelId: verdict.modelId,
|
|
1530
|
+
confidence: verdict.confidence,
|
|
1531
|
+
reason: verdict.reason
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
log.debug("On-device classifier verdict", { ...verdict });
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
log.warn("On-device classifier threw \u2014 falling through to server", {
|
|
1538
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1306
1542
|
if (!rateLimiter.tryConsume()) {
|
|
1307
1543
|
log.warn("Report throttled \u2014 rate limit exceeded");
|
|
1308
1544
|
return;
|
|
@@ -1329,6 +1565,7 @@ function createInstance(config) {
|
|
|
1329
1565
|
sessionId: core.getSessionId(),
|
|
1330
1566
|
reporterToken: core.getReporterToken(),
|
|
1331
1567
|
appVersion: config.integrations?.vercel?.analyticsId,
|
|
1568
|
+
proactiveTrigger: pendingProactiveTrigger ?? void 0,
|
|
1332
1569
|
sentryEventId: sentryCtx?.eventId,
|
|
1333
1570
|
sentryReplayId: sentryCtx?.replayId,
|
|
1334
1571
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1365,10 +1602,11 @@ function createInstance(config) {
|
|
|
1365
1602
|
}
|
|
1366
1603
|
pendingScreenshot = null;
|
|
1367
1604
|
pendingElement = null;
|
|
1605
|
+
pendingProactiveTrigger = null;
|
|
1368
1606
|
}
|
|
1369
1607
|
const sdk = {
|
|
1370
|
-
report() {
|
|
1371
|
-
widget.open();
|
|
1608
|
+
report(options) {
|
|
1609
|
+
widget.open(options);
|
|
1372
1610
|
},
|
|
1373
1611
|
on(event, handler) {
|
|
1374
1612
|
if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
|
|
@@ -1391,6 +1629,8 @@ function createInstance(config) {
|
|
|
1391
1629
|
widget.close();
|
|
1392
1630
|
},
|
|
1393
1631
|
destroy() {
|
|
1632
|
+
proactiveTriggers?.destroy();
|
|
1633
|
+
proactiveManager?.reset();
|
|
1394
1634
|
widget.destroy();
|
|
1395
1635
|
consoleCap?.destroy();
|
|
1396
1636
|
networkCap?.destroy();
|
|
@@ -1425,142 +1665,6 @@ function createNoopInstance() {
|
|
|
1425
1665
|
};
|
|
1426
1666
|
}
|
|
1427
1667
|
|
|
1428
|
-
// src/proactive-manager.ts
|
|
1429
|
-
var STORAGE_KEY_LAST_DISMISS = "mushi:lastDismiss";
|
|
1430
|
-
var STORAGE_KEY_CONSEC_DISMISS = "mushi:consecDismiss";
|
|
1431
|
-
function readStorage(key) {
|
|
1432
|
-
try {
|
|
1433
|
-
return localStorage.getItem(key);
|
|
1434
|
-
} catch {
|
|
1435
|
-
return null;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
function writeStorage(key, value) {
|
|
1439
|
-
try {
|
|
1440
|
-
localStorage.setItem(key, value);
|
|
1441
|
-
} catch {
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
function createProactiveManager(config = {}) {
|
|
1445
|
-
const maxPerSession = config.maxProactivePerSession ?? 2;
|
|
1446
|
-
const cooldownHours = config.dismissCooldownHours ?? 24;
|
|
1447
|
-
const suppressThreshold = config.suppressAfterDismissals ?? 3;
|
|
1448
|
-
let sessionPromptCount = 0;
|
|
1449
|
-
const sessionTriggerTypes = /* @__PURE__ */ new Set();
|
|
1450
|
-
function shouldShow(triggerType) {
|
|
1451
|
-
const consecDismissals = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
|
|
1452
|
-
if (consecDismissals >= suppressThreshold) return false;
|
|
1453
|
-
const lastDismiss = readStorage(STORAGE_KEY_LAST_DISMISS);
|
|
1454
|
-
if (lastDismiss) {
|
|
1455
|
-
const elapsed = Date.now() - parseInt(lastDismiss, 10);
|
|
1456
|
-
if (elapsed < cooldownHours * 60 * 60 * 1e3) return false;
|
|
1457
|
-
}
|
|
1458
|
-
if (sessionPromptCount >= maxPerSession) return false;
|
|
1459
|
-
if (sessionTriggerTypes.has(triggerType)) return false;
|
|
1460
|
-
sessionTriggerTypes.add(triggerType);
|
|
1461
|
-
sessionPromptCount++;
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1464
|
-
function recordDismissal() {
|
|
1465
|
-
writeStorage(STORAGE_KEY_LAST_DISMISS, String(Date.now()));
|
|
1466
|
-
const current = parseInt(readStorage(STORAGE_KEY_CONSEC_DISMISS) ?? "0", 10);
|
|
1467
|
-
writeStorage(STORAGE_KEY_CONSEC_DISMISS, String(current + 1));
|
|
1468
|
-
}
|
|
1469
|
-
function recordSubmission() {
|
|
1470
|
-
writeStorage(STORAGE_KEY_CONSEC_DISMISS, "0");
|
|
1471
|
-
}
|
|
1472
|
-
function reset() {
|
|
1473
|
-
sessionPromptCount = 0;
|
|
1474
|
-
sessionTriggerTypes.clear();
|
|
1475
|
-
}
|
|
1476
|
-
return { shouldShow, recordDismissal, recordSubmission, reset };
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
// src/proactive-triggers.ts
|
|
1480
|
-
function setupProactiveTriggers(callbacks) {
|
|
1481
|
-
const cleanups = [];
|
|
1482
|
-
let clickTimes = [];
|
|
1483
|
-
let lastClickTarget = null;
|
|
1484
|
-
function handleClick(e) {
|
|
1485
|
-
const now = Date.now();
|
|
1486
|
-
if (e.target === lastClickTarget) {
|
|
1487
|
-
clickTimes.push(now);
|
|
1488
|
-
clickTimes = clickTimes.filter((t) => now - t < 500);
|
|
1489
|
-
if (clickTimes.length >= 3) {
|
|
1490
|
-
const el = e.target;
|
|
1491
|
-
callbacks.onTrigger("rage_click", {
|
|
1492
|
-
element: el.tagName,
|
|
1493
|
-
id: el.id,
|
|
1494
|
-
text: el.textContent?.slice(0, 50)
|
|
1495
|
-
});
|
|
1496
|
-
clickTimes = [];
|
|
1497
|
-
}
|
|
1498
|
-
} else {
|
|
1499
|
-
lastClickTarget = e.target;
|
|
1500
|
-
clickTimes = [now];
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
document.addEventListener("click", handleClick, true);
|
|
1504
|
-
cleanups.push(() => document.removeEventListener("click", handleClick, true));
|
|
1505
|
-
if (typeof PerformanceObserver !== "undefined") {
|
|
1506
|
-
try {
|
|
1507
|
-
const observer = new PerformanceObserver((list) => {
|
|
1508
|
-
for (const entry of list.getEntries()) {
|
|
1509
|
-
if (entry.duration > 5e3) {
|
|
1510
|
-
callbacks.onTrigger("long_task", {
|
|
1511
|
-
duration: Math.round(entry.duration),
|
|
1512
|
-
startTime: Math.round(entry.startTime)
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
});
|
|
1517
|
-
observer.observe({ entryTypes: ["longtask"] });
|
|
1518
|
-
cleanups.push(() => observer.disconnect());
|
|
1519
|
-
} catch {
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
const failedRequests = [];
|
|
1523
|
-
const origFetch = globalThis.fetch;
|
|
1524
|
-
globalThis.fetch = async function(...args) {
|
|
1525
|
-
try {
|
|
1526
|
-
const res = await origFetch.apply(this, args);
|
|
1527
|
-
if (!res.ok && res.status >= 400) {
|
|
1528
|
-
const now = Date.now();
|
|
1529
|
-
failedRequests.push(now);
|
|
1530
|
-
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1531
|
-
if (recentFailures.length >= 3) {
|
|
1532
|
-
callbacks.onTrigger("api_cascade", {
|
|
1533
|
-
failureCount: recentFailures.length,
|
|
1534
|
-
windowMs: 1e4
|
|
1535
|
-
});
|
|
1536
|
-
failedRequests.length = 0;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
return res;
|
|
1540
|
-
} catch (err) {
|
|
1541
|
-
const now = Date.now();
|
|
1542
|
-
failedRequests.push(now);
|
|
1543
|
-
const recentFailures = failedRequests.filter((t) => now - t < 1e4);
|
|
1544
|
-
if (recentFailures.length >= 3) {
|
|
1545
|
-
callbacks.onTrigger("api_cascade", {
|
|
1546
|
-
failureCount: recentFailures.length,
|
|
1547
|
-
windowMs: 1e4
|
|
1548
|
-
});
|
|
1549
|
-
failedRequests.length = 0;
|
|
1550
|
-
}
|
|
1551
|
-
throw err;
|
|
1552
|
-
}
|
|
1553
|
-
};
|
|
1554
|
-
cleanups.push(() => {
|
|
1555
|
-
globalThis.fetch = origFetch;
|
|
1556
|
-
});
|
|
1557
|
-
return {
|
|
1558
|
-
destroy() {
|
|
1559
|
-
cleanups.forEach((fn) => fn());
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
1668
|
exports.Mushi = Mushi;
|
|
1565
1669
|
exports.MushiWidget = MushiWidget;
|
|
1566
1670
|
exports.createConsoleCapture = createConsoleCapture;
|