@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/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
+ };