@logspace/sdk 1.0.3 → 1.1.1
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/logspace.esm.js +1085 -739
- package/logspace.esm.js.map +1 -1
- package/logspace.iife.js +1 -1
- package/logspace.iife.js.map +1 -1
- package/logspace.umd.js +1 -1
- package/logspace.umd.js.map +1 -1
- package/package.json +2 -5
- package/sdk/capture/base.d.ts +5 -3
- package/sdk/capture/sse.d.ts +0 -3
- package/sdk/capture/websocket.d.ts +0 -3
- package/sdk/index.d.ts +25 -1
- package/sdk/storage/indexed-db.d.ts +4 -0
- package/sdk/types.d.ts +15 -6
- package/shared/types.d.ts +9 -0
- package/shared/utils.d.ts +8 -1
package/logspace.esm.js
CHANGED
|
@@ -27,6 +27,20 @@ function safeStringify(value) {
|
|
|
27
27
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
28
28
|
if (value === null) return "null";
|
|
29
29
|
if (value === void 0) return "undefined";
|
|
30
|
+
if (typeof value === "function") {
|
|
31
|
+
const name = value.name || "anonymous";
|
|
32
|
+
return `[Function: ${name}]`;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "symbol") {
|
|
35
|
+
return value.toString();
|
|
36
|
+
}
|
|
37
|
+
if (value instanceof Error) {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
name: value.name,
|
|
40
|
+
message: value.message,
|
|
41
|
+
stack: value.stack
|
|
42
|
+
});
|
|
43
|
+
}
|
|
30
44
|
if (typeof FormData !== "undefined" && value instanceof FormData) {
|
|
31
45
|
const formDataObj = {};
|
|
32
46
|
value.forEach((val, key) => {
|
|
@@ -95,6 +109,11 @@ function maskSensitiveData(text) {
|
|
|
95
109
|
pattern: /\b(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b/g,
|
|
96
110
|
replacement: "[PHONE_REDACTED]"
|
|
97
111
|
},
|
|
112
|
+
// Passwords in plain text logs (e.g., "Password: value", "password: value")
|
|
113
|
+
{
|
|
114
|
+
pattern: /\b(password|pwd|passwd)\s*[:=]\s*[^\s,;}\]]+/gi,
|
|
115
|
+
replacement: "$1: [REDACTED]"
|
|
116
|
+
},
|
|
98
117
|
// Passwords in JSON (case insensitive)
|
|
99
118
|
{
|
|
100
119
|
pattern: /"password"\s*:\s*"[^"]*"/gi,
|
|
@@ -104,7 +123,12 @@ function maskSensitiveData(text) {
|
|
|
104
123
|
pattern: /'password'\s*:\s*'[^']*'/gi,
|
|
105
124
|
replacement: "'password': '[REDACTED]'"
|
|
106
125
|
},
|
|
107
|
-
// API keys
|
|
126
|
+
// API keys (common prefixes: sk_, pk_, api_key_, etc.)
|
|
127
|
+
{
|
|
128
|
+
pattern: /\b(?:sk|pk|api|auth|secret)_(?:live|test|prod|dev)?_[A-Za-z0-9_-]{10,}/g,
|
|
129
|
+
replacement: "[API_KEY_REDACTED]"
|
|
130
|
+
},
|
|
131
|
+
// API keys and tokens in JSON
|
|
108
132
|
{
|
|
109
133
|
pattern: /"(?:api_?key|token|secret|auth_?token|access_?token)"\s*:\s*"[^"]*"/gi,
|
|
110
134
|
replacement: '"$1": "[REDACTED]"'
|
|
@@ -122,9 +146,9 @@ function maskSensitiveData(text) {
|
|
|
122
146
|
pattern: /Authorization:\s*Basic\s+[^\s]+/gi,
|
|
123
147
|
replacement: "Authorization: Basic [REDACTED]"
|
|
124
148
|
},
|
|
125
|
-
// JWT tokens (
|
|
149
|
+
// JWT tokens - matches eyJ... pattern (base64url encoded JSON starting with {"alg" or {"typ")
|
|
126
150
|
{
|
|
127
|
-
pattern: /\
|
|
151
|
+
pattern: /\beyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\b/g,
|
|
128
152
|
replacement: "[JWT_TOKEN_REDACTED]"
|
|
129
153
|
}
|
|
130
154
|
];
|
|
@@ -133,6 +157,52 @@ function maskSensitiveData(text) {
|
|
|
133
157
|
}
|
|
134
158
|
return masked;
|
|
135
159
|
}
|
|
160
|
+
const SENSITIVE_LABELS = /^(password|pwd|passwd|secret|token|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key|secret[_-]?key)\s*[:=]?\s*$/i;
|
|
161
|
+
function maskConsoleArgs(args) {
|
|
162
|
+
const result2 = [];
|
|
163
|
+
for (let i = 0; i < args.length; i++) {
|
|
164
|
+
const arg = args[i];
|
|
165
|
+
const nextArg = args[i + 1];
|
|
166
|
+
if (typeof arg === "string" && SENSITIVE_LABELS.test(arg.trim()) && nextArg !== void 0) {
|
|
167
|
+
result2.push(arg);
|
|
168
|
+
result2.push("[REDACTED]");
|
|
169
|
+
i++;
|
|
170
|
+
} else if (arg !== void 0) {
|
|
171
|
+
result2.push(maskSensitiveData(arg));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result2;
|
|
175
|
+
}
|
|
176
|
+
function maskHeaders(headers, additionalHeaders) {
|
|
177
|
+
if (!headers || typeof headers !== "object") {
|
|
178
|
+
return headers;
|
|
179
|
+
}
|
|
180
|
+
const maskedHeaders = {};
|
|
181
|
+
const defaultSensitiveHeaders = [
|
|
182
|
+
"authorization",
|
|
183
|
+
"cookie",
|
|
184
|
+
"set-cookie",
|
|
185
|
+
"x-api-key",
|
|
186
|
+
"x-auth-token",
|
|
187
|
+
"x-access-token",
|
|
188
|
+
"x-csrf-token",
|
|
189
|
+
"csrf-token"
|
|
190
|
+
];
|
|
191
|
+
const allSensitiveHeaders = [...defaultSensitiveHeaders, ...(additionalHeaders || []).map((h) => h.toLowerCase())];
|
|
192
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
193
|
+
let maskedValue = value;
|
|
194
|
+
const isSensitiveHeader = allSensitiveHeaders.some(
|
|
195
|
+
(sensitiveName) => name.toLowerCase().includes(sensitiveName.toLowerCase())
|
|
196
|
+
);
|
|
197
|
+
if (isSensitiveHeader) {
|
|
198
|
+
maskedValue = "[REDACTED]";
|
|
199
|
+
} else {
|
|
200
|
+
maskedValue = maskSensitiveData(value);
|
|
201
|
+
}
|
|
202
|
+
maskedHeaders[name] = maskedValue;
|
|
203
|
+
}
|
|
204
|
+
return maskedHeaders;
|
|
205
|
+
}
|
|
136
206
|
function isBrowser() {
|
|
137
207
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
138
208
|
}
|
|
@@ -269,8 +339,59 @@ function createLogEntry(type, data, severity = "info") {
|
|
|
269
339
|
severity
|
|
270
340
|
};
|
|
271
341
|
}
|
|
342
|
+
function applyPrivacy(log, privacy) {
|
|
343
|
+
if (!privacy.maskSensitiveData) {
|
|
344
|
+
return log;
|
|
345
|
+
}
|
|
346
|
+
const masked = { ...log, data: { ...log.data } };
|
|
347
|
+
switch (log.type) {
|
|
348
|
+
case "network":
|
|
349
|
+
if (masked.data.requestHeaders) {
|
|
350
|
+
masked.data.requestHeaders = maskHeaders(masked.data.requestHeaders, privacy.redactHeaders);
|
|
351
|
+
}
|
|
352
|
+
if (masked.data.responseHeaders) {
|
|
353
|
+
masked.data.responseHeaders = maskHeaders(masked.data.responseHeaders, privacy.redactHeaders);
|
|
354
|
+
}
|
|
355
|
+
if (masked.data.requestBody) {
|
|
356
|
+
masked.data.requestBody = maskSensitiveData(masked.data.requestBody);
|
|
357
|
+
}
|
|
358
|
+
if (masked.data.responseBody) {
|
|
359
|
+
masked.data.responseBody = maskSensitiveData(masked.data.responseBody);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case "console":
|
|
363
|
+
if (masked.data.args && Array.isArray(masked.data.args)) {
|
|
364
|
+
masked.data.args = maskConsoleArgs(masked.data.args);
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
case "websocket":
|
|
368
|
+
case "sse":
|
|
369
|
+
if (masked.data.message) {
|
|
370
|
+
masked.data.message = maskSensitiveData(masked.data.message);
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case "storage":
|
|
374
|
+
if (masked.data.value) {
|
|
375
|
+
masked.data.value = maskSensitiveData(masked.data.value);
|
|
376
|
+
}
|
|
377
|
+
if (masked.data.oldValue) {
|
|
378
|
+
masked.data.oldValue = maskSensitiveData(masked.data.oldValue);
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
case "interaction":
|
|
382
|
+
if (masked.data.value) {
|
|
383
|
+
masked.data.value = maskSensitiveData(masked.data.value);
|
|
384
|
+
}
|
|
385
|
+
if (masked.data.element?.text) {
|
|
386
|
+
masked.data.element.text = maskSensitiveData(masked.data.element.text);
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
return masked;
|
|
391
|
+
}
|
|
272
392
|
let originalConsole = null;
|
|
273
393
|
const timerMap = /* @__PURE__ */ new Map();
|
|
394
|
+
const counterMap = /* @__PURE__ */ new Map();
|
|
274
395
|
function shouldIgnoreMessage(args) {
|
|
275
396
|
if (!args || args.length === 0) return false;
|
|
276
397
|
for (const arg of args) {
|
|
@@ -309,7 +430,12 @@ const consoleCapture = {
|
|
|
309
430
|
debug: console.debug,
|
|
310
431
|
time: console.time,
|
|
311
432
|
timeEnd: console.timeEnd,
|
|
312
|
-
timeLog: console.timeLog
|
|
433
|
+
timeLog: console.timeLog,
|
|
434
|
+
trace: console.trace,
|
|
435
|
+
table: console.table,
|
|
436
|
+
count: console.count,
|
|
437
|
+
countReset: console.countReset,
|
|
438
|
+
assert: console.assert
|
|
313
439
|
};
|
|
314
440
|
levels.forEach((level) => {
|
|
315
441
|
console[level] = function(...args) {
|
|
@@ -381,6 +507,80 @@ const consoleCapture = {
|
|
|
381
507
|
);
|
|
382
508
|
handler2(log);
|
|
383
509
|
};
|
|
510
|
+
console.trace = function(...args) {
|
|
511
|
+
originalConsole.trace.apply(console, args);
|
|
512
|
+
const serializedArgs = args.map((arg) => safeStringify(arg));
|
|
513
|
+
const stackTrace = new Error().stack || "";
|
|
514
|
+
const log = createLogEntry(
|
|
515
|
+
"console",
|
|
516
|
+
{
|
|
517
|
+
level: "trace",
|
|
518
|
+
args: serializedArgs.length > 0 ? serializedArgs : ["Trace"],
|
|
519
|
+
stackTrace
|
|
520
|
+
},
|
|
521
|
+
"info"
|
|
522
|
+
);
|
|
523
|
+
handler2(log);
|
|
524
|
+
};
|
|
525
|
+
console.table = function(data, columns) {
|
|
526
|
+
originalConsole.table.call(console, data, columns);
|
|
527
|
+
const log = createLogEntry(
|
|
528
|
+
"console",
|
|
529
|
+
{
|
|
530
|
+
level: "table",
|
|
531
|
+
args: [safeStringify(data)],
|
|
532
|
+
columns
|
|
533
|
+
},
|
|
534
|
+
"info"
|
|
535
|
+
);
|
|
536
|
+
handler2(log);
|
|
537
|
+
};
|
|
538
|
+
console.count = function(label = "default") {
|
|
539
|
+
originalConsole.count.call(console, label);
|
|
540
|
+
const count = (counterMap.get(label) || 0) + 1;
|
|
541
|
+
counterMap.set(label, count);
|
|
542
|
+
const log = createLogEntry(
|
|
543
|
+
"console",
|
|
544
|
+
{
|
|
545
|
+
level: "count",
|
|
546
|
+
args: [`${label}: ${count}`],
|
|
547
|
+
counterLabel: label,
|
|
548
|
+
count
|
|
549
|
+
},
|
|
550
|
+
"info"
|
|
551
|
+
);
|
|
552
|
+
handler2(log);
|
|
553
|
+
};
|
|
554
|
+
console.countReset = function(label = "default") {
|
|
555
|
+
originalConsole.countReset.call(console, label);
|
|
556
|
+
counterMap.set(label, 0);
|
|
557
|
+
const log = createLogEntry(
|
|
558
|
+
"console",
|
|
559
|
+
{
|
|
560
|
+
level: "countReset",
|
|
561
|
+
args: [`${label}: 0`],
|
|
562
|
+
counterLabel: label
|
|
563
|
+
},
|
|
564
|
+
"info"
|
|
565
|
+
);
|
|
566
|
+
handler2(log);
|
|
567
|
+
};
|
|
568
|
+
console.assert = function(condition, ...args) {
|
|
569
|
+
originalConsole.assert.apply(console, [condition, ...args]);
|
|
570
|
+
if (!condition) {
|
|
571
|
+
const serializedArgs = args.map((arg) => safeStringify(arg));
|
|
572
|
+
const log = createLogEntry(
|
|
573
|
+
"console",
|
|
574
|
+
{
|
|
575
|
+
level: "assert",
|
|
576
|
+
args: ["Assertion failed:", ...serializedArgs],
|
|
577
|
+
stackTrace: new Error().stack
|
|
578
|
+
},
|
|
579
|
+
"warn"
|
|
580
|
+
);
|
|
581
|
+
handler2(log);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
384
584
|
},
|
|
385
585
|
uninstall() {
|
|
386
586
|
if (!isBrowser() || !originalConsole) return;
|
|
@@ -392,11 +592,18 @@ const consoleCapture = {
|
|
|
392
592
|
console.time = originalConsole.time;
|
|
393
593
|
console.timeEnd = originalConsole.timeEnd;
|
|
394
594
|
console.timeLog = originalConsole.timeLog;
|
|
595
|
+
console.trace = originalConsole.trace;
|
|
596
|
+
console.table = originalConsole.table;
|
|
597
|
+
console.count = originalConsole.count;
|
|
598
|
+
console.countReset = originalConsole.countReset;
|
|
599
|
+
console.assert = originalConsole.assert;
|
|
395
600
|
timerMap.clear();
|
|
601
|
+
counterMap.clear();
|
|
396
602
|
originalConsole = null;
|
|
397
603
|
}
|
|
398
604
|
};
|
|
399
|
-
const
|
|
605
|
+
const DEFAULT_MAX_BODY_SIZE = 10 * 1024;
|
|
606
|
+
let maxBodySize$2 = DEFAULT_MAX_BODY_SIZE;
|
|
400
607
|
let originalFetch = null;
|
|
401
608
|
let OriginalXHR = null;
|
|
402
609
|
let excludeUrls$3 = [];
|
|
@@ -491,12 +698,13 @@ function interceptSSEStream(response, url, handler2) {
|
|
|
491
698
|
}
|
|
492
699
|
const networkCapture = {
|
|
493
700
|
name: "network",
|
|
494
|
-
install(handler2, privacy) {
|
|
701
|
+
install(handler2, privacy, _onActivity, limits) {
|
|
495
702
|
if (!isBrowser()) return;
|
|
496
703
|
if (originalFetch) return;
|
|
497
704
|
captureHandler$1 = handler2;
|
|
498
705
|
excludeUrls$3 = privacy.excludeUrls || [];
|
|
499
706
|
const blockBodies = privacy.blockNetworkBodies || [];
|
|
707
|
+
maxBodySize$2 = limits?.maxNetworkBodySize ? limits.maxNetworkBodySize * 1024 : DEFAULT_MAX_BODY_SIZE;
|
|
500
708
|
originalFetch = window.fetch;
|
|
501
709
|
window.fetch = async function(...args) {
|
|
502
710
|
const startTime = performance.now();
|
|
@@ -526,7 +734,7 @@ const networkCapture = {
|
|
|
526
734
|
let requestBody;
|
|
527
735
|
if (!shouldBlockBody && options.body) {
|
|
528
736
|
const bodyStr = safeStringify(options.body);
|
|
529
|
-
requestBody = bodyStr.length >
|
|
737
|
+
requestBody = bodyStr.length > maxBodySize$2 ? bodyStr.substring(0, maxBodySize$2) + "..." : bodyStr;
|
|
530
738
|
}
|
|
531
739
|
try {
|
|
532
740
|
const response = await originalFetch(...args);
|
|
@@ -537,9 +745,9 @@ const networkCapture = {
|
|
|
537
745
|
let responseBody;
|
|
538
746
|
let responseSize;
|
|
539
747
|
try {
|
|
540
|
-
if (!shouldBlockBody && (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html"))) {
|
|
748
|
+
if (!shouldBlockBody && (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html") || contentType.includes("text/x-component"))) {
|
|
541
749
|
const text = await clonedResponse.text();
|
|
542
|
-
responseBody = text.length >
|
|
750
|
+
responseBody = text.length > maxBodySize$2 ? text.substring(0, maxBodySize$2) + "..." : text;
|
|
543
751
|
responseSize = new Blob([responseBody]).size;
|
|
544
752
|
} else if (contentLength) {
|
|
545
753
|
responseSize = parseInt(contentLength, 10);
|
|
@@ -560,7 +768,7 @@ const networkCapture = {
|
|
|
560
768
|
{
|
|
561
769
|
method,
|
|
562
770
|
url,
|
|
563
|
-
|
|
771
|
+
statusCode: response.status,
|
|
564
772
|
requestHeaders,
|
|
565
773
|
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
566
774
|
requestBody,
|
|
@@ -606,7 +814,7 @@ const networkCapture = {
|
|
|
606
814
|
{
|
|
607
815
|
method,
|
|
608
816
|
url,
|
|
609
|
-
|
|
817
|
+
statusCode: 0,
|
|
610
818
|
requestHeaders,
|
|
611
819
|
responseHeaders: {},
|
|
612
820
|
requestBody,
|
|
@@ -645,7 +853,7 @@ const networkCapture = {
|
|
|
645
853
|
xhr.send = function(body) {
|
|
646
854
|
if (body && !shouldExcludeUrl(url, excludeUrls$3)) {
|
|
647
855
|
const bodyStr = safeStringify(body);
|
|
648
|
-
requestBody = bodyStr.length >
|
|
856
|
+
requestBody = bodyStr.length > maxBodySize$2 ? bodyStr.substring(0, maxBodySize$2) + "..." : bodyStr;
|
|
649
857
|
}
|
|
650
858
|
return originalSend.apply(this, [body]);
|
|
651
859
|
};
|
|
@@ -678,7 +886,7 @@ const networkCapture = {
|
|
|
678
886
|
}
|
|
679
887
|
let responseBody;
|
|
680
888
|
let responseSize;
|
|
681
|
-
if (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html")) {
|
|
889
|
+
if (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html") || contentType.includes("text/x-component")) {
|
|
682
890
|
try {
|
|
683
891
|
let responseText;
|
|
684
892
|
if (xhr.responseType === "" || xhr.responseType === "text") {
|
|
@@ -689,7 +897,7 @@ const networkCapture = {
|
|
|
689
897
|
responseText = "";
|
|
690
898
|
}
|
|
691
899
|
if (responseText) {
|
|
692
|
-
responseBody = responseText.length >
|
|
900
|
+
responseBody = responseText.length > maxBodySize$2 ? responseText.substring(0, maxBodySize$2) + "..." : responseText;
|
|
693
901
|
responseSize = new Blob([responseBody]).size;
|
|
694
902
|
}
|
|
695
903
|
} catch {
|
|
@@ -710,7 +918,7 @@ const networkCapture = {
|
|
|
710
918
|
{
|
|
711
919
|
method,
|
|
712
920
|
url,
|
|
713
|
-
|
|
921
|
+
statusCode: xhr.status,
|
|
714
922
|
requestHeaders,
|
|
715
923
|
responseHeaders,
|
|
716
924
|
requestBody,
|
|
@@ -766,10 +974,20 @@ const errorCapture = {
|
|
|
766
974
|
};
|
|
767
975
|
window.addEventListener("error", errorHandler$1, true);
|
|
768
976
|
rejectionHandler = (event) => {
|
|
977
|
+
let reasonMessage;
|
|
978
|
+
if (event.reason instanceof Error) {
|
|
979
|
+
reasonMessage = event.reason.message || event.reason.toString();
|
|
980
|
+
} else if (typeof event.reason === "string") {
|
|
981
|
+
reasonMessage = event.reason;
|
|
982
|
+
} else if (event.reason && typeof event.reason === "object") {
|
|
983
|
+
reasonMessage = event.reason.message || safeStringify(event.reason);
|
|
984
|
+
} else {
|
|
985
|
+
reasonMessage = safeStringify(event.reason);
|
|
986
|
+
}
|
|
769
987
|
const log = createLogEntry(
|
|
770
988
|
"error",
|
|
771
989
|
{
|
|
772
|
-
message: `Unhandled Promise Rejection: ${
|
|
990
|
+
message: `Unhandled Promise Rejection: ${reasonMessage}`,
|
|
773
991
|
stack: event.reason?.stack || "",
|
|
774
992
|
context: {},
|
|
775
993
|
isUncaught: true
|
|
@@ -794,6 +1012,7 @@ const errorCapture = {
|
|
|
794
1012
|
};
|
|
795
1013
|
let OriginalWebSocket = null;
|
|
796
1014
|
let excludeUrls$2 = [];
|
|
1015
|
+
let maxBodySize$1 = 10 * 1024;
|
|
797
1016
|
const wsThrottleMap = /* @__PURE__ */ new Map();
|
|
798
1017
|
const THROTTLE_WINDOW = 1e3;
|
|
799
1018
|
const MIN_THROTTLE_MS = 100;
|
|
@@ -829,12 +1048,15 @@ function shouldCaptureMessage$1(ws, direction) {
|
|
|
829
1048
|
}
|
|
830
1049
|
return shouldCapture;
|
|
831
1050
|
}
|
|
1051
|
+
let activityCallback$1;
|
|
832
1052
|
const websocketCapture = {
|
|
833
1053
|
name: "websocket",
|
|
834
|
-
install(handler2, privacy) {
|
|
1054
|
+
install(handler2, privacy, onActivity, limits) {
|
|
835
1055
|
if (!isBrowser()) return;
|
|
836
1056
|
if (OriginalWebSocket) return;
|
|
837
1057
|
excludeUrls$2 = privacy.excludeUrls || [];
|
|
1058
|
+
activityCallback$1 = onActivity;
|
|
1059
|
+
maxBodySize$1 = limits?.maxNetworkBodySize ? limits.maxNetworkBodySize * 1024 : 10 * 1024;
|
|
838
1060
|
OriginalWebSocket = window.WebSocket;
|
|
839
1061
|
window.WebSocket = function(url, protocols) {
|
|
840
1062
|
const ws = new OriginalWebSocket(url, protocols);
|
|
@@ -852,9 +1074,10 @@ const websocketCapture = {
|
|
|
852
1074
|
handler2(log);
|
|
853
1075
|
});
|
|
854
1076
|
ws.addEventListener("message", (event) => {
|
|
1077
|
+
activityCallback$1?.();
|
|
855
1078
|
if (!shouldCaptureMessage$1(ws, "received")) return;
|
|
856
1079
|
const isBinary = event.data instanceof ArrayBuffer || event.data instanceof Blob;
|
|
857
|
-
const { content, size } = truncateMessage(event.data, isBinary);
|
|
1080
|
+
const { content, size } = truncateMessage(event.data, isBinary, maxBodySize$1);
|
|
858
1081
|
const log = createLogEntry("websocket", {
|
|
859
1082
|
url: wsUrl,
|
|
860
1083
|
event: "message",
|
|
@@ -869,9 +1092,10 @@ const websocketCapture = {
|
|
|
869
1092
|
});
|
|
870
1093
|
const originalSend = ws.send.bind(ws);
|
|
871
1094
|
ws.send = function(data) {
|
|
1095
|
+
activityCallback$1?.();
|
|
872
1096
|
if (shouldCaptureMessage$1(ws, "sent")) {
|
|
873
1097
|
const isBinary = data instanceof ArrayBuffer || data instanceof Blob;
|
|
874
|
-
const { content, size } = truncateMessage(data, isBinary);
|
|
1098
|
+
const { content, size } = truncateMessage(data, isBinary, maxBodySize$1);
|
|
875
1099
|
const log = createLogEntry("websocket", {
|
|
876
1100
|
url: wsUrl,
|
|
877
1101
|
event: "message",
|
|
@@ -934,6 +1158,7 @@ const websocketCapture = {
|
|
|
934
1158
|
};
|
|
935
1159
|
let OriginalEventSource = null;
|
|
936
1160
|
let excludeUrls$1 = [];
|
|
1161
|
+
let maxBodySize = 10 * 1024;
|
|
937
1162
|
const sseThrottleMap = /* @__PURE__ */ new Map();
|
|
938
1163
|
const SSE_THROTTLE_MS = 100;
|
|
939
1164
|
function shouldCaptureMessage(eventSource) {
|
|
@@ -950,13 +1175,16 @@ function shouldCaptureMessage(eventSource) {
|
|
|
950
1175
|
}
|
|
951
1176
|
return false;
|
|
952
1177
|
}
|
|
1178
|
+
let activityCallback;
|
|
953
1179
|
const sseCapture = {
|
|
954
1180
|
name: "sse",
|
|
955
|
-
install(handler2, privacy) {
|
|
1181
|
+
install(handler2, privacy, onActivity, limits) {
|
|
956
1182
|
if (!isBrowser()) return;
|
|
957
1183
|
if (!window.EventSource) return;
|
|
958
1184
|
if (OriginalEventSource) return;
|
|
959
1185
|
excludeUrls$1 = privacy.excludeUrls || [];
|
|
1186
|
+
activityCallback = onActivity;
|
|
1187
|
+
maxBodySize = limits?.maxNetworkBodySize ? limits.maxNetworkBodySize * 1024 : 10 * 1024;
|
|
960
1188
|
OriginalEventSource = window.EventSource;
|
|
961
1189
|
window.EventSource = function(url, eventSourceInitDict) {
|
|
962
1190
|
const eventSource = new OriginalEventSource(url, eventSourceInitDict);
|
|
@@ -974,8 +1202,9 @@ const sseCapture = {
|
|
|
974
1202
|
handler2(log);
|
|
975
1203
|
});
|
|
976
1204
|
eventSource.addEventListener("message", (event) => {
|
|
1205
|
+
activityCallback?.();
|
|
977
1206
|
if (!shouldCaptureMessage(eventSource)) return;
|
|
978
|
-
const { content, size } = truncateMessage(event.data, false);
|
|
1207
|
+
const { content, size } = truncateMessage(event.data, false, maxBodySize);
|
|
979
1208
|
const log = createLogEntry("sse", {
|
|
980
1209
|
url: sseUrl,
|
|
981
1210
|
event: "message",
|
|
@@ -1053,10 +1282,33 @@ const storageCapture = {
|
|
|
1053
1282
|
if (originalStorage) return;
|
|
1054
1283
|
captureHandler = handler2;
|
|
1055
1284
|
originalStorage = {
|
|
1285
|
+
getItem: Storage.prototype.getItem,
|
|
1056
1286
|
setItem: Storage.prototype.setItem,
|
|
1057
1287
|
removeItem: Storage.prototype.removeItem,
|
|
1058
1288
|
clear: Storage.prototype.clear
|
|
1059
1289
|
};
|
|
1290
|
+
Storage.prototype.getItem = function(key) {
|
|
1291
|
+
const isSessionStorage = this === sessionStorage;
|
|
1292
|
+
if (isLogging || key.startsWith("__logspace")) {
|
|
1293
|
+
return originalStorage.getItem.call(this, key);
|
|
1294
|
+
}
|
|
1295
|
+
const value = originalStorage.getItem.call(this, key);
|
|
1296
|
+
try {
|
|
1297
|
+
isLogging = true;
|
|
1298
|
+
const storageType = isSessionStorage ? "sessionStorage" : "localStorage";
|
|
1299
|
+
const log = createLogEntry("storage", {
|
|
1300
|
+
storageType,
|
|
1301
|
+
operation: "getItem",
|
|
1302
|
+
key,
|
|
1303
|
+
value: value ? truncateValue(value).content : null,
|
|
1304
|
+
valueSize: value ? new Blob([value]).size : 0
|
|
1305
|
+
});
|
|
1306
|
+
handler2(log);
|
|
1307
|
+
} finally {
|
|
1308
|
+
isLogging = false;
|
|
1309
|
+
}
|
|
1310
|
+
return value;
|
|
1311
|
+
};
|
|
1060
1312
|
Storage.prototype.setItem = function(key, value) {
|
|
1061
1313
|
const isSessionStorage = this === sessionStorage;
|
|
1062
1314
|
if (isLogging || key.startsWith("__logspace")) {
|
|
@@ -1145,6 +1397,7 @@ const storageCapture = {
|
|
|
1145
1397
|
uninstall() {
|
|
1146
1398
|
if (!isBrowser()) return;
|
|
1147
1399
|
if (originalStorage) {
|
|
1400
|
+
Storage.prototype.getItem = originalStorage.getItem;
|
|
1148
1401
|
Storage.prototype.setItem = originalStorage.setItem;
|
|
1149
1402
|
Storage.prototype.removeItem = originalStorage.removeItem;
|
|
1150
1403
|
Storage.prototype.clear = originalStorage.clear;
|
|
@@ -1282,6 +1535,10 @@ let fidObserver = null;
|
|
|
1282
1535
|
let clsObserver = null;
|
|
1283
1536
|
let fcpObserver = null;
|
|
1284
1537
|
let inpObserver = null;
|
|
1538
|
+
let visibilityHandler = null;
|
|
1539
|
+
let loadHandler = null;
|
|
1540
|
+
let sendVitalsTimeout = null;
|
|
1541
|
+
let navigationTimingTimeout = null;
|
|
1285
1542
|
const metrics = {
|
|
1286
1543
|
lcp: null,
|
|
1287
1544
|
fid: null,
|
|
@@ -1427,17 +1684,19 @@ const performanceCapture = {
|
|
|
1427
1684
|
inpObserver.observe({ type: "event", buffered: true, durationThreshold: 16 });
|
|
1428
1685
|
} catch {
|
|
1429
1686
|
}
|
|
1430
|
-
|
|
1687
|
+
visibilityHandler = () => {
|
|
1431
1688
|
if (document.visibilityState === "hidden") {
|
|
1432
1689
|
sendWebVitals();
|
|
1433
1690
|
}
|
|
1434
|
-
}
|
|
1435
|
-
window.addEventListener("
|
|
1436
|
-
|
|
1691
|
+
};
|
|
1692
|
+
window.addEventListener("visibilitychange", visibilityHandler);
|
|
1693
|
+
loadHandler = () => {
|
|
1694
|
+
navigationTimingTimeout = setTimeout(() => {
|
|
1437
1695
|
sendNavigationTiming();
|
|
1438
1696
|
}, 0);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1697
|
+
};
|
|
1698
|
+
window.addEventListener("load", loadHandler);
|
|
1699
|
+
sendVitalsTimeout = setTimeout(() => {
|
|
1441
1700
|
sendWebVitals();
|
|
1442
1701
|
}, 1e4);
|
|
1443
1702
|
} catch {
|
|
@@ -1450,6 +1709,22 @@ const performanceCapture = {
|
|
|
1450
1709
|
observer.disconnect();
|
|
1451
1710
|
}
|
|
1452
1711
|
});
|
|
1712
|
+
if (visibilityHandler) {
|
|
1713
|
+
window.removeEventListener("visibilitychange", visibilityHandler);
|
|
1714
|
+
visibilityHandler = null;
|
|
1715
|
+
}
|
|
1716
|
+
if (loadHandler) {
|
|
1717
|
+
window.removeEventListener("load", loadHandler);
|
|
1718
|
+
loadHandler = null;
|
|
1719
|
+
}
|
|
1720
|
+
if (sendVitalsTimeout) {
|
|
1721
|
+
clearTimeout(sendVitalsTimeout);
|
|
1722
|
+
sendVitalsTimeout = null;
|
|
1723
|
+
}
|
|
1724
|
+
if (navigationTimingTimeout) {
|
|
1725
|
+
clearTimeout(navigationTimingTimeout);
|
|
1726
|
+
navigationTimingTimeout = null;
|
|
1727
|
+
}
|
|
1453
1728
|
lcpObserver = null;
|
|
1454
1729
|
fidObserver = null;
|
|
1455
1730
|
clsObserver = null;
|
|
@@ -1470,6 +1745,71 @@ let resourceObserver = null;
|
|
|
1470
1745
|
let excludeUrls = [];
|
|
1471
1746
|
const loggedResources = /* @__PURE__ */ new Set();
|
|
1472
1747
|
let handler$1 = null;
|
|
1748
|
+
const fontUrlToFamilyMap = /* @__PURE__ */ new Map();
|
|
1749
|
+
function buildFontFamilyMap() {
|
|
1750
|
+
if (!isBrowser()) return;
|
|
1751
|
+
try {
|
|
1752
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
1753
|
+
try {
|
|
1754
|
+
if (!sheet.cssRules) continue;
|
|
1755
|
+
for (const rule2 of Array.from(sheet.cssRules)) {
|
|
1756
|
+
if (rule2 instanceof CSSFontFaceRule) {
|
|
1757
|
+
const fontFamily = rule2.style.getPropertyValue("font-family").replace(/["']/g, "").trim();
|
|
1758
|
+
const src = rule2.style.getPropertyValue("src");
|
|
1759
|
+
if (fontFamily && src) {
|
|
1760
|
+
const urlMatches = src.matchAll(/url\(["']?([^"')]+)["']?\)/g);
|
|
1761
|
+
for (const match of urlMatches) {
|
|
1762
|
+
if (match[1]) {
|
|
1763
|
+
try {
|
|
1764
|
+
const absoluteUrl = new URL(match[1], sheet.href || window.location.href).href;
|
|
1765
|
+
fontUrlToFamilyMap.set(absoluteUrl, fontFamily);
|
|
1766
|
+
fontUrlToFamilyMap.set(match[1], fontFamily);
|
|
1767
|
+
} catch {
|
|
1768
|
+
fontUrlToFamilyMap.set(match[1], fontFamily);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
} catch {
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function getFontFamilyForUrl(url) {
|
|
1782
|
+
if (fontUrlToFamilyMap.has(url)) {
|
|
1783
|
+
return fontUrlToFamilyMap.get(url);
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
const urlObj = new URL(url);
|
|
1787
|
+
const filename = urlObj.pathname.split("/").pop();
|
|
1788
|
+
if (filename) {
|
|
1789
|
+
for (const [mapUrl, family] of fontUrlToFamilyMap.entries()) {
|
|
1790
|
+
if (mapUrl.endsWith(filename) || mapUrl.includes(filename)) {
|
|
1791
|
+
return family;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
if ("fonts" in document) {
|
|
1798
|
+
try {
|
|
1799
|
+
const urlLower = url.toLowerCase();
|
|
1800
|
+
for (const font of document.fonts) {
|
|
1801
|
+
if (font.status === "loaded") {
|
|
1802
|
+
const family = font.family.replace(/["']/g, "");
|
|
1803
|
+
if (urlLower.includes(family.toLowerCase().replace(/\s+/g, ""))) {
|
|
1804
|
+
return family;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return void 0;
|
|
1812
|
+
}
|
|
1473
1813
|
function getResourceType(entry) {
|
|
1474
1814
|
const initiatorType = entry.initiatorType || "other";
|
|
1475
1815
|
const url = entry.name;
|
|
@@ -1524,13 +1864,14 @@ function processResourceEntry(entry) {
|
|
|
1524
1864
|
} else if (duration > 1e3) {
|
|
1525
1865
|
severity = "warn";
|
|
1526
1866
|
}
|
|
1867
|
+
const fontFamily = resourceType === "font" ? getFontFamilyForUrl(entry.name) : void 0;
|
|
1527
1868
|
const log = createLogEntry(
|
|
1528
1869
|
"network",
|
|
1529
1870
|
{
|
|
1530
1871
|
method: "GET",
|
|
1531
1872
|
// Resource loads are always GET
|
|
1532
1873
|
url: entry.name,
|
|
1533
|
-
|
|
1874
|
+
statusCode: 200,
|
|
1534
1875
|
// Resources that successfully load return 200
|
|
1535
1876
|
resourceType,
|
|
1536
1877
|
initiatorType: entry.initiatorType,
|
|
@@ -1539,6 +1880,8 @@ function processResourceEntry(entry) {
|
|
|
1539
1880
|
decodedBodySize,
|
|
1540
1881
|
encodedBodySize,
|
|
1541
1882
|
cached,
|
|
1883
|
+
// Font family name (only for font resources)
|
|
1884
|
+
...fontFamily && { fontFamily },
|
|
1542
1885
|
// Timing breakdown
|
|
1543
1886
|
timing: {
|
|
1544
1887
|
dns: Math.round(entry.domainLookupEnd - entry.domainLookupStart),
|
|
@@ -1562,11 +1905,13 @@ const resourceCapture = {
|
|
|
1562
1905
|
handler$1 = captureHandler2;
|
|
1563
1906
|
excludeUrls = privacy.excludeUrls || [];
|
|
1564
1907
|
try {
|
|
1908
|
+
buildFontFamilyMap();
|
|
1565
1909
|
const existingResources = performance.getEntriesByType("resource");
|
|
1566
1910
|
existingResources.forEach((entry) => {
|
|
1567
1911
|
processResourceEntry(entry);
|
|
1568
1912
|
});
|
|
1569
1913
|
resourceObserver = new PerformanceObserver((list2) => {
|
|
1914
|
+
buildFontFamilyMap();
|
|
1570
1915
|
const entries = list2.getEntries();
|
|
1571
1916
|
entries.forEach((entry) => {
|
|
1572
1917
|
processResourceEntry(entry);
|
|
@@ -1589,6 +1934,7 @@ const resourceCapture = {
|
|
|
1589
1934
|
*/
|
|
1590
1935
|
reset() {
|
|
1591
1936
|
loggedResources.clear();
|
|
1937
|
+
fontUrlToFamilyMap.clear();
|
|
1592
1938
|
}
|
|
1593
1939
|
};
|
|
1594
1940
|
let originalPushState = null;
|
|
@@ -13840,7 +14186,7 @@ const state = {
|
|
|
13840
14186
|
stopFn: null,
|
|
13841
14187
|
startTime: null
|
|
13842
14188
|
};
|
|
13843
|
-
let currentTabId = null;
|
|
14189
|
+
let currentTabId$1 = null;
|
|
13844
14190
|
const DEFAULT_CONFIG$1 = {
|
|
13845
14191
|
maskAllInputs: false,
|
|
13846
14192
|
inlineStylesheet: true,
|
|
@@ -13868,7 +14214,7 @@ function startRRWebRecording(config2 = {}, onEvent, tabId) {
|
|
|
13868
14214
|
return false;
|
|
13869
14215
|
}
|
|
13870
14216
|
const mergedConfig = { ...DEFAULT_CONFIG$1, ...config2 };
|
|
13871
|
-
currentTabId = tabId || null;
|
|
14217
|
+
currentTabId$1 = tabId || null;
|
|
13872
14218
|
try {
|
|
13873
14219
|
state.events = [];
|
|
13874
14220
|
state.startTime = Date.now();
|
|
@@ -13877,7 +14223,7 @@ function startRRWebRecording(config2 = {}, onEvent, tabId) {
|
|
|
13877
14223
|
emit: (event, isCheckout) => {
|
|
13878
14224
|
const tabAwareEvent = {
|
|
13879
14225
|
...event,
|
|
13880
|
-
tabId: currentTabId || void 0
|
|
14226
|
+
tabId: currentTabId$1 || void 0
|
|
13881
14227
|
};
|
|
13882
14228
|
state.events.push(tabAwareEvent);
|
|
13883
14229
|
onEvent?.(tabAwareEvent);
|
|
@@ -13937,486 +14283,123 @@ function addRRWebCustomEvent(tag, payload) {
|
|
|
13937
14283
|
console.warn("[LogSpace] Failed to add custom event:", error);
|
|
13938
14284
|
}
|
|
13939
14285
|
}
|
|
13940
|
-
const
|
|
13941
|
-
const
|
|
13942
|
-
const
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
this.currentSessionId = null;
|
|
13954
|
-
this.initialized = false;
|
|
13955
|
-
this.debug = false;
|
|
13956
|
-
this.tabId = this.generateTabId();
|
|
13957
|
-
}
|
|
13958
|
-
/**
|
|
13959
|
-
* Generate unique tab identifier
|
|
13960
|
-
*/
|
|
13961
|
-
generateTabId() {
|
|
13962
|
-
const timestamp = Date.now().toString(36);
|
|
13963
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
13964
|
-
return `tab_${timestamp}_${random}`;
|
|
13965
|
-
}
|
|
13966
|
-
/**
|
|
13967
|
-
* Check if BroadcastChannel is supported
|
|
13968
|
-
*/
|
|
13969
|
-
isSupported() {
|
|
13970
|
-
return typeof BroadcastChannel !== "undefined" && typeof localStorage !== "undefined";
|
|
13971
|
-
}
|
|
13972
|
-
/**
|
|
13973
|
-
* Initialize the multi-tab coordinator
|
|
13974
|
-
*/
|
|
13975
|
-
init(callbacks, debug = false) {
|
|
13976
|
-
if (this.initialized) return;
|
|
13977
|
-
if (!this.isSupported()) {
|
|
13978
|
-
if (debug) console.log("[LogSpace MultiTab] BroadcastChannel not supported, running single-tab mode");
|
|
13979
|
-
return;
|
|
13980
|
-
}
|
|
13981
|
-
this.callbacks = callbacks;
|
|
13982
|
-
this.debug = debug;
|
|
13983
|
-
this.initialized = true;
|
|
13984
|
-
this.channel = new BroadcastChannel(CHANNEL_NAME);
|
|
13985
|
-
this.channel.onmessage = (event) => this.handleMessage(event.data);
|
|
13986
|
-
window.addEventListener("beforeunload", () => this.handleUnload());
|
|
13987
|
-
this.electLeader();
|
|
13988
|
-
if (this.debug) {
|
|
13989
|
-
console.log("[LogSpace MultiTab] Initialized", { tabId: this.tabId, isLeader: this.isLeader });
|
|
13990
|
-
}
|
|
14286
|
+
const DB_NAME = "logspace-sdk-db";
|
|
14287
|
+
const DB_VERSION = 1;
|
|
14288
|
+
const STORES = {
|
|
14289
|
+
PENDING_SESSIONS: "pending_sessions",
|
|
14290
|
+
// Sessions waiting to be sent
|
|
14291
|
+
CURRENT_SESSION: "current_session"
|
|
14292
|
+
// Current active session backup
|
|
14293
|
+
};
|
|
14294
|
+
let db = null;
|
|
14295
|
+
let dbInitPromise = null;
|
|
14296
|
+
async function initDB() {
|
|
14297
|
+
if (dbInitPromise) {
|
|
14298
|
+
return dbInitPromise;
|
|
13991
14299
|
}
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
*/
|
|
13995
|
-
electLeader() {
|
|
13996
|
-
const existingLeader = this.getLeaderState();
|
|
13997
|
-
if (existingLeader && !this.isLeaderStale(existingLeader)) {
|
|
13998
|
-
this.becomeFollower(existingLeader.sessionId);
|
|
13999
|
-
} else {
|
|
14000
|
-
this.tryBecomeLeader();
|
|
14001
|
-
}
|
|
14300
|
+
if (db) {
|
|
14301
|
+
return db;
|
|
14002
14302
|
}
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
14008
|
-
const sessionId = existingSession?.id || this.generateSessionId();
|
|
14009
|
-
const isNewSession = !existingSession;
|
|
14010
|
-
const leaderState = {
|
|
14011
|
-
tabId: this.tabId,
|
|
14012
|
-
sessionId,
|
|
14013
|
-
heartbeat: Date.now()
|
|
14303
|
+
dbInitPromise = new Promise((resolve2, reject) => {
|
|
14304
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
14305
|
+
request.onerror = () => {
|
|
14306
|
+
dbInitPromise = null;
|
|
14307
|
+
reject(new Error("Failed to open IndexedDB"));
|
|
14014
14308
|
};
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14309
|
+
request.onsuccess = () => {
|
|
14310
|
+
db = request.result;
|
|
14311
|
+
resolve2(db);
|
|
14312
|
+
};
|
|
14313
|
+
request.onupgradeneeded = (event) => {
|
|
14314
|
+
const database = event.target.result;
|
|
14315
|
+
if (!database.objectStoreNames.contains(STORES.PENDING_SESSIONS)) {
|
|
14316
|
+
const pendingStore = database.createObjectStore(STORES.PENDING_SESSIONS, { keyPath: "id" });
|
|
14317
|
+
pendingStore.createIndex("by-created", "createdAt");
|
|
14022
14318
|
}
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14319
|
+
if (!database.objectStoreNames.contains(STORES.CURRENT_SESSION)) {
|
|
14320
|
+
database.createObjectStore(STORES.CURRENT_SESSION, { keyPath: "id" });
|
|
14321
|
+
}
|
|
14322
|
+
};
|
|
14323
|
+
});
|
|
14324
|
+
return dbInitPromise;
|
|
14325
|
+
}
|
|
14326
|
+
function isIndexedDBAvailable() {
|
|
14327
|
+
return typeof indexedDB !== "undefined";
|
|
14328
|
+
}
|
|
14329
|
+
async function saveSessionCheckpoint(sessionId, logs2, rrwebEvents2, sessionData) {
|
|
14330
|
+
if (!isIndexedDBAvailable()) return;
|
|
14331
|
+
try {
|
|
14332
|
+
const database = await initDB();
|
|
14333
|
+
const transaction = database.transaction(STORES.CURRENT_SESSION, "readwrite");
|
|
14334
|
+
const store = transaction.objectStore(STORES.CURRENT_SESSION);
|
|
14335
|
+
const backup = {
|
|
14336
|
+
id: "current",
|
|
14337
|
+
// Single record, always overwrite
|
|
14338
|
+
sessionId,
|
|
14339
|
+
logs: logs2,
|
|
14340
|
+
rrwebEvents: rrwebEvents2,
|
|
14341
|
+
startTime: sessionData.startTime,
|
|
14342
|
+
url: sessionData.url,
|
|
14343
|
+
title: sessionData.title,
|
|
14344
|
+
userId: sessionData.userId,
|
|
14345
|
+
userTraits: sessionData.userTraits,
|
|
14346
|
+
metadata: sessionData.metadata,
|
|
14347
|
+
lastCheckpoint: Date.now()
|
|
14348
|
+
};
|
|
14349
|
+
await new Promise((resolve2, reject) => {
|
|
14350
|
+
const request = store.put(backup);
|
|
14351
|
+
request.onsuccess = () => resolve2();
|
|
14352
|
+
request.onerror = () => reject(request.error);
|
|
14353
|
+
});
|
|
14354
|
+
} catch (error) {
|
|
14355
|
+
console.warn("[LogSpace] Failed to save session checkpoint:", error);
|
|
14046
14356
|
}
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
}
|
|
14059
|
-
}
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
*/
|
|
14063
|
-
handleMessage(message) {
|
|
14064
|
-
if (this.debug) {
|
|
14065
|
-
console.log("[LogSpace MultiTab] Received message:", message.type);
|
|
14066
|
-
}
|
|
14067
|
-
switch (message.type) {
|
|
14068
|
-
case "leader-announce":
|
|
14069
|
-
if (message.tabId !== this.tabId) {
|
|
14070
|
-
if (this.isLeader) {
|
|
14071
|
-
this.becomeFollower(message.sessionId);
|
|
14072
|
-
}
|
|
14073
|
-
this.callbacks?.onLeaderChanged(message.tabId);
|
|
14074
|
-
}
|
|
14075
|
-
break;
|
|
14076
|
-
case "leader-leaving":
|
|
14077
|
-
if (!this.isLeader) {
|
|
14078
|
-
if (this.debug) {
|
|
14079
|
-
console.log("[LogSpace MultiTab] Leader leaving, attempting to claim leadership");
|
|
14080
|
-
}
|
|
14081
|
-
setTimeout(() => this.tryBecomeLeader(), Math.random() * 100);
|
|
14082
|
-
}
|
|
14083
|
-
break;
|
|
14084
|
-
case "log":
|
|
14085
|
-
if (this.isLeader && message.tabId !== this.tabId) {
|
|
14086
|
-
this.callbacks?.onReceiveLog(message.log);
|
|
14087
|
-
}
|
|
14088
|
-
break;
|
|
14089
|
-
case "rrweb-event":
|
|
14090
|
-
if (this.isLeader && message.tabId !== this.tabId) {
|
|
14091
|
-
this.callbacks?.onReceiveRRWebEvent(message.event);
|
|
14092
|
-
}
|
|
14093
|
-
break;
|
|
14094
|
-
case "request-session":
|
|
14095
|
-
if (this.isLeader && message.tabId !== this.tabId) {
|
|
14096
|
-
const session2 = this.getSessionState();
|
|
14097
|
-
if (session2) {
|
|
14098
|
-
this.broadcast({
|
|
14099
|
-
type: "session-info",
|
|
14100
|
-
sessionId: session2.id,
|
|
14101
|
-
startTime: session2.startTime
|
|
14102
|
-
});
|
|
14103
|
-
}
|
|
14104
|
-
}
|
|
14105
|
-
break;
|
|
14106
|
-
case "session-info":
|
|
14107
|
-
if (!this.isLeader) {
|
|
14108
|
-
this.currentSessionId = message.sessionId;
|
|
14109
|
-
}
|
|
14110
|
-
break;
|
|
14111
|
-
}
|
|
14112
|
-
}
|
|
14113
|
-
/**
|
|
14114
|
-
* Start heartbeat for leader
|
|
14115
|
-
*/
|
|
14116
|
-
startHeartbeat() {
|
|
14117
|
-
this.stopHeartbeat();
|
|
14118
|
-
this.stopLeaderCheck();
|
|
14119
|
-
this.heartbeatTimer = setInterval(() => {
|
|
14120
|
-
if (this.isLeader) {
|
|
14121
|
-
this.updateHeartbeat();
|
|
14122
|
-
}
|
|
14123
|
-
}, HEARTBEAT_INTERVAL);
|
|
14124
|
-
this.updateHeartbeat();
|
|
14125
|
-
}
|
|
14126
|
-
/**
|
|
14127
|
-
* Stop heartbeat timer
|
|
14128
|
-
*/
|
|
14129
|
-
stopHeartbeat() {
|
|
14130
|
-
if (this.heartbeatTimer) {
|
|
14131
|
-
clearInterval(this.heartbeatTimer);
|
|
14132
|
-
this.heartbeatTimer = null;
|
|
14133
|
-
}
|
|
14134
|
-
}
|
|
14135
|
-
/**
|
|
14136
|
-
* Update heartbeat in localStorage
|
|
14137
|
-
*/
|
|
14138
|
-
updateHeartbeat() {
|
|
14139
|
-
const state2 = this.getLeaderState();
|
|
14140
|
-
if (state2 && state2.tabId === this.tabId) {
|
|
14141
|
-
state2.heartbeat = Date.now();
|
|
14142
|
-
try {
|
|
14143
|
-
localStorage.setItem(LEADER_KEY, JSON.stringify(state2));
|
|
14144
|
-
} catch (error) {
|
|
14145
|
-
}
|
|
14146
|
-
}
|
|
14147
|
-
}
|
|
14148
|
-
/**
|
|
14149
|
-
* Start checking if leader is alive (for followers)
|
|
14150
|
-
*/
|
|
14151
|
-
startLeaderCheck() {
|
|
14152
|
-
this.stopLeaderCheck();
|
|
14153
|
-
this.leaderCheckTimer = setInterval(() => {
|
|
14154
|
-
if (!this.isLeader) {
|
|
14155
|
-
const leader = this.getLeaderState();
|
|
14156
|
-
if (!leader || this.isLeaderStale(leader)) {
|
|
14157
|
-
if (this.debug) {
|
|
14158
|
-
console.log("[LogSpace MultiTab] Leader appears dead, attempting takeover");
|
|
14159
|
-
}
|
|
14160
|
-
this.tryBecomeLeader();
|
|
14161
|
-
}
|
|
14162
|
-
}
|
|
14163
|
-
}, LEADER_CHECK_INTERVAL);
|
|
14164
|
-
}
|
|
14165
|
-
/**
|
|
14166
|
-
* Stop leader check timer
|
|
14167
|
-
*/
|
|
14168
|
-
stopLeaderCheck() {
|
|
14169
|
-
if (this.leaderCheckTimer) {
|
|
14170
|
-
clearInterval(this.leaderCheckTimer);
|
|
14171
|
-
this.leaderCheckTimer = null;
|
|
14172
|
-
}
|
|
14173
|
-
}
|
|
14174
|
-
/**
|
|
14175
|
-
* Check if leader heartbeat is stale
|
|
14176
|
-
*/
|
|
14177
|
-
isLeaderStale(leader) {
|
|
14178
|
-
return Date.now() - leader.heartbeat > HEARTBEAT_STALE_THRESHOLD;
|
|
14179
|
-
}
|
|
14180
|
-
/**
|
|
14181
|
-
* Get leader state from localStorage
|
|
14182
|
-
*/
|
|
14183
|
-
getLeaderState() {
|
|
14184
|
-
try {
|
|
14185
|
-
const data = localStorage.getItem(LEADER_KEY);
|
|
14186
|
-
return data ? JSON.parse(data) : null;
|
|
14187
|
-
} catch {
|
|
14188
|
-
return null;
|
|
14189
|
-
}
|
|
14190
|
-
}
|
|
14191
|
-
/**
|
|
14192
|
-
* Get session state from localStorage
|
|
14193
|
-
*/
|
|
14194
|
-
getSessionState() {
|
|
14195
|
-
try {
|
|
14196
|
-
const data = localStorage.getItem(SESSION_KEY);
|
|
14197
|
-
return data ? JSON.parse(data) : null;
|
|
14198
|
-
} catch {
|
|
14199
|
-
return null;
|
|
14200
|
-
}
|
|
14201
|
-
}
|
|
14202
|
-
/**
|
|
14203
|
-
* Generate session ID
|
|
14204
|
-
*/
|
|
14205
|
-
generateSessionId() {
|
|
14206
|
-
const timestamp = Date.now().toString(36);
|
|
14207
|
-
const random = Math.random().toString(36).substring(2, 10);
|
|
14208
|
-
return `sdk_${timestamp}_${random}`;
|
|
14209
|
-
}
|
|
14210
|
-
/**
|
|
14211
|
-
* Broadcast message to other tabs
|
|
14212
|
-
*/
|
|
14213
|
-
broadcast(message) {
|
|
14214
|
-
if (this.channel) {
|
|
14215
|
-
this.channel.postMessage(message);
|
|
14216
|
-
}
|
|
14217
|
-
}
|
|
14218
|
-
/**
|
|
14219
|
-
* Handle page unload - clean leader handoff
|
|
14220
|
-
*/
|
|
14221
|
-
handleUnload() {
|
|
14222
|
-
if (this.isLeader) {
|
|
14223
|
-
this.broadcast({ type: "leader-leaving", tabId: this.tabId });
|
|
14224
|
-
try {
|
|
14225
|
-
localStorage.removeItem(LEADER_KEY);
|
|
14226
|
-
} catch {
|
|
14227
|
-
}
|
|
14228
|
-
}
|
|
14229
|
-
this.cleanup();
|
|
14230
|
-
}
|
|
14231
|
-
/**
|
|
14232
|
-
* Send log to leader (for follower tabs)
|
|
14233
|
-
*/
|
|
14234
|
-
sendLog(log) {
|
|
14235
|
-
if (!this.initialized || !this.channel) return;
|
|
14236
|
-
if (this.isLeader) {
|
|
14237
|
-
this.callbacks?.onReceiveLog(log);
|
|
14238
|
-
} else {
|
|
14239
|
-
this.broadcast({ type: "log", tabId: this.tabId, log });
|
|
14240
|
-
}
|
|
14241
|
-
}
|
|
14242
|
-
/**
|
|
14243
|
-
* Send rrweb event to leader (for follower tabs)
|
|
14244
|
-
*/
|
|
14245
|
-
sendRRWebEvent(event) {
|
|
14246
|
-
if (!this.initialized || !this.channel) return;
|
|
14247
|
-
if (this.isLeader) {
|
|
14248
|
-
this.callbacks?.onReceiveRRWebEvent(event);
|
|
14249
|
-
} else {
|
|
14250
|
-
this.broadcast({ type: "rrweb-event", tabId: this.tabId, event });
|
|
14251
|
-
}
|
|
14252
|
-
}
|
|
14253
|
-
/**
|
|
14254
|
-
* Update session user info (stored in localStorage for all tabs)
|
|
14255
|
-
*/
|
|
14256
|
-
updateSessionUser(userId, traits) {
|
|
14257
|
-
const session2 = this.getSessionState();
|
|
14258
|
-
if (session2) {
|
|
14259
|
-
session2.userId = userId;
|
|
14260
|
-
session2.userTraits = traits;
|
|
14261
|
-
try {
|
|
14262
|
-
localStorage.setItem(SESSION_KEY, JSON.stringify(session2));
|
|
14263
|
-
} catch {
|
|
14264
|
-
}
|
|
14265
|
-
}
|
|
14266
|
-
}
|
|
14267
|
-
/**
|
|
14268
|
-
* End session and clear shared state
|
|
14269
|
-
*/
|
|
14270
|
-
endSession() {
|
|
14271
|
-
try {
|
|
14272
|
-
localStorage.removeItem(SESSION_KEY);
|
|
14273
|
-
if (this.isLeader) {
|
|
14274
|
-
localStorage.removeItem(LEADER_KEY);
|
|
14275
|
-
}
|
|
14276
|
-
} catch {
|
|
14277
|
-
}
|
|
14278
|
-
}
|
|
14279
|
-
/**
|
|
14280
|
-
* Get current tab ID
|
|
14281
|
-
*/
|
|
14282
|
-
getTabId() {
|
|
14283
|
-
return this.tabId;
|
|
14284
|
-
}
|
|
14285
|
-
/**
|
|
14286
|
-
* Get current session ID
|
|
14287
|
-
*/
|
|
14288
|
-
getSessionId() {
|
|
14289
|
-
return this.currentSessionId;
|
|
14290
|
-
}
|
|
14291
|
-
/**
|
|
14292
|
-
* Check if this tab is the leader
|
|
14293
|
-
*/
|
|
14294
|
-
isLeaderTab() {
|
|
14295
|
-
return this.isLeader;
|
|
14296
|
-
}
|
|
14297
|
-
/**
|
|
14298
|
-
* Check if multi-tab mode is active
|
|
14299
|
-
*/
|
|
14300
|
-
isActive() {
|
|
14301
|
-
return this.initialized && this.channel !== null;
|
|
14302
|
-
}
|
|
14303
|
-
/**
|
|
14304
|
-
* Clean up resources
|
|
14305
|
-
*/
|
|
14306
|
-
cleanup() {
|
|
14307
|
-
this.stopHeartbeat();
|
|
14308
|
-
this.stopLeaderCheck();
|
|
14309
|
-
if (this.channel) {
|
|
14310
|
-
this.channel.close();
|
|
14311
|
-
this.channel = null;
|
|
14312
|
-
}
|
|
14313
|
-
this.initialized = false;
|
|
14314
|
-
}
|
|
14315
|
-
/**
|
|
14316
|
-
* Force become leader (for testing or recovery)
|
|
14317
|
-
*/
|
|
14318
|
-
forceLeadership() {
|
|
14319
|
-
if (!this.initialized) return;
|
|
14320
|
-
const session2 = this.getSessionState();
|
|
14321
|
-
const sessionId = session2?.id || this.generateSessionId();
|
|
14322
|
-
this.becomeLeader(sessionId, !session2);
|
|
14357
|
+
}
|
|
14358
|
+
async function getCurrentSessionBackup() {
|
|
14359
|
+
if (!isIndexedDBAvailable()) return null;
|
|
14360
|
+
try {
|
|
14361
|
+
const database = await initDB();
|
|
14362
|
+
const transaction = database.transaction(STORES.CURRENT_SESSION, "readonly");
|
|
14363
|
+
const store = transaction.objectStore(STORES.CURRENT_SESSION);
|
|
14364
|
+
return new Promise((resolve2, reject) => {
|
|
14365
|
+
const request = store.get("current");
|
|
14366
|
+
request.onsuccess = () => resolve2(request.result || null);
|
|
14367
|
+
request.onerror = () => reject(request.error);
|
|
14368
|
+
});
|
|
14369
|
+
} catch (error) {
|
|
14370
|
+
console.warn("[LogSpace] Failed to get session backup:", error);
|
|
14371
|
+
return null;
|
|
14323
14372
|
}
|
|
14324
14373
|
}
|
|
14325
|
-
|
|
14326
|
-
const DB_NAME = "logspace-sdk-db";
|
|
14327
|
-
const DB_VERSION = 1;
|
|
14328
|
-
const STORES = {
|
|
14329
|
-
PENDING_SESSIONS: "pending_sessions",
|
|
14330
|
-
// Sessions waiting to be sent
|
|
14331
|
-
CURRENT_SESSION: "current_session"
|
|
14332
|
-
// Current active session backup
|
|
14333
|
-
};
|
|
14334
|
-
let db = null;
|
|
14335
|
-
let dbInitPromise = null;
|
|
14336
|
-
async function initDB() {
|
|
14337
|
-
if (dbInitPromise) {
|
|
14338
|
-
return dbInitPromise;
|
|
14339
|
-
}
|
|
14340
|
-
if (db) {
|
|
14341
|
-
return db;
|
|
14342
|
-
}
|
|
14343
|
-
dbInitPromise = new Promise((resolve2, reject) => {
|
|
14344
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
14345
|
-
request.onerror = () => {
|
|
14346
|
-
dbInitPromise = null;
|
|
14347
|
-
reject(new Error("Failed to open IndexedDB"));
|
|
14348
|
-
};
|
|
14349
|
-
request.onsuccess = () => {
|
|
14350
|
-
db = request.result;
|
|
14351
|
-
resolve2(db);
|
|
14352
|
-
};
|
|
14353
|
-
request.onupgradeneeded = (event) => {
|
|
14354
|
-
const database = event.target.result;
|
|
14355
|
-
if (!database.objectStoreNames.contains(STORES.PENDING_SESSIONS)) {
|
|
14356
|
-
const pendingStore = database.createObjectStore(STORES.PENDING_SESSIONS, { keyPath: "id" });
|
|
14357
|
-
pendingStore.createIndex("by-created", "createdAt");
|
|
14358
|
-
}
|
|
14359
|
-
if (!database.objectStoreNames.contains(STORES.CURRENT_SESSION)) {
|
|
14360
|
-
database.createObjectStore(STORES.CURRENT_SESSION, { keyPath: "id" });
|
|
14361
|
-
}
|
|
14362
|
-
};
|
|
14363
|
-
});
|
|
14364
|
-
return dbInitPromise;
|
|
14365
|
-
}
|
|
14366
|
-
function isIndexedDBAvailable() {
|
|
14367
|
-
return typeof indexedDB !== "undefined";
|
|
14368
|
-
}
|
|
14369
|
-
async function saveSessionCheckpoint(sessionId, logs2, rrwebEvents2, sessionData) {
|
|
14374
|
+
async function clearCurrentSessionBackup() {
|
|
14370
14375
|
if (!isIndexedDBAvailable()) return;
|
|
14371
14376
|
try {
|
|
14372
14377
|
const database = await initDB();
|
|
14373
14378
|
const transaction = database.transaction(STORES.CURRENT_SESSION, "readwrite");
|
|
14374
14379
|
const store = transaction.objectStore(STORES.CURRENT_SESSION);
|
|
14375
|
-
const backup = {
|
|
14376
|
-
id: "current",
|
|
14377
|
-
// Single record, always overwrite
|
|
14378
|
-
sessionId,
|
|
14379
|
-
logs: logs2,
|
|
14380
|
-
rrwebEvents: rrwebEvents2,
|
|
14381
|
-
startTime: sessionData.startTime,
|
|
14382
|
-
url: sessionData.url,
|
|
14383
|
-
title: sessionData.title,
|
|
14384
|
-
userId: sessionData.userId,
|
|
14385
|
-
userTraits: sessionData.userTraits,
|
|
14386
|
-
metadata: sessionData.metadata,
|
|
14387
|
-
lastCheckpoint: Date.now()
|
|
14388
|
-
};
|
|
14389
14380
|
await new Promise((resolve2, reject) => {
|
|
14390
|
-
const request = store.
|
|
14381
|
+
const request = store.delete("current");
|
|
14391
14382
|
request.onsuccess = () => resolve2();
|
|
14392
14383
|
request.onerror = () => reject(request.error);
|
|
14393
14384
|
});
|
|
14394
14385
|
} catch (error) {
|
|
14395
|
-
console.warn("[LogSpace] Failed to
|
|
14386
|
+
console.warn("[LogSpace] Failed to clear session backup:", error);
|
|
14396
14387
|
}
|
|
14397
14388
|
}
|
|
14398
|
-
async function
|
|
14399
|
-
if (!isIndexedDBAvailable()) return
|
|
14389
|
+
async function clearCurrentSessionBackupFor(sessionId) {
|
|
14390
|
+
if (!isIndexedDBAvailable()) return;
|
|
14400
14391
|
try {
|
|
14401
14392
|
const database = await initDB();
|
|
14402
|
-
const transaction = database.transaction(STORES.CURRENT_SESSION, "
|
|
14393
|
+
const transaction = database.transaction(STORES.CURRENT_SESSION, "readwrite");
|
|
14403
14394
|
const store = transaction.objectStore(STORES.CURRENT_SESSION);
|
|
14404
|
-
|
|
14395
|
+
const current = await new Promise((resolve2, reject) => {
|
|
14405
14396
|
const request = store.get("current");
|
|
14406
14397
|
request.onsuccess = () => resolve2(request.result || null);
|
|
14407
14398
|
request.onerror = () => reject(request.error);
|
|
14408
14399
|
});
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
}
|
|
14413
|
-
}
|
|
14414
|
-
async function clearCurrentSessionBackup() {
|
|
14415
|
-
if (!isIndexedDBAvailable()) return;
|
|
14416
|
-
try {
|
|
14417
|
-
const database = await initDB();
|
|
14418
|
-
const transaction = database.transaction(STORES.CURRENT_SESSION, "readwrite");
|
|
14419
|
-
const store = transaction.objectStore(STORES.CURRENT_SESSION);
|
|
14400
|
+
if (!current || current.sessionId !== sessionId) {
|
|
14401
|
+
return;
|
|
14402
|
+
}
|
|
14420
14403
|
await new Promise((resolve2, reject) => {
|
|
14421
14404
|
const request = store.delete("current");
|
|
14422
14405
|
request.onsuccess = () => resolve2();
|
|
@@ -14529,14 +14512,13 @@ function closeDB() {
|
|
|
14529
14512
|
dbInitPromise = null;
|
|
14530
14513
|
}
|
|
14531
14514
|
}
|
|
14532
|
-
const SDK_VERSION = "
|
|
14515
|
+
const SDK_VERSION = "1.1.0";
|
|
14533
14516
|
let config = null;
|
|
14534
14517
|
let session = null;
|
|
14535
14518
|
let logs = [];
|
|
14536
14519
|
let rrwebEvents = [];
|
|
14537
14520
|
let initialized = false;
|
|
14538
14521
|
let endReason = "manual";
|
|
14539
|
-
let sessionEndedByHardLimit = false;
|
|
14540
14522
|
let sessionEndedByIdle = false;
|
|
14541
14523
|
let rateLimiter = { count: 0, windowStart: 0 };
|
|
14542
14524
|
let deduplication = { lastLogHash: null, lastLogCount: 0 };
|
|
@@ -14549,11 +14531,16 @@ let visibilityTimer = null;
|
|
|
14549
14531
|
let lastMouseMoveTime = 0;
|
|
14550
14532
|
let samplingTriggered = false;
|
|
14551
14533
|
let samplingTriggerType = null;
|
|
14534
|
+
let samplingFirstTriggerTime = null;
|
|
14552
14535
|
let samplingTriggerTime = null;
|
|
14553
14536
|
let samplingEndTimer = null;
|
|
14537
|
+
let samplingTrimTimer = null;
|
|
14554
14538
|
let continuingSessionId = null;
|
|
14555
14539
|
let continuingSessionStartTime = null;
|
|
14540
|
+
let pendingIdentity = null;
|
|
14541
|
+
let currentIdentity = null;
|
|
14556
14542
|
let recoveryTargetSessionId = null;
|
|
14543
|
+
const processedRecoverySessionIds = /* @__PURE__ */ new Set();
|
|
14557
14544
|
const DEFAULT_CONFIG = {
|
|
14558
14545
|
capture: {
|
|
14559
14546
|
rrweb: true,
|
|
@@ -14570,7 +14557,7 @@ const DEFAULT_CONFIG = {
|
|
|
14570
14557
|
},
|
|
14571
14558
|
rrweb: {
|
|
14572
14559
|
maskAllInputs: false,
|
|
14573
|
-
checkoutEveryNth:
|
|
14560
|
+
checkoutEveryNth: 150,
|
|
14574
14561
|
recordCanvas: false
|
|
14575
14562
|
},
|
|
14576
14563
|
privacy: {
|
|
@@ -14585,15 +14572,17 @@ const DEFAULT_CONFIG = {
|
|
|
14585
14572
|
maxLogs: 1e4,
|
|
14586
14573
|
maxSize: 10 * 1024 * 1024,
|
|
14587
14574
|
// 10MB
|
|
14588
|
-
maxDuration:
|
|
14589
|
-
//
|
|
14590
|
-
idleTimeout:
|
|
14591
|
-
//
|
|
14575
|
+
maxDuration: 1800,
|
|
14576
|
+
// 30 minutes
|
|
14577
|
+
idleTimeout: 120,
|
|
14578
|
+
// 2 minutes
|
|
14592
14579
|
navigateAwayTimeout: 120,
|
|
14593
14580
|
// 2 minutes grace period
|
|
14594
14581
|
rateLimit: 100,
|
|
14595
14582
|
// 100 logs/second
|
|
14596
|
-
deduplicate: true
|
|
14583
|
+
deduplicate: true,
|
|
14584
|
+
maxNetworkBodySize: 10
|
|
14585
|
+
// 10KB (in KB, will be multiplied by 1024)
|
|
14597
14586
|
},
|
|
14598
14587
|
autoEnd: {
|
|
14599
14588
|
continueOnRefresh: true,
|
|
@@ -14602,10 +14591,10 @@ const DEFAULT_CONFIG = {
|
|
|
14602
14591
|
},
|
|
14603
14592
|
sampling: {
|
|
14604
14593
|
enabled: false,
|
|
14605
|
-
bufferBefore:
|
|
14606
|
-
//
|
|
14607
|
-
recordAfter:
|
|
14608
|
-
//
|
|
14594
|
+
bufferBefore: 30,
|
|
14595
|
+
// 30 seconds before trigger
|
|
14596
|
+
recordAfter: 30,
|
|
14597
|
+
// 30 seconds after trigger
|
|
14609
14598
|
triggers: {
|
|
14610
14599
|
onError: true,
|
|
14611
14600
|
onConsoleError: true,
|
|
@@ -14641,6 +14630,87 @@ function generateSessionId() {
|
|
|
14641
14630
|
const random = Math.random().toString(36).substring(2, 10);
|
|
14642
14631
|
return `sdk_${timestamp}_${random}`;
|
|
14643
14632
|
}
|
|
14633
|
+
const TAB_ID_KEY = "__logspace_tab_id";
|
|
14634
|
+
const SESSION_ID_KEY = "__logspace_session_id";
|
|
14635
|
+
let currentTabId = null;
|
|
14636
|
+
function generateTabId() {
|
|
14637
|
+
const timestamp = Date.now().toString(36);
|
|
14638
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
14639
|
+
return `tab_${timestamp}_${random}`;
|
|
14640
|
+
}
|
|
14641
|
+
function getOrCreateTabId() {
|
|
14642
|
+
if (currentTabId) return currentTabId;
|
|
14643
|
+
if (typeof sessionStorage !== "undefined") {
|
|
14644
|
+
const stored = sessionStorage.getItem(TAB_ID_KEY);
|
|
14645
|
+
if (stored) {
|
|
14646
|
+
currentTabId = stored;
|
|
14647
|
+
return stored;
|
|
14648
|
+
}
|
|
14649
|
+
}
|
|
14650
|
+
const created = generateTabId();
|
|
14651
|
+
currentTabId = created;
|
|
14652
|
+
try {
|
|
14653
|
+
sessionStorage.setItem(TAB_ID_KEY, created);
|
|
14654
|
+
} catch {
|
|
14655
|
+
}
|
|
14656
|
+
return created;
|
|
14657
|
+
}
|
|
14658
|
+
function setSessionContext(sessionId, tabId) {
|
|
14659
|
+
try {
|
|
14660
|
+
sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
|
14661
|
+
sessionStorage.setItem(TAB_ID_KEY, tabId);
|
|
14662
|
+
} catch {
|
|
14663
|
+
}
|
|
14664
|
+
try {
|
|
14665
|
+
window.__logspace_session_id = sessionId;
|
|
14666
|
+
window.__logspace_tab_id = tabId;
|
|
14667
|
+
} catch {
|
|
14668
|
+
}
|
|
14669
|
+
}
|
|
14670
|
+
function getSessionOrigin() {
|
|
14671
|
+
if (typeof document === "undefined") return null;
|
|
14672
|
+
const referrer = document.referrer || void 0;
|
|
14673
|
+
let openerSessionId;
|
|
14674
|
+
let openerTabId;
|
|
14675
|
+
let openerUrl;
|
|
14676
|
+
if (typeof window !== "undefined" && window.opener) {
|
|
14677
|
+
try {
|
|
14678
|
+
const opener = window.opener;
|
|
14679
|
+
openerSessionId = opener.sessionStorage?.getItem(SESSION_ID_KEY) ?? opener.__logspace_session_id;
|
|
14680
|
+
openerTabId = opener.sessionStorage?.getItem(TAB_ID_KEY) ?? opener.__logspace_tab_id;
|
|
14681
|
+
openerUrl = opener.location?.href;
|
|
14682
|
+
} catch {
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14685
|
+
if (!referrer && !openerSessionId && !openerTabId && !openerUrl) {
|
|
14686
|
+
return null;
|
|
14687
|
+
}
|
|
14688
|
+
return {
|
|
14689
|
+
referrer,
|
|
14690
|
+
openerSessionId,
|
|
14691
|
+
openerTabId,
|
|
14692
|
+
openerUrl
|
|
14693
|
+
};
|
|
14694
|
+
}
|
|
14695
|
+
function buildSessionMetadata(metadata) {
|
|
14696
|
+
const origin = getSessionOrigin();
|
|
14697
|
+
if (!origin && !metadata) return void 0;
|
|
14698
|
+
if (!origin) return metadata;
|
|
14699
|
+
const existingOrigin = metadata?.logspaceOrigin;
|
|
14700
|
+
if (existingOrigin && typeof existingOrigin === "object" && !Array.isArray(existingOrigin)) {
|
|
14701
|
+
return {
|
|
14702
|
+
...metadata,
|
|
14703
|
+
logspaceOrigin: {
|
|
14704
|
+
...origin,
|
|
14705
|
+
...existingOrigin
|
|
14706
|
+
}
|
|
14707
|
+
};
|
|
14708
|
+
}
|
|
14709
|
+
return {
|
|
14710
|
+
...metadata || {},
|
|
14711
|
+
logspaceOrigin: origin
|
|
14712
|
+
};
|
|
14713
|
+
}
|
|
14644
14714
|
function hashLog(log) {
|
|
14645
14715
|
return `${log.type}:${JSON.stringify(log.data)}`;
|
|
14646
14716
|
}
|
|
@@ -14653,13 +14723,13 @@ function checkRateLimit() {
|
|
|
14653
14723
|
rateLimiter.count = 1;
|
|
14654
14724
|
return true;
|
|
14655
14725
|
}
|
|
14656
|
-
|
|
14726
|
+
rateLimiter.count++;
|
|
14727
|
+
if (rateLimiter.count > config.limits.rateLimit) {
|
|
14657
14728
|
if (config.debug) {
|
|
14658
14729
|
console.warn("[LogSpace] Rate limit exceeded, dropping log");
|
|
14659
14730
|
}
|
|
14660
14731
|
return false;
|
|
14661
14732
|
}
|
|
14662
|
-
rateLimiter.count++;
|
|
14663
14733
|
return true;
|
|
14664
14734
|
}
|
|
14665
14735
|
function checkDeduplication(log) {
|
|
@@ -14692,9 +14762,6 @@ function checkLimits() {
|
|
|
14692
14762
|
}
|
|
14693
14763
|
function resetIdleTimer() {
|
|
14694
14764
|
if (!config?.autoEnd.onIdle) return;
|
|
14695
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14696
|
-
return;
|
|
14697
|
-
}
|
|
14698
14765
|
if (idleTimer) {
|
|
14699
14766
|
clearTimeout(idleTimer);
|
|
14700
14767
|
}
|
|
@@ -14712,9 +14779,6 @@ function resetIdleTimer() {
|
|
|
14712
14779
|
}
|
|
14713
14780
|
function handleUserActivity() {
|
|
14714
14781
|
if (!config || !initialized) return;
|
|
14715
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14716
|
-
return;
|
|
14717
|
-
}
|
|
14718
14782
|
if (sessionEndedByIdle && (!session || session.status === "stopped")) {
|
|
14719
14783
|
if (config.debug) {
|
|
14720
14784
|
console.log("[LogSpace] User activity detected - starting new session");
|
|
@@ -14747,33 +14811,54 @@ function removeActivityListeners() {
|
|
|
14747
14811
|
window.removeEventListener("mousemove", handleMouseMove, { capture: true });
|
|
14748
14812
|
}
|
|
14749
14813
|
function triggerSampling(triggerType, triggerLog) {
|
|
14750
|
-
if (!config?.sampling.enabled
|
|
14814
|
+
if (!config?.sampling.enabled) return;
|
|
14815
|
+
if (unloadHandled || isNavigatingAway) {
|
|
14816
|
+
if (config.debug) {
|
|
14817
|
+
console.log(`[LogSpace] Ignoring sampling trigger during unload: ${triggerType}`);
|
|
14818
|
+
}
|
|
14819
|
+
return;
|
|
14820
|
+
}
|
|
14821
|
+
const isFirstTrigger = !samplingTriggered;
|
|
14751
14822
|
samplingTriggered = true;
|
|
14752
14823
|
samplingTriggerType = triggerType;
|
|
14824
|
+
if (isFirstTrigger) {
|
|
14825
|
+
samplingFirstTriggerTime = Date.now();
|
|
14826
|
+
}
|
|
14753
14827
|
samplingTriggerTime = Date.now();
|
|
14828
|
+
stopSamplingTrimTimer();
|
|
14754
14829
|
if (config.debug) {
|
|
14755
|
-
|
|
14830
|
+
if (isFirstTrigger) {
|
|
14831
|
+
console.log(`[LogSpace] Sampling triggered by: ${triggerType}`);
|
|
14832
|
+
} else {
|
|
14833
|
+
console.log(`[LogSpace] Sampling window extended by: ${triggerType}`);
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
14836
|
+
if (isFirstTrigger) {
|
|
14837
|
+
config.hooks.onSamplingTrigger?.(triggerType, triggerLog);
|
|
14838
|
+
}
|
|
14839
|
+
if (samplingEndTimer) {
|
|
14840
|
+
clearTimeout(samplingEndTimer);
|
|
14841
|
+
samplingEndTimer = null;
|
|
14756
14842
|
}
|
|
14757
|
-
config.hooks.onSamplingTrigger?.(triggerType, triggerLog);
|
|
14758
14843
|
samplingEndTimer = setTimeout(() => {
|
|
14759
14844
|
if (session?.status === "recording") {
|
|
14760
14845
|
if (config?.debug) {
|
|
14761
14846
|
console.log("[LogSpace] Sampling recordAfter period complete, ending session");
|
|
14762
14847
|
}
|
|
14763
14848
|
endReason = "samplingTriggered";
|
|
14764
|
-
|
|
14849
|
+
stopSessionInternal({ restartImmediately: true });
|
|
14765
14850
|
}
|
|
14766
14851
|
}, config.sampling.recordAfter * 1e3);
|
|
14767
14852
|
}
|
|
14768
14853
|
function checkSamplingTrigger(log) {
|
|
14769
|
-
if (!config?.sampling.enabled
|
|
14854
|
+
if (!config?.sampling.enabled) return;
|
|
14770
14855
|
if (config.sampling.triggers.onError && log.type === "error") {
|
|
14771
14856
|
triggerSampling("error", log);
|
|
14772
14857
|
return;
|
|
14773
14858
|
}
|
|
14774
14859
|
if (config.sampling.triggers.onConsoleError && log.type === "console") {
|
|
14775
14860
|
const consoleData = log.data;
|
|
14776
|
-
if (consoleData.level === "error") {
|
|
14861
|
+
if (consoleData.level === "error" || consoleData.level === "assert") {
|
|
14777
14862
|
triggerSampling("consoleError", log);
|
|
14778
14863
|
return;
|
|
14779
14864
|
}
|
|
@@ -14787,19 +14872,114 @@ function checkSamplingTrigger(log) {
|
|
|
14787
14872
|
}
|
|
14788
14873
|
}
|
|
14789
14874
|
function trimLogsToSamplingWindow() {
|
|
14790
|
-
if (!config?.sampling.enabled || !samplingTriggerTime || !session) return;
|
|
14791
|
-
const bufferStartTime =
|
|
14875
|
+
if (!config?.sampling.enabled || !samplingFirstTriggerTime || !samplingTriggerTime || !session) return;
|
|
14876
|
+
const bufferStartTime = samplingFirstTriggerTime - config.sampling.bufferBefore * 1e3;
|
|
14792
14877
|
const bufferEndTime = samplingTriggerTime + config.sampling.recordAfter * 1e3;
|
|
14793
14878
|
logs = logs.filter((log) => {
|
|
14794
14879
|
return log.timestamp >= bufferStartTime && log.timestamp <= bufferEndTime;
|
|
14795
14880
|
});
|
|
14796
|
-
|
|
14881
|
+
const recentEvents = rrwebEvents.filter((event) => {
|
|
14797
14882
|
return event.timestamp >= bufferStartTime && event.timestamp <= bufferEndTime;
|
|
14798
14883
|
});
|
|
14884
|
+
const hasFullSnapshot = recentEvents.some((event) => event.type === 2);
|
|
14885
|
+
if (config.debug) {
|
|
14886
|
+
console.log(
|
|
14887
|
+
`[LogSpace] trimLogsToSamplingWindow - before: ${rrwebEvents.length} events, recentEvents: ${recentEvents.length}, hasFullSnapshot in recent: ${hasFullSnapshot}`
|
|
14888
|
+
);
|
|
14889
|
+
console.log(
|
|
14890
|
+
`[LogSpace] trimLogsToSamplingWindow - rrweb types before: ${rrwebEvents.map((e) => e.type).join(",")}`
|
|
14891
|
+
);
|
|
14892
|
+
}
|
|
14893
|
+
if (hasFullSnapshot) {
|
|
14894
|
+
rrwebEvents = recentEvents;
|
|
14895
|
+
} else {
|
|
14896
|
+
const fullSnapshotsBeforeWindow = [];
|
|
14897
|
+
for (let i = 0; i < rrwebEvents.length; i++) {
|
|
14898
|
+
const event = rrwebEvents[i];
|
|
14899
|
+
if (event && event.type === 2 && event.timestamp < bufferStartTime) {
|
|
14900
|
+
fullSnapshotsBeforeWindow.push(event);
|
|
14901
|
+
}
|
|
14902
|
+
}
|
|
14903
|
+
if (config.debug) {
|
|
14904
|
+
console.log(
|
|
14905
|
+
`[LogSpace] trimLogsToSamplingWindow - found ${fullSnapshotsBeforeWindow.length} snapshots before window`
|
|
14906
|
+
);
|
|
14907
|
+
}
|
|
14908
|
+
if (fullSnapshotsBeforeWindow.length > 0) {
|
|
14909
|
+
rrwebEvents = [...fullSnapshotsBeforeWindow, ...recentEvents];
|
|
14910
|
+
} else {
|
|
14911
|
+
rrwebEvents = recentEvents;
|
|
14912
|
+
}
|
|
14913
|
+
}
|
|
14914
|
+
if (config.debug) {
|
|
14915
|
+
console.log(
|
|
14916
|
+
`[LogSpace] trimLogsToSamplingWindow - after: ${rrwebEvents.length} events, types: ${rrwebEvents.map((e) => e.type).join(",")}`
|
|
14917
|
+
);
|
|
14918
|
+
}
|
|
14919
|
+
rebuildSessionStats();
|
|
14799
14920
|
if (config.debug) {
|
|
14800
14921
|
console.log(`[LogSpace] Trimmed to sampling window: ${logs.length} logs, ${rrwebEvents.length} rrweb events`);
|
|
14801
14922
|
}
|
|
14802
14923
|
}
|
|
14924
|
+
const SAMPLING_TRIM_INTERVAL = 5 * 1e3;
|
|
14925
|
+
function trimRollingBuffer() {
|
|
14926
|
+
if (!config?.sampling.enabled || samplingTriggered || !session) return;
|
|
14927
|
+
const now = Date.now();
|
|
14928
|
+
const cutoffTime = now - config.sampling.bufferBefore * 1e3;
|
|
14929
|
+
const prevLogCount = logs.length;
|
|
14930
|
+
const prevRrwebCount = rrwebEvents.length;
|
|
14931
|
+
logs = logs.filter((log) => log.timestamp >= cutoffTime);
|
|
14932
|
+
const recentEvents = rrwebEvents.filter((event) => event.timestamp >= cutoffTime);
|
|
14933
|
+
const hasFullSnapshot = recentEvents.some((event) => event.type === 2);
|
|
14934
|
+
if (hasFullSnapshot) {
|
|
14935
|
+
rrwebEvents = recentEvents;
|
|
14936
|
+
} else {
|
|
14937
|
+
let firstFullSnapshotIndex = -1;
|
|
14938
|
+
const fullSnapshotIndices = [];
|
|
14939
|
+
for (let i = 0; i < rrwebEvents.length; i++) {
|
|
14940
|
+
const event = rrwebEvents[i];
|
|
14941
|
+
if (event && event.type === 2 && event.timestamp < cutoffTime) {
|
|
14942
|
+
if (firstFullSnapshotIndex === -1) {
|
|
14943
|
+
firstFullSnapshotIndex = i;
|
|
14944
|
+
}
|
|
14945
|
+
fullSnapshotIndices.push(i);
|
|
14946
|
+
}
|
|
14947
|
+
}
|
|
14948
|
+
if (firstFullSnapshotIndex >= 0) {
|
|
14949
|
+
const snapshotsToKeep = fullSnapshotIndices.map((i) => rrwebEvents[i]).filter((e) => e !== void 0);
|
|
14950
|
+
rrwebEvents = [...snapshotsToKeep, ...recentEvents];
|
|
14951
|
+
} else {
|
|
14952
|
+
rrwebEvents = recentEvents;
|
|
14953
|
+
}
|
|
14954
|
+
}
|
|
14955
|
+
currentSize = logs.reduce((sum, log) => sum + JSON.stringify(log).length, 0);
|
|
14956
|
+
rrwebSize = rrwebEvents.reduce((sum, event) => sum + JSON.stringify(event).length, 0);
|
|
14957
|
+
rebuildSessionStats();
|
|
14958
|
+
const trimmedLogs = prevLogCount - logs.length;
|
|
14959
|
+
const trimmedRrweb = prevRrwebCount - rrwebEvents.length;
|
|
14960
|
+
if (config.debug && (trimmedLogs > 0 || trimmedRrweb > 0)) {
|
|
14961
|
+
console.log(
|
|
14962
|
+
`[LogSpace] Rolling buffer trimmed: ${trimmedLogs} logs, ${trimmedRrweb} rrweb events (keeping last ${config.sampling.bufferBefore}s: ${logs.length} logs, ${rrwebEvents.length} rrweb events)`
|
|
14963
|
+
);
|
|
14964
|
+
}
|
|
14965
|
+
}
|
|
14966
|
+
function startSamplingTrimTimer() {
|
|
14967
|
+
if (samplingTrimTimer || !config?.sampling.enabled) return;
|
|
14968
|
+
samplingTrimTimer = setInterval(() => {
|
|
14969
|
+
trimRollingBuffer();
|
|
14970
|
+
}, SAMPLING_TRIM_INTERVAL);
|
|
14971
|
+
if (config.debug) {
|
|
14972
|
+
console.log(
|
|
14973
|
+
`[LogSpace] Sampling rolling buffer started (trimming every ${SAMPLING_TRIM_INTERVAL / 1e3}s, keeping ${config.sampling.bufferBefore}s)`
|
|
14974
|
+
);
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
function stopSamplingTrimTimer() {
|
|
14978
|
+
if (samplingTrimTimer) {
|
|
14979
|
+
clearInterval(samplingTrimTimer);
|
|
14980
|
+
samplingTrimTimer = null;
|
|
14981
|
+
}
|
|
14982
|
+
}
|
|
14803
14983
|
const handleLog = (logData) => {
|
|
14804
14984
|
if (!config || !session || session.status !== "recording") return;
|
|
14805
14985
|
if (!checkRateLimit()) return;
|
|
@@ -14811,40 +14991,31 @@ const handleLog = (logData) => {
|
|
|
14811
14991
|
console.log(`[LogSpace] Session auto-ended: ${limitReached}`);
|
|
14812
14992
|
}
|
|
14813
14993
|
endReason = limitReached;
|
|
14814
|
-
sessionEndedByHardLimit = true;
|
|
14815
14994
|
config.hooks.onLimitReached?.(limitReached);
|
|
14816
|
-
|
|
14995
|
+
stopSessionInternal({ restartImmediately: true });
|
|
14817
14996
|
}
|
|
14818
14997
|
return;
|
|
14819
14998
|
}
|
|
14820
|
-
const tabId =
|
|
14999
|
+
const tabId = getOrCreateTabId();
|
|
14821
15000
|
const log = {
|
|
14822
15001
|
id: `${logs.length + 1}`,
|
|
14823
15002
|
timestamp: Date.now(),
|
|
14824
15003
|
// Use absolute timestamp (consistent with rrweb events)
|
|
14825
15004
|
tabId,
|
|
14826
|
-
// Tab ID from multi-tab coordinator
|
|
14827
15005
|
tabUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
14828
15006
|
type: logData.type,
|
|
14829
15007
|
data: logData.data,
|
|
14830
15008
|
severity: logData.severity
|
|
14831
15009
|
};
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
console.log("[LogSpace] Captured (sent to leader):", log.type, log.data);
|
|
14837
|
-
}
|
|
14838
|
-
return;
|
|
14839
|
-
}
|
|
14840
|
-
logs.push(log);
|
|
14841
|
-
currentSize += JSON.stringify(log).length;
|
|
14842
|
-
updateStats(log);
|
|
15010
|
+
const maskedLog = applyPrivacy(log, config.privacy);
|
|
15011
|
+
logs.push(maskedLog);
|
|
15012
|
+
currentSize += JSON.stringify(maskedLog).length;
|
|
15013
|
+
updateStats(maskedLog);
|
|
14843
15014
|
resetIdleTimer();
|
|
14844
|
-
checkSamplingTrigger(
|
|
14845
|
-
config.hooks.onAction?.(
|
|
15015
|
+
checkSamplingTrigger(maskedLog);
|
|
15016
|
+
config.hooks.onAction?.(maskedLog, session);
|
|
14846
15017
|
if (config.debug) {
|
|
14847
|
-
console.log("[LogSpace] Captured:",
|
|
15018
|
+
console.log("[LogSpace] Captured:", maskedLog.type, maskedLog.data);
|
|
14848
15019
|
}
|
|
14849
15020
|
};
|
|
14850
15021
|
function updateStats(log) {
|
|
@@ -14877,6 +15048,24 @@ function updateStats(log) {
|
|
|
14877
15048
|
break;
|
|
14878
15049
|
}
|
|
14879
15050
|
}
|
|
15051
|
+
function rebuildSessionStats() {
|
|
15052
|
+
if (!session) return;
|
|
15053
|
+
const tabCount = session.stats.tabCount || 1;
|
|
15054
|
+
session.stats = {
|
|
15055
|
+
logCount: 0,
|
|
15056
|
+
networkLogCount: 0,
|
|
15057
|
+
consoleLogCount: 0,
|
|
15058
|
+
errorCount: 0,
|
|
15059
|
+
storageLogCount: 0,
|
|
15060
|
+
interactionLogCount: 0,
|
|
15061
|
+
performanceLogCount: 0,
|
|
15062
|
+
annotationCount: 0,
|
|
15063
|
+
tabCount
|
|
15064
|
+
};
|
|
15065
|
+
for (const log of logs) {
|
|
15066
|
+
updateStats(log);
|
|
15067
|
+
}
|
|
15068
|
+
}
|
|
14880
15069
|
function installCaptures() {
|
|
14881
15070
|
if (!config) return;
|
|
14882
15071
|
const privacyWithSdkExclusions = { ...config.privacy };
|
|
@@ -14888,16 +15077,16 @@ function installCaptures() {
|
|
|
14888
15077
|
consoleCapture.install(handleLog, config.privacy);
|
|
14889
15078
|
}
|
|
14890
15079
|
if (config.capture.network) {
|
|
14891
|
-
networkCapture.install(handleLog, privacyWithSdkExclusions);
|
|
15080
|
+
networkCapture.install(handleLog, privacyWithSdkExclusions, resetIdleTimer, config.limits);
|
|
14892
15081
|
}
|
|
14893
15082
|
if (config.capture.errors) {
|
|
14894
15083
|
errorCapture.install(handleLog, config.privacy);
|
|
14895
15084
|
}
|
|
14896
15085
|
if (config.capture.websocket) {
|
|
14897
|
-
websocketCapture.install(handleLog, config.privacy);
|
|
15086
|
+
websocketCapture.install(handleLog, config.privacy, resetIdleTimer, config.limits);
|
|
14898
15087
|
}
|
|
14899
15088
|
if (config.capture.sse) {
|
|
14900
|
-
sseCapture.install(handleLog, config.privacy);
|
|
15089
|
+
sseCapture.install(handleLog, config.privacy, resetIdleTimer, config.limits);
|
|
14901
15090
|
}
|
|
14902
15091
|
if (config.capture.storage) {
|
|
14903
15092
|
storageCapture.install(handleLog, config.privacy);
|
|
@@ -14928,12 +15117,10 @@ function uninstallCaptures() {
|
|
|
14928
15117
|
spaNavigationCapture.uninstall();
|
|
14929
15118
|
}
|
|
14930
15119
|
let unloadHandled = false;
|
|
15120
|
+
let isNavigatingAway = false;
|
|
14931
15121
|
const CHECKPOINT_INTERVAL = 15 * 1e3;
|
|
14932
15122
|
async function saveCheckpoint() {
|
|
14933
15123
|
if (!session || session.status !== "recording" || !isIndexedDBAvailable()) return;
|
|
14934
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14935
|
-
return;
|
|
14936
|
-
}
|
|
14937
15124
|
try {
|
|
14938
15125
|
await saveSessionCheckpoint(session.id, logs, rrwebEvents, {
|
|
14939
15126
|
startTime: session.startTime,
|
|
@@ -14968,6 +15155,37 @@ function stopCheckpointTimer() {
|
|
|
14968
15155
|
}
|
|
14969
15156
|
}
|
|
14970
15157
|
const EMERGENCY_BACKUP_KEY = "__logspace_emergency_backup";
|
|
15158
|
+
const SAMPLING_STATE_KEY = "__logspace_sampling_state";
|
|
15159
|
+
function saveSamplingState(sessionId) {
|
|
15160
|
+
if (!samplingTriggered) return;
|
|
15161
|
+
try {
|
|
15162
|
+
const state2 = {
|
|
15163
|
+
triggered: samplingTriggered,
|
|
15164
|
+
triggerType: samplingTriggerType,
|
|
15165
|
+
firstTriggerTime: samplingFirstTriggerTime,
|
|
15166
|
+
triggerTime: samplingTriggerTime,
|
|
15167
|
+
sessionId
|
|
15168
|
+
};
|
|
15169
|
+
localStorage.setItem(SAMPLING_STATE_KEY, JSON.stringify(state2));
|
|
15170
|
+
} catch (error) {
|
|
15171
|
+
}
|
|
15172
|
+
}
|
|
15173
|
+
function getSamplingState() {
|
|
15174
|
+
try {
|
|
15175
|
+
const state2 = localStorage.getItem(SAMPLING_STATE_KEY);
|
|
15176
|
+
if (state2) {
|
|
15177
|
+
return JSON.parse(state2);
|
|
15178
|
+
}
|
|
15179
|
+
} catch (error) {
|
|
15180
|
+
}
|
|
15181
|
+
return null;
|
|
15182
|
+
}
|
|
15183
|
+
function clearSamplingState() {
|
|
15184
|
+
try {
|
|
15185
|
+
localStorage.removeItem(SAMPLING_STATE_KEY);
|
|
15186
|
+
} catch (error) {
|
|
15187
|
+
}
|
|
15188
|
+
}
|
|
14971
15189
|
function saveEmergencyBackup(payload) {
|
|
14972
15190
|
try {
|
|
14973
15191
|
if (payload.logs.length === 0 && (!payload.rrwebEvents || payload.rrwebEvents.length < 3)) {
|
|
@@ -14997,16 +15215,9 @@ function clearEmergencyBackup() {
|
|
|
14997
15215
|
function handleUnload() {
|
|
14998
15216
|
if (unloadHandled) return;
|
|
14999
15217
|
if (!config || !session || session.status !== "recording") return;
|
|
15000
|
-
|
|
15001
|
-
if (config.debug) {
|
|
15002
|
-
console.log("[LogSpace] Follower tab unloading - no session to save (data is with leader)");
|
|
15003
|
-
}
|
|
15004
|
-
return;
|
|
15005
|
-
}
|
|
15218
|
+
isNavigatingAway = true;
|
|
15006
15219
|
unloadHandled = true;
|
|
15007
15220
|
endReason = "unload";
|
|
15008
|
-
const payload = buildPayload();
|
|
15009
|
-
if (!payload) return;
|
|
15010
15221
|
if (config.autoEnd.continueOnRefresh) {
|
|
15011
15222
|
try {
|
|
15012
15223
|
sessionStorage.setItem("logspace_continuing_session_id", session.id);
|
|
@@ -15017,6 +15228,13 @@ function handleUnload() {
|
|
|
15017
15228
|
}
|
|
15018
15229
|
} catch {
|
|
15019
15230
|
}
|
|
15231
|
+
if (samplingTriggered) {
|
|
15232
|
+
saveSamplingState(session.id);
|
|
15233
|
+
}
|
|
15234
|
+
}
|
|
15235
|
+
const payload = buildPayload();
|
|
15236
|
+
if (!payload) {
|
|
15237
|
+
return;
|
|
15020
15238
|
}
|
|
15021
15239
|
saveEmergencyBackup(payload);
|
|
15022
15240
|
if (isIndexedDBAvailable()) {
|
|
@@ -15024,7 +15242,7 @@ function handleUnload() {
|
|
|
15024
15242
|
});
|
|
15025
15243
|
}
|
|
15026
15244
|
if (config.debug) {
|
|
15027
|
-
console.log("[LogSpace] Session saved for
|
|
15245
|
+
console.log("[LogSpace] Session paused and saved for continuation");
|
|
15028
15246
|
}
|
|
15029
15247
|
}
|
|
15030
15248
|
function handleVisibilityChange() {
|
|
@@ -15036,7 +15254,7 @@ function handleVisibilityChange() {
|
|
|
15036
15254
|
}
|
|
15037
15255
|
if (config.capture.rrweb) {
|
|
15038
15256
|
addRRWebCustomEvent("tab-hidden", {
|
|
15039
|
-
tabId:
|
|
15257
|
+
tabId: getOrCreateTabId(),
|
|
15040
15258
|
timestamp: Date.now()
|
|
15041
15259
|
});
|
|
15042
15260
|
}
|
|
@@ -15045,14 +15263,8 @@ function handleVisibilityChange() {
|
|
|
15045
15263
|
}
|
|
15046
15264
|
visibilityTimer = setTimeout(() => {
|
|
15047
15265
|
if (session?.status === "recording" && document.visibilityState === "hidden") {
|
|
15048
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
15049
|
-
if (config?.debug) {
|
|
15050
|
-
console.log("[LogSpace] Grace period expired, but follower tab - not ending session");
|
|
15051
|
-
}
|
|
15052
|
-
return;
|
|
15053
|
-
}
|
|
15054
15266
|
if (config?.debug) {
|
|
15055
|
-
console.log("[LogSpace] Grace period expired,
|
|
15267
|
+
console.log("[LogSpace] Grace period expired, saving session for continuation");
|
|
15056
15268
|
}
|
|
15057
15269
|
endReason = "navigateAway";
|
|
15058
15270
|
handleUnload();
|
|
@@ -15066,8 +15278,20 @@ function handleVisibilityChange() {
|
|
|
15066
15278
|
clearTimeout(visibilityTimer);
|
|
15067
15279
|
visibilityTimer = null;
|
|
15068
15280
|
}
|
|
15281
|
+
if (isNavigatingAway || unloadHandled) {
|
|
15282
|
+
const wasNavigating = isNavigatingAway;
|
|
15283
|
+
const wasUnloadHandled = unloadHandled;
|
|
15284
|
+
isNavigatingAway = false;
|
|
15285
|
+
unloadHandled = false;
|
|
15286
|
+
if (config?.debug) {
|
|
15287
|
+
console.log("[LogSpace] Navigation flags reset - page is visible", {
|
|
15288
|
+
wasNavigating,
|
|
15289
|
+
wasUnloadHandled
|
|
15290
|
+
});
|
|
15291
|
+
}
|
|
15292
|
+
}
|
|
15069
15293
|
if (config?.capture.rrweb && session?.status === "recording") {
|
|
15070
|
-
const tabId =
|
|
15294
|
+
const tabId = getOrCreateTabId();
|
|
15071
15295
|
if (config.debug) {
|
|
15072
15296
|
console.log("[LogSpace] Tab visible, forcing full rrweb snapshot for:", tabId);
|
|
15073
15297
|
}
|
|
@@ -15087,8 +15311,13 @@ function buildPayload() {
|
|
|
15087
15311
|
if (!session) return null;
|
|
15088
15312
|
if (config?.sampling.enabled && !samplingTriggered) {
|
|
15089
15313
|
if (config.debug) {
|
|
15090
|
-
console.log("[LogSpace] Sampling enabled but not triggered - session
|
|
15314
|
+
console.log("[LogSpace] Sampling enabled but not triggered - session discarded");
|
|
15091
15315
|
}
|
|
15316
|
+
let discardReason = "noTrigger";
|
|
15317
|
+
if (endReason === "idle") discardReason = "idle";
|
|
15318
|
+
else if (endReason === "maxDuration") discardReason = "maxDuration";
|
|
15319
|
+
else if (endReason === "manual") discardReason = "manual";
|
|
15320
|
+
config.hooks.onSessionDiscarded?.(session, discardReason);
|
|
15092
15321
|
return null;
|
|
15093
15322
|
}
|
|
15094
15323
|
if (config?.sampling.enabled && samplingTriggered) {
|
|
@@ -15120,6 +15349,65 @@ function buildPayload() {
|
|
|
15120
15349
|
}
|
|
15121
15350
|
return payload;
|
|
15122
15351
|
}
|
|
15352
|
+
async function stopSessionInternal(options) {
|
|
15353
|
+
if (!session || session.status === "stopped") {
|
|
15354
|
+
return;
|
|
15355
|
+
}
|
|
15356
|
+
const shouldRestart = options?.restartImmediately === true;
|
|
15357
|
+
const endedSessionId = session.id;
|
|
15358
|
+
session.status = "stopped";
|
|
15359
|
+
session.endTime = Date.now();
|
|
15360
|
+
continuingSessionId = null;
|
|
15361
|
+
if (idleTimer) {
|
|
15362
|
+
clearTimeout(idleTimer);
|
|
15363
|
+
idleTimer = null;
|
|
15364
|
+
}
|
|
15365
|
+
if (durationTimer) {
|
|
15366
|
+
clearTimeout(durationTimer);
|
|
15367
|
+
durationTimer = null;
|
|
15368
|
+
}
|
|
15369
|
+
if (visibilityTimer) {
|
|
15370
|
+
clearTimeout(visibilityTimer);
|
|
15371
|
+
visibilityTimer = null;
|
|
15372
|
+
}
|
|
15373
|
+
stopCheckpointTimer();
|
|
15374
|
+
stopSamplingTrimTimer();
|
|
15375
|
+
if (config?.capture.rrweb) {
|
|
15376
|
+
const events = stopRRWebRecording();
|
|
15377
|
+
if (config.debug) {
|
|
15378
|
+
console.log("[LogSpace] rrweb recording stopped, events:", events.length);
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
uninstallCaptures();
|
|
15382
|
+
config?.hooks.onSessionEnd?.(session, logs);
|
|
15383
|
+
if (config?.debug) {
|
|
15384
|
+
console.log("[LogSpace] Session stopped:", session.id, {
|
|
15385
|
+
duration: session.endTime - session.startTime,
|
|
15386
|
+
logs: logs.length,
|
|
15387
|
+
rrwebEvents: rrwebEvents.length,
|
|
15388
|
+
reason: endReason
|
|
15389
|
+
});
|
|
15390
|
+
}
|
|
15391
|
+
const payload = buildPayload();
|
|
15392
|
+
if (shouldRestart) {
|
|
15393
|
+
LogSpace.startSession();
|
|
15394
|
+
}
|
|
15395
|
+
if (!payload) {
|
|
15396
|
+
await clearCurrentSessionBackupFor(endedSessionId);
|
|
15397
|
+
clearEmergencyBackup();
|
|
15398
|
+
clearSamplingState();
|
|
15399
|
+
return;
|
|
15400
|
+
}
|
|
15401
|
+
try {
|
|
15402
|
+
await sendPayload(payload);
|
|
15403
|
+
await clearCurrentSessionBackupFor(endedSessionId);
|
|
15404
|
+
clearEmergencyBackup();
|
|
15405
|
+
clearSamplingState();
|
|
15406
|
+
} catch (error) {
|
|
15407
|
+
await addPendingSession(payload);
|
|
15408
|
+
throw error;
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15123
15411
|
async function compressPayload(data) {
|
|
15124
15412
|
const encoder = new TextEncoder();
|
|
15125
15413
|
const inputData = encoder.encode(data);
|
|
@@ -15146,9 +15434,7 @@ async function compressPayload(data) {
|
|
|
15146
15434
|
}
|
|
15147
15435
|
return new Blob([inputData], { type: "application/json" });
|
|
15148
15436
|
}
|
|
15149
|
-
async function
|
|
15150
|
-
const payload = buildPayload();
|
|
15151
|
-
if (!payload) return;
|
|
15437
|
+
async function sendPayload(payload) {
|
|
15152
15438
|
if (config?.dryRun) {
|
|
15153
15439
|
console.log("[LogSpace] 🔶 DRY RUN - Session payload:", {
|
|
15154
15440
|
id: payload.id,
|
|
@@ -15198,8 +15484,8 @@ async function sendSession() {
|
|
|
15198
15484
|
// Use actual session start time for correct video seek calculation
|
|
15199
15485
|
// SDK-specific fields for identify() and session info
|
|
15200
15486
|
url: payload.url,
|
|
15201
|
-
userId:
|
|
15202
|
-
userTraits:
|
|
15487
|
+
userId: payload.userId,
|
|
15488
|
+
userTraits: payload.userTraits,
|
|
15203
15489
|
duration: payload.duration,
|
|
15204
15490
|
stats: payload.stats
|
|
15205
15491
|
};
|
|
@@ -15289,6 +15575,27 @@ async function sendPayloadToServer(payload) {
|
|
|
15289
15575
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
15290
15576
|
}
|
|
15291
15577
|
}
|
|
15578
|
+
function serializeDedupValue(value) {
|
|
15579
|
+
try {
|
|
15580
|
+
return JSON.stringify(value);
|
|
15581
|
+
} catch {
|
|
15582
|
+
return String(value);
|
|
15583
|
+
}
|
|
15584
|
+
}
|
|
15585
|
+
function getLogDedupKey(log) {
|
|
15586
|
+
if (log.id) {
|
|
15587
|
+
return `id:${log.id}|ts:${log.timestamp}|type:${log.type}`;
|
|
15588
|
+
}
|
|
15589
|
+
return `ts:${log.timestamp}|type:${log.type}|data:${serializeDedupValue(log.data)}`;
|
|
15590
|
+
}
|
|
15591
|
+
function getEventDedupKey(event) {
|
|
15592
|
+
const eventId = event.id ?? event.data?.id;
|
|
15593
|
+
if (eventId !== void 0 && eventId !== null) {
|
|
15594
|
+
return `id:${String(eventId)}|ts:${event.timestamp}|type:${event.type}`;
|
|
15595
|
+
}
|
|
15596
|
+
const data = event.data;
|
|
15597
|
+
return `ts:${event.timestamp}|type:${event.type}|data:${serializeDedupValue(data)}`;
|
|
15598
|
+
}
|
|
15292
15599
|
function mergeRecoveredData(recoveredLogs, recoveredEvents) {
|
|
15293
15600
|
if (!session || session.status !== "recording") {
|
|
15294
15601
|
if (config?.debug) {
|
|
@@ -15297,8 +15604,16 @@ function mergeRecoveredData(recoveredLogs, recoveredEvents) {
|
|
|
15297
15604
|
return;
|
|
15298
15605
|
}
|
|
15299
15606
|
if (recoveredLogs.length > 0) {
|
|
15300
|
-
const
|
|
15301
|
-
const newLogs =
|
|
15607
|
+
const existingLogKeys = new Set(logs.map((l) => getLogDedupKey(l)));
|
|
15608
|
+
const newLogs = [];
|
|
15609
|
+
for (const log of recoveredLogs) {
|
|
15610
|
+
const key = getLogDedupKey(log);
|
|
15611
|
+
if (existingLogKeys.has(key)) {
|
|
15612
|
+
continue;
|
|
15613
|
+
}
|
|
15614
|
+
existingLogKeys.add(key);
|
|
15615
|
+
newLogs.push(log);
|
|
15616
|
+
}
|
|
15302
15617
|
if (newLogs.length > 0) {
|
|
15303
15618
|
logs = [...newLogs, ...logs];
|
|
15304
15619
|
for (const log of newLogs) {
|
|
@@ -15315,8 +15630,16 @@ function mergeRecoveredData(recoveredLogs, recoveredEvents) {
|
|
|
15315
15630
|
}
|
|
15316
15631
|
}
|
|
15317
15632
|
if (recoveredEvents.length > 0) {
|
|
15318
|
-
const
|
|
15319
|
-
const newEvents =
|
|
15633
|
+
const existingEventKeys = new Set(rrwebEvents.map((e) => getEventDedupKey(e)));
|
|
15634
|
+
const newEvents = [];
|
|
15635
|
+
for (const event of recoveredEvents) {
|
|
15636
|
+
const key = getEventDedupKey(event);
|
|
15637
|
+
if (existingEventKeys.has(key)) {
|
|
15638
|
+
continue;
|
|
15639
|
+
}
|
|
15640
|
+
existingEventKeys.add(key);
|
|
15641
|
+
newEvents.push(event);
|
|
15642
|
+
}
|
|
15320
15643
|
if (newEvents.length > 0) {
|
|
15321
15644
|
rrwebEvents = [...newEvents, ...rrwebEvents];
|
|
15322
15645
|
for (const event of newEvents) {
|
|
@@ -15337,11 +15660,17 @@ async function recoverPendingSessions() {
|
|
|
15337
15660
|
try {
|
|
15338
15661
|
const emergencyBackup = getEmergencyBackup();
|
|
15339
15662
|
if (emergencyBackup) {
|
|
15340
|
-
if (
|
|
15663
|
+
if (processedRecoverySessionIds.has(emergencyBackup.id)) {
|
|
15664
|
+
if (config.debug) {
|
|
15665
|
+
console.log("[LogSpace] Skipping already processed emergency backup:", emergencyBackup.id);
|
|
15666
|
+
}
|
|
15667
|
+
clearEmergencyBackup();
|
|
15668
|
+
} else if (recoveryTargetSessionId && emergencyBackup.id === recoveryTargetSessionId) {
|
|
15341
15669
|
if (config.debug) {
|
|
15342
15670
|
console.log("[LogSpace] Merging emergency backup into continuing session:", emergencyBackup.id);
|
|
15343
15671
|
}
|
|
15344
15672
|
mergeRecoveredData(emergencyBackup.logs, emergencyBackup.rrwebEvents || []);
|
|
15673
|
+
processedRecoverySessionIds.add(emergencyBackup.id);
|
|
15345
15674
|
clearEmergencyBackup();
|
|
15346
15675
|
await clearCurrentSessionBackup();
|
|
15347
15676
|
} else {
|
|
@@ -15375,44 +15704,59 @@ async function recoverPendingSessions() {
|
|
|
15375
15704
|
if (pendingSessions.length === 0) {
|
|
15376
15705
|
const backup = await getCurrentSessionBackup();
|
|
15377
15706
|
if (backup && backup.logs.length > 0) {
|
|
15378
|
-
if (
|
|
15379
|
-
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15415
|
-
|
|
15707
|
+
if (recoveryTargetSessionId && backup.sessionId === recoveryTargetSessionId) {
|
|
15708
|
+
if (config.debug) {
|
|
15709
|
+
console.log("[LogSpace] Merging checkpoint backup into continuing session:", backup.sessionId);
|
|
15710
|
+
}
|
|
15711
|
+
mergeRecoveredData(backup.logs, backup.rrwebEvents || []);
|
|
15712
|
+
processedRecoverySessionIds.add(backup.sessionId);
|
|
15713
|
+
await clearCurrentSessionBackup();
|
|
15714
|
+
} else if (processedRecoverySessionIds.has(backup.sessionId)) {
|
|
15715
|
+
if (config.debug) {
|
|
15716
|
+
console.log("[LogSpace] Skipping already processed backup:", backup.sessionId);
|
|
15717
|
+
}
|
|
15718
|
+
await clearCurrentSessionBackup();
|
|
15719
|
+
} else {
|
|
15720
|
+
if (config.debug) {
|
|
15721
|
+
console.log("[LogSpace] Recovering crashed session:", backup.sessionId);
|
|
15722
|
+
}
|
|
15723
|
+
const recoveredPayload = {
|
|
15724
|
+
id: backup.sessionId,
|
|
15725
|
+
startTime: backup.startTime,
|
|
15726
|
+
endTime: backup.lastCheckpoint,
|
|
15727
|
+
duration: backup.lastCheckpoint - backup.startTime,
|
|
15728
|
+
url: backup.url,
|
|
15729
|
+
title: backup.title,
|
|
15730
|
+
userId: backup.userId,
|
|
15731
|
+
userTraits: backup.userTraits,
|
|
15732
|
+
metadata: backup.metadata,
|
|
15733
|
+
logs: backup.logs,
|
|
15734
|
+
stats: {
|
|
15735
|
+
logCount: backup.logs.length,
|
|
15736
|
+
networkLogCount: backup.logs.filter((l) => l.type === "network").length,
|
|
15737
|
+
consoleLogCount: backup.logs.filter((l) => l.type === "console").length,
|
|
15738
|
+
errorCount: backup.logs.filter((l) => l.type === "error").length,
|
|
15739
|
+
storageLogCount: backup.logs.filter((l) => l.type === "storage").length,
|
|
15740
|
+
interactionLogCount: backup.logs.filter((l) => l.type === "interaction").length,
|
|
15741
|
+
performanceLogCount: backup.logs.filter((l) => l.type === "performance").length,
|
|
15742
|
+
annotationCount: backup.logs.filter((l) => l.type === "annotation").length,
|
|
15743
|
+
tabCount: 1
|
|
15744
|
+
// SDK always has single tab
|
|
15745
|
+
},
|
|
15746
|
+
endReason: "crash",
|
|
15747
|
+
recordingType: "rrweb",
|
|
15748
|
+
rrwebEvents: backup.rrwebEvents,
|
|
15749
|
+
sdkVersion: SDK_VERSION
|
|
15750
|
+
};
|
|
15751
|
+
pendingSessions.push({
|
|
15752
|
+
id: backup.sessionId,
|
|
15753
|
+
data: recoveredPayload,
|
|
15754
|
+
createdAt: backup.lastCheckpoint,
|
|
15755
|
+
retryCount: 0
|
|
15756
|
+
});
|
|
15757
|
+
await addPendingSession(recoveredPayload);
|
|
15758
|
+
await clearCurrentSessionBackup();
|
|
15759
|
+
}
|
|
15416
15760
|
}
|
|
15417
15761
|
}
|
|
15418
15762
|
if (pendingSessions.length === 0) return;
|
|
@@ -15420,16 +15764,25 @@ async function recoverPendingSessions() {
|
|
|
15420
15764
|
console.log(`[LogSpace] Recovering ${pendingSessions.length} pending session(s)`);
|
|
15421
15765
|
}
|
|
15422
15766
|
for (const stored of pendingSessions) {
|
|
15767
|
+
if (processedRecoverySessionIds.has(stored.id)) {
|
|
15768
|
+
if (config.debug) {
|
|
15769
|
+
console.log("[LogSpace] Skipping already processed pending session:", stored.id);
|
|
15770
|
+
}
|
|
15771
|
+
await removePendingSession(stored.id);
|
|
15772
|
+
continue;
|
|
15773
|
+
}
|
|
15423
15774
|
if (recoveryTargetSessionId && stored.id === recoveryTargetSessionId) {
|
|
15424
15775
|
if (config.debug) {
|
|
15425
15776
|
console.log("[LogSpace] Merging pending session into continuing session:", stored.id);
|
|
15426
15777
|
}
|
|
15427
15778
|
mergeRecoveredData(stored.data.logs, stored.data.rrwebEvents || []);
|
|
15779
|
+
processedRecoverySessionIds.add(stored.id);
|
|
15428
15780
|
await removePendingSession(stored.id);
|
|
15429
15781
|
continue;
|
|
15430
15782
|
}
|
|
15431
15783
|
try {
|
|
15432
15784
|
await sendPayloadToServer(stored.data);
|
|
15785
|
+
processedRecoverySessionIds.add(stored.id);
|
|
15433
15786
|
await removePendingSession(stored.id);
|
|
15434
15787
|
if (config.debug) {
|
|
15435
15788
|
console.log("[LogSpace] Recovered session sent:", stored.id);
|
|
@@ -15452,11 +15805,11 @@ async function recoverPendingSessions() {
|
|
|
15452
15805
|
const LogSpace = {
|
|
15453
15806
|
/**
|
|
15454
15807
|
* Initialize the SDK
|
|
15808
|
+
* @throws Error if SDK is already initialized
|
|
15455
15809
|
*/
|
|
15456
15810
|
init(userConfig) {
|
|
15457
15811
|
if (initialized) {
|
|
15458
|
-
|
|
15459
|
-
return;
|
|
15812
|
+
throw new Error("[LogSpace] SDK already initialized. Call destroy() first if you need to reinitialize.");
|
|
15460
15813
|
}
|
|
15461
15814
|
if (typeof window === "undefined") {
|
|
15462
15815
|
console.warn("[LogSpace] SDK requires browser environment");
|
|
@@ -15469,54 +15822,9 @@ const LogSpace = {
|
|
|
15469
15822
|
version: SDK_VERSION,
|
|
15470
15823
|
serverUrl: config.serverUrl,
|
|
15471
15824
|
limits: config.limits,
|
|
15472
|
-
persistence: isIndexedDBAvailable() ? "enabled" : "disabled"
|
|
15473
|
-
multiTab: multiTabCoordinator.isSupported() ? "enabled" : "disabled"
|
|
15825
|
+
persistence: isIndexedDBAvailable() ? "enabled" : "disabled"
|
|
15474
15826
|
});
|
|
15475
15827
|
}
|
|
15476
|
-
if (multiTabCoordinator.isSupported()) {
|
|
15477
|
-
multiTabCoordinator.init(
|
|
15478
|
-
{
|
|
15479
|
-
onBecomeLeader: (sessionId, isNewSession) => {
|
|
15480
|
-
if (config?.debug) {
|
|
15481
|
-
console.log("[LogSpace] This tab is now the leader", { sessionId, isNewSession });
|
|
15482
|
-
}
|
|
15483
|
-
if (!isNewSession) {
|
|
15484
|
-
continuingSessionId = sessionId;
|
|
15485
|
-
}
|
|
15486
|
-
if (!session) {
|
|
15487
|
-
LogSpace.startSession();
|
|
15488
|
-
}
|
|
15489
|
-
},
|
|
15490
|
-
onBecomeFollower: (sessionId) => {
|
|
15491
|
-
if (config?.debug) {
|
|
15492
|
-
console.log("[LogSpace] This tab is now a follower", { sessionId });
|
|
15493
|
-
}
|
|
15494
|
-
if (!session) {
|
|
15495
|
-
LogSpace.startSession();
|
|
15496
|
-
}
|
|
15497
|
-
},
|
|
15498
|
-
onReceiveLog: (log) => {
|
|
15499
|
-
if (session?.status === "recording") {
|
|
15500
|
-
logs.push(log);
|
|
15501
|
-
currentSize += JSON.stringify(log).length;
|
|
15502
|
-
updateStats(log);
|
|
15503
|
-
}
|
|
15504
|
-
},
|
|
15505
|
-
onReceiveRRWebEvent: (event) => {
|
|
15506
|
-
if (session?.status === "recording") {
|
|
15507
|
-
rrwebEvents.push(event);
|
|
15508
|
-
rrwebSize += JSON.stringify(event).length;
|
|
15509
|
-
}
|
|
15510
|
-
},
|
|
15511
|
-
onLeaderChanged: (newLeaderTabId) => {
|
|
15512
|
-
if (config?.debug) {
|
|
15513
|
-
console.log("[LogSpace] Leader changed to:", newLeaderTabId);
|
|
15514
|
-
}
|
|
15515
|
-
}
|
|
15516
|
-
},
|
|
15517
|
-
config.debug
|
|
15518
|
-
);
|
|
15519
|
-
}
|
|
15520
15828
|
if (config.autoEnd.continueOnRefresh) {
|
|
15521
15829
|
try {
|
|
15522
15830
|
const savedSessionId = sessionStorage.getItem("logspace_continuing_session_id");
|
|
@@ -15563,9 +15871,8 @@ const LogSpace = {
|
|
|
15563
15871
|
installActivityListeners();
|
|
15564
15872
|
}
|
|
15565
15873
|
unloadHandled = false;
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
}
|
|
15874
|
+
isNavigatingAway = false;
|
|
15875
|
+
this.startSession();
|
|
15569
15876
|
recoverPendingSessions().catch(() => {
|
|
15570
15877
|
});
|
|
15571
15878
|
},
|
|
@@ -15581,20 +15888,16 @@ const LogSpace = {
|
|
|
15581
15888
|
console.warn("[LogSpace] Session already recording");
|
|
15582
15889
|
return;
|
|
15583
15890
|
}
|
|
15584
|
-
if (sessionEndedByHardLimit) {
|
|
15585
|
-
if (config.debug) {
|
|
15586
|
-
console.log("[LogSpace] Session not restarted - previous session ended by hard limit");
|
|
15587
|
-
}
|
|
15588
|
-
return;
|
|
15589
|
-
}
|
|
15590
15891
|
sessionEndedByIdle = false;
|
|
15591
15892
|
samplingTriggered = false;
|
|
15592
15893
|
samplingTriggerType = null;
|
|
15894
|
+
samplingFirstTriggerTime = null;
|
|
15593
15895
|
samplingTriggerTime = null;
|
|
15594
15896
|
if (samplingEndTimer) {
|
|
15595
15897
|
clearTimeout(samplingEndTimer);
|
|
15596
15898
|
samplingEndTimer = null;
|
|
15597
15899
|
}
|
|
15900
|
+
stopSamplingTrimTimer();
|
|
15598
15901
|
logs = [];
|
|
15599
15902
|
rrwebEvents = [];
|
|
15600
15903
|
currentSize = 0;
|
|
@@ -15604,28 +15907,44 @@ const LogSpace = {
|
|
|
15604
15907
|
endReason = "manual";
|
|
15605
15908
|
let sessionId;
|
|
15606
15909
|
let sessionStartTime;
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
sessionStartTime = Date.now();
|
|
15610
|
-
} else if (continuingSessionId) {
|
|
15910
|
+
let restoredSamplingState = false;
|
|
15911
|
+
if (continuingSessionId) {
|
|
15611
15912
|
sessionId = continuingSessionId;
|
|
15612
15913
|
sessionStartTime = continuingSessionStartTime || Date.now();
|
|
15613
15914
|
if (config.debug) {
|
|
15614
15915
|
console.log("[LogSpace] Using continuing session ID:", sessionId, "with original startTime:", sessionStartTime);
|
|
15615
15916
|
}
|
|
15917
|
+
const savedSamplingState = getSamplingState();
|
|
15918
|
+
if (savedSamplingState && savedSamplingState.sessionId === sessionId && savedSamplingState.triggered) {
|
|
15919
|
+
samplingTriggered = savedSamplingState.triggered;
|
|
15920
|
+
samplingTriggerType = savedSamplingState.triggerType;
|
|
15921
|
+
samplingFirstTriggerTime = savedSamplingState.firstTriggerTime;
|
|
15922
|
+
samplingTriggerTime = savedSamplingState.triggerTime;
|
|
15923
|
+
restoredSamplingState = true;
|
|
15924
|
+
if (config.debug) {
|
|
15925
|
+
console.log("[LogSpace] Restored sampling state from before refresh:", savedSamplingState.triggerType);
|
|
15926
|
+
}
|
|
15927
|
+
}
|
|
15928
|
+
clearSamplingState();
|
|
15616
15929
|
continuingSessionId = null;
|
|
15617
15930
|
continuingSessionStartTime = null;
|
|
15618
15931
|
} else {
|
|
15619
15932
|
sessionId = generateSessionId();
|
|
15620
15933
|
sessionStartTime = Date.now();
|
|
15621
15934
|
}
|
|
15935
|
+
const identityToApply = pendingIdentity || currentIdentity;
|
|
15936
|
+
const tabId = getOrCreateTabId();
|
|
15937
|
+
const sessionMetadata = buildSessionMetadata(metadata);
|
|
15622
15938
|
session = {
|
|
15623
15939
|
id: sessionId,
|
|
15940
|
+
tabId,
|
|
15624
15941
|
startTime: sessionStartTime,
|
|
15625
15942
|
status: "recording",
|
|
15626
15943
|
url: window.location.href,
|
|
15627
15944
|
title: document.title,
|
|
15628
|
-
metadata,
|
|
15945
|
+
metadata: sessionMetadata,
|
|
15946
|
+
userId: identityToApply?.userId,
|
|
15947
|
+
userTraits: identityToApply?.traits,
|
|
15629
15948
|
stats: {
|
|
15630
15949
|
logCount: 0,
|
|
15631
15950
|
networkLogCount: 0,
|
|
@@ -15635,11 +15954,10 @@ const LogSpace = {
|
|
|
15635
15954
|
interactionLogCount: 0,
|
|
15636
15955
|
performanceLogCount: 0,
|
|
15637
15956
|
annotationCount: 0,
|
|
15638
|
-
tabCount:
|
|
15639
|
-
// Will be updated by multi-tab coordinator
|
|
15957
|
+
tabCount: 1
|
|
15640
15958
|
}
|
|
15641
15959
|
};
|
|
15642
|
-
|
|
15960
|
+
setSessionContext(sessionId, tabId);
|
|
15643
15961
|
if (config.capture.rrweb) {
|
|
15644
15962
|
const rrwebStarted = startRRWebRecording(
|
|
15645
15963
|
{
|
|
@@ -15652,23 +15970,17 @@ const LogSpace = {
|
|
|
15652
15970
|
},
|
|
15653
15971
|
(event) => {
|
|
15654
15972
|
if (!session || session.status !== "recording") return;
|
|
15655
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
15656
|
-
multiTabCoordinator.sendRRWebEvent(event);
|
|
15657
|
-
resetIdleTimer();
|
|
15658
|
-
return;
|
|
15659
|
-
}
|
|
15660
15973
|
const eventSize = JSON.stringify(event).length;
|
|
15661
15974
|
rrwebSize += eventSize;
|
|
15662
15975
|
const totalSize = currentSize + rrwebSize;
|
|
15663
15976
|
if (config && totalSize >= config.limits.maxSize) {
|
|
15664
|
-
if (config.autoEnd.onLimitReached
|
|
15665
|
-
sessionEndedByHardLimit = true;
|
|
15977
|
+
if (config.autoEnd.onLimitReached) {
|
|
15666
15978
|
if (config.debug) {
|
|
15667
15979
|
console.log("[LogSpace] Session auto-ended: maxSize (rrweb)");
|
|
15668
15980
|
}
|
|
15669
15981
|
endReason = "maxSize";
|
|
15670
15982
|
config.hooks.onLimitReached?.("maxSize");
|
|
15671
|
-
|
|
15983
|
+
stopSessionInternal({ restartImmediately: true });
|
|
15672
15984
|
}
|
|
15673
15985
|
return;
|
|
15674
15986
|
}
|
|
@@ -15689,13 +16001,47 @@ const LogSpace = {
|
|
|
15689
16001
|
}
|
|
15690
16002
|
endReason = "maxDuration";
|
|
15691
16003
|
config?.hooks.onLimitReached?.("maxDuration");
|
|
15692
|
-
|
|
16004
|
+
stopSessionInternal({ restartImmediately: true });
|
|
15693
16005
|
}
|
|
15694
16006
|
}, config.limits.maxDuration * 1e3);
|
|
15695
16007
|
}
|
|
15696
16008
|
resetIdleTimer();
|
|
15697
16009
|
installCaptures();
|
|
15698
16010
|
startCheckpointTimer();
|
|
16011
|
+
if (config.sampling.enabled && !restoredSamplingState) {
|
|
16012
|
+
startSamplingTrimTimer();
|
|
16013
|
+
} else if (config.sampling.enabled && restoredSamplingState && samplingTriggerTime) {
|
|
16014
|
+
const elapsed = Date.now() - samplingTriggerTime;
|
|
16015
|
+
const remainingTime = Math.max(0, config.sampling.recordAfter * 1e3 - elapsed);
|
|
16016
|
+
if (config.debug) {
|
|
16017
|
+
console.log(`[LogSpace] Sampling was triggered before refresh, ${remainingTime}ms remaining in recordAfter`);
|
|
16018
|
+
}
|
|
16019
|
+
if (remainingTime > 0) {
|
|
16020
|
+
samplingEndTimer = setTimeout(() => {
|
|
16021
|
+
if (session?.status === "recording") {
|
|
16022
|
+
if (config?.debug) {
|
|
16023
|
+
console.log("[LogSpace] Sampling recordAfter period complete, ending session");
|
|
16024
|
+
}
|
|
16025
|
+
endReason = "samplingTriggered";
|
|
16026
|
+
stopSessionInternal({ restartImmediately: true });
|
|
16027
|
+
}
|
|
16028
|
+
}, remainingTime);
|
|
16029
|
+
} else {
|
|
16030
|
+
if (config.debug) {
|
|
16031
|
+
console.log("[LogSpace] Sampling recordAfter period already elapsed, ending session");
|
|
16032
|
+
}
|
|
16033
|
+
endReason = "samplingTriggered";
|
|
16034
|
+
stopSessionInternal({ restartImmediately: true });
|
|
16035
|
+
}
|
|
16036
|
+
}
|
|
16037
|
+
if (session && identityToApply) {
|
|
16038
|
+
if (pendingIdentity && config.debug) {
|
|
16039
|
+
console.log("[LogSpace] Applied pending user identity:", identityToApply.userId);
|
|
16040
|
+
}
|
|
16041
|
+
}
|
|
16042
|
+
if (pendingIdentity) {
|
|
16043
|
+
pendingIdentity = null;
|
|
16044
|
+
}
|
|
15699
16045
|
if (session) {
|
|
15700
16046
|
config.hooks.onSessionStart?.(session);
|
|
15701
16047
|
if (config.debug) {
|
|
@@ -15707,68 +16053,23 @@ const LogSpace = {
|
|
|
15707
16053
|
* Stop recording and send session to server
|
|
15708
16054
|
*/
|
|
15709
16055
|
async stopSession() {
|
|
15710
|
-
|
|
15711
|
-
return;
|
|
15712
|
-
}
|
|
15713
|
-
session.status = "stopped";
|
|
15714
|
-
session.endTime = Date.now();
|
|
15715
|
-
continuingSessionId = null;
|
|
15716
|
-
if (idleTimer) {
|
|
15717
|
-
clearTimeout(idleTimer);
|
|
15718
|
-
idleTimer = null;
|
|
15719
|
-
}
|
|
15720
|
-
if (durationTimer) {
|
|
15721
|
-
clearTimeout(durationTimer);
|
|
15722
|
-
durationTimer = null;
|
|
15723
|
-
}
|
|
15724
|
-
if (visibilityTimer) {
|
|
15725
|
-
clearTimeout(visibilityTimer);
|
|
15726
|
-
visibilityTimer = null;
|
|
15727
|
-
}
|
|
15728
|
-
stopCheckpointTimer();
|
|
15729
|
-
if (config?.capture.rrweb) {
|
|
15730
|
-
const events = stopRRWebRecording();
|
|
15731
|
-
if (config.debug) {
|
|
15732
|
-
console.log("[LogSpace] rrweb recording stopped, events:", events.length);
|
|
15733
|
-
}
|
|
15734
|
-
}
|
|
15735
|
-
uninstallCaptures();
|
|
15736
|
-
config?.hooks.onSessionEnd?.(session, logs);
|
|
15737
|
-
if (config?.debug) {
|
|
15738
|
-
console.log("[LogSpace] Session stopped:", session.id, {
|
|
15739
|
-
duration: session.endTime - session.startTime,
|
|
15740
|
-
logs: logs.length,
|
|
15741
|
-
rrwebEvents: rrwebEvents.length,
|
|
15742
|
-
reason: endReason
|
|
15743
|
-
});
|
|
15744
|
-
}
|
|
15745
|
-
if (!multiTabCoordinator.isActive() || multiTabCoordinator.isLeaderTab()) {
|
|
15746
|
-
try {
|
|
15747
|
-
await sendSession();
|
|
15748
|
-
await clearCurrentSessionBackup();
|
|
15749
|
-
clearEmergencyBackup();
|
|
15750
|
-
if (multiTabCoordinator.isActive()) {
|
|
15751
|
-
multiTabCoordinator.endSession();
|
|
15752
|
-
}
|
|
15753
|
-
} catch (error) {
|
|
15754
|
-
const payload = buildPayload();
|
|
15755
|
-
if (payload) {
|
|
15756
|
-
await addPendingSession(payload);
|
|
15757
|
-
}
|
|
15758
|
-
throw error;
|
|
15759
|
-
}
|
|
15760
|
-
}
|
|
16056
|
+
await stopSessionInternal();
|
|
15761
16057
|
},
|
|
15762
16058
|
/**
|
|
15763
16059
|
* Identify user
|
|
16060
|
+
* If called before session starts, the identity will be queued and applied when session starts
|
|
15764
16061
|
*/
|
|
15765
16062
|
identify(userId, traits) {
|
|
15766
|
-
|
|
16063
|
+
currentIdentity = { userId, traits };
|
|
16064
|
+
if (!session) {
|
|
16065
|
+
pendingIdentity = { userId, traits };
|
|
16066
|
+
if (config?.debug) {
|
|
16067
|
+
console.log("[LogSpace] User identity queued (session not started yet):", userId);
|
|
16068
|
+
}
|
|
16069
|
+
return;
|
|
16070
|
+
}
|
|
15767
16071
|
session.userId = userId;
|
|
15768
16072
|
session.userTraits = traits;
|
|
15769
|
-
if (multiTabCoordinator.isActive()) {
|
|
15770
|
-
multiTabCoordinator.updateSessionUser(userId, traits);
|
|
15771
|
-
}
|
|
15772
16073
|
if (config?.debug) {
|
|
15773
16074
|
console.log("[LogSpace] User identified:", userId);
|
|
15774
16075
|
}
|
|
@@ -15781,7 +16082,7 @@ const LogSpace = {
|
|
|
15781
16082
|
type: "annotation",
|
|
15782
16083
|
data: {
|
|
15783
16084
|
annotationType: "event",
|
|
15784
|
-
event,
|
|
16085
|
+
eventName: event,
|
|
15785
16086
|
properties
|
|
15786
16087
|
}
|
|
15787
16088
|
});
|
|
@@ -15862,6 +16163,51 @@ const LogSpace = {
|
|
|
15862
16163
|
getRRWebEvents() {
|
|
15863
16164
|
return [...rrwebEvents];
|
|
15864
16165
|
},
|
|
16166
|
+
/**
|
|
16167
|
+
* Get current configuration (read-only)
|
|
16168
|
+
* Returns the full NormalizedConfig plus convenience top-level fields
|
|
16169
|
+
* that match the setConfig() interface for easy verification
|
|
16170
|
+
*/
|
|
16171
|
+
getConfig() {
|
|
16172
|
+
if (!config) return null;
|
|
16173
|
+
return {
|
|
16174
|
+
...config,
|
|
16175
|
+
// Expose convenience top-level fields that match setConfig() interface
|
|
16176
|
+
rateLimit: config.limits.rateLimit,
|
|
16177
|
+
maxLogs: config.limits.maxLogs,
|
|
16178
|
+
maxSize: config.limits.maxSize,
|
|
16179
|
+
idleTimeout: config.limits.idleTimeout
|
|
16180
|
+
};
|
|
16181
|
+
},
|
|
16182
|
+
/**
|
|
16183
|
+
* Update configuration at runtime
|
|
16184
|
+
* Only certain settings can be changed after initialization
|
|
16185
|
+
*/
|
|
16186
|
+
setConfig(updates) {
|
|
16187
|
+
if (!config) {
|
|
16188
|
+
console.warn("[LogSpace] SDK not initialized, cannot update config");
|
|
16189
|
+
return;
|
|
16190
|
+
}
|
|
16191
|
+
if (updates.rateLimit !== void 0) {
|
|
16192
|
+
config.limits.rateLimit = updates.rateLimit;
|
|
16193
|
+
}
|
|
16194
|
+
if (updates.maxLogs !== void 0) {
|
|
16195
|
+
config.limits.maxLogs = updates.maxLogs;
|
|
16196
|
+
}
|
|
16197
|
+
if (updates.maxSize !== void 0) {
|
|
16198
|
+
config.limits.maxSize = updates.maxSize;
|
|
16199
|
+
}
|
|
16200
|
+
if (updates.idleTimeout !== void 0) {
|
|
16201
|
+
config.limits.idleTimeout = updates.idleTimeout;
|
|
16202
|
+
resetIdleTimer();
|
|
16203
|
+
}
|
|
16204
|
+
if (updates.debug !== void 0) {
|
|
16205
|
+
config.debug = updates.debug;
|
|
16206
|
+
}
|
|
16207
|
+
if (config.debug) {
|
|
16208
|
+
console.log("[LogSpace] Config updated:", updates);
|
|
16209
|
+
}
|
|
16210
|
+
},
|
|
15865
16211
|
/**
|
|
15866
16212
|
* Destroy SDK
|
|
15867
16213
|
*/
|
|
@@ -15884,9 +16230,7 @@ const LogSpace = {
|
|
|
15884
16230
|
visibilityTimer = null;
|
|
15885
16231
|
}
|
|
15886
16232
|
stopCheckpointTimer();
|
|
15887
|
-
|
|
15888
|
-
multiTabCoordinator.cleanup();
|
|
15889
|
-
}
|
|
16233
|
+
stopSamplingTrimTimer();
|
|
15890
16234
|
if (config?.capture.rrweb) {
|
|
15891
16235
|
stopRRWebRecording();
|
|
15892
16236
|
}
|
|
@@ -15905,14 +16249,16 @@ const LogSpace = {
|
|
|
15905
16249
|
rrwebEvents = [];
|
|
15906
16250
|
currentSize = 0;
|
|
15907
16251
|
rrwebSize = 0;
|
|
15908
|
-
sessionEndedByHardLimit = false;
|
|
15909
16252
|
sessionEndedByIdle = false;
|
|
15910
16253
|
samplingTriggered = false;
|
|
15911
16254
|
samplingTriggerType = null;
|
|
16255
|
+
samplingFirstTriggerTime = null;
|
|
15912
16256
|
samplingTriggerTime = null;
|
|
15913
16257
|
recoveryTargetSessionId = null;
|
|
15914
16258
|
unloadHandled = false;
|
|
16259
|
+
isNavigatingAway = false;
|
|
15915
16260
|
lastMouseMoveTime = 0;
|
|
16261
|
+
currentTabId = null;
|
|
15916
16262
|
initialized = false;
|
|
15917
16263
|
if (wasDebug) {
|
|
15918
16264
|
console.log("[LogSpace] SDK destroyed");
|