@diegotsi/flint-react 1.0.1 → 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) */
@@ -538,7 +537,7 @@ declare global {
538
537
  interface Props {
539
538
  projectKey: string;
540
539
  serverUrl: string;
541
- user?: FlintUser;
540
+ user?: FlintUser$1;
542
541
  meta?: Record<string, unknown>;
543
542
  theme: Theme;
544
543
  zIndex: number;
@@ -559,4 +558,14 @@ declare function FlintModal({ projectKey, serverUrl, user, meta, theme, zIndex,
559
558
 
560
559
  declare function FlintWidget(props: FlintWidgetProps): react_jsx_runtime.JSX.Element;
561
560
 
562
- 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,18 +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/frustration.ts
1152
- import { createFrustrationCollector } from "@flint/core";
1153
-
1154
- // src/collectors/network.ts
1155
- import { createNetworkCollector } from "@flint/core";
1156
-
1157
1571
  // src/i18n/index.ts
1158
1572
  import { createInstance } from "i18next";
1159
1573
  import { initReactI18next } from "react-i18next";
@@ -1204,9 +1618,7 @@ widgetI18n.use(initReactI18next).init({
1204
1618
  var i18n_default = widgetI18n;
1205
1619
 
1206
1620
  // src/store.ts
1207
- import { getSnapshot, subscribe } from "@flint/core";
1208
1621
  import { useSyncExternalStore } from "react";
1209
- import { flint as flint2 } from "@flint/core";
1210
1622
  function useFlintStore() {
1211
1623
  return useSyncExternalStore(subscribe, getSnapshot);
1212
1624
  }
@@ -1537,6 +1949,6 @@ function SparkIcon2() {
1537
1949
  export {
1538
1950
  FlintModal,
1539
1951
  FlintWidget,
1540
- flint2 as flint
1952
+ flint
1541
1953
  };
1542
1954
  //# sourceMappingURL=index.js.map