@sailfish-ai/recorder 1.0.0-beta-14 → 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 +2 -0
- package/dist/deviceInfo.js +42 -0
- package/dist/eventCache.js +13 -18
- package/dist/exponentialBackoff.js +38 -0
- package/dist/graphql.js +19 -3
- package/dist/index.js +70 -10
- package/dist/recording.js +3 -2
- package/dist/sailfish-recorder.es.js +2106 -1923
- package/dist/sailfish-recorder.umd.js +18731 -18548
- package/dist/sendSailfishMessages.js +37 -0
- package/dist/types/deviceInfo.d.ts +1 -0
- package/dist/types/eventCache.d.ts +1 -2
- package/dist/types/exponentialBackoff.d.ts +4 -0
- package/dist/types/graphql.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/recording.d.ts +2 -1
- package/dist/types/sendSailfishMessages.d.ts +2 -0
- package/dist/types/websocket.d.ts +1 -0
- package/dist/websocket.js +31 -13
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -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
|
+
}
|
package/dist/eventCache.js
CHANGED
|
@@ -2,23 +2,18 @@ let eventCache = [];
|
|
|
2
2
|
export function cacheEvents(event) {
|
|
3
3
|
eventCache.push(event);
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
initializeRecording(captureSettings, DEFAULT_CONSOLE_RECORDING_SETTINGS, DEFAULT_NETWORK_CAPTURE_SETTINGS, backendApi, apiKey, sessionId);
|
|
59
|
-
|
|
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
|
@@ -39,6 +39,7 @@ function maskInputFn(text, node) {
|
|
|
39
39
|
}
|
|
40
40
|
export async function initializeRecording(captureSettings, // TODO - Sibyl post-launch - replace type
|
|
41
41
|
consoleRecordSettings, networkRecordSettings, backendApi, apiKey, sessionId) {
|
|
42
|
+
const webSocket = initializeWebSocket(backendApi, apiKey, sessionId);
|
|
42
43
|
try {
|
|
43
44
|
record({
|
|
44
45
|
emit(event) {
|
|
@@ -53,10 +54,10 @@ consoleRecordSettings, networkRecordSettings, backendApi, apiKey, sessionId) {
|
|
|
53
54
|
maskTextClass: MASK_CLASS,
|
|
54
55
|
...captureSettings,
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
-
setInterval(() => sendRecordingEvents(webSocket, sessionId), 10000);
|
|
57
|
+
setInterval(() => sendRecordingEvents(webSocket), 10000);
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
60
60
|
console.error("Error importing plugins!", error);
|
|
61
61
|
}
|
|
62
|
+
return webSocket;
|
|
62
63
|
}
|