@diegotsi/flint-react 1.0.0 → 1.0.2

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.cjs CHANGED
@@ -32,7 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  FlintModal: () => FlintModal,
34
34
  FlintWidget: () => FlintWidget,
35
- flint: () => import_core7.flint
35
+ flint: () => flint
36
36
  });
37
37
  module.exports = __toCommonJS(index_exports);
38
38
 
@@ -40,8 +40,437 @@ module.exports = __toCommonJS(index_exports);
40
40
  var import_react2 = require("react");
41
41
  var import_react_i18next = require("react-i18next");
42
42
 
43
- // src/api.ts
44
- var import_core = require("@flint/core");
43
+ // ../core/dist/index.js
44
+ var import_fflate = require("fflate");
45
+ async function fetchWithRetry(url, init, retries = 3, baseDelay = 1e3) {
46
+ for (let attempt = 0; attempt <= retries; attempt++) {
47
+ try {
48
+ const res = await fetch(url, init);
49
+ if (res.ok || attempt === retries) return res;
50
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) return res;
51
+ } catch (err) {
52
+ if (attempt === retries) throw err;
53
+ }
54
+ await new Promise((r) => setTimeout(r, baseDelay * 2 ** attempt));
55
+ }
56
+ throw new Error("Max retries exceeded");
57
+ }
58
+ async function submitReport(serverUrl, projectKey, payload, screenshot) {
59
+ const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports`;
60
+ let body;
61
+ const headers = {
62
+ "x-project-key": projectKey
63
+ };
64
+ if (screenshot) {
65
+ const form = new FormData();
66
+ form.append("reporterId", payload.reporterId);
67
+ form.append("reporterName", payload.reporterName);
68
+ if (payload.reporterEmail) form.append("reporterEmail", payload.reporterEmail);
69
+ form.append("description", payload.description);
70
+ if (payload.expectedBehavior) form.append("expectedBehavior", payload.expectedBehavior);
71
+ if (payload.stepsToReproduce) form.append("stepsToReproduce", JSON.stringify(payload.stepsToReproduce));
72
+ if (payload.externalReplayUrl) form.append("externalReplayUrl", payload.externalReplayUrl);
73
+ if (payload.additionalContext) form.append("additionalContext", payload.additionalContext);
74
+ form.append("severity", payload.severity);
75
+ if (payload.url) form.append("url", payload.url);
76
+ if (payload.meta) form.append("meta", JSON.stringify(payload.meta));
77
+ form.append("screenshot", screenshot);
78
+ body = form;
79
+ } else {
80
+ body = JSON.stringify(payload);
81
+ headers["Content-Type"] = "application/json";
82
+ }
83
+ const res = await fetchWithRetry(url, { method: "POST", headers, body });
84
+ if (!res.ok) {
85
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
86
+ throw new Error(err.error ?? `HTTP ${res.status}`);
87
+ }
88
+ return res.json();
89
+ }
90
+ async function submitReplay(serverUrl, projectKey, reportId, events) {
91
+ const json = JSON.stringify(events);
92
+ const encoded = new TextEncoder().encode(json);
93
+ const compressed = (0, import_fflate.gzipSync)(encoded);
94
+ const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports/${reportId}/replay`;
95
+ await fetch(url, {
96
+ method: "POST",
97
+ headers: {
98
+ "x-project-key": projectKey,
99
+ "Content-Type": "application/octet-stream"
100
+ },
101
+ body: compressed.buffer
102
+ });
103
+ }
104
+ var MAX_ENTRIES = 50;
105
+ function createConsoleCollector() {
106
+ const entries = [];
107
+ let active = false;
108
+ const originals = {
109
+ log: console.log.bind(console),
110
+ warn: console.warn.bind(console),
111
+ error: console.error.bind(console)
112
+ };
113
+ let origOnerror = null;
114
+ let origUnhandled = null;
115
+ function push(level, args) {
116
+ let str;
117
+ try {
118
+ str = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
119
+ } catch {
120
+ str = String(args[0]);
121
+ }
122
+ if (str.length > 500) str = str.slice(0, 500) + "\u2026";
123
+ entries.push({ level, args: str, timestamp: Date.now() });
124
+ if (entries.length > MAX_ENTRIES) entries.shift();
125
+ }
126
+ return {
127
+ start() {
128
+ if (active) return;
129
+ active = true;
130
+ console.log = (...args) => {
131
+ push("log", args);
132
+ originals.log(...args);
133
+ };
134
+ console.warn = (...args) => {
135
+ push("warn", args);
136
+ originals.warn(...args);
137
+ };
138
+ console.error = (...args) => {
139
+ push("error", args);
140
+ originals.error(...args);
141
+ };
142
+ origOnerror = window.onerror;
143
+ window.onerror = (msg, src, line, col, err) => {
144
+ push("error", [err?.message ?? String(msg), `${src}:${line}:${col}`]);
145
+ if (typeof origOnerror === "function") return origOnerror(msg, src, line, col, err);
146
+ return false;
147
+ };
148
+ origUnhandled = window.onunhandledrejection;
149
+ window.onunhandledrejection = (event) => {
150
+ const reason = event.reason instanceof Error ? event.reason.message : JSON.stringify(event.reason);
151
+ push("error", ["UnhandledRejection:", reason]);
152
+ if (typeof origUnhandled === "function") origUnhandled.call(window, event);
153
+ };
154
+ },
155
+ stop() {
156
+ if (!active) return;
157
+ active = false;
158
+ console.log = originals.log;
159
+ console.warn = originals.warn;
160
+ console.error = originals.error;
161
+ window.onerror = origOnerror;
162
+ window.onunhandledrejection = origUnhandled;
163
+ },
164
+ getEntries() {
165
+ return [...entries];
166
+ }
167
+ };
168
+ }
169
+ function collectEnvironment() {
170
+ const ua = navigator.userAgent;
171
+ let browser = "Unknown";
172
+ const chromeM = ua.match(/Chrome\/(\d+)/);
173
+ const firefoxM = ua.match(/Firefox\/(\d+)/);
174
+ const edgeM = ua.match(/Edg\/(\d+)/);
175
+ const safariM = ua.match(/Version\/(\d+)/);
176
+ const operaM = ua.match(/OPR\/(\d+)/);
177
+ if (operaM) {
178
+ browser = `Opera ${operaM[1]}`;
179
+ } else if (edgeM) {
180
+ browser = `Edge ${edgeM[1]}`;
181
+ } else if (chromeM && !/Edg|OPR/.test(ua)) {
182
+ browser = `Chrome ${chromeM[1]}`;
183
+ } else if (firefoxM) {
184
+ browser = `Firefox ${firefoxM[1]}`;
185
+ } else if (safariM && /Safari\//.test(ua)) {
186
+ browser = `Safari ${safariM[1]}`;
187
+ }
188
+ let os = "Unknown";
189
+ const macM = ua.match(/Mac OS X (\d+[._]\d+)/);
190
+ const winM = ua.match(/Windows NT (\d+\.\d+)/);
191
+ const androidM = ua.match(/Android (\d+)/);
192
+ const iosM = ua.match(/iPhone OS (\d+[._]\d+)/);
193
+ if (macM) {
194
+ os = `macOS ${macM[1].replace("_", ".")}`;
195
+ } else if (winM) {
196
+ const winMap = {
197
+ "10.0": "10/11",
198
+ "6.3": "8.1",
199
+ "6.2": "8",
200
+ "6.1": "7"
201
+ };
202
+ os = `Windows ${winMap[winM[1]] ?? winM[1]}`;
203
+ } else if (androidM) {
204
+ os = `Android ${androidM[1]}`;
205
+ } else if (iosM) {
206
+ os = `iOS ${iosM[1].replace(/_/g, ".")}`;
207
+ } else if (/Linux/.test(ua)) {
208
+ os = "Linux";
209
+ }
210
+ return {
211
+ browser,
212
+ os,
213
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
214
+ screen: `${screen.width}x${screen.height}`,
215
+ language: navigator.language,
216
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
217
+ online: navigator.onLine
218
+ };
219
+ }
220
+ function createFrustrationCollector(opts) {
221
+ const threshold = opts?.rageClickThreshold ?? 3;
222
+ const window2 = opts?.rageClickWindow ?? 500;
223
+ const errorThreshold = opts?.errorLoopThreshold ?? 3;
224
+ const errorWindow = opts?.errorLoopWindow ?? 3e4;
225
+ const deadClicksEnabled = opts?.enableDeadClicks ?? true;
226
+ const events = [];
227
+ const listeners2 = /* @__PURE__ */ new Set();
228
+ const clickHistory = [];
229
+ const errorCounts = /* @__PURE__ */ new Map();
230
+ let clickHandler = null;
231
+ let origConsoleError = null;
232
+ function emit2(event) {
233
+ events.push(event);
234
+ if (events.length > 50) events.shift();
235
+ for (const cb of listeners2) cb(event);
236
+ }
237
+ function getCSSSelector(el) {
238
+ if (el.id) return `#${el.id}`;
239
+ const tag = el.tagName.toLowerCase();
240
+ const cls = [...el.classList].slice(0, 3).join(".");
241
+ if (cls) return `${tag}.${cls}`;
242
+ return tag;
243
+ }
244
+ function isInteractive(el) {
245
+ const tag = el.tagName.toLowerCase();
246
+ if (["a", "button", "input", "select", "textarea", "label", "summary"].includes(tag)) return true;
247
+ if (el.getAttribute("role") === "button" || el.getAttribute("tabindex")) return true;
248
+ if (el.onclick || el.getAttribute("onclick")) return true;
249
+ if (el.closest("a, button, [role=button], [onclick]")) return true;
250
+ try {
251
+ if (getComputedStyle(el).cursor === "pointer") return true;
252
+ } catch {
253
+ }
254
+ return false;
255
+ }
256
+ function handleClick(e) {
257
+ const target = e.target;
258
+ if (!target) return;
259
+ const now = Date.now();
260
+ clickHistory.push({ target, time: now });
261
+ while (clickHistory.length > 0 && now - clickHistory[0].time > 1e3) {
262
+ clickHistory.shift();
263
+ }
264
+ const recentOnSame = clickHistory.filter((c) => c.target === target && now - c.time < window2);
265
+ if (recentOnSame.length >= threshold) {
266
+ emit2({
267
+ type: "rage_click",
268
+ timestamp: now,
269
+ target: getCSSSelector(target),
270
+ details: `${recentOnSame.length} clicks in ${now - recentOnSame[0].time}ms`,
271
+ url: globalThis.location?.href ?? ""
272
+ });
273
+ clickHistory.length = 0;
274
+ }
275
+ if (deadClicksEnabled && !isInteractive(target)) {
276
+ emit2({
277
+ type: "dead_click",
278
+ timestamp: now,
279
+ target: getCSSSelector(target),
280
+ details: `Click on non-interactive <${target.tagName.toLowerCase()}>`,
281
+ url: globalThis.location?.href ?? ""
282
+ });
283
+ }
284
+ }
285
+ function patchConsoleError() {
286
+ origConsoleError = console.error;
287
+ console.error = (...args) => {
288
+ origConsoleError?.apply(console, args);
289
+ const key = String(args[0]).slice(0, 100);
290
+ const now = Date.now();
291
+ const existing = errorCounts.get(key);
292
+ if (existing && now - existing.firstSeen < errorWindow) {
293
+ existing.count++;
294
+ if (existing.count >= errorThreshold) {
295
+ emit2({
296
+ type: "error_loop",
297
+ timestamp: now,
298
+ target: "console",
299
+ details: `Same error ${existing.count}x in ${Math.round((now - existing.firstSeen) / 1e3)}s: ${key}`,
300
+ url: globalThis.location?.href ?? ""
301
+ });
302
+ errorCounts.delete(key);
303
+ }
304
+ } else {
305
+ errorCounts.set(key, { count: 1, firstSeen: now });
306
+ }
307
+ };
308
+ }
309
+ return {
310
+ start() {
311
+ clickHandler = handleClick;
312
+ document.addEventListener("click", clickHandler, true);
313
+ patchConsoleError();
314
+ },
315
+ stop() {
316
+ if (clickHandler) document.removeEventListener("click", clickHandler, true);
317
+ if (origConsoleError) console.error = origConsoleError;
318
+ clickHistory.length = 0;
319
+ errorCounts.clear();
320
+ },
321
+ getEvents() {
322
+ return [...events];
323
+ },
324
+ onFrustration(callback) {
325
+ listeners2.add(callback);
326
+ return () => listeners2.delete(callback);
327
+ }
328
+ };
329
+ }
330
+ var MAX_ENTRIES2 = 50;
331
+ var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
332
+ "browser-intake-datadoghq.com",
333
+ "rum.browser-intake-datadoghq.com",
334
+ "logs.browser-intake-datadoghq.com",
335
+ "session-replay.browser-intake-datadoghq.com"
336
+ ]);
337
+ function isBlockedUrl(url, extra) {
338
+ try {
339
+ const host = new URL(url, location.href).hostname;
340
+ const all = [...BLOCKED_HOSTS, ...extra];
341
+ return all.some((b) => host === b || host.endsWith("." + b));
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+ function truncateUrl(url) {
347
+ try {
348
+ const u = new URL(url, location.href);
349
+ const base = `${u.origin}${u.pathname}`;
350
+ return base.length > 200 ? base.slice(0, 200) + "\u2026" : base;
351
+ } catch {
352
+ return url.length > 200 ? url.slice(0, 200) + "\u2026" : url;
353
+ }
354
+ }
355
+ function createNetworkCollector(extraBlockedHosts = []) {
356
+ const entries = [];
357
+ const blocked = new Set(extraBlockedHosts);
358
+ let origFetch = null;
359
+ let origXHROpen = null;
360
+ let active = false;
361
+ function push(entry) {
362
+ entries.push(entry);
363
+ if (entries.length > MAX_ENTRIES2) entries.shift();
364
+ }
365
+ return {
366
+ start() {
367
+ if (active) return;
368
+ active = true;
369
+ origFetch = window.fetch;
370
+ window.fetch = async (input, init) => {
371
+ const method = (init?.method ?? "GET").toUpperCase();
372
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
373
+ const startTime = Date.now();
374
+ const res = await origFetch.call(window, input, init);
375
+ if (res.status >= 400 && !isBlockedUrl(url, blocked)) {
376
+ push({
377
+ method,
378
+ url: truncateUrl(url),
379
+ status: res.status,
380
+ duration: Date.now() - startTime,
381
+ timestamp: startTime
382
+ });
383
+ }
384
+ return res;
385
+ };
386
+ origXHROpen = XMLHttpRequest.prototype.open;
387
+ XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
388
+ const startTime = Date.now();
389
+ const urlStr = typeof url === "string" ? url : url.href;
390
+ this.addEventListener("load", () => {
391
+ if (this.status >= 400 && !isBlockedUrl(urlStr, blocked)) {
392
+ push({
393
+ method: method.toUpperCase(),
394
+ url: truncateUrl(urlStr),
395
+ status: this.status,
396
+ duration: Date.now() - startTime,
397
+ timestamp: startTime
398
+ });
399
+ }
400
+ });
401
+ return origXHROpen.apply(this, [method, url, async ?? true, username, password]);
402
+ };
403
+ },
404
+ stop() {
405
+ if (!active) return;
406
+ active = false;
407
+ if (origFetch) window.fetch = origFetch;
408
+ if (origXHROpen) XMLHttpRequest.prototype.open = origXHROpen;
409
+ },
410
+ getEntries() {
411
+ return [...entries];
412
+ }
413
+ };
414
+ }
415
+ var state = { user: void 0, sessionReplay: void 0 };
416
+ var listeners = /* @__PURE__ */ new Set();
417
+ function emit() {
418
+ for (const l of listeners) l();
419
+ }
420
+ function subscribe(listener) {
421
+ listeners.add(listener);
422
+ return () => listeners.delete(listener);
423
+ }
424
+ function getSnapshot() {
425
+ return state;
426
+ }
427
+ var flint = {
428
+ setUser(user) {
429
+ state = { ...state, user: user ?? void 0 };
430
+ emit();
431
+ },
432
+ setSessionReplay(url) {
433
+ state = { ...state, sessionReplay: url ?? void 0 };
434
+ emit();
435
+ }
436
+ };
437
+ var light = {
438
+ background: "rgba(255,255,255,0.90)",
439
+ backgroundSecondary: "rgba(249,250,251,0.75)",
440
+ accent: "#2563eb",
441
+ accentHover: "#1d4ed8",
442
+ text: "#111827",
443
+ textMuted: "#6b7280",
444
+ border: "rgba(255,255,255,0.9)",
445
+ shadow: "0 32px 80px rgba(0,0,0,0.18), 0 8px 32px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.04)",
446
+ buttonText: "#ffffff",
447
+ backdropFilter: "blur(32px) saturate(1.8)"
448
+ };
449
+ var dark = {
450
+ background: "rgba(15,20,35,0.88)",
451
+ backgroundSecondary: "rgba(5,8,18,0.65)",
452
+ accent: "#4d8aff",
453
+ accentHover: "#3b6fdb",
454
+ text: "#dde3ef",
455
+ textMuted: "#6b7a93",
456
+ border: "rgba(255,255,255,0.08)",
457
+ shadow: "0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
458
+ buttonText: "#ffffff",
459
+ backdropFilter: "blur(32px) saturate(1.6)"
460
+ };
461
+ function resolveTheme(theme) {
462
+ if (theme === "dark") return dark;
463
+ if (theme === "light") return light;
464
+ const override = theme;
465
+ return {
466
+ ...light,
467
+ background: override.background ?? light.background,
468
+ accent: override.accent ?? light.accent,
469
+ accentHover: override.accent ?? light.accentHover,
470
+ text: override.text ?? light.text,
471
+ border: override.border ?? light.border
472
+ };
473
+ }
45
474
 
46
475
  // src/ScreenAnnotator.tsx
47
476
  var import_modern_screenshot = require("modern-screenshot");
@@ -185,9 +614,6 @@ function ScreenAnnotator({ zIndex, onCapture, onCancel }) {
185
614
  );
186
615
  }
187
616
 
188
- // src/theme.ts
189
- var import_core2 = require("@flint/core");
190
-
191
617
  // src/FlintModal.tsx
192
618
  var import_jsx_runtime2 = require("react/jsx-runtime");
193
619
  var SEVERITIES = ["P1", "P2", "P3", "P4"];
@@ -254,7 +680,7 @@ function FlintModal({
254
680
  onError
255
681
  }) {
256
682
  const { t } = (0, import_react_i18next.useTranslation)();
257
- const colors = (0, import_core2.resolveTheme)(theme);
683
+ const colors = resolveTheme(theme);
258
684
  const isDark = theme === "dark";
259
685
  const [severity, setSeverity] = (0, import_react2.useState)("P2");
260
686
  const [description, setDescription] = (0, import_react2.useState)("");
@@ -331,13 +757,13 @@ function FlintModal({
331
757
  }
332
758
  }
333
759
  try {
334
- const res = await (0, import_core.submitReport)(serverUrl, projectKey, payload, !isText ? screenshot ?? void 0 : void 0);
760
+ const res = await submitReport(serverUrl, projectKey, payload, !isText ? screenshot ?? void 0 : void 0);
335
761
  setResult(res);
336
762
  setStatus("success");
337
763
  onSuccess?.(res);
338
764
  const events = getReplayEvents();
339
765
  if (events.length > 0) {
340
- (0, import_core.submitReplay)(serverUrl, projectKey, res.id, events).catch(() => {
766
+ submitReplay(serverUrl, projectKey, res.id, events).catch(() => {
341
767
  });
342
768
  }
343
769
  } catch (err) {
@@ -1180,15 +1606,6 @@ function CheckIcon({ size = 20 }) {
1180
1606
  var import_react4 = require("react");
1181
1607
  var import_react_i18next3 = require("react-i18next");
1182
1608
 
1183
- // src/collectors/console.ts
1184
- var import_core3 = require("@flint/core");
1185
-
1186
- // src/collectors/environment.ts
1187
- var import_core4 = require("@flint/core");
1188
-
1189
- // src/collectors/network.ts
1190
- var import_core5 = require("@flint/core");
1191
-
1192
1609
  // src/i18n/index.ts
1193
1610
  var import_i18next = require("i18next");
1194
1611
  var import_react_i18next2 = require("react-i18next");
@@ -1239,11 +1656,9 @@ widgetI18n.use(import_react_i18next2.initReactI18next).init({
1239
1656
  var i18n_default = widgetI18n;
1240
1657
 
1241
1658
  // src/store.ts
1242
- var import_core6 = require("@flint/core");
1243
1659
  var import_react3 = require("react");
1244
- var import_core7 = require("@flint/core");
1245
1660
  function useFlintStore() {
1246
- return (0, import_react3.useSyncExternalStore)(import_core6.subscribe, import_core6.getSnapshot);
1661
+ return (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot);
1247
1662
  }
1248
1663
 
1249
1664
  // src/FlintWidget.tsx
@@ -1271,6 +1686,8 @@ function WidgetContent({
1271
1686
  enableScreenshot = true,
1272
1687
  enableConsole = true,
1273
1688
  enableNetwork = true,
1689
+ enableFrustration = false,
1690
+ autoReportFrustration = false,
1274
1691
  onBeforeSubmit,
1275
1692
  onSuccess,
1276
1693
  onError,
@@ -1303,7 +1720,7 @@ function WidgetContent({
1303
1720
  const [open, setOpen] = (0, import_react4.useState)(false);
1304
1721
  const [hovered, setHovered] = (0, import_react4.useState)(false);
1305
1722
  const pendingSelection = (0, import_react4.useRef)("");
1306
- const colors = (0, import_core2.resolveTheme)(theme);
1723
+ const colors = resolveTheme(theme);
1307
1724
  const [selectionTooltip, setSelectionTooltip] = (0, import_react4.useState)(null);
1308
1725
  const tooltipRef = (0, import_react4.useRef)(null);
1309
1726
  const triggerRef = (0, import_react4.useRef)(null);
@@ -1351,7 +1768,7 @@ function WidgetContent({
1351
1768
  const replayEvents = (0, import_react4.useRef)([]);
1352
1769
  const stopReplay = (0, import_react4.useRef)(null);
1353
1770
  if (enableConsole && !consoleCollector.current) {
1354
- consoleCollector.current = (0, import_core3.createConsoleCollector)();
1771
+ consoleCollector.current = createConsoleCollector();
1355
1772
  consoleCollector.current.start();
1356
1773
  }
1357
1774
  if (enableNetwork && !networkCollector.current) {
@@ -1362,9 +1779,14 @@ function WidgetContent({
1362
1779
  return "";
1363
1780
  }
1364
1781
  })();
1365
- networkCollector.current = (0, import_core5.createNetworkCollector)(flintHost ? [flintHost] : []);
1782
+ networkCollector.current = createNetworkCollector(flintHost ? [flintHost] : []);
1366
1783
  networkCollector.current.start();
1367
1784
  }
1785
+ const frustrationCollector = (0, import_react4.useRef)(null);
1786
+ if (enableFrustration && !frustrationCollector.current) {
1787
+ frustrationCollector.current = createFrustrationCollector();
1788
+ frustrationCollector.current.start();
1789
+ }
1368
1790
  (0, import_react4.useEffect)(() => {
1369
1791
  let cancelled = false;
1370
1792
  if (enableReplay) {
@@ -1386,9 +1808,32 @@ function WidgetContent({
1386
1808
  cancelled = true;
1387
1809
  consoleCollector.current?.stop();
1388
1810
  networkCollector.current?.stop();
1811
+ frustrationCollector.current?.stop();
1389
1812
  stopReplay.current?.();
1390
1813
  };
1391
1814
  }, [enableReplay]);
1815
+ (0, import_react4.useEffect)(() => {
1816
+ if (!enableFrustration || !autoReportFrustration || !frustrationCollector.current) return;
1817
+ const unsubscribe = frustrationCollector.current.onFrustration(async (event) => {
1818
+ const user2 = resolvedUser;
1819
+ await submitReport(serverUrl, projectKey, {
1820
+ reporterId: user2?.id ?? "anonymous",
1821
+ reporterName: user2?.name ?? "Anonymous",
1822
+ reporterEmail: user2?.email,
1823
+ description: `[Auto-detected] ${event.type.replace(/_/g, " ")}: ${event.details}`,
1824
+ severity: event.type === "error_loop" ? "P1" : event.type === "rage_click" ? "P2" : "P3",
1825
+ url: event.url,
1826
+ meta: {
1827
+ environment: collectEnvironment(),
1828
+ consoleLogs: consoleCollector.current?.getEntries() ?? [],
1829
+ networkErrors: networkCollector.current?.getEntries() ?? [],
1830
+ frustrationEvent: event
1831
+ }
1832
+ }).catch(() => {
1833
+ });
1834
+ });
1835
+ return unsubscribe;
1836
+ }, [enableFrustration, autoReportFrustration]);
1392
1837
  const label = buttonLabel ?? t("buttonLabel");
1393
1838
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1394
1839
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -1485,7 +1930,7 @@ function WidgetContent({
1485
1930
  onClose?.();
1486
1931
  pendingSelection.current = "";
1487
1932
  },
1488
- getEnvironment: import_core4.collectEnvironment,
1933
+ getEnvironment: collectEnvironment,
1489
1934
  getConsoleLogs: () => consoleCollector.current?.getEntries() ?? [],
1490
1935
  getNetworkErrors: () => networkCollector.current?.getEntries() ?? [],
1491
1936
  getReplayEvents: () => [...replayEvents.current],