quick-bug-reporter-react 1.1.1 → 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
@@ -1156,22 +1156,30 @@ var LinearIntegration = class {
1156
1156
  const formData = new FormData();
1157
1157
  formData.set("provider", "linear");
1158
1158
  formData.set("title", payload.title);
1159
- formData.set("description", payload.description);
1160
- formData.set("pageUrl", payload.pageUrl);
1161
- formData.set("userAgent", payload.userAgent);
1162
- formData.set("reportedAt", payload.stoppedAt);
1163
- formData.set("captureMode", payload.captureMode);
1164
- formData.set("clientMetadata", JSON.stringify(payload.metadata));
1165
- const formattedLogs = formatNetworkLogs(payload.networkLogs);
1166
- formData.set("networkLogs", formattedLogs);
1167
- formData.append("requestsLogFile", new Blob([formattedLogs], { type: "text/plain" }), "network-logs.txt");
1168
- if (payload.videoBlob) {
1169
- const file = toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm");
1170
- 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);
1171
1165
  }
1172
1166
  if (payload.screenshotBlob) {
1173
- const file = toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png");
1174
- 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");
1175
1183
  }
1176
1184
  (onProgress ?? noop)("Submitting to Linear\u2026");
1177
1185
  const response = await this.fetchImpl(this.submitProxyEndpoint, {
@@ -1502,22 +1510,28 @@ var JiraIntegration = class {
1502
1510
  const formData = new FormData();
1503
1511
  formData.set("provider", "jira");
1504
1512
  formData.set("title", payload.title);
1505
- formData.set("description", payload.description);
1506
- formData.set("pageUrl", payload.pageUrl);
1507
- formData.set("userAgent", payload.userAgent);
1508
- formData.set("reportedAt", payload.stoppedAt);
1509
- formData.set("captureMode", payload.captureMode);
1510
- formData.set("clientMetadata", JSON.stringify(payload.metadata));
1511
- const formattedLogs = formatNetworkLogs(payload.networkLogs);
1512
- formData.append("requestsLogFile", new Blob([formattedLogs], { type: "text/plain" }), "network-logs.txt");
1513
- formData.append("clientMetadataFile", new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }), "client-metadata.json");
1514
- if (payload.videoBlob) {
1515
- const file = toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm");
1516
- 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);
1517
1517
  }
1518
1518
  if (payload.screenshotBlob) {
1519
- const file = toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png");
1520
- 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");
1521
1535
  }
1522
1536
  (onProgress ?? noop2)("Submitting to Jira\u2026");
1523
1537
  const response = await this.fetchImpl(this.submitProxyEndpoint, {