@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,4 +1,4 @@
|
|
|
1
|
-
import { buildPlaywrightContract, SDK_VERSION,
|
|
1
|
+
import { buildPlaywrightContract, SDK_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-6ROP37WI.js';
|
|
2
2
|
|
|
3
3
|
// src/types/config.ts
|
|
4
4
|
var DEFAULT_RECORDING = {
|
|
@@ -18,7 +18,7 @@ var DEFAULT_RECORDING = {
|
|
|
18
18
|
stableAttributes: ["name", "data-id", "aria-label"]
|
|
19
19
|
};
|
|
20
20
|
var DEFAULT_PRIVACY = {
|
|
21
|
-
maskSensitiveInputs:
|
|
21
|
+
maskSensitiveInputs: false,
|
|
22
22
|
maskAllText: false,
|
|
23
23
|
captureRequestBodies: false,
|
|
24
24
|
captureResponseBodies: false,
|
|
@@ -30,23 +30,9 @@ var DEFAULT_PRIVACY = {
|
|
|
30
30
|
ignoreUrls: [],
|
|
31
31
|
redactHeaders: [],
|
|
32
32
|
redactKeys: [],
|
|
33
|
+
captureSensitiveValues: false,
|
|
33
34
|
maxBodyBytes: 16 * 1024,
|
|
34
|
-
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i]
|
|
35
|
-
localRetentionMs: 24 * 36e5
|
|
36
|
-
};
|
|
37
|
-
var DEFAULT_TRANSPORT = {
|
|
38
|
-
batchMaxEvents: 200,
|
|
39
|
-
batchMaxBytes: 512 * 1024,
|
|
40
|
-
flushIntervalMs: 3e3,
|
|
41
|
-
maxMemoryEvents: 5e3,
|
|
42
|
-
maxBufferBytes: 64 * 1024 * 1024,
|
|
43
|
-
maxRetries: 6,
|
|
44
|
-
retryBaseMs: 500,
|
|
45
|
-
retryMaxMs: 3e4,
|
|
46
|
-
compress: false,
|
|
47
|
-
keepaliveMaxBytes: 60 * 1024,
|
|
48
|
-
persist: true,
|
|
49
|
-
requestTimeoutMs: 15e3
|
|
35
|
+
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i]
|
|
50
36
|
};
|
|
51
37
|
|
|
52
38
|
// src/utils/misc.ts
|
|
@@ -85,13 +71,6 @@ function debounce(fn, ms) {
|
|
|
85
71
|
};
|
|
86
72
|
return d;
|
|
87
73
|
}
|
|
88
|
-
function approxSize(value) {
|
|
89
|
-
try {
|
|
90
|
-
return JSON.stringify(value)?.length ?? 0;
|
|
91
|
-
} catch {
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
74
|
function deepMerge(base, patch) {
|
|
96
75
|
const out = { ...base };
|
|
97
76
|
if (!patch) return out;
|
|
@@ -159,13 +138,15 @@ var DataSanitizer = class {
|
|
|
159
138
|
this.cfg = cfg;
|
|
160
139
|
this.headerSet = /* @__PURE__ */ new Set([...BASE_SENSITIVE_HEADERS, ...cfg.redactHeaders.map((h) => h.toLowerCase())]);
|
|
161
140
|
this.extraKeys = new Set(cfg.redactKeys.map((k) => k.toLowerCase()));
|
|
141
|
+
this.open = cfg.captureSensitiveValues === true;
|
|
162
142
|
}
|
|
163
143
|
isSensitiveKey(key) {
|
|
144
|
+
if (this.open) return false;
|
|
164
145
|
return BASE_SENSITIVE_KEY_PATTERN.test(key) || this.extraKeys.has(key.toLowerCase());
|
|
165
146
|
}
|
|
166
147
|
/** Mascara padrões sensíveis em texto livre (JWT, Bearer, CPF, CNPJ, cartões). */
|
|
167
148
|
scrubString(input) {
|
|
168
|
-
if (!input) return input;
|
|
149
|
+
if (this.open || !input) return input;
|
|
169
150
|
let s = input.replace(JWT, REDACTED).replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
|
|
170
151
|
s = s.replace(CNPJ, REDACTED).replace(CPF, REDACTED);
|
|
171
152
|
s = s.replace(CARD, (m) => {
|
|
@@ -175,6 +156,7 @@ var DataSanitizer = class {
|
|
|
175
156
|
return s;
|
|
176
157
|
}
|
|
177
158
|
sanitizeHeaders(headers) {
|
|
159
|
+
if (this.open) return { ...headers };
|
|
178
160
|
const out = {};
|
|
179
161
|
for (const [k, v] of Object.entries(headers)) {
|
|
180
162
|
out[k] = this.headerSet.has(k.toLowerCase()) || this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
|
|
@@ -183,6 +165,7 @@ var DataSanitizer = class {
|
|
|
183
165
|
}
|
|
184
166
|
/** Redige query params sensíveis e credenciais embutidas (user:pass@). */
|
|
185
167
|
sanitizeUrl(raw, base) {
|
|
168
|
+
if (this.open) return raw;
|
|
186
169
|
try {
|
|
187
170
|
const u = new URL(raw, base ?? (typeof location !== "undefined" ? location.href : void 0));
|
|
188
171
|
if (u.username || u.password) {
|
|
@@ -201,7 +184,7 @@ var DataSanitizer = class {
|
|
|
201
184
|
}
|
|
202
185
|
/** Redige query/fragmento de um valor de atributo de URL preservando a forma original (relativa ou absoluta). */
|
|
203
186
|
redactUrlAttribute(value) {
|
|
204
|
-
if (typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
187
|
+
if (this.open || typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
205
188
|
const hashAt = value.indexOf("#");
|
|
206
189
|
const hash = hashAt >= 0 ? value.slice(hashAt) : "";
|
|
207
190
|
const noHash = hashAt >= 0 ? value.slice(0, hashAt) : value;
|
|
@@ -302,37 +285,12 @@ var ConfigError = class extends OffscriptError {
|
|
|
302
285
|
this.name = "ConfigError";
|
|
303
286
|
}
|
|
304
287
|
};
|
|
305
|
-
var AuthError = class extends OffscriptError {
|
|
306
|
-
constructor(message, status) {
|
|
307
|
-
super("AUTH_FAILED", message, { status });
|
|
308
|
-
this.name = "AuthError";
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
var SessionExpiredError = class extends OffscriptError {
|
|
312
|
-
constructor(message = "Sess\xE3o expirada") {
|
|
313
|
-
super("SESSION_EXPIRED", message, { status: 410 });
|
|
314
|
-
this.name = "SessionExpiredError";
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
var SessionDisabledError = class extends OffscriptError {
|
|
318
|
-
constructor(message = "Grava\xE7\xE3o desativada pelo backend") {
|
|
319
|
-
super("SESSION_DISABLED", message);
|
|
320
|
-
this.name = "SessionDisabledError";
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
288
|
var InvalidStateError = class extends OffscriptError {
|
|
324
289
|
constructor(message) {
|
|
325
290
|
super("INVALID_STATE", message);
|
|
326
291
|
this.name = "InvalidStateError";
|
|
327
292
|
}
|
|
328
293
|
};
|
|
329
|
-
var TransportError = class extends OffscriptError {
|
|
330
|
-
constructor(message, opts) {
|
|
331
|
-
super("TRANSPORT_FAILED", message, opts);
|
|
332
|
-
this.name = "TransportError";
|
|
333
|
-
this.retryable = opts.retryable;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
294
|
var Emitter = class {
|
|
337
295
|
constructor() {
|
|
338
296
|
this.handlers = /* @__PURE__ */ new Map();
|
|
@@ -359,591 +317,6 @@ var Emitter = class {
|
|
|
359
317
|
}
|
|
360
318
|
};
|
|
361
319
|
|
|
362
|
-
// src/core/ApiClient.ts
|
|
363
|
-
var ORIGINAL_FETCH = "__offscriptOriginal";
|
|
364
|
-
function nativeFetch() {
|
|
365
|
-
const f = globalThis.fetch;
|
|
366
|
-
if (!f) throw new TransportError("fetch indispon\xEDvel", { retryable: false });
|
|
367
|
-
return (f[ORIGINAL_FETCH] ?? f).bind(globalThis);
|
|
368
|
-
}
|
|
369
|
-
var ApiClient = class {
|
|
370
|
-
constructor(apiUrl, getToken, timeoutMs) {
|
|
371
|
-
this.apiUrl = apiUrl;
|
|
372
|
-
this.getToken = getToken;
|
|
373
|
-
this.timeoutMs = timeoutMs;
|
|
374
|
-
}
|
|
375
|
-
get baseUrl() {
|
|
376
|
-
return this.apiUrl;
|
|
377
|
-
}
|
|
378
|
-
createSession(body) {
|
|
379
|
-
return this.json("POST", "/sdk/sessions", body);
|
|
380
|
-
}
|
|
381
|
-
heartbeat(sessionId, body) {
|
|
382
|
-
return this.json("POST", `/sdk/sessions/${encodeURIComponent(sessionId)}/heartbeat`, body);
|
|
383
|
-
}
|
|
384
|
-
complete(sessionId, body) {
|
|
385
|
-
return this.json("POST", `/sdk/sessions/${encodeURIComponent(sessionId)}/complete`, body);
|
|
386
|
-
}
|
|
387
|
-
status(sessionId) {
|
|
388
|
-
return this.json("GET", `/sdk/sessions/${encodeURIComponent(sessionId)}/status`);
|
|
389
|
-
}
|
|
390
|
-
async sendEvents(batch, opts = {}) {
|
|
391
|
-
const path = `/sdk/sessions/${encodeURIComponent(batch.sessionId)}/events`;
|
|
392
|
-
return this.json("POST", path, batch, { "Idempotency-Key": batch.batchId }, opts);
|
|
393
|
-
}
|
|
394
|
-
async json(method, path, body, extraHeaders = {}, opts = {}) {
|
|
395
|
-
const headers = {
|
|
396
|
-
Accept: "application/json",
|
|
397
|
-
Authorization: `Bearer ${this.getToken()}`,
|
|
398
|
-
"X-Offscript-Sdk": SDK_VERSION,
|
|
399
|
-
...extraHeaders
|
|
400
|
-
};
|
|
401
|
-
let payload;
|
|
402
|
-
if (body !== void 0) {
|
|
403
|
-
headers["Content-Type"] = "application/json";
|
|
404
|
-
const text = JSON.stringify(body);
|
|
405
|
-
payload = text;
|
|
406
|
-
if (opts.compress && !opts.keepalive && text.length > 1024) {
|
|
407
|
-
const gz = await gzip(text);
|
|
408
|
-
if (gz) {
|
|
409
|
-
payload = gz;
|
|
410
|
-
headers["Content-Encoding"] = "gzip";
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
const ctrl = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
415
|
-
const timer = ctrl && !opts.keepalive ? setTimeout(() => ctrl.abort(), this.timeoutMs) : void 0;
|
|
416
|
-
let res;
|
|
417
|
-
try {
|
|
418
|
-
res = await nativeFetch()(this.apiUrl + path, {
|
|
419
|
-
method,
|
|
420
|
-
headers,
|
|
421
|
-
body: payload,
|
|
422
|
-
keepalive: opts.keepalive,
|
|
423
|
-
credentials: "omit",
|
|
424
|
-
mode: "cors",
|
|
425
|
-
signal: ctrl?.signal
|
|
426
|
-
});
|
|
427
|
-
} catch (e) {
|
|
428
|
-
throw new TransportError(`Falha de rede: ${e?.message ?? e}`, { retryable: true, cause: e });
|
|
429
|
-
} finally {
|
|
430
|
-
if (timer) clearTimeout(timer);
|
|
431
|
-
}
|
|
432
|
-
if (res.ok) {
|
|
433
|
-
const text = await res.text();
|
|
434
|
-
return text ? JSON.parse(text) : {};
|
|
435
|
-
}
|
|
436
|
-
const detail = await res.text().catch(() => "");
|
|
437
|
-
const msg = `HTTP ${res.status} ${method} ${path}${detail ? `: ${detail.slice(0, 200)}` : ""}`;
|
|
438
|
-
if (res.status === 401 || res.status === 403) throw new AuthError(msg, res.status);
|
|
439
|
-
if (res.status === 410) throw new SessionExpiredError(msg);
|
|
440
|
-
const retryAfter = Number(res.headers.get("retry-after"));
|
|
441
|
-
const err = new TransportError(msg, {
|
|
442
|
-
status: res.status,
|
|
443
|
-
retryable: res.status === 408 || res.status === 429 || res.status >= 500
|
|
444
|
-
});
|
|
445
|
-
if (retryAfter > 0) err.retryAfterMs = retryAfter * 1e3;
|
|
446
|
-
throw err;
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
async function gzip(text) {
|
|
450
|
-
try {
|
|
451
|
-
const CS = globalThis.CompressionStream;
|
|
452
|
-
if (!CS) return null;
|
|
453
|
-
const body = new Response(text).body;
|
|
454
|
-
if (!body) return null;
|
|
455
|
-
return new Uint8Array(await new Response(body.pipeThrough(new CS("gzip"))).arrayBuffer());
|
|
456
|
-
} catch {
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// src/transport/EventQueue.ts
|
|
462
|
-
var EventQueue = class {
|
|
463
|
-
constructor(maxEvents, maxBytes) {
|
|
464
|
-
this.maxEvents = maxEvents;
|
|
465
|
-
this.maxBytes = maxBytes;
|
|
466
|
-
this.staged = [];
|
|
467
|
-
this.pendingCount = 0;
|
|
468
|
-
this.pendingBytes = 0;
|
|
469
|
-
this.dropped = 0;
|
|
470
|
-
}
|
|
471
|
-
canAccept(priority) {
|
|
472
|
-
const factor = priority === "critical" ? 2 : priority === "normal" ? 1 : 0.5;
|
|
473
|
-
return this.pendingCount < this.maxEvents * factor && this.pendingBytes < this.maxBytes * factor;
|
|
474
|
-
}
|
|
475
|
-
noteDropped() {
|
|
476
|
-
this.dropped++;
|
|
477
|
-
}
|
|
478
|
-
push(e, size) {
|
|
479
|
-
this.staged.push(e);
|
|
480
|
-
this.pendingCount++;
|
|
481
|
-
this.pendingBytes += size;
|
|
482
|
-
}
|
|
483
|
-
/** Devolve eventos ao staging sem recontar. */
|
|
484
|
-
restore(events) {
|
|
485
|
-
this.staged = [...events, ...this.staged];
|
|
486
|
-
}
|
|
487
|
-
takeStaged() {
|
|
488
|
-
const s = this.staged;
|
|
489
|
-
this.staged = [];
|
|
490
|
-
return s;
|
|
491
|
-
}
|
|
492
|
-
peekStaged() {
|
|
493
|
-
return this.staged;
|
|
494
|
-
}
|
|
495
|
-
get stagedCount() {
|
|
496
|
-
return this.staged.length;
|
|
497
|
-
}
|
|
498
|
-
acked(count, bytes) {
|
|
499
|
-
this.pendingCount = Math.max(0, this.pendingCount - count);
|
|
500
|
-
this.pendingBytes = Math.max(0, this.pendingBytes - bytes);
|
|
501
|
-
}
|
|
502
|
-
reset(count, bytes) {
|
|
503
|
-
this.pendingCount = count;
|
|
504
|
-
this.pendingBytes = bytes;
|
|
505
|
-
}
|
|
506
|
-
get pending() {
|
|
507
|
-
return this.pendingCount;
|
|
508
|
-
}
|
|
509
|
-
get bytes() {
|
|
510
|
-
return this.pendingBytes;
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
// src/transport/RetryManager.ts
|
|
515
|
-
var RetryManager = class {
|
|
516
|
-
constructor(baseMs, maxMs, random = Math.random) {
|
|
517
|
-
this.baseMs = baseMs;
|
|
518
|
-
this.maxMs = maxMs;
|
|
519
|
-
this.random = random;
|
|
520
|
-
}
|
|
521
|
-
delay(attempt) {
|
|
522
|
-
const cap = Math.min(this.maxMs, this.baseMs * 2 ** Math.max(0, attempt));
|
|
523
|
-
return Math.round(this.random() * cap);
|
|
524
|
-
}
|
|
525
|
-
sleep(ms) {
|
|
526
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
// src/transport/EventTransport.ts
|
|
531
|
-
var EventTransport = class extends Emitter {
|
|
532
|
-
constructor(cfg, api, buffer) {
|
|
533
|
-
super();
|
|
534
|
-
this.cfg = cfg;
|
|
535
|
-
this.api = api;
|
|
536
|
-
this.buffer = buffer;
|
|
537
|
-
this.identity = null;
|
|
538
|
-
this.running = null;
|
|
539
|
-
this.again = false;
|
|
540
|
-
this.failures = 0;
|
|
541
|
-
this.stopped = true;
|
|
542
|
-
this.fatal = false;
|
|
543
|
-
this.online = true;
|
|
544
|
-
this.lastDroppedReported = 0;
|
|
545
|
-
this.queue = new EventQueue(cfg.maxMemoryEvents, cfg.maxBufferBytes);
|
|
546
|
-
this.retry = new RetryManager(cfg.retryBaseMs, cfg.retryMaxMs);
|
|
547
|
-
}
|
|
548
|
-
get bufferKind() {
|
|
549
|
-
return this.buffer.kind;
|
|
550
|
-
}
|
|
551
|
-
/** Inicia o transporte para uma sessão/aba e recupera o que ficou pendente no buffer. */
|
|
552
|
-
async start(identity) {
|
|
553
|
-
this.identity = identity;
|
|
554
|
-
this.stopped = false;
|
|
555
|
-
this.fatal = false;
|
|
556
|
-
this.failures = 0;
|
|
557
|
-
const st = await this.buffer.stats(identity.sessionId).catch(() => ({ count: 0, bytes: 0 }));
|
|
558
|
-
this.queue.reset(st.count, st.bytes);
|
|
559
|
-
this.scheduleFlush();
|
|
560
|
-
}
|
|
561
|
-
setOnline(online) {
|
|
562
|
-
this.online = online;
|
|
563
|
-
if (online && !this.stopped) void this.flush();
|
|
564
|
-
}
|
|
565
|
-
canAccept(priority) {
|
|
566
|
-
return !this.stopped && !this.fatal && this.queue.canAccept(priority);
|
|
567
|
-
}
|
|
568
|
-
noteDropped() {
|
|
569
|
-
this.queue.noteDropped();
|
|
570
|
-
if (this.queue.dropped - this.lastDroppedReported >= 1) {
|
|
571
|
-
this.lastDroppedReported = this.queue.dropped;
|
|
572
|
-
this.emit("overflow", { dropped: this.queue.dropped });
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
get pending() {
|
|
576
|
-
return this.queue.pending;
|
|
577
|
-
}
|
|
578
|
-
enqueue(event) {
|
|
579
|
-
if (this.stopped) return;
|
|
580
|
-
this.queue.push(event, approxSize(event));
|
|
581
|
-
if (this.queue.stagedCount >= 50) void this.commit();
|
|
582
|
-
else if (!this.commitTimer) this.commitTimer = setTimeout(() => void this.commit(), 100);
|
|
583
|
-
}
|
|
584
|
-
async commit() {
|
|
585
|
-
if (this.commitTimer) clearTimeout(this.commitTimer);
|
|
586
|
-
this.commitTimer = void 0;
|
|
587
|
-
const staged = this.queue.takeStaged();
|
|
588
|
-
if (!staged.length) return;
|
|
589
|
-
try {
|
|
590
|
-
await this.buffer.put(staged);
|
|
591
|
-
} catch {
|
|
592
|
-
this.queue.restore(staged);
|
|
593
|
-
throw new TransportError("Falha ao persistir eventos localmente", { retryable: true });
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
scheduleFlush() {
|
|
597
|
-
if (this.flushTimer || this.stopped) return;
|
|
598
|
-
this.flushTimer = setTimeout(() => {
|
|
599
|
-
this.flushTimer = void 0;
|
|
600
|
-
void this.flush().finally(() => this.scheduleFlush());
|
|
601
|
-
}, this.cfg.flushIntervalMs);
|
|
602
|
-
}
|
|
603
|
-
/** Flush de melhor esforço. Retorna true se tudo foi confirmado pelo backend. */
|
|
604
|
-
flush(opts = { inlineRetries: 0 }) {
|
|
605
|
-
if (this.running) {
|
|
606
|
-
this.again = true;
|
|
607
|
-
return this.running;
|
|
608
|
-
}
|
|
609
|
-
this.running = (async () => {
|
|
610
|
-
try {
|
|
611
|
-
let ok = await this.drain(opts);
|
|
612
|
-
while (this.again && ok) {
|
|
613
|
-
this.again = false;
|
|
614
|
-
ok = await this.drain(opts);
|
|
615
|
-
}
|
|
616
|
-
return ok;
|
|
617
|
-
} finally {
|
|
618
|
-
this.running = null;
|
|
619
|
-
this.again = false;
|
|
620
|
-
}
|
|
621
|
-
})();
|
|
622
|
-
return this.running;
|
|
623
|
-
}
|
|
624
|
-
/** Flush final com retries inline e prazo máximo. */
|
|
625
|
-
async flushFinal(maxWaitMs = 1e4) {
|
|
626
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
627
|
-
this.retryTimer = void 0;
|
|
628
|
-
const running = this.running;
|
|
629
|
-
if (running) await running.catch(() => false);
|
|
630
|
-
const ok = await this.flush({ inlineRetries: this.cfg.maxRetries, deadline: Date.now() + maxWaitMs });
|
|
631
|
-
return { ok, pending: this.queue.pending };
|
|
632
|
-
}
|
|
633
|
-
async drain(opts) {
|
|
634
|
-
const id = this.identity;
|
|
635
|
-
if (!id || this.fatal) return false;
|
|
636
|
-
for (; ; ) {
|
|
637
|
-
try {
|
|
638
|
-
await this.commit();
|
|
639
|
-
const { items } = await this.buffer.peek(id.sessionId, id.tabId, this.cfg.batchMaxEvents, this.cfg.batchMaxBytes);
|
|
640
|
-
if (!items.length) {
|
|
641
|
-
this.failures = 0;
|
|
642
|
-
return true;
|
|
643
|
-
}
|
|
644
|
-
const events = items.map((i) => i.event);
|
|
645
|
-
await this.sendWithRetry(events, opts);
|
|
646
|
-
await this.buffer.remove(items.map((i) => i.key));
|
|
647
|
-
this.queue.acked(items.length, items.reduce((n, i) => n + i.size, 0));
|
|
648
|
-
this.failures = 0;
|
|
649
|
-
} catch (error) {
|
|
650
|
-
return this.onFailure(error);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
onFailure(error) {
|
|
655
|
-
if (error instanceof AuthError || error instanceof SessionExpiredError) {
|
|
656
|
-
this.fatal = true;
|
|
657
|
-
this.emit("fatal", { error });
|
|
658
|
-
return false;
|
|
659
|
-
}
|
|
660
|
-
this.failures++;
|
|
661
|
-
const retryable = !(error instanceof TransportError) || error.retryable;
|
|
662
|
-
const willRetryInMs = retryable ? this.retry.delay(this.failures) : void 0;
|
|
663
|
-
this.emit("failed", { error, willRetryInMs });
|
|
664
|
-
if (retryable && !this.stopped) {
|
|
665
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
666
|
-
this.retryTimer = setTimeout(() => {
|
|
667
|
-
this.retryTimer = void 0;
|
|
668
|
-
void this.flush();
|
|
669
|
-
}, willRetryInMs);
|
|
670
|
-
}
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
|
-
async sendWithRetry(events, opts) {
|
|
674
|
-
const batch = this.buildBatch(events);
|
|
675
|
-
for (let attempt = 0; ; attempt++) {
|
|
676
|
-
try {
|
|
677
|
-
if (!this.online) throw new TransportError("Offline", { retryable: true });
|
|
678
|
-
const ack = await this.api.sendEvents(batch, { compress: this.cfg.compress });
|
|
679
|
-
this.emit("flushed", { events: events.length, batchId: batch.batchId, duplicate: !!ack.duplicate });
|
|
680
|
-
return;
|
|
681
|
-
} catch (e) {
|
|
682
|
-
if (e instanceof TransportError && e.status === 413 && events.length > 1) {
|
|
683
|
-
const mid = Math.ceil(events.length / 2);
|
|
684
|
-
await this.sendWithRetry(events.slice(0, mid), opts);
|
|
685
|
-
await this.sendWithRetry(events.slice(mid), opts);
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
const retryable = !(e instanceof TransportError) || e.retryable;
|
|
689
|
-
if (!retryable || attempt >= opts.inlineRetries) throw e;
|
|
690
|
-
const wait = Math.max(this.retry.delay(attempt), e.retryAfterMs ?? 0);
|
|
691
|
-
if (opts.deadline && Date.now() + wait > opts.deadline) throw e;
|
|
692
|
-
await this.retry.sleep(wait);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
buildBatch(events) {
|
|
697
|
-
const id = this.identity;
|
|
698
|
-
const first = events[0].sequence;
|
|
699
|
-
const last = events[events.length - 1].sequence;
|
|
700
|
-
return {
|
|
701
|
-
schemaVersion: SCHEMA_VERSION,
|
|
702
|
-
batchId: `${id.sessionId}.${id.tabId}.${first}-${last}.${events.length}`,
|
|
703
|
-
sessionId: id.sessionId,
|
|
704
|
-
executionId: id.executionId,
|
|
705
|
-
tabId: id.tabId,
|
|
706
|
-
sequenceStart: first,
|
|
707
|
-
sequenceEnd: last,
|
|
708
|
-
createdAt: Date.now(),
|
|
709
|
-
sdkVersion: SDK_VERSION,
|
|
710
|
-
events
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Chamado em pagehide/beforeunload. Persiste o que está em staging (best effort, assíncrono) e
|
|
715
|
-
* tenta enviar o final da fila com fetch keepalive (limite ~64KB). Sem ACK: os eventos continuam
|
|
716
|
-
* no IndexedDB e serão reenviados (de forma idempotente) na próxima página.
|
|
717
|
-
* Não há garantia de entrega se o navegador for encerrado abruptamente.
|
|
718
|
-
*/
|
|
719
|
-
flushOnUnload() {
|
|
720
|
-
const id = this.identity;
|
|
721
|
-
if (!id || this.stopped || this.fatal) return;
|
|
722
|
-
const staged = this.queue.takeStaged();
|
|
723
|
-
if (!staged.length) return;
|
|
724
|
-
safe(() => void this.buffer.put(staged).catch(() => void 0));
|
|
725
|
-
const events = [];
|
|
726
|
-
let bytes = 0;
|
|
727
|
-
for (const e of staged) {
|
|
728
|
-
const s = approxSize(e);
|
|
729
|
-
if (bytes + s > this.cfg.keepaliveMaxBytes) break;
|
|
730
|
-
events.push(e);
|
|
731
|
-
bytes += s;
|
|
732
|
-
}
|
|
733
|
-
if (!events.length) return;
|
|
734
|
-
safe(() => void this.api.sendEvents(this.buildBatch(events), { keepalive: true }).catch(() => void 0));
|
|
735
|
-
}
|
|
736
|
-
/** Para timers; eventos não confirmados permanecem no buffer. */
|
|
737
|
-
stop() {
|
|
738
|
-
this.stopped = true;
|
|
739
|
-
for (const t of [this.commitTimer, this.flushTimer, this.retryTimer]) if (t) clearTimeout(t);
|
|
740
|
-
this.commitTimer = this.flushTimer = this.retryTimer = void 0;
|
|
741
|
-
}
|
|
742
|
-
async clearSession(sessionId) {
|
|
743
|
-
await this.buffer.clearSession(sessionId).catch(() => void 0);
|
|
744
|
-
this.queue.reset(0, 0);
|
|
745
|
-
}
|
|
746
|
-
async purgeExpired(retentionMs) {
|
|
747
|
-
await this.buffer.purgeOlderThan(Date.now() - retentionMs).catch(() => void 0);
|
|
748
|
-
}
|
|
749
|
-
close() {
|
|
750
|
-
this.stop();
|
|
751
|
-
this.buffer.close();
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
// src/transport/PersistentBuffer.ts
|
|
756
|
-
var eventKey = (sessionId, tabId, seq) => `${sessionId}|${tabId}|${String(seq).padStart(12, "0")}`;
|
|
757
|
-
var toStored = (e) => ({
|
|
758
|
-
key: eventKey(e.sessionId, e.tabId, e.sequence),
|
|
759
|
-
sessionId: e.sessionId,
|
|
760
|
-
tabId: e.tabId,
|
|
761
|
-
sequence: e.sequence,
|
|
762
|
-
storedAt: Date.now(),
|
|
763
|
-
size: approxSize(e),
|
|
764
|
-
event: e
|
|
765
|
-
});
|
|
766
|
-
var MemoryBuffer = class {
|
|
767
|
-
constructor() {
|
|
768
|
-
this.kind = "memory";
|
|
769
|
-
this.map = /* @__PURE__ */ new Map();
|
|
770
|
-
}
|
|
771
|
-
async put(events) {
|
|
772
|
-
for (const e of events) {
|
|
773
|
-
const s = toStored(e);
|
|
774
|
-
this.map.set(s.key, s);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
async peek(sessionId, tabId, maxEvents, maxBytes) {
|
|
778
|
-
const prefix = `${sessionId}|${tabId}|`;
|
|
779
|
-
const keys = Array.from(this.map.keys()).filter((k) => k.startsWith(prefix)).sort();
|
|
780
|
-
const items = [];
|
|
781
|
-
let bytes = 0;
|
|
782
|
-
for (const k of keys) {
|
|
783
|
-
const s = this.map.get(k);
|
|
784
|
-
if (items.length >= maxEvents || items.length > 0 && bytes + s.size > maxBytes) break;
|
|
785
|
-
items.push(s);
|
|
786
|
-
bytes += s.size;
|
|
787
|
-
}
|
|
788
|
-
return { items, bytes };
|
|
789
|
-
}
|
|
790
|
-
async remove(keys) {
|
|
791
|
-
for (const k of keys) this.map.delete(k);
|
|
792
|
-
}
|
|
793
|
-
async stats(sessionId) {
|
|
794
|
-
let count = 0;
|
|
795
|
-
let bytes = 0;
|
|
796
|
-
for (const s of this.map.values()) {
|
|
797
|
-
if (sessionId && s.sessionId !== sessionId) continue;
|
|
798
|
-
count++;
|
|
799
|
-
bytes += s.size;
|
|
800
|
-
}
|
|
801
|
-
return { count, bytes };
|
|
802
|
-
}
|
|
803
|
-
async purgeOlderThan(ts) {
|
|
804
|
-
let n = 0;
|
|
805
|
-
for (const [k, s] of this.map) if (s.storedAt < ts) this.map.delete(k), n++;
|
|
806
|
-
return n;
|
|
807
|
-
}
|
|
808
|
-
async clearSession(sessionId) {
|
|
809
|
-
for (const [k, s] of this.map) if (s.sessionId === sessionId) this.map.delete(k);
|
|
810
|
-
}
|
|
811
|
-
close() {
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
var STORE = "events";
|
|
815
|
-
function req(r) {
|
|
816
|
-
return new Promise((resolve, reject) => {
|
|
817
|
-
r.onsuccess = () => resolve(r.result);
|
|
818
|
-
r.onerror = () => reject(r.error);
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
function done(tx) {
|
|
822
|
-
return new Promise((resolve, reject) => {
|
|
823
|
-
tx.oncomplete = () => resolve();
|
|
824
|
-
tx.onerror = () => reject(tx.error);
|
|
825
|
-
tx.onabort = () => reject(tx.error);
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
var IndexedDBBuffer = class _IndexedDBBuffer {
|
|
829
|
-
constructor(db) {
|
|
830
|
-
this.db = db;
|
|
831
|
-
this.kind = "indexeddb";
|
|
832
|
-
}
|
|
833
|
-
static async open(name) {
|
|
834
|
-
if (typeof indexedDB === "undefined") throw new Error("IndexedDB indispon\xEDvel");
|
|
835
|
-
const db = await new Promise((resolve, reject) => {
|
|
836
|
-
const r = indexedDB.open(name, 1);
|
|
837
|
-
r.onupgradeneeded = () => {
|
|
838
|
-
const store = r.result.createObjectStore(STORE, { keyPath: "key" });
|
|
839
|
-
store.createIndex("bySession", "sessionId");
|
|
840
|
-
store.createIndex("byStoredAt", "storedAt");
|
|
841
|
-
};
|
|
842
|
-
r.onsuccess = () => resolve(r.result);
|
|
843
|
-
r.onerror = () => reject(r.error);
|
|
844
|
-
r.onblocked = () => reject(new Error("IndexedDB bloqueado"));
|
|
845
|
-
});
|
|
846
|
-
return new _IndexedDBBuffer(db);
|
|
847
|
-
}
|
|
848
|
-
async put(events) {
|
|
849
|
-
if (!events.length) return;
|
|
850
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
851
|
-
const store = tx.objectStore(STORE);
|
|
852
|
-
for (const e of events) store.put(toStored(e));
|
|
853
|
-
await done(tx);
|
|
854
|
-
}
|
|
855
|
-
async peek(sessionId, tabId, maxEvents, maxBytes) {
|
|
856
|
-
const prefix = `${sessionId}|${tabId}|`;
|
|
857
|
-
const range = IDBKeyRange.bound(prefix, prefix + "\uFFFF");
|
|
858
|
-
const tx = this.db.transaction(STORE, "readonly");
|
|
859
|
-
const items = [];
|
|
860
|
-
let bytes = 0;
|
|
861
|
-
await new Promise((resolve, reject) => {
|
|
862
|
-
const cur = tx.objectStore(STORE).openCursor(range);
|
|
863
|
-
cur.onsuccess = () => {
|
|
864
|
-
const c = cur.result;
|
|
865
|
-
if (!c) return resolve();
|
|
866
|
-
const s = c.value;
|
|
867
|
-
if (items.length >= maxEvents || items.length > 0 && bytes + s.size > maxBytes) return resolve();
|
|
868
|
-
items.push(s);
|
|
869
|
-
bytes += s.size;
|
|
870
|
-
c.continue();
|
|
871
|
-
};
|
|
872
|
-
cur.onerror = () => reject(cur.error);
|
|
873
|
-
});
|
|
874
|
-
return { items, bytes };
|
|
875
|
-
}
|
|
876
|
-
async remove(keys) {
|
|
877
|
-
if (!keys.length) return;
|
|
878
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
879
|
-
const store = tx.objectStore(STORE);
|
|
880
|
-
for (const k of keys) store.delete(k);
|
|
881
|
-
await done(tx);
|
|
882
|
-
}
|
|
883
|
-
async stats(sessionId) {
|
|
884
|
-
const tx = this.db.transaction(STORE, "readonly");
|
|
885
|
-
const store = tx.objectStore(STORE);
|
|
886
|
-
if (!sessionId) return { count: await req(store.count()), bytes: 0 };
|
|
887
|
-
let count = 0;
|
|
888
|
-
let bytes = 0;
|
|
889
|
-
await new Promise((resolve, reject) => {
|
|
890
|
-
const cur = store.index("bySession").openCursor(IDBKeyRange.only(sessionId));
|
|
891
|
-
cur.onsuccess = () => {
|
|
892
|
-
const c = cur.result;
|
|
893
|
-
if (!c) return resolve();
|
|
894
|
-
count++;
|
|
895
|
-
bytes += c.value.size;
|
|
896
|
-
c.continue();
|
|
897
|
-
};
|
|
898
|
-
cur.onerror = () => reject(cur.error);
|
|
899
|
-
});
|
|
900
|
-
return { count, bytes };
|
|
901
|
-
}
|
|
902
|
-
async purgeOlderThan(ts) {
|
|
903
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
904
|
-
let n = 0;
|
|
905
|
-
await new Promise((resolve, reject) => {
|
|
906
|
-
const cur = tx.objectStore(STORE).index("byStoredAt").openCursor(IDBKeyRange.upperBound(ts, true));
|
|
907
|
-
cur.onsuccess = () => {
|
|
908
|
-
const c = cur.result;
|
|
909
|
-
if (!c) return resolve();
|
|
910
|
-
c.delete();
|
|
911
|
-
n++;
|
|
912
|
-
c.continue();
|
|
913
|
-
};
|
|
914
|
-
cur.onerror = () => reject(cur.error);
|
|
915
|
-
});
|
|
916
|
-
await done(tx);
|
|
917
|
-
return n;
|
|
918
|
-
}
|
|
919
|
-
async clearSession(sessionId) {
|
|
920
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
921
|
-
await new Promise((resolve, reject) => {
|
|
922
|
-
const cur = tx.objectStore(STORE).index("bySession").openCursor(IDBKeyRange.only(sessionId));
|
|
923
|
-
cur.onsuccess = () => {
|
|
924
|
-
const c = cur.result;
|
|
925
|
-
if (!c) return resolve();
|
|
926
|
-
c.delete();
|
|
927
|
-
c.continue();
|
|
928
|
-
};
|
|
929
|
-
cur.onerror = () => reject(cur.error);
|
|
930
|
-
});
|
|
931
|
-
await done(tx);
|
|
932
|
-
}
|
|
933
|
-
close() {
|
|
934
|
-
safe(() => this.db.close());
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
async function openBuffer(name, persist) {
|
|
938
|
-
if (persist) {
|
|
939
|
-
try {
|
|
940
|
-
return await IndexedDBBuffer.open(name);
|
|
941
|
-
} catch {
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
return new MemoryBuffer();
|
|
945
|
-
}
|
|
946
|
-
|
|
947
320
|
// src/recorders/types.ts
|
|
948
321
|
function guard(sink, name, fn) {
|
|
949
322
|
try {
|
|
@@ -958,6 +331,7 @@ function guard(sink, name, fn) {
|
|
|
958
331
|
|
|
959
332
|
// src/recorders/ActionRecorder.ts
|
|
960
333
|
var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
334
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["Control", "Shift", "Alt", "Meta"]);
|
|
961
335
|
var INTERACTIVE = "a,button,input,select,textarea,summary,[role=button],[role=link],[role=menuitem],[role=tab],[role=option],[data-testid],[data-test],[data-cy]";
|
|
962
336
|
var INPUT_IDLE_MS = 700;
|
|
963
337
|
var DRAG_MIN_PX = 8;
|
|
@@ -1083,7 +457,7 @@ var ActionRecorder = class {
|
|
|
1083
457
|
onInput(e) {
|
|
1084
458
|
const el = this.target(e);
|
|
1085
459
|
if (!el || !(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return;
|
|
1086
|
-
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"
|
|
460
|
+
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"].includes(el.type)) return;
|
|
1087
461
|
if (this.pendingInput && this.pendingInput.el !== el) this.flushInput();
|
|
1088
462
|
this.pendingInput ?? (this.pendingInput = { el, inputType: e.inputType ?? "input", startedAt: Date.now() });
|
|
1089
463
|
if (this.inputTimer) clearTimeout(this.inputTimer);
|
|
@@ -1101,8 +475,9 @@ var ActionRecorder = class {
|
|
|
1101
475
|
const policy = this.sink.config.privacy;
|
|
1102
476
|
const allowed = policy.allowedInputSelectors.join(",");
|
|
1103
477
|
const isPassword = el instanceof HTMLInputElement && el.type === "password";
|
|
1104
|
-
|
|
1105
|
-
|
|
478
|
+
const listed = Boolean(allowed) && safe(() => el.matches(allowed));
|
|
479
|
+
if (policy.captureSensitiveValues || !isPassword && (!policy.maskSensitiveInputs || listed)) {
|
|
480
|
+
payload.value = policy.captureSensitiveValues ? value : this.sink.sanitizer.scrubString(value);
|
|
1106
481
|
payload.valueCaptured = true;
|
|
1107
482
|
} else {
|
|
1108
483
|
payload.valueCaptured = false;
|
|
@@ -1130,13 +505,15 @@ var ActionRecorder = class {
|
|
|
1130
505
|
}
|
|
1131
506
|
}
|
|
1132
507
|
onKey(e) {
|
|
1133
|
-
const el = this.target(e);
|
|
508
|
+
const el = this.target(e) ?? document.body;
|
|
1134
509
|
if (!el) return;
|
|
1135
510
|
const modified = e.ctrlKey || e.metaKey || e.altKey;
|
|
1136
|
-
const
|
|
1137
|
-
|
|
511
|
+
const isModifier = MODIFIERS.has(e.key);
|
|
512
|
+
const relevant = KEYS.has(e.key) || isModifier || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
|
|
513
|
+
if (!relevant || isModifier && e.repeat) return;
|
|
1138
514
|
if (e.key === "Enter" || e.key === "Tab") this.flushInput();
|
|
1139
|
-
|
|
515
|
+
const modifiers = mods(e).filter((name) => name !== e.key);
|
|
516
|
+
this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers, repeat: e.repeat } }, el);
|
|
1140
517
|
}
|
|
1141
518
|
onFocus(type, e) {
|
|
1142
519
|
const el = this.target(e);
|
|
@@ -1269,7 +646,7 @@ function parseBrowser(ua) {
|
|
|
1269
646
|
}
|
|
1270
647
|
return {};
|
|
1271
648
|
}
|
|
1272
|
-
function collectContext(
|
|
649
|
+
function collectContext(reason, captureSensitiveValues = false) {
|
|
1273
650
|
const nav = navigator;
|
|
1274
651
|
const ua = nav.userAgent ?? "";
|
|
1275
652
|
let browser = parseBrowser(ua);
|
|
@@ -1293,17 +670,13 @@ function collectContext(id, reason, appVersion) {
|
|
|
1293
670
|
timezone: safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
1294
671
|
online: nav.onLine,
|
|
1295
672
|
visibility: document.visibilityState,
|
|
1296
|
-
platformId: id.platformId,
|
|
1297
|
-
testCaseId: id.testCaseId,
|
|
1298
|
-
executionId: id.executionId,
|
|
1299
673
|
sdkVersion: SDK_VERSION,
|
|
1300
|
-
|
|
674
|
+
captureSensitiveValues
|
|
1301
675
|
};
|
|
1302
676
|
}
|
|
1303
677
|
var ContextRecorder = class {
|
|
1304
|
-
constructor(sink
|
|
678
|
+
constructor(sink) {
|
|
1305
679
|
this.sink = sink;
|
|
1306
|
-
this.identity = identity;
|
|
1307
680
|
this.name = "context";
|
|
1308
681
|
this.d = new Disposer();
|
|
1309
682
|
}
|
|
@@ -1316,7 +689,7 @@ var ContextRecorder = class {
|
|
|
1316
689
|
}
|
|
1317
690
|
record(reason) {
|
|
1318
691
|
guard(this.sink, this.name, () => {
|
|
1319
|
-
const data = collectContext(
|
|
692
|
+
const data = collectContext(reason, this.sink.config.privacy.captureSensitiveValues);
|
|
1320
693
|
data.url = this.sink.sanitizer.sanitizeUrl(data.url);
|
|
1321
694
|
if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
|
|
1322
695
|
this.sink.emit("context", data, { priority: "normal" });
|
|
@@ -1475,23 +848,28 @@ function uid(prefix = "") {
|
|
|
1475
848
|
}
|
|
1476
849
|
|
|
1477
850
|
// src/recorders/HTTPRecorder.ts
|
|
1478
|
-
var XHR_PATCHED = "__offscriptXhrOriginal";
|
|
1479
851
|
var HTTPRecorder = class {
|
|
1480
|
-
constructor(sink
|
|
852
|
+
constructor(sink) {
|
|
1481
853
|
this.sink = sink;
|
|
1482
|
-
this.ownApiUrl = ownApiUrl;
|
|
1483
854
|
this.name = "http";
|
|
1484
855
|
this.inflight = 0;
|
|
1485
856
|
this.active = false;
|
|
857
|
+
this.delivering = false;
|
|
858
|
+
this.pending = /* @__PURE__ */ new Set();
|
|
1486
859
|
this.restore = [];
|
|
1487
860
|
}
|
|
1488
861
|
start() {
|
|
1489
862
|
if (this.active) return;
|
|
1490
863
|
this.active = true;
|
|
864
|
+
this.delivering = true;
|
|
1491
865
|
this.patchFetch();
|
|
1492
866
|
this.patchXhr();
|
|
1493
867
|
}
|
|
1494
868
|
stop() {
|
|
869
|
+
this.delivering = false;
|
|
870
|
+
this.detach();
|
|
871
|
+
}
|
|
872
|
+
detach() {
|
|
1495
873
|
this.active = false;
|
|
1496
874
|
for (const r of this.restore.splice(0).reverse()) {
|
|
1497
875
|
try {
|
|
@@ -1500,8 +878,30 @@ var HTTPRecorder = class {
|
|
|
1500
878
|
}
|
|
1501
879
|
}
|
|
1502
880
|
}
|
|
881
|
+
/** Stops new capture and allows up to two seconds for pending HTTP/body capture. */
|
|
882
|
+
async drain() {
|
|
883
|
+
this.detach();
|
|
884
|
+
let timer;
|
|
885
|
+
const timeout = new Promise((resolve) => {
|
|
886
|
+
timer = setTimeout(resolve, 2e3);
|
|
887
|
+
});
|
|
888
|
+
const settle = async () => {
|
|
889
|
+
while (this.delivering && this.pending.size) {
|
|
890
|
+
await Promise.allSettled([...this.pending]);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
try {
|
|
894
|
+
await Promise.race([settle(), timeout]);
|
|
895
|
+
} finally {
|
|
896
|
+
clearTimeout(timer);
|
|
897
|
+
this.delivering = false;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
track(promise) {
|
|
901
|
+
const tracked = promise.catch(() => void 0).finally(() => this.pending.delete(tracked));
|
|
902
|
+
this.pending.add(tracked);
|
|
903
|
+
}
|
|
1503
904
|
isIgnored(url) {
|
|
1504
|
-
if (this.ownApiUrl && url.startsWith(this.ownApiUrl)) return true;
|
|
1505
905
|
return this.sink.sanitizer.isIgnoredUrl(url);
|
|
1506
906
|
}
|
|
1507
907
|
// ---------------------------------------------------------------- fetch
|
|
@@ -1527,14 +927,13 @@ var HTTPRecorder = class {
|
|
|
1527
927
|
}
|
|
1528
928
|
if (ctx) {
|
|
1529
929
|
const c = ctx;
|
|
1530
|
-
p.then(
|
|
930
|
+
self.track(p.then(
|
|
1531
931
|
(res) => self.endFetch(c, res),
|
|
1532
932
|
(err) => self.failFetch(c, err)
|
|
1533
|
-
)
|
|
933
|
+
));
|
|
1534
934
|
}
|
|
1535
935
|
return p;
|
|
1536
936
|
};
|
|
1537
|
-
patched[ORIGINAL_FETCH] = orig[ORIGINAL_FETCH] ?? orig;
|
|
1538
937
|
globalThis.fetch = patched;
|
|
1539
938
|
this.restore.push(() => {
|
|
1540
939
|
if (globalThis.fetch === patched) globalThis.fetch = orig;
|
|
@@ -1582,13 +981,13 @@ var HTTPRecorder = class {
|
|
|
1582
981
|
base.responseBody = { kind: "omitted", reason: "body-used" };
|
|
1583
982
|
return void this.emit(base);
|
|
1584
983
|
}
|
|
1585
|
-
this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
984
|
+
this.track(this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
1586
985
|
const b = this.sink.sanitizer.sanitizeTextBody(text, ct, size);
|
|
1587
986
|
b.truncated = truncated || b.truncated;
|
|
1588
987
|
base.responseBody = b;
|
|
1589
988
|
}).catch(() => {
|
|
1590
989
|
base.responseBody = { kind: "omitted", reason: "read-failed" };
|
|
1591
|
-
}).finally(() => guard(this.sink, this.name, () => this.emit(base)));
|
|
990
|
+
}).finally(() => guard(this.sink, this.name, () => this.emit(base))));
|
|
1592
991
|
});
|
|
1593
992
|
}
|
|
1594
993
|
async readBounded(res) {
|
|
@@ -1599,8 +998,8 @@ var HTTPRecorder = class {
|
|
|
1599
998
|
let size = 0;
|
|
1600
999
|
let truncated = false;
|
|
1601
1000
|
for (; ; ) {
|
|
1602
|
-
const { done
|
|
1603
|
-
if (
|
|
1001
|
+
const { done, value } = await reader.read();
|
|
1002
|
+
if (done) break;
|
|
1604
1003
|
size += value.byteLength;
|
|
1605
1004
|
if (text.length < max) text += dec.decode(value, { stream: true });
|
|
1606
1005
|
if (size > max * 4) {
|
|
@@ -1657,7 +1056,6 @@ var HTTPRecorder = class {
|
|
|
1657
1056
|
}
|
|
1658
1057
|
return origSend.call(this, body);
|
|
1659
1058
|
};
|
|
1660
|
-
open[XHR_PATCHED] = origOpen;
|
|
1661
1059
|
proto.open = open;
|
|
1662
1060
|
proto.setRequestHeader = setHeader;
|
|
1663
1061
|
proto.send = send;
|
|
@@ -1679,9 +1077,14 @@ var HTTPRecorder = class {
|
|
|
1679
1077
|
this.inflight++;
|
|
1680
1078
|
xhr.addEventListener("abort", () => m.aborted = true);
|
|
1681
1079
|
xhr.addEventListener("timeout", () => m.timedOut = true);
|
|
1080
|
+
let finishCapture;
|
|
1081
|
+
this.track(new Promise((resolve) => {
|
|
1082
|
+
finishCapture = resolve;
|
|
1083
|
+
}));
|
|
1682
1084
|
xhr.addEventListener("loadend", () => {
|
|
1683
1085
|
this.inflight = Math.max(0, this.inflight - 1);
|
|
1684
1086
|
guard(this.sink, this.name, () => this.endXhr(xhr, m, ctx));
|
|
1087
|
+
finishCapture();
|
|
1685
1088
|
});
|
|
1686
1089
|
}
|
|
1687
1090
|
endXhr(xhr, m, ctx) {
|
|
@@ -1725,6 +1128,7 @@ var HTTPRecorder = class {
|
|
|
1725
1128
|
};
|
|
1726
1129
|
}
|
|
1727
1130
|
emit(data) {
|
|
1131
|
+
if (!this.delivering) return;
|
|
1728
1132
|
this.sink.emit("http", data, { priority: "normal", at: data.startedAt });
|
|
1729
1133
|
}
|
|
1730
1134
|
describeBody(body, contentType) {
|
|
@@ -1870,9 +1274,10 @@ var RRWebRecorder = class {
|
|
|
1870
1274
|
emit: (event) => this.onEvent(event),
|
|
1871
1275
|
blockSelector: block || void 0,
|
|
1872
1276
|
maskTextSelector: maskText || void 0,
|
|
1873
|
-
maskAllInputs: privacy.maskSensitiveInputs,
|
|
1874
|
-
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 },
|
|
1277
|
+
maskAllInputs: privacy.maskSensitiveInputs && !privacy.captureSensitiveValues,
|
|
1278
|
+
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 },
|
|
1875
1279
|
maskInputFn: (text, el) => {
|
|
1280
|
+
if (privacy.captureSensitiveValues) return text;
|
|
1876
1281
|
if (el?.type === "password") return "*".repeat(text.length);
|
|
1877
1282
|
if (allowed && safe(() => el.matches(allowed))) return text;
|
|
1878
1283
|
return "*".repeat(text.length);
|
|
@@ -1973,11 +1378,12 @@ var LifecycleManager = class {
|
|
|
1973
1378
|
this.lastUrl = location.href;
|
|
1974
1379
|
const d = this.d;
|
|
1975
1380
|
d.listen(window, "pagehide", () => safe(() => this.h.onPageHide()), { capture: true });
|
|
1976
|
-
d.listen(window, "
|
|
1381
|
+
d.listen(window, "pageshow", (event) => {
|
|
1382
|
+
if (event.persisted) safe(() => this.h.onPageShow?.());
|
|
1383
|
+
});
|
|
1977
1384
|
d.listen(document, "visibilitychange", () => {
|
|
1978
1385
|
const hidden = document.visibilityState === "hidden";
|
|
1979
1386
|
safe(() => this.h.onVisibility(hidden));
|
|
1980
|
-
if (hidden) safe(() => this.h.onPageHide());
|
|
1981
1387
|
});
|
|
1982
1388
|
d.listen(window, "online", () => safe(() => this.h.onConnectivity(true)));
|
|
1983
1389
|
d.listen(window, "offline", () => safe(() => this.h.onConnectivity(false)));
|
|
@@ -2014,532 +1420,1058 @@ var LifecycleManager = class {
|
|
|
2014
1420
|
}
|
|
2015
1421
|
};
|
|
2016
1422
|
|
|
2017
|
-
// src/core/
|
|
2018
|
-
var
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
this.
|
|
2022
|
-
this.
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
return
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
if (s.expiresAt && s.expiresAt < Date.now()) return null;
|
|
2047
|
-
const seq = Number(safe(() => sessionStorage.getItem(this.seqKey)) ?? 0);
|
|
2048
|
-
s.sequence = Math.max(s.sequence ?? 0, Number.isFinite(seq) ? seq : 0);
|
|
2049
|
-
return s;
|
|
2050
|
-
} catch {
|
|
2051
|
-
return null;
|
|
1423
|
+
// src/core/RecordingStore.ts
|
|
1424
|
+
var RecordingStore = class _RecordingStore {
|
|
1425
|
+
constructor(database) {
|
|
1426
|
+
this.database = database;
|
|
1427
|
+
this.pending = /* @__PURE__ */ new Set();
|
|
1428
|
+
this.uncommitted = /* @__PURE__ */ new Map();
|
|
1429
|
+
}
|
|
1430
|
+
static async open(key) {
|
|
1431
|
+
const database = await new Promise((resolve, reject) => {
|
|
1432
|
+
const request = indexedDB.open(`offscript-recording-${key}`, 1);
|
|
1433
|
+
request.onupgradeneeded = () => {
|
|
1434
|
+
request.result.createObjectStore("events", { keyPath: "sequence" });
|
|
1435
|
+
request.result.createObjectStore("state");
|
|
1436
|
+
};
|
|
1437
|
+
request.onsuccess = () => resolve(request.result);
|
|
1438
|
+
request.onerror = () => reject(request.error);
|
|
1439
|
+
});
|
|
1440
|
+
return new _RecordingStore(database);
|
|
1441
|
+
}
|
|
1442
|
+
async load() {
|
|
1443
|
+
await this.flush();
|
|
1444
|
+
const checkpoint = sessionStorage.getItem(this.checkpointKey);
|
|
1445
|
+
if (checkpoint) {
|
|
1446
|
+
const saved = JSON.parse(checkpoint);
|
|
1447
|
+
await this.write(["events", "state"], (transaction) => {
|
|
1448
|
+
for (const event of saved.events) transaction.objectStore("events").put(event);
|
|
1449
|
+
transaction.objectStore("state").put(saved.status, "status");
|
|
1450
|
+
});
|
|
1451
|
+
sessionStorage.removeItem(this.checkpointKey);
|
|
2052
1452
|
}
|
|
1453
|
+
return new Promise((resolve, reject) => {
|
|
1454
|
+
const transaction = this.database.transaction(["events", "state"], "readonly");
|
|
1455
|
+
const events = transaction.objectStore("events").getAll();
|
|
1456
|
+
const state = transaction.objectStore("state").get("status");
|
|
1457
|
+
transaction.oncomplete = () => {
|
|
1458
|
+
this.status = state.result;
|
|
1459
|
+
resolve(state.result ? { status: state.result, events: events.result } : null);
|
|
1460
|
+
};
|
|
1461
|
+
transaction.onabort = () => reject(transaction.error);
|
|
1462
|
+
});
|
|
2053
1463
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
this.
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
testCaseId: this.info.testCaseId,
|
|
2062
|
-
platformId: this.cfg.platformId,
|
|
2063
|
-
sessionToken: this.token,
|
|
2064
|
-
status,
|
|
2065
|
-
sequence: this.seq,
|
|
2066
|
-
pageCount: this.state?.pageCount ?? 0,
|
|
2067
|
-
heartbeatIntervalMs: this.state?.heartbeatIntervalMs,
|
|
2068
|
-
expiresAt: this.state?.expiresAt,
|
|
2069
|
-
updatedAt: Date.now(),
|
|
2070
|
-
...extra
|
|
2071
|
-
};
|
|
2072
|
-
safe(() => sessionStorage.setItem(this.storageKey, JSON.stringify(this.state)));
|
|
1464
|
+
append(event) {
|
|
1465
|
+
this.uncommitted.set(event.sequence, event);
|
|
1466
|
+
return this.write(["events"], (transaction) => {
|
|
1467
|
+
transaction.objectStore("events").put(event);
|
|
1468
|
+
}).then(() => {
|
|
1469
|
+
this.uncommitted.delete(event.sequence);
|
|
1470
|
+
});
|
|
2073
1471
|
}
|
|
2074
1472
|
setStatus(status) {
|
|
2075
|
-
this.
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
safe(() => sessionStorage.removeItem(this.storageKey));
|
|
2079
|
-
safe(() => sessionStorage.removeItem(this.seqKey));
|
|
2080
|
-
this.state = null;
|
|
2081
|
-
}
|
|
2082
|
-
// ---- sequência ----------------------------------------------------------
|
|
2083
|
-
nextSequence() {
|
|
2084
|
-
this.seq++;
|
|
2085
|
-
safe(() => sessionStorage.setItem(this.seqKey, String(this.seq)));
|
|
2086
|
-
return this.seq;
|
|
2087
|
-
}
|
|
2088
|
-
get lastSequence() {
|
|
2089
|
-
return this.seq;
|
|
2090
|
-
}
|
|
2091
|
-
/** Sobe a sequência até pelo menos `n` (ex.: maior sequência encontrada no buffer). */
|
|
2092
|
-
ensureSequenceAtLeast(n) {
|
|
2093
|
-
if (n > this.seq) this.seq = n;
|
|
2094
|
-
}
|
|
2095
|
-
newPage() {
|
|
2096
|
-
if (!this.info) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
2097
|
-
this.info = { ...this.info, pageId: uid("pg_") };
|
|
2098
|
-
this.persist(this.state?.status ?? "recording", { pageCount: (this.state?.pageCount ?? 0) + 1 });
|
|
2099
|
-
return this.info.pageId;
|
|
2100
|
-
}
|
|
2101
|
-
get pageCount() {
|
|
2102
|
-
return this.state?.pageCount ?? 0;
|
|
2103
|
-
}
|
|
2104
|
-
// ---- ciclo de vida ------------------------------------------------------
|
|
2105
|
-
/** Cria (ou obtém, de forma idempotente) a sessão no backend. */
|
|
2106
|
-
async create(opts, context) {
|
|
2107
|
-
if (!opts?.sessionToken || !opts.executionId || !opts.testCaseId)
|
|
2108
|
-
throw new AuthError("sessionToken, executionId e testCaseId s\xE3o obrigat\xF3rios");
|
|
2109
|
-
this.token = opts.sessionToken;
|
|
2110
|
-
const tabId = await resolveTabId(this.cfg.storagePrefix, this.loadPersisted()?.tabId);
|
|
2111
|
-
const res = await this.api.createSession({
|
|
2112
|
-
platformId: this.cfg.platformId,
|
|
2113
|
-
testCaseId: opts.testCaseId,
|
|
2114
|
-
executionId: opts.executionId,
|
|
2115
|
-
tabId,
|
|
2116
|
-
sdkVersion: SDK_VERSION,
|
|
2117
|
-
appVersion: this.cfg.appVersion,
|
|
2118
|
-
context
|
|
1473
|
+
this.status = status;
|
|
1474
|
+
return this.write(["state"], (transaction) => {
|
|
1475
|
+
transaction.objectStore("state").put(status, "status");
|
|
2119
1476
|
});
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
this.seq = 0;
|
|
2126
|
-
this.info = {
|
|
2127
|
-
sessionId: res.sessionId,
|
|
2128
|
-
executionId: opts.executionId,
|
|
2129
|
-
testCaseId: opts.testCaseId,
|
|
2130
|
-
platformId: this.cfg.platformId,
|
|
2131
|
-
tabId,
|
|
2132
|
-
pageId: uid("pg_"),
|
|
2133
|
-
startedAt: Date.now()
|
|
2134
|
-
};
|
|
2135
|
-
safe(() => sessionStorage.removeItem(this.seqKey));
|
|
2136
|
-
this.persist("recording", {
|
|
2137
|
-
heartbeatIntervalMs: res.heartbeatIntervalMs,
|
|
2138
|
-
expiresAt: res.expiresAt,
|
|
2139
|
-
pageCount: 1,
|
|
2140
|
-
sequence: 0
|
|
1477
|
+
}
|
|
1478
|
+
async clear() {
|
|
1479
|
+
await this.write(["events", "state"], (transaction) => {
|
|
1480
|
+
transaction.objectStore("events").clear();
|
|
1481
|
+
transaction.objectStore("state").clear();
|
|
2141
1482
|
});
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
this.token = state.sessionToken;
|
|
2147
|
-
const tabId = await resolveTabId(this.cfg.storagePrefix, state.tabId);
|
|
2148
|
-
const res = await this.api.status(state.sessionId);
|
|
2149
|
-
if (res.status === "expired") throw new SessionExpiredError();
|
|
2150
|
-
if (res.status === "disabled") throw new SessionDisabledError();
|
|
2151
|
-
if (res.sessionToken) this.token = res.sessionToken;
|
|
2152
|
-
this.state = { ...state, tabId };
|
|
2153
|
-
this.seq = tabId === state.tabId ? state.sequence : 0;
|
|
2154
|
-
this.info = {
|
|
2155
|
-
sessionId: state.sessionId,
|
|
2156
|
-
executionId: state.executionId,
|
|
2157
|
-
testCaseId: state.testCaseId,
|
|
2158
|
-
platformId: state.platformId,
|
|
2159
|
-
tabId,
|
|
2160
|
-
pageId: uid("pg_"),
|
|
2161
|
-
startedAt: Date.now()
|
|
2162
|
-
};
|
|
2163
|
-
this.persist(state.status, { sessionToken: this.token, expiresAt: res.expiresAt ?? state.expiresAt, pageCount: state.pageCount + 1, tabId });
|
|
2164
|
-
return res;
|
|
2165
|
-
}
|
|
2166
|
-
startHeartbeat(onResult, onError, pending, status) {
|
|
2167
|
-
this.stopHeartbeat();
|
|
2168
|
-
const every = this.state?.heartbeatIntervalMs ?? this.cfg.heartbeatIntervalMs;
|
|
2169
|
-
if (!every || every <= 0) return;
|
|
2170
|
-
this.heartbeatTimer = setInterval(async () => {
|
|
2171
|
-
if (!this.info) return;
|
|
2172
|
-
try {
|
|
2173
|
-
const r = await this.api.heartbeat(this.info.sessionId, {
|
|
2174
|
-
tabId: this.info.tabId,
|
|
2175
|
-
pageId: this.info.pageId,
|
|
2176
|
-
status: status(),
|
|
2177
|
-
pending: pending(),
|
|
2178
|
-
lastSequence: this.seq
|
|
2179
|
-
});
|
|
2180
|
-
if (r.sessionToken) {
|
|
2181
|
-
this.token = r.sessionToken;
|
|
2182
|
-
this.persist(this.state?.status ?? "recording", { sessionToken: r.sessionToken, expiresAt: r.expiresAt });
|
|
2183
|
-
}
|
|
2184
|
-
onResult(r);
|
|
2185
|
-
} catch (e) {
|
|
2186
|
-
if (e instanceof AuthError || e instanceof SessionExpiredError) onError(e);
|
|
2187
|
-
}
|
|
2188
|
-
}, every);
|
|
1483
|
+
this.failure = void 0;
|
|
1484
|
+
this.status = void 0;
|
|
1485
|
+
this.uncommitted.clear();
|
|
1486
|
+
sessionStorage.removeItem(this.checkpointKey);
|
|
2189
1487
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
this.
|
|
1488
|
+
/** Synchronous handoff of the last uncommitted events before document unload. */
|
|
1489
|
+
checkpoint() {
|
|
1490
|
+
if (!this.status) return;
|
|
1491
|
+
sessionStorage.setItem(this.checkpointKey, JSON.stringify({ status: this.status, events: [...this.uncommitted.values()] }));
|
|
2193
1492
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
const r = await this.api.complete(this.info.sessionId, { tabId: this.info.tabId, lastSequence: this.seq, ...summary });
|
|
2197
|
-
return !!r.completed;
|
|
1493
|
+
get checkpointKey() {
|
|
1494
|
+
return `${this.database.name}-handoff`;
|
|
2198
1495
|
}
|
|
2199
|
-
|
|
2200
|
-
this.
|
|
2201
|
-
this.
|
|
2202
|
-
this.state = null;
|
|
2203
|
-
this.token = "";
|
|
2204
|
-
this.seq = 0;
|
|
1496
|
+
async flush() {
|
|
1497
|
+
await Promise.allSettled([...this.pending]);
|
|
1498
|
+
if (this.failure) throw this.failure;
|
|
2205
1499
|
}
|
|
2206
|
-
|
|
2207
|
-
var heldLocks = [];
|
|
2208
|
-
async function resolveTabId(prefix, previous) {
|
|
2209
|
-
const candidate = previous ?? uid("tab_");
|
|
2210
|
-
const locks = globalThis.navigator?.locks;
|
|
2211
|
-
if (!locks || !previous) {
|
|
2212
|
-
if (locks) void claim(locks, `${prefix}:tab:${candidate}`);
|
|
2213
|
-
return candidate;
|
|
2214
|
-
}
|
|
2215
|
-
if (heldLocks.includes(candidate)) return candidate;
|
|
2216
|
-
const ok = await claim(locks, `${prefix}:tab:${candidate}`);
|
|
2217
|
-
if (ok) return candidate;
|
|
2218
|
-
const fresh = uid("tab_");
|
|
2219
|
-
void claim(locks, `${prefix}:tab:${fresh}`);
|
|
2220
|
-
return fresh;
|
|
2221
|
-
}
|
|
2222
|
-
function claim(locks, name) {
|
|
2223
|
-
return new Promise((resolve) => {
|
|
1500
|
+
async close() {
|
|
2224
1501
|
try {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
return void 0;
|
|
2229
|
-
}
|
|
2230
|
-
heldLocks.push(name.split(":tab:")[1]);
|
|
2231
|
-
resolve(true);
|
|
2232
|
-
return new Promise(() => void 0);
|
|
2233
|
-
}).catch(() => resolve(true));
|
|
2234
|
-
} catch {
|
|
2235
|
-
resolve(true);
|
|
1502
|
+
await this.flush();
|
|
1503
|
+
} finally {
|
|
1504
|
+
this.database.close();
|
|
2236
1505
|
}
|
|
2237
|
-
}
|
|
2238
|
-
|
|
1506
|
+
}
|
|
1507
|
+
write(stores, apply) {
|
|
1508
|
+
const operation = new Promise((resolve, reject) => {
|
|
1509
|
+
const transaction = this.database.transaction(stores, "readwrite");
|
|
1510
|
+
transaction.oncomplete = () => resolve();
|
|
1511
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Falha ao salvar grava\xE7\xE3o local"));
|
|
1512
|
+
try {
|
|
1513
|
+
apply(transaction);
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
transaction.abort();
|
|
1516
|
+
reject(error);
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
this.pending.add(operation);
|
|
1520
|
+
void operation.catch((error) => {
|
|
1521
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
1522
|
+
}).finally(() => this.pending.delete(operation));
|
|
1523
|
+
return operation;
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
2239
1526
|
|
|
2240
1527
|
// src/core/config.ts
|
|
2241
1528
|
function resolveConfig(input) {
|
|
2242
|
-
if (!input || typeof input !== "object") throw new ConfigError("Configura\xE7\xE3o ausente");
|
|
2243
|
-
if (!input.platformId) throw new ConfigError("platformId \xE9 obrigat\xF3rio");
|
|
2244
|
-
if (!input.apiUrl) throw new ConfigError("apiUrl \xE9 obrigat\xF3rio");
|
|
2245
|
-
let url;
|
|
2246
|
-
try {
|
|
2247
|
-
url = new URL(input.apiUrl);
|
|
2248
|
-
} catch {
|
|
2249
|
-
throw new ConfigError(`apiUrl inv\xE1lida: ${input.apiUrl}`);
|
|
2250
|
-
}
|
|
2251
|
-
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
2252
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
2253
|
-
throw new ConfigError("apiUrl deve usar HTTPS (HTTP s\xF3 \xE9 aceito em localhost)");
|
|
2254
1529
|
return {
|
|
2255
|
-
apiUrl: url.origin + url.pathname.replace(/\/+$/, ""),
|
|
2256
|
-
platformId: input.platformId,
|
|
2257
|
-
appVersion: input.appVersion,
|
|
2258
1530
|
recording: deepMerge(DEFAULT_RECORDING, input.recording),
|
|
2259
1531
|
privacy: resolvePrivacy(input.privacy),
|
|
2260
|
-
transport: deepMerge(DEFAULT_TRANSPORT, input.transport),
|
|
2261
1532
|
correlationWindowMs: input.correlationWindowMs ?? 3e3,
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
debug: input.debug ?? false
|
|
1533
|
+
controls: input.controls ?? true,
|
|
1534
|
+
persistenceKey: input.persistenceKey,
|
|
1535
|
+
onComplete: input.onComplete
|
|
2266
1536
|
};
|
|
2267
1537
|
}
|
|
2268
1538
|
|
|
1539
|
+
// src/ui/RecordingControls.ts
|
|
1540
|
+
var HOST_ATTR = "data-offscript-console";
|
|
1541
|
+
var EYE_SEQUENCE = [
|
|
1542
|
+
{ pose: "open", duration: 2600 },
|
|
1543
|
+
{ pose: "blink", duration: 160 },
|
|
1544
|
+
{ pose: "open", duration: 1800 },
|
|
1545
|
+
{ pose: "left", duration: 700 },
|
|
1546
|
+
{ pose: "open", duration: 1500 },
|
|
1547
|
+
{ pose: "wink", duration: 480 },
|
|
1548
|
+
{ pose: "open", duration: 1900 },
|
|
1549
|
+
{ pose: "right", duration: 700 },
|
|
1550
|
+
{ pose: "open", duration: 1600 },
|
|
1551
|
+
{ pose: "up", duration: 640 },
|
|
1552
|
+
{ pose: "happy", duration: 720 }
|
|
1553
|
+
];
|
|
1554
|
+
var EYE_PATHS = {
|
|
1555
|
+
open: ["M59.5 79.5v15", "M93 79.5v15"],
|
|
1556
|
+
blink: ["M52 89q7.5 5 15 0", "M85.5 89q7.5 5 15 0"],
|
|
1557
|
+
wink: ["M52 89q7.5 5 15 0", "M93 79.5v15"],
|
|
1558
|
+
happy: ["M52 91q7.5-12 15 0", "M85.5 91q7.5-12 15 0"]
|
|
1559
|
+
};
|
|
1560
|
+
var RecordingControls = class {
|
|
1561
|
+
constructor(sdk) {
|
|
1562
|
+
this.sdk = sdk;
|
|
1563
|
+
this.eyeTimer = 0;
|
|
1564
|
+
this.placeFrame = 0;
|
|
1565
|
+
this.eyeIndex = 0;
|
|
1566
|
+
this.busy = false;
|
|
1567
|
+
this.expanded = true;
|
|
1568
|
+
this.privacyOpen = true;
|
|
1569
|
+
this.failure = "";
|
|
1570
|
+
this.onVisibility = () => {
|
|
1571
|
+
window.clearTimeout(this.eyeTimer);
|
|
1572
|
+
if (!document.hidden) this.scheduleEye();
|
|
1573
|
+
};
|
|
1574
|
+
this.onViewport = () => {
|
|
1575
|
+
this.placeConfirm();
|
|
1576
|
+
this.schedulePlacePrivacy();
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
mount() {
|
|
1580
|
+
if (this.host || typeof document === "undefined") return;
|
|
1581
|
+
const host = document.createElement("div");
|
|
1582
|
+
host.setAttribute(HOST_ATTR, "");
|
|
1583
|
+
host.setAttribute("data-offscript-ignore", "");
|
|
1584
|
+
host.className = "offscript-ignore";
|
|
1585
|
+
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;";
|
|
1586
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
1587
|
+
shadow.innerHTML = TEMPLATE;
|
|
1588
|
+
this.bind(shadow);
|
|
1589
|
+
this.host = host;
|
|
1590
|
+
this.shadow = shadow;
|
|
1591
|
+
this.note = shadow.querySelector("[data-note]") ?? void 0;
|
|
1592
|
+
this.place();
|
|
1593
|
+
this.watchDom();
|
|
1594
|
+
this.unsubscribe = this.sdk.on("statusChange", () => this.render());
|
|
1595
|
+
window.addEventListener("resize", this.onViewport);
|
|
1596
|
+
window.visualViewport?.addEventListener("resize", this.onViewport);
|
|
1597
|
+
document.fonts?.ready.then(() => {
|
|
1598
|
+
if (this.host) this.onViewport();
|
|
1599
|
+
});
|
|
1600
|
+
this.watchDock();
|
|
1601
|
+
this.render();
|
|
1602
|
+
this.startEyes();
|
|
1603
|
+
}
|
|
1604
|
+
/** Independent notice so the host may destroy the controls after successful upload. */
|
|
1605
|
+
showSuccess() {
|
|
1606
|
+
const notice = document.createElement("div");
|
|
1607
|
+
notice.setAttribute("data-offscript-success", "");
|
|
1608
|
+
notice.setAttribute("data-offscript-ignore", "");
|
|
1609
|
+
notice.className = "offscript-ignore";
|
|
1610
|
+
notice.style.cssText = "position:fixed;right:16px;bottom:16px;max-width:calc(100vw - 32px);z-index:2147483647;";
|
|
1611
|
+
const shadow = notice.attachShadow({ mode: "closed" });
|
|
1612
|
+
shadow.innerHTML = SUCCESS_TEMPLATE;
|
|
1613
|
+
document.body.appendChild(notice);
|
|
1614
|
+
const timer = window.setTimeout(() => notice.remove(), 8e3);
|
|
1615
|
+
shadow.querySelector("button").addEventListener("click", () => {
|
|
1616
|
+
window.clearTimeout(timer);
|
|
1617
|
+
notice.remove();
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
unmount() {
|
|
1621
|
+
this.unsubscribe?.();
|
|
1622
|
+
this.unsubscribe = void 0;
|
|
1623
|
+
this.observer?.disconnect();
|
|
1624
|
+
this.observer = void 0;
|
|
1625
|
+
this.dockWatch?.disconnect();
|
|
1626
|
+
this.dockWatch = void 0;
|
|
1627
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
1628
|
+
this.placeFrame = 0;
|
|
1629
|
+
window.clearTimeout(this.eyeTimer);
|
|
1630
|
+
window.removeEventListener("resize", this.onViewport);
|
|
1631
|
+
window.visualViewport?.removeEventListener("resize", this.onViewport);
|
|
1632
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
1633
|
+
this.host?.remove();
|
|
1634
|
+
this.host = void 0;
|
|
1635
|
+
this.shadow = void 0;
|
|
1636
|
+
}
|
|
1637
|
+
place() {
|
|
1638
|
+
const host = this.host;
|
|
1639
|
+
const parent = document.body ?? document.documentElement;
|
|
1640
|
+
if (!host || !parent || host.parentNode === parent) return;
|
|
1641
|
+
parent.appendChild(host);
|
|
1642
|
+
}
|
|
1643
|
+
watchDom() {
|
|
1644
|
+
if (typeof MutationObserver === "undefined") return;
|
|
1645
|
+
this.observer?.disconnect();
|
|
1646
|
+
this.observer = new MutationObserver(() => {
|
|
1647
|
+
this.place();
|
|
1648
|
+
if (document.body) this.observer?.observe(document.body, { childList: true });
|
|
1649
|
+
});
|
|
1650
|
+
this.observer.observe(document.documentElement, { childList: true });
|
|
1651
|
+
if (document.body) this.observer.observe(document.body, { childList: true });
|
|
1652
|
+
}
|
|
1653
|
+
watchDock() {
|
|
1654
|
+
const dock = this.shadow?.querySelector(".dock");
|
|
1655
|
+
if (!(dock instanceof HTMLElement) || typeof ResizeObserver === "undefined") return;
|
|
1656
|
+
this.dockWatch?.disconnect();
|
|
1657
|
+
this.dockWatch = new ResizeObserver(() => this.placePrivacy());
|
|
1658
|
+
this.dockWatch.observe(dock);
|
|
1659
|
+
}
|
|
1660
|
+
bind(shadow) {
|
|
1661
|
+
const stop = (event) => event.stopPropagation();
|
|
1662
|
+
shadow.addEventListener("click", (event) => {
|
|
1663
|
+
const actionNode = event.composedPath().find((node) => node instanceof Element && node.hasAttribute("data-action"));
|
|
1664
|
+
const action = actionNode instanceof Element ? actionNode.getAttribute("data-action") : null;
|
|
1665
|
+
if (action === "open") {
|
|
1666
|
+
this.expanded = true;
|
|
1667
|
+
this.render();
|
|
1668
|
+
} else if (action === "close") {
|
|
1669
|
+
this.expanded = false;
|
|
1670
|
+
this.render();
|
|
1671
|
+
} else if (action === "record") void this.run(() => this.sdk.startRecording());
|
|
1672
|
+
else if (action === "pause") void this.run(() => this.sdk.pauseRecording());
|
|
1673
|
+
else if (action === "resume") void this.run(() => this.sdk.resumeRecording());
|
|
1674
|
+
else if (action === "stop") void this.run(() => this.sdk.finishRecording());
|
|
1675
|
+
else if (action === "new") this.openConfirm();
|
|
1676
|
+
else if (action === "cancel") this.closeConfirm();
|
|
1677
|
+
else if (action === "confirm") {
|
|
1678
|
+
this.closeConfirm();
|
|
1679
|
+
void this.run(() => this.sdk.discardRecording());
|
|
1680
|
+
} else if (action === "privacy-show") {
|
|
1681
|
+
this.privacyOpen = true;
|
|
1682
|
+
this.render();
|
|
1683
|
+
} else if (action === "privacy-hide") {
|
|
1684
|
+
this.privacyOpen = false;
|
|
1685
|
+
this.render();
|
|
1686
|
+
}
|
|
1687
|
+
}, true);
|
|
1688
|
+
for (const type of ["click", "dblclick", "auxclick", "contextmenu", "pointerdown", "pointerup", "mousedown", "mouseup", "keydown", "keyup", "keypress", "input", "change", "mouseover", "mouseout"]) {
|
|
1689
|
+
shadow.addEventListener(type, stop);
|
|
1690
|
+
}
|
|
1691
|
+
shadow.querySelector("[data-confirm]")?.addEventListener("keydown", (event) => {
|
|
1692
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") this.closeConfirm();
|
|
1693
|
+
});
|
|
1694
|
+
shadow.querySelector("[data-privacy]")?.addEventListener("keydown", (event) => {
|
|
1695
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") {
|
|
1696
|
+
this.privacyOpen = false;
|
|
1697
|
+
this.render();
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
openConfirm() {
|
|
1702
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
1703
|
+
if (!(bubble instanceof HTMLElement) || this.busy || !bubble.hidden) return;
|
|
1704
|
+
bubble.hidden = false;
|
|
1705
|
+
this.placeConfirm();
|
|
1706
|
+
this.paintPrivacy();
|
|
1707
|
+
const cancel = bubble.querySelector("[data-action='cancel']");
|
|
1708
|
+
if (cancel instanceof HTMLElement) cancel.focus();
|
|
1709
|
+
}
|
|
1710
|
+
placeConfirm() {
|
|
1711
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
1712
|
+
const card = bubble?.querySelector(".bubble");
|
|
1713
|
+
const button = this.shadow?.querySelector("[data-action='new']");
|
|
1714
|
+
if (!(bubble instanceof HTMLElement) || !(card instanceof HTMLElement) || !(button instanceof HTMLElement)) return;
|
|
1715
|
+
const buttonBox = button.getBoundingClientRect();
|
|
1716
|
+
const buttonCenter = buttonBox.left + buttonBox.width / 2;
|
|
1717
|
+
const tailHalf = 4.5;
|
|
1718
|
+
const margin = 12;
|
|
1719
|
+
const viewport = document.documentElement.clientWidth;
|
|
1720
|
+
let rightEdge = buttonCenter + 22 + tailHalf;
|
|
1721
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + card.offsetWidth, rightEdge));
|
|
1722
|
+
bubble.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
1723
|
+
const tail = Math.min(Math.max(12, rightEdge - buttonCenter - tailHalf), Math.max(12, card.offsetWidth - 18));
|
|
1724
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
1725
|
+
}
|
|
1726
|
+
closeConfirm() {
|
|
1727
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
1728
|
+
if (bubble instanceof HTMLElement) bubble.hidden = true;
|
|
1729
|
+
this.paintPrivacy();
|
|
1730
|
+
}
|
|
1731
|
+
async run(task) {
|
|
1732
|
+
if (this.busy) return;
|
|
1733
|
+
this.busy = true;
|
|
1734
|
+
this.failure = "";
|
|
1735
|
+
this.render();
|
|
1736
|
+
try {
|
|
1737
|
+
await task();
|
|
1738
|
+
} catch {
|
|
1739
|
+
this.failure = "N\xE3o foi poss\xEDvel concluir";
|
|
1740
|
+
} finally {
|
|
1741
|
+
this.busy = false;
|
|
1742
|
+
this.render();
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
render() {
|
|
1746
|
+
this.place();
|
|
1747
|
+
const { status, pending } = this.sdk.getState();
|
|
1748
|
+
const shell = this.shadow?.querySelector("[data-shell]");
|
|
1749
|
+
shell?.setAttribute("data-open", this.expanded ? "true" : "false");
|
|
1750
|
+
shell?.setAttribute("data-state", status);
|
|
1751
|
+
if (this.note) this.note.textContent = this.failure;
|
|
1752
|
+
const live = status === "recording" || status === "paused";
|
|
1753
|
+
const canFinish = live || status === "stopped" && pending > 0;
|
|
1754
|
+
if (!live) this.closeConfirm();
|
|
1755
|
+
this.paintPrivacy();
|
|
1756
|
+
this.setHidden("record", live || status === "initializing" || status === "stopping");
|
|
1757
|
+
this.setHidden("pause", status !== "recording");
|
|
1758
|
+
this.setHidden("resume", status !== "paused");
|
|
1759
|
+
this.setHidden("new", !live);
|
|
1760
|
+
this.setHidden("stop", !canFinish);
|
|
1761
|
+
this.setEnabled("record", !this.busy && !live && status !== "initializing" && status !== "stopping");
|
|
1762
|
+
this.setEnabled("pause", !this.busy && status === "recording");
|
|
1763
|
+
this.setEnabled("resume", !this.busy && status === "paused");
|
|
1764
|
+
this.setEnabled("stop", !this.busy && canFinish);
|
|
1765
|
+
this.setEnabled("new", !this.busy && live);
|
|
1766
|
+
}
|
|
1767
|
+
paintPrivacy() {
|
|
1768
|
+
const flag = this.sdk.capturesSensitiveValues();
|
|
1769
|
+
const confirm = this.shadow?.querySelector("[data-confirm]");
|
|
1770
|
+
const confirmOpen = confirm instanceof HTMLElement && !confirm.hidden;
|
|
1771
|
+
const bubble = this.shadow?.querySelector("[data-privacy]");
|
|
1772
|
+
const show = flag && this.privacyOpen && this.expanded && !confirmOpen;
|
|
1773
|
+
if (bubble instanceof HTMLElement) {
|
|
1774
|
+
bubble.hidden = !show;
|
|
1775
|
+
if (!show) this.clearPrivacyPlace(bubble);
|
|
1776
|
+
}
|
|
1777
|
+
if (show) this.schedulePlacePrivacy();
|
|
1778
|
+
this.setHidden("privacy-show", !flag || this.privacyOpen || !this.expanded);
|
|
1779
|
+
}
|
|
1780
|
+
schedulePlacePrivacy() {
|
|
1781
|
+
this.placePrivacy();
|
|
1782
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
1783
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
1784
|
+
this.placePrivacy();
|
|
1785
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
1786
|
+
this.placeFrame = 0;
|
|
1787
|
+
this.placePrivacy();
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
clearPrivacyPlace(wrap) {
|
|
1792
|
+
wrap.style.position = "";
|
|
1793
|
+
wrap.style.transform = "";
|
|
1794
|
+
wrap.style.width = "";
|
|
1795
|
+
wrap.style.maxWidth = "";
|
|
1796
|
+
wrap.style.left = "";
|
|
1797
|
+
wrap.style.right = "";
|
|
1798
|
+
wrap.style.top = "";
|
|
1799
|
+
wrap.style.bottom = "";
|
|
1800
|
+
}
|
|
1801
|
+
placePrivacy() {
|
|
1802
|
+
const wrap = this.shadow?.querySelector("[data-privacy]");
|
|
1803
|
+
const card = wrap?.querySelector(".bubble");
|
|
1804
|
+
const face = this.shadow?.querySelector(".face");
|
|
1805
|
+
if (!(wrap instanceof HTMLElement) || !(card instanceof HTMLElement) || !(face instanceof HTMLElement) || wrap.hidden) return;
|
|
1806
|
+
const box = face.getBoundingClientRect();
|
|
1807
|
+
if (box.width < 8 || box.height < 8 || box.top < 1 && box.left < 1) return;
|
|
1808
|
+
const margin = 12;
|
|
1809
|
+
const viewport = document.documentElement.clientWidth;
|
|
1810
|
+
const maxWidth = Math.min(264, viewport - margin * 2);
|
|
1811
|
+
wrap.style.position = "fixed";
|
|
1812
|
+
wrap.style.transform = "none";
|
|
1813
|
+
wrap.style.width = `${maxWidth}px`;
|
|
1814
|
+
wrap.style.maxWidth = `${maxWidth}px`;
|
|
1815
|
+
wrap.style.left = "auto";
|
|
1816
|
+
wrap.style.bottom = "auto";
|
|
1817
|
+
const faceCenter = box.left + box.width / 2;
|
|
1818
|
+
const tailHalf = 5;
|
|
1819
|
+
let rightEdge = faceCenter + 12 + tailHalf;
|
|
1820
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + maxWidth, rightEdge));
|
|
1821
|
+
wrap.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
1822
|
+
wrap.style.top = `${Math.round(Math.max(margin, box.top - 10 - wrap.offsetHeight))}px`;
|
|
1823
|
+
const tail = Math.min(Math.max(12, rightEdge - faceCenter - tailHalf), Math.max(12, maxWidth - 18));
|
|
1824
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
1825
|
+
}
|
|
1826
|
+
setEnabled(action, enabled) {
|
|
1827
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
1828
|
+
if (button instanceof HTMLButtonElement) button.disabled = !enabled;
|
|
1829
|
+
}
|
|
1830
|
+
setHidden(action, hidden) {
|
|
1831
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
1832
|
+
if (button instanceof HTMLButtonElement) button.hidden = hidden;
|
|
1833
|
+
}
|
|
1834
|
+
startEyes() {
|
|
1835
|
+
const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
1836
|
+
if (!media || media.matches) return;
|
|
1837
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
1838
|
+
this.scheduleEye();
|
|
1839
|
+
}
|
|
1840
|
+
scheduleEye() {
|
|
1841
|
+
const beat = EYE_SEQUENCE[this.eyeIndex % EYE_SEQUENCE.length];
|
|
1842
|
+
this.applyPose(beat.pose);
|
|
1843
|
+
this.eyeIndex = (this.eyeIndex + 1) % EYE_SEQUENCE.length;
|
|
1844
|
+
this.eyeTimer = window.setTimeout(() => this.scheduleEye(), beat.duration);
|
|
1845
|
+
}
|
|
1846
|
+
applyPose(pose) {
|
|
1847
|
+
const [left, right] = EYE_PATHS[pose] ?? EYE_PATHS.open;
|
|
1848
|
+
this.shadow?.querySelectorAll("[data-eye='left']").forEach((node) => node.setAttribute("d", left));
|
|
1849
|
+
this.shadow?.querySelectorAll("[data-eye='right']").forEach((node) => node.setAttribute("d", right));
|
|
1850
|
+
this.shadow?.querySelectorAll("[data-eyes]").forEach((node) => node.setAttribute("data-pose", pose));
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
var ICON = {
|
|
1854
|
+
pause: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9M11 3.5v9"/></svg>`,
|
|
1855
|
+
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>`,
|
|
1856
|
+
close: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4 4 12"/></svg>`,
|
|
1857
|
+
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>`
|
|
1858
|
+
};
|
|
1859
|
+
function mark(gradientId) {
|
|
1860
|
+
return `<svg class="mark" viewBox="14 9 138 126" fill="none" aria-hidden="true">
|
|
1861
|
+
<defs>
|
|
1862
|
+
<linearGradient id="${gradientId}" x1="112" y1="41" x2="143" y2="17" gradientUnits="userSpaceOnUse">
|
|
1863
|
+
<stop offset="0" stop-color="#753eff"></stop>
|
|
1864
|
+
<stop offset="1" stop-color="#1772ff"></stop>
|
|
1865
|
+
</linearGradient>
|
|
1866
|
+
</defs>
|
|
1867
|
+
<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>
|
|
1868
|
+
<path d="M112 41C119 37 123 27 131 22C135 19 139 18 143 17" stroke="url(#${gradientId})" stroke-width="8" stroke-linecap="round"></path>
|
|
1869
|
+
<g class="eyes" data-eyes data-pose="open" stroke="#10182e" stroke-width="7" stroke-linecap="round">
|
|
1870
|
+
<path data-eye="left" d="M59.5 79.5v15"></path>
|
|
1871
|
+
<path data-eye="right" d="M93 79.5v15"></path>
|
|
1872
|
+
</g>
|
|
1873
|
+
</svg>`;
|
|
1874
|
+
}
|
|
1875
|
+
var TEMPLATE = `
|
|
1876
|
+
<style>
|
|
1877
|
+
:host { color-scheme: dark; }
|
|
1878
|
+
button {
|
|
1879
|
+
display: grid;
|
|
1880
|
+
place-items: center;
|
|
1881
|
+
width: 32px;
|
|
1882
|
+
height: 32px;
|
|
1883
|
+
margin: 0;
|
|
1884
|
+
padding: 0;
|
|
1885
|
+
border: 1px solid #3a4458;
|
|
1886
|
+
border-radius: 999px;
|
|
1887
|
+
background: transparent;
|
|
1888
|
+
color: #f4f3fc;
|
|
1889
|
+
cursor: pointer;
|
|
1890
|
+
}
|
|
1891
|
+
button svg {
|
|
1892
|
+
width: 15px;
|
|
1893
|
+
height: 15px;
|
|
1894
|
+
fill: none;
|
|
1895
|
+
stroke: currentColor;
|
|
1896
|
+
stroke-width: 1.7;
|
|
1897
|
+
stroke-linecap: round;
|
|
1898
|
+
stroke-linejoin: round;
|
|
1899
|
+
}
|
|
1900
|
+
button:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
1901
|
+
button:focus-visible { outline: 2px solid #a596ff; outline-offset: 2px; }
|
|
1902
|
+
button:disabled { cursor: not-allowed; opacity: 0.4; }
|
|
1903
|
+
button[hidden] { display: none; }
|
|
1904
|
+
button.text {
|
|
1905
|
+
width: auto;
|
|
1906
|
+
padding: 0 12px;
|
|
1907
|
+
font: 600 12px/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
1908
|
+
}
|
|
1909
|
+
button.record {
|
|
1910
|
+
border: 0;
|
|
1911
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
1912
|
+
color: #fff;
|
|
1913
|
+
}
|
|
1914
|
+
button.finish {
|
|
1915
|
+
border: 0;
|
|
1916
|
+
background: #e5485f;
|
|
1917
|
+
color: #fff;
|
|
1918
|
+
}
|
|
1919
|
+
button.record:hover:not(:disabled),
|
|
1920
|
+
button.finish:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
1921
|
+
button.primary {
|
|
1922
|
+
border: 0;
|
|
1923
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
1924
|
+
color: #fff;
|
|
1925
|
+
}
|
|
1926
|
+
button.primary:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
1927
|
+
.launcher, .dock {
|
|
1928
|
+
position: fixed;
|
|
1929
|
+
right: 16px;
|
|
1930
|
+
bottom: 16px;
|
|
1931
|
+
z-index: 1;
|
|
1932
|
+
border: 1px solid #232b3b;
|
|
1933
|
+
background: #151b27;
|
|
1934
|
+
box-shadow: 0 18px 40px rgba(3, 5, 12, 0.5);
|
|
1935
|
+
pointer-events: auto;
|
|
1936
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
1937
|
+
}
|
|
1938
|
+
.launcher {
|
|
1939
|
+
width: 56px;
|
|
1940
|
+
height: 56px;
|
|
1941
|
+
padding: 8px;
|
|
1942
|
+
border-radius: 999px;
|
|
1943
|
+
cursor: pointer;
|
|
1944
|
+
}
|
|
1945
|
+
.launcher .mark { width: 100%; height: 100%; }
|
|
1946
|
+
.launcher::after {
|
|
1947
|
+
content: "";
|
|
1948
|
+
position: absolute;
|
|
1949
|
+
right: 1px;
|
|
1950
|
+
bottom: 1px;
|
|
1951
|
+
width: 10px;
|
|
1952
|
+
height: 10px;
|
|
1953
|
+
border-radius: 999px;
|
|
1954
|
+
background: #929bb1;
|
|
1955
|
+
box-shadow: 0 0 0 2px #151b27;
|
|
1956
|
+
}
|
|
1957
|
+
[data-state="recording"] .launcher::after { background: #1673ff; }
|
|
1958
|
+
[data-state="paused"] .launcher::after { background: #d0c8ff; }
|
|
1959
|
+
[data-state="error"] .launcher::after { background: #e5485f; }
|
|
1960
|
+
.dock {
|
|
1961
|
+
display: flex;
|
|
1962
|
+
align-items: center;
|
|
1963
|
+
gap: 10px;
|
|
1964
|
+
max-width: calc(100vw - 32px);
|
|
1965
|
+
padding: 6px 6px 6px 12px;
|
|
1966
|
+
border-radius: 999px;
|
|
1967
|
+
color: #f4f3fc;
|
|
1968
|
+
overflow: visible;
|
|
1969
|
+
}
|
|
1970
|
+
[data-open="false"] .dock, [data-open="true"] .launcher { display: none; }
|
|
1971
|
+
.brand { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: visible; }
|
|
1972
|
+
.face { position: relative; flex: none; width: 28px; height: 28px; overflow: visible; }
|
|
1973
|
+
.dock .mark { width: 28px; height: 28px; }
|
|
1974
|
+
.word {
|
|
1975
|
+
color: #f4f3fc;
|
|
1976
|
+
font-size: 14px;
|
|
1977
|
+
font-weight: 600;
|
|
1978
|
+
letter-spacing: -0.03em;
|
|
1979
|
+
line-height: 1;
|
|
1980
|
+
}
|
|
1981
|
+
.actions { display: flex; align-items: center; gap: 6px; }
|
|
1982
|
+
.note {
|
|
1983
|
+
position: fixed;
|
|
1984
|
+
right: 16px;
|
|
1985
|
+
bottom: 78px;
|
|
1986
|
+
z-index: 1;
|
|
1987
|
+
margin: 0;
|
|
1988
|
+
padding: 4px 10px;
|
|
1989
|
+
border-radius: 999px;
|
|
1990
|
+
background: #151b27;
|
|
1991
|
+
color: #ff9aa8;
|
|
1992
|
+
font: 600 11px/1.3 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
1993
|
+
pointer-events: none;
|
|
1994
|
+
}
|
|
1995
|
+
.note:empty { display: none; }
|
|
1996
|
+
[data-shell]:has(.note:not(:empty)) .privacy { bottom: calc(100% + 48px); }
|
|
1997
|
+
.eyes {
|
|
1998
|
+
transform-box: view-box;
|
|
1999
|
+
transform-origin: 0 0;
|
|
2000
|
+
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2001
|
+
}
|
|
2002
|
+
.eyes[data-pose="left"] { transform: translate(-4.5%, 0.8%); }
|
|
2003
|
+
.eyes[data-pose="right"] { transform: translate(4.5%, 0.8%); }
|
|
2004
|
+
.eyes[data-pose="up"] { transform: translate(0, -3.5%); }
|
|
2005
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2006
|
+
.eyes { transition: none; }
|
|
2007
|
+
.eyes[data-pose] { transform: none; }
|
|
2008
|
+
.speak:not([hidden]) .bubble { animation: none; }
|
|
2009
|
+
}
|
|
2010
|
+
.speak {
|
|
2011
|
+
position: fixed;
|
|
2012
|
+
right: 16px;
|
|
2013
|
+
bottom: 76px;
|
|
2014
|
+
z-index: 2;
|
|
2015
|
+
width: max-content;
|
|
2016
|
+
max-width: min(16rem, calc(100vw - 32px));
|
|
2017
|
+
pointer-events: auto;
|
|
2018
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2019
|
+
}
|
|
2020
|
+
.speak[hidden] { display: none; }
|
|
2021
|
+
.bubble {
|
|
2022
|
+
position: relative;
|
|
2023
|
+
padding: 0.6rem 0.7rem 0.55rem;
|
|
2024
|
+
border: 1px solid #3d3590;
|
|
2025
|
+
border-radius: 1rem;
|
|
2026
|
+
background: #1b1a3f;
|
|
2027
|
+
color: #c7cadc;
|
|
2028
|
+
box-shadow: 0 16px 40px rgba(3, 5, 12, 0.45);
|
|
2029
|
+
animation: bubble-in 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2030
|
+
}
|
|
2031
|
+
.bubble::after {
|
|
2032
|
+
content: "";
|
|
2033
|
+
position: absolute;
|
|
2034
|
+
right: var(--tail, 22px);
|
|
2035
|
+
bottom: -5px;
|
|
2036
|
+
width: 9px;
|
|
2037
|
+
height: 9px;
|
|
2038
|
+
border-right: 1px solid #3d3590;
|
|
2039
|
+
border-bottom: 1px solid #3d3590;
|
|
2040
|
+
background: #1b1a3f;
|
|
2041
|
+
transform: rotate(45deg);
|
|
2042
|
+
}
|
|
2043
|
+
.bubble .mark {
|
|
2044
|
+
display: block;
|
|
2045
|
+
width: 22px;
|
|
2046
|
+
height: 22px;
|
|
2047
|
+
margin: 0 0 0.35rem;
|
|
2048
|
+
}
|
|
2049
|
+
@keyframes bubble-in {
|
|
2050
|
+
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
|
2051
|
+
to { opacity: 1; transform: none; }
|
|
2052
|
+
}
|
|
2053
|
+
.bubble p {
|
|
2054
|
+
margin: 0;
|
|
2055
|
+
color: #c7cadc;
|
|
2056
|
+
font-size: 0.8125rem;
|
|
2057
|
+
font-weight: 400;
|
|
2058
|
+
line-height: 1.45;
|
|
2059
|
+
}
|
|
2060
|
+
.privacy {
|
|
2061
|
+
position: absolute;
|
|
2062
|
+
left: auto;
|
|
2063
|
+
right: 50%;
|
|
2064
|
+
bottom: calc(100% + 18px);
|
|
2065
|
+
z-index: 3;
|
|
2066
|
+
width: min(16.5rem, calc(100vw - 24px));
|
|
2067
|
+
max-width: min(16.5rem, calc(100vw - 24px));
|
|
2068
|
+
pointer-events: auto;
|
|
2069
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2070
|
+
transform: translateX(20px);
|
|
2071
|
+
}
|
|
2072
|
+
.privacy[hidden] { display: none; }
|
|
2073
|
+
.privacy .bubble {
|
|
2074
|
+
padding: 0.75rem 2.15rem 0.75rem 0.9rem;
|
|
2075
|
+
border-radius: 16px;
|
|
2076
|
+
}
|
|
2077
|
+
.privacy .bubble::after {
|
|
2078
|
+
right: var(--tail, 12px);
|
|
2079
|
+
bottom: -5px;
|
|
2080
|
+
width: 10px;
|
|
2081
|
+
height: 10px;
|
|
2082
|
+
}
|
|
2083
|
+
@media (max-width: 560px) {
|
|
2084
|
+
.word { display: none; }
|
|
2085
|
+
.privacy {
|
|
2086
|
+
position: fixed;
|
|
2087
|
+
left: 12px;
|
|
2088
|
+
right: 12px;
|
|
2089
|
+
bottom: 76px;
|
|
2090
|
+
width: auto;
|
|
2091
|
+
max-width: none;
|
|
2092
|
+
transform: none;
|
|
2093
|
+
}
|
|
2094
|
+
.privacy .bubble {
|
|
2095
|
+
padding: 0.65rem 1.85rem 0.6rem 0.75rem;
|
|
2096
|
+
}
|
|
2097
|
+
.privacy .bubble p { font-size: 0.75rem; }
|
|
2098
|
+
}
|
|
2099
|
+
.privacy button.hide {
|
|
2100
|
+
position: absolute;
|
|
2101
|
+
top: 0.4rem;
|
|
2102
|
+
right: 0.4rem;
|
|
2103
|
+
width: 24px;
|
|
2104
|
+
height: 24px;
|
|
2105
|
+
border: 0;
|
|
2106
|
+
background: transparent;
|
|
2107
|
+
color: #c7cadc;
|
|
2108
|
+
}
|
|
2109
|
+
.privacy button.hide:hover:not(:disabled) { color: #f4f3fc; }
|
|
2110
|
+
.choices { display: flex; gap: 6px; margin-top: 0.7rem; }
|
|
2111
|
+
.speak button.choice {
|
|
2112
|
+
width: auto;
|
|
2113
|
+
height: auto;
|
|
2114
|
+
min-height: 1.75rem;
|
|
2115
|
+
padding: 0 0.7rem;
|
|
2116
|
+
border-radius: 999px;
|
|
2117
|
+
font: 600 0.75rem/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2118
|
+
}
|
|
2119
|
+
.speak button.secondary {
|
|
2120
|
+
border: 1px solid #3a4458;
|
|
2121
|
+
background: transparent;
|
|
2122
|
+
color: #f4f3fc;
|
|
2123
|
+
}
|
|
2124
|
+
.speak button.secondary:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
2125
|
+
.speak button.danger {
|
|
2126
|
+
border: 0;
|
|
2127
|
+
background: #e5485f;
|
|
2128
|
+
color: #fff;
|
|
2129
|
+
}
|
|
2130
|
+
.speak button.danger:hover:not(:disabled) { color: #fff; background: color-mix(in srgb, #e5485f 88%, #fff); }
|
|
2131
|
+
</style>
|
|
2132
|
+
<div data-shell data-open="true" data-state="recording">
|
|
2133
|
+
<button type="button" class="launcher" data-action="open" aria-label="Abrir controles da grava\xE7\xE3o">
|
|
2134
|
+
${mark("offscript-trail-launcher")}
|
|
2135
|
+
</button>
|
|
2136
|
+
<div class="dock" role="toolbar" aria-label="Controles da grava\xE7\xE3o">
|
|
2137
|
+
<div class="brand">
|
|
2138
|
+
<span class="face">
|
|
2139
|
+
${mark("offscript-trail-dock")}
|
|
2140
|
+
<div class="privacy" data-privacy hidden>
|
|
2141
|
+
<div class="bubble" role="status">
|
|
2142
|
+
<button type="button" class="hide" data-action="privacy-hide" aria-label="Fechar aviso">${ICON.close}</button>
|
|
2143
|
+
<p>Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado \u2014 senha e dado sens\xEDvel tamb\xE9m.</p>
|
|
2144
|
+
</div>
|
|
2145
|
+
</div>
|
|
2146
|
+
</span>
|
|
2147
|
+
<span class="word">Offscript</span>
|
|
2148
|
+
</div>
|
|
2149
|
+
<div class="actions">
|
|
2150
|
+
<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>
|
|
2151
|
+
<button type="button" class="text record" data-action="record">Iniciar</button>
|
|
2152
|
+
<button type="button" data-action="pause" aria-label="Pausar" title="Pausar">${ICON.pause}</button>
|
|
2153
|
+
<button type="button" data-action="resume" aria-label="Retomar" title="Retomar">${ICON.play}</button>
|
|
2154
|
+
<button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova grava\xE7\xE3o</button>
|
|
2155
|
+
<button type="button" class="text finish" data-action="stop" title="Encerra a coleta e envia o que foi gravado">Finalizar</button>
|
|
2156
|
+
<button type="button" data-action="close" aria-label="Fechar" title="Fechar">${ICON.close}</button>
|
|
2157
|
+
</div>
|
|
2158
|
+
</div>
|
|
2159
|
+
<div class="speak" data-confirm hidden>
|
|
2160
|
+
<div class="bubble" role="dialog" aria-labelledby="offscript-confirm-title">
|
|
2161
|
+
${mark("offscript-trail-speak")}
|
|
2162
|
+
<p id="offscript-confirm-title">Descartar esta grava\xE7\xE3o? O que foi gravado ser\xE1 ignorado.</p>
|
|
2163
|
+
<div class="choices">
|
|
2164
|
+
<button type="button" class="choice secondary" data-action="cancel">Cancelar</button>
|
|
2165
|
+
<button type="button" class="choice danger" data-action="confirm">Descartar</button>
|
|
2166
|
+
</div>
|
|
2167
|
+
</div>
|
|
2168
|
+
</div>
|
|
2169
|
+
<p class="note" data-note></p>
|
|
2170
|
+
</div>
|
|
2171
|
+
`;
|
|
2172
|
+
var SUCCESS_TEMPLATE = `
|
|
2173
|
+
<style>
|
|
2174
|
+
:host { color-scheme: dark; }
|
|
2175
|
+
.success {
|
|
2176
|
+
display: flex;
|
|
2177
|
+
align-items: center;
|
|
2178
|
+
gap: 12px;
|
|
2179
|
+
padding: 12px 14px;
|
|
2180
|
+
border: 1px solid #377c65;
|
|
2181
|
+
border-radius: 16px;
|
|
2182
|
+
background: #152c25;
|
|
2183
|
+
color: #dcfce7;
|
|
2184
|
+
box-shadow: 0 12px 32px rgba(3, 5, 12, 0.35);
|
|
2185
|
+
font: 500 13px/1.5 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2186
|
+
}
|
|
2187
|
+
p { margin: 0; }
|
|
2188
|
+
button {
|
|
2189
|
+
flex: none;
|
|
2190
|
+
display: grid;
|
|
2191
|
+
place-items: center;
|
|
2192
|
+
width: 28px;
|
|
2193
|
+
height: 28px;
|
|
2194
|
+
padding: 0;
|
|
2195
|
+
border: 0;
|
|
2196
|
+
border-radius: 999px;
|
|
2197
|
+
background: transparent;
|
|
2198
|
+
color: inherit;
|
|
2199
|
+
font: 20px/1 ui-sans-serif, system-ui, sans-serif;
|
|
2200
|
+
cursor: pointer;
|
|
2201
|
+
}
|
|
2202
|
+
button:hover { background: #244a3c; }
|
|
2203
|
+
button:focus-visible { outline: 2px solid #86efac; outline-offset: 2px; }
|
|
2204
|
+
</style>
|
|
2205
|
+
<div class="success">
|
|
2206
|
+
<p role="status">Grava\xE7\xE3o enviada com sucesso!</p>
|
|
2207
|
+
<button type="button" aria-label="Fechar confirma\xE7\xE3o">\xD7</button>
|
|
2208
|
+
</div>
|
|
2209
|
+
`;
|
|
2210
|
+
|
|
2269
2211
|
// src/core/OffscriptSDK.ts
|
|
2270
|
-
var MAX_LOG = 5e3;
|
|
2271
2212
|
var Offscript = class {
|
|
2272
|
-
constructor(config) {
|
|
2213
|
+
constructor(config = {}) {
|
|
2273
2214
|
this.bus = new Emitter();
|
|
2274
2215
|
this.status = "idle";
|
|
2275
2216
|
this.recorders = [];
|
|
2276
2217
|
this.accepting = false;
|
|
2277
2218
|
this.destroyed = false;
|
|
2219
|
+
this.suspended = false;
|
|
2220
|
+
this.events = [];
|
|
2278
2221
|
this.recent = [];
|
|
2279
2222
|
this.actionLog = [];
|
|
2280
2223
|
this.contextLog = [];
|
|
2281
|
-
this.
|
|
2282
|
-
this.
|
|
2224
|
+
this.sequence = 0;
|
|
2225
|
+
this.pageId = uid("page_");
|
|
2226
|
+
this.submitted = false;
|
|
2283
2227
|
this.config = resolveConfig(config);
|
|
2284
2228
|
this.sanitizer = new DataSanitizer(this.config.privacy);
|
|
2285
|
-
this.sessions = new SessionManager(this.config);
|
|
2286
2229
|
}
|
|
2287
|
-
// ------------------------------------------------------------------ API pública
|
|
2288
2230
|
getState() {
|
|
2289
|
-
return { status: this.status,
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
return
|
|
2231
|
+
return { status: this.status, pending: this.submitted ? 0 : this.events.length, lastError: this.lastError };
|
|
2232
|
+
}
|
|
2233
|
+
capturesSensitiveValues() {
|
|
2234
|
+
return this.config.privacy.captureSensitiveValues;
|
|
2235
|
+
}
|
|
2236
|
+
async init() {
|
|
2237
|
+
this.assertAlive();
|
|
2238
|
+
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um navegador");
|
|
2239
|
+
this.initPromise ?? (this.initPromise = this.initializeDraft());
|
|
2240
|
+
await this.initPromise;
|
|
2241
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2242
|
+
if (this.config.controls && !this.controls) {
|
|
2243
|
+
this.controls = new RecordingControls(this);
|
|
2244
|
+
this.controls.mount();
|
|
2245
|
+
}
|
|
2293
2246
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
return this.initPromise;
|
|
2247
|
+
async initializeDraft() {
|
|
2248
|
+
if (!this.config.persistenceKey) return;
|
|
2249
|
+
this.store = await RecordingStore.open(this.config.persistenceKey);
|
|
2250
|
+
await this.restoreDraft();
|
|
2299
2251
|
}
|
|
2300
|
-
async
|
|
2301
|
-
|
|
2302
|
-
this.
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
this.transport = new EventTransport(this.config.transport, this.sessions.api, buffer);
|
|
2306
|
-
this.transport.on("flushed", (e) => this.bus.emit("flush", e));
|
|
2307
|
-
this.transport.on("overflow", (e) => this.bus.emit("overflow", e));
|
|
2308
|
-
this.transport.on("fatal", ({ error }) => void this.onFatal(error));
|
|
2309
|
-
this.transport.on("failed", ({ error }) => this.debug("transport failed", error));
|
|
2310
|
-
await this.transport.purgeExpired(this.config.privacy.localRetentionMs);
|
|
2252
|
+
async restoreDraft() {
|
|
2253
|
+
const saved = await this.store.load();
|
|
2254
|
+
if (this.suspended) return;
|
|
2255
|
+
this.clearRecording();
|
|
2256
|
+
if (!saved) {
|
|
2311
2257
|
this.setStatus("idle");
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
this.events = saved.events;
|
|
2261
|
+
this.sequence = saved.events[saved.events.length - 1]?.sequence ?? 0;
|
|
2262
|
+
this.actionLog = saved.events.filter((event) => event.kind === "action").map((event) => event.data);
|
|
2263
|
+
this.contextLog = saved.events.filter((event) => event.kind === "context").map((event) => event.data);
|
|
2264
|
+
this.setStatus(saved.status);
|
|
2265
|
+
if (saved.status === "stopped") {
|
|
2266
|
+
this.payload = this.buildPayload();
|
|
2267
|
+
return;
|
|
2317
2268
|
}
|
|
2269
|
+
this.accepting = saved.status === "recording";
|
|
2270
|
+
this.attach();
|
|
2271
|
+
await this.startRecorders();
|
|
2272
|
+
if (saved.status === "paused") this.recorders.forEach((recorder) => recorder.pause?.());
|
|
2273
|
+
}
|
|
2274
|
+
save(operation) {
|
|
2275
|
+
void operation.catch((error) => {
|
|
2276
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2277
|
+
this.lastError = { code: "LOCAL_STORAGE_FAILED", message: err.message };
|
|
2278
|
+
this.bus.emit("error", err);
|
|
2279
|
+
});
|
|
2318
2280
|
}
|
|
2319
|
-
|
|
2320
|
-
if (this.destroyed) throw new InvalidStateError("SDK destru\xEDdo");
|
|
2281
|
+
startRecording() {
|
|
2321
2282
|
if (this.startPromise) return this.startPromise;
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2283
|
+
this.startPromise = this.doStart().finally(() => {
|
|
2284
|
+
this.startPromise = void 0;
|
|
2285
|
+
});
|
|
2286
|
+
return this.startPromise;
|
|
2325
2287
|
}
|
|
2326
|
-
async doStart(
|
|
2288
|
+
async doStart() {
|
|
2327
2289
|
await this.init();
|
|
2328
|
-
|
|
2329
|
-
if (
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
}
|
|
2333
|
-
if (this.status === "stopping") throw new InvalidStateError("Grava\xE7\xE3o em encerramento");
|
|
2334
|
-
this.setStatus("initializing");
|
|
2335
|
-
try {
|
|
2336
|
-
const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion);
|
|
2337
|
-
await this.sessions.create(opts, { ...ctx, url: this.sanitizer.sanitizeUrl(ctx.url), referrer: void 0, title: void 0 });
|
|
2338
|
-
const s = this.sessions.session;
|
|
2339
|
-
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
2340
|
-
this.attach();
|
|
2341
|
-
this.accepting = true;
|
|
2342
|
-
this.setStatus("recording");
|
|
2343
|
-
this.emitLifecycle("session-start", { pageCount: 1 }, "critical");
|
|
2344
|
-
await this.startRecorders();
|
|
2345
|
-
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
2346
|
-
this.bus.emit("sessionStart", s);
|
|
2347
|
-
return s;
|
|
2348
|
-
} catch (e) {
|
|
2349
|
-
await this.teardown();
|
|
2350
|
-
this.sessions.clearPersisted();
|
|
2351
|
-
this.sessions.reset();
|
|
2352
|
-
this.fail(e);
|
|
2353
|
-
throw e;
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
async resumePersisted(state) {
|
|
2290
|
+
if (this.status === "recording" || this.status === "paused") return;
|
|
2291
|
+
if (this.completePromise || this.stopPromise) throw new InvalidStateError("Finaliza\xE7\xE3o em andamento");
|
|
2292
|
+
await this.store?.clear();
|
|
2293
|
+
this.clearRecording();
|
|
2357
2294
|
this.setStatus("initializing");
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
this.emitLifecycle("page-start", { navigationType: safe(() => performance.getEntriesByType("navigation")[0]?.type) }, "critical");
|
|
2367
|
-
await this.startRecorders();
|
|
2368
|
-
if (state.status === "paused") this.recorders.forEach((r) => r.pause?.());
|
|
2369
|
-
this.actions?.recordPageLoad(safe(() => performance.getEntriesByType("navigation")[0]?.type), document.referrer);
|
|
2370
|
-
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
2371
|
-
this.bus.emit("sessionStart", s);
|
|
2372
|
-
} catch (e) {
|
|
2373
|
-
await this.teardown();
|
|
2374
|
-
this.sessions.clearPersisted();
|
|
2375
|
-
this.sessions.reset();
|
|
2376
|
-
this.fail(e);
|
|
2377
|
-
if (!(e instanceof SessionExpiredError || e instanceof SessionDisabledError || e instanceof AuthError)) throw e;
|
|
2378
|
-
}
|
|
2295
|
+
await this.store?.setStatus("recording");
|
|
2296
|
+
this.accepting = true;
|
|
2297
|
+
this.attach();
|
|
2298
|
+
this.emitLifecycle("recording-start");
|
|
2299
|
+
await this.startRecorders();
|
|
2300
|
+
this.setStatus("recording");
|
|
2301
|
+
await this.store?.flush();
|
|
2302
|
+
this.bus.emit("recordingStart", void 0);
|
|
2379
2303
|
}
|
|
2380
2304
|
async pauseRecording() {
|
|
2305
|
+
this.assertAlive();
|
|
2306
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2381
2307
|
if (this.status === "paused") return;
|
|
2382
|
-
if (this.status !== "recording") throw new InvalidStateError(
|
|
2308
|
+
if (this.status !== "recording") throw new InvalidStateError("Sem grava\xE7\xE3o ativa");
|
|
2383
2309
|
this.actions?.flushAll();
|
|
2384
|
-
this.emitLifecycle("pause"
|
|
2310
|
+
this.emitLifecycle("pause");
|
|
2385
2311
|
this.accepting = false;
|
|
2386
2312
|
this.recorders.forEach((r) => safe(() => r.pause?.()));
|
|
2387
|
-
this.sessions.setStatus("paused");
|
|
2388
2313
|
this.setStatus("paused");
|
|
2389
|
-
|
|
2314
|
+
await this.store?.flush();
|
|
2390
2315
|
}
|
|
2391
2316
|
async resumeRecording() {
|
|
2317
|
+
this.assertAlive();
|
|
2318
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2392
2319
|
if (this.status === "recording") return;
|
|
2393
|
-
if (this.status !== "paused") throw new InvalidStateError(
|
|
2320
|
+
if (this.status !== "paused") throw new InvalidStateError("Grava\xE7\xE3o n\xE3o est\xE1 pausada");
|
|
2394
2321
|
this.accepting = true;
|
|
2395
|
-
this.
|
|
2322
|
+
this.emitLifecycle("resume");
|
|
2323
|
+
for (const r of this.recorders) await r.resume?.();
|
|
2396
2324
|
this.setStatus("recording");
|
|
2397
|
-
this.
|
|
2398
|
-
for (const r of this.recorders) await Promise.resolve(safe(() => r.resume?.()));
|
|
2325
|
+
await this.store?.flush();
|
|
2399
2326
|
}
|
|
2400
2327
|
stopRecording() {
|
|
2401
2328
|
if (this.stopPromise) return this.stopPromise;
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
return Promise.resolve({ sessionId: s?.sessionId ?? "", flushed: true, pendingEvents: 0, completed: true });
|
|
2406
|
-
return Promise.reject(new InvalidStateError(`N\xE3o \xE9 poss\xEDvel encerrar no estado ${this.status}`));
|
|
2407
|
-
}
|
|
2408
|
-
this.stopPromise = this.doStop().finally(() => this.stopPromise = void 0);
|
|
2329
|
+
this.stopPromise = this.doStop().finally(() => {
|
|
2330
|
+
this.stopPromise = void 0;
|
|
2331
|
+
});
|
|
2409
2332
|
return this.stopPromise;
|
|
2410
2333
|
}
|
|
2411
2334
|
async doStop() {
|
|
2412
|
-
|
|
2335
|
+
this.assertAlive();
|
|
2336
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2337
|
+
if (this.startPromise) await this.startPromise;
|
|
2338
|
+
if (this.payload) return this.payload;
|
|
2339
|
+
if (this.status !== "recording" && this.status !== "paused") throw new InvalidStateError("Sem grava\xE7\xE3o ativa");
|
|
2413
2340
|
this.setStatus("stopping");
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2341
|
+
this.actions?.flushAll();
|
|
2342
|
+
this.emitLifecycle("stop");
|
|
2343
|
+
const recorders = this.recorders.splice(0).reverse();
|
|
2344
|
+
this.lifecycle?.uninstall();
|
|
2345
|
+
for (const recorder of recorders) {
|
|
2346
|
+
if (!(recorder instanceof HTTPRecorder)) safe(() => recorder.stop());
|
|
2347
|
+
}
|
|
2348
|
+
for (const recorder of recorders) {
|
|
2349
|
+
if (recorder instanceof HTTPRecorder) await recorder.drain();
|
|
2417
2350
|
}
|
|
2418
2351
|
this.accepting = false;
|
|
2419
|
-
this.
|
|
2420
|
-
this.
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2352
|
+
this.rrweb = this.actions = this.context = void 0;
|
|
2353
|
+
this.payload = this.buildPayload();
|
|
2354
|
+
this.setStatus("stopped");
|
|
2355
|
+
await this.store?.flush();
|
|
2356
|
+
this.bus.emit("recordingEnd", this.payload);
|
|
2357
|
+
return this.payload;
|
|
2358
|
+
}
|
|
2359
|
+
complete(options) {
|
|
2360
|
+
this.assertAlive();
|
|
2361
|
+
if (this.completePromise) return this.completePromise;
|
|
2362
|
+
if (this.submitted) return Promise.resolve(this.response);
|
|
2363
|
+
this.completePromise = this.doComplete(options).finally(() => {
|
|
2364
|
+
this.completePromise = void 0;
|
|
2365
|
+
});
|
|
2366
|
+
return this.completePromise;
|
|
2367
|
+
}
|
|
2368
|
+
async doComplete({ endpoint, token }) {
|
|
2369
|
+
const url = new URL(endpoint, location.href);
|
|
2370
|
+
if (!["https:", "http:"].includes(url.protocol)) throw new ConfigError("Endpoint HTTP inv\xE1lido");
|
|
2371
|
+
if (!token?.trim()) throw new ConfigError("Token obrigat\xF3rio para envio");
|
|
2372
|
+
const payload = await this.stopRecording();
|
|
2373
|
+
try {
|
|
2374
|
+
const response = await fetch(url.href, {
|
|
2375
|
+
method: "POST",
|
|
2376
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${token.trim()}` },
|
|
2377
|
+
body: JSON.stringify(payload)
|
|
2378
|
+
});
|
|
2379
|
+
if (!response.ok) throw new OffscriptError("TRANSPORT_FAILED", `Falha ao enviar grava\xE7\xE3o: HTTP ${response.status}`, { status: response.status });
|
|
2380
|
+
this.submitted = true;
|
|
2381
|
+
await this.store?.clear();
|
|
2382
|
+
const text = await response.text();
|
|
2424
2383
|
try {
|
|
2425
|
-
|
|
2426
|
-
} catch
|
|
2427
|
-
this.
|
|
2384
|
+
this.response = text ? JSON.parse(text) : null;
|
|
2385
|
+
} catch {
|
|
2386
|
+
this.response = text;
|
|
2428
2387
|
}
|
|
2388
|
+
this.lastError = void 0;
|
|
2389
|
+
this.controls?.showSuccess();
|
|
2390
|
+
return this.response;
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2393
|
+
this.lastError = { code: err instanceof OffscriptError ? err.code : "INTERNAL", message: err.message };
|
|
2394
|
+
this.bus.emit("error", err);
|
|
2395
|
+
throw err;
|
|
2429
2396
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2397
|
+
}
|
|
2398
|
+
async finishRecording() {
|
|
2399
|
+
if (this.config.onComplete) return this.config.onComplete();
|
|
2400
|
+
return this.stopRecording();
|
|
2401
|
+
}
|
|
2402
|
+
async discardRecording() {
|
|
2403
|
+
this.assertAlive();
|
|
2404
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2405
|
+
if (this.startPromise) await this.startPromise;
|
|
2406
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2407
|
+
if (this.completePromise) throw new InvalidStateError("Envio em andamento");
|
|
2408
|
+
this.accepting = false;
|
|
2409
|
+
this.stopRecorders();
|
|
2432
2410
|
this.lifecycle?.uninstall();
|
|
2433
|
-
this.
|
|
2434
|
-
this.
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
return summary;
|
|
2439
|
-
}
|
|
2440
|
-
/**
|
|
2441
|
-
* Libera todos os recursos. Por padrão encerra a gravação ativa; com `{ complete: false }` apenas
|
|
2442
|
-
* desanexa a instrumentação e mantém a sessão persistida para ser retomada por outra instância.
|
|
2443
|
-
*/
|
|
2444
|
-
async destroy(opts = {}) {
|
|
2411
|
+
await this.store?.clear();
|
|
2412
|
+
this.clearRecording();
|
|
2413
|
+
this.setStatus("idle");
|
|
2414
|
+
}
|
|
2415
|
+
async destroy() {
|
|
2445
2416
|
if (this.destroyed) return;
|
|
2446
|
-
if (
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
this.
|
|
2455
|
-
await this.teardown();
|
|
2456
|
-
this.sessions.reset();
|
|
2457
|
-
this.transport?.close();
|
|
2417
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2418
|
+
if (this.startPromise) await this.startPromise;
|
|
2419
|
+
if (this.completePromise) await this.completePromise.catch(() => void 0);
|
|
2420
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2421
|
+
this.accepting = false;
|
|
2422
|
+
this.stopRecorders();
|
|
2423
|
+
this.lifecycle?.uninstall();
|
|
2424
|
+
this.controls?.unmount();
|
|
2425
|
+
await this.store?.close();
|
|
2458
2426
|
this.bus.clear();
|
|
2427
|
+
this.clearRecording();
|
|
2428
|
+
this.destroyed = true;
|
|
2459
2429
|
}
|
|
2460
|
-
/** Dados estruturados para o motor Playwright, construídos a partir do que foi gravado nesta página. */
|
|
2461
2430
|
exportPlaywrightData() {
|
|
2462
|
-
|
|
2463
|
-
if (!s) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
2464
|
-
return buildPlaywrightContract({
|
|
2465
|
-
session: { sessionId: s.sessionId, executionId: s.executionId, testCaseId: s.testCaseId, platformId: s.platformId },
|
|
2466
|
-
actions: this.actionLog,
|
|
2467
|
-
contexts: this.contextLog.map((e) => e.data)
|
|
2468
|
-
});
|
|
2431
|
+
return buildPlaywrightContract({ actions: this.actionLog, contexts: this.contextLog });
|
|
2469
2432
|
}
|
|
2470
2433
|
getRecordedActions() {
|
|
2471
2434
|
return this.actionLog;
|
|
2472
2435
|
}
|
|
2473
|
-
// ------------------------------------------------------------------ EventSink
|
|
2474
2436
|
on(type, fn) {
|
|
2475
2437
|
return this.bus.on(type, fn);
|
|
2476
2438
|
}
|
|
2477
2439
|
off(type, fn) {
|
|
2478
2440
|
this.bus.off(type, fn);
|
|
2479
2441
|
}
|
|
2480
|
-
/** Usado pelos recorders. Não confundir com os eventos públicos (`on`). */
|
|
2481
2442
|
emit(kind, data, opts = {}) {
|
|
2482
2443
|
return this.record(kind, data, opts);
|
|
2483
2444
|
}
|
|
2484
2445
|
record(kind, data, opts) {
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
}
|
|
2496
|
-
this.
|
|
2497
|
-
const sequence = this.sessions.nextSequence();
|
|
2498
|
-
const timestamp = opts.at ?? Date.now();
|
|
2499
|
-
const id = `${session.tabId}:${sequence}`;
|
|
2500
|
-
let payload = data;
|
|
2501
|
-
let relatedActionId;
|
|
2502
|
-
let relatedActionDeltaMs;
|
|
2503
|
-
if (kind === "action") {
|
|
2504
|
-
const action = { ...data, id, sessionId: session.sessionId, executionId: session.executionId, sequence, timestamp };
|
|
2505
|
-
payload = action;
|
|
2506
|
-
this.recent.push({ id, t: timestamp });
|
|
2507
|
-
if (this.recent.length > 50) this.recent.shift();
|
|
2508
|
-
this.actionLog.push(action);
|
|
2509
|
-
if (this.actionLog.length > MAX_LOG) this.actionLog.shift();
|
|
2510
|
-
} else if (kind === "http" || kind === "console" || kind === "error" || kind === "iframe") {
|
|
2511
|
-
const rel = this.correlate(timestamp);
|
|
2512
|
-
relatedActionId = rel?.id;
|
|
2513
|
-
relatedActionDeltaMs = rel ? timestamp - rel.t : void 0;
|
|
2514
|
-
}
|
|
2515
|
-
const event = {
|
|
2516
|
-
id,
|
|
2517
|
-
kind,
|
|
2518
|
-
sessionId: session.sessionId,
|
|
2519
|
-
executionId: session.executionId,
|
|
2520
|
-
tabId: session.tabId,
|
|
2521
|
-
pageId: session.pageId,
|
|
2522
|
-
sequence,
|
|
2523
|
-
timestamp,
|
|
2524
|
-
relatedActionId,
|
|
2525
|
-
relatedActionDeltaMs,
|
|
2526
|
-
data: payload
|
|
2527
|
-
};
|
|
2528
|
-
if (kind === "context") {
|
|
2529
|
-
this.contextLog.push(event);
|
|
2530
|
-
if (this.contextLog.length > 50) this.contextLog.shift();
|
|
2531
|
-
}
|
|
2532
|
-
this.eventCount++;
|
|
2533
|
-
transport.enqueue(event);
|
|
2534
|
-
this.metrics.emitted++;
|
|
2535
|
-
if (this.config.debug) this.debug("event", kind, sequence);
|
|
2536
|
-
return event;
|
|
2537
|
-
} catch (e) {
|
|
2538
|
-
this.debug("emit failed", e);
|
|
2539
|
-
return null;
|
|
2540
|
-
} finally {
|
|
2541
|
-
if (t0) this.metrics.emitMs += performance.now() - t0;
|
|
2446
|
+
if (!this.accepting) return null;
|
|
2447
|
+
this.rrweb?.recoverIfNeeded();
|
|
2448
|
+
const sequence = ++this.sequence;
|
|
2449
|
+
const timestamp = opts.at ?? Date.now();
|
|
2450
|
+
const id = `${this.pageId}:${sequence}`;
|
|
2451
|
+
let payload = data;
|
|
2452
|
+
if (kind === "action") {
|
|
2453
|
+
const action = { ...data, id, sequence, timestamp };
|
|
2454
|
+
payload = action;
|
|
2455
|
+
this.actionLog.push(action);
|
|
2456
|
+
this.recent.push({ id, t: timestamp });
|
|
2457
|
+
if (this.recent.length > 50) this.recent.shift();
|
|
2542
2458
|
}
|
|
2459
|
+
const related = ["http", "console", "error", "iframe"].includes(kind) ? this.correlate(timestamp) : void 0;
|
|
2460
|
+
const event = {
|
|
2461
|
+
id,
|
|
2462
|
+
kind,
|
|
2463
|
+
tabId: this.pageId,
|
|
2464
|
+
pageId: this.pageId,
|
|
2465
|
+
sequence,
|
|
2466
|
+
timestamp,
|
|
2467
|
+
relatedActionId: related?.id,
|
|
2468
|
+
relatedActionDeltaMs: related ? timestamp - related.t : void 0,
|
|
2469
|
+
data: payload
|
|
2470
|
+
};
|
|
2471
|
+
if (kind === "context") this.contextLog.push(data);
|
|
2472
|
+
this.events.push(event);
|
|
2473
|
+
if (this.store) this.save(this.store.append(event));
|
|
2474
|
+
return event;
|
|
2543
2475
|
}
|
|
2544
2476
|
correlate(at) {
|
|
2545
2477
|
for (let i = this.recent.length - 1; i >= 0; i--) {
|
|
@@ -2555,18 +2487,27 @@ var Offscript = class {
|
|
|
2555
2487
|
attach() {
|
|
2556
2488
|
this.lifecycle ?? (this.lifecycle = new LifecycleManager({
|
|
2557
2489
|
onPageHide: () => {
|
|
2558
|
-
if (!this.accepting) return;
|
|
2559
2490
|
this.actions?.flushAll();
|
|
2560
|
-
|
|
2561
|
-
this.
|
|
2491
|
+
if (!this.store) return;
|
|
2492
|
+
this.accepting = false;
|
|
2493
|
+
this.stopRecorders();
|
|
2494
|
+
this.suspended = true;
|
|
2495
|
+
if (!this.restorePromise) this.store.checkpoint();
|
|
2496
|
+
},
|
|
2497
|
+
onPageShow: () => {
|
|
2498
|
+
if (!this.suspended) return;
|
|
2499
|
+
this.suspended = false;
|
|
2500
|
+
this.setStatus("initializing");
|
|
2501
|
+
this.restorePromise = this.restoreDraft().finally(() => {
|
|
2502
|
+
this.restorePromise = void 0;
|
|
2503
|
+
});
|
|
2504
|
+
this.save(this.restorePromise);
|
|
2562
2505
|
},
|
|
2563
2506
|
onVisibility: (hidden) => {
|
|
2564
|
-
if (hidden
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
}
|
|
2507
|
+
if (hidden) this.actions?.flushAll();
|
|
2508
|
+
},
|
|
2509
|
+
onConnectivity: () => {
|
|
2568
2510
|
},
|
|
2569
|
-
onConnectivity: (online) => this.transport?.setOnline(online),
|
|
2570
2511
|
onNavigation: ({ from, to, kind }) => {
|
|
2571
2512
|
if (!this.accepting) return;
|
|
2572
2513
|
this.actions?.recordNavigation(from, to, kind);
|
|
@@ -2575,15 +2516,14 @@ var Offscript = class {
|
|
|
2575
2516
|
}
|
|
2576
2517
|
}));
|
|
2577
2518
|
this.lifecycle.install();
|
|
2578
|
-
this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
|
|
2579
2519
|
}
|
|
2580
2520
|
async startRecorders() {
|
|
2581
2521
|
const r = this.config.recording;
|
|
2582
2522
|
const list = [];
|
|
2583
2523
|
if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
|
|
2584
|
-
if (r.context) list.push(this.context = new ContextRecorder(this
|
|
2524
|
+
if (r.context) list.push(this.context = new ContextRecorder(this));
|
|
2585
2525
|
if (r.actions) list.push(this.actions = new ActionRecorder(this));
|
|
2586
|
-
if (r.http) list.push(new HTTPRecorder(this
|
|
2526
|
+
if (r.http) list.push(new HTTPRecorder(this));
|
|
2587
2527
|
if (r.console || r.errors) list.push(new ErrorRecorder(this));
|
|
2588
2528
|
if (r.iframes) list.push(new IframeRecorder(this));
|
|
2589
2529
|
this.recorders = list;
|
|
@@ -2596,63 +2536,42 @@ var Offscript = class {
|
|
|
2596
2536
|
}
|
|
2597
2537
|
}
|
|
2598
2538
|
}
|
|
2539
|
+
buildPayload() {
|
|
2540
|
+
return {
|
|
2541
|
+
recording: this.events.filter((event) => event.kind !== "http"),
|
|
2542
|
+
request: this.events.filter((event) => event.kind === "http"),
|
|
2543
|
+
regression: this.exportPlaywrightData()
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2599
2546
|
stopRecorders() {
|
|
2600
2547
|
for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
|
|
2601
2548
|
this.rrweb = this.actions = this.context = void 0;
|
|
2602
2549
|
}
|
|
2603
|
-
|
|
2604
|
-
this.
|
|
2605
|
-
this.
|
|
2606
|
-
this.
|
|
2607
|
-
this.
|
|
2608
|
-
this.
|
|
2609
|
-
this.
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
if (this.status !== "recording" && this.status !== "paused") return;
|
|
2618
|
-
this.accepting = false;
|
|
2619
|
-
this.stopRecorders();
|
|
2620
|
-
this.sessions.stopHeartbeat();
|
|
2621
|
-
const session = this.sessions.session;
|
|
2622
|
-
await this.transport?.flushFinal(3e3).catch(() => void 0);
|
|
2623
|
-
this.transport?.stop();
|
|
2624
|
-
this.lifecycle?.uninstall();
|
|
2625
|
-
this.sessions.clearPersisted();
|
|
2626
|
-
this.sessions.reset();
|
|
2627
|
-
this.setStatus("stopped");
|
|
2628
|
-
this.bus.emit("disabled", { reason: err.message });
|
|
2629
|
-
if (session) this.bus.emit("sessionEnd", { sessionId: session.sessionId, flushed: false, pendingEvents: this.transport?.pending ?? 0, completed: false });
|
|
2630
|
-
}
|
|
2631
|
-
async onFatal(err) {
|
|
2632
|
-
if (this.status === "error" || this.status === "stopped") return;
|
|
2633
|
-
await this.teardown();
|
|
2634
|
-
this.sessions.clearPersisted();
|
|
2635
|
-
this.sessions.reset();
|
|
2636
|
-
this.fail(err);
|
|
2637
|
-
}
|
|
2638
|
-
fail(e) {
|
|
2639
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
2640
|
-
this.lastError = { code: err.code ?? "INTERNAL", message: err.message };
|
|
2641
|
-
this.setStatus("error");
|
|
2642
|
-
this.bus.emit("error", err);
|
|
2550
|
+
clearRecording() {
|
|
2551
|
+
this.events = [];
|
|
2552
|
+
this.actionLog = [];
|
|
2553
|
+
this.contextLog = [];
|
|
2554
|
+
this.recent = [];
|
|
2555
|
+
this.sequence = 0;
|
|
2556
|
+
this.pageId = uid("page_");
|
|
2557
|
+
this.payload = void 0;
|
|
2558
|
+
this.submitted = false;
|
|
2559
|
+
this.response = void 0;
|
|
2560
|
+
this.lastError = void 0;
|
|
2561
|
+
}
|
|
2562
|
+
assertAlive() {
|
|
2563
|
+
if (this.destroyed) throw new InvalidStateError("SDK destru\xEDdo");
|
|
2643
2564
|
}
|
|
2644
2565
|
setStatus(to) {
|
|
2645
2566
|
const from = this.status;
|
|
2646
2567
|
if (from === to) return;
|
|
2647
2568
|
this.status = to;
|
|
2569
|
+
if (this.store && (to === "recording" || to === "paused" || to === "stopped")) this.save(this.store.setStatus(to));
|
|
2648
2570
|
this.bus.emit("statusChange", { from, to });
|
|
2649
2571
|
}
|
|
2650
|
-
debug(...args) {
|
|
2651
|
-
if (this.config.debug) console.debug("[offscript]", ...args);
|
|
2652
|
-
}
|
|
2653
2572
|
};
|
|
2654
2573
|
Offscript.version = SDK_VERSION;
|
|
2655
2574
|
|
|
2656
|
-
export {
|
|
2657
|
-
//# sourceMappingURL=chunk-
|
|
2658
|
-
//# sourceMappingURL=chunk-
|
|
2575
|
+
export { ConfigError, DEFAULT_PRIVACY, DEFAULT_RECORDING, DataSanitizer, InvalidStateError, Offscript, OffscriptError };
|
|
2576
|
+
//# sourceMappingURL=chunk-XBXFGCUT.js.map
|
|
2577
|
+
//# sourceMappingURL=chunk-XBXFGCUT.js.map
|