@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 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 = 500) {
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
- let responseBody = null;
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.error("Bug reporter: failed to capture screenshot", err);
430
- const metadata = collectMetadata(
431
- section,
432
- siteId,
433
- reporterName,
434
- reporterEmail,
435
- elementInfo
436
- );
437
- const consoleErrors = getCapturedErrors();
438
- const networkErrors = getCapturedNetworkErrors();
439
- onCapture({
440
- screenshot: new Blob(),
441
- metadata: __spreadProps(__spreadValues({}, metadata), { screenshotCaptureFailed: true }),
442
- consoleErrors,
443
- networkErrors
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
- "Content-Type": "application/json",
543
- "X-Bug-Reporter-Key": apiConfig.apiKey
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(url);
549
- return () => URL.revokeObjectURL(url);
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.useEffect)(() => {
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
- async function sendMessage() {
598
- if (!input.trim() || isLoading || !captureResult) return;
599
- const userMessage = input.trim();
600
- setInput("");
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
- function handleManualSubmit() {
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
  }