@bbearai/core 0.2.14 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  type ReportType = 'bug' | 'feedback' | 'suggestion' | 'test_pass' | 'test_fail';
5
5
  type Severity = 'critical' | 'high' | 'medium' | 'low';
6
- type ReportStatus = 'new' | 'triaging' | 'confirmed' | 'in_progress' | 'fixed' | 'verified' | 'wont_fix' | 'duplicate';
6
+ type ReportStatus = 'new' | 'triaging' | 'confirmed' | 'in_progress' | 'fixed' | 'ready_to_test' | 'verified' | 'resolved' | 'reviewed' | 'closed' | 'wont_fix' | 'duplicate';
7
7
  interface AppContext {
8
8
  /** Current route/screen path */
9
9
  currentRoute: string;
@@ -160,6 +160,11 @@ interface BugBearConfig {
160
160
  * the full web experience (analytics, discussions, history, etc.)
161
161
  */
162
162
  dashboardUrl?: string;
163
+ /**
164
+ * Called when an error occurs inside the BugBear SDK.
165
+ * Wire this to your error reporting service (e.g. Sentry.captureException).
166
+ */
167
+ onError?: (error: Error, context?: Record<string, unknown>) => void;
163
168
  }
164
169
  interface BugBearTheme {
165
170
  /** Primary brand color */
@@ -710,6 +715,19 @@ declare class BugBearClient {
710
715
  * Uses parameterized RPC function to prevent SQL injection
711
716
  */
712
717
  getTesterInfo(): Promise<TesterInfo | null>;
718
+ /**
719
+ * Get detailed assignment stats for the current tester via RPC.
720
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
721
+ */
722
+ getTesterStats(): Promise<{
723
+ pending: number;
724
+ in_progress: number;
725
+ passed: number;
726
+ failed: number;
727
+ blocked: number;
728
+ skipped: number;
729
+ total: number;
730
+ } | null>;
713
731
  /**
714
732
  * Basic email format validation (defense in depth)
715
733
  */
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  type ReportType = 'bug' | 'feedback' | 'suggestion' | 'test_pass' | 'test_fail';
5
5
  type Severity = 'critical' | 'high' | 'medium' | 'low';
6
- type ReportStatus = 'new' | 'triaging' | 'confirmed' | 'in_progress' | 'fixed' | 'verified' | 'wont_fix' | 'duplicate';
6
+ type ReportStatus = 'new' | 'triaging' | 'confirmed' | 'in_progress' | 'fixed' | 'ready_to_test' | 'verified' | 'resolved' | 'reviewed' | 'closed' | 'wont_fix' | 'duplicate';
7
7
  interface AppContext {
8
8
  /** Current route/screen path */
9
9
  currentRoute: string;
@@ -160,6 +160,11 @@ interface BugBearConfig {
160
160
  * the full web experience (analytics, discussions, history, etc.)
161
161
  */
162
162
  dashboardUrl?: string;
163
+ /**
164
+ * Called when an error occurs inside the BugBear SDK.
165
+ * Wire this to your error reporting service (e.g. Sentry.captureException).
166
+ */
167
+ onError?: (error: Error, context?: Record<string, unknown>) => void;
163
168
  }
164
169
  interface BugBearTheme {
165
170
  /** Primary brand color */
@@ -710,6 +715,19 @@ declare class BugBearClient {
710
715
  * Uses parameterized RPC function to prevent SQL injection
711
716
  */
712
717
  getTesterInfo(): Promise<TesterInfo | null>;
718
+ /**
719
+ * Get detailed assignment stats for the current tester via RPC.
720
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
721
+ */
722
+ getTesterStats(): Promise<{
723
+ pending: number;
724
+ in_progress: number;
725
+ passed: number;
726
+ failed: number;
727
+ blocked: number;
728
+ skipped: number;
729
+ total: number;
730
+ } | null>;
713
731
  /**
714
732
  * Basic email format validation (defense in depth)
715
733
  */
package/dist/index.js CHANGED
@@ -173,7 +173,7 @@ var ContextCaptureManager = class {
173
173
  });
174
174
  }
175
175
  captureFetch() {
176
- if (typeof window === "undefined" || typeof fetch === "undefined") return;
176
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
177
177
  this.originalFetch = window.fetch;
178
178
  const self = this;
179
179
  window.fetch = async function(input, init) {
@@ -440,7 +440,7 @@ var BugBearClient = class {
440
440
  sort_order
441
441
  )
442
442
  )
443
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
443
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
444
444
  if (error) {
445
445
  console.error("BugBear: Failed to fetch assignments", error);
446
446
  return [];
@@ -594,7 +594,7 @@ var BugBearClient = class {
594
594
  if (options?.testResult) {
595
595
  updateData.test_result = options.testResult;
596
596
  }
597
- const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
597
+ const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
598
598
  if (error) {
599
599
  console.error("BugBear: Failed to update assignment status", {
600
600
  message: error.message,
@@ -607,6 +607,9 @@ var BugBearClient = class {
607
607
  });
608
608
  return { success: false, error: error.message };
609
609
  }
610
+ if (!updatedRow) {
611
+ return { success: false, error: "Assignment status has changed. Please refresh and try again." };
612
+ }
610
613
  if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
611
614
  const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
612
615
  if (fetchError2) {
@@ -833,6 +836,28 @@ var BugBearClient = class {
833
836
  return null;
834
837
  }
835
838
  }
839
+ /**
840
+ * Get detailed assignment stats for the current tester via RPC.
841
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
842
+ */
843
+ async getTesterStats() {
844
+ try {
845
+ const testerInfo = await this.getTesterInfo();
846
+ if (!testerInfo) return null;
847
+ const { data, error } = await this.supabase.rpc("get_tester_stats", {
848
+ p_project_id: this.config.projectId,
849
+ p_tester_id: testerInfo.id
850
+ });
851
+ if (error) {
852
+ console.error("BugBear: Failed to fetch tester stats", error);
853
+ return null;
854
+ }
855
+ return data;
856
+ } catch (err) {
857
+ console.error("BugBear: Error fetching tester stats", err);
858
+ return null;
859
+ }
860
+ }
836
861
  /**
837
862
  * Basic email format validation (defense in depth)
838
863
  */
@@ -1166,75 +1191,35 @@ var BugBearClient = class {
1166
1191
  try {
1167
1192
  const testerInfo = await this.getTesterInfo();
1168
1193
  if (!testerInfo) return [];
1169
- const { data: threads, error } = await this.supabase.from("discussion_threads").select(`
1170
- id,
1171
- subject,
1172
- thread_type,
1173
- priority,
1174
- is_pinned,
1175
- is_resolved,
1176
- last_message_at,
1177
- created_at
1178
- `).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
1194
+ const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
1195
+ p_project_id: this.config.projectId,
1196
+ p_tester_id: testerInfo.id
1197
+ });
1179
1198
  if (error) {
1180
- console.error("BugBear: Failed to fetch threads", error);
1199
+ console.error("BugBear: Failed to fetch threads via RPC", error);
1181
1200
  return [];
1182
1201
  }
1183
- if (!threads || threads.length === 0) return [];
1184
- const threadIds = threads.map((t) => t.id);
1185
- const { data: readStatuses } = await this.supabase.from("discussion_read_status").select("thread_id, last_read_at, last_read_message_id").eq("tester_id", testerInfo.id).in("thread_id", threadIds);
1186
- const readStatusMap = new Map(
1187
- (readStatuses || []).map((rs) => [rs.thread_id, rs])
1188
- );
1189
- const { data: lastMessages } = await this.supabase.from("discussion_messages").select(`
1190
- id,
1191
- thread_id,
1192
- sender_type,
1193
- sender_name,
1194
- content,
1195
- created_at,
1196
- attachments
1197
- `).in("thread_id", threadIds).order("created_at", { ascending: false });
1198
- const lastMessageMap = /* @__PURE__ */ new Map();
1199
- for (const msg of lastMessages || []) {
1200
- if (!lastMessageMap.has(msg.thread_id)) {
1201
- lastMessageMap.set(msg.thread_id, msg);
1202
- }
1203
- }
1204
- const unreadCounts = await Promise.all(
1205
- threads.map(async (thread) => {
1206
- const readStatus = readStatusMap.get(thread.id);
1207
- const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
1208
- const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
1209
- return { threadId: thread.id, count: countError ? 0 : count || 0 };
1210
- })
1211
- );
1212
- const unreadCountMap = new Map(
1213
- unreadCounts.map((uc) => [uc.threadId, uc.count])
1214
- );
1215
- return threads.map((thread) => {
1216
- const lastMsg = lastMessageMap.get(thread.id);
1217
- return {
1218
- id: thread.id,
1219
- subject: thread.subject,
1220
- threadType: thread.thread_type,
1221
- priority: thread.priority,
1222
- isPinned: thread.is_pinned,
1223
- isResolved: thread.is_resolved,
1224
- lastMessageAt: thread.last_message_at,
1225
- createdAt: thread.created_at,
1226
- unreadCount: unreadCountMap.get(thread.id) || 0,
1227
- lastMessage: lastMsg ? {
1228
- id: lastMsg.id,
1229
- threadId: lastMsg.thread_id,
1230
- senderType: lastMsg.sender_type,
1231
- senderName: lastMsg.sender_name,
1232
- content: lastMsg.content,
1233
- createdAt: lastMsg.created_at,
1234
- attachments: lastMsg.attachments || []
1235
- } : void 0
1236
- };
1237
- });
1202
+ if (!data || data.length === 0) return [];
1203
+ return data.map((row) => ({
1204
+ id: row.thread_id,
1205
+ subject: row.thread_subject,
1206
+ threadType: row.thread_type,
1207
+ priority: row.thread_priority,
1208
+ isPinned: row.is_pinned,
1209
+ isResolved: row.is_resolved,
1210
+ lastMessageAt: row.last_message_at,
1211
+ createdAt: row.created_at,
1212
+ unreadCount: Number(row.unread_count) || 0,
1213
+ lastMessage: row.last_message_preview ? {
1214
+ id: "",
1215
+ threadId: row.thread_id,
1216
+ senderType: row.last_message_sender_type || "system",
1217
+ senderName: row.last_message_sender_name || "",
1218
+ content: row.last_message_preview,
1219
+ createdAt: row.last_message_at,
1220
+ attachments: []
1221
+ } : void 0
1222
+ }));
1238
1223
  } catch (err) {
1239
1224
  console.error("BugBear: Error fetching threads", err);
1240
1225
  return [];
@@ -1253,7 +1238,7 @@ var BugBearClient = class {
1253
1238
  content,
1254
1239
  created_at,
1255
1240
  attachments
1256
- `).eq("thread_id", threadId).order("created_at", { ascending: true });
1241
+ `).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
1257
1242
  if (error) {
1258
1243
  console.error("BugBear: Failed to fetch messages", error);
1259
1244
  return [];
@@ -1302,7 +1287,6 @@ var BugBearClient = class {
1302
1287
  console.error("BugBear: Failed to send message", error);
1303
1288
  return false;
1304
1289
  }
1305
- await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
1306
1290
  await this.markThreadAsRead(threadId);
1307
1291
  return true;
1308
1292
  } catch (err) {
@@ -1533,7 +1517,7 @@ var BugBearClient = class {
1533
1517
  */
1534
1518
  async getSessionFindings(sessionId) {
1535
1519
  try {
1536
- const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
1520
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
1537
1521
  if (error) {
1538
1522
  console.error("BugBear: Failed to fetch findings", error);
1539
1523
  return [];
package/dist/index.mjs CHANGED
@@ -144,7 +144,7 @@ var ContextCaptureManager = class {
144
144
  });
145
145
  }
146
146
  captureFetch() {
147
- if (typeof window === "undefined" || typeof fetch === "undefined") return;
147
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
148
148
  this.originalFetch = window.fetch;
149
149
  const self = this;
150
150
  window.fetch = async function(input, init) {
@@ -411,7 +411,7 @@ var BugBearClient = class {
411
411
  sort_order
412
412
  )
413
413
  )
414
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
414
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
415
415
  if (error) {
416
416
  console.error("BugBear: Failed to fetch assignments", error);
417
417
  return [];
@@ -565,7 +565,7 @@ var BugBearClient = class {
565
565
  if (options?.testResult) {
566
566
  updateData.test_result = options.testResult;
567
567
  }
568
- const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
568
+ const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
569
569
  if (error) {
570
570
  console.error("BugBear: Failed to update assignment status", {
571
571
  message: error.message,
@@ -578,6 +578,9 @@ var BugBearClient = class {
578
578
  });
579
579
  return { success: false, error: error.message };
580
580
  }
581
+ if (!updatedRow) {
582
+ return { success: false, error: "Assignment status has changed. Please refresh and try again." };
583
+ }
581
584
  if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
582
585
  const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
583
586
  if (fetchError2) {
@@ -804,6 +807,28 @@ var BugBearClient = class {
804
807
  return null;
805
808
  }
806
809
  }
810
+ /**
811
+ * Get detailed assignment stats for the current tester via RPC.
812
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
813
+ */
814
+ async getTesterStats() {
815
+ try {
816
+ const testerInfo = await this.getTesterInfo();
817
+ if (!testerInfo) return null;
818
+ const { data, error } = await this.supabase.rpc("get_tester_stats", {
819
+ p_project_id: this.config.projectId,
820
+ p_tester_id: testerInfo.id
821
+ });
822
+ if (error) {
823
+ console.error("BugBear: Failed to fetch tester stats", error);
824
+ return null;
825
+ }
826
+ return data;
827
+ } catch (err) {
828
+ console.error("BugBear: Error fetching tester stats", err);
829
+ return null;
830
+ }
831
+ }
807
832
  /**
808
833
  * Basic email format validation (defense in depth)
809
834
  */
@@ -1137,75 +1162,35 @@ var BugBearClient = class {
1137
1162
  try {
1138
1163
  const testerInfo = await this.getTesterInfo();
1139
1164
  if (!testerInfo) return [];
1140
- const { data: threads, error } = await this.supabase.from("discussion_threads").select(`
1141
- id,
1142
- subject,
1143
- thread_type,
1144
- priority,
1145
- is_pinned,
1146
- is_resolved,
1147
- last_message_at,
1148
- created_at
1149
- `).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
1165
+ const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
1166
+ p_project_id: this.config.projectId,
1167
+ p_tester_id: testerInfo.id
1168
+ });
1150
1169
  if (error) {
1151
- console.error("BugBear: Failed to fetch threads", error);
1170
+ console.error("BugBear: Failed to fetch threads via RPC", error);
1152
1171
  return [];
1153
1172
  }
1154
- if (!threads || threads.length === 0) return [];
1155
- const threadIds = threads.map((t) => t.id);
1156
- const { data: readStatuses } = await this.supabase.from("discussion_read_status").select("thread_id, last_read_at, last_read_message_id").eq("tester_id", testerInfo.id).in("thread_id", threadIds);
1157
- const readStatusMap = new Map(
1158
- (readStatuses || []).map((rs) => [rs.thread_id, rs])
1159
- );
1160
- const { data: lastMessages } = await this.supabase.from("discussion_messages").select(`
1161
- id,
1162
- thread_id,
1163
- sender_type,
1164
- sender_name,
1165
- content,
1166
- created_at,
1167
- attachments
1168
- `).in("thread_id", threadIds).order("created_at", { ascending: false });
1169
- const lastMessageMap = /* @__PURE__ */ new Map();
1170
- for (const msg of lastMessages || []) {
1171
- if (!lastMessageMap.has(msg.thread_id)) {
1172
- lastMessageMap.set(msg.thread_id, msg);
1173
- }
1174
- }
1175
- const unreadCounts = await Promise.all(
1176
- threads.map(async (thread) => {
1177
- const readStatus = readStatusMap.get(thread.id);
1178
- const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
1179
- const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
1180
- return { threadId: thread.id, count: countError ? 0 : count || 0 };
1181
- })
1182
- );
1183
- const unreadCountMap = new Map(
1184
- unreadCounts.map((uc) => [uc.threadId, uc.count])
1185
- );
1186
- return threads.map((thread) => {
1187
- const lastMsg = lastMessageMap.get(thread.id);
1188
- return {
1189
- id: thread.id,
1190
- subject: thread.subject,
1191
- threadType: thread.thread_type,
1192
- priority: thread.priority,
1193
- isPinned: thread.is_pinned,
1194
- isResolved: thread.is_resolved,
1195
- lastMessageAt: thread.last_message_at,
1196
- createdAt: thread.created_at,
1197
- unreadCount: unreadCountMap.get(thread.id) || 0,
1198
- lastMessage: lastMsg ? {
1199
- id: lastMsg.id,
1200
- threadId: lastMsg.thread_id,
1201
- senderType: lastMsg.sender_type,
1202
- senderName: lastMsg.sender_name,
1203
- content: lastMsg.content,
1204
- createdAt: lastMsg.created_at,
1205
- attachments: lastMsg.attachments || []
1206
- } : void 0
1207
- };
1208
- });
1173
+ if (!data || data.length === 0) return [];
1174
+ return data.map((row) => ({
1175
+ id: row.thread_id,
1176
+ subject: row.thread_subject,
1177
+ threadType: row.thread_type,
1178
+ priority: row.thread_priority,
1179
+ isPinned: row.is_pinned,
1180
+ isResolved: row.is_resolved,
1181
+ lastMessageAt: row.last_message_at,
1182
+ createdAt: row.created_at,
1183
+ unreadCount: Number(row.unread_count) || 0,
1184
+ lastMessage: row.last_message_preview ? {
1185
+ id: "",
1186
+ threadId: row.thread_id,
1187
+ senderType: row.last_message_sender_type || "system",
1188
+ senderName: row.last_message_sender_name || "",
1189
+ content: row.last_message_preview,
1190
+ createdAt: row.last_message_at,
1191
+ attachments: []
1192
+ } : void 0
1193
+ }));
1209
1194
  } catch (err) {
1210
1195
  console.error("BugBear: Error fetching threads", err);
1211
1196
  return [];
@@ -1224,7 +1209,7 @@ var BugBearClient = class {
1224
1209
  content,
1225
1210
  created_at,
1226
1211
  attachments
1227
- `).eq("thread_id", threadId).order("created_at", { ascending: true });
1212
+ `).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
1228
1213
  if (error) {
1229
1214
  console.error("BugBear: Failed to fetch messages", error);
1230
1215
  return [];
@@ -1273,7 +1258,6 @@ var BugBearClient = class {
1273
1258
  console.error("BugBear: Failed to send message", error);
1274
1259
  return false;
1275
1260
  }
1276
- await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
1277
1261
  await this.markThreadAsRead(threadId);
1278
1262
  return true;
1279
1263
  } catch (err) {
@@ -1504,7 +1488,7 @@ var BugBearClient = class {
1504
1488
  */
1505
1489
  async getSessionFindings(sessionId) {
1506
1490
  try {
1507
- const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
1491
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
1508
1492
  if (error) {
1509
1493
  console.error("BugBear: Failed to fetch findings", error);
1510
1494
  return [];
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.2.14",
3
+ "version": "0.3.0",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/index.d.ts",
10
11
  "import": "./dist/index.mjs",
11
- "require": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
12
+ "require": "./dist/index.js"
13
13
  }
14
14
  },
15
15
  "files": [