@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.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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|