@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.mjs CHANGED
@@ -249,32 +249,312 @@ function captureError(error, errorInfo) {
249
249
  };
250
250
  }
251
251
 
252
+ // src/offline-queue.ts
253
+ var LocalStorageAdapter = class {
254
+ constructor() {
255
+ this.fallback = /* @__PURE__ */ new Map();
256
+ }
257
+ get isAvailable() {
258
+ try {
259
+ const key = "__bugbear_test__";
260
+ localStorage.setItem(key, "1");
261
+ localStorage.removeItem(key);
262
+ return true;
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+ async getItem(key) {
268
+ if (this.isAvailable) return localStorage.getItem(key);
269
+ return this.fallback.get(key) ?? null;
270
+ }
271
+ async setItem(key, value) {
272
+ if (this.isAvailable) {
273
+ localStorage.setItem(key, value);
274
+ } else {
275
+ this.fallback.set(key, value);
276
+ }
277
+ }
278
+ async removeItem(key) {
279
+ if (this.isAvailable) {
280
+ localStorage.removeItem(key);
281
+ } else {
282
+ this.fallback.delete(key);
283
+ }
284
+ }
285
+ };
286
+ var OfflineQueue = class {
287
+ constructor(config) {
288
+ this.items = [];
289
+ this.storageKey = "bugbear_offline_queue";
290
+ this.flushing = false;
291
+ this.handlers = /* @__PURE__ */ new Map();
292
+ this.maxItems = config.maxItems ?? 50;
293
+ this.maxRetries = config.maxRetries ?? 5;
294
+ this.storage = config.storage ?? new LocalStorageAdapter();
295
+ }
296
+ // ── Flush handler registration ──────────────────────────────
297
+ /** Register a handler that replays a queued operation. */
298
+ registerHandler(type, handler) {
299
+ this.handlers.set(type, handler);
300
+ }
301
+ // ── Change listener ─────────────────────────────────────────
302
+ /** Subscribe to queue count changes (for UI badges). */
303
+ onChange(callback) {
304
+ this.listener = callback;
305
+ }
306
+ notify() {
307
+ this.listener?.(this.items.length);
308
+ }
309
+ // ── Persistence ─────────────────────────────────────────────
310
+ /** Load queue from persistent storage. Call once after construction. */
311
+ async load() {
312
+ try {
313
+ const raw = await this.storage.getItem(this.storageKey);
314
+ if (raw) {
315
+ this.items = JSON.parse(raw);
316
+ }
317
+ } catch {
318
+ this.items = [];
319
+ }
320
+ this.notify();
321
+ }
322
+ async save() {
323
+ try {
324
+ await this.storage.setItem(this.storageKey, JSON.stringify(this.items));
325
+ } catch (err) {
326
+ console.error("BugBear: Failed to persist offline queue", err);
327
+ }
328
+ this.notify();
329
+ }
330
+ // ── Enqueue ─────────────────────────────────────────────────
331
+ /** Add a failed operation to the queue. Returns the item ID. */
332
+ async enqueue(type, payload) {
333
+ if (this.items.length >= this.maxItems) {
334
+ this.items.shift();
335
+ }
336
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
337
+ this.items.push({ id, type, payload, createdAt: Date.now(), retries: 0 });
338
+ await this.save();
339
+ return id;
340
+ }
341
+ // ── Accessors ───────────────────────────────────────────────
342
+ /** Number of items waiting to be flushed. */
343
+ get count() {
344
+ return this.items.length;
345
+ }
346
+ /** Read-only snapshot of pending items. */
347
+ get pending() {
348
+ return [...this.items];
349
+ }
350
+ /** Whether a flush is currently in progress. */
351
+ get isFlushing() {
352
+ return this.flushing;
353
+ }
354
+ // ── Flush ───────────────────────────────────────────────────
355
+ /**
356
+ * Process all queued items in FIFO order.
357
+ * Stops early if a network error is encountered (still offline).
358
+ */
359
+ async flush() {
360
+ if (this.flushing || this.items.length === 0) {
361
+ return { flushed: 0, failed: 0 };
362
+ }
363
+ this.flushing = true;
364
+ let flushed = 0;
365
+ let failed = 0;
366
+ const snapshot = [...this.items];
367
+ for (const item of snapshot) {
368
+ const handler = this.handlers.get(item.type);
369
+ if (!handler) {
370
+ failed++;
371
+ continue;
372
+ }
373
+ try {
374
+ const result = await handler(item.payload);
375
+ if (result.success) {
376
+ this.items = this.items.filter((i) => i.id !== item.id);
377
+ flushed++;
378
+ } else if (isNetworkError(result.error)) {
379
+ break;
380
+ } else {
381
+ const idx = this.items.findIndex((i) => i.id === item.id);
382
+ if (idx !== -1) {
383
+ this.items[idx].retries++;
384
+ if (this.items[idx].retries >= this.maxRetries) {
385
+ this.items.splice(idx, 1);
386
+ }
387
+ }
388
+ failed++;
389
+ }
390
+ } catch {
391
+ break;
392
+ }
393
+ }
394
+ await this.save();
395
+ this.flushing = false;
396
+ return { flushed, failed };
397
+ }
398
+ // ── Clear ───────────────────────────────────────────────────
399
+ /** Drop all queued items. */
400
+ async clear() {
401
+ this.items = [];
402
+ await this.save();
403
+ }
404
+ };
405
+ function isNetworkError(error) {
406
+ if (!error) return false;
407
+ const msg = error.toLowerCase();
408
+ 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
409
+ msg.includes("the internet connection appears to be offline") || msg.includes("a]server with the specified hostname could not be found");
410
+ }
411
+
252
412
  // src/client.ts
253
413
  var formatPgError = (e) => {
254
414
  if (!e || typeof e !== "object") return { raw: e };
255
415
  const { message, code, details, hint } = e;
256
416
  return { message, code, details, hint };
257
417
  };
258
- var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
259
- var getEnvVar = (key) => {
260
- try {
261
- if (typeof process !== "undefined" && process.env) {
262
- return process.env[key];
263
- }
264
- } catch {
265
- }
266
- return void 0;
267
- };
268
- var HOSTED_BUGBEAR_ANON_KEY = getEnvVar("BUGBEAR_ANON_KEY") || getEnvVar("NEXT_PUBLIC_BUGBEAR_ANON_KEY") || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imt5eGd6am5xZ3ZhcHZsbnZxYXd6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkyNjgwNDIsImV4cCI6MjA4NDg0NDA0Mn0.NUkAlCHLFjeRoisbmNUVoGb4R6uQ8xs5LAEIX1BWTwU";
269
418
  var BugBearClient = class {
270
419
  constructor(config) {
271
420
  this.navigationHistory = [];
272
421
  this.reportSubmitInFlight = false;
422
+ /** Offline queue — only created when config.offlineQueue.enabled is true. */
423
+ this._queue = null;
424
+ /** Active Realtime channel references for cleanup. */
425
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
+ this.realtimeChannels = [];
427
+ if (!config.supabaseUrl) {
428
+ throw new Error("BugBear: supabaseUrl is required. Get it from your BugBear project settings.");
429
+ }
430
+ if (!config.supabaseAnonKey) {
431
+ throw new Error("BugBear: supabaseAnonKey is required. Get it from your BugBear project settings.");
432
+ }
273
433
  this.config = config;
274
- this.supabase = createClient(
275
- config.supabaseUrl || DEFAULT_SUPABASE_URL,
276
- config.supabaseAnonKey || HOSTED_BUGBEAR_ANON_KEY
277
- );
434
+ this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
435
+ if (config.offlineQueue?.enabled) {
436
+ this._queue = new OfflineQueue({
437
+ enabled: true,
438
+ maxItems: config.offlineQueue.maxItems,
439
+ maxRetries: config.offlineQueue.maxRetries
440
+ });
441
+ this.registerQueueHandlers();
442
+ }
443
+ }
444
+ // ── Offline Queue ─────────────────────────────────────────
445
+ /**
446
+ * Access the offline queue (if enabled).
447
+ * Use this to check queue.count, subscribe to changes, or trigger flush.
448
+ */
449
+ get queue() {
450
+ return this._queue;
451
+ }
452
+ /**
453
+ * Initialize the offline queue with a platform-specific storage adapter.
454
+ * Must be called after construction for React Native (which supplies AsyncStorage).
455
+ * Web callers can skip this — LocalStorageAdapter is the default.
456
+ */
457
+ async initQueue(storage) {
458
+ if (!this._queue) return;
459
+ if (storage) {
460
+ this._queue = new OfflineQueue({
461
+ enabled: true,
462
+ maxItems: this.config.offlineQueue?.maxItems,
463
+ maxRetries: this.config.offlineQueue?.maxRetries,
464
+ storage
465
+ });
466
+ this.registerQueueHandlers();
467
+ }
468
+ await this._queue.load();
469
+ }
470
+ registerQueueHandlers() {
471
+ if (!this._queue) return;
472
+ this._queue.registerHandler("report", async (payload) => {
473
+ const { error } = await this.supabase.from("reports").insert(payload).select("id").single();
474
+ if (error) return { success: false, error: error.message };
475
+ return { success: true };
476
+ });
477
+ this._queue.registerHandler("message", async (payload) => {
478
+ const { error } = await this.supabase.from("discussion_messages").insert(payload);
479
+ if (error) return { success: false, error: error.message };
480
+ return { success: true };
481
+ });
482
+ this._queue.registerHandler("feedback", async (payload) => {
483
+ const { error } = await this.supabase.from("test_feedback").insert(payload);
484
+ if (error) return { success: false, error: error.message };
485
+ return { success: true };
486
+ });
487
+ }
488
+ // ── Realtime Subscriptions ─────────────────────────────────
489
+ /** Whether realtime is enabled in config. */
490
+ get realtimeEnabled() {
491
+ return !!this.config.realtime?.enabled;
492
+ }
493
+ /**
494
+ * Subscribe to postgres_changes on relevant tables.
495
+ * Each callback fires when the corresponding table has changes —
496
+ * the provider should call its refresh function in response.
497
+ * Returns a cleanup function that unsubscribes all channels.
498
+ */
499
+ subscribeToChanges(callbacks) {
500
+ this.unsubscribeAll();
501
+ const projectId = this.config.projectId;
502
+ const debounce = (fn, ms = 500) => {
503
+ let timer;
504
+ return () => {
505
+ clearTimeout(timer);
506
+ timer = setTimeout(fn, ms);
507
+ };
508
+ };
509
+ if (callbacks.onAssignmentChange) {
510
+ const debouncedCb = debounce(callbacks.onAssignmentChange);
511
+ const channel = this.supabase.channel("bugbear-assignments").on("postgres_changes", {
512
+ event: "*",
513
+ schema: "public",
514
+ table: "test_assignments",
515
+ filter: `project_id=eq.${projectId}`
516
+ }, debouncedCb).subscribe((status) => {
517
+ if (status === "CHANNEL_ERROR") {
518
+ console.warn("BugBear: Realtime subscription failed for test_assignments");
519
+ }
520
+ });
521
+ this.realtimeChannels.push(channel);
522
+ }
523
+ if (callbacks.onMessageChange) {
524
+ const debouncedCb = debounce(callbacks.onMessageChange);
525
+ const channel = this.supabase.channel("bugbear-messages").on("postgres_changes", {
526
+ event: "INSERT",
527
+ schema: "public",
528
+ table: "discussion_messages"
529
+ }, debouncedCb).subscribe((status) => {
530
+ if (status === "CHANNEL_ERROR") {
531
+ console.warn("BugBear: Realtime subscription failed for discussion_messages");
532
+ }
533
+ });
534
+ this.realtimeChannels.push(channel);
535
+ }
536
+ if (callbacks.onReportChange) {
537
+ const debouncedCb = debounce(callbacks.onReportChange);
538
+ const channel = this.supabase.channel("bugbear-reports").on("postgres_changes", {
539
+ event: "UPDATE",
540
+ schema: "public",
541
+ table: "reports",
542
+ filter: `project_id=eq.${projectId}`
543
+ }, debouncedCb).subscribe((status) => {
544
+ if (status === "CHANNEL_ERROR") {
545
+ console.warn("BugBear: Realtime subscription failed for reports");
546
+ }
547
+ });
548
+ this.realtimeChannels.push(channel);
549
+ }
550
+ return () => this.unsubscribeAll();
551
+ }
552
+ /** Remove all active Realtime channels. */
553
+ unsubscribeAll() {
554
+ for (const channel of this.realtimeChannels) {
555
+ this.supabase.removeChannel(channel);
556
+ }
557
+ this.realtimeChannels = [];
278
558
  }
279
559
  /**
280
560
  * Track navigation for context.
@@ -334,6 +614,7 @@ var BugBearClient = class {
334
614
  return { success: false, error: "A report is already being submitted" };
335
615
  }
336
616
  this.reportSubmitInFlight = true;
617
+ let fullReport;
337
618
  try {
338
619
  const validationError = this.validateReport(report);
339
620
  if (validationError) {
@@ -350,7 +631,7 @@ var BugBearClient = class {
350
631
  return { success: false, error: "User not authenticated" };
351
632
  }
352
633
  const testerInfo = await this.getTesterInfo();
353
- const fullReport = {
634
+ fullReport = {
354
635
  project_id: this.config.projectId,
355
636
  reporter_id: userInfo.id,
356
637
  // User ID from host app (required)
@@ -377,6 +658,10 @@ var BugBearClient = class {
377
658
  };
378
659
  const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
379
660
  if (error) {
661
+ if (this._queue && isNetworkError(error.message)) {
662
+ await this._queue.enqueue("report", fullReport);
663
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
664
+ }
380
665
  console.error("BugBear: Failed to submit report", error.message);
381
666
  return { success: false, error: error.message };
382
667
  }
@@ -386,6 +671,10 @@ var BugBearClient = class {
386
671
  return { success: true, reportId: data.id };
387
672
  } catch (err) {
388
673
  const message = err instanceof Error ? err.message : "Unknown error";
674
+ if (this._queue && fullReport && isNetworkError(message)) {
675
+ await this._queue.enqueue("report", fullReport);
676
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
677
+ }
389
678
  return { success: false, error: message };
390
679
  } finally {
391
680
  this.reportSubmitInFlight = false;
@@ -395,14 +684,18 @@ var BugBearClient = class {
395
684
  * Get assigned tests for current user
396
685
  * First looks up the tester by email, then fetches their assignments
397
686
  */
398
- async getAssignedTests() {
687
+ async getAssignedTests(options) {
399
688
  try {
400
689
  const testerInfo = await this.getTesterInfo();
401
690
  if (!testerInfo) return [];
402
- const { data, error } = await this.supabase.from("test_assignments").select(`
691
+ const pageSize = Math.min(options?.pageSize ?? 100, 100);
692
+ const from = (options?.page ?? 0) * pageSize;
693
+ const to = from + pageSize - 1;
694
+ const selectFields = `
403
695
  id,
404
696
  status,
405
697
  started_at,
698
+ completed_at,
406
699
  skip_reason,
407
700
  is_verification,
408
701
  original_report_id,
@@ -437,20 +730,24 @@ var BugBearClient = class {
437
730
  color,
438
731
  description,
439
732
  login_hint
440
- )
733
+ ),
734
+ platforms
441
735
  )
442
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
443
- if (error) {
444
- console.error("BugBear: Failed to fetch assignments", formatPgError(error));
736
+ `;
737
+ const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
738
+ const [pendingResult, completedResult] = await Promise.all([
739
+ 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),
740
+ 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)
741
+ ]);
742
+ if (pendingResult.error) {
743
+ console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
445
744
  return [];
446
745
  }
447
- const mapped = (data || []).filter((item) => {
448
- if (!item.test_case) {
449
- console.warn("BugBear: Assignment returned without test_case", { id: item.id });
450
- return false;
451
- }
452
- return true;
453
- }).map((item) => ({
746
+ const allData = [
747
+ ...pendingResult.data || [],
748
+ ...completedResult.data || []
749
+ ];
750
+ const mapItem = (item) => ({
454
751
  id: item.id,
455
752
  status: item.status,
456
753
  startedAt: item.started_at,
@@ -488,12 +785,24 @@ var BugBearClient = class {
488
785
  color: item.test_case.role.color,
489
786
  description: item.test_case.role.description,
490
787
  loginHint: item.test_case.role.login_hint
491
- } : void 0
788
+ } : void 0,
789
+ platforms: item.test_case.platforms || void 0
492
790
  }
493
- }));
791
+ });
792
+ const mapped = allData.filter((item) => {
793
+ if (!item.test_case) {
794
+ console.warn("BugBear: Assignment returned without test_case", { id: item.id });
795
+ return false;
796
+ }
797
+ return true;
798
+ }).map(mapItem);
494
799
  mapped.sort((a, b) => {
495
800
  if (a.isVerification && !b.isVerification) return -1;
496
801
  if (!a.isVerification && b.isVerification) return 1;
802
+ const aActive = a.status === "pending" || a.status === "in_progress";
803
+ const bActive = b.status === "pending" || b.status === "in_progress";
804
+ if (aActive && !bActive) return -1;
805
+ if (!aActive && bActive) return 1;
497
806
  return 0;
498
807
  });
499
808
  return mapped;
@@ -658,6 +967,36 @@ var BugBearClient = class {
658
967
  async failAssignment(assignmentId) {
659
968
  return this.updateAssignmentStatus(assignmentId, "failed");
660
969
  }
970
+ /**
971
+ * Reopen a completed assignment — sets it back to in_progress with a fresh timer.
972
+ * Clears completed_at and duration_seconds so it can be re-evaluated.
973
+ */
974
+ async reopenAssignment(assignmentId) {
975
+ try {
976
+ const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
977
+ if (fetchError || !current) {
978
+ return { success: false, error: "Assignment not found" };
979
+ }
980
+ if (current.status === "pending" || current.status === "in_progress") {
981
+ return { success: true };
982
+ }
983
+ const { error } = await this.supabase.from("test_assignments").update({
984
+ status: "in_progress",
985
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
986
+ completed_at: null,
987
+ duration_seconds: null,
988
+ skip_reason: null
989
+ }).eq("id", assignmentId).eq("status", current.status);
990
+ if (error) {
991
+ console.error("BugBear: Failed to reopen assignment", error);
992
+ return { success: false, error: error.message };
993
+ }
994
+ return { success: true };
995
+ } catch (err) {
996
+ const message = err instanceof Error ? err.message : "Unknown error";
997
+ return { success: false, error: message };
998
+ }
999
+ }
661
1000
  /**
662
1001
  * Skip a test assignment with a required reason
663
1002
  * Marks the assignment as 'skipped' and records why it was skipped
@@ -698,6 +1037,7 @@ var BugBearClient = class {
698
1037
  * This empowers testers to shape better tests over time
699
1038
  */
700
1039
  async submitTestFeedback(options) {
1040
+ let feedbackPayload;
701
1041
  try {
702
1042
  const testerInfo = await this.getTesterInfo();
703
1043
  if (!testerInfo) {
@@ -717,7 +1057,7 @@ var BugBearClient = class {
717
1057
  return { success: false, error: `${name} must be between 1 and 5` };
718
1058
  }
719
1059
  }
720
- const { error: feedbackError } = await this.supabase.from("test_feedback").insert({
1060
+ feedbackPayload = {
721
1061
  project_id: this.config.projectId,
722
1062
  test_case_id: testCaseId,
723
1063
  assignment_id: assignmentId || null,
@@ -735,8 +1075,13 @@ var BugBearClient = class {
735
1075
  platform: this.getDeviceInfo().platform,
736
1076
  time_to_complete_seconds: timeToCompleteSeconds || null,
737
1077
  screenshot_urls: screenshotUrls || []
738
- });
1078
+ };
1079
+ const { error: feedbackError } = await this.supabase.from("test_feedback").insert(feedbackPayload);
739
1080
  if (feedbackError) {
1081
+ if (this._queue && isNetworkError(feedbackError.message)) {
1082
+ await this._queue.enqueue("feedback", feedbackPayload);
1083
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1084
+ }
740
1085
  console.error("BugBear: Failed to submit feedback", feedbackError);
741
1086
  return { success: false, error: feedbackError.message };
742
1087
  }
@@ -753,6 +1098,10 @@ var BugBearClient = class {
753
1098
  return { success: true };
754
1099
  } catch (err) {
755
1100
  const message = err instanceof Error ? err.message : "Unknown error";
1101
+ if (this._queue && feedbackPayload && isNetworkError(message)) {
1102
+ await this._queue.enqueue("feedback", feedbackPayload);
1103
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1104
+ }
756
1105
  console.error("BugBear: Error submitting feedback", err);
757
1106
  return { success: false, error: message };
758
1107
  }
@@ -1387,6 +1736,7 @@ var BugBearClient = class {
1387
1736
  * Send a message to a thread
1388
1737
  */
1389
1738
  async sendMessage(threadId, content, attachments) {
1739
+ let insertData;
1390
1740
  try {
1391
1741
  const testerInfo = await this.getTesterInfo();
1392
1742
  if (!testerInfo) {
@@ -1398,7 +1748,7 @@ var BugBearClient = class {
1398
1748
  console.error("BugBear: Rate limit exceeded for messages");
1399
1749
  return false;
1400
1750
  }
1401
- const insertData = {
1751
+ insertData = {
1402
1752
  thread_id: threadId,
1403
1753
  sender_type: "tester",
1404
1754
  sender_tester_id: testerInfo.id,
@@ -1413,12 +1763,21 @@ var BugBearClient = class {
1413
1763
  }
1414
1764
  const { error } = await this.supabase.from("discussion_messages").insert(insertData);
1415
1765
  if (error) {
1766
+ if (this._queue && isNetworkError(error.message)) {
1767
+ await this._queue.enqueue("message", insertData);
1768
+ return false;
1769
+ }
1416
1770
  console.error("BugBear: Failed to send message", formatPgError(error));
1417
1771
  return false;
1418
1772
  }
1419
1773
  await this.markThreadAsRead(threadId);
1420
1774
  return true;
1421
1775
  } catch (err) {
1776
+ const message = err instanceof Error ? err.message : "Unknown error";
1777
+ if (this._queue && insertData && isNetworkError(message)) {
1778
+ await this._queue.enqueue("message", insertData);
1779
+ return false;
1780
+ }
1422
1781
  console.error("BugBear: Error sending message", err);
1423
1782
  return false;
1424
1783
  }
@@ -1754,8 +2113,11 @@ function createBugBear(config) {
1754
2113
  export {
1755
2114
  BUG_CATEGORIES,
1756
2115
  BugBearClient,
2116
+ LocalStorageAdapter,
2117
+ OfflineQueue,
1757
2118
  captureError,
1758
2119
  contextCapture,
1759
2120
  createBugBear,
1760
- isBugCategory
2121
+ isBugCategory,
2122
+ isNetworkError
1761
2123
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.4.6",
3
+ "version": "0.5.1",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",