@bbearai/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/dist/index.d.mts +434 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.js +705 -0
- package/dist/index.mjs +675 -0
- package/package.json +51 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
|
|
4
|
+
var getEnvVar = (key) => {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof process !== "undefined" && process.env) {
|
|
7
|
+
return process.env[key];
|
|
8
|
+
}
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
return void 0;
|
|
12
|
+
};
|
|
13
|
+
var HOSTED_BUGBEAR_ANON_KEY = getEnvVar("BUGBEAR_ANON_KEY") || getEnvVar("NEXT_PUBLIC_BUGBEAR_ANON_KEY") || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imt5eGd6am5xZ3ZhcHZsbnZxYXd6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNjgwNDIsImV4cCI6MjA4NDg0NDA0Mn0.NUkAlCHLFjeRoisbmNUVoGb4R6uQ8xs5LAEIX1BWTwU";
|
|
14
|
+
var BugBearClient = class {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.navigationHistory = [];
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.supabase = createClient(
|
|
19
|
+
config.supabaseUrl || DEFAULT_SUPABASE_URL,
|
|
20
|
+
config.supabaseAnonKey || HOSTED_BUGBEAR_ANON_KEY
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Track navigation for context
|
|
25
|
+
*/
|
|
26
|
+
trackNavigation(route) {
|
|
27
|
+
this.navigationHistory.push(route);
|
|
28
|
+
if (this.navigationHistory.length > 10) {
|
|
29
|
+
this.navigationHistory.shift();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get current navigation history
|
|
34
|
+
*/
|
|
35
|
+
getNavigationHistory() {
|
|
36
|
+
if (this.config.getNavigationHistory) {
|
|
37
|
+
return this.config.getNavigationHistory();
|
|
38
|
+
}
|
|
39
|
+
return [...this.navigationHistory];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get current user info from host app or BugBear's own auth
|
|
43
|
+
*/
|
|
44
|
+
async getCurrentUserInfo() {
|
|
45
|
+
if (this.config.getCurrentUser) {
|
|
46
|
+
return await this.config.getCurrentUser();
|
|
47
|
+
}
|
|
48
|
+
const { data: { user } } = await this.supabase.auth.getUser();
|
|
49
|
+
if (!user || !user.email) return null;
|
|
50
|
+
return {
|
|
51
|
+
id: user.id,
|
|
52
|
+
email: user.email
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Submit a report
|
|
57
|
+
*/
|
|
58
|
+
async submitReport(report) {
|
|
59
|
+
try {
|
|
60
|
+
const userInfo = await this.getCurrentUserInfo();
|
|
61
|
+
if (!userInfo) {
|
|
62
|
+
console.error("BugBear: No user info available, cannot submit report");
|
|
63
|
+
return { success: false, error: "User not authenticated" };
|
|
64
|
+
}
|
|
65
|
+
const testerInfo = await this.getTesterInfo();
|
|
66
|
+
const fullReport = {
|
|
67
|
+
project_id: this.config.projectId,
|
|
68
|
+
reporter_id: userInfo.id,
|
|
69
|
+
// User ID from host app (required)
|
|
70
|
+
tester_id: testerInfo?.id || null,
|
|
71
|
+
// Tester record ID (optional)
|
|
72
|
+
report_type: report.type,
|
|
73
|
+
title: report.title || this.generateTitle(report),
|
|
74
|
+
description: report.description,
|
|
75
|
+
severity: report.severity,
|
|
76
|
+
failed_at_step: report.failedAtStep,
|
|
77
|
+
voice_audio_url: report.voiceAudioUrl,
|
|
78
|
+
voice_transcript: report.voiceTranscript,
|
|
79
|
+
screenshot_urls: report.screenshots || [],
|
|
80
|
+
app_context: report.appContext,
|
|
81
|
+
device_info: report.deviceInfo || this.getDeviceInfo(),
|
|
82
|
+
navigation_history: this.getNavigationHistory(),
|
|
83
|
+
assignment_id: report.assignmentId,
|
|
84
|
+
test_case_id: report.testCaseId
|
|
85
|
+
};
|
|
86
|
+
const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
|
|
87
|
+
if (error) {
|
|
88
|
+
console.error("BugBear: Failed to submit report", error.message);
|
|
89
|
+
return { success: false, error: error.message };
|
|
90
|
+
}
|
|
91
|
+
if (this.config.onReportSubmitted) {
|
|
92
|
+
this.config.onReportSubmitted(report);
|
|
93
|
+
}
|
|
94
|
+
return { success: true, reportId: data.id };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
97
|
+
return { success: false, error: message };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get assigned tests for current user
|
|
102
|
+
* First looks up the tester by email, then fetches their assignments
|
|
103
|
+
*/
|
|
104
|
+
async getAssignedTests() {
|
|
105
|
+
try {
|
|
106
|
+
const testerInfo = await this.getTesterInfo();
|
|
107
|
+
if (!testerInfo) return [];
|
|
108
|
+
const { data, error } = await this.supabase.from("test_assignments").select(`
|
|
109
|
+
id,
|
|
110
|
+
status,
|
|
111
|
+
test_case:test_cases(
|
|
112
|
+
id,
|
|
113
|
+
title,
|
|
114
|
+
test_key,
|
|
115
|
+
description,
|
|
116
|
+
steps,
|
|
117
|
+
expected_result,
|
|
118
|
+
priority,
|
|
119
|
+
target_route,
|
|
120
|
+
track:qa_tracks(
|
|
121
|
+
id,
|
|
122
|
+
name,
|
|
123
|
+
icon,
|
|
124
|
+
color,
|
|
125
|
+
test_template,
|
|
126
|
+
rubric_mode,
|
|
127
|
+
description
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
`).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
|
|
131
|
+
if (error) {
|
|
132
|
+
console.error("BugBear: Failed to fetch assignments", error);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
return (data || []).map((item) => ({
|
|
136
|
+
id: item.id,
|
|
137
|
+
status: item.status,
|
|
138
|
+
testCase: {
|
|
139
|
+
id: item.test_case.id,
|
|
140
|
+
title: item.test_case.title,
|
|
141
|
+
testKey: item.test_case.test_key,
|
|
142
|
+
description: item.test_case.description,
|
|
143
|
+
steps: item.test_case.steps,
|
|
144
|
+
expectedResult: item.test_case.expected_result,
|
|
145
|
+
priority: item.test_case.priority,
|
|
146
|
+
targetRoute: item.test_case.target_route,
|
|
147
|
+
track: item.test_case.track ? {
|
|
148
|
+
id: item.test_case.track.id,
|
|
149
|
+
name: item.test_case.track.name,
|
|
150
|
+
icon: item.test_case.track.icon,
|
|
151
|
+
color: item.test_case.track.color,
|
|
152
|
+
testTemplate: item.test_case.track.test_template,
|
|
153
|
+
rubricMode: item.test_case.track.rubric_mode || "pass_fail",
|
|
154
|
+
description: item.test_case.track.description
|
|
155
|
+
} : void 0
|
|
156
|
+
}
|
|
157
|
+
}));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error("BugBear: Error fetching assignments", err);
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get current tester info
|
|
165
|
+
* Looks up tester by email from the host app's authenticated user
|
|
166
|
+
*/
|
|
167
|
+
async getTesterInfo() {
|
|
168
|
+
try {
|
|
169
|
+
const userInfo = await this.getCurrentUserInfo();
|
|
170
|
+
if (!userInfo) return null;
|
|
171
|
+
const { data, error } = await this.supabase.from("testers").select("*").eq("project_id", this.config.projectId).eq("email", userInfo.email).eq("status", "active").single();
|
|
172
|
+
if (error || !data) return null;
|
|
173
|
+
return {
|
|
174
|
+
id: data.id,
|
|
175
|
+
name: data.name,
|
|
176
|
+
email: data.email,
|
|
177
|
+
assignedTests: data.assigned_count || 0,
|
|
178
|
+
completedTests: data.completed_count || 0
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error("BugBear: getTesterInfo error", err);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check if current user is a tester for this project
|
|
187
|
+
*/
|
|
188
|
+
async isTester() {
|
|
189
|
+
const info = await this.getTesterInfo();
|
|
190
|
+
return info !== null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if QA mode is enabled for this project
|
|
194
|
+
* This is a master switch that admins can toggle in the dashboard
|
|
195
|
+
*/
|
|
196
|
+
async isQAEnabled() {
|
|
197
|
+
try {
|
|
198
|
+
const { data, error } = await this.supabase.from("projects").select("is_qa_enabled").eq("id", this.config.projectId).single();
|
|
199
|
+
if (error) {
|
|
200
|
+
if (error.code !== "PGRST116") {
|
|
201
|
+
console.warn("BugBear: Could not check QA status", error.message || error.code || "Unknown error");
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return data?.is_qa_enabled ?? true;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if the widget should be visible
|
|
212
|
+
* (QA mode enabled AND current user is a tester)
|
|
213
|
+
*/
|
|
214
|
+
async shouldShowWidget() {
|
|
215
|
+
const [qaEnabled, isTester] = await Promise.all([
|
|
216
|
+
this.isQAEnabled(),
|
|
217
|
+
this.isTester()
|
|
218
|
+
]);
|
|
219
|
+
return qaEnabled && isTester;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Upload a screenshot
|
|
223
|
+
*/
|
|
224
|
+
async uploadScreenshot(file, filename) {
|
|
225
|
+
try {
|
|
226
|
+
const name = filename || `screenshot-${Date.now()}.png`;
|
|
227
|
+
const path = `${this.config.projectId}/${name}`;
|
|
228
|
+
const { error } = await this.supabase.storage.from("screenshots").upload(path, file, {
|
|
229
|
+
contentType: "image/png",
|
|
230
|
+
upsert: false
|
|
231
|
+
});
|
|
232
|
+
if (error) {
|
|
233
|
+
console.error("BugBear: Failed to upload screenshot", error);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const { data: { publicUrl } } = this.supabase.storage.from("screenshots").getPublicUrl(path);
|
|
237
|
+
return publicUrl;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error("BugBear: Error uploading screenshot", err);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Generate a title from the report content
|
|
245
|
+
*/
|
|
246
|
+
generateTitle(report) {
|
|
247
|
+
const prefix = report.type === "bug" ? "\u{1F41B} Bug:" : report.type === "feedback" ? "\u{1F4A1} Feedback:" : report.type === "suggestion" ? "\u2728 Suggestion:" : report.type === "test_fail" ? "\u274C Test Failed:" : "\u2705 Test Passed:";
|
|
248
|
+
const desc = report.description || "";
|
|
249
|
+
const shortDesc = desc.length > 50 ? desc.slice(0, 50) + "..." : desc;
|
|
250
|
+
return `${prefix} ${shortDesc}`;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get device info (override in platform-specific implementations)
|
|
254
|
+
*/
|
|
255
|
+
getDeviceInfo() {
|
|
256
|
+
if (typeof window !== "undefined") {
|
|
257
|
+
return {
|
|
258
|
+
platform: "web",
|
|
259
|
+
userAgent: navigator.userAgent,
|
|
260
|
+
screenSize: {
|
|
261
|
+
width: window.screen.width,
|
|
262
|
+
height: window.screen.height
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { platform: "web" };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Create a fix request for Claude Code to pick up
|
|
270
|
+
* Called from the dashboard when user clicks "Send to Claude Code"
|
|
271
|
+
*/
|
|
272
|
+
async createFixRequest(request) {
|
|
273
|
+
try {
|
|
274
|
+
const userInfo = await this.getCurrentUserInfo();
|
|
275
|
+
const fixRequest = {
|
|
276
|
+
project_id: this.config.projectId,
|
|
277
|
+
title: request.title,
|
|
278
|
+
description: request.description || null,
|
|
279
|
+
prompt: request.prompt,
|
|
280
|
+
file_path: request.filePath || null,
|
|
281
|
+
report_id: request.reportId || null,
|
|
282
|
+
status: "pending",
|
|
283
|
+
created_by: userInfo?.id || null
|
|
284
|
+
};
|
|
285
|
+
const { data, error } = await this.supabase.from("fix_requests").insert(fixRequest).select("id").single();
|
|
286
|
+
if (error) {
|
|
287
|
+
console.error("BugBear: Failed to create fix request", error.message);
|
|
288
|
+
return { success: false, error: error.message };
|
|
289
|
+
}
|
|
290
|
+
return { success: true, fixRequestId: data.id };
|
|
291
|
+
} catch (err) {
|
|
292
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
293
|
+
return { success: false, error: message };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get pending fix requests for this project
|
|
298
|
+
* Useful for dashboard to show queue status
|
|
299
|
+
*/
|
|
300
|
+
async getFixRequests(options) {
|
|
301
|
+
try {
|
|
302
|
+
let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
|
|
303
|
+
if (options?.status) {
|
|
304
|
+
query = query.eq("status", options.status);
|
|
305
|
+
}
|
|
306
|
+
const { data, error } = await query;
|
|
307
|
+
if (error) {
|
|
308
|
+
console.error("BugBear: Failed to fetch fix requests", error);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
return (data || []).map((fr) => ({
|
|
312
|
+
id: fr.id,
|
|
313
|
+
title: fr.title,
|
|
314
|
+
description: fr.description,
|
|
315
|
+
status: fr.status,
|
|
316
|
+
claimedBy: fr.claimed_by,
|
|
317
|
+
claimedAt: fr.claimed_at,
|
|
318
|
+
completedAt: fr.completed_at,
|
|
319
|
+
createdAt: fr.created_at
|
|
320
|
+
}));
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error("BugBear: Error fetching fix requests", err);
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get threads visible to the current tester
|
|
328
|
+
* Includes threads where audience is 'all' or tester is in audience_tester_ids
|
|
329
|
+
*/
|
|
330
|
+
async getThreadsForTester() {
|
|
331
|
+
try {
|
|
332
|
+
const testerInfo = await this.getTesterInfo();
|
|
333
|
+
if (!testerInfo) return [];
|
|
334
|
+
const { data: threads, error } = await this.supabase.from("discussion_threads").select(`
|
|
335
|
+
id,
|
|
336
|
+
subject,
|
|
337
|
+
thread_type,
|
|
338
|
+
priority,
|
|
339
|
+
is_pinned,
|
|
340
|
+
is_resolved,
|
|
341
|
+
last_message_at,
|
|
342
|
+
created_at
|
|
343
|
+
`).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
|
|
344
|
+
if (error) {
|
|
345
|
+
console.error("BugBear: Failed to fetch threads", error);
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
if (!threads || threads.length === 0) return [];
|
|
349
|
+
const threadIds = threads.map((t) => t.id);
|
|
350
|
+
const { data: readStatuses } = await this.supabase.from("discussion_read_status").select("thread_id, last_read_at, last_read_message_id").eq("tester_id", testerInfo.id).in("thread_id", threadIds);
|
|
351
|
+
const readStatusMap = new Map(
|
|
352
|
+
(readStatuses || []).map((rs) => [rs.thread_id, rs])
|
|
353
|
+
);
|
|
354
|
+
const { data: lastMessages } = await this.supabase.from("discussion_messages").select(`
|
|
355
|
+
id,
|
|
356
|
+
thread_id,
|
|
357
|
+
sender_type,
|
|
358
|
+
sender_name,
|
|
359
|
+
content,
|
|
360
|
+
created_at,
|
|
361
|
+
attachments
|
|
362
|
+
`).in("thread_id", threadIds).order("created_at", { ascending: false });
|
|
363
|
+
const lastMessageMap = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const msg of lastMessages || []) {
|
|
365
|
+
if (!lastMessageMap.has(msg.thread_id)) {
|
|
366
|
+
lastMessageMap.set(msg.thread_id, msg);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const unreadCounts = await Promise.all(
|
|
370
|
+
threads.map(async (thread) => {
|
|
371
|
+
const readStatus = readStatusMap.get(thread.id);
|
|
372
|
+
const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
|
|
373
|
+
const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
|
|
374
|
+
return { threadId: thread.id, count: countError ? 0 : count || 0 };
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
const unreadCountMap = new Map(
|
|
378
|
+
unreadCounts.map((uc) => [uc.threadId, uc.count])
|
|
379
|
+
);
|
|
380
|
+
return threads.map((thread) => {
|
|
381
|
+
const lastMsg = lastMessageMap.get(thread.id);
|
|
382
|
+
return {
|
|
383
|
+
id: thread.id,
|
|
384
|
+
subject: thread.subject,
|
|
385
|
+
threadType: thread.thread_type,
|
|
386
|
+
priority: thread.priority,
|
|
387
|
+
isPinned: thread.is_pinned,
|
|
388
|
+
isResolved: thread.is_resolved,
|
|
389
|
+
lastMessageAt: thread.last_message_at,
|
|
390
|
+
createdAt: thread.created_at,
|
|
391
|
+
unreadCount: unreadCountMap.get(thread.id) || 0,
|
|
392
|
+
lastMessage: lastMsg ? {
|
|
393
|
+
id: lastMsg.id,
|
|
394
|
+
threadId: lastMsg.thread_id,
|
|
395
|
+
senderType: lastMsg.sender_type,
|
|
396
|
+
senderName: lastMsg.sender_name,
|
|
397
|
+
content: lastMsg.content,
|
|
398
|
+
createdAt: lastMsg.created_at,
|
|
399
|
+
attachments: lastMsg.attachments || []
|
|
400
|
+
} : void 0
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error("BugBear: Error fetching threads", err);
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get all messages in a thread
|
|
410
|
+
*/
|
|
411
|
+
async getThreadMessages(threadId) {
|
|
412
|
+
try {
|
|
413
|
+
const { data, error } = await this.supabase.from("discussion_messages").select(`
|
|
414
|
+
id,
|
|
415
|
+
thread_id,
|
|
416
|
+
sender_type,
|
|
417
|
+
sender_name,
|
|
418
|
+
content,
|
|
419
|
+
created_at,
|
|
420
|
+
attachments
|
|
421
|
+
`).eq("thread_id", threadId).order("created_at", { ascending: true });
|
|
422
|
+
if (error) {
|
|
423
|
+
console.error("BugBear: Failed to fetch messages", error);
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
return (data || []).map((msg) => ({
|
|
427
|
+
id: msg.id,
|
|
428
|
+
threadId: msg.thread_id,
|
|
429
|
+
senderType: msg.sender_type,
|
|
430
|
+
senderName: msg.sender_name,
|
|
431
|
+
content: msg.content,
|
|
432
|
+
createdAt: msg.created_at,
|
|
433
|
+
attachments: msg.attachments || []
|
|
434
|
+
}));
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error("BugBear: Error fetching messages", err);
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Send a message to a thread
|
|
442
|
+
*/
|
|
443
|
+
async sendMessage(threadId, content) {
|
|
444
|
+
try {
|
|
445
|
+
const testerInfo = await this.getTesterInfo();
|
|
446
|
+
if (!testerInfo) {
|
|
447
|
+
console.error("BugBear: No tester info, cannot send message");
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const { error } = await this.supabase.from("discussion_messages").insert({
|
|
451
|
+
thread_id: threadId,
|
|
452
|
+
sender_type: "tester",
|
|
453
|
+
sender_id: testerInfo.id,
|
|
454
|
+
sender_name: testerInfo.name,
|
|
455
|
+
content
|
|
456
|
+
});
|
|
457
|
+
if (error) {
|
|
458
|
+
console.error("BugBear: Failed to send message", error);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
|
|
462
|
+
return true;
|
|
463
|
+
} catch (err) {
|
|
464
|
+
console.error("BugBear: Error sending message", err);
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Mark a thread as read
|
|
470
|
+
*/
|
|
471
|
+
async markThreadAsRead(threadId) {
|
|
472
|
+
try {
|
|
473
|
+
const testerInfo = await this.getTesterInfo();
|
|
474
|
+
if (!testerInfo) return;
|
|
475
|
+
const { data: latestMsg } = await this.supabase.from("discussion_messages").select("id").eq("thread_id", threadId).order("created_at", { ascending: false }).limit(1).single();
|
|
476
|
+
await this.supabase.from("discussion_read_status").upsert({
|
|
477
|
+
thread_id: threadId,
|
|
478
|
+
tester_id: testerInfo.id,
|
|
479
|
+
last_read_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
480
|
+
last_read_message_id: latestMsg?.id || null
|
|
481
|
+
}, {
|
|
482
|
+
onConflict: "thread_id,tester_id"
|
|
483
|
+
});
|
|
484
|
+
} catch (err) {
|
|
485
|
+
console.error("BugBear: Error marking thread as read", err);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get total unread message count across all threads
|
|
490
|
+
*/
|
|
491
|
+
async getUnreadCount() {
|
|
492
|
+
try {
|
|
493
|
+
const threads = await this.getThreadsForTester();
|
|
494
|
+
return threads.reduce((sum, thread) => sum + thread.unreadCount, 0);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error("BugBear: Error getting unread count", err);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function createBugBear(config) {
|
|
502
|
+
return new BugBearClient(config);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/capture.ts
|
|
506
|
+
var MAX_CONSOLE_LOGS = 50;
|
|
507
|
+
var MAX_NETWORK_REQUESTS = 20;
|
|
508
|
+
var ContextCaptureManager = class {
|
|
509
|
+
constructor() {
|
|
510
|
+
this.consoleLogs = [];
|
|
511
|
+
this.networkRequests = [];
|
|
512
|
+
this.originalConsole = {};
|
|
513
|
+
this.isCapturing = false;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Start capturing console logs and network requests
|
|
517
|
+
*/
|
|
518
|
+
startCapture() {
|
|
519
|
+
if (this.isCapturing) return;
|
|
520
|
+
this.isCapturing = true;
|
|
521
|
+
this.captureConsole();
|
|
522
|
+
this.captureFetch();
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Stop capturing and restore original functions
|
|
526
|
+
*/
|
|
527
|
+
stopCapture() {
|
|
528
|
+
if (!this.isCapturing) return;
|
|
529
|
+
this.isCapturing = false;
|
|
530
|
+
if (this.originalConsole.log) console.log = this.originalConsole.log;
|
|
531
|
+
if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
|
|
532
|
+
if (this.originalConsole.error) console.error = this.originalConsole.error;
|
|
533
|
+
if (this.originalConsole.info) console.info = this.originalConsole.info;
|
|
534
|
+
if (this.originalFetch && typeof window !== "undefined") {
|
|
535
|
+
window.fetch = this.originalFetch;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get captured context for a bug report
|
|
540
|
+
*/
|
|
541
|
+
getEnhancedContext() {
|
|
542
|
+
return {
|
|
543
|
+
consoleLogs: [...this.consoleLogs],
|
|
544
|
+
networkRequests: [...this.networkRequests],
|
|
545
|
+
performanceMetrics: this.getPerformanceMetrics(),
|
|
546
|
+
environment: this.getEnvironmentInfo()
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Clear captured data
|
|
551
|
+
*/
|
|
552
|
+
clear() {
|
|
553
|
+
this.consoleLogs = [];
|
|
554
|
+
this.networkRequests = [];
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Add a log entry manually (for custom logging)
|
|
558
|
+
*/
|
|
559
|
+
addLog(level, message, args) {
|
|
560
|
+
this.consoleLogs.push({
|
|
561
|
+
level,
|
|
562
|
+
message,
|
|
563
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
564
|
+
args
|
|
565
|
+
});
|
|
566
|
+
if (this.consoleLogs.length > MAX_CONSOLE_LOGS) {
|
|
567
|
+
this.consoleLogs = this.consoleLogs.slice(-MAX_CONSOLE_LOGS);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Add a network request manually
|
|
572
|
+
*/
|
|
573
|
+
addNetworkRequest(request) {
|
|
574
|
+
this.networkRequests.push(request);
|
|
575
|
+
if (this.networkRequests.length > MAX_NETWORK_REQUESTS) {
|
|
576
|
+
this.networkRequests = this.networkRequests.slice(-MAX_NETWORK_REQUESTS);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
captureConsole() {
|
|
580
|
+
if (typeof console === "undefined") return;
|
|
581
|
+
const levels = ["log", "warn", "error", "info"];
|
|
582
|
+
levels.forEach((level) => {
|
|
583
|
+
this.originalConsole[level] = console[level];
|
|
584
|
+
console[level] = (...args) => {
|
|
585
|
+
this.originalConsole[level]?.apply(console, args);
|
|
586
|
+
try {
|
|
587
|
+
const message = args.map((arg) => {
|
|
588
|
+
if (typeof arg === "string") return arg;
|
|
589
|
+
try {
|
|
590
|
+
return JSON.stringify(arg);
|
|
591
|
+
} catch {
|
|
592
|
+
return String(arg);
|
|
593
|
+
}
|
|
594
|
+
}).join(" ");
|
|
595
|
+
this.addLog(level, message.slice(0, 500));
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
captureFetch() {
|
|
602
|
+
if (typeof window === "undefined" || typeof fetch === "undefined") return;
|
|
603
|
+
this.originalFetch = window.fetch;
|
|
604
|
+
const self = this;
|
|
605
|
+
window.fetch = async function(input, init) {
|
|
606
|
+
const startTime = Date.now();
|
|
607
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
608
|
+
const method = init?.method || "GET";
|
|
609
|
+
try {
|
|
610
|
+
const response = await self.originalFetch.call(window, input, init);
|
|
611
|
+
self.addNetworkRequest({
|
|
612
|
+
method,
|
|
613
|
+
url: url.slice(0, 200),
|
|
614
|
+
// Limit URL length
|
|
615
|
+
status: response.status,
|
|
616
|
+
duration: Date.now() - startTime,
|
|
617
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
618
|
+
});
|
|
619
|
+
return response;
|
|
620
|
+
} catch (error) {
|
|
621
|
+
self.addNetworkRequest({
|
|
622
|
+
method,
|
|
623
|
+
url: url.slice(0, 200),
|
|
624
|
+
duration: Date.now() - startTime,
|
|
625
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
626
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
627
|
+
});
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
getPerformanceMetrics() {
|
|
633
|
+
if (typeof window === "undefined" || typeof performance === "undefined") return void 0;
|
|
634
|
+
const metrics = {};
|
|
635
|
+
try {
|
|
636
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
637
|
+
if (navigation) {
|
|
638
|
+
metrics.pageLoadTime = Math.round(navigation.loadEventEnd - navigation.startTime);
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
const memory = performance.memory;
|
|
644
|
+
if (memory) {
|
|
645
|
+
metrics.memoryUsage = Math.round(memory.usedJSHeapSize / 1024 / 1024);
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
return Object.keys(metrics).length > 0 ? metrics : void 0;
|
|
650
|
+
}
|
|
651
|
+
getEnvironmentInfo() {
|
|
652
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return void 0;
|
|
653
|
+
return {
|
|
654
|
+
language: navigator.language,
|
|
655
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
656
|
+
cookiesEnabled: navigator.cookieEnabled,
|
|
657
|
+
localStorage: typeof localStorage !== "undefined",
|
|
658
|
+
online: navigator.onLine
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
var contextCapture = new ContextCaptureManager();
|
|
663
|
+
function captureError(error, errorInfo) {
|
|
664
|
+
return {
|
|
665
|
+
errorMessage: error.message,
|
|
666
|
+
errorStack: error.stack,
|
|
667
|
+
componentStack: errorInfo?.componentStack
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
export {
|
|
671
|
+
BugBearClient,
|
|
672
|
+
captureError,
|
|
673
|
+
contextCapture,
|
|
674
|
+
createBugBear
|
|
675
|
+
};
|