@sailfish-ai/recorder 1.7.42 → 1.7.47

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/constants.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export const DomContentEventId = 24;
2
2
  export const NetworkRequestEventId = 27;
3
3
  export const xSf3RidHeader = "X-Sf3-Rid";
4
+ // Values for DomContentLoadedEvent
4
5
  export const DomContentSource = {
5
6
  loading: 0,
6
7
  contentLoaded: 1,
@@ -8,6 +9,7 @@ export const DomContentSource = {
8
9
  beforeunload: 3,
9
10
  unload: 4,
10
11
  };
12
+ // Values for NetworkRequestEvent
11
13
  export const Loading = "loading";
12
14
  export const Complete = "complete";
13
15
  export const STATIC_EXTENSIONS = [
@@ -1,4 +1,5 @@
1
1
  const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
2
+ // A wrapper around fetch that suppresses connection refused errors
2
3
  export function silentFetch(input, init) {
3
4
  return new Promise((resolve, reject) => {
4
5
  fetch(input, init)
@@ -1,4 +1,21 @@
1
1
  import { createTriageFromRecorder } from "./graphql";
2
+ // TODO - enable configuration by keyboard typing in UI
3
+ const DEFAULT_SHORTCUTS = {
4
+ enabled: false,
5
+ openModalExistingMode: { key: "e", requireCmdCtrl: false },
6
+ openModalCaptureNewMode: { key: "n", requireCmdCtrl: false },
7
+ closeModal: { key: "escape", requireCmdCtrl: false },
8
+ submitReport: { key: "enter", requireCmdCtrl: true },
9
+ startRecording: { key: "r", requireCmdCtrl: false },
10
+ stopRecording: { key: "escape", requireCmdCtrl: true },
11
+ };
12
+ export const ReportIssueContext = {
13
+ shortcuts: { ...DEFAULT_SHORTCUTS },
14
+ resolveSessionId: null,
15
+ apiKey: null,
16
+ backendApi: null,
17
+ triageBaseUrl: "https://app.sailfishqa.com",
18
+ };
2
19
  let modalEl = null;
3
20
  let currentState = {
4
21
  mode: "lookback",
@@ -9,12 +26,6 @@ let recordingStartTime = null;
9
26
  let recordingEndTime = null;
10
27
  let timerInterval = null;
11
28
  let isRecording = false;
12
- export const ReportIssueContext = {
13
- resolveSessionId: null,
14
- apiKey: null,
15
- backendApi: null,
16
- triageBaseUrl: "https://app.sailfishqa.com",
17
- };
18
29
  function isMacPlatform() {
19
30
  // Newer API (Chrome, Edge, Opera)
20
31
  const uaData = navigator.userAgentData;
@@ -33,62 +44,118 @@ function isTypingInInput() {
33
44
  el instanceof HTMLTextAreaElement ||
34
45
  (el instanceof HTMLElement && el.isContentEditable));
35
46
  }
47
+ function formatShortcutKeyLabel(key) {
48
+ const map = {
49
+ escape: "esc",
50
+ };
51
+ return (map[key.toLowerCase()] || key).toUpperCase();
52
+ }
53
+ function getShortcutLabel(config) {
54
+ const parts = [];
55
+ if (config.requireCmdCtrl) {
56
+ parts.push(getShortcutKeyCmdCtrlLabel());
57
+ }
58
+ parts.push(formatShortcutKeyLabel(config.key));
59
+ return parts
60
+ .map((p) => `<span style="background: #F1F5F9; border:1px solid #cbd5e1; border-radius: 4px; padding: 0 4px; font-weight: 500; font-size: 12px; color: #94A3B8; line-height: 16px;">${p}</span>`)
61
+ .join(config.requireCmdCtrl ? " + " : "");
62
+ }
63
+ function mergeShortcutsConfig(userConfig) {
64
+ const merged = { ...DEFAULT_SHORTCUTS };
65
+ if (!userConfig)
66
+ return merged;
67
+ // Handle boolean key separately
68
+ if (typeof userConfig.enabled === "boolean") {
69
+ merged.enabled = userConfig.enabled;
70
+ }
71
+ // Handle object keys explicitly
72
+ const objectKeys = [
73
+ "openModalExistingMode",
74
+ "openModalCaptureNewMode",
75
+ "closeModal",
76
+ "submitReport",
77
+ "startRecording",
78
+ "stopRecording",
79
+ ];
80
+ for (const k of objectKeys) {
81
+ const val = userConfig[k];
82
+ if (val && typeof val === "object") {
83
+ merged[k] = { ...merged[k], ...val };
84
+ merged[k].key = merged[k].key.toLowerCase();
85
+ }
86
+ }
87
+ return merged;
88
+ }
89
+ function getShortcutLabelFromContext(shortcutKey) {
90
+ const config = ReportIssueContext.shortcuts[shortcutKey];
91
+ return getShortcutLabel(config);
92
+ }
36
93
  export function setupIssueReporting(options) {
37
94
  ReportIssueContext.apiKey = options.apiKey;
38
95
  ReportIssueContext.backendApi = options.backendApi;
39
96
  ReportIssueContext.resolveSessionId = options.getSessionId;
40
- // Attach keyboard shortcuts
97
+ ReportIssueContext.shortcuts = mergeShortcutsConfig(options.shortcuts);
98
+ const { shortcuts } = ReportIssueContext;
41
99
  window.addEventListener("keydown", (e) => {
42
100
  const typingInInput = isTypingInInput();
43
101
  const key = e.key.toLowerCase();
44
102
  const isCmdOrCtrl = e.metaKey || e.ctrlKey;
45
- // Shortcuts for modal open
46
- if (!typingInInput && options.enableShortcuts && !isCmdOrCtrl) {
47
- if (key === "e") {
48
- e.preventDefault();
49
- injectModalHTML("lookback");
50
- return;
51
- }
52
- if (key === "n") {
53
- e.preventDefault();
54
- injectModalHTML("startnow");
55
- return;
103
+ const modalOpen = !!document.getElementById("sf-report-issue-modal");
104
+ const enableModeShortcuts = !typingInInput && (shortcuts.enabled || modalOpen);
105
+ const shortcutUsed = (shortcutKey) => key === shortcuts[shortcutKey].key &&
106
+ isCmdOrCtrl === shortcuts[shortcutKey].requireCmdCtrl;
107
+ const openModal = modalOpen
108
+ ? (mode) => {
109
+ setActiveTab(mode);
110
+ updateModeSpecificUI(mode);
56
111
  }
112
+ : injectModalHTML;
113
+ // --- Open modal OR switch mode ---
114
+ if (enableModeShortcuts && shortcutUsed("openModalExistingMode")) {
115
+ e.preventDefault();
116
+ openModal("lookback");
117
+ return;
57
118
  }
58
- // Inside modal actions
59
- const modalOpen = !!document.getElementById("sf-report-issue-modal");
60
- if (!modalOpen && !isRecording)
119
+ if (enableModeShortcuts && shortcutUsed("openModalCaptureNewMode")) {
120
+ e.preventDefault();
121
+ openModal("startnow");
61
122
  return;
62
- // Escape key → close modal (only if not recording and modal is open)
63
- if (!isCmdOrCtrl && key === "escape" && modalOpen && !isRecording) {
123
+ }
124
+ // --- Close modal ---
125
+ if (modalOpen && !isRecording && shortcutUsed("closeModal")) {
64
126
  e.preventDefault();
65
127
  closeModal();
66
128
  return;
67
129
  }
68
- // Cmd/Ctrl + Enter → submit
69
- if (isCmdOrCtrl && key === "enter" && modalOpen) {
130
+ // --- Submit report ---
131
+ if (modalOpen && shortcutUsed("submitReport")) {
70
132
  const submitBtn = document.getElementById("sf-issue-submit-btn");
71
133
  if (submitBtn && !submitBtn.disabled) {
72
134
  e.preventDefault();
73
135
  submitBtn.click();
74
136
  }
137
+ return;
75
138
  }
76
- // Cmd/Ctrl + Esc → stop recording
77
- if (isCmdOrCtrl && key === "escape" && isRecording) {
139
+ // --- Stop recording ---
140
+ if (isRecording &&
141
+ key === shortcuts.stopRecording.key &&
142
+ isCmdOrCtrl === shortcuts.stopRecording.requireCmdCtrl) {
78
143
  e.preventDefault();
79
144
  stopRecording();
145
+ return;
80
146
  }
81
- // "r" to start recording (only in capture-new mode)
82
- if (!typingInInput &&
83
- !isCmdOrCtrl &&
84
- key === "r" &&
85
- modalOpen &&
86
- currentState.mode === "startnow") {
147
+ // --- Start recording ---
148
+ if (modalOpen &&
149
+ currentState.mode === "startnow" &&
150
+ key === shortcuts.startRecording.key &&
151
+ isCmdOrCtrl === shortcuts.startRecording.requireCmdCtrl &&
152
+ !typingInInput) {
87
153
  const recordBtn = document.getElementById("sf-start-recording-btn");
88
154
  if (recordBtn) {
89
155
  e.preventDefault();
90
156
  recordBtn.click();
91
157
  }
158
+ return;
92
159
  }
93
160
  });
94
161
  }
@@ -159,11 +226,11 @@ function injectModalHTML(initialMode = "lookback") {
159
226
  <div id="sf-issue-tabs" style="display:flex; gap:4px; margin-bottom:16px; background:#f1f5f9; padding:6px; border-radius:6px; width: fit-content;">
160
227
  <button id="sf-tab-lookback" data-mode="lookback" class="sf-issue-tab ${!isStartNow ? "active" : ""}"
161
228
  style="padding:6px 12px; border:none; background:${!isStartNow ? "white" : "transparent"}; color: ${!isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
162
- Existing <span style="background: #F1F5F9; border:1px solid #cbd5e1; border-radius: 4px; width: 16px; height: 16px; font-size: 12px; display: flex; align-items: center; justify-content: center; color: #94A3B8; font-weight: 500;">E</span>
229
+ Existing ${getShortcutLabelFromContext("openModalExistingMode")}
163
230
  </button>
164
231
  <button id="sf-tab-startnow" data-mode="startnow" class="sf-issue-tab ${isStartNow ? "active" : ""}"
165
232
  style="padding:6px 12px; border:none; background:${isStartNow ? "white" : "transparent"}; color: ${isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
166
- Capture new <span style="background: #F1F5F9; border:1px solid #cbd5e1; border-radius: 4px; width: 16px; height: 16px; font-size: 12px; display: flex; align-items: center; justify-content: center; color: #94A3B8; font-weight: 500;">N</span>
233
+ Capture new ${getShortcutLabelFromContext("openModalCaptureNewMode")}
167
234
  </button>
168
235
  </div>
169
236
 
@@ -189,7 +256,7 @@ function injectModalHTML(initialMode = "lookback") {
189
256
  <textarea id="sf-issue-description" placeholder="Add description here"
190
257
  style="width:100%; height:80px; padding:8px 12px; font-size:14px;
191
258
  border:1px solid #cbd5e1; border-radius:6px; margin-bottom:20px;
192
- resize:none; outline:none;"></textarea>
259
+ resize:none; outline:none;">${currentState.description}</textarea>
193
260
 
194
261
  <div id="sf-lookback-container" style="display:${isStartNow ? "none" : "block"}; margin-bottom:20px;">
195
262
  <label for="sf-lookback-minutes" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
@@ -261,7 +328,7 @@ function injectModalHTML(initialMode = "lookback") {
261
328
  <div style="width: 14px; height: 14px; background: #fc5555; border-radius: 50%; border: 1px solid #991b1b;"></div>
262
329
  </div>
263
330
  <span>Start Recording</span>
264
- <span style="background: #F1F5F9; border:1px solid #cbd5e1; border-radius: 4px; width: 16px; height: 16px; font-size: 12px; display: flex; align-items: center; justify-content: center; color: #94A3B8; font-weight: 500;">R</span>
331
+ ${getShortcutLabelFromContext("startRecording")}
265
332
  </button>
266
333
  </div>
267
334
 
@@ -270,9 +337,7 @@ function injectModalHTML(initialMode = "lookback") {
270
337
  border-radius:6px; font-size:14px; line-height: 24px; font-weight:500;
271
338
  cursor:${isStartNow ? "not-allowed" : "pointer"}; opacity:${isStartNow ? "0.4" : "1"}; margin-bottom:1px" ${isStartNow ? "disabled" : ""}>
272
339
  Report <span style="margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: #94A3B8; font-size: 12px; line-height:16px;">
273
- <span style="background: #F1F5F9; border-radius: 4px; padding: 0 4px; font-weight: 500;">${getShortcutKeyCmdCtrlLabel()}</span>
274
- +
275
- <span style="background: #F1F5F9; border-radius: 4px; padding: 0 4px; font-weight: 500;">ENTER</span>
340
+ ${getShortcutLabelFromContext("submitReport")}
276
341
  </span>
277
342
  </button>
278
343
  </div>
@@ -487,9 +552,7 @@ function showFloatingTimer() {
487
552
  <div style="display:flex; align-items:center; gap:4px; margin-left:auto; font-size:14px; color:#0F172A;">
488
553
  <span style="color: #71717A;">stop</span>
489
554
  <span style="display: inline-flex; align-items: center; gap: 4px; color: #94A3B8; font-size: 12px; line-height:16px;">
490
- <span style="background: #F1F5F9; border-radius: 4px; border:1px solid #cbd5e1; padding: 0 4px; font-weight: 500;">${getShortcutKeyCmdCtrlLabel()}</span>
491
- +
492
- <span style="background: #F1F5F9; border-radius: 4px; border:1px solid #cbd5e1; padding: 0 4px; font-weight: 500;">ESC</span>
555
+ ${getShortcutLabelFromContext("stopRecording")}
493
556
  </span>
494
557
  </div>
495
558
  `;
package/dist/index.js CHANGED
@@ -323,6 +323,7 @@ function trackDomainChanges() {
323
323
  timestamp,
324
324
  page_visit_uuid: pageVisitUUID,
325
325
  prev_page_visit_uuid: prevPageVisitUUID,
326
+ session_id: getOrSetSessionId(),
326
327
  },
327
328
  });
328
329
  }
@@ -535,11 +536,6 @@ function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
535
536
  if (matchUrlWithWildcard(url, combinedPatterns)) {
536
537
  return true;
537
538
  }
538
- // 3️⃣ DYNAMIC-FAILURE EXCLUSION (exact host)
539
- const hostname = urlObj.hostname;
540
- if (dynamicExcludedHosts.has(hostname)) {
541
- return true;
542
- }
543
539
  return false;
544
540
  }
545
541
  // Updated XMLHttpRequest interceptor with domain exclusion
@@ -572,10 +568,6 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
572
568
  if (!url) {
573
569
  return originalSend.apply(this, args);
574
570
  }
575
- // parse host+path for exclusion
576
- const urlObj = new URL(url, window.location.href);
577
- const domain = urlObj.hostname;
578
- const hostPath = urlObj.pathname === "/" ? domain : `${domain}${urlObj.pathname}`;
579
571
  // 1️⃣ Skip injection for excluded domains/paths
580
572
  if (shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo)) {
581
573
  return originalSend.apply(this, args);
@@ -616,15 +608,6 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
616
608
  },
617
609
  ...getUrlAndStoredUuids(),
618
610
  });
619
- // 5️⃣ Update dynamic sets
620
- if (success) {
621
- // once any route passes, skip preflight on entire domain
622
- dynamicPassedHosts.add(domain);
623
- }
624
- else {
625
- // only exclude the specific failing path
626
- dynamicExcludedHosts.add(hostPath);
627
- }
628
611
  };
629
612
  // 6️⃣ On successful load
630
613
  this.addEventListener("load", () => {
@@ -654,7 +637,6 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo = []) {
654
637
  function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
655
638
  const originalFetch = window.fetch;
656
639
  const sessionId = getOrSetSessionId();
657
- const cachedDomains = new Map();
658
640
  window.fetch = new Proxy(originalFetch, {
659
641
  apply: async (target, thisArg, args) => {
660
642
  let input = args[0];
@@ -688,9 +670,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
688
670
  if (!sessionId) {
689
671
  return target.apply(thisArg, args);
690
672
  }
691
- const urlObj = new URL(url, window.location.href);
692
- const domain = urlObj.hostname;
693
- const hostPath = urlObj.pathname === "/" ? domain : `${domain}${urlObj.pathname}`;
694
673
  const networkUUID = uuidv4();
695
674
  const urlAndStoredUuids = getUrlAndStoredUuids();
696
675
  const method = init.method || "GET";
@@ -708,13 +687,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
708
687
  const status = response.status;
709
688
  const success = response.ok;
710
689
  const error = success ? "" : `Request Error: ${response.statusText}`;
711
- // 5️⃣ Update dynamic sets
712
- if (success) {
713
- dynamicPassedHosts.add(domain);
714
- }
715
- else {
716
- dynamicExcludedHosts.add(hostPath);
717
- }
718
690
  // Emit 'networkRequestFinished' event
719
691
  const eventData = {
720
692
  type: NetworkRequestEventId,
@@ -730,6 +702,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
730
702
  error,
731
703
  method,
732
704
  url,
705
+ retry_without_trace_id: isRetry,
733
706
  },
734
707
  ...urlAndStoredUuids,
735
708
  };
@@ -744,11 +717,8 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
744
717
  if (error instanceof TypeError &&
745
718
  error?.message?.toLowerCase().includes(CORS_KEYWORD.toLowerCase())) {
746
719
  // CORS/network failure: exclude just this path
747
- dynamicExcludedHosts.add(hostPath);
748
720
  return target.apply(thisArg, args);
749
721
  }
750
- // On other errors, treat as “passed” so we don’t re-preflight
751
- dynamicPassedHosts.add(domain);
752
722
  const eventData = {
753
723
  type: NetworkRequestEventId,
754
724
  timestamp: endTime,
@@ -793,7 +763,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
793
763
  }
794
764
  // Helper to retry a fetch without the X-Sf3-Rid header if the initial attempt fails due to that header
795
765
  async function retryWithoutPropagateHeaders(target, thisArg, args, url) {
796
- const domain = new URL(url).hostname;
797
766
  try {
798
767
  // **Fix:** Properly await and clone the request without the tracing header
799
768
  let input = args[0];
@@ -804,12 +773,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
804
773
  retryHeaders.delete(xSf3RidHeader);
805
774
  retryInit.headers = retryHeaders;
806
775
  const response = await target.call(thisArg, input, retryInit);
807
- if (response.ok || !BAD_HTTP_STATUS.includes(response.status)) {
808
- dynamicExcludedHosts.add(domain);
809
- cachedDomains.set(domain, ActionType.IGNORE);
810
- DEBUG &&
811
- console.info(`Retried request to ${url} without ${xSf3RidHeader} succeeded. Added "${domain}" to exclusion list.`);
812
- }
813
776
  return response;
814
777
  }
815
778
  else if (input instanceof Request) {
@@ -819,12 +782,6 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo = []) {
819
782
  retryHeaders.delete(xSf3RidHeader);
820
783
  const retryReq = new Request(cloned, { headers: retryHeaders });
821
784
  const response = await target.call(thisArg, retryReq, init);
822
- if (response.ok || !BAD_HTTP_STATUS.includes(response.status)) {
823
- dynamicExcludedHosts.add(domain);
824
- cachedDomains.set(domain, ActionType.IGNORE);
825
- DEBUG &&
826
- console.info(`Retried request to ${url} without ${xSf3RidHeader} succeeded. Added "${domain}" to exclusion list.`);
827
- }
828
785
  return response;
829
786
  }
830
787
  else {
@@ -897,24 +854,6 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
897
854
  initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS, sessionId);
898
855
  storeCredentialsAndConnection({ apiKey, backendApi });
899
856
  trackDomainChanges();
900
- // ─── Merge stored excludes + passed-in excludes ───
901
- const initialExcludes = new Set(dynamicExcludedHosts);
902
- domainsToNotPropagateHeaderTo.forEach((host) => {
903
- if (host?.trim()) {
904
- dynamicExcludedHosts.add(host.trim());
905
- }
906
- });
907
- const newlyExcluded = Array.from(dynamicExcludedHosts).filter((h) => !initialExcludes.has(h));
908
- if (newlyExcluded.length) {
909
- // single notify of the full updated list
910
- updateExcludedHostsStorageAndBackend(dynamicExcludedHosts);
911
- }
912
- // ─── Merge passed hosts ───
913
- domainsToPropagateHeaderTo.forEach((host) => {
914
- if (host?.trim()) {
915
- dynamicPassedHosts.add(host.trim());
916
- }
917
- });
918
857
  sessionStorage.setItem(SF_API_KEY_FOR_UPDATE, apiKey);
919
858
  sessionStorage.setItem(SF_BACKEND_API, backendApi);
920
859
  // Setup interceptors with custom ignore and propagate domains
@@ -942,6 +881,7 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
942
881
  }
943
882
  }
944
883
  export const initRecorder = async (options) => {
884
+ console.log("Initializing Sailfish Recorder with options:", options);
945
885
  // Only run on the client (browser) environment
946
886
  if (typeof window === "undefined") {
947
887
  return;
@@ -952,7 +892,7 @@ export const initRecorder = async (options) => {
952
892
  apiKey: options.apiKey,
953
893
  backendApi: options.backendApi ?? "https://api-service.sailfishqa.com",
954
894
  getSessionId: () => getOrSetSessionId(),
955
- enableShortcuts: options.enableShortcuts ?? false,
895
+ shortcuts: options.reportIssueShortcuts,
956
896
  });
957
897
  });
958
898
  };