@jarve/bug-reporter 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.mts CHANGED
@@ -7,6 +7,15 @@ interface BugReporterUser {
7
7
  name: string;
8
8
  email: string;
9
9
  }
10
+ type BugReporterClientContextValue = string | number | boolean | null | BugReporterClientContextValue[] | {
11
+ [key: string]: BugReporterClientContextValue;
12
+ };
13
+ type BugReporterClientContext = Record<string, BugReporterClientContextValue>;
14
+ interface BugReporterAccount {
15
+ id?: string;
16
+ name?: string;
17
+ plan?: string;
18
+ }
10
19
  interface ClickedElement {
11
20
  tagName: string;
12
21
  textContent: string | null;
@@ -38,6 +47,11 @@ interface CaptureMetadata {
38
47
  * derived from the secret API key.
39
48
  */
40
49
  siteId: string;
50
+ widgetVersion?: string;
51
+ appVersion?: string;
52
+ appEnvironment?: string;
53
+ release?: string;
54
+ clientContext?: BugReporterClientContext;
41
55
  screenshotCaptureFailed?: boolean;
42
56
  clickedElement?: ClickedElement;
43
57
  }
@@ -144,6 +158,18 @@ interface JarveBugReporterProps {
144
158
  * See Issue E11.
145
159
  */
146
160
  theme?: JarveTheme;
161
+ /** Optional host app version/release label to include in submitted reports. */
162
+ appVersion?: string;
163
+ /** Optional host environment label, e.g. "production", "preview", or "staging". */
164
+ environment?: string;
165
+ /** Optional deploy/release identifier, such as a git SHA or Vercel deployment URL. */
166
+ release?: string;
167
+ /** Optional account/tenant labels supplied by the host app. Values are redacted and size-capped. */
168
+ account?: BugReporterAccount;
169
+ /** Optional small tags supplied by the host app. Values are redacted and size-capped. */
170
+ tags?: string[];
171
+ /** Optional host debugging context. Values are redacted, depth-limited, and size-capped. */
172
+ context?: Record<string, unknown>;
147
173
  /**
148
174
  * Final chance to scrub the bug-report payload before it leaves the
149
175
  * browser. The hook receives the full `/submit` payload and must return
@@ -155,6 +181,6 @@ interface JarveBugReporterProps {
155
181
  onBeforeSubmit?: (payload: BugReportSubmitPayload) => BugReportSubmitPayload | null | false | Promise<BugReportSubmitPayload | null | false>;
156
182
  children: React.ReactNode;
157
183
  }
158
- declare function JarveBugReporter({ apiUrl, apiKey, siteId, user, buttonPosition, zIndexBase, captureResponseBodies, theme, onBeforeSubmit, children, }: JarveBugReporterProps): react_jsx_runtime.JSX.Element;
184
+ declare function JarveBugReporter({ apiUrl, apiKey, siteId, user, buttonPosition, zIndexBase, captureResponseBodies, theme, appVersion, environment, release, account, tags, context, onBeforeSubmit, children, }: JarveBugReporterProps): react_jsx_runtime.JSX.Element;
159
185
 
160
- export { type BugReportSubmitPayload, type BugReporterApiConfig, type BugReporterUser, type FloatingButtonPosition, JarveBugReporter, type JarveBugReporterProps };
186
+ export { type BugReportSubmitPayload, type BugReporterAccount, type BugReporterApiConfig, type BugReporterClientContext, type BugReporterUser, type FloatingButtonPosition, JarveBugReporter, type JarveBugReporterProps };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,15 @@ interface BugReporterUser {
7
7
  name: string;
8
8
  email: string;
9
9
  }
10
+ type BugReporterClientContextValue = string | number | boolean | null | BugReporterClientContextValue[] | {
11
+ [key: string]: BugReporterClientContextValue;
12
+ };
13
+ type BugReporterClientContext = Record<string, BugReporterClientContextValue>;
14
+ interface BugReporterAccount {
15
+ id?: string;
16
+ name?: string;
17
+ plan?: string;
18
+ }
10
19
  interface ClickedElement {
11
20
  tagName: string;
12
21
  textContent: string | null;
@@ -38,6 +47,11 @@ interface CaptureMetadata {
38
47
  * derived from the secret API key.
39
48
  */
40
49
  siteId: string;
50
+ widgetVersion?: string;
51
+ appVersion?: string;
52
+ appEnvironment?: string;
53
+ release?: string;
54
+ clientContext?: BugReporterClientContext;
41
55
  screenshotCaptureFailed?: boolean;
42
56
  clickedElement?: ClickedElement;
43
57
  }
@@ -144,6 +158,18 @@ interface JarveBugReporterProps {
144
158
  * See Issue E11.
145
159
  */
146
160
  theme?: JarveTheme;
161
+ /** Optional host app version/release label to include in submitted reports. */
162
+ appVersion?: string;
163
+ /** Optional host environment label, e.g. "production", "preview", or "staging". */
164
+ environment?: string;
165
+ /** Optional deploy/release identifier, such as a git SHA or Vercel deployment URL. */
166
+ release?: string;
167
+ /** Optional account/tenant labels supplied by the host app. Values are redacted and size-capped. */
168
+ account?: BugReporterAccount;
169
+ /** Optional small tags supplied by the host app. Values are redacted and size-capped. */
170
+ tags?: string[];
171
+ /** Optional host debugging context. Values are redacted, depth-limited, and size-capped. */
172
+ context?: Record<string, unknown>;
147
173
  /**
148
174
  * Final chance to scrub the bug-report payload before it leaves the
149
175
  * browser. The hook receives the full `/submit` payload and must return
@@ -155,6 +181,6 @@ interface JarveBugReporterProps {
155
181
  onBeforeSubmit?: (payload: BugReportSubmitPayload) => BugReportSubmitPayload | null | false | Promise<BugReportSubmitPayload | null | false>;
156
182
  children: React.ReactNode;
157
183
  }
158
- declare function JarveBugReporter({ apiUrl, apiKey, siteId, user, buttonPosition, zIndexBase, captureResponseBodies, theme, onBeforeSubmit, children, }: JarveBugReporterProps): react_jsx_runtime.JSX.Element;
184
+ declare function JarveBugReporter({ apiUrl, apiKey, siteId, user, buttonPosition, zIndexBase, captureResponseBodies, theme, appVersion, environment, release, account, tags, context, onBeforeSubmit, children, }: JarveBugReporterProps): react_jsx_runtime.JSX.Element;
159
185
 
160
- export { type BugReportSubmitPayload, type BugReporterApiConfig, type BugReporterUser, type FloatingButtonPosition, JarveBugReporter, type JarveBugReporterProps };
186
+ export { type BugReportSubmitPayload, type BugReporterAccount, type BugReporterApiConfig, type BugReporterClientContext, type BugReporterUser, type FloatingButtonPosition, JarveBugReporter, type JarveBugReporterProps };
package/dist/index.js CHANGED
@@ -256,7 +256,7 @@ function collectElementInfo(target, section, coords) {
256
256
  relativeClickY: coords.clientY - sectionRect.top
257
257
  };
258
258
  }
259
- function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
259
+ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement, context) {
260
260
  const { browser, os } = parseUserAgent();
261
261
  const now = /* @__PURE__ */ new Date();
262
262
  return {
@@ -270,6 +270,11 @@ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, cl
270
270
  timestamp: now.toISOString(),
271
271
  timestampUtc: now.toUTCString(),
272
272
  siteId,
273
+ widgetVersion: context == null ? void 0 : context.widgetVersion,
274
+ appVersion: context == null ? void 0 : context.appVersion,
275
+ appEnvironment: context == null ? void 0 : context.appEnvironment,
276
+ release: context == null ? void 0 : context.release,
277
+ clientContext: context == null ? void 0 : context.clientContext,
273
278
  reporterName,
274
279
  reporterEmail,
275
280
  clickedElement
@@ -571,6 +576,11 @@ function dataUrlToBlob(dataUrl) {
571
576
  function CaptureOverlay({
572
577
  isActive,
573
578
  siteId,
579
+ widgetVersion,
580
+ appVersion,
581
+ appEnvironment,
582
+ release,
583
+ clientContext,
574
584
  reporterName,
575
585
  reporterEmail,
576
586
  onCapture,
@@ -629,7 +639,14 @@ function CaptureOverlay({
629
639
  siteId,
630
640
  reporterName,
631
641
  reporterEmail,
632
- elementInfo
642
+ elementInfo,
643
+ {
644
+ widgetVersion,
645
+ appVersion,
646
+ appEnvironment,
647
+ release,
648
+ clientContext
649
+ }
633
650
  );
634
651
  const consoleErrors = getCapturedErrors();
635
652
  const networkErrors = getCapturedNetworkErrors();
@@ -652,7 +669,14 @@ function CaptureOverlay({
652
669
  siteId,
653
670
  reporterName,
654
671
  reporterEmail,
655
- elementInfo
672
+ elementInfo,
673
+ {
674
+ widgetVersion,
675
+ appVersion,
676
+ appEnvironment,
677
+ release,
678
+ clientContext
679
+ }
656
680
  );
657
681
  const consoleErrors = getCapturedErrors();
658
682
  const networkErrors = getCapturedNetworkErrors();
@@ -664,7 +688,14 @@ function CaptureOverlay({
664
688
  siteId,
665
689
  reporterName,
666
690
  reporterEmail,
667
- elementInfo
691
+ elementInfo,
692
+ {
693
+ widgetVersion,
694
+ appVersion,
695
+ appEnvironment,
696
+ release,
697
+ clientContext
698
+ }
668
699
  );
669
700
  const consoleErrors = getCapturedErrors();
670
701
  const networkErrors = getCapturedNetworkErrors();
@@ -679,7 +710,17 @@ function CaptureOverlay({
679
710
  setIsCapturing(false);
680
711
  }
681
712
  },
682
- [siteId, reporterName, reporterEmail, onCapture]
713
+ [
714
+ siteId,
715
+ reporterName,
716
+ reporterEmail,
717
+ onCapture,
718
+ widgetVersion,
719
+ appVersion,
720
+ appEnvironment,
721
+ release,
722
+ clientContext
723
+ ]
683
724
  );
684
725
  const handleMouseMove = (0, import_react2.useCallback)(
685
726
  (e) => {
@@ -1417,6 +1458,105 @@ function ReportModal({
1417
1458
 
1418
1459
  // src/bug-reporter.tsx
1419
1460
  var import_widget_shared6 = require("@jarve/widget-shared");
1461
+
1462
+ // src/context.ts
1463
+ var MAX_TOP_LEVEL_KEYS = 24;
1464
+ var MAX_ARRAY_ITEMS = 20;
1465
+ var MAX_DEPTH = 3;
1466
+ var MAX_STRING_LENGTH = 500;
1467
+ var MAX_SERIALIZED_BYTES = 8 * 1024;
1468
+ var SENSITIVE_KEY = /(token|password|secret|api[_-]?key|authorization|cookie|session)/i;
1469
+ function trimString(value) {
1470
+ return redactSensitive(value).slice(0, MAX_STRING_LENGTH);
1471
+ }
1472
+ function normalizeKey(key) {
1473
+ const trimmed = key.trim().slice(0, 80);
1474
+ if (!trimmed) return null;
1475
+ return trimmed;
1476
+ }
1477
+ function normalizeValue(key, value, depth) {
1478
+ if (value === void 0) return void 0;
1479
+ if (value === null) return null;
1480
+ if (SENSITIVE_KEY.test(key)) {
1481
+ return "[REDACTED]";
1482
+ }
1483
+ if (typeof value === "string") return trimString(value);
1484
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1485
+ if (typeof value === "boolean") return value;
1486
+ if (Array.isArray(value)) {
1487
+ if (depth >= MAX_DEPTH) return "[TRUNCATED]";
1488
+ return value.slice(0, MAX_ARRAY_ITEMS).map((item, index) => normalizeValue(`${key}.${index}`, item, depth + 1)).filter((item) => item !== void 0);
1489
+ }
1490
+ if (typeof value === "object") {
1491
+ if (depth >= MAX_DEPTH) return "[TRUNCATED]";
1492
+ const output = {};
1493
+ for (const [childKey, childValue] of Object.entries(value).slice(
1494
+ 0,
1495
+ MAX_TOP_LEVEL_KEYS
1496
+ )) {
1497
+ const normalizedKey = normalizeKey(childKey);
1498
+ if (!normalizedKey) continue;
1499
+ const normalizedValue = normalizeValue(normalizedKey, childValue, depth + 1);
1500
+ if (normalizedValue !== void 0) {
1501
+ output[normalizedKey] = normalizedValue;
1502
+ }
1503
+ }
1504
+ return output;
1505
+ }
1506
+ return trimString(String(value));
1507
+ }
1508
+ function serializedBytes(value) {
1509
+ try {
1510
+ return new TextEncoder().encode(JSON.stringify(value)).length;
1511
+ } catch (e) {
1512
+ return MAX_SERIALIZED_BYTES + 1;
1513
+ }
1514
+ }
1515
+ function trimToBudget(value) {
1516
+ const output = {};
1517
+ for (const [key, entry] of Object.entries(value)) {
1518
+ output[key] = entry;
1519
+ if (serializedBytes(output) > MAX_SERIALIZED_BYTES) {
1520
+ delete output[key];
1521
+ output._truncated = true;
1522
+ break;
1523
+ }
1524
+ }
1525
+ return output;
1526
+ }
1527
+ function normalizeClientContext({
1528
+ account,
1529
+ tags,
1530
+ context
1531
+ }) {
1532
+ const output = {};
1533
+ if (account) {
1534
+ const normalizedAccount = normalizeValue("account", account, 0);
1535
+ if (normalizedAccount !== void 0) {
1536
+ output.account = normalizedAccount;
1537
+ }
1538
+ }
1539
+ if (tags && tags.length > 0) {
1540
+ output.tags = tags.slice(0, MAX_ARRAY_ITEMS).map((tag) => trimString(tag));
1541
+ }
1542
+ if (context) {
1543
+ for (const [key, value] of Object.entries(context).slice(0, MAX_TOP_LEVEL_KEYS)) {
1544
+ const normalizedKey = normalizeKey(key);
1545
+ if (!normalizedKey) continue;
1546
+ const normalizedValue = normalizeValue(normalizedKey, value, 0);
1547
+ if (normalizedValue !== void 0) {
1548
+ output[normalizedKey] = normalizedValue;
1549
+ }
1550
+ }
1551
+ }
1552
+ if (Object.keys(output).length === 0) return void 0;
1553
+ return trimToBudget(output);
1554
+ }
1555
+
1556
+ // src/version.ts
1557
+ var BUG_REPORTER_WIDGET_VERSION = "1.0.2";
1558
+
1559
+ // src/bug-reporter.tsx
1420
1560
  var import_jsx_runtime4 = require("react/jsx-runtime");
1421
1561
  function isValidApiUrl(raw) {
1422
1562
  if (typeof raw !== "string" || raw.length === 0) return false;
@@ -1438,11 +1578,21 @@ function JarveBugReporter({
1438
1578
  zIndexBase,
1439
1579
  captureResponseBodies = false,
1440
1580
  theme,
1581
+ appVersion,
1582
+ environment,
1583
+ release,
1584
+ account,
1585
+ tags,
1586
+ context,
1441
1587
  onBeforeSubmit,
1442
1588
  children
1443
1589
  }) {
1444
1590
  const mounted = (0, import_widget_shared6.useHasMounted)();
1445
1591
  const apiUrlValid = (0, import_react4.useMemo)(() => isValidApiUrl(apiUrl), [apiUrl]);
1592
+ const clientContext = (0, import_react4.useMemo)(
1593
+ () => normalizeClientContext({ account, tags, context }),
1594
+ [account, tags, context]
1595
+ );
1446
1596
  (0, import_react4.useEffect)(() => {
1447
1597
  if (!apiUrlValid) {
1448
1598
  console.error(
@@ -1463,6 +1613,31 @@ function JarveBugReporter({
1463
1613
  stopNetworkCapture();
1464
1614
  };
1465
1615
  }, [apiUrlValid, isPrimary, captureResponseBodies]);
1616
+ (0, import_react4.useEffect)(() => {
1617
+ if (!apiUrlValid || !isPrimary) return;
1618
+ const controller = new AbortController();
1619
+ const timeout = window.setTimeout(() => controller.abort(), 8e3);
1620
+ void fetch(`${apiUrl.replace(/\/+$/, "")}/heartbeat`, {
1621
+ method: "POST",
1622
+ headers: {
1623
+ "Content-Type": "application/json",
1624
+ "X-Bug-Reporter-Key": apiKey.trim()
1625
+ },
1626
+ body: JSON.stringify({
1627
+ siteId,
1628
+ widgetVersion: BUG_REPORTER_WIDGET_VERSION,
1629
+ appVersion,
1630
+ environment,
1631
+ release
1632
+ }),
1633
+ signal: controller.signal
1634
+ }).catch(() => {
1635
+ });
1636
+ return () => {
1637
+ window.clearTimeout(timeout);
1638
+ controller.abort();
1639
+ };
1640
+ }, [apiUrl, apiKey, apiUrlValid, appVersion, environment, isPrimary, release, siteId]);
1466
1641
  const toggleCaptureMode = (0, import_react4.useCallback)(() => {
1467
1642
  setCaptureMode((prev) => !prev);
1468
1643
  }, []);
@@ -1549,6 +1724,11 @@ function JarveBugReporter({
1549
1724
  {
1550
1725
  isActive: captureMode,
1551
1726
  siteId,
1727
+ widgetVersion: BUG_REPORTER_WIDGET_VERSION,
1728
+ appVersion,
1729
+ appEnvironment: environment,
1730
+ release,
1731
+ clientContext,
1552
1732
  reporterName,
1553
1733
  reporterEmail,
1554
1734
  onCapture: handleCapture,
package/dist/index.mjs CHANGED
@@ -224,7 +224,7 @@ function collectElementInfo(target, section, coords) {
224
224
  relativeClickY: coords.clientY - sectionRect.top
225
225
  };
226
226
  }
227
- function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
227
+ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement, context) {
228
228
  const { browser, os } = parseUserAgent();
229
229
  const now = /* @__PURE__ */ new Date();
230
230
  return {
@@ -238,6 +238,11 @@ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, cl
238
238
  timestamp: now.toISOString(),
239
239
  timestampUtc: now.toUTCString(),
240
240
  siteId,
241
+ widgetVersion: context == null ? void 0 : context.widgetVersion,
242
+ appVersion: context == null ? void 0 : context.appVersion,
243
+ appEnvironment: context == null ? void 0 : context.appEnvironment,
244
+ release: context == null ? void 0 : context.release,
245
+ clientContext: context == null ? void 0 : context.clientContext,
241
246
  reporterName,
242
247
  reporterEmail,
243
248
  clickedElement
@@ -539,6 +544,11 @@ function dataUrlToBlob(dataUrl) {
539
544
  function CaptureOverlay({
540
545
  isActive,
541
546
  siteId,
547
+ widgetVersion,
548
+ appVersion,
549
+ appEnvironment,
550
+ release,
551
+ clientContext,
542
552
  reporterName,
543
553
  reporterEmail,
544
554
  onCapture,
@@ -597,7 +607,14 @@ function CaptureOverlay({
597
607
  siteId,
598
608
  reporterName,
599
609
  reporterEmail,
600
- elementInfo
610
+ elementInfo,
611
+ {
612
+ widgetVersion,
613
+ appVersion,
614
+ appEnvironment,
615
+ release,
616
+ clientContext
617
+ }
601
618
  );
602
619
  const consoleErrors = getCapturedErrors();
603
620
  const networkErrors = getCapturedNetworkErrors();
@@ -620,7 +637,14 @@ function CaptureOverlay({
620
637
  siteId,
621
638
  reporterName,
622
639
  reporterEmail,
623
- elementInfo
640
+ elementInfo,
641
+ {
642
+ widgetVersion,
643
+ appVersion,
644
+ appEnvironment,
645
+ release,
646
+ clientContext
647
+ }
624
648
  );
625
649
  const consoleErrors = getCapturedErrors();
626
650
  const networkErrors = getCapturedNetworkErrors();
@@ -632,7 +656,14 @@ function CaptureOverlay({
632
656
  siteId,
633
657
  reporterName,
634
658
  reporterEmail,
635
- elementInfo
659
+ elementInfo,
660
+ {
661
+ widgetVersion,
662
+ appVersion,
663
+ appEnvironment,
664
+ release,
665
+ clientContext
666
+ }
636
667
  );
637
668
  const consoleErrors = getCapturedErrors();
638
669
  const networkErrors = getCapturedNetworkErrors();
@@ -647,7 +678,17 @@ function CaptureOverlay({
647
678
  setIsCapturing(false);
648
679
  }
649
680
  },
650
- [siteId, reporterName, reporterEmail, onCapture]
681
+ [
682
+ siteId,
683
+ reporterName,
684
+ reporterEmail,
685
+ onCapture,
686
+ widgetVersion,
687
+ appVersion,
688
+ appEnvironment,
689
+ release,
690
+ clientContext
691
+ ]
651
692
  );
652
693
  const handleMouseMove = useCallback(
653
694
  (e) => {
@@ -1392,6 +1433,105 @@ function ReportModal({
1392
1433
 
1393
1434
  // src/bug-reporter.tsx
1394
1435
  import { emit, useHasMounted } from "@jarve/widget-shared";
1436
+
1437
+ // src/context.ts
1438
+ var MAX_TOP_LEVEL_KEYS = 24;
1439
+ var MAX_ARRAY_ITEMS = 20;
1440
+ var MAX_DEPTH = 3;
1441
+ var MAX_STRING_LENGTH = 500;
1442
+ var MAX_SERIALIZED_BYTES = 8 * 1024;
1443
+ var SENSITIVE_KEY = /(token|password|secret|api[_-]?key|authorization|cookie|session)/i;
1444
+ function trimString(value) {
1445
+ return redactSensitive(value).slice(0, MAX_STRING_LENGTH);
1446
+ }
1447
+ function normalizeKey(key) {
1448
+ const trimmed = key.trim().slice(0, 80);
1449
+ if (!trimmed) return null;
1450
+ return trimmed;
1451
+ }
1452
+ function normalizeValue(key, value, depth) {
1453
+ if (value === void 0) return void 0;
1454
+ if (value === null) return null;
1455
+ if (SENSITIVE_KEY.test(key)) {
1456
+ return "[REDACTED]";
1457
+ }
1458
+ if (typeof value === "string") return trimString(value);
1459
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1460
+ if (typeof value === "boolean") return value;
1461
+ if (Array.isArray(value)) {
1462
+ if (depth >= MAX_DEPTH) return "[TRUNCATED]";
1463
+ return value.slice(0, MAX_ARRAY_ITEMS).map((item, index) => normalizeValue(`${key}.${index}`, item, depth + 1)).filter((item) => item !== void 0);
1464
+ }
1465
+ if (typeof value === "object") {
1466
+ if (depth >= MAX_DEPTH) return "[TRUNCATED]";
1467
+ const output = {};
1468
+ for (const [childKey, childValue] of Object.entries(value).slice(
1469
+ 0,
1470
+ MAX_TOP_LEVEL_KEYS
1471
+ )) {
1472
+ const normalizedKey = normalizeKey(childKey);
1473
+ if (!normalizedKey) continue;
1474
+ const normalizedValue = normalizeValue(normalizedKey, childValue, depth + 1);
1475
+ if (normalizedValue !== void 0) {
1476
+ output[normalizedKey] = normalizedValue;
1477
+ }
1478
+ }
1479
+ return output;
1480
+ }
1481
+ return trimString(String(value));
1482
+ }
1483
+ function serializedBytes(value) {
1484
+ try {
1485
+ return new TextEncoder().encode(JSON.stringify(value)).length;
1486
+ } catch (e) {
1487
+ return MAX_SERIALIZED_BYTES + 1;
1488
+ }
1489
+ }
1490
+ function trimToBudget(value) {
1491
+ const output = {};
1492
+ for (const [key, entry] of Object.entries(value)) {
1493
+ output[key] = entry;
1494
+ if (serializedBytes(output) > MAX_SERIALIZED_BYTES) {
1495
+ delete output[key];
1496
+ output._truncated = true;
1497
+ break;
1498
+ }
1499
+ }
1500
+ return output;
1501
+ }
1502
+ function normalizeClientContext({
1503
+ account,
1504
+ tags,
1505
+ context
1506
+ }) {
1507
+ const output = {};
1508
+ if (account) {
1509
+ const normalizedAccount = normalizeValue("account", account, 0);
1510
+ if (normalizedAccount !== void 0) {
1511
+ output.account = normalizedAccount;
1512
+ }
1513
+ }
1514
+ if (tags && tags.length > 0) {
1515
+ output.tags = tags.slice(0, MAX_ARRAY_ITEMS).map((tag) => trimString(tag));
1516
+ }
1517
+ if (context) {
1518
+ for (const [key, value] of Object.entries(context).slice(0, MAX_TOP_LEVEL_KEYS)) {
1519
+ const normalizedKey = normalizeKey(key);
1520
+ if (!normalizedKey) continue;
1521
+ const normalizedValue = normalizeValue(normalizedKey, value, 0);
1522
+ if (normalizedValue !== void 0) {
1523
+ output[normalizedKey] = normalizedValue;
1524
+ }
1525
+ }
1526
+ }
1527
+ if (Object.keys(output).length === 0) return void 0;
1528
+ return trimToBudget(output);
1529
+ }
1530
+
1531
+ // src/version.ts
1532
+ var BUG_REPORTER_WIDGET_VERSION = "1.0.2";
1533
+
1534
+ // src/bug-reporter.tsx
1395
1535
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1396
1536
  function isValidApiUrl(raw) {
1397
1537
  if (typeof raw !== "string" || raw.length === 0) return false;
@@ -1413,11 +1553,21 @@ function JarveBugReporter({
1413
1553
  zIndexBase,
1414
1554
  captureResponseBodies = false,
1415
1555
  theme,
1556
+ appVersion,
1557
+ environment,
1558
+ release,
1559
+ account,
1560
+ tags,
1561
+ context,
1416
1562
  onBeforeSubmit,
1417
1563
  children
1418
1564
  }) {
1419
1565
  const mounted = useHasMounted();
1420
1566
  const apiUrlValid = useMemo2(() => isValidApiUrl(apiUrl), [apiUrl]);
1567
+ const clientContext = useMemo2(
1568
+ () => normalizeClientContext({ account, tags, context }),
1569
+ [account, tags, context]
1570
+ );
1421
1571
  useEffect4(() => {
1422
1572
  if (!apiUrlValid) {
1423
1573
  console.error(
@@ -1438,6 +1588,31 @@ function JarveBugReporter({
1438
1588
  stopNetworkCapture();
1439
1589
  };
1440
1590
  }, [apiUrlValid, isPrimary, captureResponseBodies]);
1591
+ useEffect4(() => {
1592
+ if (!apiUrlValid || !isPrimary) return;
1593
+ const controller = new AbortController();
1594
+ const timeout = window.setTimeout(() => controller.abort(), 8e3);
1595
+ void fetch(`${apiUrl.replace(/\/+$/, "")}/heartbeat`, {
1596
+ method: "POST",
1597
+ headers: {
1598
+ "Content-Type": "application/json",
1599
+ "X-Bug-Reporter-Key": apiKey.trim()
1600
+ },
1601
+ body: JSON.stringify({
1602
+ siteId,
1603
+ widgetVersion: BUG_REPORTER_WIDGET_VERSION,
1604
+ appVersion,
1605
+ environment,
1606
+ release
1607
+ }),
1608
+ signal: controller.signal
1609
+ }).catch(() => {
1610
+ });
1611
+ return () => {
1612
+ window.clearTimeout(timeout);
1613
+ controller.abort();
1614
+ };
1615
+ }, [apiUrl, apiKey, apiUrlValid, appVersion, environment, isPrimary, release, siteId]);
1441
1616
  const toggleCaptureMode = useCallback3(() => {
1442
1617
  setCaptureMode((prev) => !prev);
1443
1618
  }, []);
@@ -1524,6 +1699,11 @@ function JarveBugReporter({
1524
1699
  {
1525
1700
  isActive: captureMode,
1526
1701
  siteId,
1702
+ widgetVersion: BUG_REPORTER_WIDGET_VERSION,
1703
+ appVersion,
1704
+ appEnvironment: environment,
1705
+ release,
1706
+ clientContext,
1527
1707
  reporterName,
1528
1708
  reporterEmail,
1529
1709
  onCapture: handleCapture,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jarve/bug-reporter",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Bug reporter widget for Next.js apps. Reports flow to JARVE Agency dashboard.",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",