@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 +470 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +462 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
//
|
|
6
|
-
import {
|
|
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
|
-
|
|
1952
|
+
flint
|
|
1508
1953
|
};
|
|
1509
1954
|
//# sourceMappingURL=index.js.map
|