@jarve/bug-reporter 0.1.0 → 0.1.1
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/README.md +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +191 -96
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +192 -97
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
54
54
|
<html>
|
|
55
55
|
<body>
|
|
56
56
|
<JarveBugReporter
|
|
57
|
-
apiUrl="https://jarve.com.au/api/bug-reporter/external"
|
|
57
|
+
apiUrl="https://www.jarve.com.au/api/bug-reporter/external"
|
|
58
58
|
apiKey="brk_your_api_key_here"
|
|
59
59
|
user={{ name: 'James', email: 'james@example.com' }}
|
|
60
60
|
>
|
package/dist/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ interface BugReporterApiConfig {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
interface JarveBugReporterProps {
|
|
13
|
-
/** Base URL for the external bug reporter API (e.g. "https://jarve.com.au/api/bug-reporter/external") */
|
|
13
|
+
/** Base URL for the external bug reporter API (e.g. "https://www.jarve.com.au/api/bug-reporter/external") */
|
|
14
14
|
apiUrl: string;
|
|
15
15
|
/** API key for your site (starts with "brk_") */
|
|
16
16
|
apiKey: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ interface BugReporterApiConfig {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
interface JarveBugReporterProps {
|
|
13
|
-
/** Base URL for the external bug reporter API (e.g. "https://jarve.com.au/api/bug-reporter/external") */
|
|
13
|
+
/** Base URL for the external bug reporter API (e.g. "https://www.jarve.com.au/api/bug-reporter/external") */
|
|
14
14
|
apiUrl: string;
|
|
15
15
|
/** API key for your site (starts with "brk_") */
|
|
16
16
|
apiKey: string;
|
package/dist/index.js
CHANGED
|
@@ -265,14 +265,51 @@ function clearCapturedErrors() {
|
|
|
265
265
|
|
|
266
266
|
// src/network-capture.ts
|
|
267
267
|
var MAX_REQUESTS = 30;
|
|
268
|
+
var MAX_BODY_READ_BYTES = 64 * 1024;
|
|
269
|
+
var TRUNCATE_LEN = 500;
|
|
268
270
|
var capturedRequests = [];
|
|
269
271
|
var isCapturing2 = false;
|
|
270
272
|
var originalFetch = null;
|
|
271
|
-
function truncateBody(body, maxLen =
|
|
273
|
+
function truncateBody(body, maxLen = TRUNCATE_LEN) {
|
|
272
274
|
if (!body) return null;
|
|
273
275
|
if (body.length <= maxLen) return body;
|
|
274
276
|
return body.slice(0, maxLen) + "...(truncated)";
|
|
275
277
|
}
|
|
278
|
+
async function readBoundedBody(response) {
|
|
279
|
+
try {
|
|
280
|
+
const contentLength = response.headers.get("Content-Length");
|
|
281
|
+
if (contentLength) {
|
|
282
|
+
const size = parseInt(contentLength, 10);
|
|
283
|
+
if (!isNaN(size) && size > MAX_BODY_READ_BYTES) {
|
|
284
|
+
return `[body too large: ${size} bytes]`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const cloned = response.clone();
|
|
288
|
+
if (cloned.body && typeof cloned.body.getReader === "function") {
|
|
289
|
+
const reader = cloned.body.getReader();
|
|
290
|
+
const chunks = [];
|
|
291
|
+
let totalBytes = 0;
|
|
292
|
+
while (true) {
|
|
293
|
+
const { done, value } = await reader.read();
|
|
294
|
+
if (done) break;
|
|
295
|
+
totalBytes += value.byteLength;
|
|
296
|
+
if (totalBytes > MAX_BODY_READ_BYTES) {
|
|
297
|
+
reader.cancel().catch(() => {
|
|
298
|
+
});
|
|
299
|
+
return `[body too large: >${MAX_BODY_READ_BYTES} bytes]`;
|
|
300
|
+
}
|
|
301
|
+
chunks.push(value);
|
|
302
|
+
}
|
|
303
|
+
const decoder = new TextDecoder();
|
|
304
|
+
const text2 = chunks.map((c) => decoder.decode(c, { stream: true })).join("");
|
|
305
|
+
return truncateBody(text2);
|
|
306
|
+
}
|
|
307
|
+
const text = await cloned.text();
|
|
308
|
+
return truncateBody(text);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
276
313
|
function startNetworkCapture() {
|
|
277
314
|
if (isCapturing2 || typeof window === "undefined") return;
|
|
278
315
|
if (window.fetch.__bugReporterPatched) return;
|
|
@@ -288,13 +325,7 @@ function startNetworkCapture() {
|
|
|
288
325
|
try {
|
|
289
326
|
const response = await originalFetch.call(window, input, init);
|
|
290
327
|
if (response.status >= 400) {
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const cloned = response.clone();
|
|
294
|
-
const text = await cloned.text();
|
|
295
|
-
responseBody = truncateBody(text);
|
|
296
|
-
} catch (e) {
|
|
297
|
-
}
|
|
328
|
+
const responseBody = await readBoundedBody(response);
|
|
298
329
|
capturedRequests.push({
|
|
299
330
|
url,
|
|
300
331
|
method: method.toUpperCase(),
|
|
@@ -385,7 +416,7 @@ function CaptureOverlay({
|
|
|
385
416
|
);
|
|
386
417
|
const handleClick = (0, import_react.useCallback)(
|
|
387
418
|
async (e) => {
|
|
388
|
-
var _a;
|
|
419
|
+
var _a, _b;
|
|
389
420
|
if (!isActive || isCapturing3) return;
|
|
390
421
|
const target = e.target;
|
|
391
422
|
if (!(target instanceof HTMLElement)) return;
|
|
@@ -426,22 +457,48 @@ function CaptureOverlay({
|
|
|
426
457
|
const networkErrors = getCapturedNetworkErrors();
|
|
427
458
|
onCapture({ screenshot: blob, metadata, consoleErrors, networkErrors });
|
|
428
459
|
} catch (err) {
|
|
429
|
-
console.
|
|
430
|
-
|
|
431
|
-
section,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
460
|
+
console.warn("Bug reporter: first capture attempt failed, retrying with simpler settings", err);
|
|
461
|
+
try {
|
|
462
|
+
const dataUrl = await (0, import_html_to_image.toPng)(section, {
|
|
463
|
+
quality: 0.6,
|
|
464
|
+
pixelRatio: 1,
|
|
465
|
+
skipFonts: true,
|
|
466
|
+
cacheBust: true
|
|
467
|
+
});
|
|
468
|
+
const [header, base64] = dataUrl.split(",");
|
|
469
|
+
const mime = ((_b = header.match(/:(.*?);/)) == null ? void 0 : _b[1]) || "image/png";
|
|
470
|
+
const bytes = atob(base64);
|
|
471
|
+
const arr = new Uint8Array(bytes.length);
|
|
472
|
+
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
|
|
473
|
+
const retryBlob = new Blob([arr], { type: mime });
|
|
474
|
+
const metadata = collectMetadata(
|
|
475
|
+
section,
|
|
476
|
+
siteId,
|
|
477
|
+
reporterName,
|
|
478
|
+
reporterEmail,
|
|
479
|
+
elementInfo
|
|
480
|
+
);
|
|
481
|
+
const consoleErrors = getCapturedErrors();
|
|
482
|
+
const networkErrors = getCapturedNetworkErrors();
|
|
483
|
+
onCapture({ screenshot: retryBlob, metadata, consoleErrors, networkErrors });
|
|
484
|
+
} catch (e2) {
|
|
485
|
+
console.error("Bug reporter: screenshot capture failed after retry");
|
|
486
|
+
const metadata = collectMetadata(
|
|
487
|
+
section,
|
|
488
|
+
siteId,
|
|
489
|
+
reporterName,
|
|
490
|
+
reporterEmail,
|
|
491
|
+
elementInfo
|
|
492
|
+
);
|
|
493
|
+
const consoleErrors = getCapturedErrors();
|
|
494
|
+
const networkErrors = getCapturedNetworkErrors();
|
|
495
|
+
onCapture({
|
|
496
|
+
screenshot: new Blob(),
|
|
497
|
+
metadata: __spreadProps(__spreadValues({}, metadata), { screenshotCaptureFailed: true }),
|
|
498
|
+
consoleErrors,
|
|
499
|
+
networkErrors
|
|
500
|
+
});
|
|
501
|
+
}
|
|
445
502
|
} finally {
|
|
446
503
|
setIsCapturing(false);
|
|
447
504
|
}
|
|
@@ -538,34 +595,31 @@ function ReportModal({
|
|
|
538
595
|
const chatEndRef = (0, import_react2.useRef)(null);
|
|
539
596
|
const inputRef = (0, import_react2.useRef)(null);
|
|
540
597
|
const hasInitRef = (0, import_react2.useRef)(false);
|
|
541
|
-
const apiHeaders =
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
598
|
+
const apiHeaders = (0, import_react2.useMemo)(
|
|
599
|
+
() => ({
|
|
600
|
+
"Content-Type": "application/json",
|
|
601
|
+
"X-Bug-Reporter-Key": apiConfig.apiKey
|
|
602
|
+
}),
|
|
603
|
+
[apiConfig.apiKey]
|
|
604
|
+
);
|
|
545
605
|
(0, import_react2.useEffect)(() => {
|
|
546
606
|
if ((captureResult == null ? void 0 : captureResult.screenshot) && captureResult.screenshot.size > 0) {
|
|
547
607
|
const url = URL.createObjectURL(captureResult.screenshot);
|
|
548
|
-
setScreenshotUrl(
|
|
549
|
-
|
|
608
|
+
setScreenshotUrl((prev) => {
|
|
609
|
+
if (prev) URL.revokeObjectURL(prev);
|
|
610
|
+
return url;
|
|
611
|
+
});
|
|
612
|
+
return () => {
|
|
613
|
+
URL.revokeObjectURL(url);
|
|
614
|
+
setScreenshotUrl(null);
|
|
615
|
+
};
|
|
550
616
|
}
|
|
617
|
+
setScreenshotUrl((prev) => {
|
|
618
|
+
if (prev) URL.revokeObjectURL(prev);
|
|
619
|
+
return null;
|
|
620
|
+
});
|
|
551
621
|
}, [captureResult]);
|
|
552
|
-
(0, import_react2.
|
|
553
|
-
if (isOpen && captureResult && !hasInitRef.current) {
|
|
554
|
-
hasInitRef.current = true;
|
|
555
|
-
sendInitialMessage();
|
|
556
|
-
}
|
|
557
|
-
}, [isOpen, captureResult]);
|
|
558
|
-
(0, import_react2.useEffect)(() => {
|
|
559
|
-
var _a;
|
|
560
|
-
(_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
561
|
-
}, [messages]);
|
|
562
|
-
(0, import_react2.useEffect)(() => {
|
|
563
|
-
var _a;
|
|
564
|
-
if (isOpen && !isLoading) {
|
|
565
|
-
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
566
|
-
}
|
|
567
|
-
}, [isOpen, isLoading, messages]);
|
|
568
|
-
async function sendInitialMessage() {
|
|
622
|
+
const sendInitialMessage = (0, import_react2.useCallback)(async () => {
|
|
569
623
|
if (!captureResult) return;
|
|
570
624
|
setIsLoading(true);
|
|
571
625
|
try {
|
|
@@ -580,6 +634,16 @@ function ReportModal({
|
|
|
580
634
|
clickedElement: captureResult.metadata.clickedElement || null
|
|
581
635
|
})
|
|
582
636
|
});
|
|
637
|
+
if (response.status === 401) {
|
|
638
|
+
console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
|
|
639
|
+
setMessages([
|
|
640
|
+
{
|
|
641
|
+
role: "assistant",
|
|
642
|
+
content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
|
|
643
|
+
}
|
|
644
|
+
]);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
583
647
|
if (!response.ok) throw new Error("Failed to get AI response");
|
|
584
648
|
const data = await response.json();
|
|
585
649
|
setMessages([{ role: "assistant", content: data.message }]);
|
|
@@ -593,53 +657,23 @@ function ReportModal({
|
|
|
593
657
|
} finally {
|
|
594
658
|
setIsLoading(false);
|
|
595
659
|
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const newMessages = [
|
|
602
|
-
...messages,
|
|
603
|
-
{ role: "user", content: userMessage }
|
|
604
|
-
];
|
|
605
|
-
setMessages(newMessages);
|
|
606
|
-
setIsLoading(true);
|
|
607
|
-
try {
|
|
608
|
-
const response = await fetch(`${apiConfig.apiUrl}/chat`, {
|
|
609
|
-
method: "POST",
|
|
610
|
-
headers: apiHeaders,
|
|
611
|
-
body: JSON.stringify({
|
|
612
|
-
messages: newMessages,
|
|
613
|
-
metadata: captureResult.metadata,
|
|
614
|
-
consoleErrors: captureResult.consoleErrors,
|
|
615
|
-
networkErrors: captureResult.networkErrors,
|
|
616
|
-
clickedElement: captureResult.metadata.clickedElement || null
|
|
617
|
-
})
|
|
618
|
-
});
|
|
619
|
-
if (!response.ok) throw new Error("Failed to get AI response");
|
|
620
|
-
const data = await response.json();
|
|
621
|
-
setMessages([
|
|
622
|
-
...newMessages,
|
|
623
|
-
{ role: "assistant", content: data.message }
|
|
624
|
-
]);
|
|
625
|
-
if (data.readyToSubmit && data.structuredReport) {
|
|
626
|
-
await submitReport(
|
|
627
|
-
[...newMessages, { role: "assistant", content: data.message }],
|
|
628
|
-
data.structuredReport
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
} catch (e) {
|
|
632
|
-
setMessages([
|
|
633
|
-
...newMessages,
|
|
634
|
-
{
|
|
635
|
-
role: "assistant",
|
|
636
|
-
content: "I had trouble processing that. Could you try describing the issue again?"
|
|
637
|
-
}
|
|
638
|
-
]);
|
|
639
|
-
} finally {
|
|
640
|
-
setIsLoading(false);
|
|
660
|
+
}, [captureResult, apiConfig.apiUrl, apiHeaders]);
|
|
661
|
+
(0, import_react2.useEffect)(() => {
|
|
662
|
+
if (isOpen && captureResult && !hasInitRef.current) {
|
|
663
|
+
hasInitRef.current = true;
|
|
664
|
+
sendInitialMessage();
|
|
641
665
|
}
|
|
642
|
-
}
|
|
666
|
+
}, [isOpen, captureResult, sendInitialMessage]);
|
|
667
|
+
(0, import_react2.useEffect)(() => {
|
|
668
|
+
var _a;
|
|
669
|
+
(_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
670
|
+
}, [messages]);
|
|
671
|
+
(0, import_react2.useEffect)(() => {
|
|
672
|
+
var _a;
|
|
673
|
+
if (isOpen && !isLoading) {
|
|
674
|
+
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
675
|
+
}
|
|
676
|
+
}, [isOpen, isLoading, messages]);
|
|
643
677
|
const submitReport = (0, import_react2.useCallback)(
|
|
644
678
|
async (conversation, structuredReport) => {
|
|
645
679
|
if (!captureResult || modalState !== "chatting") return;
|
|
@@ -704,10 +738,67 @@ function ReportModal({
|
|
|
704
738
|
setModalState("error");
|
|
705
739
|
}
|
|
706
740
|
},
|
|
707
|
-
[captureResult, apiConfig, apiHeaders, modalState]
|
|
741
|
+
[captureResult, apiConfig.apiUrl, apiHeaders, modalState]
|
|
708
742
|
);
|
|
709
|
-
|
|
743
|
+
const handleManualSubmit = (0, import_react2.useCallback)(() => {
|
|
710
744
|
submitReport(messages);
|
|
745
|
+
}, [submitReport, messages]);
|
|
746
|
+
async function sendMessage() {
|
|
747
|
+
if (!input.trim() || isLoading || !captureResult) return;
|
|
748
|
+
const userMessage = input.trim();
|
|
749
|
+
setInput("");
|
|
750
|
+
const newMessages = [
|
|
751
|
+
...messages,
|
|
752
|
+
{ role: "user", content: userMessage }
|
|
753
|
+
];
|
|
754
|
+
setMessages(newMessages);
|
|
755
|
+
setIsLoading(true);
|
|
756
|
+
try {
|
|
757
|
+
const response = await fetch(`${apiConfig.apiUrl}/chat`, {
|
|
758
|
+
method: "POST",
|
|
759
|
+
headers: apiHeaders,
|
|
760
|
+
body: JSON.stringify({
|
|
761
|
+
messages: newMessages,
|
|
762
|
+
metadata: captureResult.metadata,
|
|
763
|
+
consoleErrors: captureResult.consoleErrors,
|
|
764
|
+
networkErrors: captureResult.networkErrors,
|
|
765
|
+
clickedElement: captureResult.metadata.clickedElement || null
|
|
766
|
+
})
|
|
767
|
+
});
|
|
768
|
+
if (response.status === 401) {
|
|
769
|
+
console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
|
|
770
|
+
setMessages([
|
|
771
|
+
...newMessages,
|
|
772
|
+
{
|
|
773
|
+
role: "assistant",
|
|
774
|
+
content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
|
|
775
|
+
}
|
|
776
|
+
]);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (!response.ok) throw new Error("Failed to get AI response");
|
|
780
|
+
const data = await response.json();
|
|
781
|
+
setMessages([
|
|
782
|
+
...newMessages,
|
|
783
|
+
{ role: "assistant", content: data.message }
|
|
784
|
+
]);
|
|
785
|
+
if (data.readyToSubmit && data.structuredReport) {
|
|
786
|
+
await submitReport(
|
|
787
|
+
[...newMessages, { role: "assistant", content: data.message }],
|
|
788
|
+
data.structuredReport
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
} catch (e) {
|
|
792
|
+
setMessages([
|
|
793
|
+
...newMessages,
|
|
794
|
+
{
|
|
795
|
+
role: "assistant",
|
|
796
|
+
content: "I had trouble processing that. Could you try describing the issue again?"
|
|
797
|
+
}
|
|
798
|
+
]);
|
|
799
|
+
} finally {
|
|
800
|
+
setIsLoading(false);
|
|
801
|
+
}
|
|
711
802
|
}
|
|
712
803
|
function handleClose() {
|
|
713
804
|
setMessages([]);
|
|
@@ -715,6 +806,10 @@ function ReportModal({
|
|
|
715
806
|
setModalState("chatting");
|
|
716
807
|
setReportId(null);
|
|
717
808
|
setErrorMessage(null);
|
|
809
|
+
if (screenshotUrl) {
|
|
810
|
+
URL.revokeObjectURL(screenshotUrl);
|
|
811
|
+
}
|
|
812
|
+
setScreenshotUrl(null);
|
|
718
813
|
hasInitRef.current = false;
|
|
719
814
|
onClose();
|
|
720
815
|
}
|