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