@offscript/sdk 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/adapters/nuxt.cjs +16 -0
- package/dist/adapters/nuxt.cjs.map +1 -0
- package/dist/adapters/nuxt.d.ts +10 -0
- package/dist/adapters/nuxt.js +14 -0
- package/dist/adapters/nuxt.js.map +1 -0
- package/dist/adapters/vanilla.cjs +18 -0
- package/dist/adapters/vanilla.cjs.map +1 -0
- package/dist/adapters/vanilla.d.ts +6 -0
- package/dist/adapters/vanilla.js +5 -0
- package/dist/adapters/vanilla.js.map +1 -0
- package/dist/adapters/vue.cjs +24 -0
- package/dist/adapters/vue.cjs.map +1 -0
- package/dist/adapters/vue.d.ts +18 -0
- package/dist/adapters/vue.js +21 -0
- package/dist/adapters/vue.js.map +1 -0
- package/dist/chunk-3NX2AGXM.cjs +2671 -0
- package/dist/chunk-3NX2AGXM.cjs.map +1 -0
- package/dist/chunk-3OSCH7J2.js +2658 -0
- package/dist/chunk-3OSCH7J2.js.map +1 -0
- package/dist/chunk-B6GO5FE5.cjs +442 -0
- package/dist/chunk-B6GO5FE5.cjs.map +1 -0
- package/dist/chunk-HAPNSHFG.cjs +20 -0
- package/dist/chunk-HAPNSHFG.cjs.map +1 -0
- package/dist/chunk-LFXHB4PN.js +426 -0
- package/dist/chunk-LFXHB4PN.js.map +1 -0
- package/dist/chunk-MBYNQSKA.js +17 -0
- package/dist/chunk-MBYNQSKA.js.map +1 -0
- package/dist/core/ApiClient.d.ts +24 -0
- package/dist/core/LifecycleManager.d.ts +26 -0
- package/dist/core/OffscriptSDK.d.ts +98 -0
- package/dist/core/SessionManager.d.ts +44 -0
- package/dist/core/config.d.ts +2 -0
- package/dist/index.cjs +98 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/playwright/ActionNormalizer.d.ts +37 -0
- package/dist/playwright/PlaywrightDataContract.d.ts +88 -0
- package/dist/playwright/SelectorGenerator.d.ts +26 -0
- package/dist/playwright/index.d.ts +3 -0
- package/dist/playwright.cjs +56 -0
- package/dist/playwright.cjs.map +1 -0
- package/dist/playwright.js +3 -0
- package/dist/playwright.js.map +1 -0
- package/dist/privacy/DataSanitizer.d.ts +32 -0
- package/dist/privacy/PrivacyConfig.d.ts +4 -0
- package/dist/recorders/ActionRecorder.d.ts +46 -0
- package/dist/recorders/ContextRecorder.d.ts +23 -0
- package/dist/recorders/ErrorRecorder.d.ts +20 -0
- package/dist/recorders/HTTPRecorder.d.ts +28 -0
- package/dist/recorders/IframeRecorder.d.ts +19 -0
- package/dist/recorders/RRWebRecorder.d.ts +29 -0
- package/dist/recorders/types.d.ts +25 -0
- package/dist/transport/EventQueue.d.ts +28 -0
- package/dist/transport/EventTransport.d.ts +84 -0
- package/dist/transport/PersistentBuffer.d.ts +60 -0
- package/dist/transport/RetryManager.d.ts +9 -0
- package/dist/types/config.d.ts +115 -0
- package/dist/types/events.d.ts +185 -0
- package/dist/types/session.d.ts +77 -0
- package/dist/utils/errors.d.ts +41 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/misc.d.ts +20 -0
- package/docs/api-contract.md +56 -0
- package/docs/architecture.md +38 -0
- package/docs/event-contract.md +26 -0
- package/docs/playwright-contract.md +39 -0
- package/package.json +80 -4
|
@@ -0,0 +1,2671 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkB6GO5FE5_cjs = require('./chunk-B6GO5FE5.cjs');
|
|
4
|
+
|
|
5
|
+
// src/types/config.ts
|
|
6
|
+
var DEFAULT_RECORDING = {
|
|
7
|
+
rrweb: true,
|
|
8
|
+
actions: true,
|
|
9
|
+
http: true,
|
|
10
|
+
console: true,
|
|
11
|
+
errors: true,
|
|
12
|
+
context: true,
|
|
13
|
+
iframes: true,
|
|
14
|
+
mouseMove: false,
|
|
15
|
+
consoleLevels: ["log", "info", "warn", "error"],
|
|
16
|
+
throttleMs: 100,
|
|
17
|
+
rrwebCheckoutEveryMs: 5 * 6e4,
|
|
18
|
+
rrwebRecordCanvas: false,
|
|
19
|
+
testIdAttributes: ["data-testid", "data-test", "data-cy", "data-qa"],
|
|
20
|
+
stableAttributes: ["name", "data-id", "aria-label"]
|
|
21
|
+
};
|
|
22
|
+
var DEFAULT_PRIVACY = {
|
|
23
|
+
maskSensitiveInputs: true,
|
|
24
|
+
maskAllText: false,
|
|
25
|
+
captureRequestBodies: false,
|
|
26
|
+
captureResponseBodies: false,
|
|
27
|
+
allowedInputSelectors: ["[data-offscript-allow]"],
|
|
28
|
+
captureChoiceValues: true,
|
|
29
|
+
blockSelectors: ["[data-offscript-block]", ".offscript-block"],
|
|
30
|
+
maskTextSelectors: ["[data-offscript-mask]", ".offscript-mask"],
|
|
31
|
+
ignoreSelectors: ["[data-offscript-ignore]", ".offscript-ignore"],
|
|
32
|
+
ignoreUrls: [],
|
|
33
|
+
redactHeaders: [],
|
|
34
|
+
redactKeys: [],
|
|
35
|
+
maxBodyBytes: 16 * 1024,
|
|
36
|
+
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i],
|
|
37
|
+
localRetentionMs: 24 * 36e5
|
|
38
|
+
};
|
|
39
|
+
var DEFAULT_TRANSPORT = {
|
|
40
|
+
batchMaxEvents: 200,
|
|
41
|
+
batchMaxBytes: 512 * 1024,
|
|
42
|
+
flushIntervalMs: 3e3,
|
|
43
|
+
maxMemoryEvents: 5e3,
|
|
44
|
+
maxBufferBytes: 64 * 1024 * 1024,
|
|
45
|
+
maxRetries: 6,
|
|
46
|
+
retryBaseMs: 500,
|
|
47
|
+
retryMaxMs: 3e4,
|
|
48
|
+
compress: false,
|
|
49
|
+
keepaliveMaxBytes: 60 * 1024,
|
|
50
|
+
persist: true,
|
|
51
|
+
requestTimeoutMs: 15e3
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/utils/misc.ts
|
|
55
|
+
function safe(fn, fallback) {
|
|
56
|
+
try {
|
|
57
|
+
return fn();
|
|
58
|
+
} catch {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function debounce(fn, ms) {
|
|
63
|
+
let timer;
|
|
64
|
+
let args;
|
|
65
|
+
const d = ((...a) => {
|
|
66
|
+
args = a;
|
|
67
|
+
if (timer) clearTimeout(timer);
|
|
68
|
+
timer = setTimeout(() => {
|
|
69
|
+
timer = void 0;
|
|
70
|
+
const x = args;
|
|
71
|
+
args = void 0;
|
|
72
|
+
if (x) fn(...x);
|
|
73
|
+
}, ms);
|
|
74
|
+
});
|
|
75
|
+
d.cancel = () => {
|
|
76
|
+
if (timer) clearTimeout(timer);
|
|
77
|
+
timer = void 0;
|
|
78
|
+
args = void 0;
|
|
79
|
+
};
|
|
80
|
+
d.flush = () => {
|
|
81
|
+
if (!timer) return;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
timer = void 0;
|
|
84
|
+
const x = args;
|
|
85
|
+
args = void 0;
|
|
86
|
+
if (x) fn(...x);
|
|
87
|
+
};
|
|
88
|
+
return d;
|
|
89
|
+
}
|
|
90
|
+
function approxSize(value) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(value)?.length ?? 0;
|
|
93
|
+
} catch {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function deepMerge(base, patch) {
|
|
98
|
+
const out = { ...base };
|
|
99
|
+
if (!patch) return out;
|
|
100
|
+
for (const [k, v] of Object.entries(patch)) if (v !== void 0) out[k] = v;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
var Disposer = class {
|
|
104
|
+
constructor() {
|
|
105
|
+
this.fns = [];
|
|
106
|
+
}
|
|
107
|
+
add(fn) {
|
|
108
|
+
this.fns.push(fn);
|
|
109
|
+
}
|
|
110
|
+
listen(target, type, handler, opts) {
|
|
111
|
+
target.addEventListener(type, handler, opts);
|
|
112
|
+
this.fns.push(() => target.removeEventListener(type, handler, opts));
|
|
113
|
+
}
|
|
114
|
+
dispose() {
|
|
115
|
+
const fns = this.fns;
|
|
116
|
+
this.fns = [];
|
|
117
|
+
for (const fn of fns.reverse()) safe(fn);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/privacy/PrivacyConfig.ts
|
|
122
|
+
function resolvePrivacy(user) {
|
|
123
|
+
return deepMerge(DEFAULT_PRIVACY, user);
|
|
124
|
+
}
|
|
125
|
+
var BASE_SENSITIVE_HEADERS = [
|
|
126
|
+
"authorization",
|
|
127
|
+
"proxy-authorization",
|
|
128
|
+
"cookie",
|
|
129
|
+
"set-cookie",
|
|
130
|
+
"x-api-key",
|
|
131
|
+
"x-auth-token",
|
|
132
|
+
"x-csrf-token",
|
|
133
|
+
"x-xsrf-token",
|
|
134
|
+
"x-session-token"
|
|
135
|
+
];
|
|
136
|
+
var BASE_SENSITIVE_KEY_PATTERN = /pass(word|wd)?|senha|secret|token|auth|bearer|api[-_]?key|apikey|credential|session|cookie|jwt|signature|private|cvv|cvc|card[-_]?(number|num)|cartao|cpf|cnpj|ssn|otp|pin$/i;
|
|
137
|
+
|
|
138
|
+
// src/privacy/DataSanitizer.ts
|
|
139
|
+
var REDACTED = "[REDACTED]";
|
|
140
|
+
var JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*\b/g;
|
|
141
|
+
var BEARER = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
142
|
+
var CPF = /\b\d{3}\.\d{3}\.\d{3}-\d{2}\b/g;
|
|
143
|
+
var CNPJ = /\b\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}\b/g;
|
|
144
|
+
var CARD = /\b(?:\d[ -]?){13,19}\b/g;
|
|
145
|
+
function luhn(digits) {
|
|
146
|
+
let sum = 0;
|
|
147
|
+
let alt = false;
|
|
148
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
149
|
+
let n = digits.charCodeAt(i) - 48;
|
|
150
|
+
if (alt) {
|
|
151
|
+
n *= 2;
|
|
152
|
+
if (n > 9) n -= 9;
|
|
153
|
+
}
|
|
154
|
+
sum += n;
|
|
155
|
+
alt = !alt;
|
|
156
|
+
}
|
|
157
|
+
return sum % 10 === 0;
|
|
158
|
+
}
|
|
159
|
+
var DataSanitizer = class {
|
|
160
|
+
constructor(cfg) {
|
|
161
|
+
this.cfg = cfg;
|
|
162
|
+
this.headerSet = /* @__PURE__ */ new Set([...BASE_SENSITIVE_HEADERS, ...cfg.redactHeaders.map((h) => h.toLowerCase())]);
|
|
163
|
+
this.extraKeys = new Set(cfg.redactKeys.map((k) => k.toLowerCase()));
|
|
164
|
+
}
|
|
165
|
+
isSensitiveKey(key) {
|
|
166
|
+
return BASE_SENSITIVE_KEY_PATTERN.test(key) || this.extraKeys.has(key.toLowerCase());
|
|
167
|
+
}
|
|
168
|
+
/** Mascara padrões sensíveis em texto livre (JWT, Bearer, CPF, CNPJ, cartões). */
|
|
169
|
+
scrubString(input) {
|
|
170
|
+
if (!input) return input;
|
|
171
|
+
let s = input.replace(JWT, REDACTED).replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
|
|
172
|
+
s = s.replace(CNPJ, REDACTED).replace(CPF, REDACTED);
|
|
173
|
+
s = s.replace(CARD, (m) => {
|
|
174
|
+
const digits = m.replace(/[ -]/g, "");
|
|
175
|
+
return digits.length >= 13 && digits.length <= 19 && luhn(digits) ? REDACTED : m;
|
|
176
|
+
});
|
|
177
|
+
return s;
|
|
178
|
+
}
|
|
179
|
+
sanitizeHeaders(headers) {
|
|
180
|
+
const out = {};
|
|
181
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
182
|
+
out[k] = this.headerSet.has(k.toLowerCase()) || this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/** Redige query params sensíveis e credenciais embutidas (user:pass@). */
|
|
187
|
+
sanitizeUrl(raw, base) {
|
|
188
|
+
try {
|
|
189
|
+
const u = new URL(raw, base ?? (typeof location !== "undefined" ? location.href : void 0));
|
|
190
|
+
if (u.username || u.password) {
|
|
191
|
+
u.username = "";
|
|
192
|
+
u.password = "";
|
|
193
|
+
}
|
|
194
|
+
for (const key of Array.from(new Set(u.searchParams.keys()))) {
|
|
195
|
+
if (this.isSensitiveKey(key)) u.searchParams.set(key, REDACTED);
|
|
196
|
+
else u.searchParams.set(key, this.scrubString(u.searchParams.get(key) ?? ""));
|
|
197
|
+
}
|
|
198
|
+
if (u.hash && /(access_token|id_token|token|code)=/i.test(u.hash)) u.hash = `#${REDACTED}`;
|
|
199
|
+
return u.toString();
|
|
200
|
+
} catch {
|
|
201
|
+
return this.scrubString(raw);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Redige query/fragmento de um valor de atributo de URL preservando a forma original (relativa ou absoluta). */
|
|
205
|
+
redactUrlAttribute(value) {
|
|
206
|
+
if (typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
207
|
+
const hashAt = value.indexOf("#");
|
|
208
|
+
const hash = hashAt >= 0 ? value.slice(hashAt) : "";
|
|
209
|
+
const noHash = hashAt >= 0 ? value.slice(0, hashAt) : value;
|
|
210
|
+
const q = noHash.indexOf("?");
|
|
211
|
+
let base = q >= 0 ? noHash.slice(0, q) : noHash;
|
|
212
|
+
base = base.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, "$1");
|
|
213
|
+
let query = "";
|
|
214
|
+
if (q >= 0) {
|
|
215
|
+
const params = new URLSearchParams(noHash.slice(q + 1));
|
|
216
|
+
for (const k of Array.from(new Set(params.keys()))) {
|
|
217
|
+
if (this.isSensitiveKey(k)) params.set(k, REDACTED);
|
|
218
|
+
else params.set(k, this.scrubString(params.get(k) ?? ""));
|
|
219
|
+
}
|
|
220
|
+
query = `?${params.toString()}`;
|
|
221
|
+
}
|
|
222
|
+
return base + query + (hash && /(access_token|id_token|token|code)=/i.test(hash) ? `#${REDACTED}` : hash);
|
|
223
|
+
}
|
|
224
|
+
parseQuery(raw) {
|
|
225
|
+
try {
|
|
226
|
+
const u = new URL(raw, typeof location !== "undefined" ? location.href : void 0);
|
|
227
|
+
const out = {};
|
|
228
|
+
u.searchParams.forEach((v, k) => {
|
|
229
|
+
const val = this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
|
|
230
|
+
const prev = out[k];
|
|
231
|
+
out[k] = prev === void 0 ? val : Array.isArray(prev) ? [...prev, val] : [prev, val];
|
|
232
|
+
});
|
|
233
|
+
return Object.keys(out).length ? out : void 0;
|
|
234
|
+
} catch {
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Redação profunda de objetos (chaves sensíveis + padrões em strings). */
|
|
239
|
+
sanitizeValue(value, depth = 0) {
|
|
240
|
+
if (depth > 8) return "[Truncated]";
|
|
241
|
+
if (typeof value === "string") return this.scrubString(value);
|
|
242
|
+
if (value === null || typeof value !== "object") return value;
|
|
243
|
+
if (Array.isArray(value)) return value.slice(0, 200).map((v) => this.sanitizeValue(v, depth + 1));
|
|
244
|
+
const out = {};
|
|
245
|
+
for (const [k, v] of Object.entries(value)) {
|
|
246
|
+
out[k] = this.isSensitiveKey(k) ? REDACTED : this.sanitizeValue(v, depth + 1);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
/** Converte um corpo textual em CapturedBody sanitizado, respeitando o limite. */
|
|
251
|
+
sanitizeTextBody(text, contentType, size = text.length) {
|
|
252
|
+
const max = this.cfg.maxBodyBytes;
|
|
253
|
+
const truncated = text.length > max;
|
|
254
|
+
const slice = truncated ? text.slice(0, max) : text;
|
|
255
|
+
if (/json/i.test(contentType) && !truncated) {
|
|
256
|
+
try {
|
|
257
|
+
return { kind: "json", value: this.sanitizeValue(JSON.parse(slice)), size };
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (/x-www-form-urlencoded/i.test(contentType)) {
|
|
262
|
+
const params = new URLSearchParams(slice);
|
|
263
|
+
const form = {};
|
|
264
|
+
params.forEach((v, k) => form[k] = this.isSensitiveKey(k) ? REDACTED : this.scrubString(v));
|
|
265
|
+
return { kind: "form", value: form, size, truncated };
|
|
266
|
+
}
|
|
267
|
+
return { kind: "text", value: this.scrubString(slice), size, truncated };
|
|
268
|
+
}
|
|
269
|
+
isTextual(contentType) {
|
|
270
|
+
return this.cfg.textContentTypes.some((re) => re.test(contentType));
|
|
271
|
+
}
|
|
272
|
+
isIgnoredUrl(url) {
|
|
273
|
+
return this.cfg.ignoreUrls.some((p) => typeof p === "string" ? url.includes(p) : p.test(url));
|
|
274
|
+
}
|
|
275
|
+
/** Referência opaca para valores não autorizados. */
|
|
276
|
+
redactedRef(value, salt) {
|
|
277
|
+
let h = 2166136261;
|
|
278
|
+
const s = salt + value;
|
|
279
|
+
for (let i = 0; i < s.length; i++) {
|
|
280
|
+
h ^= s.charCodeAt(i);
|
|
281
|
+
h = Math.imul(h, 16777619);
|
|
282
|
+
}
|
|
283
|
+
return `ref_${(h >>> 0).toString(16).padStart(8, "0")}`;
|
|
284
|
+
}
|
|
285
|
+
/** Remove valores de atributos de seletor que podem conter dados sensíveis. */
|
|
286
|
+
sanitizeSelectorCandidates(list) {
|
|
287
|
+
return list.map((c) => ({ ...c, selector: this.scrubString(c.selector) }));
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/utils/errors.ts
|
|
292
|
+
var OffscriptError = class extends Error {
|
|
293
|
+
constructor(code, message, opts = {}) {
|
|
294
|
+
super(message);
|
|
295
|
+
this.name = "OffscriptError";
|
|
296
|
+
this.code = code;
|
|
297
|
+
this.status = opts.status;
|
|
298
|
+
this.cause = opts.cause;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var ConfigError = class extends OffscriptError {
|
|
302
|
+
constructor(message) {
|
|
303
|
+
super("CONFIG_INVALID", message);
|
|
304
|
+
this.name = "ConfigError";
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var AuthError = class extends OffscriptError {
|
|
308
|
+
constructor(message, status) {
|
|
309
|
+
super("AUTH_FAILED", message, { status });
|
|
310
|
+
this.name = "AuthError";
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var SessionExpiredError = class extends OffscriptError {
|
|
314
|
+
constructor(message = "Sess\xE3o expirada") {
|
|
315
|
+
super("SESSION_EXPIRED", message, { status: 410 });
|
|
316
|
+
this.name = "SessionExpiredError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var SessionDisabledError = class extends OffscriptError {
|
|
320
|
+
constructor(message = "Grava\xE7\xE3o desativada pelo backend") {
|
|
321
|
+
super("SESSION_DISABLED", message);
|
|
322
|
+
this.name = "SessionDisabledError";
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var InvalidStateError = class extends OffscriptError {
|
|
326
|
+
constructor(message) {
|
|
327
|
+
super("INVALID_STATE", message);
|
|
328
|
+
this.name = "InvalidStateError";
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var TransportError = class extends OffscriptError {
|
|
332
|
+
constructor(message, opts) {
|
|
333
|
+
super("TRANSPORT_FAILED", message, opts);
|
|
334
|
+
this.name = "TransportError";
|
|
335
|
+
this.retryable = opts.retryable;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var Emitter = class {
|
|
339
|
+
constructor() {
|
|
340
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
341
|
+
}
|
|
342
|
+
on(type, fn) {
|
|
343
|
+
let s = this.handlers.get(type);
|
|
344
|
+
if (!s) this.handlers.set(type, s = /* @__PURE__ */ new Set());
|
|
345
|
+
s.add(fn);
|
|
346
|
+
return () => this.off(type, fn);
|
|
347
|
+
}
|
|
348
|
+
off(type, fn) {
|
|
349
|
+
this.handlers.get(type)?.delete(fn);
|
|
350
|
+
}
|
|
351
|
+
emit(type, payload) {
|
|
352
|
+
for (const fn of Array.from(this.handlers.get(type) ?? [])) {
|
|
353
|
+
try {
|
|
354
|
+
fn(payload);
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
clear() {
|
|
360
|
+
this.handlers.clear();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// src/core/ApiClient.ts
|
|
365
|
+
var ORIGINAL_FETCH = "__offscriptOriginal";
|
|
366
|
+
function nativeFetch() {
|
|
367
|
+
const f = globalThis.fetch;
|
|
368
|
+
if (!f) throw new TransportError("fetch indispon\xEDvel", { retryable: false });
|
|
369
|
+
return (f[ORIGINAL_FETCH] ?? f).bind(globalThis);
|
|
370
|
+
}
|
|
371
|
+
var ApiClient = class {
|
|
372
|
+
constructor(apiUrl, getToken, timeoutMs) {
|
|
373
|
+
this.apiUrl = apiUrl;
|
|
374
|
+
this.getToken = getToken;
|
|
375
|
+
this.timeoutMs = timeoutMs;
|
|
376
|
+
}
|
|
377
|
+
get baseUrl() {
|
|
378
|
+
return this.apiUrl;
|
|
379
|
+
}
|
|
380
|
+
createSession(body) {
|
|
381
|
+
return this.json("POST", "/sdk/sessions", body);
|
|
382
|
+
}
|
|
383
|
+
heartbeat(sessionId, body) {
|
|
384
|
+
return this.json("POST", `/sdk/sessions/${encodeURIComponent(sessionId)}/heartbeat`, body);
|
|
385
|
+
}
|
|
386
|
+
complete(sessionId, body) {
|
|
387
|
+
return this.json("POST", `/sdk/sessions/${encodeURIComponent(sessionId)}/complete`, body);
|
|
388
|
+
}
|
|
389
|
+
status(sessionId) {
|
|
390
|
+
return this.json("GET", `/sdk/sessions/${encodeURIComponent(sessionId)}/status`);
|
|
391
|
+
}
|
|
392
|
+
async sendEvents(batch, opts = {}) {
|
|
393
|
+
const path = `/sdk/sessions/${encodeURIComponent(batch.sessionId)}/events`;
|
|
394
|
+
return this.json("POST", path, batch, { "Idempotency-Key": batch.batchId }, opts);
|
|
395
|
+
}
|
|
396
|
+
async json(method, path, body, extraHeaders = {}, opts = {}) {
|
|
397
|
+
const headers = {
|
|
398
|
+
Accept: "application/json",
|
|
399
|
+
Authorization: `Bearer ${this.getToken()}`,
|
|
400
|
+
"X-Offscript-Sdk": chunkB6GO5FE5_cjs.SDK_VERSION,
|
|
401
|
+
...extraHeaders
|
|
402
|
+
};
|
|
403
|
+
let payload;
|
|
404
|
+
if (body !== void 0) {
|
|
405
|
+
headers["Content-Type"] = "application/json";
|
|
406
|
+
const text = JSON.stringify(body);
|
|
407
|
+
payload = text;
|
|
408
|
+
if (opts.compress && !opts.keepalive && text.length > 1024) {
|
|
409
|
+
const gz = await gzip(text);
|
|
410
|
+
if (gz) {
|
|
411
|
+
payload = gz;
|
|
412
|
+
headers["Content-Encoding"] = "gzip";
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const ctrl = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
417
|
+
const timer = ctrl && !opts.keepalive ? setTimeout(() => ctrl.abort(), this.timeoutMs) : void 0;
|
|
418
|
+
let res;
|
|
419
|
+
try {
|
|
420
|
+
res = await nativeFetch()(this.apiUrl + path, {
|
|
421
|
+
method,
|
|
422
|
+
headers,
|
|
423
|
+
body: payload,
|
|
424
|
+
keepalive: opts.keepalive,
|
|
425
|
+
credentials: "omit",
|
|
426
|
+
mode: "cors",
|
|
427
|
+
signal: ctrl?.signal
|
|
428
|
+
});
|
|
429
|
+
} catch (e) {
|
|
430
|
+
throw new TransportError(`Falha de rede: ${e?.message ?? e}`, { retryable: true, cause: e });
|
|
431
|
+
} finally {
|
|
432
|
+
if (timer) clearTimeout(timer);
|
|
433
|
+
}
|
|
434
|
+
if (res.ok) {
|
|
435
|
+
const text = await res.text();
|
|
436
|
+
return text ? JSON.parse(text) : {};
|
|
437
|
+
}
|
|
438
|
+
const detail = await res.text().catch(() => "");
|
|
439
|
+
const msg = `HTTP ${res.status} ${method} ${path}${detail ? `: ${detail.slice(0, 200)}` : ""}`;
|
|
440
|
+
if (res.status === 401 || res.status === 403) throw new AuthError(msg, res.status);
|
|
441
|
+
if (res.status === 410) throw new SessionExpiredError(msg);
|
|
442
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
443
|
+
const err = new TransportError(msg, {
|
|
444
|
+
status: res.status,
|
|
445
|
+
retryable: res.status === 408 || res.status === 429 || res.status >= 500
|
|
446
|
+
});
|
|
447
|
+
if (retryAfter > 0) err.retryAfterMs = retryAfter * 1e3;
|
|
448
|
+
throw err;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
async function gzip(text) {
|
|
452
|
+
try {
|
|
453
|
+
const CS = globalThis.CompressionStream;
|
|
454
|
+
if (!CS) return null;
|
|
455
|
+
const body = new Response(text).body;
|
|
456
|
+
if (!body) return null;
|
|
457
|
+
return new Uint8Array(await new Response(body.pipeThrough(new CS("gzip"))).arrayBuffer());
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/transport/EventQueue.ts
|
|
464
|
+
var EventQueue = class {
|
|
465
|
+
constructor(maxEvents, maxBytes) {
|
|
466
|
+
this.maxEvents = maxEvents;
|
|
467
|
+
this.maxBytes = maxBytes;
|
|
468
|
+
this.staged = [];
|
|
469
|
+
this.pendingCount = 0;
|
|
470
|
+
this.pendingBytes = 0;
|
|
471
|
+
this.dropped = 0;
|
|
472
|
+
}
|
|
473
|
+
canAccept(priority) {
|
|
474
|
+
const factor = priority === "critical" ? 2 : priority === "normal" ? 1 : 0.5;
|
|
475
|
+
return this.pendingCount < this.maxEvents * factor && this.pendingBytes < this.maxBytes * factor;
|
|
476
|
+
}
|
|
477
|
+
noteDropped() {
|
|
478
|
+
this.dropped++;
|
|
479
|
+
}
|
|
480
|
+
push(e, size) {
|
|
481
|
+
this.staged.push(e);
|
|
482
|
+
this.pendingCount++;
|
|
483
|
+
this.pendingBytes += size;
|
|
484
|
+
}
|
|
485
|
+
/** Devolve eventos ao staging sem recontar. */
|
|
486
|
+
restore(events) {
|
|
487
|
+
this.staged = [...events, ...this.staged];
|
|
488
|
+
}
|
|
489
|
+
takeStaged() {
|
|
490
|
+
const s = this.staged;
|
|
491
|
+
this.staged = [];
|
|
492
|
+
return s;
|
|
493
|
+
}
|
|
494
|
+
peekStaged() {
|
|
495
|
+
return this.staged;
|
|
496
|
+
}
|
|
497
|
+
get stagedCount() {
|
|
498
|
+
return this.staged.length;
|
|
499
|
+
}
|
|
500
|
+
acked(count, bytes) {
|
|
501
|
+
this.pendingCount = Math.max(0, this.pendingCount - count);
|
|
502
|
+
this.pendingBytes = Math.max(0, this.pendingBytes - bytes);
|
|
503
|
+
}
|
|
504
|
+
reset(count, bytes) {
|
|
505
|
+
this.pendingCount = count;
|
|
506
|
+
this.pendingBytes = bytes;
|
|
507
|
+
}
|
|
508
|
+
get pending() {
|
|
509
|
+
return this.pendingCount;
|
|
510
|
+
}
|
|
511
|
+
get bytes() {
|
|
512
|
+
return this.pendingBytes;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/transport/RetryManager.ts
|
|
517
|
+
var RetryManager = class {
|
|
518
|
+
constructor(baseMs, maxMs, random = Math.random) {
|
|
519
|
+
this.baseMs = baseMs;
|
|
520
|
+
this.maxMs = maxMs;
|
|
521
|
+
this.random = random;
|
|
522
|
+
}
|
|
523
|
+
delay(attempt) {
|
|
524
|
+
const cap = Math.min(this.maxMs, this.baseMs * 2 ** Math.max(0, attempt));
|
|
525
|
+
return Math.round(this.random() * cap);
|
|
526
|
+
}
|
|
527
|
+
sleep(ms) {
|
|
528
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// src/transport/EventTransport.ts
|
|
533
|
+
var EventTransport = class extends Emitter {
|
|
534
|
+
constructor(cfg, api, buffer) {
|
|
535
|
+
super();
|
|
536
|
+
this.cfg = cfg;
|
|
537
|
+
this.api = api;
|
|
538
|
+
this.buffer = buffer;
|
|
539
|
+
this.identity = null;
|
|
540
|
+
this.running = null;
|
|
541
|
+
this.again = false;
|
|
542
|
+
this.failures = 0;
|
|
543
|
+
this.stopped = true;
|
|
544
|
+
this.fatal = false;
|
|
545
|
+
this.online = true;
|
|
546
|
+
this.lastDroppedReported = 0;
|
|
547
|
+
this.queue = new EventQueue(cfg.maxMemoryEvents, cfg.maxBufferBytes);
|
|
548
|
+
this.retry = new RetryManager(cfg.retryBaseMs, cfg.retryMaxMs);
|
|
549
|
+
}
|
|
550
|
+
get bufferKind() {
|
|
551
|
+
return this.buffer.kind;
|
|
552
|
+
}
|
|
553
|
+
/** Inicia o transporte para uma sessão/aba e recupera o que ficou pendente no buffer. */
|
|
554
|
+
async start(identity) {
|
|
555
|
+
this.identity = identity;
|
|
556
|
+
this.stopped = false;
|
|
557
|
+
this.fatal = false;
|
|
558
|
+
this.failures = 0;
|
|
559
|
+
const st = await this.buffer.stats(identity.sessionId).catch(() => ({ count: 0, bytes: 0 }));
|
|
560
|
+
this.queue.reset(st.count, st.bytes);
|
|
561
|
+
this.scheduleFlush();
|
|
562
|
+
}
|
|
563
|
+
setOnline(online) {
|
|
564
|
+
this.online = online;
|
|
565
|
+
if (online && !this.stopped) void this.flush();
|
|
566
|
+
}
|
|
567
|
+
canAccept(priority) {
|
|
568
|
+
return !this.stopped && !this.fatal && this.queue.canAccept(priority);
|
|
569
|
+
}
|
|
570
|
+
noteDropped() {
|
|
571
|
+
this.queue.noteDropped();
|
|
572
|
+
if (this.queue.dropped - this.lastDroppedReported >= 1) {
|
|
573
|
+
this.lastDroppedReported = this.queue.dropped;
|
|
574
|
+
this.emit("overflow", { dropped: this.queue.dropped });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
get pending() {
|
|
578
|
+
return this.queue.pending;
|
|
579
|
+
}
|
|
580
|
+
enqueue(event) {
|
|
581
|
+
if (this.stopped) return;
|
|
582
|
+
this.queue.push(event, approxSize(event));
|
|
583
|
+
if (this.queue.stagedCount >= 50) void this.commit();
|
|
584
|
+
else if (!this.commitTimer) this.commitTimer = setTimeout(() => void this.commit(), 100);
|
|
585
|
+
}
|
|
586
|
+
async commit() {
|
|
587
|
+
if (this.commitTimer) clearTimeout(this.commitTimer);
|
|
588
|
+
this.commitTimer = void 0;
|
|
589
|
+
const staged = this.queue.takeStaged();
|
|
590
|
+
if (!staged.length) return;
|
|
591
|
+
try {
|
|
592
|
+
await this.buffer.put(staged);
|
|
593
|
+
} catch {
|
|
594
|
+
this.queue.restore(staged);
|
|
595
|
+
throw new TransportError("Falha ao persistir eventos localmente", { retryable: true });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
scheduleFlush() {
|
|
599
|
+
if (this.flushTimer || this.stopped) return;
|
|
600
|
+
this.flushTimer = setTimeout(() => {
|
|
601
|
+
this.flushTimer = void 0;
|
|
602
|
+
void this.flush().finally(() => this.scheduleFlush());
|
|
603
|
+
}, this.cfg.flushIntervalMs);
|
|
604
|
+
}
|
|
605
|
+
/** Flush de melhor esforço. Retorna true se tudo foi confirmado pelo backend. */
|
|
606
|
+
flush(opts = { inlineRetries: 0 }) {
|
|
607
|
+
if (this.running) {
|
|
608
|
+
this.again = true;
|
|
609
|
+
return this.running;
|
|
610
|
+
}
|
|
611
|
+
this.running = (async () => {
|
|
612
|
+
try {
|
|
613
|
+
let ok = await this.drain(opts);
|
|
614
|
+
while (this.again && ok) {
|
|
615
|
+
this.again = false;
|
|
616
|
+
ok = await this.drain(opts);
|
|
617
|
+
}
|
|
618
|
+
return ok;
|
|
619
|
+
} finally {
|
|
620
|
+
this.running = null;
|
|
621
|
+
this.again = false;
|
|
622
|
+
}
|
|
623
|
+
})();
|
|
624
|
+
return this.running;
|
|
625
|
+
}
|
|
626
|
+
/** Flush final com retries inline e prazo máximo. */
|
|
627
|
+
async flushFinal(maxWaitMs = 1e4) {
|
|
628
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
629
|
+
this.retryTimer = void 0;
|
|
630
|
+
const running = this.running;
|
|
631
|
+
if (running) await running.catch(() => false);
|
|
632
|
+
const ok = await this.flush({ inlineRetries: this.cfg.maxRetries, deadline: Date.now() + maxWaitMs });
|
|
633
|
+
return { ok, pending: this.queue.pending };
|
|
634
|
+
}
|
|
635
|
+
async drain(opts) {
|
|
636
|
+
const id = this.identity;
|
|
637
|
+
if (!id || this.fatal) return false;
|
|
638
|
+
for (; ; ) {
|
|
639
|
+
try {
|
|
640
|
+
await this.commit();
|
|
641
|
+
const { items } = await this.buffer.peek(id.sessionId, id.tabId, this.cfg.batchMaxEvents, this.cfg.batchMaxBytes);
|
|
642
|
+
if (!items.length) {
|
|
643
|
+
this.failures = 0;
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
const events = items.map((i) => i.event);
|
|
647
|
+
await this.sendWithRetry(events, opts);
|
|
648
|
+
await this.buffer.remove(items.map((i) => i.key));
|
|
649
|
+
this.queue.acked(items.length, items.reduce((n, i) => n + i.size, 0));
|
|
650
|
+
this.failures = 0;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return this.onFailure(error);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
onFailure(error) {
|
|
657
|
+
if (error instanceof AuthError || error instanceof SessionExpiredError) {
|
|
658
|
+
this.fatal = true;
|
|
659
|
+
this.emit("fatal", { error });
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
this.failures++;
|
|
663
|
+
const retryable = !(error instanceof TransportError) || error.retryable;
|
|
664
|
+
const willRetryInMs = retryable ? this.retry.delay(this.failures) : void 0;
|
|
665
|
+
this.emit("failed", { error, willRetryInMs });
|
|
666
|
+
if (retryable && !this.stopped) {
|
|
667
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
668
|
+
this.retryTimer = setTimeout(() => {
|
|
669
|
+
this.retryTimer = void 0;
|
|
670
|
+
void this.flush();
|
|
671
|
+
}, willRetryInMs);
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
async sendWithRetry(events, opts) {
|
|
676
|
+
const batch = this.buildBatch(events);
|
|
677
|
+
for (let attempt = 0; ; attempt++) {
|
|
678
|
+
try {
|
|
679
|
+
if (!this.online) throw new TransportError("Offline", { retryable: true });
|
|
680
|
+
const ack = await this.api.sendEvents(batch, { compress: this.cfg.compress });
|
|
681
|
+
this.emit("flushed", { events: events.length, batchId: batch.batchId, duplicate: !!ack.duplicate });
|
|
682
|
+
return;
|
|
683
|
+
} catch (e) {
|
|
684
|
+
if (e instanceof TransportError && e.status === 413 && events.length > 1) {
|
|
685
|
+
const mid = Math.ceil(events.length / 2);
|
|
686
|
+
await this.sendWithRetry(events.slice(0, mid), opts);
|
|
687
|
+
await this.sendWithRetry(events.slice(mid), opts);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const retryable = !(e instanceof TransportError) || e.retryable;
|
|
691
|
+
if (!retryable || attempt >= opts.inlineRetries) throw e;
|
|
692
|
+
const wait = Math.max(this.retry.delay(attempt), e.retryAfterMs ?? 0);
|
|
693
|
+
if (opts.deadline && Date.now() + wait > opts.deadline) throw e;
|
|
694
|
+
await this.retry.sleep(wait);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
buildBatch(events) {
|
|
699
|
+
const id = this.identity;
|
|
700
|
+
const first = events[0].sequence;
|
|
701
|
+
const last = events[events.length - 1].sequence;
|
|
702
|
+
return {
|
|
703
|
+
schemaVersion: chunkB6GO5FE5_cjs.SCHEMA_VERSION,
|
|
704
|
+
batchId: `${id.sessionId}.${id.tabId}.${first}-${last}.${events.length}`,
|
|
705
|
+
sessionId: id.sessionId,
|
|
706
|
+
executionId: id.executionId,
|
|
707
|
+
tabId: id.tabId,
|
|
708
|
+
sequenceStart: first,
|
|
709
|
+
sequenceEnd: last,
|
|
710
|
+
createdAt: Date.now(),
|
|
711
|
+
sdkVersion: chunkB6GO5FE5_cjs.SDK_VERSION,
|
|
712
|
+
events
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Chamado em pagehide/beforeunload. Persiste o que está em staging (best effort, assíncrono) e
|
|
717
|
+
* tenta enviar o final da fila com fetch keepalive (limite ~64KB). Sem ACK: os eventos continuam
|
|
718
|
+
* no IndexedDB e serão reenviados (de forma idempotente) na próxima página.
|
|
719
|
+
* Não há garantia de entrega se o navegador for encerrado abruptamente.
|
|
720
|
+
*/
|
|
721
|
+
flushOnUnload() {
|
|
722
|
+
const id = this.identity;
|
|
723
|
+
if (!id || this.stopped || this.fatal) return;
|
|
724
|
+
const staged = this.queue.takeStaged();
|
|
725
|
+
if (!staged.length) return;
|
|
726
|
+
safe(() => void this.buffer.put(staged).catch(() => void 0));
|
|
727
|
+
const events = [];
|
|
728
|
+
let bytes = 0;
|
|
729
|
+
for (const e of staged) {
|
|
730
|
+
const s = approxSize(e);
|
|
731
|
+
if (bytes + s > this.cfg.keepaliveMaxBytes) break;
|
|
732
|
+
events.push(e);
|
|
733
|
+
bytes += s;
|
|
734
|
+
}
|
|
735
|
+
if (!events.length) return;
|
|
736
|
+
safe(() => void this.api.sendEvents(this.buildBatch(events), { keepalive: true }).catch(() => void 0));
|
|
737
|
+
}
|
|
738
|
+
/** Para timers; eventos não confirmados permanecem no buffer. */
|
|
739
|
+
stop() {
|
|
740
|
+
this.stopped = true;
|
|
741
|
+
for (const t of [this.commitTimer, this.flushTimer, this.retryTimer]) if (t) clearTimeout(t);
|
|
742
|
+
this.commitTimer = this.flushTimer = this.retryTimer = void 0;
|
|
743
|
+
}
|
|
744
|
+
async clearSession(sessionId) {
|
|
745
|
+
await this.buffer.clearSession(sessionId).catch(() => void 0);
|
|
746
|
+
this.queue.reset(0, 0);
|
|
747
|
+
}
|
|
748
|
+
async purgeExpired(retentionMs) {
|
|
749
|
+
await this.buffer.purgeOlderThan(Date.now() - retentionMs).catch(() => void 0);
|
|
750
|
+
}
|
|
751
|
+
close() {
|
|
752
|
+
this.stop();
|
|
753
|
+
this.buffer.close();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// src/transport/PersistentBuffer.ts
|
|
758
|
+
var eventKey = (sessionId, tabId, seq) => `${sessionId}|${tabId}|${String(seq).padStart(12, "0")}`;
|
|
759
|
+
var toStored = (e) => ({
|
|
760
|
+
key: eventKey(e.sessionId, e.tabId, e.sequence),
|
|
761
|
+
sessionId: e.sessionId,
|
|
762
|
+
tabId: e.tabId,
|
|
763
|
+
sequence: e.sequence,
|
|
764
|
+
storedAt: Date.now(),
|
|
765
|
+
size: approxSize(e),
|
|
766
|
+
event: e
|
|
767
|
+
});
|
|
768
|
+
var MemoryBuffer = class {
|
|
769
|
+
constructor() {
|
|
770
|
+
this.kind = "memory";
|
|
771
|
+
this.map = /* @__PURE__ */ new Map();
|
|
772
|
+
}
|
|
773
|
+
async put(events) {
|
|
774
|
+
for (const e of events) {
|
|
775
|
+
const s = toStored(e);
|
|
776
|
+
this.map.set(s.key, s);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async peek(sessionId, tabId, maxEvents, maxBytes) {
|
|
780
|
+
const prefix = `${sessionId}|${tabId}|`;
|
|
781
|
+
const keys = Array.from(this.map.keys()).filter((k) => k.startsWith(prefix)).sort();
|
|
782
|
+
const items = [];
|
|
783
|
+
let bytes = 0;
|
|
784
|
+
for (const k of keys) {
|
|
785
|
+
const s = this.map.get(k);
|
|
786
|
+
if (items.length >= maxEvents || items.length > 0 && bytes + s.size > maxBytes) break;
|
|
787
|
+
items.push(s);
|
|
788
|
+
bytes += s.size;
|
|
789
|
+
}
|
|
790
|
+
return { items, bytes };
|
|
791
|
+
}
|
|
792
|
+
async remove(keys) {
|
|
793
|
+
for (const k of keys) this.map.delete(k);
|
|
794
|
+
}
|
|
795
|
+
async stats(sessionId) {
|
|
796
|
+
let count = 0;
|
|
797
|
+
let bytes = 0;
|
|
798
|
+
for (const s of this.map.values()) {
|
|
799
|
+
if (sessionId && s.sessionId !== sessionId) continue;
|
|
800
|
+
count++;
|
|
801
|
+
bytes += s.size;
|
|
802
|
+
}
|
|
803
|
+
return { count, bytes };
|
|
804
|
+
}
|
|
805
|
+
async purgeOlderThan(ts) {
|
|
806
|
+
let n = 0;
|
|
807
|
+
for (const [k, s] of this.map) if (s.storedAt < ts) this.map.delete(k), n++;
|
|
808
|
+
return n;
|
|
809
|
+
}
|
|
810
|
+
async clearSession(sessionId) {
|
|
811
|
+
for (const [k, s] of this.map) if (s.sessionId === sessionId) this.map.delete(k);
|
|
812
|
+
}
|
|
813
|
+
close() {
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
var STORE = "events";
|
|
817
|
+
function req(r) {
|
|
818
|
+
return new Promise((resolve, reject) => {
|
|
819
|
+
r.onsuccess = () => resolve(r.result);
|
|
820
|
+
r.onerror = () => reject(r.error);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
function done(tx) {
|
|
824
|
+
return new Promise((resolve, reject) => {
|
|
825
|
+
tx.oncomplete = () => resolve();
|
|
826
|
+
tx.onerror = () => reject(tx.error);
|
|
827
|
+
tx.onabort = () => reject(tx.error);
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
var IndexedDBBuffer = class _IndexedDBBuffer {
|
|
831
|
+
constructor(db) {
|
|
832
|
+
this.db = db;
|
|
833
|
+
this.kind = "indexeddb";
|
|
834
|
+
}
|
|
835
|
+
static async open(name) {
|
|
836
|
+
if (typeof indexedDB === "undefined") throw new Error("IndexedDB indispon\xEDvel");
|
|
837
|
+
const db = await new Promise((resolve, reject) => {
|
|
838
|
+
const r = indexedDB.open(name, 1);
|
|
839
|
+
r.onupgradeneeded = () => {
|
|
840
|
+
const store = r.result.createObjectStore(STORE, { keyPath: "key" });
|
|
841
|
+
store.createIndex("bySession", "sessionId");
|
|
842
|
+
store.createIndex("byStoredAt", "storedAt");
|
|
843
|
+
};
|
|
844
|
+
r.onsuccess = () => resolve(r.result);
|
|
845
|
+
r.onerror = () => reject(r.error);
|
|
846
|
+
r.onblocked = () => reject(new Error("IndexedDB bloqueado"));
|
|
847
|
+
});
|
|
848
|
+
return new _IndexedDBBuffer(db);
|
|
849
|
+
}
|
|
850
|
+
async put(events) {
|
|
851
|
+
if (!events.length) return;
|
|
852
|
+
const tx = this.db.transaction(STORE, "readwrite");
|
|
853
|
+
const store = tx.objectStore(STORE);
|
|
854
|
+
for (const e of events) store.put(toStored(e));
|
|
855
|
+
await done(tx);
|
|
856
|
+
}
|
|
857
|
+
async peek(sessionId, tabId, maxEvents, maxBytes) {
|
|
858
|
+
const prefix = `${sessionId}|${tabId}|`;
|
|
859
|
+
const range = IDBKeyRange.bound(prefix, prefix + "\uFFFF");
|
|
860
|
+
const tx = this.db.transaction(STORE, "readonly");
|
|
861
|
+
const items = [];
|
|
862
|
+
let bytes = 0;
|
|
863
|
+
await new Promise((resolve, reject) => {
|
|
864
|
+
const cur = tx.objectStore(STORE).openCursor(range);
|
|
865
|
+
cur.onsuccess = () => {
|
|
866
|
+
const c = cur.result;
|
|
867
|
+
if (!c) return resolve();
|
|
868
|
+
const s = c.value;
|
|
869
|
+
if (items.length >= maxEvents || items.length > 0 && bytes + s.size > maxBytes) return resolve();
|
|
870
|
+
items.push(s);
|
|
871
|
+
bytes += s.size;
|
|
872
|
+
c.continue();
|
|
873
|
+
};
|
|
874
|
+
cur.onerror = () => reject(cur.error);
|
|
875
|
+
});
|
|
876
|
+
return { items, bytes };
|
|
877
|
+
}
|
|
878
|
+
async remove(keys) {
|
|
879
|
+
if (!keys.length) return;
|
|
880
|
+
const tx = this.db.transaction(STORE, "readwrite");
|
|
881
|
+
const store = tx.objectStore(STORE);
|
|
882
|
+
for (const k of keys) store.delete(k);
|
|
883
|
+
await done(tx);
|
|
884
|
+
}
|
|
885
|
+
async stats(sessionId) {
|
|
886
|
+
const tx = this.db.transaction(STORE, "readonly");
|
|
887
|
+
const store = tx.objectStore(STORE);
|
|
888
|
+
if (!sessionId) return { count: await req(store.count()), bytes: 0 };
|
|
889
|
+
let count = 0;
|
|
890
|
+
let bytes = 0;
|
|
891
|
+
await new Promise((resolve, reject) => {
|
|
892
|
+
const cur = store.index("bySession").openCursor(IDBKeyRange.only(sessionId));
|
|
893
|
+
cur.onsuccess = () => {
|
|
894
|
+
const c = cur.result;
|
|
895
|
+
if (!c) return resolve();
|
|
896
|
+
count++;
|
|
897
|
+
bytes += c.value.size;
|
|
898
|
+
c.continue();
|
|
899
|
+
};
|
|
900
|
+
cur.onerror = () => reject(cur.error);
|
|
901
|
+
});
|
|
902
|
+
return { count, bytes };
|
|
903
|
+
}
|
|
904
|
+
async purgeOlderThan(ts) {
|
|
905
|
+
const tx = this.db.transaction(STORE, "readwrite");
|
|
906
|
+
let n = 0;
|
|
907
|
+
await new Promise((resolve, reject) => {
|
|
908
|
+
const cur = tx.objectStore(STORE).index("byStoredAt").openCursor(IDBKeyRange.upperBound(ts, true));
|
|
909
|
+
cur.onsuccess = () => {
|
|
910
|
+
const c = cur.result;
|
|
911
|
+
if (!c) return resolve();
|
|
912
|
+
c.delete();
|
|
913
|
+
n++;
|
|
914
|
+
c.continue();
|
|
915
|
+
};
|
|
916
|
+
cur.onerror = () => reject(cur.error);
|
|
917
|
+
});
|
|
918
|
+
await done(tx);
|
|
919
|
+
return n;
|
|
920
|
+
}
|
|
921
|
+
async clearSession(sessionId) {
|
|
922
|
+
const tx = this.db.transaction(STORE, "readwrite");
|
|
923
|
+
await new Promise((resolve, reject) => {
|
|
924
|
+
const cur = tx.objectStore(STORE).index("bySession").openCursor(IDBKeyRange.only(sessionId));
|
|
925
|
+
cur.onsuccess = () => {
|
|
926
|
+
const c = cur.result;
|
|
927
|
+
if (!c) return resolve();
|
|
928
|
+
c.delete();
|
|
929
|
+
c.continue();
|
|
930
|
+
};
|
|
931
|
+
cur.onerror = () => reject(cur.error);
|
|
932
|
+
});
|
|
933
|
+
await done(tx);
|
|
934
|
+
}
|
|
935
|
+
close() {
|
|
936
|
+
safe(() => this.db.close());
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
async function openBuffer(name, persist) {
|
|
940
|
+
if (persist) {
|
|
941
|
+
try {
|
|
942
|
+
return await IndexedDBBuffer.open(name);
|
|
943
|
+
} catch {
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return new MemoryBuffer();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/recorders/types.ts
|
|
950
|
+
function guard(sink, name, fn) {
|
|
951
|
+
try {
|
|
952
|
+
fn();
|
|
953
|
+
} catch (e) {
|
|
954
|
+
try {
|
|
955
|
+
sink?.emit("lifecycle", { type: "recorder-error", detail: { recorder: name, message: String(e?.message ?? e) } }, { priority: "low" });
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/recorders/ActionRecorder.ts
|
|
962
|
+
var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
963
|
+
var INTERACTIVE = "a,button,input,select,textarea,summary,[role=button],[role=link],[role=menuitem],[role=tab],[role=option],[data-testid],[data-test],[data-cy]";
|
|
964
|
+
var INPUT_IDLE_MS = 700;
|
|
965
|
+
var DRAG_MIN_PX = 8;
|
|
966
|
+
var ActionRecorder = class {
|
|
967
|
+
constructor(sink) {
|
|
968
|
+
this.sink = sink;
|
|
969
|
+
this.name = "actions";
|
|
970
|
+
this.d = new Disposer();
|
|
971
|
+
this.paused = false;
|
|
972
|
+
this.salt = Math.random().toString(36).slice(2);
|
|
973
|
+
this.scrollState = /* @__PURE__ */ new Map();
|
|
974
|
+
this.move = [];
|
|
975
|
+
const r = sink.config.recording;
|
|
976
|
+
this.gen = new chunkB6GO5FE5_cjs.SelectorGenerator({ testIdAttributes: r.testIdAttributes, stableAttributes: r.stableAttributes });
|
|
977
|
+
}
|
|
978
|
+
start() {
|
|
979
|
+
const d = this.d;
|
|
980
|
+
const cap = { capture: true, passive: true };
|
|
981
|
+
const on = (type, fn, target = document) => d.listen(target, type, (e) => guard(this.sink, this.name, () => !this.paused && fn(e)), cap);
|
|
982
|
+
on("click", (e) => this.onClick(e));
|
|
983
|
+
on("dblclick", (e) => this.onDblClick(e));
|
|
984
|
+
on("input", (e) => this.onInput(e));
|
|
985
|
+
on("change", (e) => this.onChange(e));
|
|
986
|
+
on("keydown", (e) => this.onKey(e));
|
|
987
|
+
on("focusin", (e) => this.onFocus("focus", e));
|
|
988
|
+
on("focusout", (e) => this.onFocus("blur", e));
|
|
989
|
+
on("scroll", (e) => this.onScroll(e));
|
|
990
|
+
on("mouseover", (e) => this.onOver(e));
|
|
991
|
+
on("mouseout", (e) => this.onOut(e));
|
|
992
|
+
on("mousedown", (e) => this.onDown(e));
|
|
993
|
+
on("mouseup", (e) => this.onUp(e));
|
|
994
|
+
on("mousemove", (e) => this.onMove(e));
|
|
995
|
+
on("dragstart", () => this.drag && (this.drag.html5 = true));
|
|
996
|
+
this.moveFlush = debounce(() => this.flushMove(), 500);
|
|
997
|
+
d.add(() => this.moveFlush?.cancel());
|
|
998
|
+
}
|
|
999
|
+
pause() {
|
|
1000
|
+
this.flushAll();
|
|
1001
|
+
this.paused = true;
|
|
1002
|
+
}
|
|
1003
|
+
resume() {
|
|
1004
|
+
this.paused = false;
|
|
1005
|
+
}
|
|
1006
|
+
stop() {
|
|
1007
|
+
guard(this.sink, this.name, () => this.flushAll());
|
|
1008
|
+
this.d.dispose();
|
|
1009
|
+
for (const s of this.scrollState.values()) s.flush.cancel();
|
|
1010
|
+
this.scrollState.clear();
|
|
1011
|
+
if (this.hover?.timer) clearTimeout(this.hover.timer);
|
|
1012
|
+
}
|
|
1013
|
+
/** Descarrega estados pendentes (input em edição, scroll, trajetória do mouse). */
|
|
1014
|
+
flushAll() {
|
|
1015
|
+
this.flushInput();
|
|
1016
|
+
for (const s of this.scrollState.values()) s.flush.flush();
|
|
1017
|
+
this.flushMove();
|
|
1018
|
+
}
|
|
1019
|
+
// ---- API usada pelo núcleo ----------------------------------------------
|
|
1020
|
+
recordNavigation(from, to, kind) {
|
|
1021
|
+
guard(this.sink, this.name, () => {
|
|
1022
|
+
if (this.paused) return;
|
|
1023
|
+
this.flushAll();
|
|
1024
|
+
const s = this.sink.sanitizer;
|
|
1025
|
+
this.emit({ type: "navigate", payload: { from: s.sanitizeUrl(from), to: s.sanitizeUrl(to), kind } });
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
recordPageLoad(navType, referrer) {
|
|
1029
|
+
guard(this.sink, this.name, () => {
|
|
1030
|
+
const s = this.sink.sanitizer;
|
|
1031
|
+
if (navType === "reload") this.emit({ type: "reload", payload: { url: s.sanitizeUrl(location.href) } });
|
|
1032
|
+
else if (navType === "back_forward" || navType === "navigate")
|
|
1033
|
+
this.emit({ type: "navigate", payload: { from: referrer ? s.sanitizeUrl(referrer) : void 0, to: s.sanitizeUrl(location.href), kind: navType === "back_forward" ? "history" : "document" } });
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
// ---- helpers --------------------------------------------------------------
|
|
1037
|
+
emit(draft, el, extra) {
|
|
1038
|
+
const s = this.sink.sanitizer;
|
|
1039
|
+
const data = {
|
|
1040
|
+
schemaVersion: chunkB6GO5FE5_cjs.ACTION_SCHEMA_VERSION,
|
|
1041
|
+
type: draft.type,
|
|
1042
|
+
url: s.sanitizeUrl(location.href),
|
|
1043
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
1044
|
+
payload: draft.payload,
|
|
1045
|
+
metadata: draft.metadata
|
|
1046
|
+
};
|
|
1047
|
+
if (extra?.x !== void 0 && extra.y !== void 0) data.coordinates = { x: Math.round(extra.x), y: Math.round(extra.y) };
|
|
1048
|
+
if (el) {
|
|
1049
|
+
const r = this.gen.generate(el);
|
|
1050
|
+
data.selector = s.scrubString(r.selector);
|
|
1051
|
+
data.selectorUnique = r.unique;
|
|
1052
|
+
data.selectorCandidates = s.sanitizeSelectorCandidates(r.candidates);
|
|
1053
|
+
data.element = this.gen.describe(el);
|
|
1054
|
+
if (data.element.text) data.element.text = s.scrubString(data.element.text);
|
|
1055
|
+
if (data.element.name) data.element.name = s.scrubString(data.element.name);
|
|
1056
|
+
}
|
|
1057
|
+
this.sink.emit("action", data, { priority: "normal" });
|
|
1058
|
+
}
|
|
1059
|
+
target(e) {
|
|
1060
|
+
const t = e.target;
|
|
1061
|
+
const el = t instanceof Element ? t : t instanceof Node ? t.parentElement : null;
|
|
1062
|
+
if (!el) return null;
|
|
1063
|
+
const { ignoreSelectors, blockSelectors } = this.sink.config.privacy;
|
|
1064
|
+
const sel = [...ignoreSelectors, ...blockSelectors].join(",");
|
|
1065
|
+
if (sel && safe(() => el.closest(sel))) return null;
|
|
1066
|
+
return el;
|
|
1067
|
+
}
|
|
1068
|
+
// ---- handlers -------------------------------------------------------------
|
|
1069
|
+
onClick(e) {
|
|
1070
|
+
const el = this.target(e);
|
|
1071
|
+
if (!el) return;
|
|
1072
|
+
this.flushInput();
|
|
1073
|
+
const last = this.lastClick;
|
|
1074
|
+
if (last && el instanceof HTMLInputElement && last.el.control === el && e.timeStamp - last.t < 100) return;
|
|
1075
|
+
this.lastClick = { el, t: e.timeStamp };
|
|
1076
|
+
const clickable = el.closest(INTERACTIVE) ?? el;
|
|
1077
|
+
this.emit({ type: "click", payload: { button: e.button, detail: e.detail, modifiers: mods(e), trusted: e.isTrusted } }, clickable, { x: e.clientX, y: e.clientY });
|
|
1078
|
+
}
|
|
1079
|
+
onDblClick(e) {
|
|
1080
|
+
const el = this.target(e);
|
|
1081
|
+
if (!el) return;
|
|
1082
|
+
const interval = this.lastClick ? Math.round(e.timeStamp - this.lastClick.t) : void 0;
|
|
1083
|
+
this.emit({ type: "dblclick", payload: { intervalMs: interval, modifiers: mods(e) } }, el.closest(INTERACTIVE) ?? el, { x: e.clientX, y: e.clientY });
|
|
1084
|
+
}
|
|
1085
|
+
onInput(e) {
|
|
1086
|
+
const el = this.target(e);
|
|
1087
|
+
if (!el || !(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return;
|
|
1088
|
+
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file", "range", "color"].includes(el.type)) return;
|
|
1089
|
+
if (this.pendingInput && this.pendingInput.el !== el) this.flushInput();
|
|
1090
|
+
this.pendingInput ?? (this.pendingInput = { el, inputType: e.inputType ?? "input", startedAt: Date.now() });
|
|
1091
|
+
if (this.inputTimer) clearTimeout(this.inputTimer);
|
|
1092
|
+
this.inputTimer = setTimeout(() => guard(this.sink, this.name, () => this.flushInput()), INPUT_IDLE_MS);
|
|
1093
|
+
}
|
|
1094
|
+
flushInput() {
|
|
1095
|
+
if (this.inputTimer) clearTimeout(this.inputTimer);
|
|
1096
|
+
this.inputTimer = void 0;
|
|
1097
|
+
const p = this.pendingInput;
|
|
1098
|
+
this.pendingInput = void 0;
|
|
1099
|
+
if (!p || !p.el.isConnected) return;
|
|
1100
|
+
const el = p.el;
|
|
1101
|
+
const value = el.value ?? "";
|
|
1102
|
+
const payload = { inputType: p.inputType, length: value.length, typingMs: Date.now() - p.startedAt, fieldType: el instanceof HTMLInputElement ? el.type : "textarea" };
|
|
1103
|
+
const policy = this.sink.config.privacy;
|
|
1104
|
+
const allowed = policy.allowedInputSelectors.join(",");
|
|
1105
|
+
const isPassword = el instanceof HTMLInputElement && el.type === "password";
|
|
1106
|
+
if (!isPassword && allowed && safe(() => el.matches(allowed))) {
|
|
1107
|
+
payload.value = this.sink.sanitizer.scrubString(value);
|
|
1108
|
+
payload.valueCaptured = true;
|
|
1109
|
+
} else {
|
|
1110
|
+
payload.valueCaptured = false;
|
|
1111
|
+
payload.valueRef = this.sink.sanitizer.redactedRef(value, this.salt);
|
|
1112
|
+
}
|
|
1113
|
+
this.emit({ type: "input", payload }, el);
|
|
1114
|
+
}
|
|
1115
|
+
onChange(e) {
|
|
1116
|
+
const el = this.target(e);
|
|
1117
|
+
if (!el) return;
|
|
1118
|
+
if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) {
|
|
1119
|
+
this.flushInput();
|
|
1120
|
+
const capture = this.sink.config.privacy.captureChoiceValues;
|
|
1121
|
+
this.emit({ type: "check", payload: { checked: el.checked, inputType: el.type, value: capture ? el.value : void 0 } }, el);
|
|
1122
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
1123
|
+
this.flushInput();
|
|
1124
|
+
const capture = this.sink.config.privacy.captureChoiceValues;
|
|
1125
|
+
const opts = Array.from(el.selectedOptions);
|
|
1126
|
+
this.emit(
|
|
1127
|
+
{ type: "select", payload: capture ? { values: opts.map((o) => o.value), labels: opts.map((o) => o.label), multiple: el.multiple } : { count: opts.length, multiple: el.multiple } },
|
|
1128
|
+
el
|
|
1129
|
+
);
|
|
1130
|
+
} else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1131
|
+
this.flushInput();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
onKey(e) {
|
|
1135
|
+
const el = this.target(e);
|
|
1136
|
+
if (!el) return;
|
|
1137
|
+
const modified = e.ctrlKey || e.metaKey || e.altKey;
|
|
1138
|
+
const relevant = KEYS.has(e.key) || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
|
|
1139
|
+
if (!relevant) return;
|
|
1140
|
+
if (e.key === "Enter" || e.key === "Tab") this.flushInput();
|
|
1141
|
+
this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers: mods(e), repeat: e.repeat } }, el);
|
|
1142
|
+
}
|
|
1143
|
+
onFocus(type, e) {
|
|
1144
|
+
const el = this.target(e);
|
|
1145
|
+
if (!el || el === document.body || el === document.documentElement) return;
|
|
1146
|
+
if (type === "blur") this.flushInput();
|
|
1147
|
+
this.emit({ type }, el);
|
|
1148
|
+
}
|
|
1149
|
+
onScroll(e) {
|
|
1150
|
+
const t = e.target;
|
|
1151
|
+
const isDoc = t === document || t === document.documentElement || t === document.body;
|
|
1152
|
+
const el = isDoc ? null : t instanceof Element ? t : null;
|
|
1153
|
+
if (!isDoc && (!el || this.target(e) === null)) return;
|
|
1154
|
+
const pos = () => isDoc ? { x: Math.round(window.scrollX), y: Math.round(window.scrollY) } : { x: Math.round(el.scrollLeft), y: Math.round(el.scrollTop) };
|
|
1155
|
+
const key = isDoc ? document : t;
|
|
1156
|
+
let st = this.scrollState.get(key);
|
|
1157
|
+
if (!st) {
|
|
1158
|
+
const start = pos();
|
|
1159
|
+
st = {
|
|
1160
|
+
...start,
|
|
1161
|
+
flush: debounce(() => {
|
|
1162
|
+
const s = this.scrollState.get(key);
|
|
1163
|
+
if (!s) return;
|
|
1164
|
+
this.scrollState.delete(key);
|
|
1165
|
+
const end = pos();
|
|
1166
|
+
if (end.x === s.x && end.y === s.y) return;
|
|
1167
|
+
guard(this.sink, this.name, () => this.emit({ type: "scroll", payload: { from: { x: s.x, y: s.y }, to: end, target: isDoc ? "window" : "element" } }, el));
|
|
1168
|
+
}, 200)
|
|
1169
|
+
};
|
|
1170
|
+
this.scrollState.set(key, st);
|
|
1171
|
+
}
|
|
1172
|
+
st.flush();
|
|
1173
|
+
}
|
|
1174
|
+
onOver(e) {
|
|
1175
|
+
const el = this.target(e);
|
|
1176
|
+
const hit = el?.closest(INTERACTIVE);
|
|
1177
|
+
if (!hit || this.hover?.el === hit) return;
|
|
1178
|
+
this.endHover();
|
|
1179
|
+
const h = { el: hit, entered: false };
|
|
1180
|
+
h.timer = setTimeout(() => {
|
|
1181
|
+
h.entered = true;
|
|
1182
|
+
guard(this.sink, this.name, () => this.emit({ type: "hover", payload: { phase: "enter" } }, hit, { x: e.clientX, y: e.clientY }));
|
|
1183
|
+
}, 200);
|
|
1184
|
+
this.hover = h;
|
|
1185
|
+
}
|
|
1186
|
+
onOut(e) {
|
|
1187
|
+
const h = this.hover;
|
|
1188
|
+
if (!h) return;
|
|
1189
|
+
const to = e.relatedTarget;
|
|
1190
|
+
if (to && h.el.contains(to)) return;
|
|
1191
|
+
this.endHover();
|
|
1192
|
+
}
|
|
1193
|
+
endHover() {
|
|
1194
|
+
const h = this.hover;
|
|
1195
|
+
if (!h) return;
|
|
1196
|
+
if (h.timer) clearTimeout(h.timer);
|
|
1197
|
+
this.hover = void 0;
|
|
1198
|
+
if (h.entered) this.emit({ type: "hover", payload: { phase: "leave" } }, h.el);
|
|
1199
|
+
}
|
|
1200
|
+
onDown(e) {
|
|
1201
|
+
const el = this.target(e);
|
|
1202
|
+
if (!el || e.button !== 0) return;
|
|
1203
|
+
this.drag = { el, x: e.clientX, y: e.clientY, t: Date.now(), points: [{ x: e.clientX, y: e.clientY, t: 0 }], html5: false };
|
|
1204
|
+
}
|
|
1205
|
+
onUp(e) {
|
|
1206
|
+
const d = this.drag;
|
|
1207
|
+
this.drag = void 0;
|
|
1208
|
+
if (!d) return;
|
|
1209
|
+
const dist = Math.hypot(e.clientX - d.x, e.clientY - d.y);
|
|
1210
|
+
if (dist < DRAG_MIN_PX) return;
|
|
1211
|
+
const dropEl = safe(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
1212
|
+
const traj = d.points.length > 50 ? d.points.filter((_, i) => i % Math.ceil(d.points.length / 50) === 0) : d.points;
|
|
1213
|
+
const to = dropEl && !(dropEl === d.el) ? this.gen.generate(dropEl) : void 0;
|
|
1214
|
+
this.emit(
|
|
1215
|
+
{
|
|
1216
|
+
type: "drag",
|
|
1217
|
+
payload: {
|
|
1218
|
+
from: { x: Math.round(d.x), y: Math.round(d.y) },
|
|
1219
|
+
to: { x: Math.round(e.clientX), y: Math.round(e.clientY) },
|
|
1220
|
+
trajectory: traj,
|
|
1221
|
+
durationMs: Date.now() - d.t,
|
|
1222
|
+
html5: d.html5,
|
|
1223
|
+
target: to ? { selector: this.sink.sanitizer.scrubString(to.selector), unique: to.unique, element: dropEl ? this.gen.describe(dropEl) : void 0 } : void 0
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
d.el,
|
|
1227
|
+
{ x: d.x, y: d.y }
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
onMove(e) {
|
|
1231
|
+
const t = Date.now();
|
|
1232
|
+
if (this.drag) {
|
|
1233
|
+
const last2 = this.drag.points[this.drag.points.length - 1];
|
|
1234
|
+
if (t - this.drag.t - last2.t >= 30) this.drag.points.push({ x: Math.round(e.clientX), y: Math.round(e.clientY), t: t - this.drag.t });
|
|
1235
|
+
}
|
|
1236
|
+
if (!this.sink.config.recording.mouseMove) return;
|
|
1237
|
+
const last = this.move[this.move.length - 1];
|
|
1238
|
+
if (last && t - last.t < this.sink.config.recording.throttleMs) return;
|
|
1239
|
+
this.move.push({ x: Math.round(e.clientX), y: Math.round(e.clientY), t });
|
|
1240
|
+
if (this.move.length >= 40) this.flushMove();
|
|
1241
|
+
else this.moveFlush?.();
|
|
1242
|
+
}
|
|
1243
|
+
flushMove() {
|
|
1244
|
+
if (!this.move.length) return;
|
|
1245
|
+
const points = this.move;
|
|
1246
|
+
this.move = [];
|
|
1247
|
+
this.emit({ type: "mousemove", payload: { points } }, null, { x: points[points.length - 1].x, y: points[points.length - 1].y });
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
function mods(e) {
|
|
1251
|
+
const m = [];
|
|
1252
|
+
if (e.ctrlKey) m.push("Control");
|
|
1253
|
+
if (e.metaKey) m.push("Meta");
|
|
1254
|
+
if (e.altKey) m.push("Alt");
|
|
1255
|
+
if (e.shiftKey) m.push("Shift");
|
|
1256
|
+
return m;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/recorders/ContextRecorder.ts
|
|
1260
|
+
function parseBrowser(ua) {
|
|
1261
|
+
const rules = [
|
|
1262
|
+
["Edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
|
|
1263
|
+
["Opera", /OPR\/([\d.]+)/],
|
|
1264
|
+
["Firefox", /Firefox\/([\d.]+)/],
|
|
1265
|
+
["Chrome", /(?:Chrome|CriOS)\/([\d.]+)/],
|
|
1266
|
+
["Safari", /Version\/([\d.]+).*Safari/]
|
|
1267
|
+
];
|
|
1268
|
+
for (const [name, re] of rules) {
|
|
1269
|
+
const m = ua.match(re);
|
|
1270
|
+
if (m) return { name, version: m[1] };
|
|
1271
|
+
}
|
|
1272
|
+
return {};
|
|
1273
|
+
}
|
|
1274
|
+
function collectContext(id, reason, appVersion) {
|
|
1275
|
+
const nav = navigator;
|
|
1276
|
+
const ua = nav.userAgent ?? "";
|
|
1277
|
+
let browser = parseBrowser(ua);
|
|
1278
|
+
const brands = nav.userAgentData?.brands?.filter((b) => !/not.?a.?brand/i.test(b.brand));
|
|
1279
|
+
const preferred = brands?.find((b) => /chrom|edge|opera/i.test(b.brand)) ?? brands?.[0];
|
|
1280
|
+
if (preferred && !browser.name) browser = { name: preferred.brand, version: preferred.version };
|
|
1281
|
+
const navEntry = safe(() => performance.getEntriesByType("navigation")[0]);
|
|
1282
|
+
return {
|
|
1283
|
+
reason,
|
|
1284
|
+
url: location.href,
|
|
1285
|
+
referrer: document.referrer || void 0,
|
|
1286
|
+
title: document.title || void 0,
|
|
1287
|
+
navigationType: navEntry?.type,
|
|
1288
|
+
browser,
|
|
1289
|
+
os: nav.userAgentData?.platform ?? nav.platform,
|
|
1290
|
+
userAgent: ua,
|
|
1291
|
+
screen: { width: screen?.width ?? 0, height: screen?.height ?? 0 },
|
|
1292
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
1293
|
+
devicePixelRatio: window.devicePixelRatio ?? 1,
|
|
1294
|
+
language: nav.language,
|
|
1295
|
+
timezone: safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
1296
|
+
online: nav.onLine,
|
|
1297
|
+
visibility: document.visibilityState,
|
|
1298
|
+
platformId: id.platformId,
|
|
1299
|
+
testCaseId: id.testCaseId,
|
|
1300
|
+
executionId: id.executionId,
|
|
1301
|
+
sdkVersion: chunkB6GO5FE5_cjs.SDK_VERSION,
|
|
1302
|
+
appVersion
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
var ContextRecorder = class {
|
|
1306
|
+
constructor(sink, identity) {
|
|
1307
|
+
this.sink = sink;
|
|
1308
|
+
this.identity = identity;
|
|
1309
|
+
this.name = "context";
|
|
1310
|
+
this.d = new Disposer();
|
|
1311
|
+
}
|
|
1312
|
+
start() {
|
|
1313
|
+
this.record("page-load");
|
|
1314
|
+
const onResize = debounce(() => this.record("viewport"), 500);
|
|
1315
|
+
this.d.listen(window, "resize", onResize);
|
|
1316
|
+
this.d.listen(window, "orientationchange", onResize);
|
|
1317
|
+
this.d.add(() => onResize.cancel());
|
|
1318
|
+
}
|
|
1319
|
+
record(reason) {
|
|
1320
|
+
guard(this.sink, this.name, () => {
|
|
1321
|
+
const data = collectContext(this.identity(), reason, this.sink.config.appVersion);
|
|
1322
|
+
data.url = this.sink.sanitizer.sanitizeUrl(data.url);
|
|
1323
|
+
if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
|
|
1324
|
+
this.sink.emit("context", data, { priority: "normal" });
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
stop() {
|
|
1328
|
+
this.d.dispose();
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
// src/recorders/ErrorRecorder.ts
|
|
1333
|
+
var MAX_ARG_DEPTH = 3;
|
|
1334
|
+
var MAX_STR = 2e3;
|
|
1335
|
+
function serializeArg(v, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1336
|
+
if (v === null || v === void 0) return v;
|
|
1337
|
+
const t = typeof v;
|
|
1338
|
+
if (t === "string") return v.length > MAX_STR ? `${v.slice(0, MAX_STR)}\u2026` : v;
|
|
1339
|
+
if (t === "number" || t === "boolean") return v;
|
|
1340
|
+
if (t === "bigint") return `${String(v)}n`;
|
|
1341
|
+
if (t === "function") return `[Function ${v.name || "anonymous"}]`;
|
|
1342
|
+
if (t === "symbol") return String(v);
|
|
1343
|
+
if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };
|
|
1344
|
+
if (typeof Node !== "undefined" && v instanceof Node) return `[${v.nodeName}]`;
|
|
1345
|
+
if (depth >= MAX_ARG_DEPTH) return "[Object]";
|
|
1346
|
+
const obj = v;
|
|
1347
|
+
if (seen.has(obj)) return "[Circular]";
|
|
1348
|
+
seen.add(obj);
|
|
1349
|
+
if (Array.isArray(obj)) return obj.slice(0, 20).map((x) => serializeArg(x, depth + 1, seen));
|
|
1350
|
+
const out = {};
|
|
1351
|
+
for (const k of Object.keys(obj).slice(0, 30)) out[k] = safe(() => serializeArg(obj[k], depth + 1, seen));
|
|
1352
|
+
return out;
|
|
1353
|
+
}
|
|
1354
|
+
var FRAME = /(?:at\s+.*?\(|at\s+|@)(.+?):(\d+):(\d+)\)?$/;
|
|
1355
|
+
function firstFrame(stack) {
|
|
1356
|
+
if (!stack) return {};
|
|
1357
|
+
for (const line of stack.split("\n")) {
|
|
1358
|
+
const m = line.trim().match(FRAME);
|
|
1359
|
+
if (m && !/offscript/i.test(m[1])) return { file: m[1], line: Number(m[2]), column: Number(m[3]) };
|
|
1360
|
+
}
|
|
1361
|
+
return {};
|
|
1362
|
+
}
|
|
1363
|
+
var originals = /* @__PURE__ */ new Map();
|
|
1364
|
+
var ErrorRecorder = class {
|
|
1365
|
+
constructor(sink) {
|
|
1366
|
+
this.sink = sink;
|
|
1367
|
+
this.name = "errors";
|
|
1368
|
+
this.d = new Disposer();
|
|
1369
|
+
this.busy = false;
|
|
1370
|
+
}
|
|
1371
|
+
start() {
|
|
1372
|
+
const { console: wantConsole, errors, consoleLevels } = this.sink.config.recording;
|
|
1373
|
+
if (wantConsole) for (const level of consoleLevels) this.wrapConsole(level);
|
|
1374
|
+
if (errors) {
|
|
1375
|
+
this.d.listen(
|
|
1376
|
+
window,
|
|
1377
|
+
"error",
|
|
1378
|
+
(e) => {
|
|
1379
|
+
const target = e.target;
|
|
1380
|
+
if (target && target !== window && target.tagName) {
|
|
1381
|
+
this.report({ source: "resource", message: `Falha ao carregar <${target.tagName.toLowerCase()}>`, file: target.src || target.href });
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
const err = e.error;
|
|
1385
|
+
const frame = err?.stack ? firstFrame(err.stack) : {};
|
|
1386
|
+
this.report({
|
|
1387
|
+
source: "window.onerror",
|
|
1388
|
+
message: e.message || String(err),
|
|
1389
|
+
name: err?.name,
|
|
1390
|
+
stack: err?.stack,
|
|
1391
|
+
file: e.filename || frame.file,
|
|
1392
|
+
line: e.lineno || frame.line,
|
|
1393
|
+
column: e.colno || frame.column
|
|
1394
|
+
});
|
|
1395
|
+
},
|
|
1396
|
+
true
|
|
1397
|
+
);
|
|
1398
|
+
this.d.listen(window, "unhandledrejection", (e) => {
|
|
1399
|
+
const r = e.reason;
|
|
1400
|
+
const isErr = r instanceof Error;
|
|
1401
|
+
const stack = isErr ? r.stack : void 0;
|
|
1402
|
+
this.report({
|
|
1403
|
+
source: "unhandledrejection",
|
|
1404
|
+
message: isErr ? r.message : String(safe(() => JSON.stringify(r)) ?? r),
|
|
1405
|
+
name: isErr ? r.name : void 0,
|
|
1406
|
+
stack,
|
|
1407
|
+
...firstFrame(stack)
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
report(partial) {
|
|
1413
|
+
guard(this.sink, this.name, () => {
|
|
1414
|
+
const s = this.sink.sanitizer;
|
|
1415
|
+
this.sink.emit(
|
|
1416
|
+
"error",
|
|
1417
|
+
{
|
|
1418
|
+
...partial,
|
|
1419
|
+
message: s.scrubString(partial.message ?? ""),
|
|
1420
|
+
stack: partial.stack ? s.scrubString(partial.stack) : void 0,
|
|
1421
|
+
file: partial.file ? s.sanitizeUrl(partial.file) : void 0,
|
|
1422
|
+
url: s.sanitizeUrl(location.href)
|
|
1423
|
+
},
|
|
1424
|
+
{ priority: "critical" }
|
|
1425
|
+
);
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
wrapConsole(level) {
|
|
1429
|
+
const current = console[level];
|
|
1430
|
+
if (!current || originals.has(level)) return;
|
|
1431
|
+
const self = this;
|
|
1432
|
+
const wrapped = function(...args) {
|
|
1433
|
+
const result = current.apply(this === void 0 ? console : this, args);
|
|
1434
|
+
if (!self.busy) {
|
|
1435
|
+
self.busy = true;
|
|
1436
|
+
try {
|
|
1437
|
+
self.captureConsole(level, args);
|
|
1438
|
+
} catch {
|
|
1439
|
+
} finally {
|
|
1440
|
+
self.busy = false;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return result;
|
|
1444
|
+
};
|
|
1445
|
+
originals.set(level, { orig: current, wrapped });
|
|
1446
|
+
console[level] = wrapped;
|
|
1447
|
+
this.d.add(() => {
|
|
1448
|
+
const entry = originals.get(level);
|
|
1449
|
+
if (entry && console[level] === entry.wrapped) console[level] = entry.orig;
|
|
1450
|
+
originals.delete(level);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
captureConsole(level, args) {
|
|
1454
|
+
const s = this.sink.sanitizer;
|
|
1455
|
+
const serial = args.map((a) => s.sanitizeValue(serializeArg(a)));
|
|
1456
|
+
const message = serial.map((a) => typeof a === "string" ? a : safe(() => JSON.stringify(a)) ?? String(a)).join(" ");
|
|
1457
|
+
const stack = level === "error" || level === "warn" ? s.scrubString(new Error().stack?.split("\n").slice(3).join("\n") ?? "") : void 0;
|
|
1458
|
+
this.sink.emit(
|
|
1459
|
+
"console",
|
|
1460
|
+
{ level, args: serial, message: message.slice(0, 4e3), stack, url: s.sanitizeUrl(location.href) },
|
|
1461
|
+
{ priority: level === "error" ? "normal" : "low" }
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
stop() {
|
|
1465
|
+
this.d.dispose();
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
// src/utils/id.ts
|
|
1470
|
+
function uid(prefix = "") {
|
|
1471
|
+
const c = globalThis.crypto;
|
|
1472
|
+
if (c?.randomUUID) return prefix + c.randomUUID();
|
|
1473
|
+
const bytes = new Uint8Array(16);
|
|
1474
|
+
if (c?.getRandomValues) c.getRandomValues(bytes);
|
|
1475
|
+
else for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
1476
|
+
return prefix + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/recorders/HTTPRecorder.ts
|
|
1480
|
+
var XHR_PATCHED = "__offscriptXhrOriginal";
|
|
1481
|
+
var HTTPRecorder = class {
|
|
1482
|
+
constructor(sink, ownApiUrl) {
|
|
1483
|
+
this.sink = sink;
|
|
1484
|
+
this.ownApiUrl = ownApiUrl;
|
|
1485
|
+
this.name = "http";
|
|
1486
|
+
this.inflight = 0;
|
|
1487
|
+
this.active = false;
|
|
1488
|
+
this.restore = [];
|
|
1489
|
+
}
|
|
1490
|
+
start() {
|
|
1491
|
+
if (this.active) return;
|
|
1492
|
+
this.active = true;
|
|
1493
|
+
this.patchFetch();
|
|
1494
|
+
this.patchXhr();
|
|
1495
|
+
}
|
|
1496
|
+
stop() {
|
|
1497
|
+
this.active = false;
|
|
1498
|
+
for (const r of this.restore.splice(0).reverse()) {
|
|
1499
|
+
try {
|
|
1500
|
+
r();
|
|
1501
|
+
} catch {
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
isIgnored(url) {
|
|
1506
|
+
if (this.ownApiUrl && url.startsWith(this.ownApiUrl)) return true;
|
|
1507
|
+
return this.sink.sanitizer.isIgnoredUrl(url);
|
|
1508
|
+
}
|
|
1509
|
+
// ---------------------------------------------------------------- fetch
|
|
1510
|
+
patchFetch() {
|
|
1511
|
+
if (typeof globalThis.fetch !== "function") return;
|
|
1512
|
+
const orig = globalThis.fetch;
|
|
1513
|
+
const self = this;
|
|
1514
|
+
const patched = function(input, init) {
|
|
1515
|
+
let ctx = null;
|
|
1516
|
+
if (self.active) {
|
|
1517
|
+
try {
|
|
1518
|
+
ctx = self.beginFetch(input, init);
|
|
1519
|
+
} catch {
|
|
1520
|
+
ctx = null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
let p;
|
|
1524
|
+
try {
|
|
1525
|
+
p = orig.apply(this, arguments);
|
|
1526
|
+
} catch (e) {
|
|
1527
|
+
if (ctx) self.inflight--;
|
|
1528
|
+
throw e;
|
|
1529
|
+
}
|
|
1530
|
+
if (ctx) {
|
|
1531
|
+
const c = ctx;
|
|
1532
|
+
p.then(
|
|
1533
|
+
(res) => self.endFetch(c, res),
|
|
1534
|
+
(err) => self.failFetch(c, err)
|
|
1535
|
+
).catch(() => void 0);
|
|
1536
|
+
}
|
|
1537
|
+
return p;
|
|
1538
|
+
};
|
|
1539
|
+
patched[ORIGINAL_FETCH] = orig[ORIGINAL_FETCH] ?? orig;
|
|
1540
|
+
globalThis.fetch = patched;
|
|
1541
|
+
this.restore.push(() => {
|
|
1542
|
+
if (globalThis.fetch === patched) globalThis.fetch = orig;
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
beginFetch(input, init) {
|
|
1546
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1547
|
+
const abs = new URL(rawUrl, location.href).href;
|
|
1548
|
+
if (this.isIgnored(abs)) return null;
|
|
1549
|
+
const isReq = typeof Request !== "undefined" && input instanceof Request;
|
|
1550
|
+
const method = (init?.method ?? (isReq ? input.method : "GET")).toUpperCase();
|
|
1551
|
+
const ctx = { requestId: uid("req_"), transport: "fetch", url: abs, method, startedAt: Date.now(), concurrent: this.inflight };
|
|
1552
|
+
const s = this.sink.sanitizer;
|
|
1553
|
+
const hdrs = {};
|
|
1554
|
+
new Headers(init?.headers ?? (isReq ? input.headers : void 0)).forEach((v, k) => hdrs[k] = v);
|
|
1555
|
+
ctx.requestHeaders = s.sanitizeHeaders(hdrs);
|
|
1556
|
+
if (this.sink.config.privacy.captureRequestBodies) {
|
|
1557
|
+
const ct = hdrs["content-type"] ?? "";
|
|
1558
|
+
if (init?.body != null) ctx.requestBody = this.describeBody(init.body, ct);
|
|
1559
|
+
else ctx.requestBody = { kind: "omitted", reason: isReq ? "request-object" : "no-body" };
|
|
1560
|
+
}
|
|
1561
|
+
this.inflight++;
|
|
1562
|
+
return ctx;
|
|
1563
|
+
}
|
|
1564
|
+
endFetch(ctx, res) {
|
|
1565
|
+
this.inflight = Math.max(0, this.inflight - 1);
|
|
1566
|
+
guard(this.sink, this.name, () => {
|
|
1567
|
+
const endedAt = Date.now();
|
|
1568
|
+
const headers = {};
|
|
1569
|
+
res.headers.forEach((v, k) => headers[k] = v);
|
|
1570
|
+
const base = this.base(ctx, endedAt);
|
|
1571
|
+
base.status = res.status;
|
|
1572
|
+
base.statusText = res.statusText;
|
|
1573
|
+
base.responseHeaders = this.sink.sanitizer.sanitizeHeaders(headers);
|
|
1574
|
+
const ct = headers["content-type"] ?? "";
|
|
1575
|
+
if (!this.sink.config.privacy.captureResponseBodies) return void this.emit(base);
|
|
1576
|
+
if (!this.sink.sanitizer.isTextual(ct) || /event-stream/i.test(ct) || !res.body) {
|
|
1577
|
+
base.responseBody = { kind: "omitted", reason: !res.body ? "no-body" : /event-stream/i.test(ct) ? "stream" : "binary" };
|
|
1578
|
+
return void this.emit(base);
|
|
1579
|
+
}
|
|
1580
|
+
let clone;
|
|
1581
|
+
try {
|
|
1582
|
+
clone = res.clone();
|
|
1583
|
+
} catch {
|
|
1584
|
+
base.responseBody = { kind: "omitted", reason: "body-used" };
|
|
1585
|
+
return void this.emit(base);
|
|
1586
|
+
}
|
|
1587
|
+
this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
1588
|
+
const b = this.sink.sanitizer.sanitizeTextBody(text, ct, size);
|
|
1589
|
+
b.truncated = truncated || b.truncated;
|
|
1590
|
+
base.responseBody = b;
|
|
1591
|
+
}).catch(() => {
|
|
1592
|
+
base.responseBody = { kind: "omitted", reason: "read-failed" };
|
|
1593
|
+
}).finally(() => guard(this.sink, this.name, () => this.emit(base)));
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
async readBounded(res) {
|
|
1597
|
+
const max = this.sink.config.privacy.maxBodyBytes;
|
|
1598
|
+
const reader = res.body.getReader();
|
|
1599
|
+
const dec = new TextDecoder();
|
|
1600
|
+
let text = "";
|
|
1601
|
+
let size = 0;
|
|
1602
|
+
let truncated = false;
|
|
1603
|
+
for (; ; ) {
|
|
1604
|
+
const { done: done2, value } = await reader.read();
|
|
1605
|
+
if (done2) break;
|
|
1606
|
+
size += value.byteLength;
|
|
1607
|
+
if (text.length < max) text += dec.decode(value, { stream: true });
|
|
1608
|
+
if (size > max * 4) {
|
|
1609
|
+
truncated = true;
|
|
1610
|
+
await reader.cancel().catch(() => void 0);
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (text.length > max) {
|
|
1615
|
+
text = text.slice(0, max);
|
|
1616
|
+
truncated = true;
|
|
1617
|
+
}
|
|
1618
|
+
return { text, size, truncated };
|
|
1619
|
+
}
|
|
1620
|
+
failFetch(ctx, err) {
|
|
1621
|
+
this.inflight = Math.max(0, this.inflight - 1);
|
|
1622
|
+
guard(this.sink, this.name, () => {
|
|
1623
|
+
const e = err;
|
|
1624
|
+
const base = this.base(ctx, Date.now());
|
|
1625
|
+
base.aborted = e?.name === "AbortError";
|
|
1626
|
+
base.error = { name: e?.name ?? "Error", message: this.sink.sanitizer.scrubString(String(e?.message ?? e)) };
|
|
1627
|
+
this.emit(base);
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
// ---------------------------------------------------------------- XHR
|
|
1631
|
+
patchXhr() {
|
|
1632
|
+
if (typeof XMLHttpRequest === "undefined") return;
|
|
1633
|
+
const proto = XMLHttpRequest.prototype;
|
|
1634
|
+
const self = this;
|
|
1635
|
+
const metas = /* @__PURE__ */ new WeakMap();
|
|
1636
|
+
const origOpen = proto.open;
|
|
1637
|
+
const origSend = proto.send;
|
|
1638
|
+
const origSet = proto.setRequestHeader;
|
|
1639
|
+
const open = function(...args) {
|
|
1640
|
+
try {
|
|
1641
|
+
if (self.active) metas.set(this, { method: String(args[0]).toUpperCase(), url: String(args[1]), headers: {} });
|
|
1642
|
+
} catch {
|
|
1643
|
+
}
|
|
1644
|
+
return origOpen.apply(this, args);
|
|
1645
|
+
};
|
|
1646
|
+
const setHeader = function(name, value) {
|
|
1647
|
+
try {
|
|
1648
|
+
const m = metas.get(this);
|
|
1649
|
+
if (m) m.headers[name.toLowerCase()] = value;
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
return origSet.call(this, name, value);
|
|
1653
|
+
};
|
|
1654
|
+
const send = function(body) {
|
|
1655
|
+
try {
|
|
1656
|
+
const m = metas.get(this);
|
|
1657
|
+
if (m && self.active) self.beginXhr(this, m, body);
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
return origSend.call(this, body);
|
|
1661
|
+
};
|
|
1662
|
+
open[XHR_PATCHED] = origOpen;
|
|
1663
|
+
proto.open = open;
|
|
1664
|
+
proto.setRequestHeader = setHeader;
|
|
1665
|
+
proto.send = send;
|
|
1666
|
+
this.restore.push(() => {
|
|
1667
|
+
if (proto.open === open) proto.open = origOpen;
|
|
1668
|
+
if (proto.setRequestHeader === setHeader) proto.setRequestHeader = origSet;
|
|
1669
|
+
if (proto.send === send) proto.send = origSend;
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
beginXhr(xhr, m, body) {
|
|
1673
|
+
const abs = new URL(m.url, location.href).href;
|
|
1674
|
+
if (this.isIgnored(abs)) return;
|
|
1675
|
+
const ctx = { requestId: uid("req_"), transport: "xhr", url: abs, method: m.method, startedAt: Date.now(), concurrent: this.inflight };
|
|
1676
|
+
ctx.requestHeaders = this.sink.sanitizer.sanitizeHeaders(m.headers);
|
|
1677
|
+
if (this.sink.config.privacy.captureRequestBodies) {
|
|
1678
|
+
ctx.requestBody = body == null ? { kind: "omitted", reason: "no-body" } : this.describeBody(body, m.headers["content-type"] ?? "");
|
|
1679
|
+
}
|
|
1680
|
+
m.ctx = ctx;
|
|
1681
|
+
this.inflight++;
|
|
1682
|
+
xhr.addEventListener("abort", () => m.aborted = true);
|
|
1683
|
+
xhr.addEventListener("timeout", () => m.timedOut = true);
|
|
1684
|
+
xhr.addEventListener("loadend", () => {
|
|
1685
|
+
this.inflight = Math.max(0, this.inflight - 1);
|
|
1686
|
+
guard(this.sink, this.name, () => this.endXhr(xhr, m, ctx));
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
endXhr(xhr, m, ctx) {
|
|
1690
|
+
const base = this.base(ctx, Date.now());
|
|
1691
|
+
const failed = xhr.status === 0;
|
|
1692
|
+
if (!failed) {
|
|
1693
|
+
base.status = xhr.status;
|
|
1694
|
+
base.statusText = xhr.statusText;
|
|
1695
|
+
}
|
|
1696
|
+
const headers = {};
|
|
1697
|
+
xhr.getAllResponseHeaders().trim().split(/\r?\n/).forEach((line) => {
|
|
1698
|
+
const i = line.indexOf(":");
|
|
1699
|
+
if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
|
|
1700
|
+
});
|
|
1701
|
+
base.responseHeaders = this.sink.sanitizer.sanitizeHeaders(headers);
|
|
1702
|
+
if (m.aborted) base.aborted = true;
|
|
1703
|
+
if (failed) base.error = { name: m.aborted ? "AbortError" : m.timedOut ? "TimeoutError" : "NetworkError", message: m.aborted ? "aborted" : m.timedOut ? "timeout" : "network error" };
|
|
1704
|
+
if (!failed && this.sink.config.privacy.captureResponseBodies) {
|
|
1705
|
+
const ct = headers["content-type"] ?? "";
|
|
1706
|
+
if (this.sink.sanitizer.isTextual(ct) && (xhr.responseType === "" || xhr.responseType === "text")) base.responseBody = this.sink.sanitizer.sanitizeTextBody(xhr.responseText, ct);
|
|
1707
|
+
else if (xhr.responseType === "json") base.responseBody = this.sink.sanitizer.sanitizeTextBody(JSON.stringify(xhr.response ?? null), "application/json");
|
|
1708
|
+
else base.responseBody = { kind: "omitted", reason: "binary" };
|
|
1709
|
+
}
|
|
1710
|
+
this.emit(base);
|
|
1711
|
+
}
|
|
1712
|
+
// ---------------------------------------------------------------- comum
|
|
1713
|
+
base(ctx, endedAt) {
|
|
1714
|
+
const s = this.sink.sanitizer;
|
|
1715
|
+
return {
|
|
1716
|
+
requestId: ctx.requestId,
|
|
1717
|
+
transport: ctx.transport,
|
|
1718
|
+
url: s.sanitizeUrl(ctx.url),
|
|
1719
|
+
method: ctx.method,
|
|
1720
|
+
query: s.parseQuery(ctx.url),
|
|
1721
|
+
requestHeaders: ctx.requestHeaders,
|
|
1722
|
+
requestBody: ctx.requestBody,
|
|
1723
|
+
startedAt: ctx.startedAt,
|
|
1724
|
+
endedAt,
|
|
1725
|
+
durationMs: endedAt - ctx.startedAt,
|
|
1726
|
+
concurrent: ctx.concurrent
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
emit(data) {
|
|
1730
|
+
this.sink.emit("http", data, { priority: "normal", at: data.startedAt });
|
|
1731
|
+
}
|
|
1732
|
+
describeBody(body, contentType) {
|
|
1733
|
+
const s = this.sink.sanitizer;
|
|
1734
|
+
const max = this.sink.config.privacy.maxBodyBytes;
|
|
1735
|
+
if (typeof body === "string") return s.sanitizeTextBody(body, contentType || "text/plain", body.length);
|
|
1736
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return s.sanitizeTextBody(body.toString(), "application/x-www-form-urlencoded");
|
|
1737
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) {
|
|
1738
|
+
const form = {};
|
|
1739
|
+
body.forEach((v, k) => {
|
|
1740
|
+
form[k] = s.isSensitiveKey(k) ? "[REDACTED]" : typeof v === "string" ? s.scrubString(v.slice(0, max)) : `[File ${v.name}, ${v.size}B]`;
|
|
1741
|
+
});
|
|
1742
|
+
return { kind: "form", value: form };
|
|
1743
|
+
}
|
|
1744
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) return { kind: "binary", size: body.size, reason: "blob" };
|
|
1745
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return { kind: "binary", size: body.byteLength };
|
|
1746
|
+
return { kind: "omitted", reason: "unsupported-body" };
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
// src/recorders/IframeRecorder.ts
|
|
1751
|
+
var SEATSIO = /(^|\.)seatsio\.net$|(^|\.)seats\.io$/i;
|
|
1752
|
+
var LIMITATION = "Conte\xFAdo interno de iframes cross-origin n\xE3o \xE9 acess\xEDvel ao SDK nem ao rrweb; apenas presen\xE7a, geometria, foco e intera\xE7\xF5es observ\xE1veis na p\xE1gina principal s\xE3o registrados.";
|
|
1753
|
+
var IframeRecorder = class {
|
|
1754
|
+
constructor(sink) {
|
|
1755
|
+
this.sink = sink;
|
|
1756
|
+
this.name = "iframes";
|
|
1757
|
+
this.d = new Disposer();
|
|
1758
|
+
this.known = /* @__PURE__ */ new WeakMap();
|
|
1759
|
+
this.counter = 0;
|
|
1760
|
+
}
|
|
1761
|
+
start() {
|
|
1762
|
+
document.querySelectorAll("iframe").forEach((f) => this.add(f));
|
|
1763
|
+
this.mo = new MutationObserver(
|
|
1764
|
+
(records) => guard(this.sink, this.name, () => {
|
|
1765
|
+
for (const r of records) {
|
|
1766
|
+
r.addedNodes.forEach((n) => this.scan(n, true));
|
|
1767
|
+
r.removedNodes.forEach((n) => this.scan(n, false));
|
|
1768
|
+
}
|
|
1769
|
+
})
|
|
1770
|
+
);
|
|
1771
|
+
this.mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
1772
|
+
this.d.add(() => this.mo?.disconnect());
|
|
1773
|
+
let focused = null;
|
|
1774
|
+
this.d.listen(
|
|
1775
|
+
window,
|
|
1776
|
+
"blur",
|
|
1777
|
+
() => setTimeout(() => {
|
|
1778
|
+
guard(this.sink, this.name, () => {
|
|
1779
|
+
const a = document.activeElement;
|
|
1780
|
+
if (a instanceof HTMLIFrameElement) {
|
|
1781
|
+
focused = a;
|
|
1782
|
+
this.event("focus", a);
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
}, 0)
|
|
1786
|
+
);
|
|
1787
|
+
this.d.listen(
|
|
1788
|
+
window,
|
|
1789
|
+
"focus",
|
|
1790
|
+
() => guard(this.sink, this.name, () => {
|
|
1791
|
+
if (focused) {
|
|
1792
|
+
this.event("blur", focused);
|
|
1793
|
+
focused = null;
|
|
1794
|
+
}
|
|
1795
|
+
})
|
|
1796
|
+
);
|
|
1797
|
+
const onResize = debounce(() => document.querySelectorAll("iframe").forEach((f) => this.known.has(f) && this.event("resized", f)), 500);
|
|
1798
|
+
this.d.listen(window, "resize", onResize);
|
|
1799
|
+
this.d.add(() => onResize.cancel());
|
|
1800
|
+
}
|
|
1801
|
+
scan(n, added) {
|
|
1802
|
+
if (!(n instanceof Element)) return;
|
|
1803
|
+
const frames = n instanceof HTMLIFrameElement ? [n] : Array.from(n.querySelectorAll("iframe"));
|
|
1804
|
+
for (const f of frames) {
|
|
1805
|
+
if (added) this.add(f);
|
|
1806
|
+
else if (this.known.has(f)) this.event("removed", f);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
add(f) {
|
|
1810
|
+
if (this.known.has(f)) return;
|
|
1811
|
+
this.known.set(f, ++this.counter);
|
|
1812
|
+
this.event("present", f);
|
|
1813
|
+
f.addEventListener("load", () => guard(this.sink, this.name, () => this.event("resized", f)), { once: true });
|
|
1814
|
+
}
|
|
1815
|
+
event(phase, f) {
|
|
1816
|
+
const src = f.getAttribute("src") ?? "";
|
|
1817
|
+
let origin;
|
|
1818
|
+
let path;
|
|
1819
|
+
let host = "";
|
|
1820
|
+
try {
|
|
1821
|
+
const u = new URL(src, location.href);
|
|
1822
|
+
origin = u.origin;
|
|
1823
|
+
path = u.pathname;
|
|
1824
|
+
host = u.hostname;
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
const crossOrigin = origin !== void 0 && origin !== location.origin && origin !== "null";
|
|
1828
|
+
const r = safe(() => f.getBoundingClientRect());
|
|
1829
|
+
const idx = this.known.get(f) ?? 0;
|
|
1830
|
+
this.sink.emit(
|
|
1831
|
+
"iframe",
|
|
1832
|
+
{
|
|
1833
|
+
phase,
|
|
1834
|
+
provider: SEATSIO.test(host) ? "seatsio" : "unknown",
|
|
1835
|
+
origin,
|
|
1836
|
+
path,
|
|
1837
|
+
crossOrigin,
|
|
1838
|
+
rect: { x: Math.round(r?.x ?? 0), y: Math.round(r?.y ?? 0), width: Math.round(r?.width ?? 0), height: Math.round(r?.height ?? 0) },
|
|
1839
|
+
index: idx,
|
|
1840
|
+
limitation: crossOrigin ? LIMITATION : "same-origin"
|
|
1841
|
+
},
|
|
1842
|
+
{ priority: "normal" }
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
stop() {
|
|
1846
|
+
this.d.dispose();
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
// src/recorders/RRWebRecorder.ts
|
|
1851
|
+
var PRIORITY = { 2: "critical", 4: "critical", 5: "critical", 0: "normal", 1: "normal", 3: "normal", 6: "normal" };
|
|
1852
|
+
var URL_ATTRS = ["src", "href", "action", "poster", "data-src", "data", "formaction", "srcset", "cite", "rr_src"];
|
|
1853
|
+
var LOW_SOURCES = /* @__PURE__ */ new Set([1, 6, 12, 3]);
|
|
1854
|
+
var RRWebRecorder = class {
|
|
1855
|
+
constructor(sink) {
|
|
1856
|
+
this.sink = sink;
|
|
1857
|
+
this.name = "rrweb";
|
|
1858
|
+
this.paused = false;
|
|
1859
|
+
this.stopped = true;
|
|
1860
|
+
this.needsSnapshot = false;
|
|
1861
|
+
this.snapshotsTaken = 0;
|
|
1862
|
+
}
|
|
1863
|
+
async start() {
|
|
1864
|
+
this.stopped = false;
|
|
1865
|
+
const { recording, privacy } = this.sink.config;
|
|
1866
|
+
const mod = this.mod ?? (this.mod = await import('rrweb'));
|
|
1867
|
+
if (this.stopped) return;
|
|
1868
|
+
const allowed = privacy.allowedInputSelectors.join(",");
|
|
1869
|
+
const block = privacy.blockSelectors.join(",");
|
|
1870
|
+
const maskText = privacy.maskAllText ? "*" : privacy.maskTextSelectors.join(",");
|
|
1871
|
+
const options = {
|
|
1872
|
+
emit: (event) => this.onEvent(event),
|
|
1873
|
+
blockSelector: block || void 0,
|
|
1874
|
+
maskTextSelector: maskText || void 0,
|
|
1875
|
+
maskAllInputs: privacy.maskSensitiveInputs,
|
|
1876
|
+
maskInputOptions: privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
|
|
1877
|
+
maskInputFn: (text, el) => {
|
|
1878
|
+
if (el?.type === "password") return "*".repeat(text.length);
|
|
1879
|
+
if (allowed && safe(() => el.matches(allowed))) return text;
|
|
1880
|
+
return "*".repeat(text.length);
|
|
1881
|
+
},
|
|
1882
|
+
recordCanvas: recording.rrwebRecordCanvas,
|
|
1883
|
+
collectFonts: false,
|
|
1884
|
+
inlineStylesheet: true,
|
|
1885
|
+
sampling: { mousemove: Math.max(50, recording.throttleMs), scroll: 150, input: "last", media: 800 },
|
|
1886
|
+
slimDOMOptions: { script: true, comment: true, headFavicon: true, headWhitespace: true },
|
|
1887
|
+
checkoutEveryNms: recording.rrwebCheckoutEveryMs > 0 ? recording.rrwebCheckoutEveryMs : void 0,
|
|
1888
|
+
errorHandler: () => true
|
|
1889
|
+
// erros internos do rrweb não propagam
|
|
1890
|
+
};
|
|
1891
|
+
this.stopFn = mod.record(options) ?? void 0;
|
|
1892
|
+
}
|
|
1893
|
+
onEvent(event) {
|
|
1894
|
+
if (this.paused) return;
|
|
1895
|
+
guard(this.sink, this.name, () => {
|
|
1896
|
+
this.sanitizeEvent(event);
|
|
1897
|
+
let priority = PRIORITY[event.type] ?? "normal";
|
|
1898
|
+
if (event.type === 3 && LOW_SOURCES.has(event.data?.source)) priority = "low";
|
|
1899
|
+
if (event.type === 2) this.snapshotsTaken++;
|
|
1900
|
+
const emitted = this.sink.emit("rrweb", event, { priority, at: event.timestamp });
|
|
1901
|
+
if (!emitted && event.type === 3) this.needsSnapshot = true;
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
/** Sanitiza URLs em atributos e padrões sensíveis em textos ANTES de persistir/enviar. */
|
|
1905
|
+
sanitizeEvent(event) {
|
|
1906
|
+
const s = this.sink.sanitizer;
|
|
1907
|
+
const clean = (k, v) => k === "srcset" ? v.split(",").map((part) => {
|
|
1908
|
+
const [u, ...rest] = part.trim().split(/\s+/);
|
|
1909
|
+
return [s.redactUrlAttribute(u), ...rest].join(" ");
|
|
1910
|
+
}).join(", ") : s.redactUrlAttribute(v);
|
|
1911
|
+
const node = (n) => {
|
|
1912
|
+
if (!n || typeof n !== "object") return;
|
|
1913
|
+
if (n.type === 2 && n.attributes) {
|
|
1914
|
+
for (const k of URL_ATTRS) if (typeof n.attributes[k] === "string") n.attributes[k] = clean(k, n.attributes[k]);
|
|
1915
|
+
} else if (n.type === 3 && typeof n.textContent === "string") {
|
|
1916
|
+
n.textContent = s.scrubString(n.textContent);
|
|
1917
|
+
}
|
|
1918
|
+
if (Array.isArray(n.childNodes)) for (const c of n.childNodes) node(c);
|
|
1919
|
+
};
|
|
1920
|
+
if (event.type === 4 && event.data?.href) event.data.href = s.sanitizeUrl(event.data.href);
|
|
1921
|
+
else if (event.type === 2) node(event.data?.node);
|
|
1922
|
+
else if (event.type === 3 && event.data?.source === 0) {
|
|
1923
|
+
for (const a of event.data.adds ?? []) node(a.node);
|
|
1924
|
+
for (const t of event.data.texts ?? []) if (typeof t.value === "string") t.value = s.scrubString(t.value);
|
|
1925
|
+
for (const a of event.data.attributes ?? []) {
|
|
1926
|
+
for (const k of URL_ATTRS) if (typeof a.attributes?.[k] === "string") a.attributes[k] = clean(k, a.attributes[k]);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
/** Chamado pelo núcleo quando há folga novamente após descartes. */
|
|
1931
|
+
recoverIfNeeded() {
|
|
1932
|
+
if (this.needsSnapshot && !this.paused) {
|
|
1933
|
+
this.needsSnapshot = false;
|
|
1934
|
+
this.takeFullSnapshot();
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
takeFullSnapshot() {
|
|
1938
|
+
if (this.stopped) return;
|
|
1939
|
+
guard(this.sink, this.name, () => this.mod?.record.takeFullSnapshot(true));
|
|
1940
|
+
}
|
|
1941
|
+
addCustomEvent(tag, payload) {
|
|
1942
|
+
if (this.stopped) return;
|
|
1943
|
+
guard(this.sink, this.name, () => this.mod?.record.addCustomEvent(tag, payload));
|
|
1944
|
+
}
|
|
1945
|
+
get snapshots() {
|
|
1946
|
+
return this.snapshotsTaken;
|
|
1947
|
+
}
|
|
1948
|
+
pause() {
|
|
1949
|
+
this.paused = true;
|
|
1950
|
+
}
|
|
1951
|
+
/** Ao retomar, as mutações perdidas durante a pausa exigem um snapshot completo. */
|
|
1952
|
+
resume() {
|
|
1953
|
+
this.paused = false;
|
|
1954
|
+
this.takeFullSnapshot();
|
|
1955
|
+
}
|
|
1956
|
+
stop() {
|
|
1957
|
+
this.stopped = true;
|
|
1958
|
+
safe(() => this.stopFn?.());
|
|
1959
|
+
this.stopFn = void 0;
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
// src/core/LifecycleManager.ts
|
|
1964
|
+
var PATCHED = "__offscriptHistoryPatched";
|
|
1965
|
+
var LifecycleManager = class {
|
|
1966
|
+
constructor(h) {
|
|
1967
|
+
this.h = h;
|
|
1968
|
+
this.d = new Disposer();
|
|
1969
|
+
this.lastUrl = "";
|
|
1970
|
+
this.installed = false;
|
|
1971
|
+
}
|
|
1972
|
+
install() {
|
|
1973
|
+
if (this.installed || typeof window === "undefined") return;
|
|
1974
|
+
this.installed = true;
|
|
1975
|
+
this.lastUrl = location.href;
|
|
1976
|
+
const d = this.d;
|
|
1977
|
+
d.listen(window, "pagehide", () => safe(() => this.h.onPageHide()), { capture: true });
|
|
1978
|
+
d.listen(window, "beforeunload", () => safe(() => this.h.onPageHide()), { capture: true });
|
|
1979
|
+
d.listen(document, "visibilitychange", () => {
|
|
1980
|
+
const hidden = document.visibilityState === "hidden";
|
|
1981
|
+
safe(() => this.h.onVisibility(hidden));
|
|
1982
|
+
if (hidden) safe(() => this.h.onPageHide());
|
|
1983
|
+
});
|
|
1984
|
+
d.listen(window, "online", () => safe(() => this.h.onConnectivity(true)));
|
|
1985
|
+
d.listen(window, "offline", () => safe(() => this.h.onConnectivity(false)));
|
|
1986
|
+
d.listen(window, "popstate", () => this.notify("popstate"));
|
|
1987
|
+
d.listen(window, "hashchange", () => this.notify("hashchange"));
|
|
1988
|
+
this.patchHistory("pushState");
|
|
1989
|
+
this.patchHistory("replaceState");
|
|
1990
|
+
}
|
|
1991
|
+
notify(kind) {
|
|
1992
|
+
const to = location.href;
|
|
1993
|
+
if (to === this.lastUrl) return;
|
|
1994
|
+
const from = this.lastUrl;
|
|
1995
|
+
this.lastUrl = to;
|
|
1996
|
+
safe(() => this.h.onNavigation({ from, to, kind }));
|
|
1997
|
+
}
|
|
1998
|
+
patchHistory(method) {
|
|
1999
|
+
const original = history[method];
|
|
2000
|
+
if (original[PATCHED]) return;
|
|
2001
|
+
const self = this;
|
|
2002
|
+
const patched = function(...args) {
|
|
2003
|
+
const result = original.apply(this, args);
|
|
2004
|
+
self.notify(method);
|
|
2005
|
+
return result;
|
|
2006
|
+
};
|
|
2007
|
+
patched[PATCHED] = original;
|
|
2008
|
+
history[method] = patched;
|
|
2009
|
+
this.d.add(() => {
|
|
2010
|
+
if (history[method] === patched) history[method] = original;
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
uninstall() {
|
|
2014
|
+
this.d.dispose();
|
|
2015
|
+
this.installed = false;
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
// src/core/SessionManager.ts
|
|
2020
|
+
var STATE_VERSION = 1;
|
|
2021
|
+
var SessionManager = class {
|
|
2022
|
+
constructor(cfg) {
|
|
2023
|
+
this.cfg = cfg;
|
|
2024
|
+
this.state = null;
|
|
2025
|
+
this.info = null;
|
|
2026
|
+
this.seq = 0;
|
|
2027
|
+
this.token = "";
|
|
2028
|
+
this.storageKey = `${cfg.storagePrefix}:state:${cfg.platformId}`;
|
|
2029
|
+
this.seqKey = `${cfg.storagePrefix}:seq:${cfg.platformId}`;
|
|
2030
|
+
this.api = new ApiClient(cfg.apiUrl, () => this.token, cfg.transport.requestTimeoutMs);
|
|
2031
|
+
}
|
|
2032
|
+
get session() {
|
|
2033
|
+
return this.info;
|
|
2034
|
+
}
|
|
2035
|
+
get tabId() {
|
|
2036
|
+
return this.info?.tabId;
|
|
2037
|
+
}
|
|
2038
|
+
get currentToken() {
|
|
2039
|
+
return this.token;
|
|
2040
|
+
}
|
|
2041
|
+
// ---- persistência -------------------------------------------------------
|
|
2042
|
+
loadPersisted() {
|
|
2043
|
+
const raw = safe(() => sessionStorage.getItem(this.storageKey));
|
|
2044
|
+
if (!raw) return null;
|
|
2045
|
+
try {
|
|
2046
|
+
const s = JSON.parse(raw);
|
|
2047
|
+
if (s.version !== STATE_VERSION || s.platformId !== this.cfg.platformId || !s.sessionId || !s.sessionToken) return null;
|
|
2048
|
+
if (s.expiresAt && s.expiresAt < Date.now()) return null;
|
|
2049
|
+
const seq = Number(safe(() => sessionStorage.getItem(this.seqKey)) ?? 0);
|
|
2050
|
+
s.sequence = Math.max(s.sequence ?? 0, Number.isFinite(seq) ? seq : 0);
|
|
2051
|
+
return s;
|
|
2052
|
+
} catch {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
persist(status, extra = {}) {
|
|
2057
|
+
if (!this.info) return;
|
|
2058
|
+
this.state = {
|
|
2059
|
+
version: STATE_VERSION,
|
|
2060
|
+
tabId: this.info.tabId,
|
|
2061
|
+
sessionId: this.info.sessionId,
|
|
2062
|
+
executionId: this.info.executionId,
|
|
2063
|
+
testCaseId: this.info.testCaseId,
|
|
2064
|
+
platformId: this.cfg.platformId,
|
|
2065
|
+
sessionToken: this.token,
|
|
2066
|
+
status,
|
|
2067
|
+
sequence: this.seq,
|
|
2068
|
+
pageCount: this.state?.pageCount ?? 0,
|
|
2069
|
+
heartbeatIntervalMs: this.state?.heartbeatIntervalMs,
|
|
2070
|
+
expiresAt: this.state?.expiresAt,
|
|
2071
|
+
updatedAt: Date.now(),
|
|
2072
|
+
...extra
|
|
2073
|
+
};
|
|
2074
|
+
safe(() => sessionStorage.setItem(this.storageKey, JSON.stringify(this.state)));
|
|
2075
|
+
}
|
|
2076
|
+
setStatus(status) {
|
|
2077
|
+
this.persist(status);
|
|
2078
|
+
}
|
|
2079
|
+
clearPersisted() {
|
|
2080
|
+
safe(() => sessionStorage.removeItem(this.storageKey));
|
|
2081
|
+
safe(() => sessionStorage.removeItem(this.seqKey));
|
|
2082
|
+
this.state = null;
|
|
2083
|
+
}
|
|
2084
|
+
// ---- sequência ----------------------------------------------------------
|
|
2085
|
+
nextSequence() {
|
|
2086
|
+
this.seq++;
|
|
2087
|
+
safe(() => sessionStorage.setItem(this.seqKey, String(this.seq)));
|
|
2088
|
+
return this.seq;
|
|
2089
|
+
}
|
|
2090
|
+
get lastSequence() {
|
|
2091
|
+
return this.seq;
|
|
2092
|
+
}
|
|
2093
|
+
/** Sobe a sequência até pelo menos `n` (ex.: maior sequência encontrada no buffer). */
|
|
2094
|
+
ensureSequenceAtLeast(n) {
|
|
2095
|
+
if (n > this.seq) this.seq = n;
|
|
2096
|
+
}
|
|
2097
|
+
newPage() {
|
|
2098
|
+
if (!this.info) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
2099
|
+
this.info = { ...this.info, pageId: uid("pg_") };
|
|
2100
|
+
this.persist(this.state?.status ?? "recording", { pageCount: (this.state?.pageCount ?? 0) + 1 });
|
|
2101
|
+
return this.info.pageId;
|
|
2102
|
+
}
|
|
2103
|
+
get pageCount() {
|
|
2104
|
+
return this.state?.pageCount ?? 0;
|
|
2105
|
+
}
|
|
2106
|
+
// ---- ciclo de vida ------------------------------------------------------
|
|
2107
|
+
/** Cria (ou obtém, de forma idempotente) a sessão no backend. */
|
|
2108
|
+
async create(opts, context) {
|
|
2109
|
+
if (!opts?.sessionToken || !opts.executionId || !opts.testCaseId)
|
|
2110
|
+
throw new AuthError("sessionToken, executionId e testCaseId s\xE3o obrigat\xF3rios");
|
|
2111
|
+
this.token = opts.sessionToken;
|
|
2112
|
+
const tabId = await resolveTabId(this.cfg.storagePrefix, this.loadPersisted()?.tabId);
|
|
2113
|
+
const res = await this.api.createSession({
|
|
2114
|
+
platformId: this.cfg.platformId,
|
|
2115
|
+
testCaseId: opts.testCaseId,
|
|
2116
|
+
executionId: opts.executionId,
|
|
2117
|
+
tabId,
|
|
2118
|
+
sdkVersion: chunkB6GO5FE5_cjs.SDK_VERSION,
|
|
2119
|
+
appVersion: this.cfg.appVersion,
|
|
2120
|
+
context
|
|
2121
|
+
});
|
|
2122
|
+
if (!res.recording?.enabled) {
|
|
2123
|
+
this.disabledReason = res.recording?.reason;
|
|
2124
|
+
throw new SessionDisabledError(res.recording?.reason);
|
|
2125
|
+
}
|
|
2126
|
+
if (res.sessionToken) this.token = res.sessionToken;
|
|
2127
|
+
this.seq = 0;
|
|
2128
|
+
this.info = {
|
|
2129
|
+
sessionId: res.sessionId,
|
|
2130
|
+
executionId: opts.executionId,
|
|
2131
|
+
testCaseId: opts.testCaseId,
|
|
2132
|
+
platformId: this.cfg.platformId,
|
|
2133
|
+
tabId,
|
|
2134
|
+
pageId: uid("pg_"),
|
|
2135
|
+
startedAt: Date.now()
|
|
2136
|
+
};
|
|
2137
|
+
safe(() => sessionStorage.removeItem(this.seqKey));
|
|
2138
|
+
this.persist("recording", {
|
|
2139
|
+
heartbeatIntervalMs: res.heartbeatIntervalMs,
|
|
2140
|
+
expiresAt: res.expiresAt,
|
|
2141
|
+
pageCount: 1,
|
|
2142
|
+
sequence: 0
|
|
2143
|
+
});
|
|
2144
|
+
return res;
|
|
2145
|
+
}
|
|
2146
|
+
/** Reidrata sessão persistida (reload/navegação MPA) após validar no backend. */
|
|
2147
|
+
async resume(state) {
|
|
2148
|
+
this.token = state.sessionToken;
|
|
2149
|
+
const tabId = await resolveTabId(this.cfg.storagePrefix, state.tabId);
|
|
2150
|
+
const res = await this.api.status(state.sessionId);
|
|
2151
|
+
if (res.status === "expired") throw new SessionExpiredError();
|
|
2152
|
+
if (res.status === "disabled") throw new SessionDisabledError();
|
|
2153
|
+
if (res.sessionToken) this.token = res.sessionToken;
|
|
2154
|
+
this.state = { ...state, tabId };
|
|
2155
|
+
this.seq = tabId === state.tabId ? state.sequence : 0;
|
|
2156
|
+
this.info = {
|
|
2157
|
+
sessionId: state.sessionId,
|
|
2158
|
+
executionId: state.executionId,
|
|
2159
|
+
testCaseId: state.testCaseId,
|
|
2160
|
+
platformId: state.platformId,
|
|
2161
|
+
tabId,
|
|
2162
|
+
pageId: uid("pg_"),
|
|
2163
|
+
startedAt: Date.now()
|
|
2164
|
+
};
|
|
2165
|
+
this.persist(state.status, { sessionToken: this.token, expiresAt: res.expiresAt ?? state.expiresAt, pageCount: state.pageCount + 1, tabId });
|
|
2166
|
+
return res;
|
|
2167
|
+
}
|
|
2168
|
+
startHeartbeat(onResult, onError, pending, status) {
|
|
2169
|
+
this.stopHeartbeat();
|
|
2170
|
+
const every = this.state?.heartbeatIntervalMs ?? this.cfg.heartbeatIntervalMs;
|
|
2171
|
+
if (!every || every <= 0) return;
|
|
2172
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
2173
|
+
if (!this.info) return;
|
|
2174
|
+
try {
|
|
2175
|
+
const r = await this.api.heartbeat(this.info.sessionId, {
|
|
2176
|
+
tabId: this.info.tabId,
|
|
2177
|
+
pageId: this.info.pageId,
|
|
2178
|
+
status: status(),
|
|
2179
|
+
pending: pending(),
|
|
2180
|
+
lastSequence: this.seq
|
|
2181
|
+
});
|
|
2182
|
+
if (r.sessionToken) {
|
|
2183
|
+
this.token = r.sessionToken;
|
|
2184
|
+
this.persist(this.state?.status ?? "recording", { sessionToken: r.sessionToken, expiresAt: r.expiresAt });
|
|
2185
|
+
}
|
|
2186
|
+
onResult(r);
|
|
2187
|
+
} catch (e) {
|
|
2188
|
+
if (e instanceof AuthError || e instanceof SessionExpiredError) onError(e);
|
|
2189
|
+
}
|
|
2190
|
+
}, every);
|
|
2191
|
+
}
|
|
2192
|
+
stopHeartbeat() {
|
|
2193
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
2194
|
+
this.heartbeatTimer = void 0;
|
|
2195
|
+
}
|
|
2196
|
+
async complete(summary) {
|
|
2197
|
+
if (!this.info) return false;
|
|
2198
|
+
const r = await this.api.complete(this.info.sessionId, { tabId: this.info.tabId, lastSequence: this.seq, ...summary });
|
|
2199
|
+
return !!r.completed;
|
|
2200
|
+
}
|
|
2201
|
+
reset() {
|
|
2202
|
+
this.stopHeartbeat();
|
|
2203
|
+
this.info = null;
|
|
2204
|
+
this.state = null;
|
|
2205
|
+
this.token = "";
|
|
2206
|
+
this.seq = 0;
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var heldLocks = [];
|
|
2210
|
+
async function resolveTabId(prefix, previous) {
|
|
2211
|
+
const candidate = previous ?? uid("tab_");
|
|
2212
|
+
const locks = globalThis.navigator?.locks;
|
|
2213
|
+
if (!locks || !previous) {
|
|
2214
|
+
if (locks) void claim(locks, `${prefix}:tab:${candidate}`);
|
|
2215
|
+
return candidate;
|
|
2216
|
+
}
|
|
2217
|
+
if (heldLocks.includes(candidate)) return candidate;
|
|
2218
|
+
const ok = await claim(locks, `${prefix}:tab:${candidate}`);
|
|
2219
|
+
if (ok) return candidate;
|
|
2220
|
+
const fresh = uid("tab_");
|
|
2221
|
+
void claim(locks, `${prefix}:tab:${fresh}`);
|
|
2222
|
+
return fresh;
|
|
2223
|
+
}
|
|
2224
|
+
function claim(locks, name) {
|
|
2225
|
+
return new Promise((resolve) => {
|
|
2226
|
+
try {
|
|
2227
|
+
void locks.request(name, { ifAvailable: true }, (lock) => {
|
|
2228
|
+
if (!lock) {
|
|
2229
|
+
resolve(false);
|
|
2230
|
+
return void 0;
|
|
2231
|
+
}
|
|
2232
|
+
heldLocks.push(name.split(":tab:")[1]);
|
|
2233
|
+
resolve(true);
|
|
2234
|
+
return new Promise(() => void 0);
|
|
2235
|
+
}).catch(() => resolve(true));
|
|
2236
|
+
} catch {
|
|
2237
|
+
resolve(true);
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/core/config.ts
|
|
2243
|
+
function resolveConfig(input) {
|
|
2244
|
+
if (!input || typeof input !== "object") throw new ConfigError("Configura\xE7\xE3o ausente");
|
|
2245
|
+
if (!input.platformId) throw new ConfigError("platformId \xE9 obrigat\xF3rio");
|
|
2246
|
+
if (!input.apiUrl) throw new ConfigError("apiUrl \xE9 obrigat\xF3rio");
|
|
2247
|
+
let url;
|
|
2248
|
+
try {
|
|
2249
|
+
url = new URL(input.apiUrl);
|
|
2250
|
+
} catch {
|
|
2251
|
+
throw new ConfigError(`apiUrl inv\xE1lida: ${input.apiUrl}`);
|
|
2252
|
+
}
|
|
2253
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
2254
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
2255
|
+
throw new ConfigError("apiUrl deve usar HTTPS (HTTP s\xF3 \xE9 aceito em localhost)");
|
|
2256
|
+
return {
|
|
2257
|
+
apiUrl: url.origin + url.pathname.replace(/\/+$/, ""),
|
|
2258
|
+
platformId: input.platformId,
|
|
2259
|
+
appVersion: input.appVersion,
|
|
2260
|
+
recording: deepMerge(DEFAULT_RECORDING, input.recording),
|
|
2261
|
+
privacy: resolvePrivacy(input.privacy),
|
|
2262
|
+
transport: deepMerge(DEFAULT_TRANSPORT, input.transport),
|
|
2263
|
+
correlationWindowMs: input.correlationWindowMs ?? 3e3,
|
|
2264
|
+
autoResume: input.autoResume ?? true,
|
|
2265
|
+
heartbeatIntervalMs: input.heartbeatIntervalMs ?? 3e4,
|
|
2266
|
+
storagePrefix: input.storagePrefix ?? "offscript",
|
|
2267
|
+
debug: input.debug ?? false
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// src/core/OffscriptSDK.ts
|
|
2272
|
+
var MAX_LOG = 5e3;
|
|
2273
|
+
var Offscript = class {
|
|
2274
|
+
constructor(config) {
|
|
2275
|
+
this.bus = new Emitter();
|
|
2276
|
+
this.status = "idle";
|
|
2277
|
+
this.recorders = [];
|
|
2278
|
+
this.accepting = false;
|
|
2279
|
+
this.destroyed = false;
|
|
2280
|
+
this.recent = [];
|
|
2281
|
+
this.actionLog = [];
|
|
2282
|
+
this.contextLog = [];
|
|
2283
|
+
this.eventCount = 0;
|
|
2284
|
+
this.metrics = { emitMs: 0, emitted: 0, dropped: 0 };
|
|
2285
|
+
this.config = resolveConfig(config);
|
|
2286
|
+
this.sanitizer = new DataSanitizer(this.config.privacy);
|
|
2287
|
+
this.sessions = new SessionManager(this.config);
|
|
2288
|
+
}
|
|
2289
|
+
// ------------------------------------------------------------------ API pública
|
|
2290
|
+
getState() {
|
|
2291
|
+
return { status: this.status, session: this.sessions.session, pending: this.transport?.pending ?? 0, lastError: this.lastError };
|
|
2292
|
+
}
|
|
2293
|
+
getMetrics() {
|
|
2294
|
+
return { emitted: this.metrics.emitted, dropped: this.metrics.dropped, avgEmitMs: this.metrics.emitted ? this.metrics.emitMs / this.metrics.emitted : 0, bufferKind: this.transport?.bufferKind };
|
|
2295
|
+
}
|
|
2296
|
+
/** Idempotente. Prepara armazenamento/transporte e retoma sessão persistida (reload/MPA). */
|
|
2297
|
+
init() {
|
|
2298
|
+
if (this.destroyed) return Promise.reject(new InvalidStateError("SDK destru\xEDdo"));
|
|
2299
|
+
this.initPromise ?? (this.initPromise = this.doInit());
|
|
2300
|
+
return this.initPromise;
|
|
2301
|
+
}
|
|
2302
|
+
async doInit() {
|
|
2303
|
+
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um ambiente de navegador");
|
|
2304
|
+
this.setStatus("initializing");
|
|
2305
|
+
try {
|
|
2306
|
+
const buffer = await openBuffer(`${this.config.storagePrefix}-${this.config.platformId}`, this.config.transport.persist);
|
|
2307
|
+
this.transport = new EventTransport(this.config.transport, this.sessions.api, buffer);
|
|
2308
|
+
this.transport.on("flushed", (e) => this.bus.emit("flush", e));
|
|
2309
|
+
this.transport.on("overflow", (e) => this.bus.emit("overflow", e));
|
|
2310
|
+
this.transport.on("fatal", ({ error }) => void this.onFatal(error));
|
|
2311
|
+
this.transport.on("failed", ({ error }) => this.debug("transport failed", error));
|
|
2312
|
+
await this.transport.purgeExpired(this.config.privacy.localRetentionMs);
|
|
2313
|
+
this.setStatus("idle");
|
|
2314
|
+
const persisted = this.config.autoResume ? this.sessions.loadPersisted() : null;
|
|
2315
|
+
if (persisted) await this.resumePersisted(persisted);
|
|
2316
|
+
} catch (e) {
|
|
2317
|
+
this.fail(e);
|
|
2318
|
+
throw e;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
async startRecording(opts) {
|
|
2322
|
+
if (this.destroyed) throw new InvalidStateError("SDK destru\xEDdo");
|
|
2323
|
+
if (this.startPromise) return this.startPromise;
|
|
2324
|
+
const p = this.doStart(opts).finally(() => this.startPromise = void 0);
|
|
2325
|
+
this.startPromise = p;
|
|
2326
|
+
return p;
|
|
2327
|
+
}
|
|
2328
|
+
async doStart(opts) {
|
|
2329
|
+
await this.init();
|
|
2330
|
+
const cur = this.sessions.session;
|
|
2331
|
+
if ((this.status === "recording" || this.status === "paused") && cur) {
|
|
2332
|
+
if (cur.executionId === opts.executionId) return cur;
|
|
2333
|
+
throw new InvalidStateError(`J\xE1 existe grava\xE7\xE3o ativa para a execu\xE7\xE3o ${cur.executionId}. Encerre-a antes.`);
|
|
2334
|
+
}
|
|
2335
|
+
if (this.status === "stopping") throw new InvalidStateError("Grava\xE7\xE3o em encerramento");
|
|
2336
|
+
this.setStatus("initializing");
|
|
2337
|
+
try {
|
|
2338
|
+
const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion);
|
|
2339
|
+
await this.sessions.create(opts, { ...ctx, url: this.sanitizer.sanitizeUrl(ctx.url), referrer: void 0, title: void 0 });
|
|
2340
|
+
const s = this.sessions.session;
|
|
2341
|
+
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
2342
|
+
this.attach();
|
|
2343
|
+
this.accepting = true;
|
|
2344
|
+
this.setStatus("recording");
|
|
2345
|
+
this.emitLifecycle("session-start", { pageCount: 1 }, "critical");
|
|
2346
|
+
await this.startRecorders();
|
|
2347
|
+
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
2348
|
+
this.bus.emit("sessionStart", s);
|
|
2349
|
+
return s;
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
await this.teardown();
|
|
2352
|
+
this.sessions.clearPersisted();
|
|
2353
|
+
this.sessions.reset();
|
|
2354
|
+
this.fail(e);
|
|
2355
|
+
throw e;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
async resumePersisted(state) {
|
|
2359
|
+
this.setStatus("initializing");
|
|
2360
|
+
try {
|
|
2361
|
+
await this.sessions.resume(state);
|
|
2362
|
+
const s = this.sessions.session;
|
|
2363
|
+
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
2364
|
+
this.attach();
|
|
2365
|
+
this.accepting = state.status === "recording";
|
|
2366
|
+
this.setStatus(state.status);
|
|
2367
|
+
this.emitLifecycle("session-resume", { pageCount: this.sessions.pageCount }, "critical");
|
|
2368
|
+
this.emitLifecycle("page-start", { navigationType: safe(() => performance.getEntriesByType("navigation")[0]?.type) }, "critical");
|
|
2369
|
+
await this.startRecorders();
|
|
2370
|
+
if (state.status === "paused") this.recorders.forEach((r) => r.pause?.());
|
|
2371
|
+
this.actions?.recordPageLoad(safe(() => performance.getEntriesByType("navigation")[0]?.type), document.referrer);
|
|
2372
|
+
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
2373
|
+
this.bus.emit("sessionStart", s);
|
|
2374
|
+
} catch (e) {
|
|
2375
|
+
await this.teardown();
|
|
2376
|
+
this.sessions.clearPersisted();
|
|
2377
|
+
this.sessions.reset();
|
|
2378
|
+
this.fail(e);
|
|
2379
|
+
if (!(e instanceof SessionExpiredError || e instanceof SessionDisabledError || e instanceof AuthError)) throw e;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
async pauseRecording() {
|
|
2383
|
+
if (this.status === "paused") return;
|
|
2384
|
+
if (this.status !== "recording") throw new InvalidStateError(`N\xE3o \xE9 poss\xEDvel pausar no estado ${this.status}`);
|
|
2385
|
+
this.actions?.flushAll();
|
|
2386
|
+
this.emitLifecycle("pause", {}, "critical");
|
|
2387
|
+
this.accepting = false;
|
|
2388
|
+
this.recorders.forEach((r) => safe(() => r.pause?.()));
|
|
2389
|
+
this.sessions.setStatus("paused");
|
|
2390
|
+
this.setStatus("paused");
|
|
2391
|
+
void this.transport?.flush();
|
|
2392
|
+
}
|
|
2393
|
+
async resumeRecording() {
|
|
2394
|
+
if (this.status === "recording") return;
|
|
2395
|
+
if (this.status !== "paused") throw new InvalidStateError(`N\xE3o \xE9 poss\xEDvel retomar no estado ${this.status}`);
|
|
2396
|
+
this.accepting = true;
|
|
2397
|
+
this.sessions.setStatus("recording");
|
|
2398
|
+
this.setStatus("recording");
|
|
2399
|
+
this.emitLifecycle("resume", {}, "critical");
|
|
2400
|
+
for (const r of this.recorders) await Promise.resolve(safe(() => r.resume?.()));
|
|
2401
|
+
}
|
|
2402
|
+
stopRecording() {
|
|
2403
|
+
if (this.stopPromise) return this.stopPromise;
|
|
2404
|
+
const s = this.sessions.session;
|
|
2405
|
+
if (!s || this.status !== "recording" && this.status !== "paused" && this.status !== "error") {
|
|
2406
|
+
if (this.status === "stopped" || this.status === "idle")
|
|
2407
|
+
return Promise.resolve({ sessionId: s?.sessionId ?? "", flushed: true, pendingEvents: 0, completed: true });
|
|
2408
|
+
return Promise.reject(new InvalidStateError(`N\xE3o \xE9 poss\xEDvel encerrar no estado ${this.status}`));
|
|
2409
|
+
}
|
|
2410
|
+
this.stopPromise = this.doStop().finally(() => this.stopPromise = void 0);
|
|
2411
|
+
return this.stopPromise;
|
|
2412
|
+
}
|
|
2413
|
+
async doStop() {
|
|
2414
|
+
const session = this.sessions.session;
|
|
2415
|
+
this.setStatus("stopping");
|
|
2416
|
+
if (this.status === "stopping") {
|
|
2417
|
+
this.actions?.flushAll();
|
|
2418
|
+
this.emitLifecycle("stop", { eventCount: this.eventCount }, "critical");
|
|
2419
|
+
}
|
|
2420
|
+
this.accepting = false;
|
|
2421
|
+
this.stopRecorders();
|
|
2422
|
+
this.sessions.stopHeartbeat();
|
|
2423
|
+
const { ok, pending } = await this.transport?.flushFinal() ?? { ok: true, pending: 0 };
|
|
2424
|
+
let completed = false;
|
|
2425
|
+
if (ok) {
|
|
2426
|
+
try {
|
|
2427
|
+
completed = await this.sessions.complete({ eventCount: this.eventCount, pages: this.sessions.pageCount });
|
|
2428
|
+
} catch (e) {
|
|
2429
|
+
this.bus.emit("error", e);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (ok) await this.transport?.clearSession(session.sessionId);
|
|
2433
|
+
this.transport?.stop();
|
|
2434
|
+
this.lifecycle?.uninstall();
|
|
2435
|
+
this.sessions.clearPersisted();
|
|
2436
|
+
this.sessions.reset();
|
|
2437
|
+
const summary = { sessionId: session.sessionId, flushed: ok, pendingEvents: pending, completed };
|
|
2438
|
+
this.setStatus("stopped");
|
|
2439
|
+
this.bus.emit("sessionEnd", summary);
|
|
2440
|
+
return summary;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Libera todos os recursos. Por padrão encerra a gravação ativa; com `{ complete: false }` apenas
|
|
2444
|
+
* desanexa a instrumentação e mantém a sessão persistida para ser retomada por outra instância.
|
|
2445
|
+
*/
|
|
2446
|
+
async destroy(opts = {}) {
|
|
2447
|
+
if (this.destroyed) return;
|
|
2448
|
+
if (opts.complete !== false && (this.status === "recording" || this.status === "paused")) {
|
|
2449
|
+
await this.stopRecording().catch(() => void 0);
|
|
2450
|
+
} else {
|
|
2451
|
+
this.actions?.flushAll();
|
|
2452
|
+
if (this.accepting) this.emitLifecycle("page-end", {}, "critical");
|
|
2453
|
+
this.accepting = false;
|
|
2454
|
+
await this.transport?.flushFinal(2e3).catch(() => void 0);
|
|
2455
|
+
}
|
|
2456
|
+
this.destroyed = true;
|
|
2457
|
+
await this.teardown();
|
|
2458
|
+
this.sessions.reset();
|
|
2459
|
+
this.transport?.close();
|
|
2460
|
+
this.bus.clear();
|
|
2461
|
+
}
|
|
2462
|
+
/** Dados estruturados para o motor Playwright, construídos a partir do que foi gravado nesta página. */
|
|
2463
|
+
exportPlaywrightData() {
|
|
2464
|
+
const s = this.sessions.session;
|
|
2465
|
+
if (!s) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
2466
|
+
return chunkB6GO5FE5_cjs.buildPlaywrightContract({
|
|
2467
|
+
session: { sessionId: s.sessionId, executionId: s.executionId, testCaseId: s.testCaseId, platformId: s.platformId },
|
|
2468
|
+
actions: this.actionLog,
|
|
2469
|
+
contexts: this.contextLog.map((e) => e.data)
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
getRecordedActions() {
|
|
2473
|
+
return this.actionLog;
|
|
2474
|
+
}
|
|
2475
|
+
// ------------------------------------------------------------------ EventSink
|
|
2476
|
+
on(type, fn) {
|
|
2477
|
+
return this.bus.on(type, fn);
|
|
2478
|
+
}
|
|
2479
|
+
off(type, fn) {
|
|
2480
|
+
this.bus.off(type, fn);
|
|
2481
|
+
}
|
|
2482
|
+
/** Usado pelos recorders. Não confundir com os eventos públicos (`on`). */
|
|
2483
|
+
emit(kind, data, opts = {}) {
|
|
2484
|
+
return this.record(kind, data, opts);
|
|
2485
|
+
}
|
|
2486
|
+
record(kind, data, opts) {
|
|
2487
|
+
const t0 = typeof performance !== "undefined" ? performance.now() : 0;
|
|
2488
|
+
try {
|
|
2489
|
+
const session = this.sessions.session;
|
|
2490
|
+
const transport = this.transport;
|
|
2491
|
+
if (!this.accepting || !session || !transport) return null;
|
|
2492
|
+
const priority = opts.priority ?? "normal";
|
|
2493
|
+
if (!transport.canAccept(priority)) {
|
|
2494
|
+
transport.noteDropped();
|
|
2495
|
+
this.metrics.dropped++;
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
this.rrweb?.recoverIfNeeded();
|
|
2499
|
+
const sequence = this.sessions.nextSequence();
|
|
2500
|
+
const timestamp = opts.at ?? Date.now();
|
|
2501
|
+
const id = `${session.tabId}:${sequence}`;
|
|
2502
|
+
let payload = data;
|
|
2503
|
+
let relatedActionId;
|
|
2504
|
+
let relatedActionDeltaMs;
|
|
2505
|
+
if (kind === "action") {
|
|
2506
|
+
const action = { ...data, id, sessionId: session.sessionId, executionId: session.executionId, sequence, timestamp };
|
|
2507
|
+
payload = action;
|
|
2508
|
+
this.recent.push({ id, t: timestamp });
|
|
2509
|
+
if (this.recent.length > 50) this.recent.shift();
|
|
2510
|
+
this.actionLog.push(action);
|
|
2511
|
+
if (this.actionLog.length > MAX_LOG) this.actionLog.shift();
|
|
2512
|
+
} else if (kind === "http" || kind === "console" || kind === "error" || kind === "iframe") {
|
|
2513
|
+
const rel = this.correlate(timestamp);
|
|
2514
|
+
relatedActionId = rel?.id;
|
|
2515
|
+
relatedActionDeltaMs = rel ? timestamp - rel.t : void 0;
|
|
2516
|
+
}
|
|
2517
|
+
const event = {
|
|
2518
|
+
id,
|
|
2519
|
+
kind,
|
|
2520
|
+
sessionId: session.sessionId,
|
|
2521
|
+
executionId: session.executionId,
|
|
2522
|
+
tabId: session.tabId,
|
|
2523
|
+
pageId: session.pageId,
|
|
2524
|
+
sequence,
|
|
2525
|
+
timestamp,
|
|
2526
|
+
relatedActionId,
|
|
2527
|
+
relatedActionDeltaMs,
|
|
2528
|
+
data: payload
|
|
2529
|
+
};
|
|
2530
|
+
if (kind === "context") {
|
|
2531
|
+
this.contextLog.push(event);
|
|
2532
|
+
if (this.contextLog.length > 50) this.contextLog.shift();
|
|
2533
|
+
}
|
|
2534
|
+
this.eventCount++;
|
|
2535
|
+
transport.enqueue(event);
|
|
2536
|
+
this.metrics.emitted++;
|
|
2537
|
+
if (this.config.debug) this.debug("event", kind, sequence);
|
|
2538
|
+
return event;
|
|
2539
|
+
} catch (e) {
|
|
2540
|
+
this.debug("emit failed", e);
|
|
2541
|
+
return null;
|
|
2542
|
+
} finally {
|
|
2543
|
+
if (t0) this.metrics.emitMs += performance.now() - t0;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
correlate(at) {
|
|
2547
|
+
for (let i = this.recent.length - 1; i >= 0; i--) {
|
|
2548
|
+
const a = this.recent[i];
|
|
2549
|
+
if (a.t <= at) return at - a.t <= this.config.correlationWindowMs ? a : void 0;
|
|
2550
|
+
}
|
|
2551
|
+
return void 0;
|
|
2552
|
+
}
|
|
2553
|
+
// ------------------------------------------------------------------ internos
|
|
2554
|
+
emitLifecycle(type, detail = {}, priority = "normal") {
|
|
2555
|
+
this.record("lifecycle", { type, detail }, { priority });
|
|
2556
|
+
}
|
|
2557
|
+
attach() {
|
|
2558
|
+
this.lifecycle ?? (this.lifecycle = new LifecycleManager({
|
|
2559
|
+
onPageHide: () => {
|
|
2560
|
+
if (!this.accepting) return;
|
|
2561
|
+
this.actions?.flushAll();
|
|
2562
|
+
this.emitLifecycle("page-end", { url: this.sanitizer.sanitizeUrl(location.href) }, "critical");
|
|
2563
|
+
this.transport?.flushOnUnload();
|
|
2564
|
+
},
|
|
2565
|
+
onVisibility: (hidden) => {
|
|
2566
|
+
if (hidden && this.accepting) {
|
|
2567
|
+
this.actions?.flushAll();
|
|
2568
|
+
void this.transport?.flush();
|
|
2569
|
+
}
|
|
2570
|
+
},
|
|
2571
|
+
onConnectivity: (online) => this.transport?.setOnline(online),
|
|
2572
|
+
onNavigation: ({ from, to, kind }) => {
|
|
2573
|
+
if (!this.accepting) return;
|
|
2574
|
+
this.actions?.recordNavigation(from, to, kind);
|
|
2575
|
+
this.emitLifecycle("navigation", { from: this.sanitizer.sanitizeUrl(from), to: this.sanitizer.sanitizeUrl(to), kind }, "critical");
|
|
2576
|
+
this.context?.record("navigation");
|
|
2577
|
+
}
|
|
2578
|
+
}));
|
|
2579
|
+
this.lifecycle.install();
|
|
2580
|
+
this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
|
|
2581
|
+
}
|
|
2582
|
+
async startRecorders() {
|
|
2583
|
+
const r = this.config.recording;
|
|
2584
|
+
const list = [];
|
|
2585
|
+
if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
|
|
2586
|
+
if (r.context) list.push(this.context = new ContextRecorder(this, () => ({ platformId: this.config.platformId, testCaseId: this.sessions.session.testCaseId, executionId: this.sessions.session.executionId })));
|
|
2587
|
+
if (r.actions) list.push(this.actions = new ActionRecorder(this));
|
|
2588
|
+
if (r.http) list.push(new HTTPRecorder(this, this.config.apiUrl));
|
|
2589
|
+
if (r.console || r.errors) list.push(new ErrorRecorder(this));
|
|
2590
|
+
if (r.iframes) list.push(new IframeRecorder(this));
|
|
2591
|
+
this.recorders = list;
|
|
2592
|
+
for (const rec of list) {
|
|
2593
|
+
try {
|
|
2594
|
+
await rec.start();
|
|
2595
|
+
} catch (e) {
|
|
2596
|
+
this.emitLifecycle("recorder-error", { recorder: rec.name, message: String(e?.message ?? e) });
|
|
2597
|
+
this.bus.emit("error", e);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
stopRecorders() {
|
|
2602
|
+
for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
|
|
2603
|
+
this.rrweb = this.actions = this.context = void 0;
|
|
2604
|
+
}
|
|
2605
|
+
async teardown() {
|
|
2606
|
+
this.accepting = false;
|
|
2607
|
+
this.stopRecorders();
|
|
2608
|
+
this.sessions.stopHeartbeat();
|
|
2609
|
+
this.lifecycle?.uninstall();
|
|
2610
|
+
this.lifecycle = void 0;
|
|
2611
|
+
this.transport?.stop();
|
|
2612
|
+
}
|
|
2613
|
+
onHeartbeat(status) {
|
|
2614
|
+
if (status === "disabled") void this.shutdownRemote(new SessionDisabledError());
|
|
2615
|
+
else if (status === "expired") void this.onFatal(new SessionExpiredError());
|
|
2616
|
+
}
|
|
2617
|
+
/** Backend desativou a gravação: encerra com flush de melhor esforço. */
|
|
2618
|
+
async shutdownRemote(err) {
|
|
2619
|
+
if (this.status !== "recording" && this.status !== "paused") return;
|
|
2620
|
+
this.accepting = false;
|
|
2621
|
+
this.stopRecorders();
|
|
2622
|
+
this.sessions.stopHeartbeat();
|
|
2623
|
+
const session = this.sessions.session;
|
|
2624
|
+
await this.transport?.flushFinal(3e3).catch(() => void 0);
|
|
2625
|
+
this.transport?.stop();
|
|
2626
|
+
this.lifecycle?.uninstall();
|
|
2627
|
+
this.sessions.clearPersisted();
|
|
2628
|
+
this.sessions.reset();
|
|
2629
|
+
this.setStatus("stopped");
|
|
2630
|
+
this.bus.emit("disabled", { reason: err.message });
|
|
2631
|
+
if (session) this.bus.emit("sessionEnd", { sessionId: session.sessionId, flushed: false, pendingEvents: this.transport?.pending ?? 0, completed: false });
|
|
2632
|
+
}
|
|
2633
|
+
async onFatal(err) {
|
|
2634
|
+
if (this.status === "error" || this.status === "stopped") return;
|
|
2635
|
+
await this.teardown();
|
|
2636
|
+
this.sessions.clearPersisted();
|
|
2637
|
+
this.sessions.reset();
|
|
2638
|
+
this.fail(err);
|
|
2639
|
+
}
|
|
2640
|
+
fail(e) {
|
|
2641
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
2642
|
+
this.lastError = { code: err.code ?? "INTERNAL", message: err.message };
|
|
2643
|
+
this.setStatus("error");
|
|
2644
|
+
this.bus.emit("error", err);
|
|
2645
|
+
}
|
|
2646
|
+
setStatus(to) {
|
|
2647
|
+
const from = this.status;
|
|
2648
|
+
if (from === to) return;
|
|
2649
|
+
this.status = to;
|
|
2650
|
+
this.bus.emit("statusChange", { from, to });
|
|
2651
|
+
}
|
|
2652
|
+
debug(...args) {
|
|
2653
|
+
if (this.config.debug) console.debug("[offscript]", ...args);
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
Offscript.version = chunkB6GO5FE5_cjs.SDK_VERSION;
|
|
2657
|
+
|
|
2658
|
+
exports.AuthError = AuthError;
|
|
2659
|
+
exports.ConfigError = ConfigError;
|
|
2660
|
+
exports.DEFAULT_PRIVACY = DEFAULT_PRIVACY;
|
|
2661
|
+
exports.DEFAULT_RECORDING = DEFAULT_RECORDING;
|
|
2662
|
+
exports.DEFAULT_TRANSPORT = DEFAULT_TRANSPORT;
|
|
2663
|
+
exports.DataSanitizer = DataSanitizer;
|
|
2664
|
+
exports.InvalidStateError = InvalidStateError;
|
|
2665
|
+
exports.Offscript = Offscript;
|
|
2666
|
+
exports.OffscriptError = OffscriptError;
|
|
2667
|
+
exports.SessionDisabledError = SessionDisabledError;
|
|
2668
|
+
exports.SessionExpiredError = SessionExpiredError;
|
|
2669
|
+
exports.TransportError = TransportError;
|
|
2670
|
+
//# sourceMappingURL=chunk-3NX2AGXM.cjs.map
|
|
2671
|
+
//# sourceMappingURL=chunk-3NX2AGXM.cjs.map
|