@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.d.mts +164 -6
- package/dist/index.d.ts +164 -6
- package/dist/index.js +401 -36
- package/dist/index.mjs +397 -35
- package/package.json +1 -1
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|