@bbearai/core 0.1.6 → 0.2.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
@@ -57,7 +57,16 @@ var BugBearClient = class {
57
57
  */
58
58
  async submitReport(report) {
59
59
  try {
60
+ const validationError = this.validateReport(report);
61
+ if (validationError) {
62
+ return { success: false, error: validationError };
63
+ }
60
64
  const userInfo = await this.getCurrentUserInfo();
65
+ const rateLimitId = userInfo?.email || this.config.projectId;
66
+ const rateLimit = await this.checkRateLimit(rateLimitId, "report_submit");
67
+ if (!rateLimit.allowed) {
68
+ return { success: false, error: rateLimit.error };
69
+ }
61
70
  if (!userInfo) {
62
71
  console.error("BugBear: No user info available, cannot submit report");
63
72
  return { success: false, error: "User not authenticated" };
@@ -108,6 +117,7 @@ var BugBearClient = class {
108
117
  const { data, error } = await this.supabase.from("test_assignments").select(`
109
118
  id,
110
119
  status,
120
+ started_at,
111
121
  test_case:test_cases(
112
122
  id,
113
123
  title,
@@ -135,6 +145,7 @@ var BugBearClient = class {
135
145
  return (data || []).map((item) => ({
136
146
  id: item.id,
137
147
  status: item.status,
148
+ startedAt: item.started_at,
138
149
  testCase: {
139
150
  id: item.test_case.id,
140
151
  title: item.test_case.title,
@@ -160,47 +171,430 @@ var BugBearClient = class {
160
171
  return [];
161
172
  }
162
173
  }
174
+ /**
175
+ * Get assignment by ID with time tracking fields
176
+ */
177
+ async getAssignment(assignmentId) {
178
+ try {
179
+ const { data, error } = await this.supabase.from("test_assignments").select(`
180
+ id,
181
+ status,
182
+ started_at,
183
+ completed_at,
184
+ duration_seconds,
185
+ test_case:test_cases(
186
+ id,
187
+ title,
188
+ test_key,
189
+ description,
190
+ steps,
191
+ expected_result,
192
+ priority,
193
+ target_route,
194
+ track:qa_tracks(
195
+ id,
196
+ name,
197
+ icon,
198
+ color,
199
+ test_template,
200
+ rubric_mode,
201
+ description
202
+ )
203
+ )
204
+ `).eq("id", assignmentId).single();
205
+ if (error || !data) return null;
206
+ const testCase = data.test_case;
207
+ if (!testCase) {
208
+ console.error("BugBear: Assignment returned without test_case");
209
+ return null;
210
+ }
211
+ const track = testCase.track;
212
+ return {
213
+ id: data.id,
214
+ status: data.status,
215
+ startedAt: data.started_at,
216
+ durationSeconds: data.duration_seconds,
217
+ testCase: {
218
+ id: testCase.id,
219
+ title: testCase.title,
220
+ testKey: testCase.test_key,
221
+ description: testCase.description,
222
+ steps: testCase.steps,
223
+ expectedResult: testCase.expected_result,
224
+ priority: testCase.priority,
225
+ targetRoute: testCase.target_route,
226
+ track: track ? {
227
+ id: track.id,
228
+ name: track.name,
229
+ icon: track.icon,
230
+ color: track.color,
231
+ testTemplate: track.test_template,
232
+ rubricMode: track.rubric_mode || "pass_fail",
233
+ description: track.description
234
+ } : void 0
235
+ }
236
+ };
237
+ } catch (err) {
238
+ console.error("BugBear: Error fetching assignment", err);
239
+ return null;
240
+ }
241
+ }
242
+ /**
243
+ * Update assignment status with automatic time tracking
244
+ * - Sets started_at when status changes to 'in_progress'
245
+ * - Calculates duration_seconds when status changes to 'passed'/'failed'/'blocked'
246
+ * - Optionally include feedback when completing a test
247
+ */
248
+ async updateAssignmentStatus(assignmentId, status, options) {
249
+ try {
250
+ const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
251
+ if (fetchError || !currentAssignment) {
252
+ return { success: false, error: "Assignment not found" };
253
+ }
254
+ const updateData = { status };
255
+ let durationSeconds;
256
+ if (status === "in_progress" && currentAssignment.status !== "in_progress") {
257
+ updateData.started_at = (/* @__PURE__ */ new Date()).toISOString();
258
+ }
259
+ if (["passed", "failed", "blocked"].includes(status)) {
260
+ updateData.completed_at = (/* @__PURE__ */ new Date()).toISOString();
261
+ if (currentAssignment.started_at) {
262
+ const startedAt = new Date(currentAssignment.started_at);
263
+ const completedAt = /* @__PURE__ */ new Date();
264
+ durationSeconds = Math.round((completedAt.getTime() - startedAt.getTime()) / 1e3);
265
+ updateData.duration_seconds = durationSeconds;
266
+ }
267
+ }
268
+ if (options?.notes) {
269
+ updateData.notes = options.notes;
270
+ }
271
+ if (options?.testResult) {
272
+ updateData.test_result = options.testResult;
273
+ }
274
+ const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
275
+ if (error) {
276
+ console.error("BugBear: Failed to update assignment status", error);
277
+ return { success: false, error: error.message };
278
+ }
279
+ if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
280
+ const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
281
+ if (fetchError2) {
282
+ console.error("BugBear: Failed to fetch assignment for feedback", fetchError2);
283
+ } else if (assignmentData?.test_case_id) {
284
+ const feedbackResult = await this.submitTestFeedback({
285
+ testCaseId: assignmentData.test_case_id,
286
+ assignmentId,
287
+ feedback: options.feedback,
288
+ timeToCompleteSeconds: durationSeconds
289
+ });
290
+ if (!feedbackResult.success) {
291
+ console.error("BugBear: Failed to submit feedback", feedbackResult.error);
292
+ }
293
+ }
294
+ }
295
+ return { success: true, durationSeconds };
296
+ } catch (err) {
297
+ const message = err instanceof Error ? err.message : "Unknown error";
298
+ console.error("BugBear: Error updating assignment status", err);
299
+ return { success: false, error: message };
300
+ }
301
+ }
302
+ /**
303
+ * Submit feedback on a test case to help improve test quality
304
+ * This empowers testers to shape better tests over time
305
+ */
306
+ async submitTestFeedback(options) {
307
+ try {
308
+ const testerInfo = await this.getTesterInfo();
309
+ if (!testerInfo) {
310
+ return { success: false, error: "Not authenticated as tester" };
311
+ }
312
+ const { testCaseId, assignmentId, feedback, timeToCompleteSeconds } = options;
313
+ if (feedback.rating < 1 || feedback.rating > 5) {
314
+ return { success: false, error: "Rating must be between 1 and 5" };
315
+ }
316
+ const { error: feedbackError } = await this.supabase.from("test_feedback").insert({
317
+ project_id: this.config.projectId,
318
+ test_case_id: testCaseId,
319
+ assignment_id: assignmentId || null,
320
+ tester_id: testerInfo.id,
321
+ rating: feedback.rating,
322
+ clarity_rating: feedback.clarityRating || null,
323
+ steps_rating: feedback.stepsRating || null,
324
+ relevance_rating: feedback.relevanceRating || null,
325
+ feedback_note: feedback.feedbackNote?.trim() || null,
326
+ suggested_improvement: feedback.suggestedImprovement?.trim() || null,
327
+ is_outdated: feedback.isOutdated || false,
328
+ needs_more_detail: feedback.needsMoreDetail || false,
329
+ steps_unclear: feedback.stepsUnclear || false,
330
+ expected_result_unclear: feedback.expectedResultUnclear || false,
331
+ platform: this.getDeviceInfo().platform,
332
+ time_to_complete_seconds: timeToCompleteSeconds || null
333
+ });
334
+ if (feedbackError) {
335
+ console.error("BugBear: Failed to submit feedback", feedbackError);
336
+ return { success: false, error: feedbackError.message };
337
+ }
338
+ if (assignmentId) {
339
+ const { error: assignmentError } = await this.supabase.from("test_assignments").update({
340
+ feedback_rating: feedback.rating,
341
+ feedback_note: feedback.feedbackNote?.trim() || null,
342
+ feedback_submitted_at: (/* @__PURE__ */ new Date()).toISOString()
343
+ }).eq("id", assignmentId);
344
+ if (assignmentError) {
345
+ console.error("BugBear: Failed to update assignment feedback fields", assignmentError);
346
+ }
347
+ }
348
+ return { success: true };
349
+ } catch (err) {
350
+ const message = err instanceof Error ? err.message : "Unknown error";
351
+ console.error("BugBear: Error submitting feedback", err);
352
+ return { success: false, error: message };
353
+ }
354
+ }
355
+ /**
356
+ * Get the currently active (in_progress) assignment for the tester
357
+ */
358
+ async getActiveAssignment() {
359
+ try {
360
+ const testerInfo = await this.getTesterInfo();
361
+ if (!testerInfo) return null;
362
+ const { data, error } = await this.supabase.from("test_assignments").select(`
363
+ id,
364
+ status,
365
+ started_at,
366
+ test_case:test_cases(
367
+ id,
368
+ title,
369
+ test_key,
370
+ description,
371
+ steps,
372
+ expected_result,
373
+ priority,
374
+ target_route,
375
+ track:qa_tracks(
376
+ id,
377
+ name,
378
+ icon,
379
+ color,
380
+ test_template,
381
+ rubric_mode,
382
+ description
383
+ )
384
+ )
385
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "in_progress").order("started_at", { ascending: false }).limit(1).maybeSingle();
386
+ if (error || !data) return null;
387
+ const testCase = data.test_case;
388
+ if (!testCase) {
389
+ console.error("BugBear: Active assignment returned without test_case");
390
+ return null;
391
+ }
392
+ const track = testCase.track;
393
+ return {
394
+ id: data.id,
395
+ status: data.status,
396
+ startedAt: data.started_at,
397
+ testCase: {
398
+ id: testCase.id,
399
+ title: testCase.title,
400
+ testKey: testCase.test_key,
401
+ description: testCase.description,
402
+ steps: testCase.steps,
403
+ expectedResult: testCase.expected_result,
404
+ priority: testCase.priority,
405
+ targetRoute: testCase.target_route,
406
+ track: track ? {
407
+ id: track.id,
408
+ name: track.name,
409
+ icon: track.icon,
410
+ color: track.color,
411
+ testTemplate: track.test_template,
412
+ rubricMode: track.rubric_mode || "pass_fail",
413
+ description: track.description
414
+ } : void 0
415
+ }
416
+ };
417
+ } catch (err) {
418
+ console.error("BugBear: Error fetching active assignment", err);
419
+ return null;
420
+ }
421
+ }
163
422
  /**
164
423
  * Get current tester info
165
424
  * Looks up tester by email from the host app's authenticated user
425
+ * Checks both primary email AND additional_emails array
426
+ * Uses parameterized RPC function to prevent SQL injection
166
427
  */
167
428
  async getTesterInfo() {
168
429
  try {
169
430
  const userInfo = await this.getCurrentUserInfo();
170
- if (!userInfo) return null;
171
- const { data, error } = await this.supabase.from("testers").select("*").eq("project_id", this.config.projectId).eq("email", userInfo.email).eq("status", "active").single();
431
+ if (!userInfo?.email) return null;
432
+ if (!this.isValidEmail(userInfo.email)) {
433
+ console.warn("BugBear: Invalid email format");
434
+ return null;
435
+ }
436
+ const { data, error } = await this.supabase.rpc("lookup_tester_by_email", {
437
+ p_project_id: this.config.projectId,
438
+ p_email: userInfo.email
439
+ }).maybeSingle();
172
440
  if (error || !data) return null;
441
+ const tester = data;
173
442
  return {
174
- id: data.id,
175
- name: data.name,
176
- email: data.email,
177
- additionalEmails: data.additional_emails || [],
178
- avatarUrl: data.avatar_url || void 0,
179
- platforms: data.platforms || [],
180
- assignedTests: data.assigned_count || 0,
181
- completedTests: data.completed_count || 0
443
+ id: tester.id,
444
+ name: tester.name,
445
+ email: tester.email,
446
+ additionalEmails: tester.additional_emails || [],
447
+ avatarUrl: tester.avatar_url || void 0,
448
+ platforms: tester.platforms || [],
449
+ assignedTests: tester.assigned_count || 0,
450
+ completedTests: tester.completed_count || 0
182
451
  };
183
452
  } catch (err) {
184
453
  console.error("BugBear: getTesterInfo error", err);
185
454
  return null;
186
455
  }
187
456
  }
457
+ /**
458
+ * Basic email format validation (defense in depth)
459
+ */
460
+ isValidEmail(email) {
461
+ if (!email || email.length > 254) return false;
462
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
463
+ return emailRegex.test(email);
464
+ }
465
+ /**
466
+ * Validate report input before submission
467
+ * Returns error message if invalid, null if valid
468
+ */
469
+ validateReport(report) {
470
+ const validTypes = ["bug", "feedback", "suggestion", "test_pass", "test_fail"];
471
+ if (report.type && !validTypes.includes(report.type)) {
472
+ return `Invalid report type: ${report.type}. Must be one of: ${validTypes.join(", ")}`;
473
+ }
474
+ const validSeverities = ["critical", "high", "medium", "low"];
475
+ if (report.severity && !validSeverities.includes(report.severity)) {
476
+ return `Invalid severity: ${report.severity}. Must be one of: ${validSeverities.join(", ")}`;
477
+ }
478
+ if (report.title && report.title.length > 500) {
479
+ return "Title must be 500 characters or less";
480
+ }
481
+ if (report.description && report.description.length > 1e4) {
482
+ return "Description must be 10,000 characters or less";
483
+ }
484
+ if (report.screenshots && Array.isArray(report.screenshots)) {
485
+ if (report.screenshots.length > 10) {
486
+ return "Maximum 10 screenshots allowed";
487
+ }
488
+ for (const url of report.screenshots) {
489
+ if (typeof url !== "string" || url.length > 2e3) {
490
+ return "Invalid screenshot URL";
491
+ }
492
+ }
493
+ }
494
+ return null;
495
+ }
496
+ /**
497
+ * Validate profile update input
498
+ * Returns error message if invalid, null if valid
499
+ */
500
+ validateProfileUpdate(updates) {
501
+ if (updates.name !== void 0) {
502
+ if (typeof updates.name !== "string" || updates.name.length > 100) {
503
+ return "Name must be 100 characters or less";
504
+ }
505
+ }
506
+ if (updates.additionalEmails !== void 0) {
507
+ if (!Array.isArray(updates.additionalEmails)) {
508
+ return "Additional emails must be an array";
509
+ }
510
+ if (updates.additionalEmails.length > 5) {
511
+ return "Maximum 5 additional emails allowed";
512
+ }
513
+ for (const email of updates.additionalEmails) {
514
+ if (!this.isValidEmail(email)) {
515
+ return `Invalid email format: ${email}`;
516
+ }
517
+ }
518
+ }
519
+ if (updates.avatarUrl !== void 0 && updates.avatarUrl !== null) {
520
+ if (typeof updates.avatarUrl !== "string" || updates.avatarUrl.length > 2e3) {
521
+ return "Invalid avatar URL";
522
+ }
523
+ }
524
+ if (updates.platforms !== void 0) {
525
+ if (!Array.isArray(updates.platforms)) {
526
+ return "Platforms must be an array";
527
+ }
528
+ const validPlatforms = ["ios", "android", "web", "desktop", "other"];
529
+ for (const platform of updates.platforms) {
530
+ if (!validPlatforms.includes(platform)) {
531
+ return `Invalid platform: ${platform}. Must be one of: ${validPlatforms.join(", ")}`;
532
+ }
533
+ }
534
+ }
535
+ return null;
536
+ }
537
+ /**
538
+ * Check rate limit for an action
539
+ * Returns { allowed: boolean, error?: string, remaining?: number }
540
+ */
541
+ async checkRateLimit(identifier, action) {
542
+ try {
543
+ const { data, error } = await this.supabase.rpc("check_rate_limit", {
544
+ p_identifier: identifier,
545
+ p_action: action,
546
+ p_project_id: this.config.projectId
547
+ });
548
+ if (error) {
549
+ console.warn("BugBear: Rate limit check failed, allowing request", error.message);
550
+ return { allowed: true };
551
+ }
552
+ if (!data.allowed) {
553
+ return {
554
+ allowed: false,
555
+ error: `Rate limit exceeded. Try again in ${Math.ceil((new Date(data.reset_at).getTime() - Date.now()) / 1e3)} seconds.`,
556
+ remaining: 0,
557
+ resetAt: data.reset_at
558
+ };
559
+ }
560
+ return {
561
+ allowed: true,
562
+ remaining: data.remaining,
563
+ resetAt: data.reset_at
564
+ };
565
+ } catch (err) {
566
+ console.warn("BugBear: Rate limit check error", err);
567
+ return { allowed: true };
568
+ }
569
+ }
188
570
  /**
189
571
  * Update tester profile
190
572
  * Allows testers to update their name, additional emails, avatar, and platforms
191
573
  */
192
574
  async updateTesterProfile(updates) {
193
575
  try {
576
+ const validationError = this.validateProfileUpdate(updates);
577
+ if (validationError) {
578
+ return { success: false, error: validationError };
579
+ }
194
580
  const userInfo = await this.getCurrentUserInfo();
195
581
  if (!userInfo) {
196
582
  return { success: false, error: "Not authenticated" };
197
583
  }
584
+ const rateLimit = await this.checkRateLimit(userInfo.email, "profile_update");
585
+ if (!rateLimit.allowed) {
586
+ return { success: false, error: rateLimit.error };
587
+ }
588
+ const testerInfo = await this.getTesterInfo();
589
+ if (!testerInfo) {
590
+ return { success: false, error: "Not a registered tester" };
591
+ }
198
592
  const updateData = {};
199
593
  if (updates.name !== void 0) updateData.name = updates.name;
200
594
  if (updates.additionalEmails !== void 0) updateData.additional_emails = updates.additionalEmails;
201
595
  if (updates.avatarUrl !== void 0) updateData.avatar_url = updates.avatarUrl;
202
596
  if (updates.platforms !== void 0) updateData.platforms = updates.platforms;
203
- const { error } = await this.supabase.from("testers").update(updateData).eq("project_id", this.config.projectId).eq("email", userInfo.email);
597
+ const { error } = await this.supabase.from("testers").update(updateData).eq("id", testerInfo.id);
204
598
  if (error) {
205
599
  console.error("BugBear: updateTesterProfile error", error);
206
600
  return { success: false, error: error.message };
@@ -476,6 +870,11 @@ var BugBearClient = class {
476
870
  console.error("BugBear: No tester info, cannot send message");
477
871
  return false;
478
872
  }
873
+ const rateLimit = await this.checkRateLimit(testerInfo.email, "message_send");
874
+ if (!rateLimit.allowed) {
875
+ console.error("BugBear: Rate limit exceeded for messages");
876
+ return false;
877
+ }
479
878
  const { error } = await this.supabase.from("discussion_messages").insert({
480
879
  thread_id: threadId,
481
880
  sender_type: "tester",
@@ -570,6 +969,248 @@ var BugBearClient = class {
570
969
  return { success: false, error: message };
571
970
  }
572
971
  }
972
+ // ============================================
973
+ // QA Sessions (Sprint 1)
974
+ // ============================================
975
+ /**
976
+ * Start a new QA session for exploratory testing
977
+ */
978
+ async startSession(options = {}) {
979
+ try {
980
+ const testerInfo = await this.getTesterInfo();
981
+ if (!testerInfo) {
982
+ return { success: false, error: "Not authenticated as a tester" };
983
+ }
984
+ const activeSession = await this.getActiveSession();
985
+ if (activeSession) {
986
+ return { success: false, error: "You already have an active session. End it before starting a new one." };
987
+ }
988
+ const { data, error } = await this.supabase.rpc("start_qa_session", {
989
+ p_project_id: this.config.projectId,
990
+ p_tester_id: testerInfo.id,
991
+ p_focus_area: options.focusArea || null,
992
+ p_track: options.track || null,
993
+ p_platform: options.platform || null
994
+ });
995
+ if (error) {
996
+ console.error("BugBear: Failed to start session", error);
997
+ return { success: false, error: error.message };
998
+ }
999
+ const session = await this.getSession(data);
1000
+ if (!session) {
1001
+ return { success: false, error: "Session created but could not be fetched" };
1002
+ }
1003
+ return { success: true, session };
1004
+ } catch (err) {
1005
+ const message = err instanceof Error ? err.message : "Unknown error";
1006
+ console.error("BugBear: Error starting session", err);
1007
+ return { success: false, error: message };
1008
+ }
1009
+ }
1010
+ /**
1011
+ * End the current QA session
1012
+ */
1013
+ async endSession(sessionId, options = {}) {
1014
+ try {
1015
+ const { data, error } = await this.supabase.rpc("end_qa_session", {
1016
+ p_session_id: sessionId,
1017
+ p_notes: options.notes || null,
1018
+ p_routes_covered: options.routesCovered || null
1019
+ });
1020
+ if (error) {
1021
+ console.error("BugBear: Failed to end session", error);
1022
+ return { success: false, error: error.message };
1023
+ }
1024
+ const session = this.transformSession(data);
1025
+ return { success: true, session };
1026
+ } catch (err) {
1027
+ const message = err instanceof Error ? err.message : "Unknown error";
1028
+ console.error("BugBear: Error ending session", err);
1029
+ return { success: false, error: message };
1030
+ }
1031
+ }
1032
+ /**
1033
+ * Get the current active session for the tester
1034
+ */
1035
+ async getActiveSession() {
1036
+ try {
1037
+ const testerInfo = await this.getTesterInfo();
1038
+ if (!testerInfo) return null;
1039
+ const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
1040
+ if (error || !data) return null;
1041
+ return this.transformSession(data);
1042
+ } catch (err) {
1043
+ console.error("BugBear: Error fetching active session", err);
1044
+ return null;
1045
+ }
1046
+ }
1047
+ /**
1048
+ * Get a session by ID
1049
+ */
1050
+ async getSession(sessionId) {
1051
+ try {
1052
+ const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
1053
+ if (error || !data) return null;
1054
+ return this.transformSession(data);
1055
+ } catch (err) {
1056
+ console.error("BugBear: Error fetching session", err);
1057
+ return null;
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Get session history for the tester
1062
+ */
1063
+ async getSessionHistory(limit = 10) {
1064
+ try {
1065
+ const testerInfo = await this.getTesterInfo();
1066
+ if (!testerInfo) return [];
1067
+ const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).order("started_at", { ascending: false }).limit(limit);
1068
+ if (error) {
1069
+ console.error("BugBear: Failed to fetch session history", error);
1070
+ return [];
1071
+ }
1072
+ return (data || []).map((s) => this.transformSession(s));
1073
+ } catch (err) {
1074
+ console.error("BugBear: Error fetching session history", err);
1075
+ return [];
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Add a finding during a session
1080
+ */
1081
+ async addFinding(sessionId, options) {
1082
+ try {
1083
+ const { data, error } = await this.supabase.rpc("add_session_finding", {
1084
+ p_session_id: sessionId,
1085
+ p_type: options.type,
1086
+ p_title: options.title,
1087
+ p_description: options.description || null,
1088
+ p_severity: options.severity || "observation",
1089
+ p_route: options.route || null,
1090
+ p_screenshot_url: options.screenshotUrl || null,
1091
+ p_console_logs: options.consoleLogs || null,
1092
+ p_network_snapshot: options.networkSnapshot || null,
1093
+ p_device_info: options.deviceInfo || null,
1094
+ p_app_context: options.appContext || null
1095
+ });
1096
+ if (error) {
1097
+ console.error("BugBear: Failed to add finding", error);
1098
+ return { success: false, error: error.message };
1099
+ }
1100
+ const finding = this.transformFinding(data);
1101
+ return { success: true, finding };
1102
+ } catch (err) {
1103
+ const message = err instanceof Error ? err.message : "Unknown error";
1104
+ console.error("BugBear: Error adding finding", err);
1105
+ return { success: false, error: message };
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Get findings for a session
1110
+ */
1111
+ async getSessionFindings(sessionId) {
1112
+ try {
1113
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
1114
+ if (error) {
1115
+ console.error("BugBear: Failed to fetch findings", error);
1116
+ return [];
1117
+ }
1118
+ return (data || []).map((f) => this.transformFinding(f));
1119
+ } catch (err) {
1120
+ console.error("BugBear: Error fetching findings", err);
1121
+ return [];
1122
+ }
1123
+ }
1124
+ /**
1125
+ * Convert a finding to a bug report
1126
+ */
1127
+ async convertFindingToBug(findingId) {
1128
+ try {
1129
+ const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
1130
+ p_finding_id: findingId
1131
+ });
1132
+ if (error) {
1133
+ console.error("BugBear: Failed to convert finding", error);
1134
+ return { success: false, error: error.message };
1135
+ }
1136
+ return { success: true, bugId: data };
1137
+ } catch (err) {
1138
+ const message = err instanceof Error ? err.message : "Unknown error";
1139
+ console.error("BugBear: Error converting finding", err);
1140
+ return { success: false, error: message };
1141
+ }
1142
+ }
1143
+ /**
1144
+ * Dismiss a finding
1145
+ */
1146
+ async dismissFinding(findingId, reason) {
1147
+ try {
1148
+ const { error } = await this.supabase.from("qa_findings").update({
1149
+ dismissed: true,
1150
+ dismissed_reason: reason || null,
1151
+ dismissed_at: (/* @__PURE__ */ new Date()).toISOString()
1152
+ }).eq("id", findingId);
1153
+ if (error) {
1154
+ console.error("BugBear: Failed to dismiss finding", error);
1155
+ return { success: false, error: error.message };
1156
+ }
1157
+ return { success: true };
1158
+ } catch (err) {
1159
+ const message = err instanceof Error ? err.message : "Unknown error";
1160
+ console.error("BugBear: Error dismissing finding", err);
1161
+ return { success: false, error: message };
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Transform database session to QASession type
1166
+ */
1167
+ transformSession(data) {
1168
+ return {
1169
+ id: data.id,
1170
+ projectId: data.project_id,
1171
+ testerId: data.tester_id,
1172
+ focusArea: data.focus_area,
1173
+ track: data.track,
1174
+ platform: data.platform,
1175
+ startedAt: data.started_at,
1176
+ endedAt: data.ended_at,
1177
+ notes: data.notes,
1178
+ routesCovered: data.routes_covered || [],
1179
+ status: data.status,
1180
+ durationMinutes: data.duration_minutes,
1181
+ findingsCount: data.findings_count || 0,
1182
+ bugsFiled: data.bugs_filed || 0,
1183
+ createdAt: data.created_at,
1184
+ updatedAt: data.updated_at
1185
+ };
1186
+ }
1187
+ /**
1188
+ * Transform database finding to QAFinding type
1189
+ */
1190
+ transformFinding(data) {
1191
+ return {
1192
+ id: data.id,
1193
+ sessionId: data.session_id,
1194
+ projectId: data.project_id,
1195
+ type: data.type,
1196
+ severity: data.severity,
1197
+ title: data.title,
1198
+ description: data.description,
1199
+ route: data.route,
1200
+ screenshotUrl: data.screenshot_url,
1201
+ consoleLogs: data.console_logs,
1202
+ networkSnapshot: data.network_snapshot,
1203
+ deviceInfo: data.device_info,
1204
+ appContext: data.app_context,
1205
+ convertedToBugId: data.converted_to_bug_id,
1206
+ convertedToTestId: data.converted_to_test_id,
1207
+ dismissed: data.dismissed || false,
1208
+ dismissedReason: data.dismissed_reason,
1209
+ dismissedAt: data.dismissed_at,
1210
+ createdAt: data.created_at,
1211
+ updatedAt: data.updated_at
1212
+ };
1213
+ }
573
1214
  };
574
1215
  function createBugBear(config) {
575
1216
  return new BugBearClient(config);