@offscript/sdk 0.1.1 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -119
- package/dist/adapters/nuxt.cjs +3 -3
- package/dist/adapters/nuxt.js +2 -2
- package/dist/adapters/vanilla.cjs +5 -5
- package/dist/adapters/vanilla.d.ts +2 -3
- package/dist/adapters/vanilla.js +3 -3
- package/dist/adapters/vue.cjs +3 -3
- package/dist/adapters/vue.js +2 -2
- package/dist/chunk-2MY2FWIA.cjs +20 -0
- package/dist/chunk-2MY2FWIA.cjs.map +1 -0
- package/dist/{chunk-LFXHB4PN.js → chunk-6ROP37WI.js} +5 -6
- package/dist/chunk-6ROP37WI.js.map +1 -0
- package/dist/{chunk-B6GO5FE5.cjs → chunk-HPDLEAAL.cjs} +5 -6
- package/dist/chunk-HPDLEAAL.cjs.map +1 -0
- package/dist/{chunk-MBYNQSKA.js → chunk-V2YYWGIA.js} +5 -5
- package/dist/chunk-V2YYWGIA.js.map +1 -0
- package/dist/{chunk-3NX2AGXM.cjs → chunk-VYCDQLP7.cjs} +1084 -1170
- package/dist/chunk-VYCDQLP7.cjs.map +1 -0
- package/dist/{chunk-3OSCH7J2.js → chunk-XBXFGCUT.js} +1081 -1162
- package/dist/chunk-XBXFGCUT.js.map +1 -0
- package/dist/core/LifecycleManager.d.ts +1 -0
- package/dist/core/OffscriptSDK.d.ts +34 -52
- package/dist/core/RecordingStore.d.ts +26 -0
- package/dist/index.cjs +20 -40
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/playwright/PlaywrightDataContract.d.ts +0 -7
- package/dist/playwright.cjs +13 -13
- package/dist/playwright.js +1 -1
- package/dist/privacy/DataSanitizer.d.ts +1 -0
- package/dist/recorders/ActionRecorder.d.ts +1 -1
- package/dist/recorders/ContextRecorder.d.ts +2 -8
- package/dist/recorders/HTTPRecorder.d.ts +7 -2
- package/dist/types/config.d.ts +13 -41
- package/dist/types/events.d.ts +4 -23
- package/dist/types/recording.d.ts +20 -0
- package/dist/ui/RecordingControls.d.ts +62 -0
- package/dist/utils/errors.d.ts +1 -18
- package/docs/api-contract.md +4 -53
- package/docs/architecture.md +5 -34
- package/docs/event-contract.md +3 -24
- package/docs/playwright-contract.md +3 -37
- package/package.json +2 -2
- package/dist/chunk-3NX2AGXM.cjs.map +0 -1
- package/dist/chunk-3OSCH7J2.js.map +0 -1
- package/dist/chunk-B6GO5FE5.cjs.map +0 -1
- package/dist/chunk-HAPNSHFG.cjs +0 -20
- package/dist/chunk-HAPNSHFG.cjs.map +0 -1
- package/dist/chunk-LFXHB4PN.js.map +0 -1
- package/dist/chunk-MBYNQSKA.js.map +0 -1
- package/dist/core/ApiClient.d.ts +0 -24
- package/dist/core/SessionManager.d.ts +0 -44
- package/dist/transport/EventQueue.d.ts +0 -28
- package/dist/transport/EventTransport.d.ts +0 -84
- package/dist/transport/PersistentBuffer.d.ts +0 -60
- package/dist/transport/RetryManager.d.ts +0 -9
- package/dist/types/session.d.ts +0 -77
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkHPDLEAAL_cjs = require('./chunk-HPDLEAAL.cjs');
|
|
4
4
|
|
|
5
5
|
// src/types/config.ts
|
|
6
6
|
var DEFAULT_RECORDING = {
|
|
@@ -20,7 +20,7 @@ var DEFAULT_RECORDING = {
|
|
|
20
20
|
stableAttributes: ["name", "data-id", "aria-label"]
|
|
21
21
|
};
|
|
22
22
|
var DEFAULT_PRIVACY = {
|
|
23
|
-
maskSensitiveInputs:
|
|
23
|
+
maskSensitiveInputs: false,
|
|
24
24
|
maskAllText: false,
|
|
25
25
|
captureRequestBodies: false,
|
|
26
26
|
captureResponseBodies: false,
|
|
@@ -32,23 +32,9 @@ var DEFAULT_PRIVACY = {
|
|
|
32
32
|
ignoreUrls: [],
|
|
33
33
|
redactHeaders: [],
|
|
34
34
|
redactKeys: [],
|
|
35
|
+
captureSensitiveValues: false,
|
|
35
36
|
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
|
|
37
|
+
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i]
|
|
52
38
|
};
|
|
53
39
|
|
|
54
40
|
// src/utils/misc.ts
|
|
@@ -87,13 +73,6 @@ function debounce(fn, ms) {
|
|
|
87
73
|
};
|
|
88
74
|
return d;
|
|
89
75
|
}
|
|
90
|
-
function approxSize(value) {
|
|
91
|
-
try {
|
|
92
|
-
return JSON.stringify(value)?.length ?? 0;
|
|
93
|
-
} catch {
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
76
|
function deepMerge(base, patch) {
|
|
98
77
|
const out = { ...base };
|
|
99
78
|
if (!patch) return out;
|
|
@@ -161,13 +140,15 @@ var DataSanitizer = class {
|
|
|
161
140
|
this.cfg = cfg;
|
|
162
141
|
this.headerSet = /* @__PURE__ */ new Set([...BASE_SENSITIVE_HEADERS, ...cfg.redactHeaders.map((h) => h.toLowerCase())]);
|
|
163
142
|
this.extraKeys = new Set(cfg.redactKeys.map((k) => k.toLowerCase()));
|
|
143
|
+
this.open = cfg.captureSensitiveValues === true;
|
|
164
144
|
}
|
|
165
145
|
isSensitiveKey(key) {
|
|
146
|
+
if (this.open) return false;
|
|
166
147
|
return BASE_SENSITIVE_KEY_PATTERN.test(key) || this.extraKeys.has(key.toLowerCase());
|
|
167
148
|
}
|
|
168
149
|
/** Mascara padrões sensíveis em texto livre (JWT, Bearer, CPF, CNPJ, cartões). */
|
|
169
150
|
scrubString(input) {
|
|
170
|
-
if (!input) return input;
|
|
151
|
+
if (this.open || !input) return input;
|
|
171
152
|
let s = input.replace(JWT, REDACTED).replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
|
|
172
153
|
s = s.replace(CNPJ, REDACTED).replace(CPF, REDACTED);
|
|
173
154
|
s = s.replace(CARD, (m) => {
|
|
@@ -177,6 +158,7 @@ var DataSanitizer = class {
|
|
|
177
158
|
return s;
|
|
178
159
|
}
|
|
179
160
|
sanitizeHeaders(headers) {
|
|
161
|
+
if (this.open) return { ...headers };
|
|
180
162
|
const out = {};
|
|
181
163
|
for (const [k, v] of Object.entries(headers)) {
|
|
182
164
|
out[k] = this.headerSet.has(k.toLowerCase()) || this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
|
|
@@ -185,6 +167,7 @@ var DataSanitizer = class {
|
|
|
185
167
|
}
|
|
186
168
|
/** Redige query params sensíveis e credenciais embutidas (user:pass@). */
|
|
187
169
|
sanitizeUrl(raw, base) {
|
|
170
|
+
if (this.open) return raw;
|
|
188
171
|
try {
|
|
189
172
|
const u = new URL(raw, base ?? (typeof location !== "undefined" ? location.href : void 0));
|
|
190
173
|
if (u.username || u.password) {
|
|
@@ -203,7 +186,7 @@ var DataSanitizer = class {
|
|
|
203
186
|
}
|
|
204
187
|
/** Redige query/fragmento de um valor de atributo de URL preservando a forma original (relativa ou absoluta). */
|
|
205
188
|
redactUrlAttribute(value) {
|
|
206
|
-
if (typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
189
|
+
if (this.open || typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
207
190
|
const hashAt = value.indexOf("#");
|
|
208
191
|
const hash = hashAt >= 0 ? value.slice(hashAt) : "";
|
|
209
192
|
const noHash = hashAt >= 0 ? value.slice(0, hashAt) : value;
|
|
@@ -304,37 +287,12 @@ var ConfigError = class extends OffscriptError {
|
|
|
304
287
|
this.name = "ConfigError";
|
|
305
288
|
}
|
|
306
289
|
};
|
|
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
290
|
var InvalidStateError = class extends OffscriptError {
|
|
326
291
|
constructor(message) {
|
|
327
292
|
super("INVALID_STATE", message);
|
|
328
293
|
this.name = "InvalidStateError";
|
|
329
294
|
}
|
|
330
295
|
};
|
|
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
296
|
var Emitter = class {
|
|
339
297
|
constructor() {
|
|
340
298
|
this.handlers = /* @__PURE__ */ new Map();
|
|
@@ -361,591 +319,6 @@ var Emitter = class {
|
|
|
361
319
|
}
|
|
362
320
|
};
|
|
363
321
|
|
|
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
322
|
// src/recorders/types.ts
|
|
950
323
|
function guard(sink, name, fn) {
|
|
951
324
|
try {
|
|
@@ -960,6 +333,7 @@ function guard(sink, name, fn) {
|
|
|
960
333
|
|
|
961
334
|
// src/recorders/ActionRecorder.ts
|
|
962
335
|
var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
336
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["Control", "Shift", "Alt", "Meta"]);
|
|
963
337
|
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
338
|
var INPUT_IDLE_MS = 700;
|
|
965
339
|
var DRAG_MIN_PX = 8;
|
|
@@ -973,7 +347,7 @@ var ActionRecorder = class {
|
|
|
973
347
|
this.scrollState = /* @__PURE__ */ new Map();
|
|
974
348
|
this.move = [];
|
|
975
349
|
const r = sink.config.recording;
|
|
976
|
-
this.gen = new
|
|
350
|
+
this.gen = new chunkHPDLEAAL_cjs.SelectorGenerator({ testIdAttributes: r.testIdAttributes, stableAttributes: r.stableAttributes });
|
|
977
351
|
}
|
|
978
352
|
start() {
|
|
979
353
|
const d = this.d;
|
|
@@ -1037,7 +411,7 @@ var ActionRecorder = class {
|
|
|
1037
411
|
emit(draft, el, extra) {
|
|
1038
412
|
const s = this.sink.sanitizer;
|
|
1039
413
|
const data = {
|
|
1040
|
-
schemaVersion:
|
|
414
|
+
schemaVersion: chunkHPDLEAAL_cjs.ACTION_SCHEMA_VERSION,
|
|
1041
415
|
type: draft.type,
|
|
1042
416
|
url: s.sanitizeUrl(location.href),
|
|
1043
417
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
@@ -1085,7 +459,7 @@ var ActionRecorder = class {
|
|
|
1085
459
|
onInput(e) {
|
|
1086
460
|
const el = this.target(e);
|
|
1087
461
|
if (!el || !(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return;
|
|
1088
|
-
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"
|
|
462
|
+
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"].includes(el.type)) return;
|
|
1089
463
|
if (this.pendingInput && this.pendingInput.el !== el) this.flushInput();
|
|
1090
464
|
this.pendingInput ?? (this.pendingInput = { el, inputType: e.inputType ?? "input", startedAt: Date.now() });
|
|
1091
465
|
if (this.inputTimer) clearTimeout(this.inputTimer);
|
|
@@ -1103,8 +477,9 @@ var ActionRecorder = class {
|
|
|
1103
477
|
const policy = this.sink.config.privacy;
|
|
1104
478
|
const allowed = policy.allowedInputSelectors.join(",");
|
|
1105
479
|
const isPassword = el instanceof HTMLInputElement && el.type === "password";
|
|
1106
|
-
|
|
1107
|
-
|
|
480
|
+
const listed = Boolean(allowed) && safe(() => el.matches(allowed));
|
|
481
|
+
if (policy.captureSensitiveValues || !isPassword && (!policy.maskSensitiveInputs || listed)) {
|
|
482
|
+
payload.value = policy.captureSensitiveValues ? value : this.sink.sanitizer.scrubString(value);
|
|
1108
483
|
payload.valueCaptured = true;
|
|
1109
484
|
} else {
|
|
1110
485
|
payload.valueCaptured = false;
|
|
@@ -1132,13 +507,15 @@ var ActionRecorder = class {
|
|
|
1132
507
|
}
|
|
1133
508
|
}
|
|
1134
509
|
onKey(e) {
|
|
1135
|
-
const el = this.target(e);
|
|
510
|
+
const el = this.target(e) ?? document.body;
|
|
1136
511
|
if (!el) return;
|
|
1137
512
|
const modified = e.ctrlKey || e.metaKey || e.altKey;
|
|
1138
|
-
const
|
|
1139
|
-
|
|
513
|
+
const isModifier = MODIFIERS.has(e.key);
|
|
514
|
+
const relevant = KEYS.has(e.key) || isModifier || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
|
|
515
|
+
if (!relevant || isModifier && e.repeat) return;
|
|
1140
516
|
if (e.key === "Enter" || e.key === "Tab") this.flushInput();
|
|
1141
|
-
|
|
517
|
+
const modifiers = mods(e).filter((name) => name !== e.key);
|
|
518
|
+
this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers, repeat: e.repeat } }, el);
|
|
1142
519
|
}
|
|
1143
520
|
onFocus(type, e) {
|
|
1144
521
|
const el = this.target(e);
|
|
@@ -1271,7 +648,7 @@ function parseBrowser(ua) {
|
|
|
1271
648
|
}
|
|
1272
649
|
return {};
|
|
1273
650
|
}
|
|
1274
|
-
function collectContext(
|
|
651
|
+
function collectContext(reason, captureSensitiveValues = false) {
|
|
1275
652
|
const nav = navigator;
|
|
1276
653
|
const ua = nav.userAgent ?? "";
|
|
1277
654
|
let browser = parseBrowser(ua);
|
|
@@ -1295,17 +672,13 @@ function collectContext(id, reason, appVersion) {
|
|
|
1295
672
|
timezone: safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
1296
673
|
online: nav.onLine,
|
|
1297
674
|
visibility: document.visibilityState,
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
executionId: id.executionId,
|
|
1301
|
-
sdkVersion: chunkB6GO5FE5_cjs.SDK_VERSION,
|
|
1302
|
-
appVersion
|
|
675
|
+
sdkVersion: chunkHPDLEAAL_cjs.SDK_VERSION,
|
|
676
|
+
captureSensitiveValues
|
|
1303
677
|
};
|
|
1304
678
|
}
|
|
1305
679
|
var ContextRecorder = class {
|
|
1306
|
-
constructor(sink
|
|
680
|
+
constructor(sink) {
|
|
1307
681
|
this.sink = sink;
|
|
1308
|
-
this.identity = identity;
|
|
1309
682
|
this.name = "context";
|
|
1310
683
|
this.d = new Disposer();
|
|
1311
684
|
}
|
|
@@ -1318,7 +691,7 @@ var ContextRecorder = class {
|
|
|
1318
691
|
}
|
|
1319
692
|
record(reason) {
|
|
1320
693
|
guard(this.sink, this.name, () => {
|
|
1321
|
-
const data = collectContext(
|
|
694
|
+
const data = collectContext(reason, this.sink.config.privacy.captureSensitiveValues);
|
|
1322
695
|
data.url = this.sink.sanitizer.sanitizeUrl(data.url);
|
|
1323
696
|
if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
|
|
1324
697
|
this.sink.emit("context", data, { priority: "normal" });
|
|
@@ -1477,23 +850,28 @@ function uid(prefix = "") {
|
|
|
1477
850
|
}
|
|
1478
851
|
|
|
1479
852
|
// src/recorders/HTTPRecorder.ts
|
|
1480
|
-
var XHR_PATCHED = "__offscriptXhrOriginal";
|
|
1481
853
|
var HTTPRecorder = class {
|
|
1482
|
-
constructor(sink
|
|
854
|
+
constructor(sink) {
|
|
1483
855
|
this.sink = sink;
|
|
1484
|
-
this.ownApiUrl = ownApiUrl;
|
|
1485
856
|
this.name = "http";
|
|
1486
857
|
this.inflight = 0;
|
|
1487
858
|
this.active = false;
|
|
859
|
+
this.delivering = false;
|
|
860
|
+
this.pending = /* @__PURE__ */ new Set();
|
|
1488
861
|
this.restore = [];
|
|
1489
862
|
}
|
|
1490
863
|
start() {
|
|
1491
864
|
if (this.active) return;
|
|
1492
865
|
this.active = true;
|
|
866
|
+
this.delivering = true;
|
|
1493
867
|
this.patchFetch();
|
|
1494
868
|
this.patchXhr();
|
|
1495
869
|
}
|
|
1496
870
|
stop() {
|
|
871
|
+
this.delivering = false;
|
|
872
|
+
this.detach();
|
|
873
|
+
}
|
|
874
|
+
detach() {
|
|
1497
875
|
this.active = false;
|
|
1498
876
|
for (const r of this.restore.splice(0).reverse()) {
|
|
1499
877
|
try {
|
|
@@ -1502,8 +880,30 @@ var HTTPRecorder = class {
|
|
|
1502
880
|
}
|
|
1503
881
|
}
|
|
1504
882
|
}
|
|
883
|
+
/** Stops new capture and allows up to two seconds for pending HTTP/body capture. */
|
|
884
|
+
async drain() {
|
|
885
|
+
this.detach();
|
|
886
|
+
let timer;
|
|
887
|
+
const timeout = new Promise((resolve) => {
|
|
888
|
+
timer = setTimeout(resolve, 2e3);
|
|
889
|
+
});
|
|
890
|
+
const settle = async () => {
|
|
891
|
+
while (this.delivering && this.pending.size) {
|
|
892
|
+
await Promise.allSettled([...this.pending]);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
try {
|
|
896
|
+
await Promise.race([settle(), timeout]);
|
|
897
|
+
} finally {
|
|
898
|
+
clearTimeout(timer);
|
|
899
|
+
this.delivering = false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
track(promise) {
|
|
903
|
+
const tracked = promise.catch(() => void 0).finally(() => this.pending.delete(tracked));
|
|
904
|
+
this.pending.add(tracked);
|
|
905
|
+
}
|
|
1505
906
|
isIgnored(url) {
|
|
1506
|
-
if (this.ownApiUrl && url.startsWith(this.ownApiUrl)) return true;
|
|
1507
907
|
return this.sink.sanitizer.isIgnoredUrl(url);
|
|
1508
908
|
}
|
|
1509
909
|
// ---------------------------------------------------------------- fetch
|
|
@@ -1529,14 +929,13 @@ var HTTPRecorder = class {
|
|
|
1529
929
|
}
|
|
1530
930
|
if (ctx) {
|
|
1531
931
|
const c = ctx;
|
|
1532
|
-
p.then(
|
|
932
|
+
self.track(p.then(
|
|
1533
933
|
(res) => self.endFetch(c, res),
|
|
1534
934
|
(err) => self.failFetch(c, err)
|
|
1535
|
-
)
|
|
935
|
+
));
|
|
1536
936
|
}
|
|
1537
937
|
return p;
|
|
1538
938
|
};
|
|
1539
|
-
patched[ORIGINAL_FETCH] = orig[ORIGINAL_FETCH] ?? orig;
|
|
1540
939
|
globalThis.fetch = patched;
|
|
1541
940
|
this.restore.push(() => {
|
|
1542
941
|
if (globalThis.fetch === patched) globalThis.fetch = orig;
|
|
@@ -1584,13 +983,13 @@ var HTTPRecorder = class {
|
|
|
1584
983
|
base.responseBody = { kind: "omitted", reason: "body-used" };
|
|
1585
984
|
return void this.emit(base);
|
|
1586
985
|
}
|
|
1587
|
-
this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
986
|
+
this.track(this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
1588
987
|
const b = this.sink.sanitizer.sanitizeTextBody(text, ct, size);
|
|
1589
988
|
b.truncated = truncated || b.truncated;
|
|
1590
989
|
base.responseBody = b;
|
|
1591
990
|
}).catch(() => {
|
|
1592
991
|
base.responseBody = { kind: "omitted", reason: "read-failed" };
|
|
1593
|
-
}).finally(() => guard(this.sink, this.name, () => this.emit(base)));
|
|
992
|
+
}).finally(() => guard(this.sink, this.name, () => this.emit(base))));
|
|
1594
993
|
});
|
|
1595
994
|
}
|
|
1596
995
|
async readBounded(res) {
|
|
@@ -1601,8 +1000,8 @@ var HTTPRecorder = class {
|
|
|
1601
1000
|
let size = 0;
|
|
1602
1001
|
let truncated = false;
|
|
1603
1002
|
for (; ; ) {
|
|
1604
|
-
const { done
|
|
1605
|
-
if (
|
|
1003
|
+
const { done, value } = await reader.read();
|
|
1004
|
+
if (done) break;
|
|
1606
1005
|
size += value.byteLength;
|
|
1607
1006
|
if (text.length < max) text += dec.decode(value, { stream: true });
|
|
1608
1007
|
if (size > max * 4) {
|
|
@@ -1659,7 +1058,6 @@ var HTTPRecorder = class {
|
|
|
1659
1058
|
}
|
|
1660
1059
|
return origSend.call(this, body);
|
|
1661
1060
|
};
|
|
1662
|
-
open[XHR_PATCHED] = origOpen;
|
|
1663
1061
|
proto.open = open;
|
|
1664
1062
|
proto.setRequestHeader = setHeader;
|
|
1665
1063
|
proto.send = send;
|
|
@@ -1681,9 +1079,14 @@ var HTTPRecorder = class {
|
|
|
1681
1079
|
this.inflight++;
|
|
1682
1080
|
xhr.addEventListener("abort", () => m.aborted = true);
|
|
1683
1081
|
xhr.addEventListener("timeout", () => m.timedOut = true);
|
|
1082
|
+
let finishCapture;
|
|
1083
|
+
this.track(new Promise((resolve) => {
|
|
1084
|
+
finishCapture = resolve;
|
|
1085
|
+
}));
|
|
1684
1086
|
xhr.addEventListener("loadend", () => {
|
|
1685
1087
|
this.inflight = Math.max(0, this.inflight - 1);
|
|
1686
1088
|
guard(this.sink, this.name, () => this.endXhr(xhr, m, ctx));
|
|
1089
|
+
finishCapture();
|
|
1687
1090
|
});
|
|
1688
1091
|
}
|
|
1689
1092
|
endXhr(xhr, m, ctx) {
|
|
@@ -1727,6 +1130,7 @@ var HTTPRecorder = class {
|
|
|
1727
1130
|
};
|
|
1728
1131
|
}
|
|
1729
1132
|
emit(data) {
|
|
1133
|
+
if (!this.delivering) return;
|
|
1730
1134
|
this.sink.emit("http", data, { priority: "normal", at: data.startedAt });
|
|
1731
1135
|
}
|
|
1732
1136
|
describeBody(body, contentType) {
|
|
@@ -1872,9 +1276,10 @@ var RRWebRecorder = class {
|
|
|
1872
1276
|
emit: (event) => this.onEvent(event),
|
|
1873
1277
|
blockSelector: block || void 0,
|
|
1874
1278
|
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 },
|
|
1279
|
+
maskAllInputs: privacy.maskSensitiveInputs && !privacy.captureSensitiveValues,
|
|
1280
|
+
maskInputOptions: privacy.captureSensitiveValues ? { password: false } : 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
1281
|
maskInputFn: (text, el) => {
|
|
1282
|
+
if (privacy.captureSensitiveValues) return text;
|
|
1878
1283
|
if (el?.type === "password") return "*".repeat(text.length);
|
|
1879
1284
|
if (allowed && safe(() => el.matches(allowed))) return text;
|
|
1880
1285
|
return "*".repeat(text.length);
|
|
@@ -1975,11 +1380,12 @@ var LifecycleManager = class {
|
|
|
1975
1380
|
this.lastUrl = location.href;
|
|
1976
1381
|
const d = this.d;
|
|
1977
1382
|
d.listen(window, "pagehide", () => safe(() => this.h.onPageHide()), { capture: true });
|
|
1978
|
-
d.listen(window, "
|
|
1383
|
+
d.listen(window, "pageshow", (event) => {
|
|
1384
|
+
if (event.persisted) safe(() => this.h.onPageShow?.());
|
|
1385
|
+
});
|
|
1979
1386
|
d.listen(document, "visibilitychange", () => {
|
|
1980
1387
|
const hidden = document.visibilityState === "hidden";
|
|
1981
1388
|
safe(() => this.h.onVisibility(hidden));
|
|
1982
|
-
if (hidden) safe(() => this.h.onPageHide());
|
|
1983
1389
|
});
|
|
1984
1390
|
d.listen(window, "online", () => safe(() => this.h.onConnectivity(true)));
|
|
1985
1391
|
d.listen(window, "offline", () => safe(() => this.h.onConnectivity(false)));
|
|
@@ -2016,532 +1422,1058 @@ var LifecycleManager = class {
|
|
|
2016
1422
|
}
|
|
2017
1423
|
};
|
|
2018
1424
|
|
|
2019
|
-
// src/core/
|
|
2020
|
-
var
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
this.
|
|
2024
|
-
this.
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
return
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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;
|
|
1425
|
+
// src/core/RecordingStore.ts
|
|
1426
|
+
var RecordingStore = class _RecordingStore {
|
|
1427
|
+
constructor(database) {
|
|
1428
|
+
this.database = database;
|
|
1429
|
+
this.pending = /* @__PURE__ */ new Set();
|
|
1430
|
+
this.uncommitted = /* @__PURE__ */ new Map();
|
|
1431
|
+
}
|
|
1432
|
+
static async open(key) {
|
|
1433
|
+
const database = await new Promise((resolve, reject) => {
|
|
1434
|
+
const request = indexedDB.open(`offscript-recording-${key}`, 1);
|
|
1435
|
+
request.onupgradeneeded = () => {
|
|
1436
|
+
request.result.createObjectStore("events", { keyPath: "sequence" });
|
|
1437
|
+
request.result.createObjectStore("state");
|
|
1438
|
+
};
|
|
1439
|
+
request.onsuccess = () => resolve(request.result);
|
|
1440
|
+
request.onerror = () => reject(request.error);
|
|
1441
|
+
});
|
|
1442
|
+
return new _RecordingStore(database);
|
|
1443
|
+
}
|
|
1444
|
+
async load() {
|
|
1445
|
+
await this.flush();
|
|
1446
|
+
const checkpoint = sessionStorage.getItem(this.checkpointKey);
|
|
1447
|
+
if (checkpoint) {
|
|
1448
|
+
const saved = JSON.parse(checkpoint);
|
|
1449
|
+
await this.write(["events", "state"], (transaction) => {
|
|
1450
|
+
for (const event of saved.events) transaction.objectStore("events").put(event);
|
|
1451
|
+
transaction.objectStore("state").put(saved.status, "status");
|
|
1452
|
+
});
|
|
1453
|
+
sessionStorage.removeItem(this.checkpointKey);
|
|
2054
1454
|
}
|
|
1455
|
+
return new Promise((resolve, reject) => {
|
|
1456
|
+
const transaction = this.database.transaction(["events", "state"], "readonly");
|
|
1457
|
+
const events = transaction.objectStore("events").getAll();
|
|
1458
|
+
const state = transaction.objectStore("state").get("status");
|
|
1459
|
+
transaction.oncomplete = () => {
|
|
1460
|
+
this.status = state.result;
|
|
1461
|
+
resolve(state.result ? { status: state.result, events: events.result } : null);
|
|
1462
|
+
};
|
|
1463
|
+
transaction.onabort = () => reject(transaction.error);
|
|
1464
|
+
});
|
|
2055
1465
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
this.
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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)));
|
|
1466
|
+
append(event) {
|
|
1467
|
+
this.uncommitted.set(event.sequence, event);
|
|
1468
|
+
return this.write(["events"], (transaction) => {
|
|
1469
|
+
transaction.objectStore("events").put(event);
|
|
1470
|
+
}).then(() => {
|
|
1471
|
+
this.uncommitted.delete(event.sequence);
|
|
1472
|
+
});
|
|
2075
1473
|
}
|
|
2076
1474
|
setStatus(status) {
|
|
2077
|
-
this.
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
|
1475
|
+
this.status = status;
|
|
1476
|
+
return this.write(["state"], (transaction) => {
|
|
1477
|
+
transaction.objectStore("state").put(status, "status");
|
|
2121
1478
|
});
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
|
1479
|
+
}
|
|
1480
|
+
async clear() {
|
|
1481
|
+
await this.write(["events", "state"], (transaction) => {
|
|
1482
|
+
transaction.objectStore("events").clear();
|
|
1483
|
+
transaction.objectStore("state").clear();
|
|
2143
1484
|
});
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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);
|
|
1485
|
+
this.failure = void 0;
|
|
1486
|
+
this.status = void 0;
|
|
1487
|
+
this.uncommitted.clear();
|
|
1488
|
+
sessionStorage.removeItem(this.checkpointKey);
|
|
2191
1489
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
this.
|
|
1490
|
+
/** Synchronous handoff of the last uncommitted events before document unload. */
|
|
1491
|
+
checkpoint() {
|
|
1492
|
+
if (!this.status) return;
|
|
1493
|
+
sessionStorage.setItem(this.checkpointKey, JSON.stringify({ status: this.status, events: [...this.uncommitted.values()] }));
|
|
2195
1494
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
const r = await this.api.complete(this.info.sessionId, { tabId: this.info.tabId, lastSequence: this.seq, ...summary });
|
|
2199
|
-
return !!r.completed;
|
|
1495
|
+
get checkpointKey() {
|
|
1496
|
+
return `${this.database.name}-handoff`;
|
|
2200
1497
|
}
|
|
2201
|
-
|
|
2202
|
-
this.
|
|
2203
|
-
this.
|
|
2204
|
-
this.state = null;
|
|
2205
|
-
this.token = "";
|
|
2206
|
-
this.seq = 0;
|
|
1498
|
+
async flush() {
|
|
1499
|
+
await Promise.allSettled([...this.pending]);
|
|
1500
|
+
if (this.failure) throw this.failure;
|
|
2207
1501
|
}
|
|
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) => {
|
|
1502
|
+
async close() {
|
|
2226
1503
|
try {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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);
|
|
1504
|
+
await this.flush();
|
|
1505
|
+
} finally {
|
|
1506
|
+
this.database.close();
|
|
2238
1507
|
}
|
|
2239
|
-
}
|
|
2240
|
-
|
|
1508
|
+
}
|
|
1509
|
+
write(stores, apply) {
|
|
1510
|
+
const operation = new Promise((resolve, reject) => {
|
|
1511
|
+
const transaction = this.database.transaction(stores, "readwrite");
|
|
1512
|
+
transaction.oncomplete = () => resolve();
|
|
1513
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Falha ao salvar grava\xE7\xE3o local"));
|
|
1514
|
+
try {
|
|
1515
|
+
apply(transaction);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
transaction.abort();
|
|
1518
|
+
reject(error);
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
this.pending.add(operation);
|
|
1522
|
+
void operation.catch((error) => {
|
|
1523
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
1524
|
+
}).finally(() => this.pending.delete(operation));
|
|
1525
|
+
return operation;
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
2241
1528
|
|
|
2242
1529
|
// src/core/config.ts
|
|
2243
1530
|
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
1531
|
return {
|
|
2257
|
-
apiUrl: url.origin + url.pathname.replace(/\/+$/, ""),
|
|
2258
|
-
platformId: input.platformId,
|
|
2259
|
-
appVersion: input.appVersion,
|
|
2260
1532
|
recording: deepMerge(DEFAULT_RECORDING, input.recording),
|
|
2261
1533
|
privacy: resolvePrivacy(input.privacy),
|
|
2262
|
-
transport: deepMerge(DEFAULT_TRANSPORT, input.transport),
|
|
2263
1534
|
correlationWindowMs: input.correlationWindowMs ?? 3e3,
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
debug: input.debug ?? false
|
|
1535
|
+
controls: input.controls ?? true,
|
|
1536
|
+
persistenceKey: input.persistenceKey,
|
|
1537
|
+
onComplete: input.onComplete
|
|
2268
1538
|
};
|
|
2269
1539
|
}
|
|
2270
1540
|
|
|
1541
|
+
// src/ui/RecordingControls.ts
|
|
1542
|
+
var HOST_ATTR = "data-offscript-console";
|
|
1543
|
+
var EYE_SEQUENCE = [
|
|
1544
|
+
{ pose: "open", duration: 2600 },
|
|
1545
|
+
{ pose: "blink", duration: 160 },
|
|
1546
|
+
{ pose: "open", duration: 1800 },
|
|
1547
|
+
{ pose: "left", duration: 700 },
|
|
1548
|
+
{ pose: "open", duration: 1500 },
|
|
1549
|
+
{ pose: "wink", duration: 480 },
|
|
1550
|
+
{ pose: "open", duration: 1900 },
|
|
1551
|
+
{ pose: "right", duration: 700 },
|
|
1552
|
+
{ pose: "open", duration: 1600 },
|
|
1553
|
+
{ pose: "up", duration: 640 },
|
|
1554
|
+
{ pose: "happy", duration: 720 }
|
|
1555
|
+
];
|
|
1556
|
+
var EYE_PATHS = {
|
|
1557
|
+
open: ["M59.5 79.5v15", "M93 79.5v15"],
|
|
1558
|
+
blink: ["M52 89q7.5 5 15 0", "M85.5 89q7.5 5 15 0"],
|
|
1559
|
+
wink: ["M52 89q7.5 5 15 0", "M93 79.5v15"],
|
|
1560
|
+
happy: ["M52 91q7.5-12 15 0", "M85.5 91q7.5-12 15 0"]
|
|
1561
|
+
};
|
|
1562
|
+
var RecordingControls = class {
|
|
1563
|
+
constructor(sdk) {
|
|
1564
|
+
this.sdk = sdk;
|
|
1565
|
+
this.eyeTimer = 0;
|
|
1566
|
+
this.placeFrame = 0;
|
|
1567
|
+
this.eyeIndex = 0;
|
|
1568
|
+
this.busy = false;
|
|
1569
|
+
this.expanded = true;
|
|
1570
|
+
this.privacyOpen = true;
|
|
1571
|
+
this.failure = "";
|
|
1572
|
+
this.onVisibility = () => {
|
|
1573
|
+
window.clearTimeout(this.eyeTimer);
|
|
1574
|
+
if (!document.hidden) this.scheduleEye();
|
|
1575
|
+
};
|
|
1576
|
+
this.onViewport = () => {
|
|
1577
|
+
this.placeConfirm();
|
|
1578
|
+
this.schedulePlacePrivacy();
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
mount() {
|
|
1582
|
+
if (this.host || typeof document === "undefined") return;
|
|
1583
|
+
const host = document.createElement("div");
|
|
1584
|
+
host.setAttribute(HOST_ATTR, "");
|
|
1585
|
+
host.setAttribute("data-offscript-ignore", "");
|
|
1586
|
+
host.className = "offscript-ignore";
|
|
1587
|
+
host.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;margin:0;padding:0;border:0;overflow:visible;z-index:2147483646;pointer-events:none;";
|
|
1588
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
1589
|
+
shadow.innerHTML = TEMPLATE;
|
|
1590
|
+
this.bind(shadow);
|
|
1591
|
+
this.host = host;
|
|
1592
|
+
this.shadow = shadow;
|
|
1593
|
+
this.note = shadow.querySelector("[data-note]") ?? void 0;
|
|
1594
|
+
this.place();
|
|
1595
|
+
this.watchDom();
|
|
1596
|
+
this.unsubscribe = this.sdk.on("statusChange", () => this.render());
|
|
1597
|
+
window.addEventListener("resize", this.onViewport);
|
|
1598
|
+
window.visualViewport?.addEventListener("resize", this.onViewport);
|
|
1599
|
+
document.fonts?.ready.then(() => {
|
|
1600
|
+
if (this.host) this.onViewport();
|
|
1601
|
+
});
|
|
1602
|
+
this.watchDock();
|
|
1603
|
+
this.render();
|
|
1604
|
+
this.startEyes();
|
|
1605
|
+
}
|
|
1606
|
+
/** Independent notice so the host may destroy the controls after successful upload. */
|
|
1607
|
+
showSuccess() {
|
|
1608
|
+
const notice = document.createElement("div");
|
|
1609
|
+
notice.setAttribute("data-offscript-success", "");
|
|
1610
|
+
notice.setAttribute("data-offscript-ignore", "");
|
|
1611
|
+
notice.className = "offscript-ignore";
|
|
1612
|
+
notice.style.cssText = "position:fixed;right:16px;bottom:16px;max-width:calc(100vw - 32px);z-index:2147483647;";
|
|
1613
|
+
const shadow = notice.attachShadow({ mode: "closed" });
|
|
1614
|
+
shadow.innerHTML = SUCCESS_TEMPLATE;
|
|
1615
|
+
document.body.appendChild(notice);
|
|
1616
|
+
const timer = window.setTimeout(() => notice.remove(), 8e3);
|
|
1617
|
+
shadow.querySelector("button").addEventListener("click", () => {
|
|
1618
|
+
window.clearTimeout(timer);
|
|
1619
|
+
notice.remove();
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
unmount() {
|
|
1623
|
+
this.unsubscribe?.();
|
|
1624
|
+
this.unsubscribe = void 0;
|
|
1625
|
+
this.observer?.disconnect();
|
|
1626
|
+
this.observer = void 0;
|
|
1627
|
+
this.dockWatch?.disconnect();
|
|
1628
|
+
this.dockWatch = void 0;
|
|
1629
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
1630
|
+
this.placeFrame = 0;
|
|
1631
|
+
window.clearTimeout(this.eyeTimer);
|
|
1632
|
+
window.removeEventListener("resize", this.onViewport);
|
|
1633
|
+
window.visualViewport?.removeEventListener("resize", this.onViewport);
|
|
1634
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
1635
|
+
this.host?.remove();
|
|
1636
|
+
this.host = void 0;
|
|
1637
|
+
this.shadow = void 0;
|
|
1638
|
+
}
|
|
1639
|
+
place() {
|
|
1640
|
+
const host = this.host;
|
|
1641
|
+
const parent = document.body ?? document.documentElement;
|
|
1642
|
+
if (!host || !parent || host.parentNode === parent) return;
|
|
1643
|
+
parent.appendChild(host);
|
|
1644
|
+
}
|
|
1645
|
+
watchDom() {
|
|
1646
|
+
if (typeof MutationObserver === "undefined") return;
|
|
1647
|
+
this.observer?.disconnect();
|
|
1648
|
+
this.observer = new MutationObserver(() => {
|
|
1649
|
+
this.place();
|
|
1650
|
+
if (document.body) this.observer?.observe(document.body, { childList: true });
|
|
1651
|
+
});
|
|
1652
|
+
this.observer.observe(document.documentElement, { childList: true });
|
|
1653
|
+
if (document.body) this.observer.observe(document.body, { childList: true });
|
|
1654
|
+
}
|
|
1655
|
+
watchDock() {
|
|
1656
|
+
const dock = this.shadow?.querySelector(".dock");
|
|
1657
|
+
if (!(dock instanceof HTMLElement) || typeof ResizeObserver === "undefined") return;
|
|
1658
|
+
this.dockWatch?.disconnect();
|
|
1659
|
+
this.dockWatch = new ResizeObserver(() => this.placePrivacy());
|
|
1660
|
+
this.dockWatch.observe(dock);
|
|
1661
|
+
}
|
|
1662
|
+
bind(shadow) {
|
|
1663
|
+
const stop = (event) => event.stopPropagation();
|
|
1664
|
+
shadow.addEventListener("click", (event) => {
|
|
1665
|
+
const actionNode = event.composedPath().find((node) => node instanceof Element && node.hasAttribute("data-action"));
|
|
1666
|
+
const action = actionNode instanceof Element ? actionNode.getAttribute("data-action") : null;
|
|
1667
|
+
if (action === "open") {
|
|
1668
|
+
this.expanded = true;
|
|
1669
|
+
this.render();
|
|
1670
|
+
} else if (action === "close") {
|
|
1671
|
+
this.expanded = false;
|
|
1672
|
+
this.render();
|
|
1673
|
+
} else if (action === "record") void this.run(() => this.sdk.startRecording());
|
|
1674
|
+
else if (action === "pause") void this.run(() => this.sdk.pauseRecording());
|
|
1675
|
+
else if (action === "resume") void this.run(() => this.sdk.resumeRecording());
|
|
1676
|
+
else if (action === "stop") void this.run(() => this.sdk.finishRecording());
|
|
1677
|
+
else if (action === "new") this.openConfirm();
|
|
1678
|
+
else if (action === "cancel") this.closeConfirm();
|
|
1679
|
+
else if (action === "confirm") {
|
|
1680
|
+
this.closeConfirm();
|
|
1681
|
+
void this.run(() => this.sdk.discardRecording());
|
|
1682
|
+
} else if (action === "privacy-show") {
|
|
1683
|
+
this.privacyOpen = true;
|
|
1684
|
+
this.render();
|
|
1685
|
+
} else if (action === "privacy-hide") {
|
|
1686
|
+
this.privacyOpen = false;
|
|
1687
|
+
this.render();
|
|
1688
|
+
}
|
|
1689
|
+
}, true);
|
|
1690
|
+
for (const type of ["click", "dblclick", "auxclick", "contextmenu", "pointerdown", "pointerup", "mousedown", "mouseup", "keydown", "keyup", "keypress", "input", "change", "mouseover", "mouseout"]) {
|
|
1691
|
+
shadow.addEventListener(type, stop);
|
|
1692
|
+
}
|
|
1693
|
+
shadow.querySelector("[data-confirm]")?.addEventListener("keydown", (event) => {
|
|
1694
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") this.closeConfirm();
|
|
1695
|
+
});
|
|
1696
|
+
shadow.querySelector("[data-privacy]")?.addEventListener("keydown", (event) => {
|
|
1697
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") {
|
|
1698
|
+
this.privacyOpen = false;
|
|
1699
|
+
this.render();
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
openConfirm() {
|
|
1704
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
1705
|
+
if (!(bubble instanceof HTMLElement) || this.busy || !bubble.hidden) return;
|
|
1706
|
+
bubble.hidden = false;
|
|
1707
|
+
this.placeConfirm();
|
|
1708
|
+
this.paintPrivacy();
|
|
1709
|
+
const cancel = bubble.querySelector("[data-action='cancel']");
|
|
1710
|
+
if (cancel instanceof HTMLElement) cancel.focus();
|
|
1711
|
+
}
|
|
1712
|
+
placeConfirm() {
|
|
1713
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
1714
|
+
const card = bubble?.querySelector(".bubble");
|
|
1715
|
+
const button = this.shadow?.querySelector("[data-action='new']");
|
|
1716
|
+
if (!(bubble instanceof HTMLElement) || !(card instanceof HTMLElement) || !(button instanceof HTMLElement)) return;
|
|
1717
|
+
const buttonBox = button.getBoundingClientRect();
|
|
1718
|
+
const buttonCenter = buttonBox.left + buttonBox.width / 2;
|
|
1719
|
+
const tailHalf = 4.5;
|
|
1720
|
+
const margin = 12;
|
|
1721
|
+
const viewport = document.documentElement.clientWidth;
|
|
1722
|
+
let rightEdge = buttonCenter + 22 + tailHalf;
|
|
1723
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + card.offsetWidth, rightEdge));
|
|
1724
|
+
bubble.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
1725
|
+
const tail = Math.min(Math.max(12, rightEdge - buttonCenter - tailHalf), Math.max(12, card.offsetWidth - 18));
|
|
1726
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
1727
|
+
}
|
|
1728
|
+
closeConfirm() {
|
|
1729
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
1730
|
+
if (bubble instanceof HTMLElement) bubble.hidden = true;
|
|
1731
|
+
this.paintPrivacy();
|
|
1732
|
+
}
|
|
1733
|
+
async run(task) {
|
|
1734
|
+
if (this.busy) return;
|
|
1735
|
+
this.busy = true;
|
|
1736
|
+
this.failure = "";
|
|
1737
|
+
this.render();
|
|
1738
|
+
try {
|
|
1739
|
+
await task();
|
|
1740
|
+
} catch {
|
|
1741
|
+
this.failure = "N\xE3o foi poss\xEDvel concluir";
|
|
1742
|
+
} finally {
|
|
1743
|
+
this.busy = false;
|
|
1744
|
+
this.render();
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
render() {
|
|
1748
|
+
this.place();
|
|
1749
|
+
const { status, pending } = this.sdk.getState();
|
|
1750
|
+
const shell = this.shadow?.querySelector("[data-shell]");
|
|
1751
|
+
shell?.setAttribute("data-open", this.expanded ? "true" : "false");
|
|
1752
|
+
shell?.setAttribute("data-state", status);
|
|
1753
|
+
if (this.note) this.note.textContent = this.failure;
|
|
1754
|
+
const live = status === "recording" || status === "paused";
|
|
1755
|
+
const canFinish = live || status === "stopped" && pending > 0;
|
|
1756
|
+
if (!live) this.closeConfirm();
|
|
1757
|
+
this.paintPrivacy();
|
|
1758
|
+
this.setHidden("record", live || status === "initializing" || status === "stopping");
|
|
1759
|
+
this.setHidden("pause", status !== "recording");
|
|
1760
|
+
this.setHidden("resume", status !== "paused");
|
|
1761
|
+
this.setHidden("new", !live);
|
|
1762
|
+
this.setHidden("stop", !canFinish);
|
|
1763
|
+
this.setEnabled("record", !this.busy && !live && status !== "initializing" && status !== "stopping");
|
|
1764
|
+
this.setEnabled("pause", !this.busy && status === "recording");
|
|
1765
|
+
this.setEnabled("resume", !this.busy && status === "paused");
|
|
1766
|
+
this.setEnabled("stop", !this.busy && canFinish);
|
|
1767
|
+
this.setEnabled("new", !this.busy && live);
|
|
1768
|
+
}
|
|
1769
|
+
paintPrivacy() {
|
|
1770
|
+
const flag = this.sdk.capturesSensitiveValues();
|
|
1771
|
+
const confirm = this.shadow?.querySelector("[data-confirm]");
|
|
1772
|
+
const confirmOpen = confirm instanceof HTMLElement && !confirm.hidden;
|
|
1773
|
+
const bubble = this.shadow?.querySelector("[data-privacy]");
|
|
1774
|
+
const show = flag && this.privacyOpen && this.expanded && !confirmOpen;
|
|
1775
|
+
if (bubble instanceof HTMLElement) {
|
|
1776
|
+
bubble.hidden = !show;
|
|
1777
|
+
if (!show) this.clearPrivacyPlace(bubble);
|
|
1778
|
+
}
|
|
1779
|
+
if (show) this.schedulePlacePrivacy();
|
|
1780
|
+
this.setHidden("privacy-show", !flag || this.privacyOpen || !this.expanded);
|
|
1781
|
+
}
|
|
1782
|
+
schedulePlacePrivacy() {
|
|
1783
|
+
this.placePrivacy();
|
|
1784
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
1785
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
1786
|
+
this.placePrivacy();
|
|
1787
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
1788
|
+
this.placeFrame = 0;
|
|
1789
|
+
this.placePrivacy();
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
clearPrivacyPlace(wrap) {
|
|
1794
|
+
wrap.style.position = "";
|
|
1795
|
+
wrap.style.transform = "";
|
|
1796
|
+
wrap.style.width = "";
|
|
1797
|
+
wrap.style.maxWidth = "";
|
|
1798
|
+
wrap.style.left = "";
|
|
1799
|
+
wrap.style.right = "";
|
|
1800
|
+
wrap.style.top = "";
|
|
1801
|
+
wrap.style.bottom = "";
|
|
1802
|
+
}
|
|
1803
|
+
placePrivacy() {
|
|
1804
|
+
const wrap = this.shadow?.querySelector("[data-privacy]");
|
|
1805
|
+
const card = wrap?.querySelector(".bubble");
|
|
1806
|
+
const face = this.shadow?.querySelector(".face");
|
|
1807
|
+
if (!(wrap instanceof HTMLElement) || !(card instanceof HTMLElement) || !(face instanceof HTMLElement) || wrap.hidden) return;
|
|
1808
|
+
const box = face.getBoundingClientRect();
|
|
1809
|
+
if (box.width < 8 || box.height < 8 || box.top < 1 && box.left < 1) return;
|
|
1810
|
+
const margin = 12;
|
|
1811
|
+
const viewport = document.documentElement.clientWidth;
|
|
1812
|
+
const maxWidth = Math.min(264, viewport - margin * 2);
|
|
1813
|
+
wrap.style.position = "fixed";
|
|
1814
|
+
wrap.style.transform = "none";
|
|
1815
|
+
wrap.style.width = `${maxWidth}px`;
|
|
1816
|
+
wrap.style.maxWidth = `${maxWidth}px`;
|
|
1817
|
+
wrap.style.left = "auto";
|
|
1818
|
+
wrap.style.bottom = "auto";
|
|
1819
|
+
const faceCenter = box.left + box.width / 2;
|
|
1820
|
+
const tailHalf = 5;
|
|
1821
|
+
let rightEdge = faceCenter + 12 + tailHalf;
|
|
1822
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + maxWidth, rightEdge));
|
|
1823
|
+
wrap.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
1824
|
+
wrap.style.top = `${Math.round(Math.max(margin, box.top - 10 - wrap.offsetHeight))}px`;
|
|
1825
|
+
const tail = Math.min(Math.max(12, rightEdge - faceCenter - tailHalf), Math.max(12, maxWidth - 18));
|
|
1826
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
1827
|
+
}
|
|
1828
|
+
setEnabled(action, enabled) {
|
|
1829
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
1830
|
+
if (button instanceof HTMLButtonElement) button.disabled = !enabled;
|
|
1831
|
+
}
|
|
1832
|
+
setHidden(action, hidden) {
|
|
1833
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
1834
|
+
if (button instanceof HTMLButtonElement) button.hidden = hidden;
|
|
1835
|
+
}
|
|
1836
|
+
startEyes() {
|
|
1837
|
+
const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
1838
|
+
if (!media || media.matches) return;
|
|
1839
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
1840
|
+
this.scheduleEye();
|
|
1841
|
+
}
|
|
1842
|
+
scheduleEye() {
|
|
1843
|
+
const beat = EYE_SEQUENCE[this.eyeIndex % EYE_SEQUENCE.length];
|
|
1844
|
+
this.applyPose(beat.pose);
|
|
1845
|
+
this.eyeIndex = (this.eyeIndex + 1) % EYE_SEQUENCE.length;
|
|
1846
|
+
this.eyeTimer = window.setTimeout(() => this.scheduleEye(), beat.duration);
|
|
1847
|
+
}
|
|
1848
|
+
applyPose(pose) {
|
|
1849
|
+
const [left, right] = EYE_PATHS[pose] ?? EYE_PATHS.open;
|
|
1850
|
+
this.shadow?.querySelectorAll("[data-eye='left']").forEach((node) => node.setAttribute("d", left));
|
|
1851
|
+
this.shadow?.querySelectorAll("[data-eye='right']").forEach((node) => node.setAttribute("d", right));
|
|
1852
|
+
this.shadow?.querySelectorAll("[data-eyes]").forEach((node) => node.setAttribute("data-pose", pose));
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
var ICON = {
|
|
1856
|
+
pause: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9M11 3.5v9"/></svg>`,
|
|
1857
|
+
play: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3.8 12.2 8 6 12.2z" fill="currentColor" stroke="none"/></svg>`,
|
|
1858
|
+
close: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4 4 12"/></svg>`,
|
|
1859
|
+
speak: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 3.5h9a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5H7.2L4.5 13.5v-2H3.5A1.5 1.5 0 0 1 2 10V5a1.5 1.5 0 0 1 1.5-1.5Z"/></svg>`
|
|
1860
|
+
};
|
|
1861
|
+
function mark(gradientId) {
|
|
1862
|
+
return `<svg class="mark" viewBox="14 9 138 126" fill="none" aria-hidden="true">
|
|
1863
|
+
<defs>
|
|
1864
|
+
<linearGradient id="${gradientId}" x1="112" y1="41" x2="143" y2="17" gradientUnits="userSpaceOnUse">
|
|
1865
|
+
<stop offset="0" stop-color="#753eff"></stop>
|
|
1866
|
+
<stop offset="1" stop-color="#1772ff"></stop>
|
|
1867
|
+
</linearGradient>
|
|
1868
|
+
</defs>
|
|
1869
|
+
<path d="M72 23C88 23 101 26 109 32L112 40C122 50 128 64 128 80C128 109 104 131 72 131C41 131 16 108 16 80C16 46 40 23 72 23Z" fill="#eeeef7"></path>
|
|
1870
|
+
<path d="M112 41C119 37 123 27 131 22C135 19 139 18 143 17" stroke="url(#${gradientId})" stroke-width="8" stroke-linecap="round"></path>
|
|
1871
|
+
<g class="eyes" data-eyes data-pose="open" stroke="#10182e" stroke-width="7" stroke-linecap="round">
|
|
1872
|
+
<path data-eye="left" d="M59.5 79.5v15"></path>
|
|
1873
|
+
<path data-eye="right" d="M93 79.5v15"></path>
|
|
1874
|
+
</g>
|
|
1875
|
+
</svg>`;
|
|
1876
|
+
}
|
|
1877
|
+
var TEMPLATE = `
|
|
1878
|
+
<style>
|
|
1879
|
+
:host { color-scheme: dark; }
|
|
1880
|
+
button {
|
|
1881
|
+
display: grid;
|
|
1882
|
+
place-items: center;
|
|
1883
|
+
width: 32px;
|
|
1884
|
+
height: 32px;
|
|
1885
|
+
margin: 0;
|
|
1886
|
+
padding: 0;
|
|
1887
|
+
border: 1px solid #3a4458;
|
|
1888
|
+
border-radius: 999px;
|
|
1889
|
+
background: transparent;
|
|
1890
|
+
color: #f4f3fc;
|
|
1891
|
+
cursor: pointer;
|
|
1892
|
+
}
|
|
1893
|
+
button svg {
|
|
1894
|
+
width: 15px;
|
|
1895
|
+
height: 15px;
|
|
1896
|
+
fill: none;
|
|
1897
|
+
stroke: currentColor;
|
|
1898
|
+
stroke-width: 1.7;
|
|
1899
|
+
stroke-linecap: round;
|
|
1900
|
+
stroke-linejoin: round;
|
|
1901
|
+
}
|
|
1902
|
+
button:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
1903
|
+
button:focus-visible { outline: 2px solid #a596ff; outline-offset: 2px; }
|
|
1904
|
+
button:disabled { cursor: not-allowed; opacity: 0.4; }
|
|
1905
|
+
button[hidden] { display: none; }
|
|
1906
|
+
button.text {
|
|
1907
|
+
width: auto;
|
|
1908
|
+
padding: 0 12px;
|
|
1909
|
+
font: 600 12px/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
1910
|
+
}
|
|
1911
|
+
button.record {
|
|
1912
|
+
border: 0;
|
|
1913
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
1914
|
+
color: #fff;
|
|
1915
|
+
}
|
|
1916
|
+
button.finish {
|
|
1917
|
+
border: 0;
|
|
1918
|
+
background: #e5485f;
|
|
1919
|
+
color: #fff;
|
|
1920
|
+
}
|
|
1921
|
+
button.record:hover:not(:disabled),
|
|
1922
|
+
button.finish:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
1923
|
+
button.primary {
|
|
1924
|
+
border: 0;
|
|
1925
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
1926
|
+
color: #fff;
|
|
1927
|
+
}
|
|
1928
|
+
button.primary:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
1929
|
+
.launcher, .dock {
|
|
1930
|
+
position: fixed;
|
|
1931
|
+
right: 16px;
|
|
1932
|
+
bottom: 16px;
|
|
1933
|
+
z-index: 1;
|
|
1934
|
+
border: 1px solid #232b3b;
|
|
1935
|
+
background: #151b27;
|
|
1936
|
+
box-shadow: 0 18px 40px rgba(3, 5, 12, 0.5);
|
|
1937
|
+
pointer-events: auto;
|
|
1938
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
1939
|
+
}
|
|
1940
|
+
.launcher {
|
|
1941
|
+
width: 56px;
|
|
1942
|
+
height: 56px;
|
|
1943
|
+
padding: 8px;
|
|
1944
|
+
border-radius: 999px;
|
|
1945
|
+
cursor: pointer;
|
|
1946
|
+
}
|
|
1947
|
+
.launcher .mark { width: 100%; height: 100%; }
|
|
1948
|
+
.launcher::after {
|
|
1949
|
+
content: "";
|
|
1950
|
+
position: absolute;
|
|
1951
|
+
right: 1px;
|
|
1952
|
+
bottom: 1px;
|
|
1953
|
+
width: 10px;
|
|
1954
|
+
height: 10px;
|
|
1955
|
+
border-radius: 999px;
|
|
1956
|
+
background: #929bb1;
|
|
1957
|
+
box-shadow: 0 0 0 2px #151b27;
|
|
1958
|
+
}
|
|
1959
|
+
[data-state="recording"] .launcher::after { background: #1673ff; }
|
|
1960
|
+
[data-state="paused"] .launcher::after { background: #d0c8ff; }
|
|
1961
|
+
[data-state="error"] .launcher::after { background: #e5485f; }
|
|
1962
|
+
.dock {
|
|
1963
|
+
display: flex;
|
|
1964
|
+
align-items: center;
|
|
1965
|
+
gap: 10px;
|
|
1966
|
+
max-width: calc(100vw - 32px);
|
|
1967
|
+
padding: 6px 6px 6px 12px;
|
|
1968
|
+
border-radius: 999px;
|
|
1969
|
+
color: #f4f3fc;
|
|
1970
|
+
overflow: visible;
|
|
1971
|
+
}
|
|
1972
|
+
[data-open="false"] .dock, [data-open="true"] .launcher { display: none; }
|
|
1973
|
+
.brand { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: visible; }
|
|
1974
|
+
.face { position: relative; flex: none; width: 28px; height: 28px; overflow: visible; }
|
|
1975
|
+
.dock .mark { width: 28px; height: 28px; }
|
|
1976
|
+
.word {
|
|
1977
|
+
color: #f4f3fc;
|
|
1978
|
+
font-size: 14px;
|
|
1979
|
+
font-weight: 600;
|
|
1980
|
+
letter-spacing: -0.03em;
|
|
1981
|
+
line-height: 1;
|
|
1982
|
+
}
|
|
1983
|
+
.actions { display: flex; align-items: center; gap: 6px; }
|
|
1984
|
+
.note {
|
|
1985
|
+
position: fixed;
|
|
1986
|
+
right: 16px;
|
|
1987
|
+
bottom: 78px;
|
|
1988
|
+
z-index: 1;
|
|
1989
|
+
margin: 0;
|
|
1990
|
+
padding: 4px 10px;
|
|
1991
|
+
border-radius: 999px;
|
|
1992
|
+
background: #151b27;
|
|
1993
|
+
color: #ff9aa8;
|
|
1994
|
+
font: 600 11px/1.3 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
1995
|
+
pointer-events: none;
|
|
1996
|
+
}
|
|
1997
|
+
.note:empty { display: none; }
|
|
1998
|
+
[data-shell]:has(.note:not(:empty)) .privacy { bottom: calc(100% + 48px); }
|
|
1999
|
+
.eyes {
|
|
2000
|
+
transform-box: view-box;
|
|
2001
|
+
transform-origin: 0 0;
|
|
2002
|
+
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2003
|
+
}
|
|
2004
|
+
.eyes[data-pose="left"] { transform: translate(-4.5%, 0.8%); }
|
|
2005
|
+
.eyes[data-pose="right"] { transform: translate(4.5%, 0.8%); }
|
|
2006
|
+
.eyes[data-pose="up"] { transform: translate(0, -3.5%); }
|
|
2007
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2008
|
+
.eyes { transition: none; }
|
|
2009
|
+
.eyes[data-pose] { transform: none; }
|
|
2010
|
+
.speak:not([hidden]) .bubble { animation: none; }
|
|
2011
|
+
}
|
|
2012
|
+
.speak {
|
|
2013
|
+
position: fixed;
|
|
2014
|
+
right: 16px;
|
|
2015
|
+
bottom: 76px;
|
|
2016
|
+
z-index: 2;
|
|
2017
|
+
width: max-content;
|
|
2018
|
+
max-width: min(16rem, calc(100vw - 32px));
|
|
2019
|
+
pointer-events: auto;
|
|
2020
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2021
|
+
}
|
|
2022
|
+
.speak[hidden] { display: none; }
|
|
2023
|
+
.bubble {
|
|
2024
|
+
position: relative;
|
|
2025
|
+
padding: 0.6rem 0.7rem 0.55rem;
|
|
2026
|
+
border: 1px solid #3d3590;
|
|
2027
|
+
border-radius: 1rem;
|
|
2028
|
+
background: #1b1a3f;
|
|
2029
|
+
color: #c7cadc;
|
|
2030
|
+
box-shadow: 0 16px 40px rgba(3, 5, 12, 0.45);
|
|
2031
|
+
animation: bubble-in 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2032
|
+
}
|
|
2033
|
+
.bubble::after {
|
|
2034
|
+
content: "";
|
|
2035
|
+
position: absolute;
|
|
2036
|
+
right: var(--tail, 22px);
|
|
2037
|
+
bottom: -5px;
|
|
2038
|
+
width: 9px;
|
|
2039
|
+
height: 9px;
|
|
2040
|
+
border-right: 1px solid #3d3590;
|
|
2041
|
+
border-bottom: 1px solid #3d3590;
|
|
2042
|
+
background: #1b1a3f;
|
|
2043
|
+
transform: rotate(45deg);
|
|
2044
|
+
}
|
|
2045
|
+
.bubble .mark {
|
|
2046
|
+
display: block;
|
|
2047
|
+
width: 22px;
|
|
2048
|
+
height: 22px;
|
|
2049
|
+
margin: 0 0 0.35rem;
|
|
2050
|
+
}
|
|
2051
|
+
@keyframes bubble-in {
|
|
2052
|
+
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
|
2053
|
+
to { opacity: 1; transform: none; }
|
|
2054
|
+
}
|
|
2055
|
+
.bubble p {
|
|
2056
|
+
margin: 0;
|
|
2057
|
+
color: #c7cadc;
|
|
2058
|
+
font-size: 0.8125rem;
|
|
2059
|
+
font-weight: 400;
|
|
2060
|
+
line-height: 1.45;
|
|
2061
|
+
}
|
|
2062
|
+
.privacy {
|
|
2063
|
+
position: absolute;
|
|
2064
|
+
left: auto;
|
|
2065
|
+
right: 50%;
|
|
2066
|
+
bottom: calc(100% + 18px);
|
|
2067
|
+
z-index: 3;
|
|
2068
|
+
width: min(16.5rem, calc(100vw - 24px));
|
|
2069
|
+
max-width: min(16.5rem, calc(100vw - 24px));
|
|
2070
|
+
pointer-events: auto;
|
|
2071
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2072
|
+
transform: translateX(20px);
|
|
2073
|
+
}
|
|
2074
|
+
.privacy[hidden] { display: none; }
|
|
2075
|
+
.privacy .bubble {
|
|
2076
|
+
padding: 0.75rem 2.15rem 0.75rem 0.9rem;
|
|
2077
|
+
border-radius: 16px;
|
|
2078
|
+
}
|
|
2079
|
+
.privacy .bubble::after {
|
|
2080
|
+
right: var(--tail, 12px);
|
|
2081
|
+
bottom: -5px;
|
|
2082
|
+
width: 10px;
|
|
2083
|
+
height: 10px;
|
|
2084
|
+
}
|
|
2085
|
+
@media (max-width: 560px) {
|
|
2086
|
+
.word { display: none; }
|
|
2087
|
+
.privacy {
|
|
2088
|
+
position: fixed;
|
|
2089
|
+
left: 12px;
|
|
2090
|
+
right: 12px;
|
|
2091
|
+
bottom: 76px;
|
|
2092
|
+
width: auto;
|
|
2093
|
+
max-width: none;
|
|
2094
|
+
transform: none;
|
|
2095
|
+
}
|
|
2096
|
+
.privacy .bubble {
|
|
2097
|
+
padding: 0.65rem 1.85rem 0.6rem 0.75rem;
|
|
2098
|
+
}
|
|
2099
|
+
.privacy .bubble p { font-size: 0.75rem; }
|
|
2100
|
+
}
|
|
2101
|
+
.privacy button.hide {
|
|
2102
|
+
position: absolute;
|
|
2103
|
+
top: 0.4rem;
|
|
2104
|
+
right: 0.4rem;
|
|
2105
|
+
width: 24px;
|
|
2106
|
+
height: 24px;
|
|
2107
|
+
border: 0;
|
|
2108
|
+
background: transparent;
|
|
2109
|
+
color: #c7cadc;
|
|
2110
|
+
}
|
|
2111
|
+
.privacy button.hide:hover:not(:disabled) { color: #f4f3fc; }
|
|
2112
|
+
.choices { display: flex; gap: 6px; margin-top: 0.7rem; }
|
|
2113
|
+
.speak button.choice {
|
|
2114
|
+
width: auto;
|
|
2115
|
+
height: auto;
|
|
2116
|
+
min-height: 1.75rem;
|
|
2117
|
+
padding: 0 0.7rem;
|
|
2118
|
+
border-radius: 999px;
|
|
2119
|
+
font: 600 0.75rem/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2120
|
+
}
|
|
2121
|
+
.speak button.secondary {
|
|
2122
|
+
border: 1px solid #3a4458;
|
|
2123
|
+
background: transparent;
|
|
2124
|
+
color: #f4f3fc;
|
|
2125
|
+
}
|
|
2126
|
+
.speak button.secondary:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
2127
|
+
.speak button.danger {
|
|
2128
|
+
border: 0;
|
|
2129
|
+
background: #e5485f;
|
|
2130
|
+
color: #fff;
|
|
2131
|
+
}
|
|
2132
|
+
.speak button.danger:hover:not(:disabled) { color: #fff; background: color-mix(in srgb, #e5485f 88%, #fff); }
|
|
2133
|
+
</style>
|
|
2134
|
+
<div data-shell data-open="true" data-state="recording">
|
|
2135
|
+
<button type="button" class="launcher" data-action="open" aria-label="Abrir controles da grava\xE7\xE3o">
|
|
2136
|
+
${mark("offscript-trail-launcher")}
|
|
2137
|
+
</button>
|
|
2138
|
+
<div class="dock" role="toolbar" aria-label="Controles da grava\xE7\xE3o">
|
|
2139
|
+
<div class="brand">
|
|
2140
|
+
<span class="face">
|
|
2141
|
+
${mark("offscript-trail-dock")}
|
|
2142
|
+
<div class="privacy" data-privacy hidden>
|
|
2143
|
+
<div class="bubble" role="status">
|
|
2144
|
+
<button type="button" class="hide" data-action="privacy-hide" aria-label="Fechar aviso">${ICON.close}</button>
|
|
2145
|
+
<p>Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado \u2014 senha e dado sens\xEDvel tamb\xE9m.</p>
|
|
2146
|
+
</div>
|
|
2147
|
+
</div>
|
|
2148
|
+
</span>
|
|
2149
|
+
<span class="word">Offscript</span>
|
|
2150
|
+
</div>
|
|
2151
|
+
<div class="actions">
|
|
2152
|
+
<button type="button" data-action="privacy-show" aria-label="Abrir aviso" title="Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado" hidden>${ICON.speak}</button>
|
|
2153
|
+
<button type="button" class="text record" data-action="record">Iniciar</button>
|
|
2154
|
+
<button type="button" data-action="pause" aria-label="Pausar" title="Pausar">${ICON.pause}</button>
|
|
2155
|
+
<button type="button" data-action="resume" aria-label="Retomar" title="Retomar">${ICON.play}</button>
|
|
2156
|
+
<button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova grava\xE7\xE3o</button>
|
|
2157
|
+
<button type="button" class="text finish" data-action="stop" title="Encerra a coleta e envia o que foi gravado">Finalizar</button>
|
|
2158
|
+
<button type="button" data-action="close" aria-label="Fechar" title="Fechar">${ICON.close}</button>
|
|
2159
|
+
</div>
|
|
2160
|
+
</div>
|
|
2161
|
+
<div class="speak" data-confirm hidden>
|
|
2162
|
+
<div class="bubble" role="dialog" aria-labelledby="offscript-confirm-title">
|
|
2163
|
+
${mark("offscript-trail-speak")}
|
|
2164
|
+
<p id="offscript-confirm-title">Descartar esta grava\xE7\xE3o? O que foi gravado ser\xE1 ignorado.</p>
|
|
2165
|
+
<div class="choices">
|
|
2166
|
+
<button type="button" class="choice secondary" data-action="cancel">Cancelar</button>
|
|
2167
|
+
<button type="button" class="choice danger" data-action="confirm">Descartar</button>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
<p class="note" data-note></p>
|
|
2172
|
+
</div>
|
|
2173
|
+
`;
|
|
2174
|
+
var SUCCESS_TEMPLATE = `
|
|
2175
|
+
<style>
|
|
2176
|
+
:host { color-scheme: dark; }
|
|
2177
|
+
.success {
|
|
2178
|
+
display: flex;
|
|
2179
|
+
align-items: center;
|
|
2180
|
+
gap: 12px;
|
|
2181
|
+
padding: 12px 14px;
|
|
2182
|
+
border: 1px solid #377c65;
|
|
2183
|
+
border-radius: 16px;
|
|
2184
|
+
background: #152c25;
|
|
2185
|
+
color: #dcfce7;
|
|
2186
|
+
box-shadow: 0 12px 32px rgba(3, 5, 12, 0.35);
|
|
2187
|
+
font: 500 13px/1.5 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2188
|
+
}
|
|
2189
|
+
p { margin: 0; }
|
|
2190
|
+
button {
|
|
2191
|
+
flex: none;
|
|
2192
|
+
display: grid;
|
|
2193
|
+
place-items: center;
|
|
2194
|
+
width: 28px;
|
|
2195
|
+
height: 28px;
|
|
2196
|
+
padding: 0;
|
|
2197
|
+
border: 0;
|
|
2198
|
+
border-radius: 999px;
|
|
2199
|
+
background: transparent;
|
|
2200
|
+
color: inherit;
|
|
2201
|
+
font: 20px/1 ui-sans-serif, system-ui, sans-serif;
|
|
2202
|
+
cursor: pointer;
|
|
2203
|
+
}
|
|
2204
|
+
button:hover { background: #244a3c; }
|
|
2205
|
+
button:focus-visible { outline: 2px solid #86efac; outline-offset: 2px; }
|
|
2206
|
+
</style>
|
|
2207
|
+
<div class="success">
|
|
2208
|
+
<p role="status">Grava\xE7\xE3o enviada com sucesso!</p>
|
|
2209
|
+
<button type="button" aria-label="Fechar confirma\xE7\xE3o">\xD7</button>
|
|
2210
|
+
</div>
|
|
2211
|
+
`;
|
|
2212
|
+
|
|
2271
2213
|
// src/core/OffscriptSDK.ts
|
|
2272
|
-
var MAX_LOG = 5e3;
|
|
2273
2214
|
var Offscript = class {
|
|
2274
|
-
constructor(config) {
|
|
2215
|
+
constructor(config = {}) {
|
|
2275
2216
|
this.bus = new Emitter();
|
|
2276
2217
|
this.status = "idle";
|
|
2277
2218
|
this.recorders = [];
|
|
2278
2219
|
this.accepting = false;
|
|
2279
2220
|
this.destroyed = false;
|
|
2221
|
+
this.suspended = false;
|
|
2222
|
+
this.events = [];
|
|
2280
2223
|
this.recent = [];
|
|
2281
2224
|
this.actionLog = [];
|
|
2282
2225
|
this.contextLog = [];
|
|
2283
|
-
this.
|
|
2284
|
-
this.
|
|
2226
|
+
this.sequence = 0;
|
|
2227
|
+
this.pageId = uid("page_");
|
|
2228
|
+
this.submitted = false;
|
|
2285
2229
|
this.config = resolveConfig(config);
|
|
2286
2230
|
this.sanitizer = new DataSanitizer(this.config.privacy);
|
|
2287
|
-
this.sessions = new SessionManager(this.config);
|
|
2288
2231
|
}
|
|
2289
|
-
// ------------------------------------------------------------------ API pública
|
|
2290
2232
|
getState() {
|
|
2291
|
-
return { status: this.status,
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
return
|
|
2233
|
+
return { status: this.status, pending: this.submitted ? 0 : this.events.length, lastError: this.lastError };
|
|
2234
|
+
}
|
|
2235
|
+
capturesSensitiveValues() {
|
|
2236
|
+
return this.config.privacy.captureSensitiveValues;
|
|
2237
|
+
}
|
|
2238
|
+
async init() {
|
|
2239
|
+
this.assertAlive();
|
|
2240
|
+
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um navegador");
|
|
2241
|
+
this.initPromise ?? (this.initPromise = this.initializeDraft());
|
|
2242
|
+
await this.initPromise;
|
|
2243
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2244
|
+
if (this.config.controls && !this.controls) {
|
|
2245
|
+
this.controls = new RecordingControls(this);
|
|
2246
|
+
this.controls.mount();
|
|
2247
|
+
}
|
|
2295
2248
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
return this.initPromise;
|
|
2249
|
+
async initializeDraft() {
|
|
2250
|
+
if (!this.config.persistenceKey) return;
|
|
2251
|
+
this.store = await RecordingStore.open(this.config.persistenceKey);
|
|
2252
|
+
await this.restoreDraft();
|
|
2301
2253
|
}
|
|
2302
|
-
async
|
|
2303
|
-
|
|
2304
|
-
this.
|
|
2305
|
-
|
|
2306
|
-
|
|
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);
|
|
2254
|
+
async restoreDraft() {
|
|
2255
|
+
const saved = await this.store.load();
|
|
2256
|
+
if (this.suspended) return;
|
|
2257
|
+
this.clearRecording();
|
|
2258
|
+
if (!saved) {
|
|
2313
2259
|
this.setStatus("idle");
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
this.events = saved.events;
|
|
2263
|
+
this.sequence = saved.events[saved.events.length - 1]?.sequence ?? 0;
|
|
2264
|
+
this.actionLog = saved.events.filter((event) => event.kind === "action").map((event) => event.data);
|
|
2265
|
+
this.contextLog = saved.events.filter((event) => event.kind === "context").map((event) => event.data);
|
|
2266
|
+
this.setStatus(saved.status);
|
|
2267
|
+
if (saved.status === "stopped") {
|
|
2268
|
+
this.payload = this.buildPayload();
|
|
2269
|
+
return;
|
|
2319
2270
|
}
|
|
2271
|
+
this.accepting = saved.status === "recording";
|
|
2272
|
+
this.attach();
|
|
2273
|
+
await this.startRecorders();
|
|
2274
|
+
if (saved.status === "paused") this.recorders.forEach((recorder) => recorder.pause?.());
|
|
2275
|
+
}
|
|
2276
|
+
save(operation) {
|
|
2277
|
+
void operation.catch((error) => {
|
|
2278
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2279
|
+
this.lastError = { code: "LOCAL_STORAGE_FAILED", message: err.message };
|
|
2280
|
+
this.bus.emit("error", err);
|
|
2281
|
+
});
|
|
2320
2282
|
}
|
|
2321
|
-
|
|
2322
|
-
if (this.destroyed) throw new InvalidStateError("SDK destru\xEDdo");
|
|
2283
|
+
startRecording() {
|
|
2323
2284
|
if (this.startPromise) return this.startPromise;
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2285
|
+
this.startPromise = this.doStart().finally(() => {
|
|
2286
|
+
this.startPromise = void 0;
|
|
2287
|
+
});
|
|
2288
|
+
return this.startPromise;
|
|
2327
2289
|
}
|
|
2328
|
-
async doStart(
|
|
2290
|
+
async doStart() {
|
|
2329
2291
|
await this.init();
|
|
2330
|
-
|
|
2331
|
-
if (
|
|
2332
|
-
|
|
2333
|
-
|
|
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) {
|
|
2292
|
+
if (this.status === "recording" || this.status === "paused") return;
|
|
2293
|
+
if (this.completePromise || this.stopPromise) throw new InvalidStateError("Finaliza\xE7\xE3o em andamento");
|
|
2294
|
+
await this.store?.clear();
|
|
2295
|
+
this.clearRecording();
|
|
2359
2296
|
this.setStatus("initializing");
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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
|
-
}
|
|
2297
|
+
await this.store?.setStatus("recording");
|
|
2298
|
+
this.accepting = true;
|
|
2299
|
+
this.attach();
|
|
2300
|
+
this.emitLifecycle("recording-start");
|
|
2301
|
+
await this.startRecorders();
|
|
2302
|
+
this.setStatus("recording");
|
|
2303
|
+
await this.store?.flush();
|
|
2304
|
+
this.bus.emit("recordingStart", void 0);
|
|
2381
2305
|
}
|
|
2382
2306
|
async pauseRecording() {
|
|
2307
|
+
this.assertAlive();
|
|
2308
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2383
2309
|
if (this.status === "paused") return;
|
|
2384
|
-
if (this.status !== "recording") throw new InvalidStateError(
|
|
2310
|
+
if (this.status !== "recording") throw new InvalidStateError("Sem grava\xE7\xE3o ativa");
|
|
2385
2311
|
this.actions?.flushAll();
|
|
2386
|
-
this.emitLifecycle("pause"
|
|
2312
|
+
this.emitLifecycle("pause");
|
|
2387
2313
|
this.accepting = false;
|
|
2388
2314
|
this.recorders.forEach((r) => safe(() => r.pause?.()));
|
|
2389
|
-
this.sessions.setStatus("paused");
|
|
2390
2315
|
this.setStatus("paused");
|
|
2391
|
-
|
|
2316
|
+
await this.store?.flush();
|
|
2392
2317
|
}
|
|
2393
2318
|
async resumeRecording() {
|
|
2319
|
+
this.assertAlive();
|
|
2320
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2394
2321
|
if (this.status === "recording") return;
|
|
2395
|
-
if (this.status !== "paused") throw new InvalidStateError(
|
|
2322
|
+
if (this.status !== "paused") throw new InvalidStateError("Grava\xE7\xE3o n\xE3o est\xE1 pausada");
|
|
2396
2323
|
this.accepting = true;
|
|
2397
|
-
this.
|
|
2324
|
+
this.emitLifecycle("resume");
|
|
2325
|
+
for (const r of this.recorders) await r.resume?.();
|
|
2398
2326
|
this.setStatus("recording");
|
|
2399
|
-
this.
|
|
2400
|
-
for (const r of this.recorders) await Promise.resolve(safe(() => r.resume?.()));
|
|
2327
|
+
await this.store?.flush();
|
|
2401
2328
|
}
|
|
2402
2329
|
stopRecording() {
|
|
2403
2330
|
if (this.stopPromise) return this.stopPromise;
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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);
|
|
2331
|
+
this.stopPromise = this.doStop().finally(() => {
|
|
2332
|
+
this.stopPromise = void 0;
|
|
2333
|
+
});
|
|
2411
2334
|
return this.stopPromise;
|
|
2412
2335
|
}
|
|
2413
2336
|
async doStop() {
|
|
2414
|
-
|
|
2337
|
+
this.assertAlive();
|
|
2338
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2339
|
+
if (this.startPromise) await this.startPromise;
|
|
2340
|
+
if (this.payload) return this.payload;
|
|
2341
|
+
if (this.status !== "recording" && this.status !== "paused") throw new InvalidStateError("Sem grava\xE7\xE3o ativa");
|
|
2415
2342
|
this.setStatus("stopping");
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2343
|
+
this.actions?.flushAll();
|
|
2344
|
+
this.emitLifecycle("stop");
|
|
2345
|
+
const recorders = this.recorders.splice(0).reverse();
|
|
2346
|
+
this.lifecycle?.uninstall();
|
|
2347
|
+
for (const recorder of recorders) {
|
|
2348
|
+
if (!(recorder instanceof HTTPRecorder)) safe(() => recorder.stop());
|
|
2349
|
+
}
|
|
2350
|
+
for (const recorder of recorders) {
|
|
2351
|
+
if (recorder instanceof HTTPRecorder) await recorder.drain();
|
|
2419
2352
|
}
|
|
2420
2353
|
this.accepting = false;
|
|
2421
|
-
this.
|
|
2422
|
-
this.
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2354
|
+
this.rrweb = this.actions = this.context = void 0;
|
|
2355
|
+
this.payload = this.buildPayload();
|
|
2356
|
+
this.setStatus("stopped");
|
|
2357
|
+
await this.store?.flush();
|
|
2358
|
+
this.bus.emit("recordingEnd", this.payload);
|
|
2359
|
+
return this.payload;
|
|
2360
|
+
}
|
|
2361
|
+
complete(options) {
|
|
2362
|
+
this.assertAlive();
|
|
2363
|
+
if (this.completePromise) return this.completePromise;
|
|
2364
|
+
if (this.submitted) return Promise.resolve(this.response);
|
|
2365
|
+
this.completePromise = this.doComplete(options).finally(() => {
|
|
2366
|
+
this.completePromise = void 0;
|
|
2367
|
+
});
|
|
2368
|
+
return this.completePromise;
|
|
2369
|
+
}
|
|
2370
|
+
async doComplete({ endpoint, token }) {
|
|
2371
|
+
const url = new URL(endpoint, location.href);
|
|
2372
|
+
if (!["https:", "http:"].includes(url.protocol)) throw new ConfigError("Endpoint HTTP inv\xE1lido");
|
|
2373
|
+
if (!token?.trim()) throw new ConfigError("Token obrigat\xF3rio para envio");
|
|
2374
|
+
const payload = await this.stopRecording();
|
|
2375
|
+
try {
|
|
2376
|
+
const response = await fetch(url.href, {
|
|
2377
|
+
method: "POST",
|
|
2378
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${token.trim()}` },
|
|
2379
|
+
body: JSON.stringify(payload)
|
|
2380
|
+
});
|
|
2381
|
+
if (!response.ok) throw new OffscriptError("TRANSPORT_FAILED", `Falha ao enviar grava\xE7\xE3o: HTTP ${response.status}`, { status: response.status });
|
|
2382
|
+
this.submitted = true;
|
|
2383
|
+
await this.store?.clear();
|
|
2384
|
+
const text = await response.text();
|
|
2426
2385
|
try {
|
|
2427
|
-
|
|
2428
|
-
} catch
|
|
2429
|
-
this.
|
|
2386
|
+
this.response = text ? JSON.parse(text) : null;
|
|
2387
|
+
} catch {
|
|
2388
|
+
this.response = text;
|
|
2430
2389
|
}
|
|
2390
|
+
this.lastError = void 0;
|
|
2391
|
+
this.controls?.showSuccess();
|
|
2392
|
+
return this.response;
|
|
2393
|
+
} catch (error) {
|
|
2394
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2395
|
+
this.lastError = { code: err instanceof OffscriptError ? err.code : "INTERNAL", message: err.message };
|
|
2396
|
+
this.bus.emit("error", err);
|
|
2397
|
+
throw err;
|
|
2431
2398
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2399
|
+
}
|
|
2400
|
+
async finishRecording() {
|
|
2401
|
+
if (this.config.onComplete) return this.config.onComplete();
|
|
2402
|
+
return this.stopRecording();
|
|
2403
|
+
}
|
|
2404
|
+
async discardRecording() {
|
|
2405
|
+
this.assertAlive();
|
|
2406
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2407
|
+
if (this.startPromise) await this.startPromise;
|
|
2408
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2409
|
+
if (this.completePromise) throw new InvalidStateError("Envio em andamento");
|
|
2410
|
+
this.accepting = false;
|
|
2411
|
+
this.stopRecorders();
|
|
2434
2412
|
this.lifecycle?.uninstall();
|
|
2435
|
-
this.
|
|
2436
|
-
this.
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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 = {}) {
|
|
2413
|
+
await this.store?.clear();
|
|
2414
|
+
this.clearRecording();
|
|
2415
|
+
this.setStatus("idle");
|
|
2416
|
+
}
|
|
2417
|
+
async destroy() {
|
|
2447
2418
|
if (this.destroyed) return;
|
|
2448
|
-
if (
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
this.
|
|
2457
|
-
await this.teardown();
|
|
2458
|
-
this.sessions.reset();
|
|
2459
|
-
this.transport?.close();
|
|
2419
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2420
|
+
if (this.startPromise) await this.startPromise;
|
|
2421
|
+
if (this.completePromise) await this.completePromise.catch(() => void 0);
|
|
2422
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2423
|
+
this.accepting = false;
|
|
2424
|
+
this.stopRecorders();
|
|
2425
|
+
this.lifecycle?.uninstall();
|
|
2426
|
+
this.controls?.unmount();
|
|
2427
|
+
await this.store?.close();
|
|
2460
2428
|
this.bus.clear();
|
|
2429
|
+
this.clearRecording();
|
|
2430
|
+
this.destroyed = true;
|
|
2461
2431
|
}
|
|
2462
|
-
/** Dados estruturados para o motor Playwright, construídos a partir do que foi gravado nesta página. */
|
|
2463
2432
|
exportPlaywrightData() {
|
|
2464
|
-
|
|
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
|
-
});
|
|
2433
|
+
return chunkHPDLEAAL_cjs.buildPlaywrightContract({ actions: this.actionLog, contexts: this.contextLog });
|
|
2471
2434
|
}
|
|
2472
2435
|
getRecordedActions() {
|
|
2473
2436
|
return this.actionLog;
|
|
2474
2437
|
}
|
|
2475
|
-
// ------------------------------------------------------------------ EventSink
|
|
2476
2438
|
on(type, fn) {
|
|
2477
2439
|
return this.bus.on(type, fn);
|
|
2478
2440
|
}
|
|
2479
2441
|
off(type, fn) {
|
|
2480
2442
|
this.bus.off(type, fn);
|
|
2481
2443
|
}
|
|
2482
|
-
/** Usado pelos recorders. Não confundir com os eventos públicos (`on`). */
|
|
2483
2444
|
emit(kind, data, opts = {}) {
|
|
2484
2445
|
return this.record(kind, data, opts);
|
|
2485
2446
|
}
|
|
2486
2447
|
record(kind, data, opts) {
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
}
|
|
2498
|
-
this.
|
|
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;
|
|
2448
|
+
if (!this.accepting) return null;
|
|
2449
|
+
this.rrweb?.recoverIfNeeded();
|
|
2450
|
+
const sequence = ++this.sequence;
|
|
2451
|
+
const timestamp = opts.at ?? Date.now();
|
|
2452
|
+
const id = `${this.pageId}:${sequence}`;
|
|
2453
|
+
let payload = data;
|
|
2454
|
+
if (kind === "action") {
|
|
2455
|
+
const action = { ...data, id, sequence, timestamp };
|
|
2456
|
+
payload = action;
|
|
2457
|
+
this.actionLog.push(action);
|
|
2458
|
+
this.recent.push({ id, t: timestamp });
|
|
2459
|
+
if (this.recent.length > 50) this.recent.shift();
|
|
2544
2460
|
}
|
|
2461
|
+
const related = ["http", "console", "error", "iframe"].includes(kind) ? this.correlate(timestamp) : void 0;
|
|
2462
|
+
const event = {
|
|
2463
|
+
id,
|
|
2464
|
+
kind,
|
|
2465
|
+
tabId: this.pageId,
|
|
2466
|
+
pageId: this.pageId,
|
|
2467
|
+
sequence,
|
|
2468
|
+
timestamp,
|
|
2469
|
+
relatedActionId: related?.id,
|
|
2470
|
+
relatedActionDeltaMs: related ? timestamp - related.t : void 0,
|
|
2471
|
+
data: payload
|
|
2472
|
+
};
|
|
2473
|
+
if (kind === "context") this.contextLog.push(data);
|
|
2474
|
+
this.events.push(event);
|
|
2475
|
+
if (this.store) this.save(this.store.append(event));
|
|
2476
|
+
return event;
|
|
2545
2477
|
}
|
|
2546
2478
|
correlate(at) {
|
|
2547
2479
|
for (let i = this.recent.length - 1; i >= 0; i--) {
|
|
@@ -2557,18 +2489,27 @@ var Offscript = class {
|
|
|
2557
2489
|
attach() {
|
|
2558
2490
|
this.lifecycle ?? (this.lifecycle = new LifecycleManager({
|
|
2559
2491
|
onPageHide: () => {
|
|
2560
|
-
if (!this.accepting) return;
|
|
2561
2492
|
this.actions?.flushAll();
|
|
2562
|
-
|
|
2563
|
-
this.
|
|
2493
|
+
if (!this.store) return;
|
|
2494
|
+
this.accepting = false;
|
|
2495
|
+
this.stopRecorders();
|
|
2496
|
+
this.suspended = true;
|
|
2497
|
+
if (!this.restorePromise) this.store.checkpoint();
|
|
2498
|
+
},
|
|
2499
|
+
onPageShow: () => {
|
|
2500
|
+
if (!this.suspended) return;
|
|
2501
|
+
this.suspended = false;
|
|
2502
|
+
this.setStatus("initializing");
|
|
2503
|
+
this.restorePromise = this.restoreDraft().finally(() => {
|
|
2504
|
+
this.restorePromise = void 0;
|
|
2505
|
+
});
|
|
2506
|
+
this.save(this.restorePromise);
|
|
2564
2507
|
},
|
|
2565
2508
|
onVisibility: (hidden) => {
|
|
2566
|
-
if (hidden
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2509
|
+
if (hidden) this.actions?.flushAll();
|
|
2510
|
+
},
|
|
2511
|
+
onConnectivity: () => {
|
|
2570
2512
|
},
|
|
2571
|
-
onConnectivity: (online) => this.transport?.setOnline(online),
|
|
2572
2513
|
onNavigation: ({ from, to, kind }) => {
|
|
2573
2514
|
if (!this.accepting) return;
|
|
2574
2515
|
this.actions?.recordNavigation(from, to, kind);
|
|
@@ -2577,15 +2518,14 @@ var Offscript = class {
|
|
|
2577
2518
|
}
|
|
2578
2519
|
}));
|
|
2579
2520
|
this.lifecycle.install();
|
|
2580
|
-
this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
|
|
2581
2521
|
}
|
|
2582
2522
|
async startRecorders() {
|
|
2583
2523
|
const r = this.config.recording;
|
|
2584
2524
|
const list = [];
|
|
2585
2525
|
if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
|
|
2586
|
-
if (r.context) list.push(this.context = new ContextRecorder(this
|
|
2526
|
+
if (r.context) list.push(this.context = new ContextRecorder(this));
|
|
2587
2527
|
if (r.actions) list.push(this.actions = new ActionRecorder(this));
|
|
2588
|
-
if (r.http) list.push(new HTTPRecorder(this
|
|
2528
|
+
if (r.http) list.push(new HTTPRecorder(this));
|
|
2589
2529
|
if (r.console || r.errors) list.push(new ErrorRecorder(this));
|
|
2590
2530
|
if (r.iframes) list.push(new IframeRecorder(this));
|
|
2591
2531
|
this.recorders = list;
|
|
@@ -2598,74 +2538,48 @@ var Offscript = class {
|
|
|
2598
2538
|
}
|
|
2599
2539
|
}
|
|
2600
2540
|
}
|
|
2541
|
+
buildPayload() {
|
|
2542
|
+
return {
|
|
2543
|
+
recording: this.events.filter((event) => event.kind !== "http"),
|
|
2544
|
+
request: this.events.filter((event) => event.kind === "http"),
|
|
2545
|
+
regression: this.exportPlaywrightData()
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2601
2548
|
stopRecorders() {
|
|
2602
2549
|
for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
|
|
2603
2550
|
this.rrweb = this.actions = this.context = void 0;
|
|
2604
2551
|
}
|
|
2605
|
-
|
|
2606
|
-
this.
|
|
2607
|
-
this.
|
|
2608
|
-
this.
|
|
2609
|
-
this.
|
|
2610
|
-
this.
|
|
2611
|
-
this.
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
|
|
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);
|
|
2552
|
+
clearRecording() {
|
|
2553
|
+
this.events = [];
|
|
2554
|
+
this.actionLog = [];
|
|
2555
|
+
this.contextLog = [];
|
|
2556
|
+
this.recent = [];
|
|
2557
|
+
this.sequence = 0;
|
|
2558
|
+
this.pageId = uid("page_");
|
|
2559
|
+
this.payload = void 0;
|
|
2560
|
+
this.submitted = false;
|
|
2561
|
+
this.response = void 0;
|
|
2562
|
+
this.lastError = void 0;
|
|
2563
|
+
}
|
|
2564
|
+
assertAlive() {
|
|
2565
|
+
if (this.destroyed) throw new InvalidStateError("SDK destru\xEDdo");
|
|
2645
2566
|
}
|
|
2646
2567
|
setStatus(to) {
|
|
2647
2568
|
const from = this.status;
|
|
2648
2569
|
if (from === to) return;
|
|
2649
2570
|
this.status = to;
|
|
2571
|
+
if (this.store && (to === "recording" || to === "paused" || to === "stopped")) this.save(this.store.setStatus(to));
|
|
2650
2572
|
this.bus.emit("statusChange", { from, to });
|
|
2651
2573
|
}
|
|
2652
|
-
debug(...args) {
|
|
2653
|
-
if (this.config.debug) console.debug("[offscript]", ...args);
|
|
2654
|
-
}
|
|
2655
2574
|
};
|
|
2656
|
-
Offscript.version =
|
|
2575
|
+
Offscript.version = chunkHPDLEAAL_cjs.SDK_VERSION;
|
|
2657
2576
|
|
|
2658
|
-
exports.AuthError = AuthError;
|
|
2659
2577
|
exports.ConfigError = ConfigError;
|
|
2660
2578
|
exports.DEFAULT_PRIVACY = DEFAULT_PRIVACY;
|
|
2661
2579
|
exports.DEFAULT_RECORDING = DEFAULT_RECORDING;
|
|
2662
|
-
exports.DEFAULT_TRANSPORT = DEFAULT_TRANSPORT;
|
|
2663
2580
|
exports.DataSanitizer = DataSanitizer;
|
|
2664
2581
|
exports.InvalidStateError = InvalidStateError;
|
|
2665
2582
|
exports.Offscript = Offscript;
|
|
2666
2583
|
exports.OffscriptError = OffscriptError;
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
exports.TransportError = TransportError;
|
|
2670
|
-
//# sourceMappingURL=chunk-3NX2AGXM.cjs.map
|
|
2671
|
-
//# sourceMappingURL=chunk-3NX2AGXM.cjs.map
|
|
2584
|
+
//# sourceMappingURL=chunk-VYCDQLP7.cjs.map
|
|
2585
|
+
//# sourceMappingURL=chunk-VYCDQLP7.cjs.map
|