@sailfish-ai/recorder 1.0.0-beta-2 → 1.0.2

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # JS/TS Record-Only Package
2
+
3
+ ## TODO - Rename all Sailfish to GrepLion
@@ -0,0 +1,42 @@
1
+ // deviceInfo.tsx
2
+ import { sendMessage } from "./websocket";
3
+ export async function gatherAndCacheDeviceInfo() {
4
+ // Gather device information asynchronously
5
+ // TODO - Sibyl v3 - get additional information from the device
6
+ // const connection = navigator.connection || {};
7
+ const geolocation = await getGeolocation();
8
+ const info = {
9
+ // connection: {
10
+ // downlink: connection.downlink,
11
+ // downlinkMax: connection.downlinkMax,
12
+ // effectiveType: connection.effectiveType,
13
+ // rtt: connection.rtt,
14
+ // },
15
+ // deviceMemory: navigator.deviceMemory,
16
+ language: navigator.language,
17
+ userAgent: navigator.userAgent,
18
+ geolocation,
19
+ };
20
+ // Immediately add the device info to the event cache
21
+ sendMessage({ type: "deviceInfo", data: { deviceInfo: info } });
22
+ }
23
+ // Geolocation handling
24
+ function getGeolocation() {
25
+ return new Promise((resolve) => {
26
+ if ("geolocation" in navigator) {
27
+ navigator.geolocation.getCurrentPosition((position) => {
28
+ resolve({
29
+ latitude: position.coords.latitude,
30
+ longitude: position.coords.longitude,
31
+ });
32
+ }, (error) => {
33
+ console.error("Error getting geolocation:", error);
34
+ resolve(undefined);
35
+ });
36
+ }
37
+ else {
38
+ console.warn("Geolocation is not supported by this browser.");
39
+ resolve(undefined);
40
+ }
41
+ });
42
+ }
@@ -2,23 +2,18 @@ let eventCache = [];
2
2
  export function cacheEvents(event) {
3
3
  eventCache.push(event);
4
4
  }
5
- // export function sendCachedEvents(webSocket: ReconnectingWebSocket): void {
6
- // if (eventCache.length > 0 && webSocket.readyState === WebSocket.OPEN) {
7
- // const message = {
8
- // type: "events",
9
- // events: eventCache,
10
- // };
11
- // webSocket.send(JSON.stringify(message));
12
- // eventCache = [];
13
- // }
14
- // }
15
- export function sendRecordingEvents(webSocket, sessionId) {
16
- if (eventCache.length > 0) {
17
- const message = {
18
- type: "events",
19
- events: eventCache,
20
- };
21
- webSocket.send(JSON.stringify(message));
22
- eventCache = [];
5
+ export function sendRecordingEvents(webSocket) {
6
+ if (webSocket && webSocket.readyState === WebSocket.OPEN) {
7
+ if (eventCache.length > 0) {
8
+ const message = {
9
+ type: "events",
10
+ events: eventCache,
11
+ };
12
+ webSocket.send(JSON.stringify(message));
13
+ eventCache = [];
14
+ }
15
+ }
16
+ else {
17
+ eventCache.push(...eventCache);
23
18
  }
24
19
  }
@@ -0,0 +1,38 @@
1
+ const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
2
+ export function silentFetch(input, init) {
3
+ return new Promise((resolve, reject) => {
4
+ fetch(input, init)
5
+ .then(resolve)
6
+ .catch((error) => {
7
+ if (error.message.includes("ERR_CONNECTION_REFUSED")) {
8
+ // Suppress the error but still reject for handling in retry logic
9
+ resolve(new Response(null, { status: 502, statusText: "Bad Gateway" })); // Return a 502 to trigger retry without logging
10
+ }
11
+ else {
12
+ reject(error); // Reject other errors normally
13
+ }
14
+ });
15
+ });
16
+ }
17
+ export function exponentialBackoff(fn, action, retries = 5, // Max retry attempts
18
+ initialDelay = 2000, // Initial delay in ms
19
+ backoffFactor = 2) {
20
+ let attempt = 0;
21
+ const attemptRequest = async () => {
22
+ try {
23
+ return await fn(); // Attempt to call the provided function
24
+ }
25
+ catch (error) {
26
+ attempt++;
27
+ if (attempt > retries) {
28
+ throw error; // Throw the error if max retries are exceeded
29
+ }
30
+ const delay = initialDelay * Math.pow(backoffFactor, attempt - 1); // Calculate exponential delay
31
+ if (DEBUG)
32
+ console.log(`Attempt ${attempt} failed: ${action}; Retrying in ${delay}ms...`);
33
+ await new Promise((resolve) => setTimeout(resolve, delay)); // Wait for delay
34
+ return attemptRequest(); // Recursively retry
35
+ }
36
+ };
37
+ return attemptRequest(); // Start the initial attempt
38
+ }
package/dist/graphql.js CHANGED
@@ -1,11 +1,27 @@
1
- export function sendGraphQLRequest(operationName, query, variables) {
2
- return fetch(`${variables["backendApi"]}/graphql/?apiKey=${variables["apiKey"]}`, {
1
+ import { exponentialBackoff } from "./exponentialBackoff"; // Import your custom backoff function
2
+ const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
3
+ export function sendGraphQLRequest(operationName, query, variables, retries = 5, // Number of retries before giving up
4
+ initialBackoff = 2000, // Initial backoff in milliseconds
5
+ backoffFactor = 2) {
6
+ const apiEndpoint = `${variables["backendApi"]}/graphql/?apiKey=${variables["apiKey"]}`;
7
+ const action = "Sending GraphQL request to Sailfish AI";
8
+ if (DEBUG)
9
+ console.log(`Initial GraphQL request for ${operationName} at ${apiEndpoint}`);
10
+ // Use the custom exponentialBackoff function
11
+ return exponentialBackoff(() => fetch(apiEndpoint, {
3
12
  method: "POST",
4
13
  headers: {
5
14
  "Content-Type": "application/json",
6
15
  },
7
16
  body: JSON.stringify({ operationName, query, variables }),
8
- }).then((response) => response.json());
17
+ }).then((response) => {
18
+ if (DEBUG)
19
+ console.log(`Received response with status: ${response.status}`);
20
+ if (!response.ok) {
21
+ throw new Error(`GraphQL request failed with status ${response.status}`);
22
+ }
23
+ return response.json();
24
+ }), action, retries, initialBackoff, backoffFactor);
9
25
  }
10
26
  export function fetchCaptureSettings(apiKey, backendApi) {
11
27
  return sendGraphQLRequest("GetCaptureSettingsFromApiKey", `
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { v4 as uuidv4 } from "uuid";
2
+ import { gatherAndCacheDeviceInfo } from "./deviceInfo";
2
3
  import { sendRecordingEvents } from "./eventCache";
3
4
  import { fetchCaptureSettings, startRecordingSession } from "./graphql";
4
5
  import { initializeRecording } from "./recording";
5
- import { initializeWebSocket } from "./websocket";
6
6
  // Default Settings
7
7
  export const DEFAULT_DOMAINS_TO_IGNORE = [];
8
8
  export const DEFAULT_CAPTURE_SETTINGS = {
@@ -46,17 +46,81 @@ export const DEFAULT_NETWORK_CAPTURE_SETTINGS = {
46
46
  recordInitialRequests: false,
47
47
  };
48
48
  // Functions
49
+ // Function to get the current sessionId from sessionStorage
50
+ function getOrSetSessionId(forceNew = false) {
51
+ let sessionId = sessionStorage.getItem("sailfishSessionId");
52
+ if (!sessionId || forceNew) {
53
+ sessionId = uuidv4();
54
+ sessionStorage.setItem("sailfishSessionId", sessionId);
55
+ }
56
+ return sessionId;
57
+ }
58
+ // Function to reset the sessionId when the page becomes visible again
59
+ function handleVisibilityChange() {
60
+ if (document.visibilityState === "visible") {
61
+ getOrSetSessionId(true); // Force a new sessionId when the user returns to the page
62
+ }
63
+ }
64
+ // Initialize event listeners for visibility change and page unload
65
+ document.addEventListener("visibilitychange", handleVisibilityChange);
66
+ window.addEventListener("beforeunload", () => {
67
+ sessionStorage.removeItem("sailfishSessionId");
68
+ });
69
+ function storeCredentialsAndConnection({ apiKey, backendApi, }) {
70
+ sessionStorage.setItem("sailfishApiKey", apiKey);
71
+ sessionStorage.setItem("sailfishBackendApi", backendApi);
72
+ }
73
+ // Intercepting XMLHttpRequest
74
+ (function () {
75
+ const originalOpen = XMLHttpRequest.prototype.open;
76
+ const originalSend = XMLHttpRequest.prototype.send;
77
+ const sessionId = getOrSetSessionId();
78
+ XMLHttpRequest.prototype.open = function (...args) {
79
+ this._url = args[1]; // Store the request URL
80
+ originalOpen.apply(this, args);
81
+ };
82
+ XMLHttpRequest.prototype.send = function (...args) {
83
+ if (sessionId) {
84
+ this.setRequestHeader("X-Sf3-Rid", sessionId);
85
+ }
86
+ originalSend.apply(this, args);
87
+ };
88
+ })();
89
+ // Intercepting fetch API
90
+ (function () {
91
+ const originalFetch = window.fetch;
92
+ const sessionId = getOrSetSessionId();
93
+ window.fetch = async function (input, init = {}) {
94
+ if (sessionId) {
95
+ init.headers = {
96
+ ...init.headers,
97
+ "X-Sf3-Rid": sessionId,
98
+ };
99
+ }
100
+ return originalFetch(input, init);
101
+ };
102
+ })();
103
+ // Main Recording Function
49
104
  export async function startRecording({ apiKey, backendApi, }) {
50
- const sessionId = uuidv4(); // Use the same value for recordingId and sessionId
105
+ let sessionId = getOrSetSessionId();
106
+ storeCredentialsAndConnection({ apiKey, backendApi });
107
+ gatherAndCacheDeviceInfo();
51
108
  try {
52
109
  const captureSettingsResponse = await fetchCaptureSettings(apiKey, backendApi);
53
110
  const captureSettings = captureSettingsResponse.data?.captureSettingsFromApiKey ||
54
111
  DEFAULT_CAPTURE_SETTINGS;
55
112
  const sessionResponse = await startRecordingSession(apiKey, sessionId, backendApi);
56
113
  if (sessionResponse.data?.startRecordingSession) {
57
- const webSocket = initializeWebSocket(backendApi, apiKey, sessionId);
58
- initializeRecording(captureSettings, DEFAULT_CONSOLE_RECORDING_SETTINGS, DEFAULT_NETWORK_CAPTURE_SETTINGS, backendApi, apiKey, sessionId);
59
- setInterval(() => sendRecordingEvents(webSocket, sessionId), 10000);
114
+ // Initialize recording
115
+ const websocket = await initializeRecording(captureSettings, DEFAULT_CONSOLE_RECORDING_SETTINGS, DEFAULT_NETWORK_CAPTURE_SETTINGS, backendApi, apiKey, sessionId);
116
+ // Set up an interval to send recording events
117
+ setInterval(() => {
118
+ if (websocket && websocket.readyState === WebSocket.OPEN) {
119
+ sendRecordingEvents(websocket);
120
+ }
121
+ else {
122
+ }
123
+ }, 10000);
60
124
  }
61
125
  else {
62
126
  console.error("Failed to start recording session:", sessionResponse.errors || sessionResponse);
@@ -72,14 +136,10 @@ function extractHostname(url) {
72
136
  hostname = hostname.split("?")[0];
73
137
  return hostname;
74
138
  }
75
- function getWebSocketHost(url) {
76
- const parser = document.createElement("a");
77
- parser.href = url;
78
- return `${parser.hostname}${parser.port ? `:${parser.port}` : ""}`;
79
- }
80
139
  // Re-export from other modules
81
140
  export * from "./eventCache";
82
141
  export * from "./graphql";
83
142
  export * from "./recording";
143
+ export * from "./sendSailfishMessages";
84
144
  export * from "./types";
85
145
  export * from "./websocket";
package/dist/recording.js CHANGED
@@ -1,16 +1,45 @@
1
1
  import { record } from "@sailfish-rrweb/record";
2
2
  import { getRecordConsolePlugin, } from "@sailfish-rrweb/rrweb-plugin-console-record";
3
- import { getRecordNetworkPlugin, } from "@sailfish-rrweb/rrweb-plugin-network-record";
4
3
  import { cacheEvents, sendRecordingEvents } from "./eventCache";
5
4
  import { initializeWebSocket } from "./websocket";
6
5
  const MASK_CLASS = "sailfishSanitize";
7
6
  const DEFAULT_DOMAINS_TO_IGNORE = [];
8
7
  function maskInputFn(text, node) {
9
- // The maskInputFn logic here
10
- return text; // Placeholder return
8
+ // Exclude input[type=hidden] fields
9
+ if (node.type === "hidden") {
10
+ return "";
11
+ }
12
+ const patterns = {
13
+ creditCard: /\b(?:\d[ -]*?){13,16}\b/,
14
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/,
15
+ };
16
+ const MASK_CLASS = "mask"; // Assume this is a known constant
17
+ // Check for data attributes indicating sensitive information
18
+ // Check if element or parents have MASK_CLASS in their className
19
+ if (node.closest(`.${MASK_CLASS}`)) {
20
+ // Mask the input and retain the length of the input
21
+ return "*".repeat(text.length);
22
+ }
23
+ else if (node.hasAttribute("data-cc") ||
24
+ (node.getAttribute("autocomplete")?.startsWith("cc-") ?? false) ||
25
+ patterns.creditCard.test(text)) {
26
+ // Mask all but the last 4 digits of a credit card number
27
+ return "**** **** **** " + text.slice(-4);
28
+ }
29
+ else if (node.hasAttribute("data-ssn") || patterns.ssn.test(text)) {
30
+ // Mask the first 5 digits of an SSN
31
+ return "***-**-" + text.slice(-4);
32
+ }
33
+ else if (node.hasAttribute("data-dob")) {
34
+ // Mask the day and month of a date of birth, revealing only the year
35
+ return "**/**/" + text.slice(-4);
36
+ }
37
+ // Default to returning the original text
38
+ return text;
11
39
  }
12
- export async function initializeRecording(captureSettings, // TODO - Sibyl launch - replace type
40
+ export async function initializeRecording(captureSettings, // TODO - Sibyl post-launch - replace type
13
41
  consoleRecordSettings, networkRecordSettings, backendApi, apiKey, sessionId) {
42
+ const webSocket = initializeWebSocket(backendApi, apiKey, sessionId);
14
43
  try {
15
44
  record({
16
45
  emit(event) {
@@ -18,17 +47,17 @@ consoleRecordSettings, networkRecordSettings, backendApi, apiKey, sessionId) {
18
47
  },
19
48
  plugins: [
20
49
  getRecordConsolePlugin(consoleRecordSettings),
21
- getRecordNetworkPlugin(networkRecordSettings),
50
+ // getRecordNetworkPlugin(networkRecordSettings),
22
51
  ],
23
52
  maskInputOptions: { text: true }, // Fix the incorrect property name
24
53
  maskInputFn,
25
54
  maskTextClass: MASK_CLASS,
26
55
  ...captureSettings,
27
56
  });
28
- const webSocket = initializeWebSocket(backendApi, apiKey, sessionId);
29
- setInterval(() => sendRecordingEvents(webSocket, sessionId), 10000);
57
+ setInterval(() => sendRecordingEvents(webSocket), 10000);
30
58
  }
31
59
  catch (error) {
32
60
  console.error("Error importing plugins!", error);
33
61
  }
62
+ return webSocket;
34
63
  }