@diegotsi/flint-core 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/dist/index.cjs +451 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +217 -22
- package/dist/index.d.ts +217 -22
- package/dist/index.js +448 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/dist/index.cjs
CHANGED
|
@@ -20,8 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
Flint: () => Flint,
|
|
24
|
+
_setFormErrorCollector: () => _setFormErrorCollector,
|
|
23
25
|
collectEnvironment: () => collectEnvironment,
|
|
24
26
|
createConsoleCollector: () => createConsoleCollector,
|
|
27
|
+
createFormErrorCollector: () => createFormErrorCollector,
|
|
25
28
|
createFrustrationCollector: () => createFrustrationCollector,
|
|
26
29
|
createNetworkCollector: () => createNetworkCollector,
|
|
27
30
|
flint: () => flint,
|
|
@@ -35,10 +38,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
35
38
|
|
|
36
39
|
// src/api.ts
|
|
37
40
|
var import_fflate = require("fflate");
|
|
38
|
-
async function fetchWithRetry(url,
|
|
41
|
+
async function fetchWithRetry(url, init2, retries = 3, baseDelay = 1e3) {
|
|
39
42
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
40
43
|
try {
|
|
41
|
-
const res = await fetch(url,
|
|
44
|
+
const res = await fetch(url, init2);
|
|
42
45
|
if (res.ok || attempt === retries) return res;
|
|
43
46
|
if (res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
44
47
|
} catch (err) {
|
|
@@ -215,89 +218,232 @@ function collectEnvironment() {
|
|
|
215
218
|
};
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
// src/collectors/
|
|
219
|
-
var MAX_ENTRIES2 =
|
|
220
|
-
var
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
221
|
+
// src/collectors/formErrors.ts
|
|
222
|
+
var MAX_ENTRIES2 = 30;
|
|
223
|
+
var POST_SUBMIT_CHECK_MS = 300;
|
|
224
|
+
var SILENT_SUBMIT_WINDOW_MS = 400;
|
|
225
|
+
function getFormId(form) {
|
|
226
|
+
if (form.id) return `#${form.id}`;
|
|
227
|
+
if (form.getAttribute("name")) return `form[name="${form.getAttribute("name")}"]`;
|
|
228
|
+
if (form.action && form.action !== location.href) return form.action;
|
|
229
|
+
return `form:${Array.from(document.forms).indexOf(form)}`;
|
|
230
|
+
}
|
|
231
|
+
function getFieldLabel(field, root) {
|
|
232
|
+
const ariaLabel = field.getAttribute("aria-label");
|
|
233
|
+
if (ariaLabel) return ariaLabel;
|
|
234
|
+
const id = field.id;
|
|
235
|
+
if (id) {
|
|
236
|
+
const label = root.querySelector(`label[for="${id}"]`);
|
|
237
|
+
if (label?.textContent) return label.textContent.trim();
|
|
238
|
+
}
|
|
239
|
+
const parentLabel = field.closest("label");
|
|
240
|
+
if (parentLabel?.textContent) {
|
|
241
|
+
const text = parentLabel.textContent.trim();
|
|
242
|
+
if (text.length < 60) return text;
|
|
233
243
|
}
|
|
244
|
+
return field.name || field.placeholder || field.tagName.toLowerCase();
|
|
234
245
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
246
|
+
function getErrorMessage(field) {
|
|
247
|
+
const errId = field.getAttribute("aria-errormessage");
|
|
248
|
+
if (errId) {
|
|
249
|
+
const el = document.getElementById(errId);
|
|
250
|
+
if (el?.textContent) return el.textContent.trim();
|
|
251
|
+
}
|
|
252
|
+
const descId = field.getAttribute("aria-describedby");
|
|
253
|
+
if (descId) {
|
|
254
|
+
for (const id of descId.split(/\s+/)) {
|
|
255
|
+
const el = document.getElementById(id);
|
|
256
|
+
if (el?.textContent?.trim()) return el.textContent.trim();
|
|
257
|
+
}
|
|
242
258
|
}
|
|
259
|
+
if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement) {
|
|
260
|
+
if (field.validationMessage) return field.validationMessage;
|
|
261
|
+
}
|
|
262
|
+
const next = field.nextElementSibling;
|
|
263
|
+
if (next?.getAttribute("role") === "alert" && next.textContent) {
|
|
264
|
+
return next.textContent.trim();
|
|
265
|
+
}
|
|
266
|
+
return void 0;
|
|
243
267
|
}
|
|
244
|
-
function
|
|
268
|
+
function collectInvalidFields(root) {
|
|
269
|
+
const fields = [];
|
|
270
|
+
const invalidEls = root.querySelectorAll('[aria-invalid="true"], :invalid');
|
|
271
|
+
for (const el of invalidEls) {
|
|
272
|
+
if (el instanceof HTMLFormElement) continue;
|
|
273
|
+
if (el.tagName === "FIELDSET") continue;
|
|
274
|
+
fields.push({
|
|
275
|
+
name: getFieldLabel(el, root),
|
|
276
|
+
message: getErrorMessage(el)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return fields;
|
|
280
|
+
}
|
|
281
|
+
function collectVisibleErrors(root) {
|
|
282
|
+
const fields = [];
|
|
283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
284
|
+
const alerts = root.querySelectorAll('[role="alert"]');
|
|
285
|
+
for (const el of alerts) {
|
|
286
|
+
const text = el.textContent?.trim();
|
|
287
|
+
if (!text || text.length > 200) continue;
|
|
288
|
+
if (seen.has(text)) continue;
|
|
289
|
+
seen.add(text);
|
|
290
|
+
fields.push({ name: "alert", message: text });
|
|
291
|
+
}
|
|
292
|
+
const errorEls = root.querySelectorAll(
|
|
293
|
+
"[data-error], [data-field-error], .error-message, .field-error, .form-error"
|
|
294
|
+
);
|
|
295
|
+
for (const el of errorEls) {
|
|
296
|
+
const text = el.textContent?.trim();
|
|
297
|
+
if (!text || text.length > 200) continue;
|
|
298
|
+
if (seen.has(text)) continue;
|
|
299
|
+
seen.add(text);
|
|
300
|
+
fields.push({ name: "error", message: text });
|
|
301
|
+
}
|
|
302
|
+
return fields;
|
|
303
|
+
}
|
|
304
|
+
function isSubmitButton(el) {
|
|
305
|
+
if (el.tagName === "BUTTON") {
|
|
306
|
+
const type = el.getAttribute("type");
|
|
307
|
+
return type === "submit" || type === null || type === "";
|
|
308
|
+
}
|
|
309
|
+
if (el.tagName === "INPUT") {
|
|
310
|
+
return el.type === "submit";
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
function findClosestForm(el) {
|
|
315
|
+
if ("form" in el && el.form) {
|
|
316
|
+
return el.form;
|
|
317
|
+
}
|
|
318
|
+
return el.closest("form");
|
|
319
|
+
}
|
|
320
|
+
function createFormErrorCollector() {
|
|
245
321
|
const entries = [];
|
|
246
|
-
const
|
|
247
|
-
let origFetch = null;
|
|
248
|
-
let origXHROpen = null;
|
|
322
|
+
const attemptCounts = /* @__PURE__ */ new Map();
|
|
249
323
|
let active = false;
|
|
324
|
+
let submitHandler = null;
|
|
325
|
+
let invalidHandler = null;
|
|
326
|
+
let clickHandler = null;
|
|
327
|
+
let lastSubmitTime = 0;
|
|
328
|
+
const recentRecords = /* @__PURE__ */ new Map();
|
|
329
|
+
const DEDUP_MS = 500;
|
|
250
330
|
function push(entry) {
|
|
331
|
+
const key = `${entry.formId}:${entry.timestamp}`;
|
|
332
|
+
const last = recentRecords.get(entry.formId);
|
|
333
|
+
if (last && entry.timestamp - last < DEDUP_MS) return;
|
|
334
|
+
recentRecords.set(entry.formId, entry.timestamp);
|
|
251
335
|
entries.push(entry);
|
|
252
336
|
if (entries.length > MAX_ENTRIES2) entries.shift();
|
|
253
337
|
}
|
|
338
|
+
function recordFormErrors(form, type) {
|
|
339
|
+
const formId = getFormId(form);
|
|
340
|
+
let fields = collectInvalidFields(form);
|
|
341
|
+
if (fields.length === 0) {
|
|
342
|
+
fields = collectVisibleErrors(form);
|
|
343
|
+
}
|
|
344
|
+
if (fields.length === 0) return;
|
|
345
|
+
const count = (attemptCounts.get(formId) ?? 0) + 1;
|
|
346
|
+
attemptCounts.set(formId, count);
|
|
347
|
+
push({
|
|
348
|
+
type,
|
|
349
|
+
formId,
|
|
350
|
+
fields,
|
|
351
|
+
attemptNumber: count,
|
|
352
|
+
url: location.href,
|
|
353
|
+
timestamp: Date.now()
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function handleSubmitButtonClick(button) {
|
|
357
|
+
const form = findClosestForm(button);
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
if (!active) return;
|
|
360
|
+
if (Date.now() - lastSubmitTime < SILENT_SUBMIT_WINDOW_MS) return;
|
|
361
|
+
const root = form ?? document.body;
|
|
362
|
+
const formId = form ? getFormId(form) : `page:${location.pathname}`;
|
|
363
|
+
let fields = form ? collectInvalidFields(form) : [];
|
|
364
|
+
if (fields.length === 0) {
|
|
365
|
+
fields = collectVisibleErrors(root);
|
|
366
|
+
}
|
|
367
|
+
if (fields.length === 0 && form) {
|
|
368
|
+
fields = collectVisibleErrors(document.body);
|
|
369
|
+
}
|
|
370
|
+
if (fields.length === 0) return;
|
|
371
|
+
const count = (attemptCounts.get(formId) ?? 0) + 1;
|
|
372
|
+
attemptCounts.set(formId, count);
|
|
373
|
+
push({
|
|
374
|
+
type: "silent_submit",
|
|
375
|
+
formId,
|
|
376
|
+
fields,
|
|
377
|
+
attemptNumber: count,
|
|
378
|
+
url: location.href,
|
|
379
|
+
timestamp: Date.now()
|
|
380
|
+
});
|
|
381
|
+
}, SILENT_SUBMIT_WINDOW_MS);
|
|
382
|
+
}
|
|
254
383
|
return {
|
|
255
384
|
start() {
|
|
256
385
|
if (active) return;
|
|
257
386
|
active = true;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
method,
|
|
267
|
-
url: truncateUrl(url),
|
|
268
|
-
status: res.status,
|
|
269
|
-
duration: Date.now() - startTime,
|
|
270
|
-
timestamp: startTime
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
return res;
|
|
387
|
+
submitHandler = (e) => {
|
|
388
|
+
lastSubmitTime = Date.now();
|
|
389
|
+
const form = e.target;
|
|
390
|
+
if (!(form instanceof HTMLFormElement)) return;
|
|
391
|
+
setTimeout(() => {
|
|
392
|
+
if (!active) return;
|
|
393
|
+
recordFormErrors(form, "validation_failed");
|
|
394
|
+
}, POST_SUBMIT_CHECK_MS);
|
|
274
395
|
};
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
396
|
+
document.addEventListener("submit", submitHandler, true);
|
|
397
|
+
invalidHandler = (e) => {
|
|
398
|
+
lastSubmitTime = Date.now();
|
|
399
|
+
const field = e.target;
|
|
400
|
+
const form = field.closest("form");
|
|
401
|
+
if (!form) return;
|
|
402
|
+
setTimeout(() => {
|
|
403
|
+
if (!active) return;
|
|
404
|
+
recordFormErrors(form, "validation_failed");
|
|
405
|
+
}, POST_SUBMIT_CHECK_MS);
|
|
406
|
+
};
|
|
407
|
+
document.addEventListener("invalid", invalidHandler, true);
|
|
408
|
+
clickHandler = (e) => {
|
|
409
|
+
const target = e.target;
|
|
410
|
+
if (!target) return;
|
|
411
|
+
const button = target.closest("button, input[type=submit]");
|
|
412
|
+
if (!button) return;
|
|
413
|
+
if (!isSubmitButton(button)) return;
|
|
414
|
+
handleSubmitButtonClick(button);
|
|
291
415
|
};
|
|
416
|
+
document.addEventListener("click", clickHandler, true);
|
|
292
417
|
},
|
|
293
418
|
stop() {
|
|
294
419
|
if (!active) return;
|
|
295
420
|
active = false;
|
|
296
|
-
if (
|
|
297
|
-
if (
|
|
421
|
+
if (submitHandler) document.removeEventListener("submit", submitHandler, true);
|
|
422
|
+
if (invalidHandler) document.removeEventListener("invalid", invalidHandler, true);
|
|
423
|
+
if (clickHandler) document.removeEventListener("click", clickHandler, true);
|
|
424
|
+
attemptCounts.clear();
|
|
425
|
+
recentRecords.clear();
|
|
298
426
|
},
|
|
299
427
|
getEntries() {
|
|
300
428
|
return [...entries];
|
|
429
|
+
},
|
|
430
|
+
report(formId, errors) {
|
|
431
|
+
const fields = [];
|
|
432
|
+
for (const [name, err] of Object.entries(errors)) {
|
|
433
|
+
if (!err) continue;
|
|
434
|
+
fields.push({ name, message: err.message });
|
|
435
|
+
}
|
|
436
|
+
if (fields.length === 0) return;
|
|
437
|
+
const count = (attemptCounts.get(formId) ?? 0) + 1;
|
|
438
|
+
attemptCounts.set(formId, count);
|
|
439
|
+
push({
|
|
440
|
+
type: "validation_failed",
|
|
441
|
+
formId,
|
|
442
|
+
fields,
|
|
443
|
+
attemptNumber: count,
|
|
444
|
+
url: location.href,
|
|
445
|
+
timestamp: Date.now()
|
|
446
|
+
});
|
|
301
447
|
}
|
|
302
448
|
};
|
|
303
449
|
}
|
|
@@ -347,9 +493,7 @@ function createFrustrationCollector(opts) {
|
|
|
347
493
|
while (clickHistory.length > 0 && now - clickHistory[0].time > 1e3) {
|
|
348
494
|
clickHistory.shift();
|
|
349
495
|
}
|
|
350
|
-
const recentOnSame = clickHistory.filter(
|
|
351
|
-
(c) => c.target === target && now - c.time < window2
|
|
352
|
-
);
|
|
496
|
+
const recentOnSame = clickHistory.filter((c) => c.target === target && now - c.time < window2);
|
|
353
497
|
if (recentOnSame.length >= threshold) {
|
|
354
498
|
emit2({
|
|
355
499
|
type: "rage_click",
|
|
@@ -416,7 +560,98 @@ function createFrustrationCollector(opts) {
|
|
|
416
560
|
};
|
|
417
561
|
}
|
|
418
562
|
|
|
563
|
+
// src/collectors/network.ts
|
|
564
|
+
var MAX_ENTRIES3 = 50;
|
|
565
|
+
var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
|
|
566
|
+
"browser-intake-datadoghq.com",
|
|
567
|
+
"rum.browser-intake-datadoghq.com",
|
|
568
|
+
"logs.browser-intake-datadoghq.com",
|
|
569
|
+
"session-replay.browser-intake-datadoghq.com"
|
|
570
|
+
]);
|
|
571
|
+
function isBlockedUrl(url, extra) {
|
|
572
|
+
try {
|
|
573
|
+
const host = new URL(url, location.href).hostname;
|
|
574
|
+
const all = [...BLOCKED_HOSTS, ...extra];
|
|
575
|
+
return all.some((b) => host === b || host.endsWith("." + b));
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function truncateUrl(url) {
|
|
581
|
+
try {
|
|
582
|
+
const u = new URL(url, location.href);
|
|
583
|
+
const base = `${u.origin}${u.pathname}`;
|
|
584
|
+
return base.length > 200 ? base.slice(0, 200) + "\u2026" : base;
|
|
585
|
+
} catch {
|
|
586
|
+
return url.length > 200 ? url.slice(0, 200) + "\u2026" : url;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function createNetworkCollector(extraBlockedHosts = []) {
|
|
590
|
+
const entries = [];
|
|
591
|
+
const blocked = new Set(extraBlockedHosts);
|
|
592
|
+
let origFetch = null;
|
|
593
|
+
let origXHROpen = null;
|
|
594
|
+
let active = false;
|
|
595
|
+
function push(entry) {
|
|
596
|
+
entries.push(entry);
|
|
597
|
+
if (entries.length > MAX_ENTRIES3) entries.shift();
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
start() {
|
|
601
|
+
if (active) return;
|
|
602
|
+
active = true;
|
|
603
|
+
origFetch = window.fetch;
|
|
604
|
+
window.fetch = async (input, init2) => {
|
|
605
|
+
const method = (init2?.method ?? "GET").toUpperCase();
|
|
606
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
607
|
+
const startTime = Date.now();
|
|
608
|
+
const res = await origFetch.call(window, input, init2);
|
|
609
|
+
if (res.status >= 400 && !isBlockedUrl(url, blocked)) {
|
|
610
|
+
push({
|
|
611
|
+
method,
|
|
612
|
+
url: truncateUrl(url),
|
|
613
|
+
status: res.status,
|
|
614
|
+
duration: Date.now() - startTime,
|
|
615
|
+
timestamp: startTime
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return res;
|
|
619
|
+
};
|
|
620
|
+
origXHROpen = XMLHttpRequest.prototype.open;
|
|
621
|
+
XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
|
|
622
|
+
const startTime = Date.now();
|
|
623
|
+
const urlStr = typeof url === "string" ? url : url.href;
|
|
624
|
+
this.addEventListener("load", () => {
|
|
625
|
+
if (this.status >= 400 && !isBlockedUrl(urlStr, blocked)) {
|
|
626
|
+
push({
|
|
627
|
+
method: method.toUpperCase(),
|
|
628
|
+
url: truncateUrl(urlStr),
|
|
629
|
+
status: this.status,
|
|
630
|
+
duration: Date.now() - startTime,
|
|
631
|
+
timestamp: startTime
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
return origXHROpen.apply(this, [method, url, async ?? true, username, password]);
|
|
636
|
+
};
|
|
637
|
+
},
|
|
638
|
+
stop() {
|
|
639
|
+
if (!active) return;
|
|
640
|
+
active = false;
|
|
641
|
+
if (origFetch) window.fetch = origFetch;
|
|
642
|
+
if (origXHROpen) XMLHttpRequest.prototype.open = origXHROpen;
|
|
643
|
+
},
|
|
644
|
+
getEntries() {
|
|
645
|
+
return [...entries];
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
419
650
|
// src/store.ts
|
|
651
|
+
var formErrorCollectorRef = null;
|
|
652
|
+
function _setFormErrorCollector(collector) {
|
|
653
|
+
formErrorCollectorRef = collector;
|
|
654
|
+
}
|
|
420
655
|
var state = { user: void 0, sessionReplay: void 0 };
|
|
421
656
|
var listeners = /* @__PURE__ */ new Set();
|
|
422
657
|
function emit() {
|
|
@@ -437,7 +672,155 @@ var flint = {
|
|
|
437
672
|
setSessionReplay(url) {
|
|
438
673
|
state = { ...state, sessionReplay: url ?? void 0 };
|
|
439
674
|
emit();
|
|
675
|
+
},
|
|
676
|
+
/**
|
|
677
|
+
* Report a form validation failure with exact field-level errors.
|
|
678
|
+
* Call this from your form library's error callback.
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* // React Hook Form + Zod
|
|
682
|
+
* const onSubmit = handleSubmit(onValid, (errors) => {
|
|
683
|
+
* flint.reportFormError('checkout-form', errors);
|
|
684
|
+
* });
|
|
685
|
+
*/
|
|
686
|
+
reportFormError(formId, errors) {
|
|
687
|
+
formErrorCollectorRef?.report(formId, errors);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/singleton.ts
|
|
692
|
+
var DEFAULT_REPLAY_BUFFER_MS = 6e4;
|
|
693
|
+
var instance = null;
|
|
694
|
+
function init(config) {
|
|
695
|
+
if (instance) {
|
|
696
|
+
console.warn("[Flint] Already initialized. Call Flint.shutdown() first to re-initialize.");
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const {
|
|
700
|
+
enableConsole = true,
|
|
701
|
+
enableNetwork = true,
|
|
702
|
+
enableFormErrors = true,
|
|
703
|
+
enableFrustration = false,
|
|
704
|
+
autoReportFrustration = false,
|
|
705
|
+
enableReplay = false,
|
|
706
|
+
replayBufferMs = DEFAULT_REPLAY_BUFFER_MS,
|
|
707
|
+
blockedHosts = [],
|
|
708
|
+
frustration: frustrationOpts,
|
|
709
|
+
onFrustration,
|
|
710
|
+
_replayRecorder
|
|
711
|
+
} = config;
|
|
712
|
+
const flintHost = (() => {
|
|
713
|
+
try {
|
|
714
|
+
return new URL(config.serverUrl).hostname;
|
|
715
|
+
} catch {
|
|
716
|
+
return "";
|
|
717
|
+
}
|
|
718
|
+
})();
|
|
719
|
+
const consoleCol = enableConsole ? createConsoleCollector() : null;
|
|
720
|
+
consoleCol?.start();
|
|
721
|
+
const networkCol = enableNetwork ? createNetworkCollector([...blockedHosts, ...flintHost ? [flintHost] : []]) : null;
|
|
722
|
+
networkCol?.start();
|
|
723
|
+
const formErrorsCol = enableFormErrors ? createFormErrorCollector() : null;
|
|
724
|
+
if (formErrorsCol) {
|
|
725
|
+
formErrorsCol.start();
|
|
726
|
+
_setFormErrorCollector(formErrorsCol);
|
|
727
|
+
}
|
|
728
|
+
const frustrationCol = enableFrustration ? createFrustrationCollector(frustrationOpts) : null;
|
|
729
|
+
frustrationCol?.start();
|
|
730
|
+
if (config.user) {
|
|
731
|
+
flint.setUser(config.user);
|
|
732
|
+
}
|
|
733
|
+
const replayEvents = [];
|
|
734
|
+
let stopReplay = null;
|
|
735
|
+
instance = {
|
|
736
|
+
config,
|
|
737
|
+
console: consoleCol,
|
|
738
|
+
network: networkCol,
|
|
739
|
+
formErrors: formErrorsCol,
|
|
740
|
+
frustration: frustrationCol,
|
|
741
|
+
replayEvents,
|
|
742
|
+
stopReplay: null
|
|
743
|
+
};
|
|
744
|
+
if (enableReplay && _replayRecorder) {
|
|
745
|
+
_replayRecorder((event) => {
|
|
746
|
+
replayEvents.push(event);
|
|
747
|
+
const cutoff = Date.now() - replayBufferMs;
|
|
748
|
+
while (replayEvents.length > 0 && replayEvents[0].timestamp < cutoff) {
|
|
749
|
+
replayEvents.shift();
|
|
750
|
+
}
|
|
751
|
+
}).then((stop) => {
|
|
752
|
+
stopReplay = stop ?? null;
|
|
753
|
+
if (instance) instance.stopReplay = stopReplay;
|
|
754
|
+
});
|
|
440
755
|
}
|
|
756
|
+
if (frustrationCol && autoReportFrustration) {
|
|
757
|
+
frustrationCol.onFrustration(async (event) => {
|
|
758
|
+
onFrustration?.(event);
|
|
759
|
+
const user = getSnapshot().user ?? config.user;
|
|
760
|
+
await submitReport(config.serverUrl, config.projectKey, {
|
|
761
|
+
reporterId: user?.id ?? "anonymous",
|
|
762
|
+
reporterName: user?.name ?? "Anonymous",
|
|
763
|
+
reporterEmail: user?.email,
|
|
764
|
+
description: `[Auto-detected] ${event.type.replace(/_/g, " ")}: ${event.details}`,
|
|
765
|
+
severity: event.type === "error_loop" ? "P1" : event.type === "rage_click" ? "P2" : "P3",
|
|
766
|
+
url: event.url,
|
|
767
|
+
meta: {
|
|
768
|
+
...config.meta,
|
|
769
|
+
environment: collectEnvironment(),
|
|
770
|
+
consoleLogs: consoleCol?.getEntries() ?? [],
|
|
771
|
+
networkErrors: networkCol?.getEntries() ?? [],
|
|
772
|
+
formErrors: formErrorsCol?.getEntries() ?? [],
|
|
773
|
+
frustrationEvent: event
|
|
774
|
+
}
|
|
775
|
+
}).catch(() => {
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
} else if (frustrationCol && onFrustration) {
|
|
779
|
+
frustrationCol.onFrustration(onFrustration);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function shutdown() {
|
|
783
|
+
if (!instance) return;
|
|
784
|
+
instance.console?.stop();
|
|
785
|
+
instance.network?.stop();
|
|
786
|
+
instance.formErrors?.stop();
|
|
787
|
+
_setFormErrorCollector(null);
|
|
788
|
+
instance.frustration?.stop();
|
|
789
|
+
instance.stopReplay?.();
|
|
790
|
+
instance = null;
|
|
791
|
+
}
|
|
792
|
+
function isInitialized() {
|
|
793
|
+
return instance !== null;
|
|
794
|
+
}
|
|
795
|
+
function getInstance() {
|
|
796
|
+
return instance;
|
|
797
|
+
}
|
|
798
|
+
function getMeta(extraMeta) {
|
|
799
|
+
return {
|
|
800
|
+
...extraMeta,
|
|
801
|
+
environment: collectEnvironment(),
|
|
802
|
+
consoleLogs: instance?.console?.getEntries() ?? [],
|
|
803
|
+
networkErrors: instance?.network?.getEntries() ?? [],
|
|
804
|
+
formErrors: instance?.formErrors?.getEntries() ?? []
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function getReplayEvents() {
|
|
808
|
+
return instance ? [...instance.replayEvents] : [];
|
|
809
|
+
}
|
|
810
|
+
function getConfig() {
|
|
811
|
+
return instance?.config ?? null;
|
|
812
|
+
}
|
|
813
|
+
var Flint = {
|
|
814
|
+
init,
|
|
815
|
+
shutdown,
|
|
816
|
+
isInitialized,
|
|
817
|
+
getInstance,
|
|
818
|
+
getMeta,
|
|
819
|
+
getReplayEvents,
|
|
820
|
+
getConfig,
|
|
821
|
+
setUser: flint.setUser,
|
|
822
|
+
setSessionReplay: flint.setSessionReplay,
|
|
823
|
+
reportFormError: flint.reportFormError
|
|
441
824
|
};
|
|
442
825
|
|
|
443
826
|
// src/theme.ts
|
|
@@ -480,8 +863,11 @@ function resolveTheme(theme) {
|
|
|
480
863
|
}
|
|
481
864
|
// Annotate the CommonJS export names for ESM import in node:
|
|
482
865
|
0 && (module.exports = {
|
|
866
|
+
Flint,
|
|
867
|
+
_setFormErrorCollector,
|
|
483
868
|
collectEnvironment,
|
|
484
869
|
createConsoleCollector,
|
|
870
|
+
createFormErrorCollector,
|
|
485
871
|
createFrustrationCollector,
|
|
486
872
|
createNetworkCollector,
|
|
487
873
|
flint,
|