quick-bug-reporter-react 1.2.0 → 1.3.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
@@ -8,6 +8,7 @@ Drop-in bug reporter for React apps — screenshot capture, video recording, ann
8
8
  - **Video recording** — Screen + microphone via `MediaRecorder` API
9
9
  - **Annotation** — Drag-to-highlight on captured screenshots
10
10
  - **Network logging** — Automatic fetch interception during capture
11
+ - **Console capture** — Automatic console log and JS error capture
11
12
  - **Integrations** — Linear and Jira (direct API or backend proxy)
12
13
  - **Zero config UI** — Floating bug button + modal wizard, ships its own styles
13
14
 
@@ -56,18 +57,20 @@ import {
56
57
  BugReporterProvider,
57
58
  FloatingBugButton,
58
59
  BugReporterModal,
59
- LinearIntegration,
60
+ JiraIntegration,
60
61
  } from "quick-bug-reporter-react";
61
62
 
62
- const linear = new LinearIntegration({
63
- submitProxyEndpoint: "/api/bug-report",
63
+ const jira = new JiraIntegration({
64
+ createIssueProxyEndpoint: "/api/jira/create-issue",
65
+ uploadAttachmentProxyEndpoint: "/api/jira/upload-attachment",
66
+ projectKey: "BUG",
64
67
  });
65
68
 
66
69
  export default function App({ children }) {
67
70
  return (
68
71
  <BugReporterProvider
69
- integrations={{ linear }}
70
- defaultProvider="linear"
72
+ integrations={{ jira }}
73
+ defaultProvider="jira"
71
74
  >
72
75
  {children}
73
76
  <FloatingBugButton />
@@ -81,32 +84,41 @@ That's it — a floating "Report Bug" button appears in the bottom-right corner.
81
84
 
82
85
  ## Integrations
83
86
 
84
- Both integrations support two modes:
87
+ ### Jira
88
+
89
+ ```ts
90
+ import { JiraIntegration } from "quick-bug-reporter-react";
91
+
92
+ // ✅ Recommended: split proxy endpoints (parallel uploads, fastest)
93
+ const jira = new JiraIntegration({
94
+ createIssueProxyEndpoint: "/api/jira/create-issue",
95
+ uploadAttachmentProxyEndpoint: "/api/jira/upload-attachment",
96
+ projectKey: "BUG",
97
+ issueType: "Bug", // optional, defaults to "Bug"
98
+ });
99
+ ```
85
100
 
86
- | Mode | When to use | How it works |
87
- |------|------------|--------------|
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.** |
101
+ | Option | Description |
102
+ |--------|-------------|
103
+ | `projectKey` | Jira project key (e.g. `"BUG"`) |
104
+ | `issueType` | Issue type name, defaults to `"Bug"` |
105
+ | `createIssueProxyEndpoint` | Proxy for issue creation (recommended) |
106
+ | `uploadAttachmentProxyEndpoint` | Proxy for attachment uploads (recommended) |
107
+ | `submitProxyEndpoint` | Single endpoint fallback (slower — see [below](#legacy-single-endpoint)) |
108
+ | `baseUrl` | Jira instance URL (direct mode only — not for browser SPAs) |
109
+ | `email` | Jira email (direct mode only) |
110
+ | `apiToken` | Jira API token (direct mode only) |
111
+ | `fetchImpl` | Custom fetch implementation |
90
112
 
91
113
  ### Linear
92
114
 
93
115
  ```ts
94
116
  import { LinearIntegration } from "quick-bug-reporter-react";
95
117
 
96
- // ✅ Recommended: Backend proxy (works everywhere)
97
- const linear = new LinearIntegration({
98
- submitProxyEndpoint: "/api/bug-report",
99
- });
100
-
101
- // Or split proxy endpoints for finer control:
118
+ // ✅ Recommended: split proxy endpoints (parallel uploads, fastest)
102
119
  const linear = new LinearIntegration({
103
120
  createIssueProxyEndpoint: "/api/linear/create-issue",
104
121
  uploadProxyEndpoint: "/api/linear/upload",
105
- });
106
-
107
- // ⚠️ Direct API — server-side / Next.js API routes only
108
- const linear = new LinearIntegration({
109
- apiKey: "lin_api_...",
110
122
  teamId: "TEAM_ID",
111
123
  projectId: "PROJECT_ID", // optional — assigns issues to a Linear project
112
124
  });
@@ -114,101 +126,62 @@ const linear = new LinearIntegration({
114
126
 
115
127
  | Option | Description |
116
128
  |--------|-------------|
117
- | `apiKey` | Linear API key (direct mode only) |
118
129
  | `teamId` | Linear team ID |
119
130
  | `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
-
125
- ### Jira
126
-
127
- ```ts
128
- import { JiraIntegration } from "quick-bug-reporter-react";
129
-
130
- // ✅ Recommended: Backend proxy
131
- const jira = new JiraIntegration({
132
- submitProxyEndpoint: "/api/bug-report",
133
- });
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
-
142
- // ⚠️ Direct API — server-side only (CORS + exposes credentials)
143
- const jira = new JiraIntegration({
144
- baseUrl: "https://your-domain.atlassian.net",
145
- email: "you@example.com",
146
- apiToken: "...",
147
- projectKey: "BUG",
148
- issueType: "Bug", // optional, defaults to "Bug"
149
- });
150
- ```
151
-
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 |
131
+ | `createIssueProxyEndpoint` | Proxy for issue creation (recommended) |
132
+ | `uploadProxyEndpoint` | Proxy for file uploads (recommended) |
133
+ | `submitProxyEndpoint` | Single endpoint fallback (slower see [below](#legacy-single-endpoint)) |
134
+ | `apiKey` | Linear API key (direct mode only — not for browser SPAs) |
162
135
  | `fetchImpl` | Custom fetch implementation |
163
136
 
164
137
  ---
165
138
 
166
- ### Proxy endpoint contract
139
+ ### Proxy endpoint contract (split endpoints)
167
140
 
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.
141
+ With split endpoints the library handles formatting and **uploads attachments in parallel** your proxy only needs to forward auth. This is ~3× faster than a single endpoint.
169
142
 
170
- #### FormData fields
143
+ #### `createIssueProxyEndpoint`
171
144
 
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) |
145
+ Receives a JSON POST:
186
146
 
187
- #### Expected response
188
-
189
- **Jira proxy:**
190
147
  ```json
191
148
  {
192
- "jira": { "id": "10001", "key": "BUG-42", "url": "https://you.atlassian.net/browse/BUG-42" },
193
- "warnings": []
149
+ "summary": "Bug title", // Jira
150
+ "title": "Bug title", // Linear
151
+ "description": "Pre-formatted description (ready to use as-is)",
152
+ "issueType": "Bug", // Jira only
153
+ "projectKey": "BUG", // Jira only
154
+ "teamId": "...", // Linear only
155
+ "projectId": "..." // Linear only (if configured)
194
156
  }
195
157
  ```
196
158
 
197
- **Linear proxy:**
159
+ Must return:
160
+
198
161
  ```json
199
- {
200
- "linear": { "id": "...", "identifier": "ENG-123", "url": "https://linear.app/..." },
201
- "warnings": []
202
- }
162
+ // Jira
163
+ { "id": "10001", "key": "BUG-42", "url": "https://you.atlassian.net/browse/BUG-42" }
164
+
165
+ // Linear
166
+ { "id": "...", "identifier": "ENG-123", "url": "https://linear.app/..." }
203
167
  ```
204
168
 
205
- #### Example Jira proxy (Node.js / Express)
169
+ #### `uploadAttachmentProxyEndpoint` (Jira)
170
+
171
+ Receives `FormData` with `issueKey` (string) + `file` (File). Returns `{ "ok": true }`.
172
+
173
+ #### `uploadProxyEndpoint` (Linear)
174
+
175
+ Receives `FormData` with `file` (File) + `filename` (string) + `contentType` (string). Returns `{ "assetUrl": "https://..." }`.
176
+
177
+ #### Example: Jira proxy (Node.js / Express)
206
178
 
207
179
  ```ts
208
- app.post("/api/bug-report", upload.any(), async (req, res) => {
209
- const { title, description, issueType, projectKey } = req.body;
180
+ // POST /api/jira/create-issue forward issue creation with auth
181
+ app.post("/api/jira/create-issue", express.json(), async (req, res) => {
182
+ const { summary, description, issueType, projectKey } = req.body;
210
183
 
211
- // 1. Convert plain-text description to Jira ADF
184
+ // Convert plain-text description to Jira ADF
212
185
  const adf = {
213
186
  type: "doc", version: 1,
214
187
  content: description.split(/\n{2,}/).filter(Boolean).map(chunk => ({
@@ -217,8 +190,7 @@ app.post("/api/bug-report", upload.any(), async (req, res) => {
217
190
  })),
218
191
  };
219
192
 
220
- // 2. Create the issue
221
- const issue = await fetch(`${JIRA_BASE}/rest/api/3/issue`, {
193
+ const jiraRes = await fetch(`${JIRA_BASE}/rest/api/3/issue`, {
222
194
  method: "POST",
223
195
  headers: {
224
196
  Authorization: `Basic ${btoa(`${JIRA_EMAIL}:${JIRA_TOKEN}`)}`,
@@ -227,38 +199,173 @@ app.post("/api/bug-report", upload.any(), async (req, res) => {
227
199
  body: JSON.stringify({
228
200
  fields: {
229
201
  project: { key: projectKey },
230
- summary: title,
202
+ summary,
231
203
  description: adf,
232
204
  issuetype: { name: issueType || "Bug" },
233
205
  },
234
206
  }),
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,
207
+ });
208
+
209
+ const data = await jiraRes.json();
210
+ res.status(jiraRes.ok ? 201 : jiraRes.status).json(
211
+ jiraRes.ok
212
+ ? { id: data.id, key: data.key, url: `${JIRA_BASE}/browse/${data.key}` }
213
+ : { error: data.errorMessages?.join("; ") || "Jira issue creation failed" }
214
+ );
215
+ });
216
+
217
+ // POST /api/jira/upload-attachment — forward file upload with auth
218
+ app.post("/api/jira/upload-attachment", upload.single("file"), async (req, res) => {
219
+ const issueKey = req.body.issueKey;
220
+ const form = new FormData();
221
+ form.append("file", req.file.buffer, req.file.originalname);
222
+
223
+ const jiraRes = await fetch(`${JIRA_BASE}/rest/api/3/issue/${issueKey}/attachments`, {
224
+ method: "POST",
225
+ headers: {
226
+ Authorization: `Basic ${btoa(`${JIRA_EMAIL}:${JIRA_TOKEN}`)}`,
227
+ "X-Atlassian-Token": "no-check",
228
+ },
229
+ body: form,
230
+ });
231
+
232
+ res.status(jiraRes.ok ? 200 : jiraRes.status).json(
233
+ jiraRes.ok ? { ok: true } : { error: "Upload failed" }
234
+ );
235
+ });
236
+ ```
237
+
238
+ #### Example: Linear proxy (Node.js / Express)
239
+
240
+ Linear uses a **GraphQL API**. Your proxy forwards mutations to `https://api.linear.app/graphql` with the API key server-side.
241
+
242
+ ```ts
243
+ const LINEAR_API = "https://api.linear.app/graphql";
244
+ const LINEAR_API_KEY = process.env.LINEAR_API_KEY; // e.g. "lin_api_..."
245
+
246
+ // Helper: execute a Linear GraphQL mutation
247
+ async function linearGql(query: string, variables: Record<string, unknown>) {
248
+ const res = await fetch(LINEAR_API, {
249
+ method: "POST",
250
+ headers: {
251
+ Authorization: LINEAR_API_KEY,
252
+ "Content-Type": "application/json",
253
+ },
254
+ body: JSON.stringify({ query, variables }),
255
+ });
256
+ const body = await res.json();
257
+ if (!res.ok || body.errors?.length) {
258
+ throw new Error(body.errors?.[0]?.message || `Linear API error (${res.status})`);
259
+ }
260
+ return body.data;
261
+ }
262
+
263
+ // POST /api/linear/create-issue — create issue via GraphQL
264
+ app.post("/api/linear/create-issue", express.json(), async (req, res) => {
265
+ const { title, description, teamId, projectId } = req.body;
266
+
267
+ try {
268
+ const data = await linearGql(
269
+ `mutation IssueCreate($input: IssueCreateInput!) {
270
+ issueCreate(input: $input) {
271
+ success
272
+ issue { id identifier url }
273
+ }
274
+ }`,
275
+ {
276
+ input: {
277
+ title,
278
+ description,
279
+ teamId,
280
+ ...(projectId ? { projectId } : {}),
281
+ },
282
+ }
283
+ );
284
+
285
+ const issue = data.issueCreate.issue;
286
+ res.status(201).json({
287
+ id: issue.id,
288
+ identifier: issue.identifier,
289
+ url: issue.url,
248
290
  });
291
+ } catch (err) {
292
+ res.status(500).json({ error: err.message });
249
293
  }
294
+ });
250
295
 
251
- res.json({ jira: { id: issue.id, key: issue.key, url: `${JIRA_BASE}/browse/${issue.key}` } });
296
+ // POST /api/linear/upload upload file via GraphQL fileUpload + PUT
297
+ app.post("/api/linear/upload", upload.single("file"), async (req, res) => {
298
+ const file = req.file;
299
+ const contentType = req.body.contentType || file.mimetype;
300
+ const filename = req.body.filename || file.originalname;
301
+
302
+ try {
303
+ // 1. Request an upload URL from Linear
304
+ const data = await linearGql(
305
+ `mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) {
306
+ fileUpload(contentType: $contentType, filename: $filename, size: $size) {
307
+ success
308
+ uploadFile { uploadUrl assetUrl headers { key value } }
309
+ }
310
+ }`,
311
+ { contentType, filename, size: file.size }
312
+ );
313
+
314
+ const { uploadUrl, assetUrl, headers } = data.fileUpload.uploadFile;
315
+
316
+ // 2. PUT the file to the upload URL with Linear's signed headers
317
+ const uploadHeaders: Record<string, string> = {};
318
+ for (const h of headers) uploadHeaders[h.key] = h.value;
319
+
320
+ const uploadRes = await fetch(uploadUrl, {
321
+ method: "PUT",
322
+ headers: uploadHeaders,
323
+ body: file.buffer,
324
+ });
325
+
326
+ if (!uploadRes.ok) throw new Error(`Upload failed (${uploadRes.status})`);
327
+
328
+ res.json({ assetUrl });
329
+ } catch (err) {
330
+ res.status(500).json({ error: err.message });
331
+ }
252
332
  });
253
333
  ```
254
334
 
255
- ### Advanced: Split proxy endpoints
335
+ > **Key points for Linear proxy implementers:**
336
+ > - The `create-issue` endpoint receives `{ title, description, teamId, projectId }` — use these directly in the `IssueCreateInput` GraphQL mutation. The `description` is already pre-formatted by the library.
337
+ > - The `upload` endpoint must: (1) call `fileUpload` mutation to get a signed upload URL, (2) `PUT` the file binary to that URL with the returned headers, (3) return the `assetUrl`.
338
+ > - `projectId` is optional — only include it in the mutation input if it's present in the request body.
339
+
340
+ ---
341
+
342
+ ### Legacy: single endpoint
343
+
344
+ `submitProxyEndpoint` bundles everything into one request. The proxy must create the issue **and** upload all attachments server-side, which means uploads happen sequentially and are **~3× slower**. Use split endpoints above instead when possible.
345
+
346
+ <details>
347
+ <summary>Single endpoint FormData fields (click to expand)</summary>
348
+
349
+ | Field | Type | Always sent | Description |
350
+ |-------|------|:-----------:|-------------|
351
+ | `provider` | string | Yes | `"linear"` or `"jira"` |
352
+ | `title` | string | Yes | Issue title |
353
+ | `description` | string | Yes | **Pre-formatted** issue description (ready to use as-is) |
354
+ | `issueType` | string | Jira only | Issue type (e.g. `"Bug"`) |
355
+ | `projectKey` | string | Jira only | Jira project key (if configured) |
356
+ | `teamId` | string | Linear only | Linear team ID (if configured) |
357
+ | `projectId` | string | Linear only | Linear project ID (if configured) |
358
+ | `screenshotFile` | File | If screenshot | `bug-screenshot.png` |
359
+ | `screenRecordingFile` | File | If video | `bug-recording.webm` |
360
+ | `networkLogsFile` | File | Yes | `network-logs.txt` |
361
+ | `clientMetadataFile` | File | Yes | `client-metadata.json` |
362
+ | `consoleLogsFile` | File | If present | `console-logs.txt` (JS errors + console output) |
363
+
364
+ **Jira response:** `{ "jira": { "id": "...", "key": "BUG-42", "url": "..." }, "warnings": [] }`
256
365
 
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:
366
+ **Linear response:** `{ "linear": { "id": "...", "identifier": "ENG-123", "url": "..." }, "warnings": [] }`
258
367
 
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 }`
368
+ </details>
262
369
 
263
370
  ### Advanced: Custom fetch
264
371
 
@@ -291,6 +398,7 @@ const linear = new LinearIntegration({
291
398
  - `ScreenshotCapturer` — HTML-to-canvas screenshot engine
292
399
  - `ScreenRecorder` — Screen + mic recording via MediaRecorder
293
400
  - `NetworkLogger` — Fetch interception logger
401
+ - `ConsoleCapture` — Console log and JS error capture
294
402
 
295
403
  ### Integrations
296
404