quick-bug-reporter-react 1.3.2 → 1.5.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.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { NetworkLogger, BugSessionArtifacts, RecordingStopReason, ReportCaptureMode, BugReporterIntegration, BugClientMetadata, ConsoleLogEntry, CapturedJsError, SubmitProgressCallback, BugSubmitResult, BugTrackerProvider, BugReporterIntegrations, ScreenshotHighlightRegion } from '@quick-bug-reporter/core';
1
+ import { NetworkLogger, BugSessionArtifacts, RecordingStopReason, ReportCaptureMode, NetworkLogEntry, BugReporterIntegration, BugClientMetadata, ConsoleLogEntry, CapturedJsError, SubmitProgressCallback, BugSubmitResult, BugTrackerProvider, BugReporterIntegrations, ScreenshotHighlightRegion } from '@quick-bug-reporter/core';
2
2
  export { BugClientMetadata, BugReportPayload, BugReporterIntegration, BugReporterIntegrations, BugSessionArtifacts, BugSubmitResult, BugTrackerProvider, CapturedJsError, CloudIntegration, CloudIntegrationOptions, ConsoleCapture, ConsoleLogEntry, DEFAULT_MAX_RECORDING_MS, JiraIntegration, JiraIntegrationOptions, LinearIntegration, LinearIntegrationOptions, NetworkLogEntry, NetworkLogger, RecordingStopReason, ReportCaptureMode, ScreenshotHighlightRegion, SubmitProgressCallback, formatConsoleLogs, formatJsErrors, formatNetworkLogs, toErrorMessage } from '@quick-bug-reporter/core';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { ReactNode } from 'react';
@@ -62,6 +62,7 @@ declare class BugSession {
62
62
  private autoStopTimeout;
63
63
  private stopInFlight;
64
64
  private lastArtifacts;
65
+ private screenshotLogsPendingSubmit;
65
66
  constructor(options?: BugSessionOptions);
66
67
  start(): Promise<void>;
67
68
  captureScreenshot(region?: CaptureRegion): Promise<BugSessionArtifacts>;
@@ -71,6 +72,7 @@ declare class BugSession {
71
72
  getMaxDurationMs(): number;
72
73
  getLastArtifacts(): BugSessionArtifacts | null;
73
74
  getLastCaptureMode(): ReportCaptureMode | null;
75
+ finalizeNetworkLogsForSubmit(captureMode: ReportCaptureMode): NetworkLogEntry[];
74
76
  resetArtifacts(): void;
75
77
  dispose(): Promise<void>;
76
78
  private stopInternal;
@@ -90,6 +92,10 @@ type BugReporterSubmitOptions = {
90
92
  consoleLogs?: ConsoleLogEntry[];
91
93
  jsErrors?: CapturedJsError[];
92
94
  onProgress?: SubmitProgressCallback;
95
+ stepsToReproduce?: string;
96
+ expectedResult?: string;
97
+ actualResult?: string;
98
+ additionalContext?: string;
93
99
  };
94
100
  declare class BugReporter {
95
101
  private integration;
@@ -151,7 +157,12 @@ type BugReporterContextValue = {
151
157
  screenshotHighlightCount: number;
152
158
  updateScreenshotAnnotation: (annotation: ScreenshotAnnotationState) => void;
153
159
  clearDraft: () => void;
154
- submitReport: (title: string, description: string) => Promise<BugSubmitResult | null>;
160
+ submitReport: (title: string, structuredFields: {
161
+ stepsToReproduce: string;
162
+ expectedResult: string;
163
+ actualResult: string;
164
+ additionalContext: string;
165
+ }) => Promise<BugSubmitResult | null>;
155
166
  resetMessages: () => void;
156
167
  };
157
168
  declare function BugReporterProvider({ children, integrations, defaultProvider, maxDurationMs, }: BugReporterProviderProps): react_jsx_runtime.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NetworkLogger, BugSessionArtifacts, RecordingStopReason, ReportCaptureMode, BugReporterIntegration, BugClientMetadata, ConsoleLogEntry, CapturedJsError, SubmitProgressCallback, BugSubmitResult, BugTrackerProvider, BugReporterIntegrations, ScreenshotHighlightRegion } from '@quick-bug-reporter/core';
1
+ import { NetworkLogger, BugSessionArtifacts, RecordingStopReason, ReportCaptureMode, NetworkLogEntry, BugReporterIntegration, BugClientMetadata, ConsoleLogEntry, CapturedJsError, SubmitProgressCallback, BugSubmitResult, BugTrackerProvider, BugReporterIntegrations, ScreenshotHighlightRegion } from '@quick-bug-reporter/core';
2
2
  export { BugClientMetadata, BugReportPayload, BugReporterIntegration, BugReporterIntegrations, BugSessionArtifacts, BugSubmitResult, BugTrackerProvider, CapturedJsError, CloudIntegration, CloudIntegrationOptions, ConsoleCapture, ConsoleLogEntry, DEFAULT_MAX_RECORDING_MS, JiraIntegration, JiraIntegrationOptions, LinearIntegration, LinearIntegrationOptions, NetworkLogEntry, NetworkLogger, RecordingStopReason, ReportCaptureMode, ScreenshotHighlightRegion, SubmitProgressCallback, formatConsoleLogs, formatJsErrors, formatNetworkLogs, toErrorMessage } from '@quick-bug-reporter/core';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { ReactNode } from 'react';
@@ -62,6 +62,7 @@ declare class BugSession {
62
62
  private autoStopTimeout;
63
63
  private stopInFlight;
64
64
  private lastArtifacts;
65
+ private screenshotLogsPendingSubmit;
65
66
  constructor(options?: BugSessionOptions);
66
67
  start(): Promise<void>;
67
68
  captureScreenshot(region?: CaptureRegion): Promise<BugSessionArtifacts>;
@@ -71,6 +72,7 @@ declare class BugSession {
71
72
  getMaxDurationMs(): number;
72
73
  getLastArtifacts(): BugSessionArtifacts | null;
73
74
  getLastCaptureMode(): ReportCaptureMode | null;
75
+ finalizeNetworkLogsForSubmit(captureMode: ReportCaptureMode): NetworkLogEntry[];
74
76
  resetArtifacts(): void;
75
77
  dispose(): Promise<void>;
76
78
  private stopInternal;
@@ -90,6 +92,10 @@ type BugReporterSubmitOptions = {
90
92
  consoleLogs?: ConsoleLogEntry[];
91
93
  jsErrors?: CapturedJsError[];
92
94
  onProgress?: SubmitProgressCallback;
95
+ stepsToReproduce?: string;
96
+ expectedResult?: string;
97
+ actualResult?: string;
98
+ additionalContext?: string;
93
99
  };
94
100
  declare class BugReporter {
95
101
  private integration;
@@ -151,7 +157,12 @@ type BugReporterContextValue = {
151
157
  screenshotHighlightCount: number;
152
158
  updateScreenshotAnnotation: (annotation: ScreenshotAnnotationState) => void;
153
159
  clearDraft: () => void;
154
- submitReport: (title: string, description: string) => Promise<BugSubmitResult | null>;
160
+ submitReport: (title: string, structuredFields: {
161
+ stepsToReproduce: string;
162
+ expectedResult: string;
163
+ actualResult: string;
164
+ additionalContext: string;
165
+ }) => Promise<BugSubmitResult | null>;
155
166
  resetMessages: () => void;
156
167
  };
157
168
  declare function BugReporterProvider({ children, integrations, defaultProvider, maxDurationMs, }: BugReporterProviderProps): react_jsx_runtime.JSX.Element;
package/dist/index.js CHANGED
@@ -876,42 +876,104 @@ var CloudIntegration = class {
876
876
  throw new Error("CloudIntegration: projectKey is required.");
877
877
  }
878
878
  this.projectKey = options.projectKey;
879
- this.endpoint = options.endpoint ?? "/api/ingest";
879
+ this.endpoint = options.ingestUrl ?? options.endpoint ?? "/api/ingest";
880
+ this.appVersion = options.appVersion;
881
+ this.environment = options.environment;
880
882
  this.fetchFn = options.fetchImpl ?? fetch.bind(globalThis);
881
883
  }
882
884
  async submit(payload, onProgress = noop3) {
883
885
  onProgress("Preparing report\u2026");
884
- const ua = payload.userAgent || navigator.userAgent;
886
+ const ua = payload.userAgent || getRuntimeUserAgent();
885
887
  const browserName = parseBrowserName(ua);
888
+ const browserVersion = parseBrowserVersion(ua);
886
889
  const osName = parseOsName(ua);
887
- const body = {
888
- project_key: this.projectKey,
889
- title: payload.title,
890
- provider: "cloud",
891
- capture_mode: payload.captureMode,
892
- has_screenshot: Boolean(payload.screenshotBlob),
893
- has_video: Boolean(payload.videoBlob),
894
- has_network_logs: payload.networkLogs.length > 0,
895
- has_console_logs: payload.consoleLogs.length > 0,
896
- js_error_count: payload.jsErrors.length,
897
- user_agent: ua,
898
- browser_name: browserName,
899
- os_name: osName,
900
- device_type: getDeviceType(),
901
- screen_resolution: getScreenResolution(),
902
- viewport: getViewport(),
903
- color_scheme: payload.metadata.colorScheme !== "unknown" ? payload.metadata.colorScheme : null,
904
- locale: payload.metadata.locale,
905
- timezone: payload.metadata.timezone,
906
- connection_type: payload.metadata.connection?.effectiveType ?? null,
907
- page_url: payload.pageUrl,
908
- environment: getEnvironment()
909
- };
890
+ const osVersion = parseOsVersion(ua, osName);
891
+ const fd = new FormData();
892
+ fd.set("project_key", this.projectKey);
893
+ fd.set("title", payload.title);
894
+ fd.set("description", payload.description || "");
895
+ if (payload.stepsToReproduce) fd.set("steps_to_reproduce", payload.stepsToReproduce);
896
+ if (payload.expectedResult) fd.set("expected_result", payload.expectedResult);
897
+ if (payload.actualResult) fd.set("actual_result", payload.actualResult);
898
+ if (payload.additionalContext) fd.set("additional_context", payload.additionalContext);
899
+ fd.set("provider", "cloud");
900
+ fd.set("capture_mode", payload.captureMode);
901
+ fd.set("has_screenshot", String(Boolean(payload.screenshotBlob)));
902
+ fd.set("has_video", String(Boolean(payload.videoBlob)));
903
+ fd.set("has_network_logs", String(payload.networkLogs.length > 0));
904
+ fd.set("has_console_logs", String(payload.consoleLogs.length > 0));
905
+ fd.set("js_error_count", String(payload.jsErrors.length));
906
+ fd.set("user_agent", ua);
907
+ fd.set("browser_name", browserName);
908
+ fd.set("browser_version", browserVersion ?? "");
909
+ fd.set("os_name", osName);
910
+ fd.set("os_version", osVersion ?? "");
911
+ fd.set("device_type", getDeviceType());
912
+ fd.set("screen_resolution", getScreenResolution());
913
+ fd.set("viewport", getViewport());
914
+ fd.set("color_scheme", payload.metadata.colorScheme !== "unknown" ? payload.metadata.colorScheme : "");
915
+ fd.set("locale", payload.metadata.locale ?? "");
916
+ fd.set("timezone", payload.metadata.timezone ?? "");
917
+ fd.set("connection_type", payload.metadata.connection?.effectiveType ?? "");
918
+ fd.set("page_url", payload.pageUrl || "");
919
+ fd.set("environment", this.environment ?? getEnvironment());
920
+ fd.set("app_version", this.appVersion ?? "");
921
+ fd.set("platform", payload.metadata.platform ?? "");
922
+ fd.set("duration_ms", String(payload.elapsedMs));
923
+ fd.set("stopped_at", payload.stoppedAt || "");
924
+ if (payload.metadata.mobile) {
925
+ fd.set("platform", payload.metadata.mobile.platform);
926
+ fd.set("device_model", payload.metadata.mobile.deviceModel ?? "");
927
+ fd.set("device_brand", payload.metadata.mobile.deviceBrand ?? "");
928
+ fd.set("os_version", payload.metadata.mobile.osVersion ?? "");
929
+ fd.set("app_build_number", payload.metadata.mobile.appBuildNumber ?? "");
930
+ fd.set("is_emulator", String(payload.metadata.mobile.isEmulator));
931
+ fd.set(
932
+ "battery_level",
933
+ payload.metadata.mobile.batteryLevel == null ? "" : String(payload.metadata.mobile.batteryLevel)
934
+ );
935
+ fd.set(
936
+ "free_storage_mb",
937
+ payload.metadata.mobile.freeStorageMb == null ? "" : String(payload.metadata.mobile.freeStorageMb)
938
+ );
939
+ fd.set("invocation_method", payload.metadata.mobile.invocationMethod);
940
+ }
941
+ if (payload.screenshotBlob) {
942
+ fd.append("screenshot", payload.screenshotBlob, "bug-screenshot.png");
943
+ }
944
+ if (payload.videoBlob) {
945
+ fd.append("video", payload.videoBlob, "bug-recording.webm");
946
+ }
947
+ fd.append(
948
+ "network_logs",
949
+ new Blob([formatNetworkLogs(payload.networkLogs)], { type: "text/plain" }),
950
+ "network-logs.txt"
951
+ );
952
+ const parts = [];
953
+ if (payload.jsErrors.length > 0) {
954
+ parts.push("=== JavaScript Errors ===\n" + formatJsErrors(payload.jsErrors));
955
+ } else {
956
+ parts.push("=== JavaScript Errors ===\nNo JavaScript errors captured.");
957
+ }
958
+ if (payload.consoleLogs.length > 0) {
959
+ parts.push("=== Console Output ===\n" + formatConsoleLogs(payload.consoleLogs));
960
+ } else {
961
+ parts.push("=== Console Output ===\nNo console output captured.");
962
+ }
963
+ fd.append(
964
+ "console_logs",
965
+ new Blob([parts.join("\n\n")], { type: "text/plain" }),
966
+ "console-logs.txt"
967
+ );
968
+ fd.append(
969
+ "metadata",
970
+ new Blob([JSON.stringify(payload.metadata, null, 2)], { type: "application/json" }),
971
+ "client-metadata.json"
972
+ );
910
973
  onProgress("Sending report\u2026");
911
974
  const response = await this.fetchFn(this.endpoint, {
912
975
  method: "POST",
913
- headers: { "Content-Type": "application/json" },
914
- body: JSON.stringify(body)
976
+ body: fd
915
977
  });
916
978
  if (!response.ok) {
917
979
  const errorBody = await response.json().catch(() => ({}));
@@ -920,12 +982,22 @@ var CloudIntegration = class {
920
982
  }
921
983
  const result = await response.json();
922
984
  onProgress("Report submitted.");
985
+ const fwd = result.forwarding;
986
+ const externalKey = fwd?.key;
987
+ const externalUrl = fwd?.url;
988
+ const warnings = [];
989
+ if (result.forwarding_status === "queued") {
990
+ warnings.push("Tracker forwarding is running in the background.");
991
+ }
992
+ if (fwd?.error) {
993
+ warnings.push(`Forwarding: ${fwd.error}`);
994
+ }
923
995
  return {
924
996
  provider: "cloud",
925
997
  issueId: result.id,
926
- issueKey: `QB-${result.id.slice(0, 8)}`,
927
- issueUrl: null,
928
- warnings: []
998
+ issueKey: externalKey || `QB-${result.id.slice(0, 8)}`,
999
+ issueUrl: externalUrl || null,
1000
+ warnings
929
1001
  };
930
1002
  }
931
1003
  };
@@ -937,14 +1009,50 @@ function parseBrowserName(ua) {
937
1009
  if (ua.includes("Safari/") && !ua.includes("Chrome/")) return "Safari";
938
1010
  return "Unknown";
939
1011
  }
1012
+ function parseBrowserVersion(ua) {
1013
+ return matchVersion(ua, /Edg\/([\d.]+)/) || matchVersion(ua, /OPR\/([\d.]+)/) || matchVersion(ua, /Opera\/([\d.]+)/) || matchVersion(ua, /Chrome\/([\d.]+)/) || matchVersion(ua, /Firefox\/([\d.]+)/) || matchVersion(ua, /Version\/([\d.]+).*Safari/);
1014
+ }
1015
+ function getRuntimeUserAgent() {
1016
+ if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
1017
+ return navigator.userAgent;
1018
+ }
1019
+ return "";
1020
+ }
940
1021
  function parseOsName(ua) {
941
1022
  if (ua.includes("Windows")) return "Windows";
942
- if (ua.includes("Mac OS")) return "macOS";
943
- if (ua.includes("Linux")) return "Linux";
944
1023
  if (ua.includes("Android")) return "Android";
945
1024
  if (ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
1025
+ if (ua.includes("Mac OS")) return "macOS";
1026
+ if (ua.includes("Linux")) return "Linux";
946
1027
  return "Unknown";
947
1028
  }
1029
+ function parseOsVersion(ua, osName) {
1030
+ if (osName === "Windows") {
1031
+ const nt = matchVersion(ua, /Windows NT ([\d.]+)/);
1032
+ if (!nt) return null;
1033
+ if (nt.startsWith("10.0")) return "10/11";
1034
+ if (nt.startsWith("6.3")) return "8.1";
1035
+ if (nt.startsWith("6.2")) return "8";
1036
+ if (nt.startsWith("6.1")) return "7";
1037
+ return nt;
1038
+ }
1039
+ if (osName === "macOS") {
1040
+ const mac = matchVersion(ua, /Mac OS X ([0-9_]+)/);
1041
+ return mac ? mac.replace(/_/g, ".") : null;
1042
+ }
1043
+ if (osName === "Android") {
1044
+ return matchVersion(ua, /Android ([\d.]+)/);
1045
+ }
1046
+ if (osName === "iOS") {
1047
+ const ios = matchVersion(ua, /OS ([0-9_]+) like Mac OS X/);
1048
+ return ios ? ios.replace(/_/g, ".") : null;
1049
+ }
1050
+ return null;
1051
+ }
1052
+ function matchVersion(ua, pattern) {
1053
+ const match = ua.match(pattern);
1054
+ return match?.[1] ?? null;
1055
+ }
948
1056
  function getDeviceType() {
949
1057
  if (typeof window === "undefined") return "unknown";
950
1058
  const w = window.innerWidth;
@@ -1423,6 +1531,7 @@ var BugSession = class {
1423
1531
  this.autoStopTimeout = null;
1424
1532
  this.stopInFlight = null;
1425
1533
  this.lastArtifacts = null;
1534
+ this.screenshotLogsPendingSubmit = false;
1426
1535
  this.maxDurationMs = options.maxDurationMs ?? DEFAULT_MAX_RECORDING_MS;
1427
1536
  this.screenRecorder = options.screenRecorder ?? new ScreenRecorder();
1428
1537
  this.screenshotCapturer = options.screenshotCapturer ?? new ScreenshotCapturer();
@@ -1433,9 +1542,13 @@ var BugSession = class {
1433
1542
  if (this.recording) {
1434
1543
  return;
1435
1544
  }
1545
+ if (this.networkLogger.isRecording()) {
1546
+ this.networkLogger.stop();
1547
+ }
1436
1548
  this.clearAutoStopTimer();
1437
1549
  this.networkLogger.clear();
1438
1550
  this.lastArtifacts = null;
1551
+ this.screenshotLogsPendingSubmit = false;
1439
1552
  this.networkLogger.start();
1440
1553
  try {
1441
1554
  await this.screenRecorder.start({
@@ -1458,14 +1571,18 @@ var BugSession = class {
1458
1571
  if (this.recording) {
1459
1572
  await this.stop("manual");
1460
1573
  }
1574
+ if (this.networkLogger.isRecording()) {
1575
+ this.networkLogger.stop();
1576
+ }
1461
1577
  this.clearAutoStopTimer();
1462
1578
  this.networkLogger.clear();
1463
1579
  this.lastArtifacts = null;
1580
+ this.screenshotLogsPendingSubmit = false;
1464
1581
  const startedAtMs = Date.now();
1465
1582
  this.networkLogger.start();
1466
1583
  try {
1467
1584
  const screenshotBlob = region ? await this.screenshotCapturer.captureRegion(region) : await this.screenshotCapturer.capture();
1468
- const networkLogs = this.networkLogger.stop();
1585
+ const networkLogs = this.networkLogger.getLogs();
1469
1586
  const stoppedAtMs = Date.now();
1470
1587
  const artifacts = {
1471
1588
  videoBlob: null,
@@ -1478,10 +1595,12 @@ var BugSession = class {
1478
1595
  stopReason: "manual"
1479
1596
  };
1480
1597
  this.lastArtifacts = artifacts;
1598
+ this.screenshotLogsPendingSubmit = true;
1481
1599
  return artifacts;
1482
1600
  } catch (error) {
1483
1601
  this.networkLogger.stop();
1484
1602
  this.networkLogger.clear();
1603
+ this.screenshotLogsPendingSubmit = false;
1485
1604
  throw error;
1486
1605
  }
1487
1606
  }
@@ -1515,17 +1634,32 @@ var BugSession = class {
1515
1634
  getLastCaptureMode() {
1516
1635
  return this.lastArtifacts?.captureMode ?? null;
1517
1636
  }
1637
+ finalizeNetworkLogsForSubmit(captureMode) {
1638
+ if (captureMode === "screenshot" && this.screenshotLogsPendingSubmit && this.networkLogger.isRecording()) {
1639
+ const logs = this.networkLogger.stop();
1640
+ this.screenshotLogsPendingSubmit = false;
1641
+ return logs;
1642
+ }
1643
+ return this.networkLogger.getLogs();
1644
+ }
1518
1645
  resetArtifacts() {
1519
1646
  this.lastArtifacts = null;
1520
1647
  this.screenRecorder.clearLastBlob();
1648
+ if (this.networkLogger.isRecording()) {
1649
+ this.networkLogger.stop();
1650
+ }
1521
1651
  this.networkLogger.clear();
1652
+ this.screenshotLogsPendingSubmit = false;
1522
1653
  }
1523
1654
  async dispose() {
1524
1655
  await this.stop("manual");
1525
1656
  this.clearAutoStopTimer();
1526
1657
  this.screenRecorder.dispose();
1527
- this.networkLogger.stop();
1658
+ if (this.networkLogger.isRecording()) {
1659
+ this.networkLogger.stop();
1660
+ }
1528
1661
  this.networkLogger.clear();
1662
+ this.screenshotLogsPendingSubmit = false;
1529
1663
  }
1530
1664
  async stopInternal(reason) {
1531
1665
  this.clearAutoStopTimer();
@@ -1546,6 +1680,7 @@ var BugSession = class {
1546
1680
  stopReason: reason
1547
1681
  };
1548
1682
  this.lastArtifacts = artifacts;
1683
+ this.screenshotLogsPendingSubmit = false;
1549
1684
  return artifacts;
1550
1685
  }
1551
1686
  async handleForcedStop(reason) {
@@ -1696,9 +1831,13 @@ var BugReporter = class {
1696
1831
  const payload = {
1697
1832
  title: normalizedTitle,
1698
1833
  description: normalizedDescription,
1834
+ stepsToReproduce: options.stepsToReproduce,
1835
+ expectedResult: options.expectedResult,
1836
+ actualResult: options.actualResult,
1837
+ additionalContext: options.additionalContext,
1699
1838
  videoBlob: artifacts.videoBlob,
1700
1839
  screenshotBlob: options.screenshotBlob ?? artifacts.screenshotBlob,
1701
- networkLogs: artifacts.networkLogs,
1840
+ networkLogs: this.session.finalizeNetworkLogsForSubmit(artifacts.captureMode),
1702
1841
  consoleLogs: options.consoleLogs ?? [],
1703
1842
  jsErrors: options.jsErrors ?? [],
1704
1843
  captureMode: artifacts.captureMode,
@@ -2228,7 +2367,7 @@ function BugReporterProvider({
2228
2367
  setScreenshotAnnotation(annotation);
2229
2368
  }, []);
2230
2369
  const submitReport = useCallback(
2231
- async (title, description) => {
2370
+ async (title, structuredFields) => {
2232
2371
  const reporter = getOrCreateReporter();
2233
2372
  if (!reporter) {
2234
2373
  setError("No bug tracker integration is configured.");
@@ -2248,6 +2387,25 @@ function BugReporterProvider({
2248
2387
  setSubmissionProgress("Preparing submission\u2026");
2249
2388
  setError(null);
2250
2389
  setSuccess(null);
2390
+ const { stepsToReproduce, expectedResult, actualResult, additionalContext } = structuredFields;
2391
+ const sections = [];
2392
+ if (stepsToReproduce.trim()) {
2393
+ sections.push(`## Steps to Reproduce
2394
+ ${stepsToReproduce.trim()}`);
2395
+ }
2396
+ if (expectedResult.trim()) {
2397
+ sections.push(`## Expected Result
2398
+ ${expectedResult.trim()}`);
2399
+ }
2400
+ if (actualResult.trim()) {
2401
+ sections.push(`## Actual Result
2402
+ ${actualResult.trim()}`);
2403
+ }
2404
+ if (additionalContext.trim()) {
2405
+ sections.push(`## Additional Context
2406
+ ${additionalContext.trim()}`);
2407
+ }
2408
+ const description = sections.length > 0 ? sections.join("\n\n") : "No description provided";
2251
2409
  const screenshotBlobForSubmit = draftMode === "screenshot" ? screenshotAnnotation.annotatedBlob ?? screenshotBlob : null;
2252
2410
  const metadata = {
2253
2411
  annotation: draftMode === "screenshot" && screenshotAnnotation.highlights.length > 0 ? {
@@ -2262,13 +2420,21 @@ function BugReporterProvider({
2262
2420
  };
2263
2421
  try {
2264
2422
  const result = await reporter.submit(title, description, {
2423
+ stepsToReproduce,
2424
+ expectedResult,
2425
+ actualResult,
2426
+ additionalContext,
2265
2427
  screenshotBlob: screenshotBlobForSubmit,
2266
2428
  metadata,
2267
2429
  consoleLogs,
2268
2430
  jsErrors,
2269
2431
  onProgress: setSubmissionProgress
2270
2432
  });
2271
- setSuccess(`Submitted to ${getProviderLabel(result.provider)} (${result.issueKey}).`);
2433
+ if (result.provider === "cloud" && !result.issueUrl) {
2434
+ setSuccess("Report received by QuickBugs Cloud. Tracker forwarding is running in the background.");
2435
+ } else {
2436
+ setSuccess(`Submitted to ${getProviderLabel(result.provider)} (${result.issueKey}).`);
2437
+ }
2272
2438
  clearDraft();
2273
2439
  setIsOpen(false);
2274
2440
  return result;
@@ -3055,6 +3221,7 @@ function providerLabel(provider) {
3055
3221
  if (provider === "cloud") return "QuickBugs Cloud";
3056
3222
  return provider;
3057
3223
  }
3224
+ var CHAR_LIMIT = 4e3;
3058
3225
  function BugReporterModal() {
3059
3226
  const {
3060
3227
  autoStopNotice,
@@ -3084,9 +3251,39 @@ function BugReporterModal() {
3084
3251
  videoPreviewUrl
3085
3252
  } = useBugReporter();
3086
3253
  const [title, setTitle] = useState("");
3087
- const [description, setDescription] = useState("");
3254
+ const [stepsToReproduce, setStepsToReproduce] = useState("");
3255
+ const [expectedResult, setExpectedResult] = useState("");
3256
+ const [actualResult, setActualResult] = useState("");
3257
+ const [additionalContext, setAdditionalContext] = useState("");
3088
3258
  const [step, setStep] = useState("review");
3259
+ const [activeTab, setActiveTab] = useState("steps");
3260
+ const totalChars = stepsToReproduce.length + expectedResult.length + actualResult.length + additionalContext.length;
3261
+ const isOverLimit = totalChars > CHAR_LIMIT;
3089
3262
  const elapsedLabel = useMemo(() => formatElapsed2(elapsedMs), [elapsedMs]);
3263
+ const handleStepsKeyDown = (event) => {
3264
+ if (event.key === "Enter" && !event.shiftKey) {
3265
+ event.preventDefault();
3266
+ const textarea = event.currentTarget;
3267
+ const cursorPos = textarea.selectionStart;
3268
+ const textBeforeCursor = stepsToReproduce.substring(0, cursorPos);
3269
+ const textAfterCursor = stepsToReproduce.substring(cursorPos);
3270
+ const lines = textBeforeCursor.split("\n");
3271
+ const currentLine = lines[lines.length - 1];
3272
+ const numberMatch = currentLine.match(/^(\d+)\.\s/);
3273
+ const nextNumber = numberMatch ? parseInt(numberMatch[1]) + 1 : lines.length > 0 && stepsToReproduce.trim() === "" ? 1 : lines.length + 1;
3274
+ const newText = textBeforeCursor + "\n" + nextNumber + ". " + textAfterCursor;
3275
+ setStepsToReproduce(newText);
3276
+ setTimeout(() => {
3277
+ const newCursorPos = cursorPos + ("\n" + nextNumber + ". ").length;
3278
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
3279
+ }, 0);
3280
+ }
3281
+ };
3282
+ const handleStepsFocus = () => {
3283
+ if (stepsToReproduce.trim() === "") {
3284
+ setStepsToReproduce("1. ");
3285
+ }
3286
+ };
3090
3287
  const handleDialogOpenChange = (open) => {
3091
3288
  if (open) {
3092
3289
  openModal();
@@ -3097,15 +3294,24 @@ function BugReporterModal() {
3097
3294
  };
3098
3295
  const handleSubmit = async (event) => {
3099
3296
  event.preventDefault();
3100
- const result = await submitReport(title, description);
3297
+ const result = await submitReport(title, {
3298
+ stepsToReproduce,
3299
+ expectedResult,
3300
+ actualResult,
3301
+ additionalContext
3302
+ });
3101
3303
  if (result) {
3102
3304
  setTitle("");
3103
- setDescription("");
3305
+ setStepsToReproduce("");
3306
+ setExpectedResult("");
3307
+ setActualResult("");
3308
+ setAdditionalContext("");
3104
3309
  setStep("review");
3310
+ setActiveTab("steps");
3105
3311
  }
3106
3312
  };
3107
3313
  const hasIntegrations = availableProviders.length > 0;
3108
- const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0;
3314
+ const canSubmit = !isSubmitting && !isCapturingScreenshot && hasIntegrations && !!selectedProvider && hasDraft && title.trim().length > 0 && !isOverLimit;
3109
3315
  return /* @__PURE__ */ jsx(Dialog, { open: isOpen, onOpenChange: handleDialogOpenChange, children: /* @__PURE__ */ jsx(
3110
3316
  DialogContent,
3111
3317
  {
@@ -3271,18 +3477,91 @@ function BugReporterModal() {
3271
3477
  }
3272
3478
  )
3273
3479
  ] }),
3274
- /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 sm:col-span-2", children: [
3275
- /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", htmlFor: "bug-description", children: "Quick note" }),
3276
- /* @__PURE__ */ jsx(
3480
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 sm:col-span-2", children: [
3481
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", children: "Bug Details" }),
3482
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1 border-b border-gray-200", children: [
3483
+ /* @__PURE__ */ jsx(
3484
+ "button",
3485
+ {
3486
+ type: "button",
3487
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "steps" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3488
+ onClick: () => setActiveTab("steps"),
3489
+ children: "Steps"
3490
+ }
3491
+ ),
3492
+ /* @__PURE__ */ jsx(
3493
+ "button",
3494
+ {
3495
+ type: "button",
3496
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "expected" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3497
+ onClick: () => setActiveTab("expected"),
3498
+ children: "Expected"
3499
+ }
3500
+ ),
3501
+ /* @__PURE__ */ jsx(
3502
+ "button",
3503
+ {
3504
+ type: "button",
3505
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "actual" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3506
+ onClick: () => setActiveTab("actual"),
3507
+ children: "Actual"
3508
+ }
3509
+ ),
3510
+ /* @__PURE__ */ jsx(
3511
+ "button",
3512
+ {
3513
+ type: "button",
3514
+ className: `px-3 py-2 text-sm font-medium transition-colors ${activeTab === "context" ? "border-b-2 border-indigo-600 text-indigo-600" : "text-gray-500 hover:text-gray-700"}`,
3515
+ onClick: () => setActiveTab("context"),
3516
+ children: "Context"
3517
+ }
3518
+ )
3519
+ ] }),
3520
+ activeTab === "steps" && /* @__PURE__ */ jsx(
3277
3521
  Textarea,
3278
3522
  {
3279
- id: "bug-description",
3280
- maxLength: 4e3,
3281
- placeholder: "What did you expect, what happened, and any quick repro steps.",
3282
- value: description,
3283
- onChange: (event) => setDescription(event.target.value)
3523
+ id: "bug-steps",
3524
+ placeholder: "Press Enter to start numbering steps...",
3525
+ value: stepsToReproduce,
3526
+ onChange: (event) => setStepsToReproduce(event.target.value),
3527
+ onKeyDown: handleStepsKeyDown,
3528
+ onFocus: handleStepsFocus
3284
3529
  }
3285
- )
3530
+ ),
3531
+ activeTab === "expected" && /* @__PURE__ */ jsx(
3532
+ Textarea,
3533
+ {
3534
+ id: "bug-expected",
3535
+ placeholder: "Describe what should happen...",
3536
+ value: expectedResult,
3537
+ onChange: (event) => setExpectedResult(event.target.value)
3538
+ }
3539
+ ),
3540
+ activeTab === "actual" && /* @__PURE__ */ jsx(
3541
+ Textarea,
3542
+ {
3543
+ id: "bug-actual",
3544
+ placeholder: "Describe what actually happened...",
3545
+ value: actualResult,
3546
+ onChange: (event) => setActualResult(event.target.value)
3547
+ }
3548
+ ),
3549
+ activeTab === "context" && /* @__PURE__ */ jsx(
3550
+ Textarea,
3551
+ {
3552
+ id: "bug-context",
3553
+ placeholder: "Any additional information, workarounds, or notes...",
3554
+ value: additionalContext,
3555
+ onChange: (event) => setAdditionalContext(event.target.value)
3556
+ }
3557
+ ),
3558
+ /* @__PURE__ */ jsxs("p", { className: `text-xs ${isOverLimit ? "text-red-600 font-medium" : "text-gray-500"}`, children: [
3559
+ totalChars,
3560
+ "/",
3561
+ CHAR_LIMIT,
3562
+ " characters ",
3563
+ isOverLimit && "(over limit)"
3564
+ ] })
3286
3565
  ] }),
3287
3566
  /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3288
3567
  /* @__PURE__ */ jsx("label", { className: "text-sm font-medium", htmlFor: "bug-provider", children: "Submit to" }),