@sailfish-ai/recorder 1.7.3 → 1.7.7

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,113 @@
1
+ import { SourceMapConsumer } from "source-map-js";
2
+ import { sendMessage } from "./websocket";
3
+ /**
4
+ * Resolves stack traces using source maps.
5
+ * @param stackTrace - The minified stack trace.
6
+ * @returns The mapped stack trace with original file/line/column.
7
+ */
8
+ export async function resolveStackTrace(stackTrace) {
9
+ if (!stackTrace)
10
+ return ["No stack trace available"];
11
+ const traceLines = Array.isArray(stackTrace) ? stackTrace : stackTrace.split("\n");
12
+ const mappedStack = [];
13
+ for (const line of traceLines) {
14
+ const match = line.match(/\/assets\/([^\/]+\.js):(\d+):(\d+)/);
15
+ if (!match) {
16
+ mappedStack.push(line); // Keep non-matching lines
17
+ continue;
18
+ }
19
+ const [, fileUrl, lineNum, colNum] = match;
20
+ const fileName = fileUrl.split("/").pop();
21
+ const sourceMapUrl = `/assets/${fileName}.map`;
22
+ try {
23
+ const response = await fetch(sourceMapUrl);
24
+ if (!response.ok) {
25
+ break;
26
+ }
27
+ const rawSourceMap = await response.json();
28
+ if (!rawSourceMap.sources || !Array.isArray(rawSourceMap.sources)) {
29
+ mappedStack.push(line);
30
+ continue;
31
+ }
32
+ const consumer = await new SourceMapConsumer(rawSourceMap);
33
+ const original = consumer.originalPositionFor({
34
+ line: parseInt(lineNum, 10) - 1,
35
+ column: parseInt(colNum, 10),
36
+ });
37
+ if (original.source && original.line) {
38
+ // Get index of original source in rawSourceMap.sources
39
+ const sourceIndex = rawSourceMap.sources.indexOf(original.source);
40
+ let contextSnippet = [];
41
+ if (sourceIndex !== -1 &&
42
+ rawSourceMap.sourcesContent &&
43
+ rawSourceMap.sourcesContent[sourceIndex]) {
44
+ const lines = rawSourceMap.sourcesContent[sourceIndex].split("\n");
45
+ const errorLine = original.line || 1;
46
+ // Define range (clamp within file bounds)
47
+ const start = Math.max(errorLine - 6, 0); // -6 because index is 0-based
48
+ const end = Math.min(errorLine + 4, lines.length); // +4 to get 5 lines after
49
+ // Extract and trim context lines
50
+ contextSnippet = lines.slice(start, end).map((line, i) => {
51
+ const lineNumber = start + i + 1;
52
+ const prefix = lineNumber === errorLine ? "👉" : " ";
53
+ return `${prefix} ${lineNumber.toString().padStart(4)} | ${line.trim()}`;
54
+ });
55
+ }
56
+ mappedStack.push(`${original.source}:${original.line}:${original.column} (${original.name || "anonymous"})`);
57
+ }
58
+ else {
59
+ mappedStack.push(line);
60
+ }
61
+ }
62
+ catch (err) {
63
+ mappedStack.push(line);
64
+ }
65
+ }
66
+ return mappedStack;
67
+ }
68
+ /**
69
+ * Captures full error details and resolves the stack trace.
70
+ */
71
+ async function captureError(error, isPromiseRejection = false) {
72
+ let errorMessage;
73
+ let stack;
74
+ if (error instanceof Error) {
75
+ errorMessage = error.message;
76
+ stack = error.stack || "No stack trace";
77
+ }
78
+ else if (typeof error === "string") {
79
+ errorMessage = error;
80
+ stack = "No stack trace available";
81
+ }
82
+ else {
83
+ errorMessage = "Unknown error occurred";
84
+ stack = "No stack trace available";
85
+ }
86
+ const mappedStack = await resolveStackTrace(stack);
87
+ const filteredStack = mappedStack.filter((line) => !line.includes("chunk-") && !line.includes("react-dom"));
88
+ const errorDetails = {
89
+ message: errorMessage,
90
+ stack,
91
+ filteredStack,
92
+ userAgent: navigator.userAgent,
93
+ url: window.location.href,
94
+ timestamp: Date.now(),
95
+ level: "error",
96
+ };
97
+ // Sends the mapped error details to the backend.
98
+ sendMessage({
99
+ type: "frontendError",
100
+ data: errorDetails,
101
+ });
102
+ }
103
+ /**
104
+ * Initializes the error interceptor globally.
105
+ */
106
+ export function initializeErrorInterceptor() {
107
+ window.addEventListener("error", (event) => {
108
+ captureError(event.error || event.message);
109
+ });
110
+ window.addEventListener("unhandledrejection", (event) => {
111
+ captureError(event.reason, true);
112
+ });
113
+ }
@@ -16,7 +16,7 @@ export function sendRecordingEvents(webSocket) {
16
16
  let batchSize = 0;
17
17
  const sendBatch = () => {
18
18
  if (batch.length > 0) {
19
- const message = JSON.stringify({ type: "events", events: batch });
19
+ const message = JSON.stringify({ type: "events", events: batch, mapUuid: window.sfMapUuid });
20
20
  webSocket.send(message);
21
21
  batch = [];
22
22
  batchSize = 0;
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
  }