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 +225 -117
- package/dist/index.cjs +1495 -1394
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -207
- package/dist/index.d.ts +4 -207
- package/dist/index.js +1495 -1395
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +11 -10
package/dist/index.cjs
CHANGED
|
@@ -13,7 +13,123 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
13
13
|
|
|
14
14
|
var html2canvas__default = /*#__PURE__*/_interopDefault(html2canvas);
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// ../core/dist/index.js
|
|
17
|
+
var MAX_CONSOLE_ENTRIES = 200;
|
|
18
|
+
var MAX_ERROR_ENTRIES = 50;
|
|
19
|
+
var MAX_ARG_LENGTH = 1e3;
|
|
20
|
+
function serializeArg(arg) {
|
|
21
|
+
if (typeof arg === "string") {
|
|
22
|
+
return arg.length > MAX_ARG_LENGTH ? arg.slice(0, MAX_ARG_LENGTH) + "\u2026" : arg;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const json = JSON.stringify(arg);
|
|
26
|
+
return json.length > MAX_ARG_LENGTH ? json.slice(0, MAX_ARG_LENGTH) + "\u2026" : json;
|
|
27
|
+
} catch {
|
|
28
|
+
return String(arg);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
var ConsoleCapture = class {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.consoleLogs = [];
|
|
34
|
+
this.errors = [];
|
|
35
|
+
this.originals = {};
|
|
36
|
+
this.errorHandler = null;
|
|
37
|
+
this.rejectionHandler = null;
|
|
38
|
+
this.active = false;
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
if (this.active || typeof window === "undefined") {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.active = true;
|
|
45
|
+
this.consoleLogs = [];
|
|
46
|
+
this.errors = [];
|
|
47
|
+
const levels = ["log", "info", "warn", "error"];
|
|
48
|
+
for (const level of levels) {
|
|
49
|
+
const original = console[level];
|
|
50
|
+
this.originals[level] = original;
|
|
51
|
+
const capture = this;
|
|
52
|
+
console[level] = (...args) => {
|
|
53
|
+
try {
|
|
54
|
+
capture.consoleLogs.push({
|
|
55
|
+
level,
|
|
56
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57
|
+
args: args.map(serializeArg)
|
|
58
|
+
});
|
|
59
|
+
if (capture.consoleLogs.length > MAX_CONSOLE_ENTRIES) {
|
|
60
|
+
capture.consoleLogs.shift();
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
original.apply(console, args);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
this.errorHandler = (event) => {
|
|
68
|
+
try {
|
|
69
|
+
this.errors.push({
|
|
70
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71
|
+
message: event.message || "Unknown error",
|
|
72
|
+
source: event.filename || void 0,
|
|
73
|
+
lineno: event.lineno || void 0,
|
|
74
|
+
colno: event.colno || void 0,
|
|
75
|
+
stack: event.error?.stack || void 0,
|
|
76
|
+
type: "error"
|
|
77
|
+
});
|
|
78
|
+
if (this.errors.length > MAX_ERROR_ENTRIES) {
|
|
79
|
+
this.errors.shift();
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
window.addEventListener("error", this.errorHandler);
|
|
85
|
+
this.rejectionHandler = (event) => {
|
|
86
|
+
try {
|
|
87
|
+
const reason = event.reason;
|
|
88
|
+
this.errors.push({
|
|
89
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
91
|
+
stack: reason instanceof Error ? reason.stack || void 0 : void 0,
|
|
92
|
+
type: "unhandledrejection"
|
|
93
|
+
});
|
|
94
|
+
if (this.errors.length > MAX_ERROR_ENTRIES) {
|
|
95
|
+
this.errors.shift();
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
window.addEventListener("unhandledrejection", this.rejectionHandler);
|
|
101
|
+
}
|
|
102
|
+
snapshot() {
|
|
103
|
+
return {
|
|
104
|
+
consoleLogs: [...this.consoleLogs],
|
|
105
|
+
jsErrors: [...this.errors]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
stop() {
|
|
109
|
+
if (!this.active) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const [level, original] of Object.entries(this.originals)) {
|
|
113
|
+
if (original) {
|
|
114
|
+
console[level] = original;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.originals = {};
|
|
118
|
+
if (this.errorHandler) {
|
|
119
|
+
window.removeEventListener("error", this.errorHandler);
|
|
120
|
+
this.errorHandler = null;
|
|
121
|
+
}
|
|
122
|
+
if (this.rejectionHandler) {
|
|
123
|
+
window.removeEventListener("unhandledrejection", this.rejectionHandler);
|
|
124
|
+
this.rejectionHandler = null;
|
|
125
|
+
}
|
|
126
|
+
this.active = false;
|
|
127
|
+
}
|
|
128
|
+
clear() {
|
|
129
|
+
this.consoleLogs = [];
|
|
130
|
+
this.errors = [];
|
|
131
|
+
}
|
|
132
|
+
};
|
|
17
133
|
function resolveMethod(input, init) {
|
|
18
134
|
if (init?.method) {
|
|
19
135
|
return init.method;
|
|
@@ -105,1550 +221,1528 @@ var NetworkLogger = class {
|
|
|
105
221
|
return this.recording;
|
|
106
222
|
}
|
|
107
223
|
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
224
|
+
var DEFAULT_MAX_RECORDING_MS = 2 * 60 * 1e3;
|
|
225
|
+
function formatConsoleLogs(logs) {
|
|
226
|
+
if (logs.length === 0) {
|
|
227
|
+
return "No console output captured.";
|
|
228
|
+
}
|
|
229
|
+
return logs.map((entry) => {
|
|
230
|
+
const tag = entry.level.toUpperCase().padEnd(5);
|
|
231
|
+
const args = entry.args.join(" ");
|
|
232
|
+
return `[${entry.timestamp}] ${tag} ${args}`;
|
|
233
|
+
}).join("\n");
|
|
234
|
+
}
|
|
235
|
+
function formatJsErrors(errors) {
|
|
236
|
+
if (errors.length === 0) {
|
|
237
|
+
return "No JavaScript errors captured.";
|
|
238
|
+
}
|
|
239
|
+
return errors.map((entry) => {
|
|
240
|
+
const lines = [`[${entry.timestamp}] ${entry.type}: ${entry.message}`];
|
|
241
|
+
if (entry.source) {
|
|
242
|
+
lines.push(` at ${entry.source}${entry.lineno ? `:${entry.lineno}` : ""}${entry.colno ? `:${entry.colno}` : ""}`);
|
|
121
243
|
}
|
|
122
|
-
if (
|
|
123
|
-
|
|
244
|
+
if (entry.stack) {
|
|
245
|
+
lines.push(entry.stack.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
124
246
|
}
|
|
125
|
-
return
|
|
247
|
+
return lines.join("\n");
|
|
248
|
+
}).join("\n\n");
|
|
249
|
+
}
|
|
250
|
+
function formatNetworkLogs(logs) {
|
|
251
|
+
if (logs.length === 0) {
|
|
252
|
+
return "No network requests captured.";
|
|
126
253
|
}
|
|
127
|
-
|
|
128
|
-
|
|
254
|
+
return logs.map((entry) => {
|
|
255
|
+
const status = entry.status === null ? "FAILED" : String(entry.status);
|
|
256
|
+
return `[${entry.timestamp}] ${entry.method.toUpperCase()} ${entry.url} -> ${status} (${entry.durationMs}ms)`;
|
|
257
|
+
}).join("\n");
|
|
258
|
+
}
|
|
259
|
+
function toErrorMessage(error) {
|
|
260
|
+
if (error instanceof Error && error.message.trim().length > 0) {
|
|
261
|
+
return error.message;
|
|
129
262
|
}
|
|
130
|
-
return
|
|
263
|
+
return "Unknown error";
|
|
131
264
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
265
|
+
function toBlobFile(blob, fileName, fallbackMimeType) {
|
|
266
|
+
return new File([blob], fileName, {
|
|
267
|
+
type: blob.type || fallbackMimeType,
|
|
268
|
+
lastModified: Date.now()
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
var DEFAULT_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
|
|
272
|
+
var noop = () => {
|
|
273
|
+
};
|
|
274
|
+
function isRecord(value) {
|
|
275
|
+
return typeof value === "object" && value !== null;
|
|
276
|
+
}
|
|
277
|
+
function parseErrorMessage(body, fallback) {
|
|
278
|
+
if (!isRecord(body)) {
|
|
279
|
+
return fallback;
|
|
135
280
|
}
|
|
136
|
-
|
|
281
|
+
if (Array.isArray(body.errors)) {
|
|
282
|
+
const messages = body.errors.map((entry) => {
|
|
283
|
+
if (isRecord(entry) && typeof entry.message === "string") {
|
|
284
|
+
return entry.message;
|
|
285
|
+
}
|
|
286
|
+
return "";
|
|
287
|
+
}).filter(Boolean);
|
|
288
|
+
if (messages.length > 0) {
|
|
289
|
+
return messages.join("; ");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return fallback;
|
|
137
293
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.displayStream = null;
|
|
142
|
-
this.microphoneStream = null;
|
|
143
|
-
this.mixedStream = null;
|
|
144
|
-
this.audioContext = null;
|
|
145
|
-
this.displayAudioStream = null;
|
|
146
|
-
this.microphoneAudioStream = null;
|
|
147
|
-
this.chunks = [];
|
|
148
|
-
this.recording = false;
|
|
149
|
-
this.stopPromise = null;
|
|
150
|
-
this.stopResolver = null;
|
|
151
|
-
this.lastBlob = null;
|
|
152
|
-
this.onEnded = null;
|
|
294
|
+
function ensureString(value, message) {
|
|
295
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
296
|
+
throw new Error(message);
|
|
153
297
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
function buildCleanDescription(payload, mediaLinks) {
|
|
301
|
+
const lines = [
|
|
302
|
+
payload.description,
|
|
303
|
+
"",
|
|
304
|
+
"### Context",
|
|
305
|
+
`- Reported At: ${payload.stoppedAt}`,
|
|
306
|
+
`- Capture Mode: ${payload.captureMode === "screenshot" ? "Screenshot" : "Video"}`,
|
|
307
|
+
`- Page URL: ${payload.pageUrl || "Unknown"}`,
|
|
308
|
+
""
|
|
309
|
+
];
|
|
310
|
+
if (mediaLinks.screenshotUrl || mediaLinks.recordingUrl) {
|
|
311
|
+
lines.push("### Media");
|
|
312
|
+
if (mediaLinks.screenshotUrl) {
|
|
313
|
+
lines.push(`- Screenshot: [Open screenshot](${mediaLinks.screenshotUrl})`);
|
|
157
314
|
}
|
|
158
|
-
if (
|
|
159
|
-
|
|
315
|
+
if (mediaLinks.recordingUrl) {
|
|
316
|
+
lines.push(`- Recording: [Open recording](${mediaLinks.recordingUrl})`);
|
|
160
317
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
track.stop();
|
|
202
|
-
}
|
|
203
|
-
for (const track of microphoneStream.getTracks()) {
|
|
204
|
-
track.stop();
|
|
205
|
-
}
|
|
206
|
-
throw new Error("Microphone audio track is unavailable.");
|
|
207
|
-
}
|
|
208
|
-
for (const track of microphoneAudioTracks) {
|
|
209
|
-
track.enabled = true;
|
|
210
|
-
}
|
|
211
|
-
const mixedAudioTracks = await this.buildMixedAudioTracks(displayStream, microphoneStream);
|
|
212
|
-
const tracks = [...displayStream.getVideoTracks(), ...mixedAudioTracks];
|
|
213
|
-
if (tracks.length === 0) {
|
|
214
|
-
for (const track of displayStream.getTracks()) {
|
|
215
|
-
track.stop();
|
|
216
|
-
}
|
|
217
|
-
for (const track of microphoneStream.getTracks()) {
|
|
218
|
-
track.stop();
|
|
219
|
-
}
|
|
220
|
-
throw new Error("No media tracks available for recording.");
|
|
221
|
-
}
|
|
222
|
-
this.mixedStream = new MediaStream(tracks);
|
|
223
|
-
const mimeType = pickMimeType();
|
|
224
|
-
const recorderOptions = {
|
|
225
|
-
videoBitsPerSecond: 12e5,
|
|
226
|
-
audioBitsPerSecond: 96e3
|
|
227
|
-
};
|
|
228
|
-
if (mimeType) {
|
|
229
|
-
recorderOptions.mimeType = mimeType;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
this.mediaRecorder = new MediaRecorder(this.mixedStream, recorderOptions);
|
|
233
|
-
} catch {
|
|
234
|
-
if (mimeType) {
|
|
235
|
-
try {
|
|
236
|
-
this.mediaRecorder = new MediaRecorder(this.mixedStream, { mimeType });
|
|
237
|
-
} catch {
|
|
238
|
-
this.mediaRecorder = new MediaRecorder(this.mixedStream);
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
this.mediaRecorder = new MediaRecorder(this.mixedStream);
|
|
242
|
-
}
|
|
318
|
+
lines.push("");
|
|
319
|
+
}
|
|
320
|
+
lines.push(
|
|
321
|
+
"*Network logs and client metadata are attached as comments below.*"
|
|
322
|
+
);
|
|
323
|
+
return lines.join("\n");
|
|
324
|
+
}
|
|
325
|
+
var LinearIntegration = class {
|
|
326
|
+
constructor(options) {
|
|
327
|
+
this.provider = "linear";
|
|
328
|
+
this.apiKey = options.apiKey;
|
|
329
|
+
this.teamId = options.teamId;
|
|
330
|
+
this.projectId = options.projectId;
|
|
331
|
+
this.graphqlEndpoint = options.graphqlEndpoint ?? DEFAULT_GRAPHQL_ENDPOINT;
|
|
332
|
+
this.submitProxyEndpoint = options.submitProxyEndpoint;
|
|
333
|
+
this.createIssueProxyEndpoint = options.createIssueProxyEndpoint;
|
|
334
|
+
this.uploadProxyEndpoint = options.uploadProxyEndpoint;
|
|
335
|
+
this.fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
336
|
+
}
|
|
337
|
+
async submit(payload, onProgress) {
|
|
338
|
+
if (this.submitProxyEndpoint) {
|
|
339
|
+
return this.submitViaProxy(payload, onProgress);
|
|
340
|
+
}
|
|
341
|
+
const progress = onProgress ?? noop;
|
|
342
|
+
progress("Uploading media\u2026");
|
|
343
|
+
const [screenshotUrl, recordingUrl] = await Promise.all([
|
|
344
|
+
payload.screenshotBlob ? this.uploadAsset(payload.screenshotBlob, "bug-screenshot.png", "image/png") : Promise.resolve(null),
|
|
345
|
+
payload.videoBlob ? this.uploadAsset(payload.videoBlob, "bug-recording.webm", "video/webm") : Promise.resolve(null)
|
|
346
|
+
]);
|
|
347
|
+
progress("Creating Linear issue\u2026");
|
|
348
|
+
const description = buildCleanDescription(payload, { screenshotUrl, recordingUrl });
|
|
349
|
+
const issue = await this.createIssue(payload.title, description);
|
|
350
|
+
progress("Attaching logs\u2026");
|
|
351
|
+
const comments = [];
|
|
352
|
+
const logsComment = "### Network Logs\n```text\n" + formatNetworkLogs(payload.networkLogs) + "\n```";
|
|
353
|
+
comments.push(this.addComment(issue.id, logsComment));
|
|
354
|
+
if (payload.jsErrors.length > 0 || payload.consoleLogs.length > 0) {
|
|
355
|
+
const parts = [];
|
|
356
|
+
if (payload.jsErrors.length > 0) {
|
|
357
|
+
parts.push("### JavaScript Errors\n```text\n" + formatJsErrors(payload.jsErrors) + "\n```");
|
|
243
358
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.chunks.push(event.data);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
this.mediaRecorder.onstop = () => {
|
|
250
|
-
const mime = this.mediaRecorder?.mimeType || mimeType || "video/webm";
|
|
251
|
-
const blob = this.chunks.length > 0 ? new Blob(this.chunks, { type: mime }) : null;
|
|
252
|
-
this.lastBlob = blob;
|
|
253
|
-
this.recording = false;
|
|
254
|
-
this.cleanupStreams();
|
|
255
|
-
const resolve = this.stopResolver;
|
|
256
|
-
this.resetStopPromise();
|
|
257
|
-
resolve?.(blob);
|
|
258
|
-
};
|
|
259
|
-
for (const track of displayStream.getVideoTracks()) {
|
|
260
|
-
track.onended = () => {
|
|
261
|
-
if (!this.recording) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
void this.stop().finally(() => {
|
|
265
|
-
this.onEnded?.();
|
|
266
|
-
});
|
|
267
|
-
};
|
|
359
|
+
if (payload.consoleLogs.length > 0) {
|
|
360
|
+
parts.push("### Console Output\n```text\n" + formatConsoleLogs(payload.consoleLogs) + "\n```");
|
|
268
361
|
}
|
|
269
|
-
this.
|
|
270
|
-
this.recording = true;
|
|
271
|
-
} catch (error) {
|
|
272
|
-
this.cleanupStreams();
|
|
273
|
-
this.recording = false;
|
|
274
|
-
throw mapRecorderError(error);
|
|
362
|
+
comments.push(this.addComment(issue.id, parts.join("\n\n")));
|
|
275
363
|
}
|
|
364
|
+
const metadataComment = "### Client Metadata\n```json\n" + JSON.stringify(payload.metadata, null, 2) + "\n```";
|
|
365
|
+
comments.push(this.addComment(issue.id, metadataComment));
|
|
366
|
+
await Promise.all(comments);
|
|
367
|
+
progress("Done!");
|
|
368
|
+
return {
|
|
369
|
+
provider: this.provider,
|
|
370
|
+
issueId: issue.id,
|
|
371
|
+
issueKey: issue.identifier,
|
|
372
|
+
issueUrl: issue.url,
|
|
373
|
+
warnings: []
|
|
374
|
+
};
|
|
276
375
|
}
|
|
277
|
-
async
|
|
278
|
-
if (!this.
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
if (this.stopPromise) {
|
|
282
|
-
return this.stopPromise;
|
|
376
|
+
async submitViaProxy(payload, onProgress) {
|
|
377
|
+
if (!this.submitProxyEndpoint) {
|
|
378
|
+
throw new Error("Linear submit proxy endpoint is not configured.");
|
|
283
379
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
380
|
+
const formData = new FormData();
|
|
381
|
+
formData.set("provider", "linear");
|
|
382
|
+
formData.set("title", payload.title);
|
|
383
|
+
formData.set("description", buildCleanDescription(payload, { screenshotUrl: null, recordingUrl: null }));
|
|
384
|
+
if (this.teamId) {
|
|
385
|
+
formData.set("teamId", this.teamId);
|
|
288
386
|
}
|
|
289
|
-
this.
|
|
290
|
-
this.
|
|
291
|
-
});
|
|
292
|
-
if (this.mediaRecorder.state === "inactive") {
|
|
293
|
-
this.mediaRecorder.onstop?.(new Event("stop"));
|
|
294
|
-
} else {
|
|
295
|
-
this.mediaRecorder.stop();
|
|
387
|
+
if (this.projectId) {
|
|
388
|
+
formData.set("projectId", this.projectId);
|
|
296
389
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
isRecording() {
|
|
300
|
-
return this.recording;
|
|
301
|
-
}
|
|
302
|
-
getLastBlob() {
|
|
303
|
-
return this.lastBlob;
|
|
304
|
-
}
|
|
305
|
-
clearLastBlob() {
|
|
306
|
-
this.lastBlob = null;
|
|
307
|
-
}
|
|
308
|
-
dispose() {
|
|
309
|
-
this.cleanupStreams();
|
|
310
|
-
this.recording = false;
|
|
311
|
-
this.resetStopPromise();
|
|
312
|
-
}
|
|
313
|
-
cleanupStreams() {
|
|
314
|
-
if (this.audioContext) {
|
|
315
|
-
void this.audioContext.close().catch(() => void 0);
|
|
390
|
+
if (payload.screenshotBlob) {
|
|
391
|
+
formData.append("screenshotFile", toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png"), "bug-screenshot.png");
|
|
316
392
|
}
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
track.stop();
|
|
320
|
-
}
|
|
393
|
+
if (payload.videoBlob) {
|
|
394
|
+
formData.append("screenRecordingFile", toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm"), "bug-recording.webm");
|
|
321
395
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
396
|
+
formData.append("networkLogsFile", new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }), "network-logs.txt");
|
|
397
|
+
formData.append("clientMetadataFile", new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }), "client-metadata.json");
|
|
398
|
+
if (payload.consoleLogs.length > 0 || payload.jsErrors.length > 0) {
|
|
399
|
+
const consoleParts = [];
|
|
400
|
+
if (payload.jsErrors.length > 0) {
|
|
401
|
+
consoleParts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
|
|
325
402
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for (const track of this.microphoneStream.getTracks()) {
|
|
329
|
-
track.stop();
|
|
403
|
+
if (payload.consoleLogs.length > 0) {
|
|
404
|
+
consoleParts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
|
|
330
405
|
}
|
|
406
|
+
formData.append("consoleLogsFile", new Blob([consoleParts.join("\n\n")], { type: "text/plain" }), "console-logs.txt");
|
|
331
407
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
408
|
+
(onProgress ?? noop)("Submitting to Linear\u2026");
|
|
409
|
+
const response = await this.fetchImpl(this.submitProxyEndpoint, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
body: formData
|
|
412
|
+
});
|
|
413
|
+
const body = await response.json().catch(() => null);
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
const errorMessage = typeof body?.error === "string" ? body.error : body?.error?.message || "Linear proxy submission failed.";
|
|
416
|
+
throw new Error(errorMessage);
|
|
336
417
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
418
|
+
const linear = body?.linear;
|
|
419
|
+
if (!linear?.id || !linear.identifier) {
|
|
420
|
+
throw new Error("Linear proxy submission failed: invalid response.");
|
|
341
421
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
422
|
+
return {
|
|
423
|
+
provider: this.provider,
|
|
424
|
+
issueId: linear.id,
|
|
425
|
+
issueKey: linear.identifier,
|
|
426
|
+
issueUrl: linear.url ?? null,
|
|
427
|
+
warnings: Array.isArray(body?.warnings) ? body.warnings : []
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async createIssue(title, description) {
|
|
431
|
+
if (this.createIssueProxyEndpoint) {
|
|
432
|
+
const response2 = await this.fetchImpl(this.createIssueProxyEndpoint, {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: {
|
|
435
|
+
"Content-Type": "application/json"
|
|
436
|
+
},
|
|
437
|
+
body: JSON.stringify({
|
|
438
|
+
title,
|
|
439
|
+
description,
|
|
440
|
+
teamId: this.teamId,
|
|
441
|
+
projectId: this.projectId
|
|
442
|
+
})
|
|
443
|
+
});
|
|
444
|
+
const payload = await response2.json().catch(() => null);
|
|
445
|
+
if (!response2.ok || !payload?.id || !payload.identifier) {
|
|
446
|
+
throw new Error(payload?.error || "Linear issue creation proxy failed.");
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
id: payload.id,
|
|
450
|
+
identifier: payload.identifier,
|
|
451
|
+
url: payload.url ?? null
|
|
452
|
+
};
|
|
373
453
|
}
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
microphoneGain.gain.value = 1;
|
|
379
|
-
microphoneSource.connect(microphoneGain);
|
|
380
|
-
microphoneGain.connect(destination);
|
|
454
|
+
if (!this.apiKey || !this.teamId) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
"Linear integration is missing credentials. Configure apiKey + teamId or a createIssueProxyEndpoint."
|
|
457
|
+
);
|
|
381
458
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
459
|
+
const query = `
|
|
460
|
+
mutation IssueCreate($input: IssueCreateInput!) {
|
|
461
|
+
issueCreate(input: $input) {
|
|
462
|
+
success
|
|
463
|
+
issue {
|
|
464
|
+
id
|
|
465
|
+
identifier
|
|
466
|
+
url
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
`;
|
|
471
|
+
const issueInput = {
|
|
472
|
+
teamId: this.teamId,
|
|
473
|
+
title,
|
|
474
|
+
description
|
|
475
|
+
};
|
|
476
|
+
if (this.projectId) {
|
|
477
|
+
issueInput.projectId = this.projectId;
|
|
386
478
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
479
|
+
const response = await this.fetchImpl(this.graphqlEndpoint, {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: {
|
|
482
|
+
Authorization: this.apiKey,
|
|
483
|
+
"Content-Type": "application/json"
|
|
484
|
+
},
|
|
485
|
+
body: JSON.stringify({
|
|
486
|
+
query,
|
|
487
|
+
variables: {
|
|
488
|
+
input: issueInput
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
});
|
|
492
|
+
const body = await response.json().catch(() => null);
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
throw new Error(parseErrorMessage(body, `Linear issue creation failed (${response.status})`));
|
|
401
495
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
overrideStyle.textContent = `html, body { height: auto !important; overflow: visible !important; }`;
|
|
407
|
-
clonedDoc.head.appendChild(overrideStyle);
|
|
408
|
-
}
|
|
409
|
-
function sanitizeCloneForModernColors(clonedDocument) {
|
|
410
|
-
const styleElements = clonedDocument.querySelectorAll("style");
|
|
411
|
-
for (const styleElement of styleElements) {
|
|
412
|
-
if (!styleElement.textContent) {
|
|
413
|
-
continue;
|
|
496
|
+
const parsedIssueCreate = isRecord(body) && isRecord(body.data) && isRecord(body.data.issueCreate) ? body.data.issueCreate : null;
|
|
497
|
+
const parsedIssue = parsedIssueCreate && isRecord(parsedIssueCreate.issue) ? parsedIssueCreate.issue : null;
|
|
498
|
+
if (!parsedIssueCreate || !parsedIssue || parsedIssueCreate.success !== true) {
|
|
499
|
+
throw new Error(parseErrorMessage(body, "Linear issue creation failed."));
|
|
414
500
|
}
|
|
415
|
-
|
|
501
|
+
return {
|
|
502
|
+
id: ensureString(parsedIssue.id, "Linear did not return issue id."),
|
|
503
|
+
identifier: ensureString(parsedIssue.identifier, "Linear did not return issue identifier."),
|
|
504
|
+
url: typeof parsedIssue.url === "string" ? parsedIssue.url : null
|
|
505
|
+
};
|
|
416
506
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (!style) {
|
|
421
|
-
continue;
|
|
507
|
+
async addComment(issueId, body) {
|
|
508
|
+
if (!this.apiKey) {
|
|
509
|
+
return;
|
|
422
510
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
511
|
+
const query = `
|
|
512
|
+
mutation CommentCreate($input: CommentCreateInput!) {
|
|
513
|
+
commentCreate(input: $input) {
|
|
514
|
+
success
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
518
|
+
const response = await this.fetchImpl(this.graphqlEndpoint, {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: {
|
|
521
|
+
Authorization: this.apiKey,
|
|
522
|
+
"Content-Type": "application/json"
|
|
523
|
+
},
|
|
524
|
+
body: JSON.stringify({
|
|
525
|
+
query,
|
|
526
|
+
variables: {
|
|
527
|
+
input: { issueId, body }
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
});
|
|
531
|
+
if (!response.ok) ;
|
|
432
532
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
533
|
+
async uploadAsset(blob, fileName, fallbackMimeType) {
|
|
534
|
+
const file = toBlobFile(blob, fileName, fallbackMimeType);
|
|
535
|
+
if (this.uploadProxyEndpoint) {
|
|
536
|
+
const formData = new FormData();
|
|
537
|
+
formData.set("file", file, file.name);
|
|
538
|
+
formData.set("filename", file.name);
|
|
539
|
+
formData.set("contentType", file.type || fallbackMimeType);
|
|
540
|
+
const response = await this.fetchImpl(this.uploadProxyEndpoint, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
body: formData
|
|
543
|
+
});
|
|
544
|
+
const payload = await response.json().catch(() => null);
|
|
545
|
+
if (!response.ok || !payload?.assetUrl) {
|
|
546
|
+
throw new Error(payload?.error || "Linear upload proxy failed.");
|
|
547
|
+
}
|
|
548
|
+
return payload.assetUrl;
|
|
439
549
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
throw new Error("Could not find a capture target for screenshot.");
|
|
550
|
+
if (!this.apiKey) {
|
|
551
|
+
throw new Error("Linear upload requires apiKey or uploadProxyEndpoint.");
|
|
443
552
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
553
|
+
const uploadTarget = await this.requestUploadTarget(file);
|
|
554
|
+
const uploadHeaders = uploadTarget.headers.reduce((acc, entry) => {
|
|
555
|
+
if (entry.key && entry.value) {
|
|
556
|
+
acc[entry.key] = entry.value;
|
|
557
|
+
}
|
|
558
|
+
return acc;
|
|
559
|
+
}, {});
|
|
560
|
+
const uploadResponse = await this.fetchImpl(uploadTarget.uploadUrl, {
|
|
561
|
+
method: "PUT",
|
|
562
|
+
headers: uploadHeaders,
|
|
563
|
+
body: file
|
|
564
|
+
});
|
|
565
|
+
if (!uploadResponse.ok) {
|
|
448
566
|
throw new Error(
|
|
449
|
-
|
|
567
|
+
"Linear media upload failed. Configure an uploadProxyEndpoint if your browser blocks direct uploads."
|
|
450
568
|
);
|
|
451
569
|
}
|
|
570
|
+
return uploadTarget.assetUrl;
|
|
452
571
|
}
|
|
453
|
-
async
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const origHtmlHeight = htmlEl.style.height;
|
|
465
|
-
const origHtmlOverflow = htmlEl.style.overflow;
|
|
466
|
-
const origBodyHeight = bodyEl.style.height;
|
|
467
|
-
const origBodyOverflow = bodyEl.style.overflow;
|
|
468
|
-
htmlEl.style.setProperty("height", "auto", "important");
|
|
469
|
-
htmlEl.style.setProperty("overflow", "visible", "important");
|
|
470
|
-
bodyEl.style.setProperty("height", "auto", "important");
|
|
471
|
-
bodyEl.style.setProperty("overflow", "visible", "important");
|
|
472
|
-
const sharedOptions = {
|
|
473
|
-
backgroundColor,
|
|
474
|
-
logging: false,
|
|
475
|
-
useCORS: true,
|
|
476
|
-
allowTaint: false,
|
|
477
|
-
scale,
|
|
478
|
-
windowWidth: viewportWidth,
|
|
479
|
-
windowHeight: viewportHeight,
|
|
480
|
-
scrollX: 0,
|
|
481
|
-
scrollY: 0,
|
|
482
|
-
ignoreElements: (element) => {
|
|
483
|
-
return element instanceof HTMLElement && element.getAttribute(BUG_REPORTER_UI_ATTR) === "true";
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
let lastError = null;
|
|
487
|
-
const attempts = [
|
|
488
|
-
{ foreignObjectRendering: true, sanitizeColorFunctions: false },
|
|
489
|
-
{ foreignObjectRendering: false, sanitizeColorFunctions: false },
|
|
490
|
-
{ foreignObjectRendering: false, sanitizeColorFunctions: true }
|
|
491
|
-
];
|
|
492
|
-
try {
|
|
493
|
-
for (const attempt of attempts) {
|
|
494
|
-
try {
|
|
495
|
-
const fullCanvas = await html2canvas__default.default(target, {
|
|
496
|
-
...sharedOptions,
|
|
497
|
-
foreignObjectRendering: attempt.foreignObjectRendering,
|
|
498
|
-
onclone: (clonedDocument) => {
|
|
499
|
-
unclampClonedLayout(clonedDocument);
|
|
500
|
-
if (attempt.sanitizeColorFunctions) {
|
|
501
|
-
sanitizeCloneForModernColors(clonedDocument);
|
|
502
|
-
}
|
|
572
|
+
async requestUploadTarget(file) {
|
|
573
|
+
const query = `
|
|
574
|
+
mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) {
|
|
575
|
+
fileUpload(contentType: $contentType, filename: $filename, size: $size) {
|
|
576
|
+
success
|
|
577
|
+
uploadFile {
|
|
578
|
+
uploadUrl
|
|
579
|
+
assetUrl
|
|
580
|
+
headers {
|
|
581
|
+
key
|
|
582
|
+
value
|
|
503
583
|
}
|
|
504
|
-
});
|
|
505
|
-
const cropW = Math.round(viewportWidth * scale);
|
|
506
|
-
const cropH = Math.round(viewportHeight * scale);
|
|
507
|
-
const cropX = Math.round(savedScrollX * scale);
|
|
508
|
-
const cropY = Math.round(savedScrollY * scale);
|
|
509
|
-
const cropCanvas = document.createElement("canvas");
|
|
510
|
-
cropCanvas.width = cropW;
|
|
511
|
-
cropCanvas.height = cropH;
|
|
512
|
-
const ctx = cropCanvas.getContext("2d");
|
|
513
|
-
if (!ctx) {
|
|
514
|
-
return await canvasToPngBlob(fullCanvas);
|
|
515
584
|
}
|
|
516
|
-
ctx.drawImage(fullCanvas, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);
|
|
517
|
-
return await canvasToPngBlob(cropCanvas);
|
|
518
|
-
} catch (error) {
|
|
519
|
-
lastError = error;
|
|
520
585
|
}
|
|
521
586
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
587
|
+
`;
|
|
588
|
+
const response = await this.fetchImpl(this.graphqlEndpoint, {
|
|
589
|
+
method: "POST",
|
|
590
|
+
headers: {
|
|
591
|
+
Authorization: ensureString(this.apiKey, "Linear upload requires apiKey."),
|
|
592
|
+
"Content-Type": "application/json"
|
|
593
|
+
},
|
|
594
|
+
body: JSON.stringify({
|
|
595
|
+
query,
|
|
596
|
+
variables: {
|
|
597
|
+
contentType: file.type || "application/octet-stream",
|
|
598
|
+
filename: file.name,
|
|
599
|
+
size: file.size
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
});
|
|
603
|
+
const body = await response.json().catch(() => null);
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
throw new Error(parseErrorMessage(body, `Linear upload target request failed (${response.status})`));
|
|
529
606
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const bitmap = await createImageBitmap(blob);
|
|
537
|
-
const scale = bitmap.width / (window.innerWidth || 1);
|
|
538
|
-
const sx = Math.round(region.x * scale);
|
|
539
|
-
const sy = Math.round(region.y * scale);
|
|
540
|
-
const sw = Math.round(region.width * scale);
|
|
541
|
-
const sh = Math.round(region.height * scale);
|
|
542
|
-
const canvas = document.createElement("canvas");
|
|
543
|
-
canvas.width = Math.max(1, sw);
|
|
544
|
-
canvas.height = Math.max(1, sh);
|
|
545
|
-
const ctx = canvas.getContext("2d");
|
|
546
|
-
if (!ctx) {
|
|
547
|
-
bitmap.close();
|
|
548
|
-
throw new Error("Could not create canvas for region crop.");
|
|
607
|
+
const uploadFile = isRecord(body) && isRecord(body.data) && isRecord(body.data.fileUpload) && isRecord(body.data.fileUpload.uploadFile) ? body.data.fileUpload.uploadFile : null;
|
|
608
|
+
const headers = Array.isArray(uploadFile?.headers) ? uploadFile.headers.filter((entry) => {
|
|
609
|
+
return isRecord(entry) && typeof entry.key === "string" && typeof entry.value === "string";
|
|
610
|
+
}) : [];
|
|
611
|
+
if (!uploadFile || typeof uploadFile.uploadUrl !== "string" || typeof uploadFile.assetUrl !== "string") {
|
|
612
|
+
throw new Error(parseErrorMessage(body, "Linear did not return a valid upload target."));
|
|
549
613
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
614
|
+
return {
|
|
615
|
+
uploadUrl: uploadFile.uploadUrl,
|
|
616
|
+
assetUrl: uploadFile.assetUrl,
|
|
617
|
+
headers
|
|
618
|
+
};
|
|
553
619
|
}
|
|
554
620
|
};
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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");
|
|
621
|
+
var noop2 = () => {
|
|
622
|
+
};
|
|
623
|
+
function isRecord2(value) {
|
|
624
|
+
return typeof value === "object" && value !== null;
|
|
567
625
|
}
|
|
568
|
-
function
|
|
569
|
-
if (
|
|
570
|
-
|
|
626
|
+
function toBasicAuth(email, token) {
|
|
627
|
+
if (typeof btoa !== "function") {
|
|
628
|
+
throw new Error("btoa is unavailable; use backend proxy endpoints for Jira auth.");
|
|
571
629
|
}
|
|
572
|
-
return
|
|
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");
|
|
630
|
+
return `Basic ${btoa(`${email}:${token}`)}`;
|
|
582
631
|
}
|
|
583
|
-
function
|
|
584
|
-
if (
|
|
585
|
-
return
|
|
632
|
+
function getErrorDetail(payload, fallback) {
|
|
633
|
+
if (!isRecord2(payload)) {
|
|
634
|
+
return fallback;
|
|
586
635
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
636
|
+
if (Array.isArray(payload.errorMessages) && payload.errorMessages.length > 0) {
|
|
637
|
+
return payload.errorMessages.map((entry) => String(entry)).join("; ");
|
|
638
|
+
}
|
|
639
|
+
if (isRecord2(payload.errors)) {
|
|
640
|
+
return Object.values(payload.errors).map((entry) => String(entry)).join("; ");
|
|
641
|
+
}
|
|
642
|
+
return fallback;
|
|
591
643
|
}
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
644
|
+
function buildCleanDescription2(payload) {
|
|
645
|
+
const lines = [
|
|
646
|
+
payload.description,
|
|
647
|
+
"",
|
|
648
|
+
"Context:",
|
|
649
|
+
`- Reported At: ${payload.stoppedAt}`,
|
|
650
|
+
`- Capture Mode: ${payload.captureMode === "screenshot" ? "Screenshot" : "Video"}`,
|
|
651
|
+
`- Page URL: ${payload.pageUrl || "Unknown"}`
|
|
652
|
+
];
|
|
653
|
+
const hasScreenshot = Boolean(payload.screenshotBlob);
|
|
654
|
+
const hasVideo = Boolean(payload.videoBlob);
|
|
655
|
+
if (hasScreenshot || hasVideo) {
|
|
656
|
+
lines.push("", "Attachments:");
|
|
657
|
+
if (hasScreenshot) lines.push("- Screenshot attached");
|
|
658
|
+
if (hasVideo) lines.push("- Screen recording attached");
|
|
659
|
+
lines.push("- Network logs attached (network-logs.txt)");
|
|
660
|
+
lines.push("- Client metadata attached (client-metadata.json)");
|
|
595
661
|
}
|
|
596
|
-
return "
|
|
662
|
+
return lines.join("\n");
|
|
597
663
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
type:
|
|
601
|
-
|
|
602
|
-
});
|
|
664
|
+
function toJiraAdf(text) {
|
|
665
|
+
const paragraphs = text.split(/\n{2,}/).map((chunk) => chunk.trim()).filter(Boolean).map((chunk) => ({
|
|
666
|
+
type: "paragraph",
|
|
667
|
+
content: [{ type: "text", text: chunk }]
|
|
668
|
+
}));
|
|
669
|
+
return {
|
|
670
|
+
type: "doc",
|
|
671
|
+
version: 1,
|
|
672
|
+
content: paragraphs
|
|
673
|
+
};
|
|
603
674
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
height: null,
|
|
617
|
-
pixelRatio: null
|
|
618
|
-
},
|
|
619
|
-
screen: {
|
|
620
|
-
width: null,
|
|
621
|
-
height: null,
|
|
622
|
-
availWidth: null,
|
|
623
|
-
availHeight: null,
|
|
624
|
-
colorDepth: null
|
|
625
|
-
},
|
|
626
|
-
device: {
|
|
627
|
-
hardwareConcurrency: null,
|
|
628
|
-
deviceMemoryGb: null,
|
|
629
|
-
maxTouchPoints: null,
|
|
630
|
-
online: null,
|
|
631
|
-
cookieEnabled: null
|
|
632
|
-
},
|
|
633
|
-
connection: {
|
|
634
|
-
effectiveType: null,
|
|
635
|
-
downlinkMbps: null,
|
|
636
|
-
rttMs: null,
|
|
637
|
-
saveData: null
|
|
638
|
-
}
|
|
639
|
-
};
|
|
675
|
+
var JiraIntegration = class {
|
|
676
|
+
constructor(options) {
|
|
677
|
+
this.provider = "jira";
|
|
678
|
+
this.baseUrl = options.baseUrl?.replace(/\/$/, "");
|
|
679
|
+
this.email = options.email;
|
|
680
|
+
this.apiToken = options.apiToken;
|
|
681
|
+
this.projectKey = options.projectKey;
|
|
682
|
+
this.issueType = options.issueType ?? "Bug";
|
|
683
|
+
this.submitProxyEndpoint = options.submitProxyEndpoint;
|
|
684
|
+
this.createIssueProxyEndpoint = options.createIssueProxyEndpoint;
|
|
685
|
+
this.uploadAttachmentProxyEndpoint = options.uploadAttachmentProxyEndpoint;
|
|
686
|
+
this.fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
640
687
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (typeof window.matchMedia === "function") {
|
|
645
|
-
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
646
|
-
colorScheme = "dark";
|
|
647
|
-
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
648
|
-
colorScheme = "light";
|
|
688
|
+
async submit(payload, onProgress) {
|
|
689
|
+
if (this.submitProxyEndpoint) {
|
|
690
|
+
return this.submitViaProxy(payload, onProgress);
|
|
649
691
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return {
|
|
658
|
-
locale: typeof navigator.language === "string" ? navigator.language : null,
|
|
659
|
-
timezone,
|
|
660
|
-
language: typeof navigator.language === "string" ? navigator.language : null,
|
|
661
|
-
languages: Array.isArray(navigator.languages) ? [...navigator.languages] : [],
|
|
662
|
-
platform: typeof nav.userAgentData?.platform === "string" && nav.userAgentData.platform || (typeof navigator.platform === "string" ? navigator.platform : null),
|
|
663
|
-
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
664
|
-
colorScheme,
|
|
665
|
-
viewport: {
|
|
666
|
-
width: typeof window.innerWidth === "number" ? window.innerWidth : null,
|
|
667
|
-
height: typeof window.innerHeight === "number" ? window.innerHeight : null,
|
|
668
|
-
pixelRatio: typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : null
|
|
669
|
-
},
|
|
670
|
-
screen: {
|
|
671
|
-
width: typeof window.screen?.width === "number" ? window.screen.width : null,
|
|
672
|
-
height: typeof window.screen?.height === "number" ? window.screen.height : null,
|
|
673
|
-
availWidth: typeof window.screen?.availWidth === "number" ? window.screen.availWidth : null,
|
|
674
|
-
availHeight: typeof window.screen?.availHeight === "number" ? window.screen.availHeight : null,
|
|
675
|
-
colorDepth: typeof window.screen?.colorDepth === "number" ? window.screen.colorDepth : null
|
|
676
|
-
},
|
|
677
|
-
device: {
|
|
678
|
-
hardwareConcurrency: typeof navigator.hardwareConcurrency === "number" ? navigator.hardwareConcurrency : null,
|
|
679
|
-
deviceMemoryGb: typeof nav.deviceMemory === "number" ? nav.deviceMemory : null,
|
|
680
|
-
maxTouchPoints: typeof navigator.maxTouchPoints === "number" ? navigator.maxTouchPoints : null,
|
|
681
|
-
online: typeof navigator.onLine === "boolean" ? navigator.onLine : null,
|
|
682
|
-
cookieEnabled: typeof navigator.cookieEnabled === "boolean" ? navigator.cookieEnabled : null
|
|
683
|
-
},
|
|
684
|
-
connection: {
|
|
685
|
-
effectiveType: typeof connection?.effectiveType === "string" ? connection.effectiveType : null,
|
|
686
|
-
downlinkMbps: typeof connection?.downlink === "number" ? connection.downlink : null,
|
|
687
|
-
rttMs: typeof connection?.rtt === "number" ? connection.rtt : null,
|
|
688
|
-
saveData: typeof connection?.saveData === "boolean" ? connection.saveData : null
|
|
692
|
+
const progress = onProgress ?? noop2;
|
|
693
|
+
progress("Creating Jira issue\u2026");
|
|
694
|
+
const issue = await this.createIssue(payload);
|
|
695
|
+
progress("Uploading attachments\u2026");
|
|
696
|
+
const uploads = [];
|
|
697
|
+
if (payload.screenshotBlob) {
|
|
698
|
+
uploads.push(this.uploadAttachment(issue.key, payload.screenshotBlob, "bug-screenshot.png", "image/png"));
|
|
689
699
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
// src/core/BugSession.ts
|
|
694
|
-
var BugSession = class {
|
|
695
|
-
constructor(options = {}) {
|
|
696
|
-
this.recording = false;
|
|
697
|
-
this.startedAtMs = null;
|
|
698
|
-
this.autoStopTimeout = null;
|
|
699
|
-
this.stopInFlight = null;
|
|
700
|
-
this.lastArtifacts = null;
|
|
701
|
-
this.maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_RECORDING_MS;
|
|
702
|
-
this.screenRecorder = options.screenRecorder ?? new ScreenRecorder();
|
|
703
|
-
this.screenshotCapturer = options.screenshotCapturer ?? new ScreenshotCapturer();
|
|
704
|
-
this.networkLogger = options.networkLogger ?? new NetworkLogger();
|
|
705
|
-
this.onAutoStop = options.onAutoStop;
|
|
706
|
-
}
|
|
707
|
-
async start() {
|
|
708
|
-
if (this.recording) {
|
|
709
|
-
return;
|
|
700
|
+
if (payload.videoBlob) {
|
|
701
|
+
uploads.push(this.uploadAttachment(issue.key, payload.videoBlob, "bug-recording.webm", "video/webm"));
|
|
710
702
|
}
|
|
711
|
-
|
|
712
|
-
this.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
this.
|
|
723
|
-
this.networkLogger.clear();
|
|
724
|
-
throw error;
|
|
703
|
+
const logsBlob = new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" });
|
|
704
|
+
uploads.push(this.uploadAttachment(issue.key, logsBlob, "network-logs.txt", "text/plain"));
|
|
705
|
+
if (payload.consoleLogs.length > 0 || payload.jsErrors.length > 0) {
|
|
706
|
+
const consoleParts = [];
|
|
707
|
+
if (payload.jsErrors.length > 0) {
|
|
708
|
+
consoleParts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
|
|
709
|
+
}
|
|
710
|
+
if (payload.consoleLogs.length > 0) {
|
|
711
|
+
consoleParts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
|
|
712
|
+
}
|
|
713
|
+
const consoleBlob = new Blob([consoleParts.join("\n\n")], { type: "text/plain" });
|
|
714
|
+
uploads.push(this.uploadAttachment(issue.key, consoleBlob, "console-logs.txt", "text/plain"));
|
|
725
715
|
}
|
|
726
|
-
|
|
727
|
-
this.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
716
|
+
const metadataBlob = new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" });
|
|
717
|
+
uploads.push(this.uploadAttachment(issue.key, metadataBlob, "client-metadata.json", "application/json"));
|
|
718
|
+
await Promise.all(uploads);
|
|
719
|
+
progress("Done!");
|
|
720
|
+
return {
|
|
721
|
+
provider: this.provider,
|
|
722
|
+
issueId: issue.id,
|
|
723
|
+
issueKey: issue.key,
|
|
724
|
+
issueUrl: issue.url,
|
|
725
|
+
warnings: []
|
|
726
|
+
};
|
|
731
727
|
}
|
|
732
|
-
async
|
|
733
|
-
if (this.
|
|
734
|
-
|
|
728
|
+
async submitViaProxy(payload, onProgress) {
|
|
729
|
+
if (!this.submitProxyEndpoint) {
|
|
730
|
+
throw new Error("Jira submit proxy endpoint is not configured.");
|
|
735
731
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
this.
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const networkLogs = this.networkLogger.stop();
|
|
744
|
-
const stoppedAtMs = Date.now();
|
|
745
|
-
const artifacts = {
|
|
746
|
-
videoBlob: null,
|
|
747
|
-
screenshotBlob,
|
|
748
|
-
networkLogs,
|
|
749
|
-
captureMode: "screenshot",
|
|
750
|
-
startedAt: new Date(startedAtMs).toISOString(),
|
|
751
|
-
stoppedAt: new Date(stoppedAtMs).toISOString(),
|
|
752
|
-
elapsedMs: Math.max(0, stoppedAtMs - startedAtMs),
|
|
753
|
-
stopReason: "manual"
|
|
754
|
-
};
|
|
755
|
-
this.lastArtifacts = artifacts;
|
|
756
|
-
return artifacts;
|
|
757
|
-
} catch (error) {
|
|
758
|
-
this.networkLogger.stop();
|
|
759
|
-
this.networkLogger.clear();
|
|
760
|
-
throw error;
|
|
732
|
+
const formData = new FormData();
|
|
733
|
+
formData.set("provider", "jira");
|
|
734
|
+
formData.set("title", payload.title);
|
|
735
|
+
formData.set("description", buildCleanDescription2(payload));
|
|
736
|
+
formData.set("issueType", this.issueType);
|
|
737
|
+
if (this.projectKey) {
|
|
738
|
+
formData.set("projectKey", this.projectKey);
|
|
761
739
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (this.stopInFlight) {
|
|
765
|
-
return this.stopInFlight;
|
|
740
|
+
if (payload.screenshotBlob) {
|
|
741
|
+
formData.append("screenshotFile", toBlobFile(payload.screenshotBlob, "bug-screenshot.png", "image/png"), "bug-screenshot.png");
|
|
766
742
|
}
|
|
767
|
-
if (
|
|
768
|
-
|
|
743
|
+
if (payload.videoBlob) {
|
|
744
|
+
formData.append("screenRecordingFile", toBlobFile(payload.videoBlob, "bug-recording.webm", "video/webm"), "bug-recording.webm");
|
|
769
745
|
}
|
|
770
|
-
|
|
771
|
-
|
|
746
|
+
formData.append("networkLogsFile", new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }), "network-logs.txt");
|
|
747
|
+
formData.append("clientMetadataFile", new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }), "client-metadata.json");
|
|
748
|
+
if (payload.consoleLogs.length > 0 || payload.jsErrors.length > 0) {
|
|
749
|
+
const consoleParts = [];
|
|
750
|
+
if (payload.jsErrors.length > 0) {
|
|
751
|
+
consoleParts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
|
|
752
|
+
}
|
|
753
|
+
if (payload.consoleLogs.length > 0) {
|
|
754
|
+
consoleParts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
|
|
755
|
+
}
|
|
756
|
+
formData.append("consoleLogsFile", new Blob([consoleParts.join("\n\n")], { type: "text/plain" }), "console-logs.txt");
|
|
757
|
+
}
|
|
758
|
+
(onProgress ?? noop2)("Submitting to Jira\u2026");
|
|
759
|
+
const response = await this.fetchImpl(this.submitProxyEndpoint, {
|
|
760
|
+
method: "POST",
|
|
761
|
+
body: formData
|
|
772
762
|
});
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
getElapsedMs() {
|
|
779
|
-
if (this.recording && this.startedAtMs) {
|
|
780
|
-
return Math.max(0, Date.now() - this.startedAtMs);
|
|
763
|
+
const body = await response.json().catch(() => null);
|
|
764
|
+
if (!response.ok) {
|
|
765
|
+
const errorMessage = typeof body?.error === "string" ? body.error : body?.error?.message || "Jira proxy submission failed.";
|
|
766
|
+
throw new Error(errorMessage);
|
|
781
767
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
}
|
|
793
|
-
resetArtifacts() {
|
|
794
|
-
this.lastArtifacts = null;
|
|
795
|
-
this.screenRecorder.clearLastBlob();
|
|
796
|
-
this.networkLogger.clear();
|
|
797
|
-
}
|
|
798
|
-
async dispose() {
|
|
799
|
-
await this.stop("manual");
|
|
800
|
-
this.clearAutoStopTimer();
|
|
801
|
-
this.screenRecorder.dispose();
|
|
802
|
-
this.networkLogger.stop();
|
|
803
|
-
this.networkLogger.clear();
|
|
804
|
-
}
|
|
805
|
-
async stopInternal(reason) {
|
|
806
|
-
this.clearAutoStopTimer();
|
|
807
|
-
const startedAtMs = this.startedAtMs ?? Date.now();
|
|
808
|
-
this.recording = false;
|
|
809
|
-
this.startedAtMs = null;
|
|
810
|
-
const videoBlob = await this.screenRecorder.stop();
|
|
811
|
-
const networkLogs = this.networkLogger.stop();
|
|
812
|
-
const stoppedAtMs = Date.now();
|
|
813
|
-
const artifacts = {
|
|
814
|
-
videoBlob,
|
|
815
|
-
screenshotBlob: null,
|
|
816
|
-
networkLogs,
|
|
817
|
-
captureMode: "video",
|
|
818
|
-
startedAt: new Date(startedAtMs).toISOString(),
|
|
819
|
-
stoppedAt: new Date(stoppedAtMs).toISOString(),
|
|
820
|
-
elapsedMs: Math.max(0, stoppedAtMs - startedAtMs),
|
|
821
|
-
stopReason: reason
|
|
768
|
+
const jira = body?.jira;
|
|
769
|
+
if (!jira?.id || !jira.key) {
|
|
770
|
+
throw new Error("Jira proxy submission failed: invalid response.");
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
provider: this.provider,
|
|
774
|
+
issueId: jira.id,
|
|
775
|
+
issueKey: jira.key,
|
|
776
|
+
issueUrl: jira.url ?? null,
|
|
777
|
+
warnings: Array.isArray(body?.warnings) ? body.warnings : []
|
|
822
778
|
};
|
|
823
|
-
this.lastArtifacts = artifacts;
|
|
824
|
-
return artifacts;
|
|
825
779
|
}
|
|
826
|
-
async
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
780
|
+
async createIssue(payload) {
|
|
781
|
+
if (this.createIssueProxyEndpoint) {
|
|
782
|
+
const response2 = await this.fetchImpl(this.createIssueProxyEndpoint, {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: {
|
|
785
|
+
"Content-Type": "application/json"
|
|
786
|
+
},
|
|
787
|
+
body: JSON.stringify({
|
|
788
|
+
summary: payload.title,
|
|
789
|
+
description: buildCleanDescription2(payload),
|
|
790
|
+
issueType: this.issueType,
|
|
791
|
+
projectKey: this.projectKey
|
|
792
|
+
})
|
|
793
|
+
});
|
|
794
|
+
const data2 = await response2.json().catch(() => null);
|
|
795
|
+
if (!response2.ok || !data2?.id || !data2.key || !data2.url) {
|
|
796
|
+
throw new Error(data2?.error || "Jira issue creation proxy failed.");
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
id: data2.id,
|
|
800
|
+
key: data2.key,
|
|
801
|
+
url: data2.url
|
|
802
|
+
};
|
|
830
803
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
this.autoStopTimeout = null;
|
|
804
|
+
if (!this.baseUrl || !this.email || !this.apiToken || !this.projectKey) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
"Jira integration is missing credentials. Configure baseUrl + email + apiToken + projectKey or a createIssueProxyEndpoint."
|
|
807
|
+
);
|
|
836
808
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
809
|
+
const response = await this.fetchImpl(`${this.baseUrl}/rest/api/3/issue`, {
|
|
810
|
+
method: "POST",
|
|
811
|
+
credentials: "omit",
|
|
812
|
+
headers: {
|
|
813
|
+
Authorization: toBasicAuth(this.email, this.apiToken),
|
|
814
|
+
"Content-Type": "application/json",
|
|
815
|
+
Accept: "application/json"
|
|
816
|
+
},
|
|
817
|
+
body: JSON.stringify({
|
|
818
|
+
fields: {
|
|
819
|
+
project: { key: this.projectKey },
|
|
820
|
+
summary: payload.title,
|
|
821
|
+
description: toJiraAdf(buildCleanDescription2(payload)),
|
|
822
|
+
issuetype: { name: this.issueType }
|
|
823
|
+
}
|
|
824
|
+
})
|
|
847
825
|
});
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
async captureScreenshot(region) {
|
|
853
|
-
return this.session.captureScreenshot(region);
|
|
854
|
-
}
|
|
855
|
-
async stop() {
|
|
856
|
-
return this.session.stop("manual");
|
|
857
|
-
}
|
|
858
|
-
async submit(title, description, options = {}) {
|
|
859
|
-
if (this.isRecording()) {
|
|
860
|
-
await this.stop();
|
|
826
|
+
const data = await response.json().catch(() => null);
|
|
827
|
+
if (!response.ok) {
|
|
828
|
+
throw new Error(getErrorDetail(data, `Jira issue creation failed (${response.status}).`));
|
|
861
829
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
throw new Error("Capture a screenshot or record and stop a bug session before submitting.");
|
|
830
|
+
if (!isRecord2(data) || typeof data.id !== "string" || typeof data.key !== "string") {
|
|
831
|
+
throw new Error("Jira issue creation failed: invalid API response.");
|
|
865
832
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
833
|
+
return {
|
|
834
|
+
id: data.id,
|
|
835
|
+
key: data.key,
|
|
836
|
+
url: `${this.baseUrl}/browse/${data.key}`
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async uploadAttachment(issueKey, blob, fileName, fallbackMimeType) {
|
|
840
|
+
const file = toBlobFile(blob, fileName, fallbackMimeType);
|
|
841
|
+
if (this.uploadAttachmentProxyEndpoint) {
|
|
842
|
+
const formData2 = new FormData();
|
|
843
|
+
formData2.set("issueKey", issueKey);
|
|
844
|
+
formData2.set("file", file, file.name);
|
|
845
|
+
const response2 = await this.fetchImpl(this.uploadAttachmentProxyEndpoint, {
|
|
846
|
+
method: "POST",
|
|
847
|
+
body: formData2
|
|
848
|
+
});
|
|
849
|
+
if (!response2.ok) {
|
|
850
|
+
const data = await response2.json().catch(() => null);
|
|
851
|
+
throw new Error(data?.error || "Jira attachment upload proxy failed.");
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
869
854
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
855
|
+
if (!this.baseUrl || !this.email || !this.apiToken) {
|
|
856
|
+
throw new Error("Jira attachment upload requires credentials or uploadAttachmentProxyEndpoint.");
|
|
857
|
+
}
|
|
858
|
+
const formData = new FormData();
|
|
859
|
+
formData.set("file", file, file.name);
|
|
860
|
+
const response = await this.fetchImpl(`${this.baseUrl}/rest/api/3/issue/${issueKey}/attachments`, {
|
|
861
|
+
method: "POST",
|
|
862
|
+
credentials: "omit",
|
|
863
|
+
headers: {
|
|
864
|
+
Authorization: toBasicAuth(this.email, this.apiToken),
|
|
865
|
+
Accept: "application/json",
|
|
866
|
+
"X-Atlassian-Token": "no-check"
|
|
878
867
|
},
|
|
879
|
-
|
|
868
|
+
body: formData
|
|
869
|
+
});
|
|
870
|
+
if (!response.ok) {
|
|
871
|
+
const data = await response.json().catch(() => null);
|
|
872
|
+
throw new Error(getErrorDetail(data, `Jira attachment upload failed (${response.status}).`));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
var noop3 = () => {
|
|
877
|
+
};
|
|
878
|
+
var CloudIntegration = class {
|
|
879
|
+
constructor(options) {
|
|
880
|
+
this.provider = "cloud";
|
|
881
|
+
if (!options.projectKey) {
|
|
882
|
+
throw new Error("CloudIntegration: projectKey is required.");
|
|
883
|
+
}
|
|
884
|
+
this.projectKey = options.projectKey;
|
|
885
|
+
this.endpoint = options.endpoint ?? "/api/ingest";
|
|
886
|
+
this.fetchFn = options.fetchImpl ?? fetch.bind(globalThis);
|
|
887
|
+
}
|
|
888
|
+
async submit(payload, onProgress = noop3) {
|
|
889
|
+
onProgress("Preparing report\u2026");
|
|
890
|
+
const ua = payload.userAgent || navigator.userAgent;
|
|
891
|
+
const browserName = parseBrowserName(ua);
|
|
892
|
+
const osName = parseOsName(ua);
|
|
893
|
+
const body = {
|
|
894
|
+
project_key: this.projectKey,
|
|
895
|
+
title: payload.title,
|
|
896
|
+
provider: "cloud",
|
|
897
|
+
capture_mode: payload.captureMode,
|
|
898
|
+
has_screenshot: Boolean(payload.screenshotBlob),
|
|
899
|
+
has_video: Boolean(payload.videoBlob),
|
|
900
|
+
has_network_logs: payload.networkLogs.length > 0,
|
|
901
|
+
has_console_logs: payload.consoleLogs.length > 0,
|
|
902
|
+
js_error_count: payload.jsErrors.length,
|
|
903
|
+
user_agent: ua,
|
|
904
|
+
browser_name: browserName,
|
|
905
|
+
os_name: osName,
|
|
906
|
+
device_type: getDeviceType(),
|
|
907
|
+
screen_resolution: getScreenResolution(),
|
|
908
|
+
viewport: getViewport(),
|
|
909
|
+
color_scheme: payload.metadata.colorScheme !== "unknown" ? payload.metadata.colorScheme : null,
|
|
910
|
+
locale: payload.metadata.locale,
|
|
911
|
+
timezone: payload.metadata.timezone,
|
|
912
|
+
connection_type: payload.metadata.connection?.effectiveType ?? null,
|
|
913
|
+
page_url: payload.pageUrl,
|
|
914
|
+
environment: getEnvironment()
|
|
880
915
|
};
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
916
|
+
onProgress("Sending report\u2026");
|
|
917
|
+
const response = await this.fetchFn(this.endpoint, {
|
|
918
|
+
method: "POST",
|
|
919
|
+
headers: { "Content-Type": "application/json" },
|
|
920
|
+
body: JSON.stringify(body)
|
|
921
|
+
});
|
|
922
|
+
if (!response.ok) {
|
|
923
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
924
|
+
const message = errorBody.error ?? `HTTP ${response.status}`;
|
|
925
|
+
throw new Error(`CloudIntegration: ${message}`);
|
|
926
|
+
}
|
|
927
|
+
const result = await response.json();
|
|
928
|
+
onProgress("Report submitted.");
|
|
929
|
+
return {
|
|
930
|
+
provider: "cloud",
|
|
931
|
+
issueId: result.id,
|
|
932
|
+
issueKey: `QB-${result.id.slice(0, 8)}`,
|
|
933
|
+
issueUrl: null,
|
|
934
|
+
warnings: []
|
|
896
935
|
};
|
|
897
|
-
options.onProgress?.("Submitting to " + this.integration.provider + "\u2026");
|
|
898
|
-
const result = await this.integration.submit(payload, options.onProgress);
|
|
899
|
-
this.session.resetArtifacts();
|
|
900
|
-
return result;
|
|
901
|
-
}
|
|
902
|
-
isRecording() {
|
|
903
|
-
return this.session.isRecording();
|
|
904
|
-
}
|
|
905
|
-
getElapsedMs() {
|
|
906
|
-
return this.session.getElapsedMs();
|
|
907
|
-
}
|
|
908
|
-
getMaxDurationMs() {
|
|
909
|
-
return this.session.getMaxDurationMs();
|
|
910
|
-
}
|
|
911
|
-
getLastArtifacts() {
|
|
912
|
-
return this.session.getLastArtifacts();
|
|
913
|
-
}
|
|
914
|
-
clearDraft() {
|
|
915
|
-
this.session.resetArtifacts();
|
|
916
|
-
}
|
|
917
|
-
setIntegration(integration) {
|
|
918
|
-
this.integration = integration;
|
|
919
|
-
}
|
|
920
|
-
getSelectedProvider() {
|
|
921
|
-
return this.integration.provider;
|
|
922
|
-
}
|
|
923
|
-
async dispose() {
|
|
924
|
-
await this.session.dispose();
|
|
925
936
|
}
|
|
926
937
|
};
|
|
938
|
+
function parseBrowserName(ua) {
|
|
939
|
+
if (ua.includes("Firefox/")) return "Firefox";
|
|
940
|
+
if (ua.includes("Edg/")) return "Edge";
|
|
941
|
+
if (ua.includes("OPR/") || ua.includes("Opera/")) return "Opera";
|
|
942
|
+
if (ua.includes("Chrome/") && !ua.includes("Edg/")) return "Chrome";
|
|
943
|
+
if (ua.includes("Safari/") && !ua.includes("Chrome/")) return "Safari";
|
|
944
|
+
return "Unknown";
|
|
945
|
+
}
|
|
946
|
+
function parseOsName(ua) {
|
|
947
|
+
if (ua.includes("Windows")) return "Windows";
|
|
948
|
+
if (ua.includes("Mac OS")) return "macOS";
|
|
949
|
+
if (ua.includes("Linux")) return "Linux";
|
|
950
|
+
if (ua.includes("Android")) return "Android";
|
|
951
|
+
if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
|
|
952
|
+
return "Unknown";
|
|
953
|
+
}
|
|
954
|
+
function getDeviceType() {
|
|
955
|
+
if (typeof window === "undefined") return "unknown";
|
|
956
|
+
const w = window.innerWidth;
|
|
957
|
+
if (w < 768) return "mobile";
|
|
958
|
+
if (w < 1024) return "tablet";
|
|
959
|
+
return "desktop";
|
|
960
|
+
}
|
|
961
|
+
function getScreenResolution() {
|
|
962
|
+
if (typeof screen === "undefined") return "";
|
|
963
|
+
return `${screen.width}x${screen.height}`;
|
|
964
|
+
}
|
|
965
|
+
function getViewport() {
|
|
966
|
+
if (typeof window === "undefined") return "";
|
|
967
|
+
return `${window.innerWidth}x${window.innerHeight}`;
|
|
968
|
+
}
|
|
969
|
+
function getEnvironment() {
|
|
970
|
+
if (typeof window === "undefined") return "unknown";
|
|
971
|
+
const hostname = window.location.hostname;
|
|
972
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") return "development";
|
|
973
|
+
if (hostname.includes("staging") || hostname.includes("preview")) return "staging";
|
|
974
|
+
return "production";
|
|
975
|
+
}
|
|
927
976
|
|
|
928
|
-
// src/core/
|
|
929
|
-
var
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
977
|
+
// src/core/ScreenRecorder.ts
|
|
978
|
+
var MIME_TYPES = ["video/webm;codecs=vp9,opus", "video/webm;codecs=vp8,opus", "video/webm"];
|
|
979
|
+
function mapRecorderError(error) {
|
|
980
|
+
if (error instanceof DOMException) {
|
|
981
|
+
if (error.name === "NotAllowedError" || error.name === "SecurityError") {
|
|
982
|
+
return new Error("Screen or microphone permission was denied.");
|
|
983
|
+
}
|
|
984
|
+
if (error.name === "AbortError") {
|
|
985
|
+
return new Error("Recording setup was cancelled before it started.");
|
|
986
|
+
}
|
|
987
|
+
if (error.name === "NotFoundError") {
|
|
988
|
+
return new Error("No screen/tab or microphone source was found.");
|
|
989
|
+
}
|
|
990
|
+
if (error.name === "NotReadableError") {
|
|
991
|
+
return new Error("Unable to access the selected screen/tab or microphone.");
|
|
992
|
+
}
|
|
993
|
+
return new Error(error.message || "Failed to start recording.");
|
|
935
994
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return json.length > MAX_ARG_LENGTH ? json.slice(0, MAX_ARG_LENGTH) + "\u2026" : json;
|
|
939
|
-
} catch {
|
|
940
|
-
return String(arg);
|
|
995
|
+
if (error instanceof Error) {
|
|
996
|
+
return error;
|
|
941
997
|
}
|
|
998
|
+
return new Error("Failed to start recording.");
|
|
942
999
|
}
|
|
943
|
-
|
|
1000
|
+
function pickMimeType() {
|
|
1001
|
+
if (typeof MediaRecorder === "undefined") {
|
|
1002
|
+
return void 0;
|
|
1003
|
+
}
|
|
1004
|
+
return MIME_TYPES.find((mime) => MediaRecorder.isTypeSupported(mime));
|
|
1005
|
+
}
|
|
1006
|
+
var ScreenRecorder = class {
|
|
944
1007
|
constructor() {
|
|
945
|
-
this.
|
|
946
|
-
this.
|
|
947
|
-
this.
|
|
948
|
-
this.
|
|
949
|
-
this.
|
|
950
|
-
this.
|
|
1008
|
+
this.mediaRecorder = null;
|
|
1009
|
+
this.displayStream = null;
|
|
1010
|
+
this.microphoneStream = null;
|
|
1011
|
+
this.mixedStream = null;
|
|
1012
|
+
this.audioContext = null;
|
|
1013
|
+
this.displayAudioStream = null;
|
|
1014
|
+
this.microphoneAudioStream = null;
|
|
1015
|
+
this.chunks = [];
|
|
1016
|
+
this.recording = false;
|
|
1017
|
+
this.stopPromise = null;
|
|
1018
|
+
this.stopResolver = null;
|
|
1019
|
+
this.lastBlob = null;
|
|
1020
|
+
this.onEnded = null;
|
|
951
1021
|
}
|
|
952
|
-
start() {
|
|
953
|
-
if (this.
|
|
1022
|
+
async start(options = {}) {
|
|
1023
|
+
if (this.recording) {
|
|
954
1024
|
return;
|
|
955
1025
|
}
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
};
|
|
1026
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices || typeof navigator.mediaDevices.getDisplayMedia !== "function" || typeof navigator.mediaDevices.getUserMedia !== "function" || typeof MediaRecorder === "undefined") {
|
|
1027
|
+
throw new Error("This browser does not support screen and microphone recording.");
|
|
978
1028
|
}
|
|
979
|
-
this.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1029
|
+
this.cleanupStreams();
|
|
1030
|
+
this.resetStopPromise();
|
|
1031
|
+
this.chunks = [];
|
|
1032
|
+
this.lastBlob = null;
|
|
1033
|
+
this.onEnded = options.onEnded ?? null;
|
|
1034
|
+
try {
|
|
1035
|
+
const displayOptions = {
|
|
1036
|
+
video: {
|
|
1037
|
+
frameRate: { ideal: 15, max: 24 },
|
|
1038
|
+
width: { ideal: 1280, max: 1920 },
|
|
1039
|
+
height: { ideal: 720, max: 1080 }
|
|
1040
|
+
},
|
|
1041
|
+
audio: true,
|
|
1042
|
+
// Browser hints only; the user still decides what to share in the native prompt.
|
|
1043
|
+
preferCurrentTab: true,
|
|
1044
|
+
selfBrowserSurface: "include",
|
|
1045
|
+
surfaceSwitching: "include",
|
|
1046
|
+
monitorTypeSurfaces: "include"
|
|
1047
|
+
};
|
|
1048
|
+
const displayStream = await navigator.mediaDevices.getDisplayMedia(displayOptions);
|
|
1049
|
+
this.displayStream = displayStream;
|
|
1050
|
+
let microphoneStream;
|
|
1051
|
+
try {
|
|
1052
|
+
microphoneStream = await navigator.mediaDevices.getUserMedia({
|
|
1053
|
+
audio: {
|
|
1054
|
+
echoCancellation: true,
|
|
1055
|
+
noiseSuppression: true
|
|
1056
|
+
},
|
|
1057
|
+
video: false
|
|
989
1058
|
});
|
|
990
|
-
|
|
991
|
-
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
for (const track of displayStream.getTracks()) {
|
|
1061
|
+
track.stop();
|
|
992
1062
|
}
|
|
993
|
-
|
|
1063
|
+
throw error;
|
|
994
1064
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1065
|
+
this.microphoneStream = microphoneStream;
|
|
1066
|
+
const microphoneAudioTracks = microphoneStream.getAudioTracks();
|
|
1067
|
+
if (microphoneAudioTracks.length === 0) {
|
|
1068
|
+
for (const track of displayStream.getTracks()) {
|
|
1069
|
+
track.stop();
|
|
1070
|
+
}
|
|
1071
|
+
for (const track of microphoneStream.getTracks()) {
|
|
1072
|
+
track.stop();
|
|
1073
|
+
}
|
|
1074
|
+
throw new Error("Microphone audio track is unavailable.");
|
|
1075
|
+
}
|
|
1076
|
+
for (const track of microphoneAudioTracks) {
|
|
1077
|
+
track.enabled = true;
|
|
1078
|
+
}
|
|
1079
|
+
const mixedAudioTracks = await this.buildMixedAudioTracks(displayStream, microphoneStream);
|
|
1080
|
+
const tracks = [...displayStream.getVideoTracks(), ...mixedAudioTracks];
|
|
1081
|
+
if (tracks.length === 0) {
|
|
1082
|
+
for (const track of displayStream.getTracks()) {
|
|
1083
|
+
track.stop();
|
|
1008
1084
|
}
|
|
1085
|
+
for (const track of microphoneStream.getTracks()) {
|
|
1086
|
+
track.stop();
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error("No media tracks available for recording.");
|
|
1089
|
+
}
|
|
1090
|
+
this.mixedStream = new MediaStream(tracks);
|
|
1091
|
+
const mimeType = pickMimeType();
|
|
1092
|
+
const recorderOptions = {
|
|
1093
|
+
videoBitsPerSecond: 12e5,
|
|
1094
|
+
audioBitsPerSecond: 96e3
|
|
1095
|
+
};
|
|
1096
|
+
if (mimeType) {
|
|
1097
|
+
recorderOptions.mimeType = mimeType;
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
this.mediaRecorder = new MediaRecorder(this.mixedStream, recorderOptions);
|
|
1009
1101
|
} catch {
|
|
1102
|
+
if (mimeType) {
|
|
1103
|
+
try {
|
|
1104
|
+
this.mediaRecorder = new MediaRecorder(this.mixedStream, { mimeType });
|
|
1105
|
+
} catch {
|
|
1106
|
+
this.mediaRecorder = new MediaRecorder(this.mixedStream);
|
|
1107
|
+
}
|
|
1108
|
+
} else {
|
|
1109
|
+
this.mediaRecorder = new MediaRecorder(this.mixedStream);
|
|
1110
|
+
}
|
|
1010
1111
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1112
|
+
this.mediaRecorder.ondataavailable = (event) => {
|
|
1113
|
+
if (event.data && event.data.size > 0) {
|
|
1114
|
+
this.chunks.push(event.data);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
this.mediaRecorder.onstop = () => {
|
|
1118
|
+
const mime = this.mediaRecorder?.mimeType || mimeType || "video/webm";
|
|
1119
|
+
const blob = this.chunks.length > 0 ? new Blob(this.chunks, { type: mime }) : null;
|
|
1120
|
+
this.lastBlob = blob;
|
|
1121
|
+
this.recording = false;
|
|
1122
|
+
this.cleanupStreams();
|
|
1123
|
+
const resolve = this.stopResolver;
|
|
1124
|
+
this.resetStopPromise();
|
|
1125
|
+
resolve?.(blob);
|
|
1126
|
+
};
|
|
1127
|
+
for (const track of displayStream.getVideoTracks()) {
|
|
1128
|
+
track.onended = () => {
|
|
1129
|
+
if (!this.recording) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
void this.stop().finally(() => {
|
|
1133
|
+
this.onEnded?.();
|
|
1134
|
+
});
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
this.mediaRecorder.start(1e3);
|
|
1138
|
+
this.recording = true;
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
this.cleanupStreams();
|
|
1141
|
+
this.recording = false;
|
|
1142
|
+
throw mapRecorderError(error);
|
|
1143
|
+
}
|
|
1019
1144
|
}
|
|
1020
|
-
stop() {
|
|
1021
|
-
if (!this.
|
|
1022
|
-
return;
|
|
1145
|
+
async stop() {
|
|
1146
|
+
if (!this.recording && !this.stopPromise) {
|
|
1147
|
+
return this.lastBlob;
|
|
1023
1148
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
console[level] = original;
|
|
1027
|
-
}
|
|
1149
|
+
if (this.stopPromise) {
|
|
1150
|
+
return this.stopPromise;
|
|
1028
1151
|
}
|
|
1029
|
-
this.
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
this.
|
|
1152
|
+
if (!this.mediaRecorder) {
|
|
1153
|
+
this.recording = false;
|
|
1154
|
+
this.cleanupStreams();
|
|
1155
|
+
return this.lastBlob;
|
|
1033
1156
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1157
|
+
this.stopPromise = new Promise((resolve) => {
|
|
1158
|
+
this.stopResolver = resolve;
|
|
1159
|
+
});
|
|
1160
|
+
if (this.mediaRecorder.state === "inactive") {
|
|
1161
|
+
this.mediaRecorder.onstop?.(new Event("stop"));
|
|
1162
|
+
} else {
|
|
1163
|
+
this.mediaRecorder.stop();
|
|
1037
1164
|
}
|
|
1038
|
-
this.
|
|
1165
|
+
return this.stopPromise;
|
|
1039
1166
|
}
|
|
1040
|
-
|
|
1041
|
-
this.
|
|
1042
|
-
this.errors = [];
|
|
1167
|
+
isRecording() {
|
|
1168
|
+
return this.recording;
|
|
1043
1169
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
// src/integrations/linear.ts
|
|
1047
|
-
var DEFAULT_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
|
|
1048
|
-
var noop = () => {
|
|
1049
|
-
};
|
|
1050
|
-
function isRecord(value) {
|
|
1051
|
-
return typeof value === "object" && value !== null;
|
|
1052
|
-
}
|
|
1053
|
-
function parseErrorMessage(body, fallback) {
|
|
1054
|
-
if (!isRecord(body)) {
|
|
1055
|
-
return fallback;
|
|
1170
|
+
getLastBlob() {
|
|
1171
|
+
return this.lastBlob;
|
|
1056
1172
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
if (isRecord(entry) && typeof entry.message === "string") {
|
|
1060
|
-
return entry.message;
|
|
1061
|
-
}
|
|
1062
|
-
return "";
|
|
1063
|
-
}).filter(Boolean);
|
|
1064
|
-
if (messages.length > 0) {
|
|
1065
|
-
return messages.join("; ");
|
|
1066
|
-
}
|
|
1173
|
+
clearLastBlob() {
|
|
1174
|
+
this.lastBlob = null;
|
|
1067
1175
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
throw new Error(message);
|
|
1176
|
+
dispose() {
|
|
1177
|
+
this.cleanupStreams();
|
|
1178
|
+
this.recording = false;
|
|
1179
|
+
this.resetStopPromise();
|
|
1073
1180
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const lines = [
|
|
1078
|
-
payload.description,
|
|
1079
|
-
"",
|
|
1080
|
-
"### Context",
|
|
1081
|
-
`- Reported At: ${payload.stoppedAt}`,
|
|
1082
|
-
`- Capture Mode: ${payload.captureMode === "screenshot" ? "Screenshot" : "Video"}`,
|
|
1083
|
-
`- Page URL: ${payload.pageUrl || "Unknown"}`,
|
|
1084
|
-
""
|
|
1085
|
-
];
|
|
1086
|
-
if (mediaLinks.screenshotUrl || mediaLinks.recordingUrl) {
|
|
1087
|
-
lines.push("### Media");
|
|
1088
|
-
if (mediaLinks.screenshotUrl) {
|
|
1089
|
-
lines.push(`- Screenshot: [Open screenshot](${mediaLinks.screenshotUrl})`);
|
|
1181
|
+
cleanupStreams() {
|
|
1182
|
+
if (this.audioContext) {
|
|
1183
|
+
void this.audioContext.close().catch(() => void 0);
|
|
1090
1184
|
}
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1185
|
+
if (this.mixedStream) {
|
|
1186
|
+
for (const track of this.mixedStream.getTracks()) {
|
|
1187
|
+
track.stop();
|
|
1188
|
+
}
|
|
1093
1189
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
);
|
|
1099
|
-
return lines.join("\n");
|
|
1100
|
-
}
|
|
1101
|
-
var LinearIntegration = class {
|
|
1102
|
-
constructor(options) {
|
|
1103
|
-
this.provider = "linear";
|
|
1104
|
-
this.apiKey = options.apiKey;
|
|
1105
|
-
this.teamId = options.teamId;
|
|
1106
|
-
this.projectId = options.projectId;
|
|
1107
|
-
this.graphqlEndpoint = options.graphqlEndpoint ?? DEFAULT_GRAPHQL_ENDPOINT;
|
|
1108
|
-
this.submitProxyEndpoint = options.submitProxyEndpoint;
|
|
1109
|
-
this.createIssueProxyEndpoint = options.createIssueProxyEndpoint;
|
|
1110
|
-
this.uploadProxyEndpoint = options.uploadProxyEndpoint;
|
|
1111
|
-
this.fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
1112
|
-
}
|
|
1113
|
-
async submit(payload, onProgress) {
|
|
1114
|
-
if (this.submitProxyEndpoint) {
|
|
1115
|
-
return this.submitViaProxy(payload, onProgress);
|
|
1190
|
+
if (this.displayStream) {
|
|
1191
|
+
for (const track of this.displayStream.getTracks()) {
|
|
1192
|
+
track.stop();
|
|
1193
|
+
}
|
|
1116
1194
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
payload.screenshotBlob ? this.uploadAsset(payload.screenshotBlob, "bug-screenshot.png", "image/png") : Promise.resolve(null),
|
|
1121
|
-
payload.videoBlob ? this.uploadAsset(payload.videoBlob, "bug-recording.webm", "video/webm") : Promise.resolve(null)
|
|
1122
|
-
]);
|
|
1123
|
-
progress("Creating Linear issue\u2026");
|
|
1124
|
-
const description = buildCleanDescription(payload, { screenshotUrl, recordingUrl });
|
|
1125
|
-
const issue = await this.createIssue(payload.title, description);
|
|
1126
|
-
progress("Attaching logs\u2026");
|
|
1127
|
-
const comments = [];
|
|
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```");
|
|
1195
|
+
if (this.microphoneStream) {
|
|
1196
|
+
for (const track of this.microphoneStream.getTracks()) {
|
|
1197
|
+
track.stop();
|
|
1134
1198
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1199
|
+
}
|
|
1200
|
+
if (this.displayAudioStream) {
|
|
1201
|
+
for (const track of this.displayAudioStream.getTracks()) {
|
|
1202
|
+
track.stop();
|
|
1137
1203
|
}
|
|
1138
|
-
comments.push(this.addComment(issue.id, parts.join("\n\n")));
|
|
1139
1204
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1205
|
+
if (this.microphoneAudioStream) {
|
|
1206
|
+
for (const track of this.microphoneAudioStream.getTracks()) {
|
|
1207
|
+
track.stop();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
this.mediaRecorder = null;
|
|
1211
|
+
this.mixedStream = null;
|
|
1212
|
+
this.displayStream = null;
|
|
1213
|
+
this.microphoneStream = null;
|
|
1214
|
+
this.audioContext = null;
|
|
1215
|
+
this.displayAudioStream = null;
|
|
1216
|
+
this.microphoneAudioStream = null;
|
|
1151
1217
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1218
|
+
resetStopPromise() {
|
|
1219
|
+
this.stopPromise = null;
|
|
1220
|
+
this.stopResolver = null;
|
|
1221
|
+
}
|
|
1222
|
+
async buildMixedAudioTracks(displayStream, microphoneStream) {
|
|
1223
|
+
const displayAudioTracks = displayStream.getAudioTracks();
|
|
1224
|
+
const microphoneAudioTracks = microphoneStream.getAudioTracks();
|
|
1225
|
+
if (displayAudioTracks.length === 0 && microphoneAudioTracks.length === 0) {
|
|
1226
|
+
return [];
|
|
1155
1227
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
formData.set("title", payload.title);
|
|
1159
|
-
formData.set("description", buildCleanDescription(payload, { screenshotUrl: null, recordingUrl: null }));
|
|
1160
|
-
if (this.teamId) {
|
|
1161
|
-
formData.set("teamId", this.teamId);
|
|
1228
|
+
if (typeof AudioContext === "undefined") {
|
|
1229
|
+
return [...displayAudioTracks, ...microphoneAudioTracks];
|
|
1162
1230
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1231
|
+
const audioContext = new AudioContext();
|
|
1232
|
+
await audioContext.resume().catch(() => void 0);
|
|
1233
|
+
const destination = audioContext.createMediaStreamDestination();
|
|
1234
|
+
if (displayAudioTracks.length > 0) {
|
|
1235
|
+
this.displayAudioStream = new MediaStream(displayAudioTracks);
|
|
1236
|
+
const displaySource = audioContext.createMediaStreamSource(this.displayAudioStream);
|
|
1237
|
+
const displayGain = audioContext.createGain();
|
|
1238
|
+
displayGain.gain.value = 1;
|
|
1239
|
+
displaySource.connect(displayGain);
|
|
1240
|
+
displayGain.connect(destination);
|
|
1165
1241
|
}
|
|
1166
|
-
if (
|
|
1167
|
-
|
|
1242
|
+
if (microphoneAudioTracks.length > 0) {
|
|
1243
|
+
this.microphoneAudioStream = new MediaStream(microphoneAudioTracks);
|
|
1244
|
+
const microphoneSource = audioContext.createMediaStreamSource(this.microphoneAudioStream);
|
|
1245
|
+
const microphoneGain = audioContext.createGain();
|
|
1246
|
+
microphoneGain.gain.value = 1;
|
|
1247
|
+
microphoneSource.connect(microphoneGain);
|
|
1248
|
+
microphoneGain.connect(destination);
|
|
1168
1249
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1250
|
+
this.audioContext = audioContext;
|
|
1251
|
+
const mixedAudioTracks = destination.stream.getAudioTracks();
|
|
1252
|
+
if (mixedAudioTracks.length > 0) {
|
|
1253
|
+
return mixedAudioTracks;
|
|
1171
1254
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1255
|
+
return [...displayAudioTracks, ...microphoneAudioTracks];
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
var BUG_REPORTER_UI_ATTR = "data-bug-reporter-ui";
|
|
1259
|
+
var UNSUPPORTED_COLOR_FUNCTION_PATTERN = /\b(?:lab|lch|oklab|oklch|color)\([^)]*\)/gi;
|
|
1260
|
+
var COLOR_FALLBACK_VALUE = "rgb(17, 24, 39)";
|
|
1261
|
+
var DEFAULT_BACKGROUND_COLOR = "#ffffff";
|
|
1262
|
+
function replaceUnsupportedColorFunctions(value) {
|
|
1263
|
+
return value.replace(UNSUPPORTED_COLOR_FUNCTION_PATTERN, COLOR_FALLBACK_VALUE);
|
|
1264
|
+
}
|
|
1265
|
+
function unclampClonedLayout(clonedDoc) {
|
|
1266
|
+
for (const el of [clonedDoc.documentElement, clonedDoc.body]) {
|
|
1267
|
+
if (!el) {
|
|
1268
|
+
continue;
|
|
1183
1269
|
}
|
|
1184
|
-
(
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1270
|
+
el.style.setProperty("height", "auto", "important");
|
|
1271
|
+
el.style.setProperty("overflow", "visible", "important");
|
|
1272
|
+
}
|
|
1273
|
+
const overrideStyle = clonedDoc.createElement("style");
|
|
1274
|
+
overrideStyle.textContent = `html, body { height: auto !important; overflow: visible !important; }`;
|
|
1275
|
+
clonedDoc.head.appendChild(overrideStyle);
|
|
1276
|
+
}
|
|
1277
|
+
function sanitizeCloneForModernColors(clonedDocument) {
|
|
1278
|
+
const styleElements = clonedDocument.querySelectorAll("style");
|
|
1279
|
+
for (const styleElement of styleElements) {
|
|
1280
|
+
if (!styleElement.textContent) {
|
|
1281
|
+
continue;
|
|
1193
1282
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1283
|
+
styleElement.textContent = replaceUnsupportedColorFunctions(styleElement.textContent);
|
|
1284
|
+
}
|
|
1285
|
+
const styledElements = clonedDocument.querySelectorAll("[style]");
|
|
1286
|
+
for (const element of styledElements) {
|
|
1287
|
+
const style = element.getAttribute("style");
|
|
1288
|
+
if (!style) {
|
|
1289
|
+
continue;
|
|
1197
1290
|
}
|
|
1198
|
-
|
|
1199
|
-
provider: this.provider,
|
|
1200
|
-
issueId: linear.id,
|
|
1201
|
-
issueKey: linear.identifier,
|
|
1202
|
-
issueUrl: linear.url ?? null,
|
|
1203
|
-
warnings: Array.isArray(body?.warnings) ? body.warnings : []
|
|
1204
|
-
};
|
|
1291
|
+
element.setAttribute("style", replaceUnsupportedColorFunctions(style));
|
|
1205
1292
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const payload = await response2.json().catch(() => null);
|
|
1221
|
-
if (!response2.ok || !payload?.id || !payload.identifier) {
|
|
1222
|
-
throw new Error(payload?.error || "Linear issue creation proxy failed.");
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
id: payload.id,
|
|
1226
|
-
identifier: payload.identifier,
|
|
1227
|
-
url: payload.url ?? null
|
|
1228
|
-
};
|
|
1293
|
+
}
|
|
1294
|
+
async function canvasToPngBlob(canvas) {
|
|
1295
|
+
const blob = await new Promise((resolve) => {
|
|
1296
|
+
canvas.toBlob(resolve, "image/png", 1);
|
|
1297
|
+
});
|
|
1298
|
+
if (!blob) {
|
|
1299
|
+
throw new Error("Failed to generate screenshot image.");
|
|
1300
|
+
}
|
|
1301
|
+
return blob;
|
|
1302
|
+
}
|
|
1303
|
+
var ScreenshotCapturer = class {
|
|
1304
|
+
async capture() {
|
|
1305
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1306
|
+
throw new Error("Screenshot capture is not available in this environment.");
|
|
1229
1307
|
}
|
|
1230
|
-
|
|
1308
|
+
const target = document.documentElement;
|
|
1309
|
+
if (!target) {
|
|
1310
|
+
throw new Error("Could not find a capture target for screenshot.");
|
|
1311
|
+
}
|
|
1312
|
+
try {
|
|
1313
|
+
return await this.captureViaDomSnapshot(target);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
const message = error instanceof Error ? error.message : "Unknown DOM capture error.";
|
|
1231
1316
|
throw new Error(
|
|
1232
|
-
|
|
1317
|
+
`Quick screenshot failed in this browser (${message}). Try video capture for this page.`
|
|
1233
1318
|
);
|
|
1234
1319
|
}
|
|
1235
|
-
const query = `
|
|
1236
|
-
mutation IssueCreate($input: IssueCreateInput!) {
|
|
1237
|
-
issueCreate(input: $input) {
|
|
1238
|
-
success
|
|
1239
|
-
issue {
|
|
1240
|
-
id
|
|
1241
|
-
identifier
|
|
1242
|
-
url
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
`;
|
|
1247
|
-
const issueInput = {
|
|
1248
|
-
teamId: this.teamId,
|
|
1249
|
-
title,
|
|
1250
|
-
description
|
|
1251
|
-
};
|
|
1252
|
-
if (this.projectId) {
|
|
1253
|
-
issueInput.projectId = this.projectId;
|
|
1254
|
-
}
|
|
1255
|
-
const response = await this.fetchImpl(this.graphqlEndpoint, {
|
|
1256
|
-
method: "POST",
|
|
1257
|
-
headers: {
|
|
1258
|
-
Authorization: this.apiKey,
|
|
1259
|
-
"Content-Type": "application/json"
|
|
1260
|
-
},
|
|
1261
|
-
body: JSON.stringify({
|
|
1262
|
-
query,
|
|
1263
|
-
variables: {
|
|
1264
|
-
input: issueInput
|
|
1265
|
-
}
|
|
1266
|
-
})
|
|
1267
|
-
});
|
|
1268
|
-
const body = await response.json().catch(() => null);
|
|
1269
|
-
if (!response.ok) {
|
|
1270
|
-
throw new Error(parseErrorMessage(body, `Linear issue creation failed (${response.status})`));
|
|
1271
|
-
}
|
|
1272
|
-
const parsedIssueCreate = isRecord(body) && isRecord(body.data) && isRecord(body.data.issueCreate) ? body.data.issueCreate : null;
|
|
1273
|
-
const parsedIssue = parsedIssueCreate && isRecord(parsedIssueCreate.issue) ? parsedIssueCreate.issue : null;
|
|
1274
|
-
if (!parsedIssueCreate || !parsedIssue || parsedIssueCreate.success !== true) {
|
|
1275
|
-
throw new Error(parseErrorMessage(body, "Linear issue creation failed."));
|
|
1276
|
-
}
|
|
1277
|
-
return {
|
|
1278
|
-
id: ensureString(parsedIssue.id, "Linear did not return issue id."),
|
|
1279
|
-
identifier: ensureString(parsedIssue.identifier, "Linear did not return issue identifier."),
|
|
1280
|
-
url: typeof parsedIssue.url === "string" ? parsedIssue.url : null
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
async addComment(issueId, body) {
|
|
1284
|
-
if (!this.apiKey) {
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
const query = `
|
|
1288
|
-
mutation CommentCreate($input: CommentCreateInput!) {
|
|
1289
|
-
commentCreate(input: $input) {
|
|
1290
|
-
success
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
`;
|
|
1294
|
-
const response = await this.fetchImpl(this.graphqlEndpoint, {
|
|
1295
|
-
method: "POST",
|
|
1296
|
-
headers: {
|
|
1297
|
-
Authorization: this.apiKey,
|
|
1298
|
-
"Content-Type": "application/json"
|
|
1299
|
-
},
|
|
1300
|
-
body: JSON.stringify({
|
|
1301
|
-
query,
|
|
1302
|
-
variables: {
|
|
1303
|
-
input: { issueId, body }
|
|
1304
|
-
}
|
|
1305
|
-
})
|
|
1306
|
-
});
|
|
1307
|
-
if (!response.ok) ;
|
|
1308
1320
|
}
|
|
1309
|
-
async
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1321
|
+
async captureViaDomSnapshot(target) {
|
|
1322
|
+
const viewportWidth = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
|
|
1323
|
+
const viewportHeight = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
|
|
1324
|
+
const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
|
|
1325
|
+
const bodyBackgroundColor = window.getComputedStyle(document.body).backgroundColor;
|
|
1326
|
+
const backgroundColor = bodyBackgroundColor && bodyBackgroundColor !== "rgba(0, 0, 0, 0)" ? bodyBackgroundColor : DEFAULT_BACKGROUND_COLOR;
|
|
1327
|
+
const savedScrollX = window.scrollX;
|
|
1328
|
+
const savedScrollY = window.scrollY;
|
|
1329
|
+
window.scrollTo(0, 0);
|
|
1330
|
+
const htmlEl = document.documentElement;
|
|
1331
|
+
const bodyEl = document.body;
|
|
1332
|
+
const origHtmlHeight = htmlEl.style.height;
|
|
1333
|
+
const origHtmlOverflow = htmlEl.style.overflow;
|
|
1334
|
+
const origBodyHeight = bodyEl.style.height;
|
|
1335
|
+
const origBodyOverflow = bodyEl.style.overflow;
|
|
1336
|
+
htmlEl.style.setProperty("height", "auto", "important");
|
|
1337
|
+
htmlEl.style.setProperty("overflow", "visible", "important");
|
|
1338
|
+
bodyEl.style.setProperty("height", "auto", "important");
|
|
1339
|
+
bodyEl.style.setProperty("overflow", "visible", "important");
|
|
1340
|
+
const sharedOptions = {
|
|
1341
|
+
backgroundColor,
|
|
1342
|
+
logging: false,
|
|
1343
|
+
useCORS: true,
|
|
1344
|
+
allowTaint: false,
|
|
1345
|
+
scale,
|
|
1346
|
+
windowWidth: viewportWidth,
|
|
1347
|
+
windowHeight: viewportHeight,
|
|
1348
|
+
scrollX: 0,
|
|
1349
|
+
scrollY: 0,
|
|
1350
|
+
ignoreElements: (element) => {
|
|
1351
|
+
return element instanceof HTMLElement && element.getAttribute(BUG_REPORTER_UI_ATTR) === "true";
|
|
1333
1352
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
success
|
|
1353
|
-
uploadFile {
|
|
1354
|
-
uploadUrl
|
|
1355
|
-
assetUrl
|
|
1356
|
-
headers {
|
|
1357
|
-
key
|
|
1358
|
-
value
|
|
1353
|
+
};
|
|
1354
|
+
let lastError = null;
|
|
1355
|
+
const attempts = [
|
|
1356
|
+
{ foreignObjectRendering: true, sanitizeColorFunctions: false },
|
|
1357
|
+
{ foreignObjectRendering: false, sanitizeColorFunctions: false },
|
|
1358
|
+
{ foreignObjectRendering: false, sanitizeColorFunctions: true }
|
|
1359
|
+
];
|
|
1360
|
+
try {
|
|
1361
|
+
for (const attempt of attempts) {
|
|
1362
|
+
try {
|
|
1363
|
+
const fullCanvas = await html2canvas__default.default(target, {
|
|
1364
|
+
...sharedOptions,
|
|
1365
|
+
foreignObjectRendering: attempt.foreignObjectRendering,
|
|
1366
|
+
onclone: (clonedDocument) => {
|
|
1367
|
+
unclampClonedLayout(clonedDocument);
|
|
1368
|
+
if (attempt.sanitizeColorFunctions) {
|
|
1369
|
+
sanitizeCloneForModernColors(clonedDocument);
|
|
1370
|
+
}
|
|
1359
1371
|
}
|
|
1372
|
+
});
|
|
1373
|
+
const cropW = Math.round(viewportWidth * scale);
|
|
1374
|
+
const cropH = Math.round(viewportHeight * scale);
|
|
1375
|
+
const cropX = Math.round(savedScrollX * scale);
|
|
1376
|
+
const cropY = Math.round(savedScrollY * scale);
|
|
1377
|
+
const cropCanvas = document.createElement("canvas");
|
|
1378
|
+
cropCanvas.width = cropW;
|
|
1379
|
+
cropCanvas.height = cropH;
|
|
1380
|
+
const ctx = cropCanvas.getContext("2d");
|
|
1381
|
+
if (!ctx) {
|
|
1382
|
+
return await canvasToPngBlob(fullCanvas);
|
|
1360
1383
|
}
|
|
1384
|
+
ctx.drawImage(fullCanvas, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);
|
|
1385
|
+
return await canvasToPngBlob(cropCanvas);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
lastError = error;
|
|
1361
1388
|
}
|
|
1362
1389
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
body: JSON.stringify({
|
|
1371
|
-
query,
|
|
1372
|
-
variables: {
|
|
1373
|
-
contentType: file.type || "application/octet-stream",
|
|
1374
|
-
filename: file.name,
|
|
1375
|
-
size: file.size
|
|
1376
|
-
}
|
|
1377
|
-
})
|
|
1378
|
-
});
|
|
1379
|
-
const body = await response.json().catch(() => null);
|
|
1380
|
-
if (!response.ok) {
|
|
1381
|
-
throw new Error(parseErrorMessage(body, `Linear upload target request failed (${response.status})`));
|
|
1390
|
+
throw lastError ?? new Error("DOM snapshot capture failed.");
|
|
1391
|
+
} finally {
|
|
1392
|
+
htmlEl.style.height = origHtmlHeight;
|
|
1393
|
+
htmlEl.style.overflow = origHtmlOverflow;
|
|
1394
|
+
bodyEl.style.height = origBodyHeight;
|
|
1395
|
+
bodyEl.style.overflow = origBodyOverflow;
|
|
1396
|
+
window.scrollTo(savedScrollX, savedScrollY);
|
|
1382
1397
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1398
|
+
}
|
|
1399
|
+
async captureRegion(region) {
|
|
1400
|
+
const fullBlob = await this.capture();
|
|
1401
|
+
return this.cropBlob(fullBlob, region);
|
|
1402
|
+
}
|
|
1403
|
+
async cropBlob(blob, region) {
|
|
1404
|
+
const bitmap = await createImageBitmap(blob);
|
|
1405
|
+
const scale = bitmap.width / (window.innerWidth || 1);
|
|
1406
|
+
const sx = Math.round(region.x * scale);
|
|
1407
|
+
const sy = Math.round(region.y * scale);
|
|
1408
|
+
const sw = Math.round(region.width * scale);
|
|
1409
|
+
const sh = Math.round(region.height * scale);
|
|
1410
|
+
const canvas = document.createElement("canvas");
|
|
1411
|
+
canvas.width = Math.max(1, sw);
|
|
1412
|
+
canvas.height = Math.max(1, sh);
|
|
1413
|
+
const ctx = canvas.getContext("2d");
|
|
1414
|
+
if (!ctx) {
|
|
1415
|
+
bitmap.close();
|
|
1416
|
+
throw new Error("Could not create canvas for region crop.");
|
|
1389
1417
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
headers
|
|
1394
|
-
};
|
|
1418
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
1419
|
+
bitmap.close();
|
|
1420
|
+
return canvasToPngBlob(canvas);
|
|
1395
1421
|
}
|
|
1396
1422
|
};
|
|
1397
1423
|
|
|
1398
|
-
// src/
|
|
1399
|
-
var
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1424
|
+
// src/core/BugSession.ts
|
|
1425
|
+
var BugSession = class {
|
|
1426
|
+
constructor(options = {}) {
|
|
1427
|
+
this.recording = false;
|
|
1428
|
+
this.startedAtMs = null;
|
|
1429
|
+
this.autoStopTimeout = null;
|
|
1430
|
+
this.stopInFlight = null;
|
|
1431
|
+
this.lastArtifacts = null;
|
|
1432
|
+
this.maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_RECORDING_MS;
|
|
1433
|
+
this.screenRecorder = options.screenRecorder ?? new ScreenRecorder();
|
|
1434
|
+
this.screenshotCapturer = options.screenshotCapturer ?? new ScreenshotCapturer();
|
|
1435
|
+
this.networkLogger = options.networkLogger ?? new NetworkLogger();
|
|
1436
|
+
this.onAutoStop = options.onAutoStop;
|
|
1407
1437
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1438
|
+
async start() {
|
|
1439
|
+
if (this.recording) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
this.clearAutoStopTimer();
|
|
1443
|
+
this.networkLogger.clear();
|
|
1444
|
+
this.lastArtifacts = null;
|
|
1445
|
+
this.networkLogger.start();
|
|
1446
|
+
try {
|
|
1447
|
+
await this.screenRecorder.start({
|
|
1448
|
+
onEnded: () => {
|
|
1449
|
+
void this.handleForcedStop("screen_ended");
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
this.networkLogger.stop();
|
|
1454
|
+
this.networkLogger.clear();
|
|
1455
|
+
throw error;
|
|
1456
|
+
}
|
|
1457
|
+
this.recording = true;
|
|
1458
|
+
this.startedAtMs = Date.now();
|
|
1459
|
+
this.autoStopTimeout = setTimeout(() => {
|
|
1460
|
+
void this.handleForcedStop("time_limit");
|
|
1461
|
+
}, this.maxDurationMs);
|
|
1413
1462
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (isRecord2(payload.errors)) {
|
|
1418
|
-
return Object.values(payload.errors).map((entry) => String(entry)).join("; ");
|
|
1419
|
-
}
|
|
1420
|
-
return fallback;
|
|
1421
|
-
}
|
|
1422
|
-
function buildCleanDescription2(payload) {
|
|
1423
|
-
const lines = [
|
|
1424
|
-
payload.description,
|
|
1425
|
-
"",
|
|
1426
|
-
"Context:",
|
|
1427
|
-
`- Reported At: ${payload.stoppedAt}`,
|
|
1428
|
-
`- Capture Mode: ${payload.captureMode === "screenshot" ? "Screenshot" : "Video"}`,
|
|
1429
|
-
`- Page URL: ${payload.pageUrl || "Unknown"}`
|
|
1430
|
-
];
|
|
1431
|
-
const hasScreenshot = Boolean(payload.screenshotBlob);
|
|
1432
|
-
const hasVideo = Boolean(payload.videoBlob);
|
|
1433
|
-
if (hasScreenshot || hasVideo) {
|
|
1434
|
-
lines.push("", "Attachments:");
|
|
1435
|
-
if (hasScreenshot) lines.push("- Screenshot attached");
|
|
1436
|
-
if (hasVideo) lines.push("- Screen recording attached");
|
|
1437
|
-
lines.push("- Network logs attached (network-logs.txt)");
|
|
1438
|
-
lines.push("- Client metadata attached (client-metadata.json)");
|
|
1439
|
-
}
|
|
1440
|
-
return lines.join("\n");
|
|
1441
|
-
}
|
|
1442
|
-
function toJiraAdf(text) {
|
|
1443
|
-
const paragraphs = text.split(/\n{2,}/).map((chunk) => chunk.trim()).filter(Boolean).map((chunk) => ({
|
|
1444
|
-
type: "paragraph",
|
|
1445
|
-
content: [{ type: "text", text: chunk }]
|
|
1446
|
-
}));
|
|
1447
|
-
return {
|
|
1448
|
-
type: "doc",
|
|
1449
|
-
version: 1,
|
|
1450
|
-
content: paragraphs
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
var JiraIntegration = class {
|
|
1454
|
-
constructor(options) {
|
|
1455
|
-
this.provider = "jira";
|
|
1456
|
-
this.baseUrl = options.baseUrl?.replace(/\/$/, "");
|
|
1457
|
-
this.email = options.email;
|
|
1458
|
-
this.apiToken = options.apiToken;
|
|
1459
|
-
this.projectKey = options.projectKey;
|
|
1460
|
-
this.issueType = options.issueType ?? "Bug";
|
|
1461
|
-
this.submitProxyEndpoint = options.submitProxyEndpoint;
|
|
1462
|
-
this.createIssueProxyEndpoint = options.createIssueProxyEndpoint;
|
|
1463
|
-
this.uploadAttachmentProxyEndpoint = options.uploadAttachmentProxyEndpoint;
|
|
1464
|
-
this.fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
1465
|
-
}
|
|
1466
|
-
async submit(payload, onProgress) {
|
|
1467
|
-
if (this.submitProxyEndpoint) {
|
|
1468
|
-
return this.submitViaProxy(payload, onProgress);
|
|
1469
|
-
}
|
|
1470
|
-
const progress = onProgress ?? noop2;
|
|
1471
|
-
progress("Creating Jira issue\u2026");
|
|
1472
|
-
const issue = await this.createIssue(payload);
|
|
1473
|
-
progress("Uploading attachments\u2026");
|
|
1474
|
-
const uploads = [];
|
|
1475
|
-
if (payload.screenshotBlob) {
|
|
1476
|
-
uploads.push(this.uploadAttachment(issue.key, payload.screenshotBlob, "bug-screenshot.png", "image/png"));
|
|
1477
|
-
}
|
|
1478
|
-
if (payload.videoBlob) {
|
|
1479
|
-
uploads.push(this.uploadAttachment(issue.key, payload.videoBlob, "bug-recording.webm", "video/webm"));
|
|
1463
|
+
async captureScreenshot(region) {
|
|
1464
|
+
if (this.recording) {
|
|
1465
|
+
await this.stop("manual");
|
|
1480
1466
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1467
|
+
this.clearAutoStopTimer();
|
|
1468
|
+
this.networkLogger.clear();
|
|
1469
|
+
this.lastArtifacts = null;
|
|
1470
|
+
const startedAtMs = Date.now();
|
|
1471
|
+
this.networkLogger.start();
|
|
1472
|
+
try {
|
|
1473
|
+
const screenshotBlob = region ? await this.screenshotCapturer.captureRegion(region) : await this.screenshotCapturer.capture();
|
|
1474
|
+
const networkLogs = this.networkLogger.stop();
|
|
1475
|
+
const stoppedAtMs = Date.now();
|
|
1476
|
+
const artifacts = {
|
|
1477
|
+
videoBlob: null,
|
|
1478
|
+
screenshotBlob,
|
|
1479
|
+
networkLogs,
|
|
1480
|
+
captureMode: "screenshot",
|
|
1481
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1482
|
+
stoppedAt: new Date(stoppedAtMs).toISOString(),
|
|
1483
|
+
elapsedMs: Math.max(0, stoppedAtMs - startedAtMs),
|
|
1484
|
+
stopReason: "manual"
|
|
1485
|
+
};
|
|
1486
|
+
this.lastArtifacts = artifacts;
|
|
1487
|
+
return artifacts;
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
this.networkLogger.stop();
|
|
1490
|
+
this.networkLogger.clear();
|
|
1491
|
+
throw error;
|
|
1493
1492
|
}
|
|
1494
|
-
const metadataBlob = new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" });
|
|
1495
|
-
uploads.push(this.uploadAttachment(issue.key, metadataBlob, "client-metadata.json", "application/json"));
|
|
1496
|
-
await Promise.all(uploads);
|
|
1497
|
-
progress("Done!");
|
|
1498
|
-
return {
|
|
1499
|
-
provider: this.provider,
|
|
1500
|
-
issueId: issue.id,
|
|
1501
|
-
issueKey: issue.key,
|
|
1502
|
-
issueUrl: issue.url,
|
|
1503
|
-
warnings: []
|
|
1504
|
-
};
|
|
1505
1493
|
}
|
|
1506
|
-
async
|
|
1507
|
-
if (
|
|
1508
|
-
|
|
1509
|
-
}
|
|
1510
|
-
const formData = new FormData();
|
|
1511
|
-
formData.set("provider", "jira");
|
|
1512
|
-
formData.set("title", payload.title);
|
|
1513
|
-
formData.set("description", buildCleanDescription2(payload));
|
|
1514
|
-
formData.set("issueType", this.issueType);
|
|
1515
|
-
if (this.projectKey) {
|
|
1516
|
-
formData.set("projectKey", this.projectKey);
|
|
1517
|
-
}
|
|
1518
|
-
if (payload.screenshotBlob) {
|
|
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");
|
|
1494
|
+
async stop(reason = "manual") {
|
|
1495
|
+
if (this.stopInFlight) {
|
|
1496
|
+
return this.stopInFlight;
|
|
1523
1497
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
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");
|
|
1498
|
+
if (!this.recording) {
|
|
1499
|
+
return this.lastArtifacts;
|
|
1535
1500
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
method: "POST",
|
|
1539
|
-
body: formData
|
|
1501
|
+
this.stopInFlight = this.stopInternal(reason).finally(() => {
|
|
1502
|
+
this.stopInFlight = null;
|
|
1540
1503
|
});
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
if (
|
|
1548
|
-
|
|
1504
|
+
return this.stopInFlight;
|
|
1505
|
+
}
|
|
1506
|
+
isRecording() {
|
|
1507
|
+
return this.recording;
|
|
1508
|
+
}
|
|
1509
|
+
getElapsedMs() {
|
|
1510
|
+
if (this.recording && this.startedAtMs) {
|
|
1511
|
+
return Math.max(0, Date.now() - this.startedAtMs);
|
|
1549
1512
|
}
|
|
1550
|
-
return
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1513
|
+
return this.lastArtifacts?.elapsedMs ?? 0;
|
|
1514
|
+
}
|
|
1515
|
+
getMaxDurationMs() {
|
|
1516
|
+
return this.maxDurationMs;
|
|
1517
|
+
}
|
|
1518
|
+
getLastArtifacts() {
|
|
1519
|
+
return this.lastArtifacts;
|
|
1520
|
+
}
|
|
1521
|
+
getLastCaptureMode() {
|
|
1522
|
+
return this.lastArtifacts?.captureMode ?? null;
|
|
1523
|
+
}
|
|
1524
|
+
resetArtifacts() {
|
|
1525
|
+
this.lastArtifacts = null;
|
|
1526
|
+
this.screenRecorder.clearLastBlob();
|
|
1527
|
+
this.networkLogger.clear();
|
|
1528
|
+
}
|
|
1529
|
+
async dispose() {
|
|
1530
|
+
await this.stop("manual");
|
|
1531
|
+
this.clearAutoStopTimer();
|
|
1532
|
+
this.screenRecorder.dispose();
|
|
1533
|
+
this.networkLogger.stop();
|
|
1534
|
+
this.networkLogger.clear();
|
|
1535
|
+
}
|
|
1536
|
+
async stopInternal(reason) {
|
|
1537
|
+
this.clearAutoStopTimer();
|
|
1538
|
+
const startedAtMs = this.startedAtMs ?? Date.now();
|
|
1539
|
+
this.recording = false;
|
|
1540
|
+
this.startedAtMs = null;
|
|
1541
|
+
const videoBlob = await this.screenRecorder.stop();
|
|
1542
|
+
const networkLogs = this.networkLogger.stop();
|
|
1543
|
+
const stoppedAtMs = Date.now();
|
|
1544
|
+
const artifacts = {
|
|
1545
|
+
videoBlob,
|
|
1546
|
+
screenshotBlob: null,
|
|
1547
|
+
networkLogs,
|
|
1548
|
+
captureMode: "video",
|
|
1549
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1550
|
+
stoppedAt: new Date(stoppedAtMs).toISOString(),
|
|
1551
|
+
elapsedMs: Math.max(0, stoppedAtMs - startedAtMs),
|
|
1552
|
+
stopReason: reason
|
|
1556
1553
|
};
|
|
1554
|
+
this.lastArtifacts = artifacts;
|
|
1555
|
+
return artifacts;
|
|
1557
1556
|
}
|
|
1558
|
-
async
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
headers: {
|
|
1563
|
-
"Content-Type": "application/json"
|
|
1564
|
-
},
|
|
1565
|
-
body: JSON.stringify({
|
|
1566
|
-
summary: payload.title,
|
|
1567
|
-
description: buildCleanDescription2(payload),
|
|
1568
|
-
issueType: this.issueType,
|
|
1569
|
-
projectKey: this.projectKey
|
|
1570
|
-
})
|
|
1571
|
-
});
|
|
1572
|
-
const data2 = await response2.json().catch(() => null);
|
|
1573
|
-
if (!response2.ok || !data2?.id || !data2.key || !data2.url) {
|
|
1574
|
-
throw new Error(data2?.error || "Jira issue creation proxy failed.");
|
|
1575
|
-
}
|
|
1576
|
-
return {
|
|
1577
|
-
id: data2.id,
|
|
1578
|
-
key: data2.key,
|
|
1579
|
-
url: data2.url
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
if (!this.baseUrl || !this.email || !this.apiToken || !this.projectKey) {
|
|
1583
|
-
throw new Error(
|
|
1584
|
-
"Jira integration is missing credentials. Configure baseUrl + email + apiToken + projectKey or a createIssueProxyEndpoint."
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
|
-
const response = await this.fetchImpl(`${this.baseUrl}/rest/api/3/issue`, {
|
|
1588
|
-
method: "POST",
|
|
1589
|
-
credentials: "omit",
|
|
1590
|
-
headers: {
|
|
1591
|
-
Authorization: toBasicAuth(this.email, this.apiToken),
|
|
1592
|
-
"Content-Type": "application/json",
|
|
1593
|
-
Accept: "application/json"
|
|
1594
|
-
},
|
|
1595
|
-
body: JSON.stringify({
|
|
1596
|
-
fields: {
|
|
1597
|
-
project: { key: this.projectKey },
|
|
1598
|
-
summary: payload.title,
|
|
1599
|
-
description: toJiraAdf(buildCleanDescription2(payload)),
|
|
1600
|
-
issuetype: { name: this.issueType }
|
|
1601
|
-
}
|
|
1602
|
-
})
|
|
1603
|
-
});
|
|
1604
|
-
const data = await response.json().catch(() => null);
|
|
1605
|
-
if (!response.ok) {
|
|
1606
|
-
throw new Error(getErrorDetail(data, `Jira issue creation failed (${response.status}).`));
|
|
1557
|
+
async handleForcedStop(reason) {
|
|
1558
|
+
const artifacts = await this.stop(reason);
|
|
1559
|
+
if (artifacts && this.onAutoStop) {
|
|
1560
|
+
this.onAutoStop(artifacts);
|
|
1607
1561
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1562
|
+
}
|
|
1563
|
+
clearAutoStopTimer() {
|
|
1564
|
+
if (this.autoStopTimeout) {
|
|
1565
|
+
clearTimeout(this.autoStopTimeout);
|
|
1566
|
+
this.autoStopTimeout = null;
|
|
1610
1567
|
}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
// src/core/WebMetadata.ts
|
|
1572
|
+
function collectClientEnvironmentMetadata() {
|
|
1573
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
1611
1574
|
return {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1575
|
+
locale: null,
|
|
1576
|
+
timezone: null,
|
|
1577
|
+
language: null,
|
|
1578
|
+
languages: [],
|
|
1579
|
+
platform: null,
|
|
1580
|
+
referrer: null,
|
|
1581
|
+
colorScheme: "unknown",
|
|
1582
|
+
viewport: {
|
|
1583
|
+
width: null,
|
|
1584
|
+
height: null,
|
|
1585
|
+
pixelRatio: null
|
|
1586
|
+
},
|
|
1587
|
+
screen: {
|
|
1588
|
+
width: null,
|
|
1589
|
+
height: null,
|
|
1590
|
+
availWidth: null,
|
|
1591
|
+
availHeight: null,
|
|
1592
|
+
colorDepth: null
|
|
1593
|
+
},
|
|
1594
|
+
device: {
|
|
1595
|
+
hardwareConcurrency: null,
|
|
1596
|
+
deviceMemoryGb: null,
|
|
1597
|
+
maxTouchPoints: null,
|
|
1598
|
+
online: null,
|
|
1599
|
+
cookieEnabled: null
|
|
1600
|
+
},
|
|
1601
|
+
connection: {
|
|
1602
|
+
effectiveType: null,
|
|
1603
|
+
downlinkMbps: null,
|
|
1604
|
+
rttMs: null,
|
|
1605
|
+
saveData: null
|
|
1606
|
+
}
|
|
1615
1607
|
};
|
|
1616
1608
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
body: formData2
|
|
1626
|
-
});
|
|
1627
|
-
if (!response2.ok) {
|
|
1628
|
-
const data = await response2.json().catch(() => null);
|
|
1629
|
-
throw new Error(data?.error || "Jira attachment upload proxy failed.");
|
|
1630
|
-
}
|
|
1631
|
-
return;
|
|
1609
|
+
const nav = navigator;
|
|
1610
|
+
const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
1611
|
+
let colorScheme = "unknown";
|
|
1612
|
+
if (typeof window.matchMedia === "function") {
|
|
1613
|
+
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
1614
|
+
colorScheme = "dark";
|
|
1615
|
+
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
1616
|
+
colorScheme = "light";
|
|
1632
1617
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1618
|
+
}
|
|
1619
|
+
let timezone = null;
|
|
1620
|
+
try {
|
|
1621
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
1622
|
+
} catch {
|
|
1623
|
+
timezone = null;
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
locale: typeof navigator.language === "string" ? navigator.language : null,
|
|
1627
|
+
timezone,
|
|
1628
|
+
language: typeof navigator.language === "string" ? navigator.language : null,
|
|
1629
|
+
languages: Array.isArray(navigator.languages) ? [...navigator.languages] : [],
|
|
1630
|
+
platform: typeof nav.userAgentData?.platform === "string" && nav.userAgentData.platform || (typeof navigator.platform === "string" ? navigator.platform : null),
|
|
1631
|
+
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
1632
|
+
colorScheme,
|
|
1633
|
+
viewport: {
|
|
1634
|
+
width: typeof window.innerWidth === "number" ? window.innerWidth : null,
|
|
1635
|
+
height: typeof window.innerHeight === "number" ? window.innerHeight : null,
|
|
1636
|
+
pixelRatio: typeof window.devicePixelRatio === "number" ? window.devicePixelRatio : null
|
|
1637
|
+
},
|
|
1638
|
+
screen: {
|
|
1639
|
+
width: typeof window.screen?.width === "number" ? window.screen.width : null,
|
|
1640
|
+
height: typeof window.screen?.height === "number" ? window.screen.height : null,
|
|
1641
|
+
availWidth: typeof window.screen?.availWidth === "number" ? window.screen.availWidth : null,
|
|
1642
|
+
availHeight: typeof window.screen?.availHeight === "number" ? window.screen.availHeight : null,
|
|
1643
|
+
colorDepth: typeof window.screen?.colorDepth === "number" ? window.screen.colorDepth : null
|
|
1644
|
+
},
|
|
1645
|
+
device: {
|
|
1646
|
+
hardwareConcurrency: typeof navigator.hardwareConcurrency === "number" ? navigator.hardwareConcurrency : null,
|
|
1647
|
+
deviceMemoryGb: typeof nav.deviceMemory === "number" ? nav.deviceMemory : null,
|
|
1648
|
+
maxTouchPoints: typeof navigator.maxTouchPoints === "number" ? navigator.maxTouchPoints : null,
|
|
1649
|
+
online: typeof navigator.onLine === "boolean" ? navigator.onLine : null,
|
|
1650
|
+
cookieEnabled: typeof navigator.cookieEnabled === "boolean" ? navigator.cookieEnabled : null
|
|
1651
|
+
},
|
|
1652
|
+
connection: {
|
|
1653
|
+
effectiveType: typeof connection?.effectiveType === "string" ? connection.effectiveType : null,
|
|
1654
|
+
downlinkMbps: typeof connection?.downlink === "number" ? connection.downlink : null,
|
|
1655
|
+
rttMs: typeof connection?.rtt === "number" ? connection.rtt : null,
|
|
1656
|
+
saveData: typeof connection?.saveData === "boolean" ? connection.saveData : null
|
|
1635
1657
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
body: formData
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/core/BugReporter.ts
|
|
1662
|
+
var BugReporter = class {
|
|
1663
|
+
constructor(options) {
|
|
1664
|
+
this.integration = options.integration;
|
|
1665
|
+
this.session = options.session ?? new BugSession({
|
|
1666
|
+
maxDurationMs: options.maxDurationMs ?? DEFAULT_MAX_RECORDING_MS,
|
|
1667
|
+
onAutoStop: options.onAutoStop
|
|
1647
1668
|
});
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1669
|
+
}
|
|
1670
|
+
async start() {
|
|
1671
|
+
await this.session.start();
|
|
1672
|
+
}
|
|
1673
|
+
async captureScreenshot(region) {
|
|
1674
|
+
return this.session.captureScreenshot(region);
|
|
1675
|
+
}
|
|
1676
|
+
async stop() {
|
|
1677
|
+
return this.session.stop("manual");
|
|
1678
|
+
}
|
|
1679
|
+
async submit(title, description, options = {}) {
|
|
1680
|
+
if (this.isRecording()) {
|
|
1681
|
+
await this.stop();
|
|
1682
|
+
}
|
|
1683
|
+
const artifacts = this.session.getLastArtifacts();
|
|
1684
|
+
if (!artifacts) {
|
|
1685
|
+
throw new Error("Capture a screenshot or record and stop a bug session before submitting.");
|
|
1686
|
+
}
|
|
1687
|
+
const normalizedTitle = title.trim();
|
|
1688
|
+
if (!normalizedTitle) {
|
|
1689
|
+
throw new Error("A bug title is required.");
|
|
1651
1690
|
}
|
|
1691
|
+
const normalizedDescription = description.trim() || "No additional details provided.";
|
|
1692
|
+
const metadata = {
|
|
1693
|
+
...collectClientEnvironmentMetadata(),
|
|
1694
|
+
captureMode: artifacts.captureMode,
|
|
1695
|
+
capture: {
|
|
1696
|
+
startedAt: artifacts.startedAt,
|
|
1697
|
+
stoppedAt: artifacts.stoppedAt,
|
|
1698
|
+
elapsedMs: artifacts.elapsedMs
|
|
1699
|
+
},
|
|
1700
|
+
...options.metadata || {}
|
|
1701
|
+
};
|
|
1702
|
+
const payload = {
|
|
1703
|
+
title: normalizedTitle,
|
|
1704
|
+
description: normalizedDescription,
|
|
1705
|
+
videoBlob: artifacts.videoBlob,
|
|
1706
|
+
screenshotBlob: options.screenshotBlob ?? artifacts.screenshotBlob,
|
|
1707
|
+
networkLogs: artifacts.networkLogs,
|
|
1708
|
+
consoleLogs: options.consoleLogs ?? [],
|
|
1709
|
+
jsErrors: options.jsErrors ?? [],
|
|
1710
|
+
captureMode: artifacts.captureMode,
|
|
1711
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
1712
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1713
|
+
startedAt: artifacts.startedAt,
|
|
1714
|
+
stoppedAt: artifacts.stoppedAt,
|
|
1715
|
+
elapsedMs: artifacts.elapsedMs,
|
|
1716
|
+
metadata
|
|
1717
|
+
};
|
|
1718
|
+
options.onProgress?.("Submitting to " + this.integration.provider + "\u2026");
|
|
1719
|
+
const result = await this.integration.submit(payload, options.onProgress);
|
|
1720
|
+
this.session.resetArtifacts();
|
|
1721
|
+
return result;
|
|
1722
|
+
}
|
|
1723
|
+
isRecording() {
|
|
1724
|
+
return this.session.isRecording();
|
|
1725
|
+
}
|
|
1726
|
+
getElapsedMs() {
|
|
1727
|
+
return this.session.getElapsedMs();
|
|
1728
|
+
}
|
|
1729
|
+
getMaxDurationMs() {
|
|
1730
|
+
return this.session.getMaxDurationMs();
|
|
1731
|
+
}
|
|
1732
|
+
getLastArtifacts() {
|
|
1733
|
+
return this.session.getLastArtifacts();
|
|
1734
|
+
}
|
|
1735
|
+
clearDraft() {
|
|
1736
|
+
this.session.resetArtifacts();
|
|
1737
|
+
}
|
|
1738
|
+
setIntegration(integration) {
|
|
1739
|
+
this.integration = integration;
|
|
1740
|
+
}
|
|
1741
|
+
getSelectedProvider() {
|
|
1742
|
+
return this.integration.provider;
|
|
1743
|
+
}
|
|
1744
|
+
async dispose() {
|
|
1745
|
+
await this.session.dispose();
|
|
1652
1746
|
}
|
|
1653
1747
|
};
|
|
1654
1748
|
function RegionSelector({ onSelect, onCancel }) {
|
|
@@ -1766,7 +1860,10 @@ function RegionSelector({ onSelect, onCancel }) {
|
|
|
1766
1860
|
}
|
|
1767
1861
|
var BugReporterContext = react.createContext(null);
|
|
1768
1862
|
function getProviderLabel(provider) {
|
|
1769
|
-
|
|
1863
|
+
if (provider === "linear") return "Linear";
|
|
1864
|
+
if (provider === "jira") return "Jira";
|
|
1865
|
+
if (provider === "cloud") return "QuickBugs Cloud";
|
|
1866
|
+
return provider;
|
|
1770
1867
|
}
|
|
1771
1868
|
function BugReporterProvider({
|
|
1772
1869
|
children,
|
|
@@ -1784,7 +1881,7 @@ function BugReporterProvider({
|
|
|
1784
1881
|
const [error, setError] = react.useState(null);
|
|
1785
1882
|
const [success, setSuccess] = react.useState(null);
|
|
1786
1883
|
const [autoStopNotice, setAutoStopNotice] = react.useState(null);
|
|
1787
|
-
const [selectedProvider, setSelectedProvider] = react.useState(null);
|
|
1884
|
+
const [selectedProvider, setSelectedProvider] = react.useState(defaultProvider ?? null);
|
|
1788
1885
|
const [draftMode, setDraftMode] = react.useState(null);
|
|
1789
1886
|
const [videoBlob, setVideoBlob] = react.useState(null);
|
|
1790
1887
|
const [videoPreviewUrl, setVideoPreviewUrl] = react.useState(null);
|
|
@@ -1808,7 +1905,7 @@ function BugReporterProvider({
|
|
|
1808
1905
|
};
|
|
1809
1906
|
}, []);
|
|
1810
1907
|
const availableProviders = react.useMemo(() => {
|
|
1811
|
-
return ["linear", "jira"].filter((provider) => Boolean(integrations[provider]));
|
|
1908
|
+
return ["cloud", "linear", "jira"].filter((provider) => Boolean(integrations[provider]));
|
|
1812
1909
|
}, [integrations]);
|
|
1813
1910
|
const hasDraft = react.useMemo(() => {
|
|
1814
1911
|
if (draftMode === "video") {
|
|
@@ -2959,7 +3056,10 @@ function formatElapsed2(ms) {
|
|
|
2959
3056
|
return `${minutes}:${seconds}`;
|
|
2960
3057
|
}
|
|
2961
3058
|
function providerLabel(provider) {
|
|
2962
|
-
|
|
3059
|
+
if (provider === "linear") return "Linear";
|
|
3060
|
+
if (provider === "jira") return "Jira";
|
|
3061
|
+
if (provider === "cloud") return "QuickBugs Cloud";
|
|
3062
|
+
return provider;
|
|
2963
3063
|
}
|
|
2964
3064
|
function BugReporterModal() {
|
|
2965
3065
|
const {
|
|
@@ -3248,6 +3348,7 @@ exports.BugReporter = BugReporter;
|
|
|
3248
3348
|
exports.BugReporterModal = BugReporterModal;
|
|
3249
3349
|
exports.BugReporterProvider = BugReporterProvider;
|
|
3250
3350
|
exports.BugSession = BugSession;
|
|
3351
|
+
exports.CloudIntegration = CloudIntegration;
|
|
3251
3352
|
exports.ConsoleCapture = ConsoleCapture;
|
|
3252
3353
|
exports.DEFAULT_MAX_RECORDING_MS = DEFAULT_MAX_RECORDING_MS;
|
|
3253
3354
|
exports.FloatingBugButton = FloatingBugButton;
|