@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.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- export { flint } from '@flint/core';
3
2
 
4
3
  type Severity = "P1" | "P2" | "P3" | "P4";
5
4
  interface EnvironmentInfo {
@@ -31,7 +30,7 @@ interface ThemeOverride {
31
30
  text?: string;
32
31
  border?: string;
33
32
  }
34
- interface FlintUser {
33
+ interface FlintUser$1 {
35
34
  id: string;
36
35
  name: string;
37
36
  email?: string;
@@ -47,7 +46,7 @@ interface FlintWidgetProps {
47
46
  /** Full URL of the flint-server, e.g. "https://bugs.example.com" */
48
47
  serverUrl: string;
49
48
  /** Authenticated user info (optional) */
50
- user?: FlintUser;
49
+ user?: FlintUser$1;
51
50
  /** Arbitrary metadata to attach to every report */
52
51
  meta?: Record<string, unknown>;
53
52
  /** Extra fields for developer use (e.g. external session replay URL) */
@@ -77,6 +76,10 @@ interface FlintWidgetProps {
77
76
  enableConsole?: boolean;
78
77
  /** Enable network error collection. Default: true */
79
78
  enableNetwork?: boolean;
79
+ /** Enable frustration detection (rage clicks, dead clicks, error loops). Default: false */
80
+ enableFrustration?: boolean;
81
+ /** Auto-submit bug report on frustration detection. Default: false */
82
+ autoReportFrustration?: boolean;
80
83
  /** Called before report submission. Return false to cancel. */
81
84
  onBeforeSubmit?: (payload: ReportPayload) => boolean | Promise<boolean>;
82
85
  /** Called after successful submission */
@@ -534,7 +537,7 @@ declare global {
534
537
  interface Props {
535
538
  projectKey: string;
536
539
  serverUrl: string;
537
- user?: FlintUser;
540
+ user?: FlintUser$1;
538
541
  meta?: Record<string, unknown>;
539
542
  theme: Theme;
540
543
  zIndex: number;
@@ -555,4 +558,14 @@ declare function FlintModal({ projectKey, serverUrl, user, meta, theme, zIndex,
555
558
 
556
559
  declare function FlintWidget(props: FlintWidgetProps): react_jsx_runtime.JSX.Element;
557
560
 
558
- export { FlintModal, type FlintUser, FlintWidget, type FlintWidgetProps, type Locale, type ReportPayload, type ReportResult, type Severity, type Theme, type ThemeOverride };
561
+ interface FlintUser {
562
+ id: string;
563
+ name: string;
564
+ email?: string;
565
+ }
566
+ declare const flint: {
567
+ setUser(user: FlintUser | null): void;
568
+ setSessionReplay(url: string | (() => string) | null): void;
569
+ };
570
+
571
+ export { FlintModal, type FlintUser$1 as FlintUser, FlintWidget, type FlintWidgetProps, type Locale, type ReportPayload, type ReportResult, type Severity, type Theme, type ThemeOverride, flint };
package/dist/index.js CHANGED
@@ -2,8 +2,437 @@
2
2
  import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
3
3
  import { useTranslation } from "react-i18next";
4
4
 
5
- // src/api.ts
6
- import { submitReplay, submitReport } from "@flint/core";
5
+ // ../core/dist/index.js
6
+ import { gzipSync } from "fflate";
7
+ async function fetchWithRetry(url, init, retries = 3, baseDelay = 1e3) {
8
+ for (let attempt = 0; attempt <= retries; attempt++) {
9
+ try {
10
+ const res = await fetch(url, init);
11
+ if (res.ok || attempt === retries) return res;
12
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) return res;
13
+ } catch (err) {
14
+ if (attempt === retries) throw err;
15
+ }
16
+ await new Promise((r) => setTimeout(r, baseDelay * 2 ** attempt));
17
+ }
18
+ throw new Error("Max retries exceeded");
19
+ }
20
+ async function submitReport(serverUrl, projectKey, payload, screenshot) {
21
+ const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports`;
22
+ let body;
23
+ const headers = {
24
+ "x-project-key": projectKey
25
+ };
26
+ if (screenshot) {
27
+ const form = new FormData();
28
+ form.append("reporterId", payload.reporterId);
29
+ form.append("reporterName", payload.reporterName);
30
+ if (payload.reporterEmail) form.append("reporterEmail", payload.reporterEmail);
31
+ form.append("description", payload.description);
32
+ if (payload.expectedBehavior) form.append("expectedBehavior", payload.expectedBehavior);
33
+ if (payload.stepsToReproduce) form.append("stepsToReproduce", JSON.stringify(payload.stepsToReproduce));
34
+ if (payload.externalReplayUrl) form.append("externalReplayUrl", payload.externalReplayUrl);
35
+ if (payload.additionalContext) form.append("additionalContext", payload.additionalContext);
36
+ form.append("severity", payload.severity);
37
+ if (payload.url) form.append("url", payload.url);
38
+ if (payload.meta) form.append("meta", JSON.stringify(payload.meta));
39
+ form.append("screenshot", screenshot);
40
+ body = form;
41
+ } else {
42
+ body = JSON.stringify(payload);
43
+ headers["Content-Type"] = "application/json";
44
+ }
45
+ const res = await fetchWithRetry(url, { method: "POST", headers, body });
46
+ if (!res.ok) {
47
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
48
+ throw new Error(err.error ?? `HTTP ${res.status}`);
49
+ }
50
+ return res.json();
51
+ }
52
+ async function submitReplay(serverUrl, projectKey, reportId, events) {
53
+ const json = JSON.stringify(events);
54
+ const encoded = new TextEncoder().encode(json);
55
+ const compressed = gzipSync(encoded);
56
+ const url = `${serverUrl.replace(/\/$/, "")}/api/v1/bug-reports/${reportId}/replay`;
57
+ await fetch(url, {
58
+ method: "POST",
59
+ headers: {
60
+ "x-project-key": projectKey,
61
+ "Content-Type": "application/octet-stream"
62
+ },
63
+ body: compressed.buffer
64
+ });
65
+ }
66
+ var MAX_ENTRIES = 50;
67
+ function createConsoleCollector() {
68
+ const entries = [];
69
+ let active = false;
70
+ const originals = {
71
+ log: console.log.bind(console),
72
+ warn: console.warn.bind(console),
73
+ error: console.error.bind(console)
74
+ };
75
+ let origOnerror = null;
76
+ let origUnhandled = null;
77
+ function push(level, args) {
78
+ let str;
79
+ try {
80
+ str = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
81
+ } catch {
82
+ str = String(args[0]);
83
+ }
84
+ if (str.length > 500) str = str.slice(0, 500) + "\u2026";
85
+ entries.push({ level, args: str, timestamp: Date.now() });
86
+ if (entries.length > MAX_ENTRIES) entries.shift();
87
+ }
88
+ return {
89
+ start() {
90
+ if (active) return;
91
+ active = true;
92
+ console.log = (...args) => {
93
+ push("log", args);
94
+ originals.log(...args);
95
+ };
96
+ console.warn = (...args) => {
97
+ push("warn", args);
98
+ originals.warn(...args);
99
+ };
100
+ console.error = (...args) => {
101
+ push("error", args);
102
+ originals.error(...args);
103
+ };
104
+ origOnerror = window.onerror;
105
+ window.onerror = (msg, src, line, col, err) => {
106
+ push("error", [err?.message ?? String(msg), `${src}:${line}:${col}`]);
107
+ if (typeof origOnerror === "function") return origOnerror(msg, src, line, col, err);
108
+ return false;
109
+ };
110
+ origUnhandled = window.onunhandledrejection;
111
+ window.onunhandledrejection = (event) => {
112
+ const reason = event.reason instanceof Error ? event.reason.message : JSON.stringify(event.reason);
113
+ push("error", ["UnhandledRejection:", reason]);
114
+ if (typeof origUnhandled === "function") origUnhandled.call(window, event);
115
+ };
116
+ },
117
+ stop() {
118
+ if (!active) return;
119
+ active = false;
120
+ console.log = originals.log;
121
+ console.warn = originals.warn;
122
+ console.error = originals.error;
123
+ window.onerror = origOnerror;
124
+ window.onunhandledrejection = origUnhandled;
125
+ },
126
+ getEntries() {
127
+ return [...entries];
128
+ }
129
+ };
130
+ }
131
+ function collectEnvironment() {
132
+ const ua = navigator.userAgent;
133
+ let browser = "Unknown";
134
+ const chromeM = ua.match(/Chrome\/(\d+)/);
135
+ const firefoxM = ua.match(/Firefox\/(\d+)/);
136
+ const edgeM = ua.match(/Edg\/(\d+)/);
137
+ const safariM = ua.match(/Version\/(\d+)/);
138
+ const operaM = ua.match(/OPR\/(\d+)/);
139
+ if (operaM) {
140
+ browser = `Opera ${operaM[1]}`;
141
+ } else if (edgeM) {
142
+ browser = `Edge ${edgeM[1]}`;
143
+ } else if (chromeM && !/Edg|OPR/.test(ua)) {
144
+ browser = `Chrome ${chromeM[1]}`;
145
+ } else if (firefoxM) {
146
+ browser = `Firefox ${firefoxM[1]}`;
147
+ } else if (safariM && /Safari\//.test(ua)) {
148
+ browser = `Safari ${safariM[1]}`;
149
+ }
150
+ let os = "Unknown";
151
+ const macM = ua.match(/Mac OS X (\d+[._]\d+)/);
152
+ const winM = ua.match(/Windows NT (\d+\.\d+)/);
153
+ const androidM = ua.match(/Android (\d+)/);
154
+ const iosM = ua.match(/iPhone OS (\d+[._]\d+)/);
155
+ if (macM) {
156
+ os = `macOS ${macM[1].replace("_", ".")}`;
157
+ } else if (winM) {
158
+ const winMap = {
159
+ "10.0": "10/11",
160
+ "6.3": "8.1",
161
+ "6.2": "8",
162
+ "6.1": "7"
163
+ };
164
+ os = `Windows ${winMap[winM[1]] ?? winM[1]}`;
165
+ } else if (androidM) {
166
+ os = `Android ${androidM[1]}`;
167
+ } else if (iosM) {
168
+ os = `iOS ${iosM[1].replace(/_/g, ".")}`;
169
+ } else if (/Linux/.test(ua)) {
170
+ os = "Linux";
171
+ }
172
+ return {
173
+ browser,
174
+ os,
175
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
176
+ screen: `${screen.width}x${screen.height}`,
177
+ language: navigator.language,
178
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
179
+ online: navigator.onLine
180
+ };
181
+ }
182
+ function createFrustrationCollector(opts) {
183
+ const threshold = opts?.rageClickThreshold ?? 3;
184
+ const window2 = opts?.rageClickWindow ?? 500;
185
+ const errorThreshold = opts?.errorLoopThreshold ?? 3;
186
+ const errorWindow = opts?.errorLoopWindow ?? 3e4;
187
+ const deadClicksEnabled = opts?.enableDeadClicks ?? true;
188
+ const events = [];
189
+ const listeners2 = /* @__PURE__ */ new Set();
190
+ const clickHistory = [];
191
+ const errorCounts = /* @__PURE__ */ new Map();
192
+ let clickHandler = null;
193
+ let origConsoleError = null;
194
+ function emit2(event) {
195
+ events.push(event);
196
+ if (events.length > 50) events.shift();
197
+ for (const cb of listeners2) cb(event);
198
+ }
199
+ function getCSSSelector(el) {
200
+ if (el.id) return `#${el.id}`;
201
+ const tag = el.tagName.toLowerCase();
202
+ const cls = [...el.classList].slice(0, 3).join(".");
203
+ if (cls) return `${tag}.${cls}`;
204
+ return tag;
205
+ }
206
+ function isInteractive(el) {
207
+ const tag = el.tagName.toLowerCase();
208
+ if (["a", "button", "input", "select", "textarea", "label", "summary"].includes(tag)) return true;
209
+ if (el.getAttribute("role") === "button" || el.getAttribute("tabindex")) return true;
210
+ if (el.onclick || el.getAttribute("onclick")) return true;
211
+ if (el.closest("a, button, [role=button], [onclick]")) return true;
212
+ try {
213
+ if (getComputedStyle(el).cursor === "pointer") return true;
214
+ } catch {
215
+ }
216
+ return false;
217
+ }
218
+ function handleClick(e) {
219
+ const target = e.target;
220
+ if (!target) return;
221
+ const now = Date.now();
222
+ clickHistory.push({ target, time: now });
223
+ while (clickHistory.length > 0 && now - clickHistory[0].time > 1e3) {
224
+ clickHistory.shift();
225
+ }
226
+ const recentOnSame = clickHistory.filter((c) => c.target === target && now - c.time < window2);
227
+ if (recentOnSame.length >= threshold) {
228
+ emit2({
229
+ type: "rage_click",
230
+ timestamp: now,
231
+ target: getCSSSelector(target),
232
+ details: `${recentOnSame.length} clicks in ${now - recentOnSame[0].time}ms`,
233
+ url: globalThis.location?.href ?? ""
234
+ });
235
+ clickHistory.length = 0;
236
+ }
237
+ if (deadClicksEnabled && !isInteractive(target)) {
238
+ emit2({
239
+ type: "dead_click",
240
+ timestamp: now,
241
+ target: getCSSSelector(target),
242
+ details: `Click on non-interactive <${target.tagName.toLowerCase()}>`,
243
+ url: globalThis.location?.href ?? ""
244
+ });
245
+ }
246
+ }
247
+ function patchConsoleError() {
248
+ origConsoleError = console.error;
249
+ console.error = (...args) => {
250
+ origConsoleError?.apply(console, args);
251
+ const key = String(args[0]).slice(0, 100);
252
+ const now = Date.now();
253
+ const existing = errorCounts.get(key);
254
+ if (existing && now - existing.firstSeen < errorWindow) {
255
+ existing.count++;
256
+ if (existing.count >= errorThreshold) {
257
+ emit2({
258
+ type: "error_loop",
259
+ timestamp: now,
260
+ target: "console",
261
+ details: `Same error ${existing.count}x in ${Math.round((now - existing.firstSeen) / 1e3)}s: ${key}`,
262
+ url: globalThis.location?.href ?? ""
263
+ });
264
+ errorCounts.delete(key);
265
+ }
266
+ } else {
267
+ errorCounts.set(key, { count: 1, firstSeen: now });
268
+ }
269
+ };
270
+ }
271
+ return {
272
+ start() {
273
+ clickHandler = handleClick;
274
+ document.addEventListener("click", clickHandler, true);
275
+ patchConsoleError();
276
+ },
277
+ stop() {
278
+ if (clickHandler) document.removeEventListener("click", clickHandler, true);
279
+ if (origConsoleError) console.error = origConsoleError;
280
+ clickHistory.length = 0;
281
+ errorCounts.clear();
282
+ },
283
+ getEvents() {
284
+ return [...events];
285
+ },
286
+ onFrustration(callback) {
287
+ listeners2.add(callback);
288
+ return () => listeners2.delete(callback);
289
+ }
290
+ };
291
+ }
292
+ var MAX_ENTRIES2 = 50;
293
+ var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
294
+ "browser-intake-datadoghq.com",
295
+ "rum.browser-intake-datadoghq.com",
296
+ "logs.browser-intake-datadoghq.com",
297
+ "session-replay.browser-intake-datadoghq.com"
298
+ ]);
299
+ function isBlockedUrl(url, extra) {
300
+ try {
301
+ const host = new URL(url, location.href).hostname;
302
+ const all = [...BLOCKED_HOSTS, ...extra];
303
+ return all.some((b) => host === b || host.endsWith("." + b));
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+ function truncateUrl(url) {
309
+ try {
310
+ const u = new URL(url, location.href);
311
+ const base = `${u.origin}${u.pathname}`;
312
+ return base.length > 200 ? base.slice(0, 200) + "\u2026" : base;
313
+ } catch {
314
+ return url.length > 200 ? url.slice(0, 200) + "\u2026" : url;
315
+ }
316
+ }
317
+ function createNetworkCollector(extraBlockedHosts = []) {
318
+ const entries = [];
319
+ const blocked = new Set(extraBlockedHosts);
320
+ let origFetch = null;
321
+ let origXHROpen = null;
322
+ let active = false;
323
+ function push(entry) {
324
+ entries.push(entry);
325
+ if (entries.length > MAX_ENTRIES2) entries.shift();
326
+ }
327
+ return {
328
+ start() {
329
+ if (active) return;
330
+ active = true;
331
+ origFetch = window.fetch;
332
+ window.fetch = async (input, init) => {
333
+ const method = (init?.method ?? "GET").toUpperCase();
334
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
335
+ const startTime = Date.now();
336
+ const res = await origFetch.call(window, input, init);
337
+ if (res.status >= 400 && !isBlockedUrl(url, blocked)) {
338
+ push({
339
+ method,
340
+ url: truncateUrl(url),
341
+ status: res.status,
342
+ duration: Date.now() - startTime,
343
+ timestamp: startTime
344
+ });
345
+ }
346
+ return res;
347
+ };
348
+ origXHROpen = XMLHttpRequest.prototype.open;
349
+ XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
350
+ const startTime = Date.now();
351
+ const urlStr = typeof url === "string" ? url : url.href;
352
+ this.addEventListener("load", () => {
353
+ if (this.status >= 400 && !isBlockedUrl(urlStr, blocked)) {
354
+ push({
355
+ method: method.toUpperCase(),
356
+ url: truncateUrl(urlStr),
357
+ status: this.status,
358
+ duration: Date.now() - startTime,
359
+ timestamp: startTime
360
+ });
361
+ }
362
+ });
363
+ return origXHROpen.apply(this, [method, url, async ?? true, username, password]);
364
+ };
365
+ },
366
+ stop() {
367
+ if (!active) return;
368
+ active = false;
369
+ if (origFetch) window.fetch = origFetch;
370
+ if (origXHROpen) XMLHttpRequest.prototype.open = origXHROpen;
371
+ },
372
+ getEntries() {
373
+ return [...entries];
374
+ }
375
+ };
376
+ }
377
+ var state = { user: void 0, sessionReplay: void 0 };
378
+ var listeners = /* @__PURE__ */ new Set();
379
+ function emit() {
380
+ for (const l of listeners) l();
381
+ }
382
+ function subscribe(listener) {
383
+ listeners.add(listener);
384
+ return () => listeners.delete(listener);
385
+ }
386
+ function getSnapshot() {
387
+ return state;
388
+ }
389
+ var flint = {
390
+ setUser(user) {
391
+ state = { ...state, user: user ?? void 0 };
392
+ emit();
393
+ },
394
+ setSessionReplay(url) {
395
+ state = { ...state, sessionReplay: url ?? void 0 };
396
+ emit();
397
+ }
398
+ };
399
+ var light = {
400
+ background: "rgba(255,255,255,0.90)",
401
+ backgroundSecondary: "rgba(249,250,251,0.75)",
402
+ accent: "#2563eb",
403
+ accentHover: "#1d4ed8",
404
+ text: "#111827",
405
+ textMuted: "#6b7280",
406
+ border: "rgba(255,255,255,0.9)",
407
+ 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)",
408
+ buttonText: "#ffffff",
409
+ backdropFilter: "blur(32px) saturate(1.8)"
410
+ };
411
+ var dark = {
412
+ background: "rgba(15,20,35,0.88)",
413
+ backgroundSecondary: "rgba(5,8,18,0.65)",
414
+ accent: "#4d8aff",
415
+ accentHover: "#3b6fdb",
416
+ text: "#dde3ef",
417
+ textMuted: "#6b7a93",
418
+ border: "rgba(255,255,255,0.08)",
419
+ shadow: "0 24px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
420
+ buttonText: "#ffffff",
421
+ backdropFilter: "blur(32px) saturate(1.6)"
422
+ };
423
+ function resolveTheme(theme) {
424
+ if (theme === "dark") return dark;
425
+ if (theme === "light") return light;
426
+ const override = theme;
427
+ return {
428
+ ...light,
429
+ background: override.background ?? light.background,
430
+ accent: override.accent ?? light.accent,
431
+ accentHover: override.accent ?? light.accentHover,
432
+ text: override.text ?? light.text,
433
+ border: override.border ?? light.border
434
+ };
435
+ }
7
436
 
8
437
  // src/ScreenAnnotator.tsx
9
438
  import { domToCanvas } from "modern-screenshot";
@@ -147,9 +576,6 @@ function ScreenAnnotator({ zIndex, onCapture, onCancel }) {
147
576
  );
148
577
  }
149
578
 
150
- // src/theme.ts
151
- import { resolveTheme } from "@flint/core";
152
-
153
579
  // src/FlintModal.tsx
154
580
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
155
581
  var SEVERITIES = ["P1", "P2", "P3", "P4"];
@@ -1142,15 +1568,6 @@ function CheckIcon({ size = 20 }) {
1142
1568
  import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
1143
1569
  import { I18nextProvider, useTranslation as useTranslation2 } from "react-i18next";
1144
1570
 
1145
- // src/collectors/console.ts
1146
- import { createConsoleCollector } from "@flint/core";
1147
-
1148
- // src/collectors/environment.ts
1149
- import { collectEnvironment } from "@flint/core";
1150
-
1151
- // src/collectors/network.ts
1152
- import { createNetworkCollector } from "@flint/core";
1153
-
1154
1571
  // src/i18n/index.ts
1155
1572
  import { createInstance } from "i18next";
1156
1573
  import { initReactI18next } from "react-i18next";
@@ -1201,9 +1618,7 @@ widgetI18n.use(initReactI18next).init({
1201
1618
  var i18n_default = widgetI18n;
1202
1619
 
1203
1620
  // src/store.ts
1204
- import { getSnapshot, subscribe } from "@flint/core";
1205
1621
  import { useSyncExternalStore } from "react";
1206
- import { flint as flint2 } from "@flint/core";
1207
1622
  function useFlintStore() {
1208
1623
  return useSyncExternalStore(subscribe, getSnapshot);
1209
1624
  }
@@ -1233,6 +1648,8 @@ function WidgetContent({
1233
1648
  enableScreenshot = true,
1234
1649
  enableConsole = true,
1235
1650
  enableNetwork = true,
1651
+ enableFrustration = false,
1652
+ autoReportFrustration = false,
1236
1653
  onBeforeSubmit,
1237
1654
  onSuccess,
1238
1655
  onError,
@@ -1327,6 +1744,11 @@ function WidgetContent({
1327
1744
  networkCollector.current = createNetworkCollector(flintHost ? [flintHost] : []);
1328
1745
  networkCollector.current.start();
1329
1746
  }
1747
+ const frustrationCollector = useRef3(null);
1748
+ if (enableFrustration && !frustrationCollector.current) {
1749
+ frustrationCollector.current = createFrustrationCollector();
1750
+ frustrationCollector.current.start();
1751
+ }
1330
1752
  useEffect3(() => {
1331
1753
  let cancelled = false;
1332
1754
  if (enableReplay) {
@@ -1348,9 +1770,32 @@ function WidgetContent({
1348
1770
  cancelled = true;
1349
1771
  consoleCollector.current?.stop();
1350
1772
  networkCollector.current?.stop();
1773
+ frustrationCollector.current?.stop();
1351
1774
  stopReplay.current?.();
1352
1775
  };
1353
1776
  }, [enableReplay]);
1777
+ useEffect3(() => {
1778
+ if (!enableFrustration || !autoReportFrustration || !frustrationCollector.current) return;
1779
+ const unsubscribe = frustrationCollector.current.onFrustration(async (event) => {
1780
+ const user2 = resolvedUser;
1781
+ await submitReport(serverUrl, projectKey, {
1782
+ reporterId: user2?.id ?? "anonymous",
1783
+ reporterName: user2?.name ?? "Anonymous",
1784
+ reporterEmail: user2?.email,
1785
+ description: `[Auto-detected] ${event.type.replace(/_/g, " ")}: ${event.details}`,
1786
+ severity: event.type === "error_loop" ? "P1" : event.type === "rage_click" ? "P2" : "P3",
1787
+ url: event.url,
1788
+ meta: {
1789
+ environment: collectEnvironment(),
1790
+ consoleLogs: consoleCollector.current?.getEntries() ?? [],
1791
+ networkErrors: networkCollector.current?.getEntries() ?? [],
1792
+ frustrationEvent: event
1793
+ }
1794
+ }).catch(() => {
1795
+ });
1796
+ });
1797
+ return unsubscribe;
1798
+ }, [enableFrustration, autoReportFrustration]);
1354
1799
  const label = buttonLabel ?? t("buttonLabel");
1355
1800
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1356
1801
  /* @__PURE__ */ jsxs3(
@@ -1504,6 +1949,6 @@ function SparkIcon2() {
1504
1949
  export {
1505
1950
  FlintModal,
1506
1951
  FlintWidget,
1507
- flint2 as flint
1952
+ flint
1508
1953
  };
1509
1954
  //# sourceMappingURL=index.js.map