@bbearai/core 0.4.6 → 0.5.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/dist/index.js CHANGED
@@ -22,10 +22,13 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BUG_CATEGORIES: () => BUG_CATEGORIES,
24
24
  BugBearClient: () => BugBearClient,
25
+ LocalStorageAdapter: () => LocalStorageAdapter,
26
+ OfflineQueue: () => OfflineQueue,
25
27
  captureError: () => captureError,
26
28
  contextCapture: () => contextCapture,
27
29
  createBugBear: () => createBugBear,
28
- isBugCategory: () => isBugCategory
30
+ isBugCategory: () => isBugCategory,
31
+ isNetworkError: () => isNetworkError
29
32
  });
30
33
  module.exports = __toCommonJS(index_exports);
31
34
 
@@ -280,32 +283,312 @@ function captureError(error, errorInfo) {
280
283
  };
281
284
  }
282
285
 
286
+ // src/offline-queue.ts
287
+ var LocalStorageAdapter = class {
288
+ constructor() {
289
+ this.fallback = /* @__PURE__ */ new Map();
290
+ }
291
+ get isAvailable() {
292
+ try {
293
+ const key = "__bugbear_test__";
294
+ localStorage.setItem(key, "1");
295
+ localStorage.removeItem(key);
296
+ return true;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ async getItem(key) {
302
+ if (this.isAvailable) return localStorage.getItem(key);
303
+ return this.fallback.get(key) ?? null;
304
+ }
305
+ async setItem(key, value) {
306
+ if (this.isAvailable) {
307
+ localStorage.setItem(key, value);
308
+ } else {
309
+ this.fallback.set(key, value);
310
+ }
311
+ }
312
+ async removeItem(key) {
313
+ if (this.isAvailable) {
314
+ localStorage.removeItem(key);
315
+ } else {
316
+ this.fallback.delete(key);
317
+ }
318
+ }
319
+ };
320
+ var OfflineQueue = class {
321
+ constructor(config) {
322
+ this.items = [];
323
+ this.storageKey = "bugbear_offline_queue";
324
+ this.flushing = false;
325
+ this.handlers = /* @__PURE__ */ new Map();
326
+ this.maxItems = config.maxItems ?? 50;
327
+ this.maxRetries = config.maxRetries ?? 5;
328
+ this.storage = config.storage ?? new LocalStorageAdapter();
329
+ }
330
+ // ── Flush handler registration ──────────────────────────────
331
+ /** Register a handler that replays a queued operation. */
332
+ registerHandler(type, handler) {
333
+ this.handlers.set(type, handler);
334
+ }
335
+ // ── Change listener ─────────────────────────────────────────
336
+ /** Subscribe to queue count changes (for UI badges). */
337
+ onChange(callback) {
338
+ this.listener = callback;
339
+ }
340
+ notify() {
341
+ this.listener?.(this.items.length);
342
+ }
343
+ // ── Persistence ─────────────────────────────────────────────
344
+ /** Load queue from persistent storage. Call once after construction. */
345
+ async load() {
346
+ try {
347
+ const raw = await this.storage.getItem(this.storageKey);
348
+ if (raw) {
349
+ this.items = JSON.parse(raw);
350
+ }
351
+ } catch {
352
+ this.items = [];
353
+ }
354
+ this.notify();
355
+ }
356
+ async save() {
357
+ try {
358
+ await this.storage.setItem(this.storageKey, JSON.stringify(this.items));
359
+ } catch (err) {
360
+ console.error("BugBear: Failed to persist offline queue", err);
361
+ }
362
+ this.notify();
363
+ }
364
+ // ── Enqueue ─────────────────────────────────────────────────
365
+ /** Add a failed operation to the queue. Returns the item ID. */
366
+ async enqueue(type, payload) {
367
+ if (this.items.length >= this.maxItems) {
368
+ this.items.shift();
369
+ }
370
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
371
+ this.items.push({ id, type, payload, createdAt: Date.now(), retries: 0 });
372
+ await this.save();
373
+ return id;
374
+ }
375
+ // ── Accessors ───────────────────────────────────────────────
376
+ /** Number of items waiting to be flushed. */
377
+ get count() {
378
+ return this.items.length;
379
+ }
380
+ /** Read-only snapshot of pending items. */
381
+ get pending() {
382
+ return [...this.items];
383
+ }
384
+ /** Whether a flush is currently in progress. */
385
+ get isFlushing() {
386
+ return this.flushing;
387
+ }
388
+ // ── Flush ───────────────────────────────────────────────────
389
+ /**
390
+ * Process all queued items in FIFO order.
391
+ * Stops early if a network error is encountered (still offline).
392
+ */
393
+ async flush() {
394
+ if (this.flushing || this.items.length === 0) {
395
+ return { flushed: 0, failed: 0 };
396
+ }
397
+ this.flushing = true;
398
+ let flushed = 0;
399
+ let failed = 0;
400
+ const snapshot = [...this.items];
401
+ for (const item of snapshot) {
402
+ const handler = this.handlers.get(item.type);
403
+ if (!handler) {
404
+ failed++;
405
+ continue;
406
+ }
407
+ try {
408
+ const result = await handler(item.payload);
409
+ if (result.success) {
410
+ this.items = this.items.filter((i) => i.id !== item.id);
411
+ flushed++;
412
+ } else if (isNetworkError(result.error)) {
413
+ break;
414
+ } else {
415
+ const idx = this.items.findIndex((i) => i.id === item.id);
416
+ if (idx !== -1) {
417
+ this.items[idx].retries++;
418
+ if (this.items[idx].retries >= this.maxRetries) {
419
+ this.items.splice(idx, 1);
420
+ }
421
+ }
422
+ failed++;
423
+ }
424
+ } catch {
425
+ break;
426
+ }
427
+ }
428
+ await this.save();
429
+ this.flushing = false;
430
+ return { flushed, failed };
431
+ }
432
+ // ── Clear ───────────────────────────────────────────────────
433
+ /** Drop all queued items. */
434
+ async clear() {
435
+ this.items = [];
436
+ await this.save();
437
+ }
438
+ };
439
+ function isNetworkError(error) {
440
+ if (!error) return false;
441
+ const msg = error.toLowerCase();
442
+ return msg.includes("failed to fetch") || msg.includes("networkerror") || msg.includes("network request failed") || msg.includes("timeout") || msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("load failed") || // Safari
443
+ msg.includes("the internet connection appears to be offline") || msg.includes("a]server with the specified hostname could not be found");
444
+ }
445
+
283
446
  // src/client.ts
284
447
  var formatPgError = (e) => {
285
448
  if (!e || typeof e !== "object") return { raw: e };
286
449
  const { message, code, details, hint } = e;
287
450
  return { message, code, details, hint };
288
451
  };
289
- var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
290
- var getEnvVar = (key) => {
291
- try {
292
- if (typeof process !== "undefined" && process.env) {
293
- return process.env[key];
294
- }
295
- } catch {
296
- }
297
- return void 0;
298
- };
299
- var HOSTED_BUGBEAR_ANON_KEY = getEnvVar("BUGBEAR_ANON_KEY") || getEnvVar("NEXT_PUBLIC_BUGBEAR_ANON_KEY") || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imt5eGd6am5xZ3ZhcHZsbnZxYXd6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNjgwNDIsImV4cCI6MjA4NDg0NDA0Mn0.NUkAlCHLFjeRoisbmNUVoGb4R6uQ8xs5LAEIX1BWTwU";
300
452
  var BugBearClient = class {
301
453
  constructor(config) {
302
454
  this.navigationHistory = [];
303
455
  this.reportSubmitInFlight = false;
456
+ /** Offline queue — only created when config.offlineQueue.enabled is true. */
457
+ this._queue = null;
458
+ /** Active Realtime channel references for cleanup. */
459
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
460
+ this.realtimeChannels = [];
461
+ if (!config.supabaseUrl) {
462
+ throw new Error("BugBear: supabaseUrl is required. Get it from your BugBear project settings.");
463
+ }
464
+ if (!config.supabaseAnonKey) {
465
+ throw new Error("BugBear: supabaseAnonKey is required. Get it from your BugBear project settings.");
466
+ }
304
467
  this.config = config;
305
- this.supabase = (0, import_supabase_js.createClient)(
306
- config.supabaseUrl || DEFAULT_SUPABASE_URL,
307
- config.supabaseAnonKey || HOSTED_BUGBEAR_ANON_KEY
308
- );
468
+ this.supabase = (0, import_supabase_js.createClient)(config.supabaseUrl, config.supabaseAnonKey);
469
+ if (config.offlineQueue?.enabled) {
470
+ this._queue = new OfflineQueue({
471
+ enabled: true,
472
+ maxItems: config.offlineQueue.maxItems,
473
+ maxRetries: config.offlineQueue.maxRetries
474
+ });
475
+ this.registerQueueHandlers();
476
+ }
477
+ }
478
+ // ── Offline Queue ─────────────────────────────────────────
479
+ /**
480
+ * Access the offline queue (if enabled).
481
+ * Use this to check queue.count, subscribe to changes, or trigger flush.
482
+ */
483
+ get queue() {
484
+ return this._queue;
485
+ }
486
+ /**
487
+ * Initialize the offline queue with a platform-specific storage adapter.
488
+ * Must be called after construction for React Native (which supplies AsyncStorage).
489
+ * Web callers can skip this — LocalStorageAdapter is the default.
490
+ */
491
+ async initQueue(storage) {
492
+ if (!this._queue) return;
493
+ if (storage) {
494
+ this._queue = new OfflineQueue({
495
+ enabled: true,
496
+ maxItems: this.config.offlineQueue?.maxItems,
497
+ maxRetries: this.config.offlineQueue?.maxRetries,
498
+ storage
499
+ });
500
+ this.registerQueueHandlers();
501
+ }
502
+ await this._queue.load();
503
+ }
504
+ registerQueueHandlers() {
505
+ if (!this._queue) return;
506
+ this._queue.registerHandler("report", async (payload) => {
507
+ const { error } = await this.supabase.from("reports").insert(payload).select("id").single();
508
+ if (error) return { success: false, error: error.message };
509
+ return { success: true };
510
+ });
511
+ this._queue.registerHandler("message", async (payload) => {
512
+ const { error } = await this.supabase.from("discussion_messages").insert(payload);
513
+ if (error) return { success: false, error: error.message };
514
+ return { success: true };
515
+ });
516
+ this._queue.registerHandler("feedback", async (payload) => {
517
+ const { error } = await this.supabase.from("test_feedback").insert(payload);
518
+ if (error) return { success: false, error: error.message };
519
+ return { success: true };
520
+ });
521
+ }
522
+ // ── Realtime Subscriptions ─────────────────────────────────
523
+ /** Whether realtime is enabled in config. */
524
+ get realtimeEnabled() {
525
+ return !!this.config.realtime?.enabled;
526
+ }
527
+ /**
528
+ * Subscribe to postgres_changes on relevant tables.
529
+ * Each callback fires when the corresponding table has changes —
530
+ * the provider should call its refresh function in response.
531
+ * Returns a cleanup function that unsubscribes all channels.
532
+ */
533
+ subscribeToChanges(callbacks) {
534
+ this.unsubscribeAll();
535
+ const projectId = this.config.projectId;
536
+ const debounce = (fn, ms = 500) => {
537
+ let timer;
538
+ return () => {
539
+ clearTimeout(timer);
540
+ timer = setTimeout(fn, ms);
541
+ };
542
+ };
543
+ if (callbacks.onAssignmentChange) {
544
+ const debouncedCb = debounce(callbacks.onAssignmentChange);
545
+ const channel = this.supabase.channel("bugbear-assignments").on("postgres_changes", {
546
+ event: "*",
547
+ schema: "public",
548
+ table: "test_assignments",
549
+ filter: `project_id=eq.${projectId}`
550
+ }, debouncedCb).subscribe((status) => {
551
+ if (status === "CHANNEL_ERROR") {
552
+ console.warn("BugBear: Realtime subscription failed for test_assignments");
553
+ }
554
+ });
555
+ this.realtimeChannels.push(channel);
556
+ }
557
+ if (callbacks.onMessageChange) {
558
+ const debouncedCb = debounce(callbacks.onMessageChange);
559
+ const channel = this.supabase.channel("bugbear-messages").on("postgres_changes", {
560
+ event: "INSERT",
561
+ schema: "public",
562
+ table: "discussion_messages"
563
+ }, debouncedCb).subscribe((status) => {
564
+ if (status === "CHANNEL_ERROR") {
565
+ console.warn("BugBear: Realtime subscription failed for discussion_messages");
566
+ }
567
+ });
568
+ this.realtimeChannels.push(channel);
569
+ }
570
+ if (callbacks.onReportChange) {
571
+ const debouncedCb = debounce(callbacks.onReportChange);
572
+ const channel = this.supabase.channel("bugbear-reports").on("postgres_changes", {
573
+ event: "UPDATE",
574
+ schema: "public",
575
+ table: "reports",
576
+ filter: `project_id=eq.${projectId}`
577
+ }, debouncedCb).subscribe((status) => {
578
+ if (status === "CHANNEL_ERROR") {
579
+ console.warn("BugBear: Realtime subscription failed for reports");
580
+ }
581
+ });
582
+ this.realtimeChannels.push(channel);
583
+ }
584
+ return () => this.unsubscribeAll();
585
+ }
586
+ /** Remove all active Realtime channels. */
587
+ unsubscribeAll() {
588
+ for (const channel of this.realtimeChannels) {
589
+ this.supabase.removeChannel(channel);
590
+ }
591
+ this.realtimeChannels = [];
309
592
  }
310
593
  /**
311
594
  * Track navigation for context.
@@ -365,6 +648,7 @@ var BugBearClient = class {
365
648
  return { success: false, error: "A report is already being submitted" };
366
649
  }
367
650
  this.reportSubmitInFlight = true;
651
+ let fullReport;
368
652
  try {
369
653
  const validationError = this.validateReport(report);
370
654
  if (validationError) {
@@ -381,7 +665,7 @@ var BugBearClient = class {
381
665
  return { success: false, error: "User not authenticated" };
382
666
  }
383
667
  const testerInfo = await this.getTesterInfo();
384
- const fullReport = {
668
+ fullReport = {
385
669
  project_id: this.config.projectId,
386
670
  reporter_id: userInfo.id,
387
671
  // User ID from host app (required)
@@ -408,6 +692,10 @@ var BugBearClient = class {
408
692
  };
409
693
  const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
410
694
  if (error) {
695
+ if (this._queue && isNetworkError(error.message)) {
696
+ await this._queue.enqueue("report", fullReport);
697
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
698
+ }
411
699
  console.error("BugBear: Failed to submit report", error.message);
412
700
  return { success: false, error: error.message };
413
701
  }
@@ -417,6 +705,10 @@ var BugBearClient = class {
417
705
  return { success: true, reportId: data.id };
418
706
  } catch (err) {
419
707
  const message = err instanceof Error ? err.message : "Unknown error";
708
+ if (this._queue && fullReport && isNetworkError(message)) {
709
+ await this._queue.enqueue("report", fullReport);
710
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
711
+ }
420
712
  return { success: false, error: message };
421
713
  } finally {
422
714
  this.reportSubmitInFlight = false;
@@ -426,14 +718,18 @@ var BugBearClient = class {
426
718
  * Get assigned tests for current user
427
719
  * First looks up the tester by email, then fetches their assignments
428
720
  */
429
- async getAssignedTests() {
721
+ async getAssignedTests(options) {
430
722
  try {
431
723
  const testerInfo = await this.getTesterInfo();
432
724
  if (!testerInfo) return [];
433
- const { data, error } = await this.supabase.from("test_assignments").select(`
725
+ const pageSize = Math.min(options?.pageSize ?? 100, 100);
726
+ const from = (options?.page ?? 0) * pageSize;
727
+ const to = from + pageSize - 1;
728
+ const selectFields = `
434
729
  id,
435
730
  status,
436
731
  started_at,
732
+ completed_at,
437
733
  skip_reason,
438
734
  is_verification,
439
735
  original_report_id,
@@ -468,20 +764,24 @@ var BugBearClient = class {
468
764
  color,
469
765
  description,
470
766
  login_hint
471
- )
767
+ ),
768
+ platforms
472
769
  )
473
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
474
- if (error) {
475
- console.error("BugBear: Failed to fetch assignments", formatPgError(error));
770
+ `;
771
+ const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
772
+ const [pendingResult, completedResult] = await Promise.all([
773
+ this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).range(from, to),
774
+ this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["passed", "failed", "skipped", "blocked"]).gte("completed_at", twentyFourHoursAgo).order("completed_at", { ascending: false }).limit(50)
775
+ ]);
776
+ if (pendingResult.error) {
777
+ console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
476
778
  return [];
477
779
  }
478
- const mapped = (data || []).filter((item) => {
479
- if (!item.test_case) {
480
- console.warn("BugBear: Assignment returned without test_case", { id: item.id });
481
- return false;
482
- }
483
- return true;
484
- }).map((item) => ({
780
+ const allData = [
781
+ ...pendingResult.data || [],
782
+ ...completedResult.data || []
783
+ ];
784
+ const mapItem = (item) => ({
485
785
  id: item.id,
486
786
  status: item.status,
487
787
  startedAt: item.started_at,
@@ -519,12 +819,24 @@ var BugBearClient = class {
519
819
  color: item.test_case.role.color,
520
820
  description: item.test_case.role.description,
521
821
  loginHint: item.test_case.role.login_hint
522
- } : void 0
822
+ } : void 0,
823
+ platforms: item.test_case.platforms || void 0
523
824
  }
524
- }));
825
+ });
826
+ const mapped = allData.filter((item) => {
827
+ if (!item.test_case) {
828
+ console.warn("BugBear: Assignment returned without test_case", { id: item.id });
829
+ return false;
830
+ }
831
+ return true;
832
+ }).map(mapItem);
525
833
  mapped.sort((a, b) => {
526
834
  if (a.isVerification && !b.isVerification) return -1;
527
835
  if (!a.isVerification && b.isVerification) return 1;
836
+ const aActive = a.status === "pending" || a.status === "in_progress";
837
+ const bActive = b.status === "pending" || b.status === "in_progress";
838
+ if (aActive && !bActive) return -1;
839
+ if (!aActive && bActive) return 1;
528
840
  return 0;
529
841
  });
530
842
  return mapped;
@@ -689,6 +1001,36 @@ var BugBearClient = class {
689
1001
  async failAssignment(assignmentId) {
690
1002
  return this.updateAssignmentStatus(assignmentId, "failed");
691
1003
  }
1004
+ /**
1005
+ * Reopen a completed assignment — sets it back to in_progress with a fresh timer.
1006
+ * Clears completed_at and duration_seconds so it can be re-evaluated.
1007
+ */
1008
+ async reopenAssignment(assignmentId) {
1009
+ try {
1010
+ const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
1011
+ if (fetchError || !current) {
1012
+ return { success: false, error: "Assignment not found" };
1013
+ }
1014
+ if (current.status === "pending" || current.status === "in_progress") {
1015
+ return { success: true };
1016
+ }
1017
+ const { error } = await this.supabase.from("test_assignments").update({
1018
+ status: "in_progress",
1019
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1020
+ completed_at: null,
1021
+ duration_seconds: null,
1022
+ skip_reason: null
1023
+ }).eq("id", assignmentId).eq("status", current.status);
1024
+ if (error) {
1025
+ console.error("BugBear: Failed to reopen assignment", error);
1026
+ return { success: false, error: error.message };
1027
+ }
1028
+ return { success: true };
1029
+ } catch (err) {
1030
+ const message = err instanceof Error ? err.message : "Unknown error";
1031
+ return { success: false, error: message };
1032
+ }
1033
+ }
692
1034
  /**
693
1035
  * Skip a test assignment with a required reason
694
1036
  * Marks the assignment as 'skipped' and records why it was skipped
@@ -729,6 +1071,7 @@ var BugBearClient = class {
729
1071
  * This empowers testers to shape better tests over time
730
1072
  */
731
1073
  async submitTestFeedback(options) {
1074
+ let feedbackPayload;
732
1075
  try {
733
1076
  const testerInfo = await this.getTesterInfo();
734
1077
  if (!testerInfo) {
@@ -748,7 +1091,7 @@ var BugBearClient = class {
748
1091
  return { success: false, error: `${name} must be between 1 and 5` };
749
1092
  }
750
1093
  }
751
- const { error: feedbackError } = await this.supabase.from("test_feedback").insert({
1094
+ feedbackPayload = {
752
1095
  project_id: this.config.projectId,
753
1096
  test_case_id: testCaseId,
754
1097
  assignment_id: assignmentId || null,
@@ -766,8 +1109,13 @@ var BugBearClient = class {
766
1109
  platform: this.getDeviceInfo().platform,
767
1110
  time_to_complete_seconds: timeToCompleteSeconds || null,
768
1111
  screenshot_urls: screenshotUrls || []
769
- });
1112
+ };
1113
+ const { error: feedbackError } = await this.supabase.from("test_feedback").insert(feedbackPayload);
770
1114
  if (feedbackError) {
1115
+ if (this._queue && isNetworkError(feedbackError.message)) {
1116
+ await this._queue.enqueue("feedback", feedbackPayload);
1117
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1118
+ }
771
1119
  console.error("BugBear: Failed to submit feedback", feedbackError);
772
1120
  return { success: false, error: feedbackError.message };
773
1121
  }
@@ -784,6 +1132,10 @@ var BugBearClient = class {
784
1132
  return { success: true };
785
1133
  } catch (err) {
786
1134
  const message = err instanceof Error ? err.message : "Unknown error";
1135
+ if (this._queue && feedbackPayload && isNetworkError(message)) {
1136
+ await this._queue.enqueue("feedback", feedbackPayload);
1137
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1138
+ }
787
1139
  console.error("BugBear: Error submitting feedback", err);
788
1140
  return { success: false, error: message };
789
1141
  }
@@ -1418,6 +1770,7 @@ var BugBearClient = class {
1418
1770
  * Send a message to a thread
1419
1771
  */
1420
1772
  async sendMessage(threadId, content, attachments) {
1773
+ let insertData;
1421
1774
  try {
1422
1775
  const testerInfo = await this.getTesterInfo();
1423
1776
  if (!testerInfo) {
@@ -1429,7 +1782,7 @@ var BugBearClient = class {
1429
1782
  console.error("BugBear: Rate limit exceeded for messages");
1430
1783
  return false;
1431
1784
  }
1432
- const insertData = {
1785
+ insertData = {
1433
1786
  thread_id: threadId,
1434
1787
  sender_type: "tester",
1435
1788
  sender_tester_id: testerInfo.id,
@@ -1444,12 +1797,21 @@ var BugBearClient = class {
1444
1797
  }
1445
1798
  const { error } = await this.supabase.from("discussion_messages").insert(insertData);
1446
1799
  if (error) {
1800
+ if (this._queue && isNetworkError(error.message)) {
1801
+ await this._queue.enqueue("message", insertData);
1802
+ return false;
1803
+ }
1447
1804
  console.error("BugBear: Failed to send message", formatPgError(error));
1448
1805
  return false;
1449
1806
  }
1450
1807
  await this.markThreadAsRead(threadId);
1451
1808
  return true;
1452
1809
  } catch (err) {
1810
+ const message = err instanceof Error ? err.message : "Unknown error";
1811
+ if (this._queue && insertData && isNetworkError(message)) {
1812
+ await this._queue.enqueue("message", insertData);
1813
+ return false;
1814
+ }
1453
1815
  console.error("BugBear: Error sending message", err);
1454
1816
  return false;
1455
1817
  }
@@ -1786,8 +2148,11 @@ function createBugBear(config) {
1786
2148
  0 && (module.exports = {
1787
2149
  BUG_CATEGORIES,
1788
2150
  BugBearClient,
2151
+ LocalStorageAdapter,
2152
+ OfflineQueue,
1789
2153
  captureError,
1790
2154
  contextCapture,
1791
2155
  createBugBear,
1792
- isBugCategory
2156
+ isBugCategory,
2157
+ isNetworkError
1793
2158
  });