quick-bug-reporter-react 1.1.1 → 1.3.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
@@ -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,78 +84,288 @@ 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
85
88
 
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.** |
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
+ ```
100
+
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)
118
+ // ✅ Recommended: split proxy endpoints (parallel uploads, fastest)
97
119
  const linear = new LinearIntegration({
98
- submitProxyEndpoint: "/api/bug-report",
120
+ createIssueProxyEndpoint: "/api/linear/create-issue",
121
+ uploadProxyEndpoint: "/api/linear/upload",
122
+ teamId: "TEAM_ID",
123
+ projectId: "PROJECT_ID", // optional — assigns issues to a Linear project
99
124
  });
125
+ ```
100
126
 
101
- // ⚠️ Direct API — server-side / Next.js API routes only
102
- // Does NOT work from browser SPAs (CORS + exposes API key)
103
- const linear = new LinearIntegration({
104
- apiKey: "lin_api_...",
105
- teamId: "TEAM_ID",
106
- projectId: "PROJECT_ID", // optional
127
+ | Option | Description |
128
+ |--------|-------------|
129
+ | `teamId` | Linear team ID |
130
+ | `projectId` | Linear project ID — optional, assigns every issue to this project |
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) |
135
+ | `fetchImpl` | Custom fetch implementation |
136
+
137
+ ---
138
+
139
+ ### Proxy endpoint contract (split endpoints)
140
+
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.
142
+
143
+ #### `createIssueProxyEndpoint`
144
+
145
+ Receives a JSON POST:
146
+
147
+ ```json
148
+ {
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)
156
+ }
157
+ ```
158
+
159
+ Must return:
160
+
161
+ ```json
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/..." }
167
+ ```
168
+
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)
178
+
179
+ ```ts
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;
183
+
184
+ // Convert plain-text description to Jira ADF
185
+ const adf = {
186
+ type: "doc", version: 1,
187
+ content: description.split(/\n{2,}/).filter(Boolean).map(chunk => ({
188
+ type: "paragraph",
189
+ content: [{ type: "text", text: chunk.trim() }],
190
+ })),
191
+ };
192
+
193
+ const jiraRes = await fetch(`${JIRA_BASE}/rest/api/3/issue`, {
194
+ method: "POST",
195
+ headers: {
196
+ Authorization: `Basic ${btoa(`${JIRA_EMAIL}:${JIRA_TOKEN}`)}`,
197
+ "Content-Type": "application/json",
198
+ },
199
+ body: JSON.stringify({
200
+ fields: {
201
+ project: { key: projectKey },
202
+ summary,
203
+ description: adf,
204
+ issuetype: { name: issueType || "Bug" },
205
+ },
206
+ }),
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
+ );
107
235
  });
108
236
  ```
109
237
 
110
- ### Jira
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.
111
241
 
112
242
  ```ts
113
- import { JiraIntegration } from "quick-bug-reporter-react";
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
+ }
114
262
 
115
- // Recommended: Backend proxy
116
- const jira = new JiraIntegration({
117
- submitProxyEndpoint: "/api/bug-report",
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,
290
+ });
291
+ } catch (err) {
292
+ res.status(500).json({ error: err.message });
293
+ }
118
294
  });
119
295
 
120
- // ⚠️ Direct API server-side only (CORS + exposes credentials)
121
- const jira = new JiraIntegration({
122
- baseUrl: "https://your-domain.atlassian.net",
123
- email: "you@example.com",
124
- apiToken: "...",
125
- projectKey: "BUG",
126
- issueType: "Bug", // optional, defaults to "Bug"
296
+ // POST /api/linear/uploadupload 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
+ }
127
332
  });
128
333
  ```
129
334
 
130
- ### Proxy endpoint
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.
131
339
 
132
- Your backend proxy receives a `FormData` POST with these fields:
340
+ ---
133
341
 
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 |
342
+ ### Legacy: single endpoint
147
343
 
148
- The proxy should return JSON:
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.
149
345
 
150
- ```json
151
- {
152
- "linear": { "id": "...", "identifier": "ENG-123", "url": "https://..." },
153
- "warnings": []
154
- }
155
- ```
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": [] }`
365
+
366
+ **Linear response:** `{ "linear": { "id": "...", "identifier": "ENG-123", "url": "..." }, "warnings": [] }`
367
+
368
+ </details>
156
369
 
157
370
  ### Advanced: Custom fetch
158
371
 
@@ -185,6 +398,7 @@ const linear = new LinearIntegration({
185
398
  - `ScreenshotCapturer` — HTML-to-canvas screenshot engine
186
399
  - `ScreenRecorder` — Screen + mic recording via MediaRecorder
187
400
  - `NetworkLogger` — Fetch interception logger
401
+ - `ConsoleCapture` — Console log and JS error capture
188
402
 
189
403
  ### Integrations
190
404