quick-bug-reporter-react 1.0.6 → 1.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/README.md CHANGED
@@ -86,7 +86,7 @@ Both integrations support two modes:
86
86
  | Mode | When to use | How it works |
87
87
  |------|------------|--------------|
88
88
  | **Backend proxy** (recommended) | Production apps | Your server-side endpoint receives the report and calls Linear/Jira. API keys stay on the server. |
89
- | **Direct API** | Server-side only (Next.js API routes, etc.) | The library calls Linear/Jira APIs directly. **⚠️ Does NOT work from browser-only SPAs due to CORS.** |
89
+ | **Direct API** | Server-side only (Next.js API routes, etc.) | The library calls Linear/Jira APIs directly. **Does NOT work from browser-only SPAs due to CORS.** |
90
90
 
91
91
  ### Linear
92
92
 
@@ -98,15 +98,30 @@ const linear = new LinearIntegration({
98
98
  submitProxyEndpoint: "/api/bug-report",
99
99
  });
100
100
 
101
+ // Or split proxy endpoints for finer control:
102
+ const linear = new LinearIntegration({
103
+ createIssueProxyEndpoint: "/api/linear/create-issue",
104
+ uploadProxyEndpoint: "/api/linear/upload",
105
+ });
106
+
101
107
  // ⚠️ Direct API — server-side / Next.js API routes only
102
- // Does NOT work from browser SPAs (CORS + exposes API key)
103
108
  const linear = new LinearIntegration({
104
109
  apiKey: "lin_api_...",
105
110
  teamId: "TEAM_ID",
106
- projectId: "PROJECT_ID", // optional
111
+ projectId: "PROJECT_ID", // optional — assigns issues to a Linear project
107
112
  });
108
113
  ```
109
114
 
115
+ | Option | Description |
116
+ |--------|-------------|
117
+ | `apiKey` | Linear API key (direct mode only) |
118
+ | `teamId` | Linear team ID |
119
+ | `projectId` | Linear project ID — optional, assigns every issue to this project |
120
+ | `submitProxyEndpoint` | Single endpoint that handles the entire submission |
121
+ | `createIssueProxyEndpoint` | Proxy for issue creation only |
122
+ | `uploadProxyEndpoint` | Proxy for file uploads only |
123
+ | `fetchImpl` | Custom fetch implementation |
124
+
110
125
  ### Jira
111
126
 
112
127
  ```ts
@@ -117,6 +132,13 @@ const jira = new JiraIntegration({
117
132
  submitProxyEndpoint: "/api/bug-report",
118
133
  });
119
134
 
135
+ // Or split proxy endpoints:
136
+ const jira = new JiraIntegration({
137
+ createIssueProxyEndpoint: "/api/jira/create-issue",
138
+ uploadAttachmentProxyEndpoint: "/api/jira/upload-attachment",
139
+ projectKey: "BUG",
140
+ });
141
+
120
142
  // ⚠️ Direct API — server-side only (CORS + exposes credentials)
121
143
  const jira = new JiraIntegration({
122
144
  baseUrl: "https://your-domain.atlassian.net",
@@ -127,33 +149,117 @@ const jira = new JiraIntegration({
127
149
  });
128
150
  ```
129
151
 
130
- ### Proxy endpoint
131
-
132
- Your backend proxy receives a `FormData` POST with these fields:
133
-
134
- | Field | Type | Description |
135
- |-------|------|-------------|
136
- | `provider` | string | `"linear"` or `"jira"` |
137
- | `title` | string | Bug title |
138
- | `description` | string | Bug description |
139
- | `pageUrl` | string | URL where the bug was reported |
140
- | `userAgent` | string | Browser user agent |
141
- | `captureMode` | string | `"screenshot"` or `"video"` |
142
- | `clientMetadata` | JSON string | Device/browser metadata |
143
- | `networkLogs` | string | Formatted network logs |
144
- | `screenshotFile` | File | Screenshot PNG (if applicable) |
145
- | `screenRecordingFile` | File | Screen recording WebM (if applicable) |
146
- | `requestsLogFile` | File | Network logs as .txt |
147
-
148
- The proxy should return JSON:
152
+ | Option | Description |
153
+ |--------|-------------|
154
+ | `baseUrl` | Jira instance URL (direct mode only) |
155
+ | `email` | Jira email (direct mode only) |
156
+ | `apiToken` | Jira API token (direct mode only) |
157
+ | `projectKey` | Jira project key (e.g. `"BUG"`) |
158
+ | `issueType` | Issue type name, defaults to `"Bug"` |
159
+ | `submitProxyEndpoint` | Single endpoint that handles the entire submission |
160
+ | `createIssueProxyEndpoint` | Proxy for issue creation only |
161
+ | `uploadAttachmentProxyEndpoint` | Proxy for attachment uploads only |
162
+ | `fetchImpl` | Custom fetch implementation |
163
+
164
+ ---
165
+
166
+ ### Proxy endpoint contract
167
+
168
+ When using `submitProxyEndpoint`, the library sends a single `FormData` POST with a **pre-formatted description** and all attachment files. Your proxy just needs to create the issue and upload the files — no custom formatting required.
169
+
170
+ #### FormData fields
171
+
172
+ | Field | Type | Always sent | Description |
173
+ |-------|------|:-----------:|-------------|
174
+ | `provider` | string | Yes | `"linear"` or `"jira"` |
175
+ | `title` | string | Yes | Issue title |
176
+ | `description` | string | Yes | **Pre-formatted** issue description (ready to use as-is) |
177
+ | `issueType` | string | Jira only | Issue type (e.g. `"Bug"`) |
178
+ | `projectKey` | string | Jira only | Jira project key (if configured) |
179
+ | `teamId` | string | Linear only | Linear team ID (if configured) |
180
+ | `projectId` | string | Linear only | Linear project ID (if configured) |
181
+ | `screenshotFile` | File | If screenshot | `bug-screenshot.png` |
182
+ | `screenRecordingFile` | File | If video | `bug-recording.webm` |
183
+ | `networkLogsFile` | File | Yes | `network-logs.txt` |
184
+ | `clientMetadataFile` | File | Yes | `client-metadata.json` |
185
+ | `consoleLogsFile` | File | If present | `console-logs.txt` (JS errors + console output) |
186
+
187
+ #### Expected response
188
+
189
+ **Jira proxy:**
190
+ ```json
191
+ {
192
+ "jira": { "id": "10001", "key": "BUG-42", "url": "https://you.atlassian.net/browse/BUG-42" },
193
+ "warnings": []
194
+ }
195
+ ```
149
196
 
197
+ **Linear proxy:**
150
198
  ```json
151
199
  {
152
- "linear": { "id": "...", "identifier": "ENG-123", "url": "https://..." },
200
+ "linear": { "id": "...", "identifier": "ENG-123", "url": "https://linear.app/..." },
153
201
  "warnings": []
154
202
  }
155
203
  ```
156
204
 
205
+ #### Example Jira proxy (Node.js / Express)
206
+
207
+ ```ts
208
+ app.post("/api/bug-report", upload.any(), async (req, res) => {
209
+ const { title, description, issueType, projectKey } = req.body;
210
+
211
+ // 1. Convert plain-text description to Jira ADF
212
+ const adf = {
213
+ type: "doc", version: 1,
214
+ content: description.split(/\n{2,}/).filter(Boolean).map(chunk => ({
215
+ type: "paragraph",
216
+ content: [{ type: "text", text: chunk.trim() }],
217
+ })),
218
+ };
219
+
220
+ // 2. Create the issue
221
+ const issue = await fetch(`${JIRA_BASE}/rest/api/3/issue`, {
222
+ method: "POST",
223
+ headers: {
224
+ Authorization: `Basic ${btoa(`${JIRA_EMAIL}:${JIRA_TOKEN}`)}`,
225
+ "Content-Type": "application/json",
226
+ },
227
+ body: JSON.stringify({
228
+ fields: {
229
+ project: { key: projectKey },
230
+ summary: title,
231
+ description: adf,
232
+ issuetype: { name: issueType || "Bug" },
233
+ },
234
+ }),
235
+ }).then(r => r.json());
236
+
237
+ // 3. Upload all attachment files
238
+ for (const file of req.files) {
239
+ const form = new FormData();
240
+ form.append("file", file.buffer, file.originalname);
241
+ await fetch(`${JIRA_BASE}/rest/api/3/issue/${issue.key}/attachments`, {
242
+ method: "POST",
243
+ headers: {
244
+ Authorization: `Basic ${btoa(`${JIRA_EMAIL}:${JIRA_TOKEN}`)}`,
245
+ "X-Atlassian-Token": "no-check",
246
+ },
247
+ body: form,
248
+ });
249
+ }
250
+
251
+ res.json({ jira: { id: issue.id, key: issue.key, url: `${JIRA_BASE}/browse/${issue.key}` } });
252
+ });
253
+ ```
254
+
255
+ ### Advanced: Split proxy endpoints
256
+
257
+ Instead of a single `submitProxyEndpoint`, you can use separate endpoints for issue creation and file uploads. This gives the **library** full control over formatting while your proxy only handles auth:
258
+
259
+ - **`createIssueProxyEndpoint`** — receives `{ title, description, issueType, projectKey }` as JSON, returns `{ id, key, url }`
260
+ - **`uploadAttachmentProxyEndpoint`** (Jira) — receives `FormData` with `issueKey` + `file`, returns `{ ok: true }`
261
+ - **`uploadProxyEndpoint`** (Linear) — receives `FormData` with `file` + `filename` + `contentType`, returns `{ assetUrl }`
262
+
157
263
  ### Advanced: Custom fetch
158
264
 
159
265
  Both integrations accept a `fetchImpl` option to customize how HTTP requests are made (useful for adding auth headers, logging, or proxying):
package/dist/index.cjs CHANGED
@@ -555,6 +555,31 @@ var ScreenshotCapturer = class {
555
555
 
556
556
  // src/core/types.ts
557
557
  var DEFAULT_MAX_RECORDING_MS = 2 * 60 * 1e3;
558
+ function formatConsoleLogs(logs) {
559
+ if (logs.length === 0) {
560
+ return "No console output captured.";
561
+ }
562
+ return logs.map((entry) => {
563
+ const tag = entry.level.toUpperCase().padEnd(5);
564
+ const args = entry.args.join(" ");
565
+ return `[${entry.timestamp}] ${tag} ${args}`;
566
+ }).join("\n");
567
+ }
568
+ function formatJsErrors(errors) {
569
+ if (errors.length === 0) {
570
+ return "No JavaScript errors captured.";
571
+ }
572
+ return errors.map((entry) => {
573
+ const lines = [`[${entry.timestamp}] ${entry.type}: ${entry.message}`];
574
+ if (entry.source) {
575
+ lines.push(` at ${entry.source}${entry.lineno ? `:${entry.lineno}` : ""}${entry.colno ? `:${entry.colno}` : ""}`);
576
+ }
577
+ if (entry.stack) {
578
+ lines.push(entry.stack.split("\n").map((l) => ` ${l}`).join("\n"));
579
+ }
580
+ return lines.join("\n");
581
+ }).join("\n\n");
582
+ }
558
583
  function formatNetworkLogs(logs) {
559
584
  if (logs.length === 0) {
560
585
  return "No network requests captured.";
@@ -859,6 +884,8 @@ var BugReporter = class {
859
884
  videoBlob: artifacts.videoBlob,
860
885
  screenshotBlob: options.screenshotBlob ?? artifacts.screenshotBlob,
861
886
  networkLogs: artifacts.networkLogs,
887
+ consoleLogs: options.consoleLogs ?? [],
888
+ jsErrors: options.jsErrors ?? [],
862
889
  captureMode: artifacts.captureMode,
863
890
  pageUrl: typeof window !== "undefined" ? window.location.href : "",
864
891
  userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
@@ -898,6 +925,124 @@ var BugReporter = class {
898
925
  }
899
926
  };
900
927
 
928
+ // src/core/ConsoleCapture.ts
929
+ var MAX_CONSOLE_ENTRIES = 200;
930
+ var MAX_ERROR_ENTRIES = 50;
931
+ var MAX_ARG_LENGTH = 1e3;
932
+ function serializeArg(arg) {
933
+ if (typeof arg === "string") {
934
+ return arg.length > MAX_ARG_LENGTH ? arg.slice(0, MAX_ARG_LENGTH) + "\u2026" : arg;
935
+ }
936
+ try {
937
+ const json = JSON.stringify(arg);
938
+ return json.length > MAX_ARG_LENGTH ? json.slice(0, MAX_ARG_LENGTH) + "\u2026" : json;
939
+ } catch {
940
+ return String(arg);
941
+ }
942
+ }
943
+ var ConsoleCapture = class {
944
+ constructor() {
945
+ this.consoleLogs = [];
946
+ this.errors = [];
947
+ this.originals = {};
948
+ this.errorHandler = null;
949
+ this.rejectionHandler = null;
950
+ this.active = false;
951
+ }
952
+ start() {
953
+ if (this.active || typeof window === "undefined") {
954
+ return;
955
+ }
956
+ this.active = true;
957
+ this.consoleLogs = [];
958
+ this.errors = [];
959
+ const levels = ["log", "info", "warn", "error"];
960
+ for (const level of levels) {
961
+ const original = console[level];
962
+ this.originals[level] = original;
963
+ const capture = this;
964
+ console[level] = (...args) => {
965
+ try {
966
+ capture.consoleLogs.push({
967
+ level,
968
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
969
+ args: args.map(serializeArg)
970
+ });
971
+ if (capture.consoleLogs.length > MAX_CONSOLE_ENTRIES) {
972
+ capture.consoleLogs.shift();
973
+ }
974
+ } catch {
975
+ }
976
+ original.apply(console, args);
977
+ };
978
+ }
979
+ this.errorHandler = (event) => {
980
+ try {
981
+ this.errors.push({
982
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
983
+ message: event.message || "Unknown error",
984
+ source: event.filename || void 0,
985
+ lineno: event.lineno || void 0,
986
+ colno: event.colno || void 0,
987
+ stack: event.error?.stack || void 0,
988
+ type: "error"
989
+ });
990
+ if (this.errors.length > MAX_ERROR_ENTRIES) {
991
+ this.errors.shift();
992
+ }
993
+ } catch {
994
+ }
995
+ };
996
+ window.addEventListener("error", this.errorHandler);
997
+ this.rejectionHandler = (event) => {
998
+ try {
999
+ const reason = event.reason;
1000
+ this.errors.push({
1001
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1002
+ message: reason instanceof Error ? reason.message : String(reason),
1003
+ stack: reason instanceof Error ? reason.stack || void 0 : void 0,
1004
+ type: "unhandledrejection"
1005
+ });
1006
+ if (this.errors.length > MAX_ERROR_ENTRIES) {
1007
+ this.errors.shift();
1008
+ }
1009
+ } catch {
1010
+ }
1011
+ };
1012
+ window.addEventListener("unhandledrejection", this.rejectionHandler);
1013
+ }
1014
+ snapshot() {
1015
+ return {
1016
+ consoleLogs: [...this.consoleLogs],
1017
+ jsErrors: [...this.errors]
1018
+ };
1019
+ }
1020
+ stop() {
1021
+ if (!this.active) {
1022
+ return;
1023
+ }
1024
+ for (const [level, original] of Object.entries(this.originals)) {
1025
+ if (original) {
1026
+ console[level] = original;
1027
+ }
1028
+ }
1029
+ this.originals = {};
1030
+ if (this.errorHandler) {
1031
+ window.removeEventListener("error", this.errorHandler);
1032
+ this.errorHandler = null;
1033
+ }
1034
+ if (this.rejectionHandler) {
1035
+ window.removeEventListener("unhandledrejection", this.rejectionHandler);
1036
+ this.rejectionHandler = null;
1037
+ }
1038
+ this.active = false;
1039
+ }
1040
+ clear() {
1041
+ this.consoleLogs = [];
1042
+ this.errors = [];
1043
+ }
1044
+ };
1045
+
901
1046
  // src/integrations/linear.ts
902
1047
  var DEFAULT_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
903
1048
  var noop = () => {
@@ -979,12 +1124,22 @@ var LinearIntegration = class {
979
1124
  const description = buildCleanDescription(payload, { screenshotUrl, recordingUrl });
980
1125
  const issue = await this.createIssue(payload.title, description);
981
1126
  progress("Attaching logs\u2026");
1127
+ const comments = [];
982
1128
  const logsComment = "### Network Logs\n```text\n" + formatNetworkLogs(payload.networkLogs) + "\n```";
1129
+ comments.push(this.addComment(issue.id, logsComment));
1130
+ if (payload.jsErrors.length > 0 || payload.consoleLogs.length > 0) {
1131
+ const parts = [];
1132
+ if (payload.jsErrors.length > 0) {
1133
+ parts.push("### JavaScript Errors\n```text\n" + formatJsErrors(payload.jsErrors) + "\n```");
1134
+ }
1135
+ if (payload.consoleLogs.length > 0) {
1136
+ parts.push("### Console Output\n```text\n" + formatConsoleLogs(payload.consoleLogs) + "\n```");
1137
+ }
1138
+ comments.push(this.addComment(issue.id, parts.join("\n\n")));
1139
+ }
983
1140
  const metadataComment = "### Client Metadata\n```json\n" + JSON.stringify(payload.metadata, null, 2) + "\n```";
984
- await Promise.all([
985
- this.addComment(issue.id, logsComment),
986
- this.addComment(issue.id, metadataComment)
987
- ]);
1141
+ comments.push(this.addComment(issue.id, metadataComment));
1142
+ await Promise.all(comments);
988
1143
  progress("Done!");
989
1144
  return {
990
1145
  provider: this.provider,
@@ -1001,22 +1156,30 @@ var LinearIntegration = class {
1001
1156
  const formData = new FormData();
1002
1157
  formData.set("provider", "linear");
1003
1158
  formData.set("title", payload.title);
1004
- formData.set("description", payload.description);
1005
- formData.set("pageUrl", payload.pageUrl);
1006
- formData.set("userAgent", payload.userAgent);
1007
- formData.set("reportedAt", payload.stoppedAt);
1008
- formData.set("captureMode", payload.captureMode);
1009
- formData.set("clientMetadata", JSON.stringify(payload.metadata));
1010
- const formattedLogs = formatNetworkLogs(payload.networkLogs);
1011
- formData.set("networkLogs", formattedLogs);
1012
- formData.append("requestsLogFile", new Blob([formattedLogs], { type: "text/plain" }), "network-logs.txt");
1013
- if (payload.videoBlob) {
1014
- const file = toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm");
1015
- formData.append("screenRecordingFile", file, file.name);
1159
+ formData.set("description", buildCleanDescription(payload, { screenshotUrl: null, recordingUrl: null }));
1160
+ if (this.teamId) {
1161
+ formData.set("teamId", this.teamId);
1162
+ }
1163
+ if (this.projectId) {
1164
+ formData.set("projectId", this.projectId);
1016
1165
  }
1017
1166
  if (payload.screenshotBlob) {
1018
- const file = toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png");
1019
- formData.append("screenshotFile", file, file.name);
1167
+ formData.append("screenshotFile", toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png"), "bug-screenshot.png");
1168
+ }
1169
+ if (payload.videoBlob) {
1170
+ formData.append("screenRecordingFile", toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm"), "bug-recording.webm");
1171
+ }
1172
+ formData.append("networkLogsFile", new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }), "network-logs.txt");
1173
+ formData.append("clientMetadataFile", new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }), "client-metadata.json");
1174
+ if (payload.consoleLogs.length > 0 || payload.jsErrors.length > 0) {
1175
+ const consoleParts = [];
1176
+ if (payload.jsErrors.length > 0) {
1177
+ consoleParts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
1178
+ }
1179
+ if (payload.consoleLogs.length > 0) {
1180
+ consoleParts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
1181
+ }
1182
+ formData.append("consoleLogsFile", new Blob([consoleParts.join("\n\n")], { type: "text/plain" }), "console-logs.txt");
1020
1183
  }
1021
1184
  (onProgress ?? noop)("Submitting to Linear\u2026");
1022
1185
  const response = await this.fetchImpl(this.submitProxyEndpoint, {
@@ -1317,6 +1480,17 @@ var JiraIntegration = class {
1317
1480
  }
1318
1481
  const logsBlob = new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" });
1319
1482
  uploads.push(this.uploadAttachment(issue.key, logsBlob, "network-logs.txt", "text/plain"));
1483
+ if (payload.consoleLogs.length > 0 || payload.jsErrors.length > 0) {
1484
+ const consoleParts = [];
1485
+ if (payload.jsErrors.length > 0) {
1486
+ consoleParts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
1487
+ }
1488
+ if (payload.consoleLogs.length > 0) {
1489
+ consoleParts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
1490
+ }
1491
+ const consoleBlob = new Blob([consoleParts.join("\n\n")], { type: "text/plain" });
1492
+ uploads.push(this.uploadAttachment(issue.key, consoleBlob, "console-logs.txt", "text/plain"));
1493
+ }
1320
1494
  const metadataBlob = new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" });
1321
1495
  uploads.push(this.uploadAttachment(issue.key, metadataBlob, "client-metadata.json", "application/json"));
1322
1496
  await Promise.all(uploads);
@@ -1336,22 +1510,28 @@ var JiraIntegration = class {
1336
1510
  const formData = new FormData();
1337
1511
  formData.set("provider", "jira");
1338
1512
  formData.set("title", payload.title);
1339
- formData.set("description", payload.description);
1340
- formData.set("pageUrl", payload.pageUrl);
1341
- formData.set("userAgent", payload.userAgent);
1342
- formData.set("reportedAt", payload.stoppedAt);
1343
- formData.set("captureMode", payload.captureMode);
1344
- formData.set("clientMetadata", JSON.stringify(payload.metadata));
1345
- const formattedLogs = formatNetworkLogs(payload.networkLogs);
1346
- formData.append("requestsLogFile", new Blob([formattedLogs], { type: "text/plain" }), "network-logs.txt");
1347
- formData.append("clientMetadataFile", new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }), "client-metadata.json");
1348
- if (payload.videoBlob) {
1349
- const file = toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm");
1350
- formData.append("screenRecordingFile", file, file.name);
1513
+ formData.set("description", buildCleanDescription2(payload));
1514
+ formData.set("issueType", this.issueType);
1515
+ if (this.projectKey) {
1516
+ formData.set("projectKey", this.projectKey);
1351
1517
  }
1352
1518
  if (payload.screenshotBlob) {
1353
- const file = toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png");
1354
- formData.append("screenshotFile", file, file.name);
1519
+ formData.append("screenshotFile", toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png"), "bug-screenshot.png");
1520
+ }
1521
+ if (payload.videoBlob) {
1522
+ formData.append("screenRecordingFile", toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm"), "bug-recording.webm");
1523
+ }
1524
+ formData.append("networkLogsFile", new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }), "network-logs.txt");
1525
+ formData.append("clientMetadataFile", new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }), "client-metadata.json");
1526
+ if (payload.consoleLogs.length > 0 || payload.jsErrors.length > 0) {
1527
+ const consoleParts = [];
1528
+ if (payload.jsErrors.length > 0) {
1529
+ consoleParts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
1530
+ }
1531
+ if (payload.consoleLogs.length > 0) {
1532
+ consoleParts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
1533
+ }
1534
+ formData.append("consoleLogsFile", new Blob([consoleParts.join("\n\n")], { type: "text/plain" }), "console-logs.txt");
1355
1535
  }
1356
1536
  (onProgress ?? noop2)("Submitting to Jira\u2026");
1357
1537
  const response = await this.fetchImpl(this.submitProxyEndpoint, {
@@ -1617,6 +1797,16 @@ function BugReporterProvider({
1617
1797
  imageHeight: 0
1618
1798
  });
1619
1799
  const reporterRef = react.useRef(null);
1800
+ const consoleCaptureRef = react.useRef(null);
1801
+ react.useEffect(() => {
1802
+ const capture = new ConsoleCapture();
1803
+ capture.start();
1804
+ consoleCaptureRef.current = capture;
1805
+ return () => {
1806
+ capture.stop();
1807
+ consoleCaptureRef.current = null;
1808
+ };
1809
+ }, []);
1620
1810
  const availableProviders = react.useMemo(() => {
1621
1811
  return ["linear", "jira"].filter((provider) => Boolean(integrations[provider]));
1622
1812
  }, [integrations]);
@@ -1740,6 +1930,18 @@ function BugReporterProvider({
1740
1930
  window.clearInterval(interval);
1741
1931
  };
1742
1932
  }, [isRecording]);
1933
+ react.useEffect(() => {
1934
+ if (!isRecording) {
1935
+ return;
1936
+ }
1937
+ const onBeforeUnload = (event) => {
1938
+ event.preventDefault();
1939
+ };
1940
+ window.addEventListener("beforeunload", onBeforeUnload);
1941
+ return () => {
1942
+ window.removeEventListener("beforeunload", onBeforeUnload);
1943
+ };
1944
+ }, [isRecording]);
1743
1945
  react.useEffect(() => {
1744
1946
  return () => {
1745
1947
  setScreenshotPreviewUrl((current) => {
@@ -1963,10 +2165,16 @@ function BugReporterProvider({
1963
2165
  highlights: screenshotAnnotation.highlights
1964
2166
  } : void 0
1965
2167
  };
2168
+ const { consoleLogs, jsErrors } = consoleCaptureRef.current?.snapshot() ?? {
2169
+ consoleLogs: [],
2170
+ jsErrors: []
2171
+ };
1966
2172
  try {
1967
2173
  const result = await reporter.submit(title, description, {
1968
2174
  screenshotBlob: screenshotBlobForSubmit,
1969
2175
  metadata,
2176
+ consoleLogs,
2177
+ jsErrors,
1970
2178
  onProgress: setSubmissionProgress
1971
2179
  });
1972
2180
  setSuccess(`Submitted to ${getProviderLabel(result.provider)} (${result.issueKey}).`);
@@ -3040,6 +3248,7 @@ exports.BugReporter = BugReporter;
3040
3248
  exports.BugReporterModal = BugReporterModal;
3041
3249
  exports.BugReporterProvider = BugReporterProvider;
3042
3250
  exports.BugSession = BugSession;
3251
+ exports.ConsoleCapture = ConsoleCapture;
3043
3252
  exports.DEFAULT_MAX_RECORDING_MS = DEFAULT_MAX_RECORDING_MS;
3044
3253
  exports.FloatingBugButton = FloatingBugButton;
3045
3254
  exports.JiraIntegration = JiraIntegration;
@@ -3048,6 +3257,8 @@ exports.NetworkLogger = NetworkLogger;
3048
3257
  exports.ScreenRecorder = ScreenRecorder;
3049
3258
  exports.ScreenshotCapturer = ScreenshotCapturer;
3050
3259
  exports.collectClientEnvironmentMetadata = collectClientEnvironmentMetadata;
3260
+ exports.formatConsoleLogs = formatConsoleLogs;
3261
+ exports.formatJsErrors = formatJsErrors;
3051
3262
  exports.formatNetworkLogs = formatNetworkLogs;
3052
3263
  exports.toErrorMessage = toErrorMessage;
3053
3264
  exports.useBugReporter = useBugReporter;