@sailfish-ai/recorder 1.7.2 → 1.7.6

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.
@@ -0,0 +1,12 @@
1
+ export const DomContentEventId = 24;
2
+ export const NetworkRequestEventId = 27;
3
+ export const xSf3RidHeader = "X-Sf3-Rid";
4
+ export const DomContentSource = {
5
+ loading: 0,
6
+ contentLoaded: 1,
7
+ complete: 2,
8
+ beforeunload: 3,
9
+ unload: 4,
10
+ };
11
+ export const Loading = "loading";
12
+ export const Complete = "complete";
package/dist/graphql.js CHANGED
@@ -42,13 +42,18 @@ export function fetchCaptureSettings(apiKey, backendApi) {
42
42
  `, { apiKey, backendApi });
43
43
  }
44
44
  export function startRecordingSession(apiKey, recordingId, backendApi) {
45
- return sendGraphQLRequest("StartSession", `mutation StartSession($apiKey: UUID!, $recordingSessionId: UUID!) {
46
- startRecordingSession(companyApiKey: $apiKey, sessionId: $recordingSessionId) {
45
+ return sendGraphQLRequest("StartSession", `mutation StartSession($apiKey: UUID!, $recordingSessionId: UUID!, $pageSplitRecording: Boolean) {
46
+ startRecordingSession(
47
+ companyApiKey: $apiKey,
48
+ sessionId: $recordingSessionId,
49
+ pageSplitRecordingPossible: $pageSplitRecording
50
+ ) {
47
51
  id
48
52
  }
49
53
  }`, {
50
54
  apiKey,
51
55
  recordingSessionId: recordingId,
56
+ pageSplitRecording: true, // for creating shadow session with page split recording data in BE
52
57
  backendApi,
53
58
  });
54
59
  }
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // import { NetworkRecordOptions } from "@sailfish-rrweb/rrweb-plugin-network-record";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { gatherAndCacheDeviceInfo } from "./deviceInfo";
4
- import { sendRecordingEvents } from "./eventCache";
4
+ import { cacheEvents, sendRecordingEvents } from "./eventCache";
5
5
  import { fetchCaptureSettings, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
6
6
  import { sendMapUuidIfAvailable } from "./mapUuid";
7
- import { initializeConsolePlugin, initializeRecording } from "./recording";
7
+ import { initializeDomContentEvents, initializeConsolePlugin, initializeRecording, getUrlAndStoredUuids, } from "./recording";
8
8
  import { sendMessage } from "./websocket";
9
+ import { NetworkRequestEventId, xSf3RidHeader } from "./constants";
9
10
  // Default list of domains to ignore
10
11
  const DOMAINS_TO_NOT_PROPAGATE_HEADER_TO_DEFAULT = [
11
12
  "t.co",
@@ -57,18 +58,23 @@ export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
57
58
  // recordInitialRequests: false,
58
59
  // };
59
60
  function trackDomainChanges() {
60
- let lastDomain = window.location.href;
61
+ let lastDomain = window.location.href.split("?")[0];
61
62
  const checkDomainChange = (forceSend = false) => {
62
- const currentDomain = window.location.href;
63
+ const currentDomain = window.location.href.split("?")[0];
63
64
  if (forceSend || currentDomain !== lastDomain) {
64
- // console.log(`Domain changed from ${lastDomain} to ${currentDomain}`);
65
65
  lastDomain = currentDomain;
66
+ const pageVisitUUID = uuidv4();
67
+ const prevPageVisitUUID = sessionStorage.getItem("pageVisitUUID");
68
+ sessionStorage.setItem("pageVisitUUID", pageVisitUUID);
69
+ sessionStorage.setItem("prevPageVisitUUID", prevPageVisitUUID);
66
70
  const timestamp = Date.now();
67
71
  sendMessage({
68
72
  type: "routeChange",
69
73
  data: {
70
74
  url: currentDomain,
71
75
  timestamp,
76
+ page_visit_uuid: pageVisitUUID,
77
+ prev_page_visit_uuid: prevPageVisitUUID,
72
78
  },
73
79
  });
74
80
  }
@@ -130,10 +136,17 @@ function handleVisibilityChange() {
130
136
  getOrSetSessionId(); // Restore sessionId when the user returns to the page
131
137
  }
132
138
  }
139
+ function clearPageVisitUuid() {
140
+ sessionStorage.removeItem("pageVisitUUID");
141
+ sessionStorage.removeItem("prevPageVisitUUID");
142
+ }
133
143
  // Initialize event listeners for visibility change and page unload
134
144
  document.addEventListener("visibilitychange", handleVisibilityChange);
135
145
  // Clearing window.name on refresh to generate and retain a new sessionId
136
- window.addEventListener("beforeunload", () => (window.name = ""));
146
+ window.addEventListener("beforeunload", () => {
147
+ window.name = "";
148
+ clearPageVisitUuid();
149
+ });
137
150
  function storeCredentialsAndConnection({ apiKey, backendApi, }) {
138
151
  sessionStorage.setItem("sailfishApiKey", apiKey);
139
152
  sessionStorage.setItem("sailfishBackendApi", backendApi);
@@ -232,7 +245,9 @@ function setupXMLHttpRequestInterceptor(domainsToNotPropagateHeaderTo, domainsTo
232
245
  matchUrlWithWildcard(url, domainsToPropagateHeadersTo);
233
246
  if (sessionId && shouldPropagateHeader) {
234
247
  try {
235
- this.setRequestHeader("X-Sf3-Rid", sessionId);
248
+ const pageVisitUUID = sessionStorage.getItem("pageVisitUUID");
249
+ const networkUUID = uuidv4();
250
+ this.setRequestHeader(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
236
251
  }
237
252
  catch (e) {
238
253
  console.warn(`Could not set X-Sf3-Rid header for ${url}`, e);
@@ -274,7 +289,7 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
274
289
  return target.apply(thisArg, args);
275
290
  }
276
291
  if (cachedResult === "propagate") {
277
- return injectHeader(target, thisArg, args, input, init, sessionId);
292
+ return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
278
293
  }
279
294
  }
280
295
  // Check domain exclusion
@@ -290,38 +305,95 @@ function setupFetchInterceptor(domainsToNotPropagateHeadersTo, domainsToPropagat
290
305
  return target.apply(thisArg, args);
291
306
  }
292
307
  cache.set(url, "propagate");
293
- return injectHeader(target, thisArg, args, input, init, sessionId);
308
+ return injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url);
294
309
  },
295
310
  });
296
- // Helper function to inject the X-Sf3-Rid header
297
- function injectHeader(target, thisArg, args, input, init, sessionId) {
311
+ // Wrapper function to emit 'networkRequest' event
312
+ async function injectHeaderWrapper(target, thisArg, args, input, init, sessionId, url) {
298
313
  if (sessionId) {
299
- if (input instanceof Request) {
300
- // Clone the Request and modify headers
301
- const clonedRequest = input.clone();
302
- const newHeaders = new Headers(clonedRequest.headers);
303
- newHeaders.set("X-Sf3-Rid", sessionId);
304
- const modifiedRequest = new Request(clonedRequest, {
305
- headers: newHeaders,
314
+ const networkUUID = uuidv4();
315
+ const urlAndStoredUuids = getUrlAndStoredUuids();
316
+ const method = init.method || "GET";
317
+ const startTime = Date.now();
318
+ try {
319
+ const response = await injectHeader(target, thisArg, input, init, sessionId, urlAndStoredUuids.page_visit_uuid, networkUUID);
320
+ const endTime = Date.now();
321
+ const status = response.status;
322
+ const success = response.ok;
323
+ const error = success ? '' : `Request Error: ${response.statusText}`;
324
+ // Emit 'networkRequestFinished' event
325
+ cacheEvents({
326
+ type: NetworkRequestEventId,
327
+ timestamp: endTime,
328
+ data: {
329
+ request_id: networkUUID,
330
+ session_id: sessionId,
331
+ timestamp_start: startTime,
332
+ timestamp_end: endTime,
333
+ response_code: status,
334
+ success,
335
+ error,
336
+ method,
337
+ url,
338
+ },
339
+ ...urlAndStoredUuids,
306
340
  });
307
- return target.call(thisArg, modifiedRequest, init);
341
+ return response;
308
342
  }
309
- else {
310
- // For string or URL input, modify init to add headers
311
- const modifiedInit = { ...init };
312
- const newHeaders = new Headers(init.headers || {});
313
- newHeaders.set("X-Sf3-Rid", sessionId);
314
- modifiedInit.headers = newHeaders;
315
- return target.call(thisArg, input, modifiedInit);
343
+ catch (error) {
344
+ const endTime = Date.now();
345
+ const success = false;
346
+ const responseCode = error.response?.status || 500;
347
+ const errorMessage = error.message || "Fetch request failed";
348
+ // Emit 'networkRequestFinished' event with error
349
+ cacheEvents({
350
+ type: NetworkRequestEventId,
351
+ timestamp: endTime,
352
+ data: {
353
+ request_id: networkUUID,
354
+ session_id: sessionId,
355
+ timestamp_start: startTime,
356
+ timestamp_end: endTime,
357
+ response_code: responseCode,
358
+ success,
359
+ error: errorMessage,
360
+ method,
361
+ url,
362
+ },
363
+ ...urlAndStoredUuids
364
+ });
365
+ throw error;
316
366
  }
317
367
  }
318
368
  else {
319
369
  return target.apply(thisArg, args);
320
370
  }
321
371
  }
372
+ // Helper function to inject the X-Sf3-Rid header
373
+ async function injectHeader(target, thisArg, input, init, sessionId, pageVisitUUID, networkUUID) {
374
+ if (input instanceof Request) {
375
+ // Clone the Request and modify headers
376
+ const clonedRequest = input.clone();
377
+ const newHeaders = new Headers(clonedRequest.headers);
378
+ newHeaders.set(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
379
+ const modifiedRequest = new Request(clonedRequest, {
380
+ headers: newHeaders,
381
+ });
382
+ return await target.call(thisArg, modifiedRequest, init);
383
+ }
384
+ else {
385
+ // For string or URL input, modify init to add headers
386
+ const modifiedInit = { ...init };
387
+ const newHeaders = new Headers(init.headers || {});
388
+ newHeaders.set(xSf3RidHeader, `${sessionId}/${pageVisitUUID}/${networkUUID}`);
389
+ modifiedInit.headers = newHeaders;
390
+ return await target.call(thisArg, input, modifiedInit);
391
+ }
392
+ }
322
393
  }
323
394
  // Main Recording Function
324
395
  export async function startRecording({ apiKey, backendApi = "https://api-service.sailfishqa.com", domainsToPropagateHeaderTo = [], domainsToNotPropagateHeaderTo = [], serviceVersion = "", serviceIdentifier = "", }) {
396
+ initializeDomContentEvents();
325
397
  initializeConsolePlugin(DEFAULT_CONSOLE_RECORDING_SETTINGS);
326
398
  let sessionId = getOrSetSessionId();
327
399
  storeCredentialsAndConnection({ apiKey, backendApi });
package/dist/recording.js CHANGED
@@ -5,6 +5,7 @@ import { EventType } from "@sailfish-rrweb/types";
5
5
  import { ZendeskAPI } from "react-zendesk";
6
6
  import { cacheEvents, sendRecordingEvents } from "./eventCache";
7
7
  import { initializeWebSocket } from "./websocket";
8
+ import { DomContentEventId, DomContentSource, Loading, Complete } from "./constants";
8
9
  const MASK_CLASS = "sailfishSanitize";
9
10
  const ZENDESK_ELEMENT_ID = "zendesk_chat";
10
11
  const ZENDESK_PROVIDER = "Zendesk";
@@ -34,6 +35,56 @@ function maskInputFn(text, node) {
34
35
  }
35
36
  return text;
36
37
  }
38
+ export const getUrlAndStoredUuids = () => ({
39
+ page_visit_uuid: sessionStorage.getItem("pageVisitUUID"),
40
+ prev_page_visit_uuid: sessionStorage.getItem("prevPageVisitUUID"),
41
+ href: location.origin + location.pathname,
42
+ });
43
+ export function initializeDomContentEvents() {
44
+ document.addEventListener("readystatechange", () => {
45
+ const timestamp = Date.now();
46
+ const newEvent = {
47
+ type: DomContentEventId,
48
+ data: { source: 0, info: "" },
49
+ timestamp,
50
+ ...getUrlAndStoredUuids(),
51
+ };
52
+ switch (document.readyState) {
53
+ case Loading:
54
+ newEvent.data.source = DomContentSource.loading;
55
+ break;
56
+ case Complete:
57
+ newEvent.data.source = DomContentSource.complete;
58
+ break;
59
+ }
60
+ if (newEvent.data.info)
61
+ cacheEvents(newEvent);
62
+ });
63
+ document.addEventListener("DOMContentLoaded", () => {
64
+ cacheEvents({
65
+ type: DomContentEventId,
66
+ data: { source: DomContentSource.contentLoaded },
67
+ timestamp: Date.now(),
68
+ ...getUrlAndStoredUuids(),
69
+ });
70
+ });
71
+ window.addEventListener("beforeunload", () => {
72
+ cacheEvents({
73
+ type: DomContentEventId,
74
+ data: { source: DomContentSource.beforeunload },
75
+ timestamp: Date.now(),
76
+ ...getUrlAndStoredUuids(),
77
+ });
78
+ });
79
+ window.addEventListener("unload", () => {
80
+ cacheEvents({
81
+ type: DomContentEventId,
82
+ data: { source: DomContentSource.unload },
83
+ timestamp: Date.now(),
84
+ ...getUrlAndStoredUuids(),
85
+ });
86
+ });
87
+ }
37
88
  export function initializeConsolePlugin(consoleRecordSettings) {
38
89
  const { name, observer } = getRecordConsolePlugin(consoleRecordSettings);
39
90
  observer((payload) => cacheEvents({
@@ -52,6 +103,7 @@ backendApi, apiKey, sessionId) {
52
103
  try {
53
104
  record({
54
105
  emit(event) {
106
+ Object.assign(event, getUrlAndStoredUuids());
55
107
  // Attach sessionId to each event
56
108
  event.sessionId = sessionId;
57
109
  cacheEvents(event);