@offscript/sdk 0.1.2 → 0.1.6
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 +18 -115
- 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-IBEPWI7S.js → chunk-5ZBR6FI5.js} +522 -1231
- package/dist/chunk-5ZBR6FI5.js.map +1 -0
- package/dist/{chunk-WUFW7TEL.js → chunk-KHKIWCLM.js} +3 -5
- package/dist/chunk-KHKIWCLM.js.map +1 -0
- package/dist/{chunk-KQVK5EJR.cjs → chunk-MZXF2LF2.cjs} +3 -5
- package/dist/chunk-MZXF2LF2.cjs.map +1 -0
- package/dist/{chunk-I6JPZAQA.cjs → chunk-PCJRHW66.cjs} +525 -1239
- package/dist/chunk-PCJRHW66.cjs.map +1 -0
- package/dist/{chunk-HECPMGDC.js → chunk-R2PPPR4K.js} +5 -5
- package/dist/chunk-R2PPPR4K.js.map +1 -0
- package/dist/chunk-RAUSXUGS.cjs +20 -0
- package/dist/chunk-RAUSXUGS.cjs.map +1 -0
- package/dist/core/LifecycleManager.d.ts +1 -0
- package/dist/core/OffscriptSDK.d.ts +41 -70
- 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/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 +9 -45
- package/dist/types/events.d.ts +2 -23
- package/dist/types/recording.d.ts +20 -0
- package/dist/ui/RecordingControls.d.ts +6 -4
- 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-HECPMGDC.js.map +0 -1
- package/dist/chunk-I6JPZAQA.cjs.map +0 -1
- package/dist/chunk-IBEPWI7S.js.map +0 -1
- package/dist/chunk-KQVK5EJR.cjs.map +0 -1
- package/dist/chunk-TBPD2LMI.cjs +0 -20
- package/dist/chunk-TBPD2LMI.cjs.map +0 -1
- package/dist/chunk-WUFW7TEL.js.map +0 -1
- package/dist/core/ApiClient.d.ts +0 -24
- package/dist/core/SessionManager.d.ts +0 -45
- 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-KHKIWCLM.js';
|
|
2
2
|
|
|
3
3
|
// src/types/config.ts
|
|
4
4
|
var DEFAULT_RECORDING = {
|
|
@@ -32,22 +32,7 @@ var DEFAULT_PRIVACY = {
|
|
|
32
32
|
redactKeys: [],
|
|
33
33
|
captureSensitiveValues: false,
|
|
34
34
|
maxBodyBytes: 16 * 1024,
|
|
35
|
-
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i]
|
|
36
|
-
localRetentionMs: 24 * 36e5
|
|
37
|
-
};
|
|
38
|
-
var DEFAULT_TRANSPORT = {
|
|
39
|
-
batchMaxEvents: 200,
|
|
40
|
-
batchMaxBytes: 512 * 1024,
|
|
41
|
-
flushIntervalMs: 3e3,
|
|
42
|
-
maxMemoryEvents: 5e3,
|
|
43
|
-
maxBufferBytes: 64 * 1024 * 1024,
|
|
44
|
-
maxRetries: 6,
|
|
45
|
-
retryBaseMs: 500,
|
|
46
|
-
retryMaxMs: 3e4,
|
|
47
|
-
compress: false,
|
|
48
|
-
keepaliveMaxBytes: 60 * 1024,
|
|
49
|
-
persist: true,
|
|
50
|
-
requestTimeoutMs: 15e3
|
|
35
|
+
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i]
|
|
51
36
|
};
|
|
52
37
|
|
|
53
38
|
// src/utils/misc.ts
|
|
@@ -86,13 +71,6 @@ function debounce(fn, ms) {
|
|
|
86
71
|
};
|
|
87
72
|
return d;
|
|
88
73
|
}
|
|
89
|
-
function approxSize(value) {
|
|
90
|
-
try {
|
|
91
|
-
return JSON.stringify(value)?.length ?? 0;
|
|
92
|
-
} catch {
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
74
|
function deepMerge(base, patch) {
|
|
97
75
|
const out = { ...base };
|
|
98
76
|
if (!patch) return out;
|
|
@@ -307,37 +285,12 @@ var ConfigError = class extends OffscriptError {
|
|
|
307
285
|
this.name = "ConfigError";
|
|
308
286
|
}
|
|
309
287
|
};
|
|
310
|
-
var AuthError = class extends OffscriptError {
|
|
311
|
-
constructor(message, status) {
|
|
312
|
-
super("AUTH_FAILED", message, { status });
|
|
313
|
-
this.name = "AuthError";
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
var SessionExpiredError = class extends OffscriptError {
|
|
317
|
-
constructor(message = "Sess\xE3o expirada") {
|
|
318
|
-
super("SESSION_EXPIRED", message, { status: 410 });
|
|
319
|
-
this.name = "SessionExpiredError";
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
var SessionDisabledError = class extends OffscriptError {
|
|
323
|
-
constructor(message = "Grava\xE7\xE3o desativada pelo backend") {
|
|
324
|
-
super("SESSION_DISABLED", message);
|
|
325
|
-
this.name = "SessionDisabledError";
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
288
|
var InvalidStateError = class extends OffscriptError {
|
|
329
289
|
constructor(message) {
|
|
330
290
|
super("INVALID_STATE", message);
|
|
331
291
|
this.name = "InvalidStateError";
|
|
332
292
|
}
|
|
333
293
|
};
|
|
334
|
-
var TransportError = class extends OffscriptError {
|
|
335
|
-
constructor(message, opts) {
|
|
336
|
-
super("TRANSPORT_FAILED", message, opts);
|
|
337
|
-
this.name = "TransportError";
|
|
338
|
-
this.retryable = opts.retryable;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
294
|
var Emitter = class {
|
|
342
295
|
constructor() {
|
|
343
296
|
this.handlers = /* @__PURE__ */ new Map();
|
|
@@ -364,591 +317,6 @@ var Emitter = class {
|
|
|
364
317
|
}
|
|
365
318
|
};
|
|
366
319
|
|
|
367
|
-
// src/core/ApiClient.ts
|
|
368
|
-
var ORIGINAL_FETCH = "__offscriptOriginal";
|
|
369
|
-
function nativeFetch() {
|
|
370
|
-
const f = globalThis.fetch;
|
|
371
|
-
if (!f) throw new TransportError("fetch indispon\xEDvel", { retryable: false });
|
|
372
|
-
return (f[ORIGINAL_FETCH] ?? f).bind(globalThis);
|
|
373
|
-
}
|
|
374
|
-
var ApiClient = class {
|
|
375
|
-
constructor(apiUrl, getToken, timeoutMs) {
|
|
376
|
-
this.apiUrl = apiUrl;
|
|
377
|
-
this.getToken = getToken;
|
|
378
|
-
this.timeoutMs = timeoutMs;
|
|
379
|
-
}
|
|
380
|
-
get baseUrl() {
|
|
381
|
-
return this.apiUrl;
|
|
382
|
-
}
|
|
383
|
-
createSession(body) {
|
|
384
|
-
return this.json("POST", "/sdk/sessions", body);
|
|
385
|
-
}
|
|
386
|
-
heartbeat(sessionId, body) {
|
|
387
|
-
return this.json("POST", `/sdk/sessions/${encodeURIComponent(sessionId)}/heartbeat`, body);
|
|
388
|
-
}
|
|
389
|
-
complete(sessionId, body) {
|
|
390
|
-
return this.json("POST", `/sdk/sessions/${encodeURIComponent(sessionId)}/complete`, body);
|
|
391
|
-
}
|
|
392
|
-
status(sessionId) {
|
|
393
|
-
return this.json("GET", `/sdk/sessions/${encodeURIComponent(sessionId)}/status`);
|
|
394
|
-
}
|
|
395
|
-
async sendEvents(batch, opts = {}) {
|
|
396
|
-
const path = `/sdk/sessions/${encodeURIComponent(batch.sessionId)}/events`;
|
|
397
|
-
return this.json("POST", path, batch, { "Idempotency-Key": batch.batchId }, opts);
|
|
398
|
-
}
|
|
399
|
-
async json(method, path, body, extraHeaders = {}, opts = {}) {
|
|
400
|
-
const headers = {
|
|
401
|
-
Accept: "application/json",
|
|
402
|
-
Authorization: `Bearer ${this.getToken()}`,
|
|
403
|
-
"X-Offscript-Sdk": SDK_VERSION,
|
|
404
|
-
...extraHeaders
|
|
405
|
-
};
|
|
406
|
-
let payload;
|
|
407
|
-
if (body !== void 0) {
|
|
408
|
-
headers["Content-Type"] = "application/json";
|
|
409
|
-
const text = JSON.stringify(body);
|
|
410
|
-
payload = text;
|
|
411
|
-
if (opts.compress && !opts.keepalive && text.length > 1024) {
|
|
412
|
-
const gz = await gzip(text);
|
|
413
|
-
if (gz) {
|
|
414
|
-
payload = gz;
|
|
415
|
-
headers["Content-Encoding"] = "gzip";
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const ctrl = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
420
|
-
const timer = ctrl && !opts.keepalive ? setTimeout(() => ctrl.abort(), this.timeoutMs) : void 0;
|
|
421
|
-
let res;
|
|
422
|
-
try {
|
|
423
|
-
res = await nativeFetch()(this.apiUrl + path, {
|
|
424
|
-
method,
|
|
425
|
-
headers,
|
|
426
|
-
body: payload,
|
|
427
|
-
keepalive: opts.keepalive,
|
|
428
|
-
credentials: "omit",
|
|
429
|
-
mode: "cors",
|
|
430
|
-
signal: ctrl?.signal
|
|
431
|
-
});
|
|
432
|
-
} catch (e) {
|
|
433
|
-
throw new TransportError(`Falha de rede: ${e?.message ?? e}`, { retryable: true, cause: e });
|
|
434
|
-
} finally {
|
|
435
|
-
if (timer) clearTimeout(timer);
|
|
436
|
-
}
|
|
437
|
-
if (res.ok) {
|
|
438
|
-
const text = await res.text();
|
|
439
|
-
return text ? JSON.parse(text) : {};
|
|
440
|
-
}
|
|
441
|
-
const detail = await res.text().catch(() => "");
|
|
442
|
-
const msg = `HTTP ${res.status} ${method} ${path}${detail ? `: ${detail.slice(0, 200)}` : ""}`;
|
|
443
|
-
if (res.status === 401 || res.status === 403) throw new AuthError(msg, res.status);
|
|
444
|
-
if (res.status === 410) throw new SessionExpiredError(msg);
|
|
445
|
-
const retryAfter = Number(res.headers.get("retry-after"));
|
|
446
|
-
const err = new TransportError(msg, {
|
|
447
|
-
status: res.status,
|
|
448
|
-
retryable: res.status === 408 || res.status === 429 || res.status >= 500
|
|
449
|
-
});
|
|
450
|
-
if (retryAfter > 0) err.retryAfterMs = retryAfter * 1e3;
|
|
451
|
-
throw err;
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
async function gzip(text) {
|
|
455
|
-
try {
|
|
456
|
-
const CS = globalThis.CompressionStream;
|
|
457
|
-
if (!CS) return null;
|
|
458
|
-
const body = new Response(text).body;
|
|
459
|
-
if (!body) return null;
|
|
460
|
-
return new Uint8Array(await new Response(body.pipeThrough(new CS("gzip"))).arrayBuffer());
|
|
461
|
-
} catch {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// src/transport/EventQueue.ts
|
|
467
|
-
var EventQueue = class {
|
|
468
|
-
constructor(maxEvents, maxBytes) {
|
|
469
|
-
this.maxEvents = maxEvents;
|
|
470
|
-
this.maxBytes = maxBytes;
|
|
471
|
-
this.staged = [];
|
|
472
|
-
this.pendingCount = 0;
|
|
473
|
-
this.pendingBytes = 0;
|
|
474
|
-
this.dropped = 0;
|
|
475
|
-
}
|
|
476
|
-
canAccept(priority) {
|
|
477
|
-
const factor = priority === "critical" ? 2 : priority === "normal" ? 1 : 0.5;
|
|
478
|
-
return this.pendingCount < this.maxEvents * factor && this.pendingBytes < this.maxBytes * factor;
|
|
479
|
-
}
|
|
480
|
-
noteDropped() {
|
|
481
|
-
this.dropped++;
|
|
482
|
-
}
|
|
483
|
-
push(e, size) {
|
|
484
|
-
this.staged.push(e);
|
|
485
|
-
this.pendingCount++;
|
|
486
|
-
this.pendingBytes += size;
|
|
487
|
-
}
|
|
488
|
-
/** Devolve eventos ao staging sem recontar. */
|
|
489
|
-
restore(events) {
|
|
490
|
-
this.staged = [...events, ...this.staged];
|
|
491
|
-
}
|
|
492
|
-
takeStaged() {
|
|
493
|
-
const s = this.staged;
|
|
494
|
-
this.staged = [];
|
|
495
|
-
return s;
|
|
496
|
-
}
|
|
497
|
-
peekStaged() {
|
|
498
|
-
return this.staged;
|
|
499
|
-
}
|
|
500
|
-
get stagedCount() {
|
|
501
|
-
return this.staged.length;
|
|
502
|
-
}
|
|
503
|
-
acked(count, bytes) {
|
|
504
|
-
this.pendingCount = Math.max(0, this.pendingCount - count);
|
|
505
|
-
this.pendingBytes = Math.max(0, this.pendingBytes - bytes);
|
|
506
|
-
}
|
|
507
|
-
reset(count, bytes) {
|
|
508
|
-
this.pendingCount = count;
|
|
509
|
-
this.pendingBytes = bytes;
|
|
510
|
-
}
|
|
511
|
-
get pending() {
|
|
512
|
-
return this.pendingCount;
|
|
513
|
-
}
|
|
514
|
-
get bytes() {
|
|
515
|
-
return this.pendingBytes;
|
|
516
|
-
}
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
// src/transport/RetryManager.ts
|
|
520
|
-
var RetryManager = class {
|
|
521
|
-
constructor(baseMs, maxMs, random = Math.random) {
|
|
522
|
-
this.baseMs = baseMs;
|
|
523
|
-
this.maxMs = maxMs;
|
|
524
|
-
this.random = random;
|
|
525
|
-
}
|
|
526
|
-
delay(attempt) {
|
|
527
|
-
const cap = Math.min(this.maxMs, this.baseMs * 2 ** Math.max(0, attempt));
|
|
528
|
-
return Math.round(this.random() * cap);
|
|
529
|
-
}
|
|
530
|
-
sleep(ms) {
|
|
531
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
// src/transport/EventTransport.ts
|
|
536
|
-
var EventTransport = class extends Emitter {
|
|
537
|
-
constructor(cfg, api, buffer) {
|
|
538
|
-
super();
|
|
539
|
-
this.cfg = cfg;
|
|
540
|
-
this.api = api;
|
|
541
|
-
this.buffer = buffer;
|
|
542
|
-
this.identity = null;
|
|
543
|
-
this.running = null;
|
|
544
|
-
this.again = false;
|
|
545
|
-
this.failures = 0;
|
|
546
|
-
this.stopped = true;
|
|
547
|
-
this.fatal = false;
|
|
548
|
-
this.online = true;
|
|
549
|
-
this.lastDroppedReported = 0;
|
|
550
|
-
this.queue = new EventQueue(cfg.maxMemoryEvents, cfg.maxBufferBytes);
|
|
551
|
-
this.retry = new RetryManager(cfg.retryBaseMs, cfg.retryMaxMs);
|
|
552
|
-
}
|
|
553
|
-
get bufferKind() {
|
|
554
|
-
return this.buffer.kind;
|
|
555
|
-
}
|
|
556
|
-
/** Inicia o transporte para uma sessão/aba e recupera o que ficou pendente no buffer. */
|
|
557
|
-
async start(identity) {
|
|
558
|
-
this.identity = identity;
|
|
559
|
-
this.stopped = false;
|
|
560
|
-
this.fatal = false;
|
|
561
|
-
this.failures = 0;
|
|
562
|
-
const st = await this.buffer.stats(identity.sessionId).catch(() => ({ count: 0, bytes: 0 }));
|
|
563
|
-
this.queue.reset(st.count, st.bytes);
|
|
564
|
-
this.scheduleFlush();
|
|
565
|
-
}
|
|
566
|
-
setOnline(online) {
|
|
567
|
-
this.online = online;
|
|
568
|
-
if (online && !this.stopped) void this.flush();
|
|
569
|
-
}
|
|
570
|
-
canAccept(priority) {
|
|
571
|
-
return !this.stopped && !this.fatal && this.queue.canAccept(priority);
|
|
572
|
-
}
|
|
573
|
-
noteDropped() {
|
|
574
|
-
this.queue.noteDropped();
|
|
575
|
-
if (this.queue.dropped - this.lastDroppedReported >= 1) {
|
|
576
|
-
this.lastDroppedReported = this.queue.dropped;
|
|
577
|
-
this.emit("overflow", { dropped: this.queue.dropped });
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
get pending() {
|
|
581
|
-
return this.queue.pending;
|
|
582
|
-
}
|
|
583
|
-
enqueue(event) {
|
|
584
|
-
if (this.stopped) return;
|
|
585
|
-
this.queue.push(event, approxSize(event));
|
|
586
|
-
if (this.queue.stagedCount >= 50) void this.commit();
|
|
587
|
-
else if (!this.commitTimer) this.commitTimer = setTimeout(() => void this.commit(), 100);
|
|
588
|
-
}
|
|
589
|
-
async commit() {
|
|
590
|
-
if (this.commitTimer) clearTimeout(this.commitTimer);
|
|
591
|
-
this.commitTimer = void 0;
|
|
592
|
-
const staged = this.queue.takeStaged();
|
|
593
|
-
if (!staged.length) return;
|
|
594
|
-
try {
|
|
595
|
-
await this.buffer.put(staged);
|
|
596
|
-
} catch {
|
|
597
|
-
this.queue.restore(staged);
|
|
598
|
-
throw new TransportError("Falha ao persistir eventos localmente", { retryable: true });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
scheduleFlush() {
|
|
602
|
-
if (this.flushTimer || this.stopped) return;
|
|
603
|
-
this.flushTimer = setTimeout(() => {
|
|
604
|
-
this.flushTimer = void 0;
|
|
605
|
-
void this.flush().finally(() => this.scheduleFlush());
|
|
606
|
-
}, this.cfg.flushIntervalMs);
|
|
607
|
-
}
|
|
608
|
-
/** Flush de melhor esforço. Retorna true se tudo foi confirmado pelo backend. */
|
|
609
|
-
flush(opts = { inlineRetries: 0 }) {
|
|
610
|
-
if (this.running) {
|
|
611
|
-
this.again = true;
|
|
612
|
-
return this.running;
|
|
613
|
-
}
|
|
614
|
-
this.running = (async () => {
|
|
615
|
-
try {
|
|
616
|
-
let ok = await this.drain(opts);
|
|
617
|
-
while (this.again && ok) {
|
|
618
|
-
this.again = false;
|
|
619
|
-
ok = await this.drain(opts);
|
|
620
|
-
}
|
|
621
|
-
return ok;
|
|
622
|
-
} finally {
|
|
623
|
-
this.running = null;
|
|
624
|
-
this.again = false;
|
|
625
|
-
}
|
|
626
|
-
})();
|
|
627
|
-
return this.running;
|
|
628
|
-
}
|
|
629
|
-
/** Flush final com retries inline e prazo máximo. */
|
|
630
|
-
async flushFinal(maxWaitMs = 1e4) {
|
|
631
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
632
|
-
this.retryTimer = void 0;
|
|
633
|
-
const running = this.running;
|
|
634
|
-
if (running) await running.catch(() => false);
|
|
635
|
-
const ok = await this.flush({ inlineRetries: this.cfg.maxRetries, deadline: Date.now() + maxWaitMs });
|
|
636
|
-
return { ok, pending: this.queue.pending };
|
|
637
|
-
}
|
|
638
|
-
async drain(opts) {
|
|
639
|
-
const id = this.identity;
|
|
640
|
-
if (!id || this.fatal) return false;
|
|
641
|
-
for (; ; ) {
|
|
642
|
-
try {
|
|
643
|
-
await this.commit();
|
|
644
|
-
const { items } = await this.buffer.peek(id.sessionId, id.tabId, this.cfg.batchMaxEvents, this.cfg.batchMaxBytes);
|
|
645
|
-
if (!items.length) {
|
|
646
|
-
this.failures = 0;
|
|
647
|
-
return true;
|
|
648
|
-
}
|
|
649
|
-
const events = items.map((i) => i.event);
|
|
650
|
-
await this.sendWithRetry(events, opts);
|
|
651
|
-
await this.buffer.remove(items.map((i) => i.key));
|
|
652
|
-
this.queue.acked(items.length, items.reduce((n, i) => n + i.size, 0));
|
|
653
|
-
this.failures = 0;
|
|
654
|
-
} catch (error) {
|
|
655
|
-
return this.onFailure(error);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
onFailure(error) {
|
|
660
|
-
if (error instanceof AuthError || error instanceof SessionExpiredError) {
|
|
661
|
-
this.fatal = true;
|
|
662
|
-
this.emit("fatal", { error });
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
665
|
-
this.failures++;
|
|
666
|
-
const retryable = !(error instanceof TransportError) || error.retryable;
|
|
667
|
-
const willRetryInMs = retryable ? this.retry.delay(this.failures) : void 0;
|
|
668
|
-
this.emit("failed", { error, willRetryInMs });
|
|
669
|
-
if (retryable && !this.stopped) {
|
|
670
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
671
|
-
this.retryTimer = setTimeout(() => {
|
|
672
|
-
this.retryTimer = void 0;
|
|
673
|
-
void this.flush();
|
|
674
|
-
}, willRetryInMs);
|
|
675
|
-
}
|
|
676
|
-
return false;
|
|
677
|
-
}
|
|
678
|
-
async sendWithRetry(events, opts) {
|
|
679
|
-
const batch = this.buildBatch(events);
|
|
680
|
-
for (let attempt = 0; ; attempt++) {
|
|
681
|
-
try {
|
|
682
|
-
if (!this.online) throw new TransportError("Offline", { retryable: true });
|
|
683
|
-
const ack = await this.api.sendEvents(batch, { compress: this.cfg.compress });
|
|
684
|
-
this.emit("flushed", { events: events.length, batchId: batch.batchId, duplicate: !!ack.duplicate });
|
|
685
|
-
return;
|
|
686
|
-
} catch (e) {
|
|
687
|
-
if (e instanceof TransportError && e.status === 413 && events.length > 1) {
|
|
688
|
-
const mid = Math.ceil(events.length / 2);
|
|
689
|
-
await this.sendWithRetry(events.slice(0, mid), opts);
|
|
690
|
-
await this.sendWithRetry(events.slice(mid), opts);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
const retryable = !(e instanceof TransportError) || e.retryable;
|
|
694
|
-
if (!retryable || attempt >= opts.inlineRetries) throw e;
|
|
695
|
-
const wait = Math.max(this.retry.delay(attempt), e.retryAfterMs ?? 0);
|
|
696
|
-
if (opts.deadline && Date.now() + wait > opts.deadline) throw e;
|
|
697
|
-
await this.retry.sleep(wait);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
buildBatch(events) {
|
|
702
|
-
const id = this.identity;
|
|
703
|
-
const first = events[0].sequence;
|
|
704
|
-
const last = events[events.length - 1].sequence;
|
|
705
|
-
return {
|
|
706
|
-
schemaVersion: SCHEMA_VERSION,
|
|
707
|
-
batchId: `${id.sessionId}.${id.tabId}.${first}-${last}.${events.length}`,
|
|
708
|
-
sessionId: id.sessionId,
|
|
709
|
-
executionId: id.executionId,
|
|
710
|
-
tabId: id.tabId,
|
|
711
|
-
sequenceStart: first,
|
|
712
|
-
sequenceEnd: last,
|
|
713
|
-
createdAt: Date.now(),
|
|
714
|
-
sdkVersion: SDK_VERSION,
|
|
715
|
-
events
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Chamado em pagehide/beforeunload. Persiste o que está em staging (best effort, assíncrono) e
|
|
720
|
-
* tenta enviar o final da fila com fetch keepalive (limite ~64KB). Sem ACK: os eventos continuam
|
|
721
|
-
* no IndexedDB e serão reenviados (de forma idempotente) na próxima página.
|
|
722
|
-
* Não há garantia de entrega se o navegador for encerrado abruptamente.
|
|
723
|
-
*/
|
|
724
|
-
flushOnUnload() {
|
|
725
|
-
const id = this.identity;
|
|
726
|
-
if (!id || this.stopped || this.fatal) return;
|
|
727
|
-
const staged = this.queue.takeStaged();
|
|
728
|
-
if (!staged.length) return;
|
|
729
|
-
safe(() => void this.buffer.put(staged).catch(() => void 0));
|
|
730
|
-
const events = [];
|
|
731
|
-
let bytes = 0;
|
|
732
|
-
for (const e of staged) {
|
|
733
|
-
const s = approxSize(e);
|
|
734
|
-
if (bytes + s > this.cfg.keepaliveMaxBytes) break;
|
|
735
|
-
events.push(e);
|
|
736
|
-
bytes += s;
|
|
737
|
-
}
|
|
738
|
-
if (!events.length) return;
|
|
739
|
-
safe(() => void this.api.sendEvents(this.buildBatch(events), { keepalive: true }).catch(() => void 0));
|
|
740
|
-
}
|
|
741
|
-
/** Para timers; eventos não confirmados permanecem no buffer. */
|
|
742
|
-
stop() {
|
|
743
|
-
this.stopped = true;
|
|
744
|
-
for (const t of [this.commitTimer, this.flushTimer, this.retryTimer]) if (t) clearTimeout(t);
|
|
745
|
-
this.commitTimer = this.flushTimer = this.retryTimer = void 0;
|
|
746
|
-
}
|
|
747
|
-
async clearSession(sessionId) {
|
|
748
|
-
await this.buffer.clearSession(sessionId).catch(() => void 0);
|
|
749
|
-
this.queue.reset(0, 0);
|
|
750
|
-
}
|
|
751
|
-
async purgeExpired(retentionMs) {
|
|
752
|
-
await this.buffer.purgeOlderThan(Date.now() - retentionMs).catch(() => void 0);
|
|
753
|
-
}
|
|
754
|
-
close() {
|
|
755
|
-
this.stop();
|
|
756
|
-
this.buffer.close();
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
// src/transport/PersistentBuffer.ts
|
|
761
|
-
var eventKey = (sessionId, tabId, seq) => `${sessionId}|${tabId}|${String(seq).padStart(12, "0")}`;
|
|
762
|
-
var toStored = (e) => ({
|
|
763
|
-
key: eventKey(e.sessionId, e.tabId, e.sequence),
|
|
764
|
-
sessionId: e.sessionId,
|
|
765
|
-
tabId: e.tabId,
|
|
766
|
-
sequence: e.sequence,
|
|
767
|
-
storedAt: Date.now(),
|
|
768
|
-
size: approxSize(e),
|
|
769
|
-
event: e
|
|
770
|
-
});
|
|
771
|
-
var MemoryBuffer = class {
|
|
772
|
-
constructor() {
|
|
773
|
-
this.kind = "memory";
|
|
774
|
-
this.map = /* @__PURE__ */ new Map();
|
|
775
|
-
}
|
|
776
|
-
async put(events) {
|
|
777
|
-
for (const e of events) {
|
|
778
|
-
const s = toStored(e);
|
|
779
|
-
this.map.set(s.key, s);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
async peek(sessionId, tabId, maxEvents, maxBytes) {
|
|
783
|
-
const prefix = `${sessionId}|${tabId}|`;
|
|
784
|
-
const keys = Array.from(this.map.keys()).filter((k) => k.startsWith(prefix)).sort();
|
|
785
|
-
const items = [];
|
|
786
|
-
let bytes = 0;
|
|
787
|
-
for (const k of keys) {
|
|
788
|
-
const s = this.map.get(k);
|
|
789
|
-
if (items.length >= maxEvents || items.length > 0 && bytes + s.size > maxBytes) break;
|
|
790
|
-
items.push(s);
|
|
791
|
-
bytes += s.size;
|
|
792
|
-
}
|
|
793
|
-
return { items, bytes };
|
|
794
|
-
}
|
|
795
|
-
async remove(keys) {
|
|
796
|
-
for (const k of keys) this.map.delete(k);
|
|
797
|
-
}
|
|
798
|
-
async stats(sessionId) {
|
|
799
|
-
let count = 0;
|
|
800
|
-
let bytes = 0;
|
|
801
|
-
for (const s of this.map.values()) {
|
|
802
|
-
if (sessionId && s.sessionId !== sessionId) continue;
|
|
803
|
-
count++;
|
|
804
|
-
bytes += s.size;
|
|
805
|
-
}
|
|
806
|
-
return { count, bytes };
|
|
807
|
-
}
|
|
808
|
-
async purgeOlderThan(ts) {
|
|
809
|
-
let n = 0;
|
|
810
|
-
for (const [k, s] of this.map) if (s.storedAt < ts) this.map.delete(k), n++;
|
|
811
|
-
return n;
|
|
812
|
-
}
|
|
813
|
-
async clearSession(sessionId) {
|
|
814
|
-
for (const [k, s] of this.map) if (s.sessionId === sessionId) this.map.delete(k);
|
|
815
|
-
}
|
|
816
|
-
close() {
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
var STORE = "events";
|
|
820
|
-
function req(r) {
|
|
821
|
-
return new Promise((resolve, reject) => {
|
|
822
|
-
r.onsuccess = () => resolve(r.result);
|
|
823
|
-
r.onerror = () => reject(r.error);
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
function done(tx) {
|
|
827
|
-
return new Promise((resolve, reject) => {
|
|
828
|
-
tx.oncomplete = () => resolve();
|
|
829
|
-
tx.onerror = () => reject(tx.error);
|
|
830
|
-
tx.onabort = () => reject(tx.error);
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
var IndexedDBBuffer = class _IndexedDBBuffer {
|
|
834
|
-
constructor(db) {
|
|
835
|
-
this.db = db;
|
|
836
|
-
this.kind = "indexeddb";
|
|
837
|
-
}
|
|
838
|
-
static async open(name) {
|
|
839
|
-
if (typeof indexedDB === "undefined") throw new Error("IndexedDB indispon\xEDvel");
|
|
840
|
-
const db = await new Promise((resolve, reject) => {
|
|
841
|
-
const r = indexedDB.open(name, 1);
|
|
842
|
-
r.onupgradeneeded = () => {
|
|
843
|
-
const store = r.result.createObjectStore(STORE, { keyPath: "key" });
|
|
844
|
-
store.createIndex("bySession", "sessionId");
|
|
845
|
-
store.createIndex("byStoredAt", "storedAt");
|
|
846
|
-
};
|
|
847
|
-
r.onsuccess = () => resolve(r.result);
|
|
848
|
-
r.onerror = () => reject(r.error);
|
|
849
|
-
r.onblocked = () => reject(new Error("IndexedDB bloqueado"));
|
|
850
|
-
});
|
|
851
|
-
return new _IndexedDBBuffer(db);
|
|
852
|
-
}
|
|
853
|
-
async put(events) {
|
|
854
|
-
if (!events.length) return;
|
|
855
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
856
|
-
const store = tx.objectStore(STORE);
|
|
857
|
-
for (const e of events) store.put(toStored(e));
|
|
858
|
-
await done(tx);
|
|
859
|
-
}
|
|
860
|
-
async peek(sessionId, tabId, maxEvents, maxBytes) {
|
|
861
|
-
const prefix = `${sessionId}|${tabId}|`;
|
|
862
|
-
const range = IDBKeyRange.bound(prefix, prefix + "\uFFFF");
|
|
863
|
-
const tx = this.db.transaction(STORE, "readonly");
|
|
864
|
-
const items = [];
|
|
865
|
-
let bytes = 0;
|
|
866
|
-
await new Promise((resolve, reject) => {
|
|
867
|
-
const cur = tx.objectStore(STORE).openCursor(range);
|
|
868
|
-
cur.onsuccess = () => {
|
|
869
|
-
const c = cur.result;
|
|
870
|
-
if (!c) return resolve();
|
|
871
|
-
const s = c.value;
|
|
872
|
-
if (items.length >= maxEvents || items.length > 0 && bytes + s.size > maxBytes) return resolve();
|
|
873
|
-
items.push(s);
|
|
874
|
-
bytes += s.size;
|
|
875
|
-
c.continue();
|
|
876
|
-
};
|
|
877
|
-
cur.onerror = () => reject(cur.error);
|
|
878
|
-
});
|
|
879
|
-
return { items, bytes };
|
|
880
|
-
}
|
|
881
|
-
async remove(keys) {
|
|
882
|
-
if (!keys.length) return;
|
|
883
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
884
|
-
const store = tx.objectStore(STORE);
|
|
885
|
-
for (const k of keys) store.delete(k);
|
|
886
|
-
await done(tx);
|
|
887
|
-
}
|
|
888
|
-
async stats(sessionId) {
|
|
889
|
-
const tx = this.db.transaction(STORE, "readonly");
|
|
890
|
-
const store = tx.objectStore(STORE);
|
|
891
|
-
if (!sessionId) return { count: await req(store.count()), bytes: 0 };
|
|
892
|
-
let count = 0;
|
|
893
|
-
let bytes = 0;
|
|
894
|
-
await new Promise((resolve, reject) => {
|
|
895
|
-
const cur = store.index("bySession").openCursor(IDBKeyRange.only(sessionId));
|
|
896
|
-
cur.onsuccess = () => {
|
|
897
|
-
const c = cur.result;
|
|
898
|
-
if (!c) return resolve();
|
|
899
|
-
count++;
|
|
900
|
-
bytes += c.value.size;
|
|
901
|
-
c.continue();
|
|
902
|
-
};
|
|
903
|
-
cur.onerror = () => reject(cur.error);
|
|
904
|
-
});
|
|
905
|
-
return { count, bytes };
|
|
906
|
-
}
|
|
907
|
-
async purgeOlderThan(ts) {
|
|
908
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
909
|
-
let n = 0;
|
|
910
|
-
await new Promise((resolve, reject) => {
|
|
911
|
-
const cur = tx.objectStore(STORE).index("byStoredAt").openCursor(IDBKeyRange.upperBound(ts, true));
|
|
912
|
-
cur.onsuccess = () => {
|
|
913
|
-
const c = cur.result;
|
|
914
|
-
if (!c) return resolve();
|
|
915
|
-
c.delete();
|
|
916
|
-
n++;
|
|
917
|
-
c.continue();
|
|
918
|
-
};
|
|
919
|
-
cur.onerror = () => reject(cur.error);
|
|
920
|
-
});
|
|
921
|
-
await done(tx);
|
|
922
|
-
return n;
|
|
923
|
-
}
|
|
924
|
-
async clearSession(sessionId) {
|
|
925
|
-
const tx = this.db.transaction(STORE, "readwrite");
|
|
926
|
-
await new Promise((resolve, reject) => {
|
|
927
|
-
const cur = tx.objectStore(STORE).index("bySession").openCursor(IDBKeyRange.only(sessionId));
|
|
928
|
-
cur.onsuccess = () => {
|
|
929
|
-
const c = cur.result;
|
|
930
|
-
if (!c) return resolve();
|
|
931
|
-
c.delete();
|
|
932
|
-
c.continue();
|
|
933
|
-
};
|
|
934
|
-
cur.onerror = () => reject(cur.error);
|
|
935
|
-
});
|
|
936
|
-
await done(tx);
|
|
937
|
-
}
|
|
938
|
-
close() {
|
|
939
|
-
safe(() => this.db.close());
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
async function openBuffer(name, persist) {
|
|
943
|
-
if (persist) {
|
|
944
|
-
try {
|
|
945
|
-
return await IndexedDBBuffer.open(name);
|
|
946
|
-
} catch {
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return new MemoryBuffer();
|
|
950
|
-
}
|
|
951
|
-
|
|
952
320
|
// src/recorders/types.ts
|
|
953
321
|
function guard(sink, name, fn) {
|
|
954
322
|
try {
|
|
@@ -1278,7 +646,7 @@ function parseBrowser(ua) {
|
|
|
1278
646
|
}
|
|
1279
647
|
return {};
|
|
1280
648
|
}
|
|
1281
|
-
function collectContext(
|
|
649
|
+
function collectContext(reason, captureSensitiveValues = false) {
|
|
1282
650
|
const nav = navigator;
|
|
1283
651
|
const ua = nav.userAgent ?? "";
|
|
1284
652
|
let browser = parseBrowser(ua);
|
|
@@ -1302,18 +670,13 @@ function collectContext(id, reason, appVersion, captureSensitiveValues = false)
|
|
|
1302
670
|
timezone: safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
1303
671
|
online: nav.onLine,
|
|
1304
672
|
visibility: document.visibilityState,
|
|
1305
|
-
platformId: id.platformId,
|
|
1306
|
-
testCaseId: id.testCaseId,
|
|
1307
|
-
executionId: id.executionId,
|
|
1308
673
|
sdkVersion: SDK_VERSION,
|
|
1309
|
-
appVersion,
|
|
1310
674
|
captureSensitiveValues
|
|
1311
675
|
};
|
|
1312
676
|
}
|
|
1313
677
|
var ContextRecorder = class {
|
|
1314
|
-
constructor(sink
|
|
678
|
+
constructor(sink) {
|
|
1315
679
|
this.sink = sink;
|
|
1316
|
-
this.identity = identity;
|
|
1317
680
|
this.name = "context";
|
|
1318
681
|
this.d = new Disposer();
|
|
1319
682
|
}
|
|
@@ -1326,7 +689,7 @@ var ContextRecorder = class {
|
|
|
1326
689
|
}
|
|
1327
690
|
record(reason) {
|
|
1328
691
|
guard(this.sink, this.name, () => {
|
|
1329
|
-
const data = collectContext(
|
|
692
|
+
const data = collectContext(reason, this.sink.config.privacy.captureSensitiveValues);
|
|
1330
693
|
data.url = this.sink.sanitizer.sanitizeUrl(data.url);
|
|
1331
694
|
if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
|
|
1332
695
|
this.sink.emit("context", data, { priority: "normal" });
|
|
@@ -1485,23 +848,28 @@ function uid(prefix = "") {
|
|
|
1485
848
|
}
|
|
1486
849
|
|
|
1487
850
|
// src/recorders/HTTPRecorder.ts
|
|
1488
|
-
var XHR_PATCHED = "__offscriptXhrOriginal";
|
|
1489
851
|
var HTTPRecorder = class {
|
|
1490
|
-
constructor(sink
|
|
852
|
+
constructor(sink) {
|
|
1491
853
|
this.sink = sink;
|
|
1492
|
-
this.ownApiUrl = ownApiUrl;
|
|
1493
854
|
this.name = "http";
|
|
1494
855
|
this.inflight = 0;
|
|
1495
856
|
this.active = false;
|
|
857
|
+
this.delivering = false;
|
|
858
|
+
this.pending = /* @__PURE__ */ new Set();
|
|
1496
859
|
this.restore = [];
|
|
1497
860
|
}
|
|
1498
861
|
start() {
|
|
1499
862
|
if (this.active) return;
|
|
1500
863
|
this.active = true;
|
|
864
|
+
this.delivering = true;
|
|
1501
865
|
this.patchFetch();
|
|
1502
866
|
this.patchXhr();
|
|
1503
867
|
}
|
|
1504
868
|
stop() {
|
|
869
|
+
this.delivering = false;
|
|
870
|
+
this.detach();
|
|
871
|
+
}
|
|
872
|
+
detach() {
|
|
1505
873
|
this.active = false;
|
|
1506
874
|
for (const r of this.restore.splice(0).reverse()) {
|
|
1507
875
|
try {
|
|
@@ -1510,8 +878,30 @@ var HTTPRecorder = class {
|
|
|
1510
878
|
}
|
|
1511
879
|
}
|
|
1512
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
|
+
}
|
|
1513
904
|
isIgnored(url) {
|
|
1514
|
-
if (this.ownApiUrl && url.startsWith(this.ownApiUrl)) return true;
|
|
1515
905
|
return this.sink.sanitizer.isIgnoredUrl(url);
|
|
1516
906
|
}
|
|
1517
907
|
// ---------------------------------------------------------------- fetch
|
|
@@ -1537,14 +927,13 @@ var HTTPRecorder = class {
|
|
|
1537
927
|
}
|
|
1538
928
|
if (ctx) {
|
|
1539
929
|
const c = ctx;
|
|
1540
|
-
p.then(
|
|
930
|
+
self.track(p.then(
|
|
1541
931
|
(res) => self.endFetch(c, res),
|
|
1542
932
|
(err) => self.failFetch(c, err)
|
|
1543
|
-
)
|
|
933
|
+
));
|
|
1544
934
|
}
|
|
1545
935
|
return p;
|
|
1546
936
|
};
|
|
1547
|
-
patched[ORIGINAL_FETCH] = orig[ORIGINAL_FETCH] ?? orig;
|
|
1548
937
|
globalThis.fetch = patched;
|
|
1549
938
|
this.restore.push(() => {
|
|
1550
939
|
if (globalThis.fetch === patched) globalThis.fetch = orig;
|
|
@@ -1592,13 +981,13 @@ var HTTPRecorder = class {
|
|
|
1592
981
|
base.responseBody = { kind: "omitted", reason: "body-used" };
|
|
1593
982
|
return void this.emit(base);
|
|
1594
983
|
}
|
|
1595
|
-
this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
984
|
+
this.track(this.readBounded(clone).then(({ text, size, truncated }) => {
|
|
1596
985
|
const b = this.sink.sanitizer.sanitizeTextBody(text, ct, size);
|
|
1597
986
|
b.truncated = truncated || b.truncated;
|
|
1598
987
|
base.responseBody = b;
|
|
1599
988
|
}).catch(() => {
|
|
1600
989
|
base.responseBody = { kind: "omitted", reason: "read-failed" };
|
|
1601
|
-
}).finally(() => guard(this.sink, this.name, () => this.emit(base)));
|
|
990
|
+
}).finally(() => guard(this.sink, this.name, () => this.emit(base))));
|
|
1602
991
|
});
|
|
1603
992
|
}
|
|
1604
993
|
async readBounded(res) {
|
|
@@ -1609,8 +998,8 @@ var HTTPRecorder = class {
|
|
|
1609
998
|
let size = 0;
|
|
1610
999
|
let truncated = false;
|
|
1611
1000
|
for (; ; ) {
|
|
1612
|
-
const { done
|
|
1613
|
-
if (
|
|
1001
|
+
const { done, value } = await reader.read();
|
|
1002
|
+
if (done) break;
|
|
1614
1003
|
size += value.byteLength;
|
|
1615
1004
|
if (text.length < max) text += dec.decode(value, { stream: true });
|
|
1616
1005
|
if (size > max * 4) {
|
|
@@ -1667,7 +1056,6 @@ var HTTPRecorder = class {
|
|
|
1667
1056
|
}
|
|
1668
1057
|
return origSend.call(this, body);
|
|
1669
1058
|
};
|
|
1670
|
-
open[XHR_PATCHED] = origOpen;
|
|
1671
1059
|
proto.open = open;
|
|
1672
1060
|
proto.setRequestHeader = setHeader;
|
|
1673
1061
|
proto.send = send;
|
|
@@ -1689,9 +1077,14 @@ var HTTPRecorder = class {
|
|
|
1689
1077
|
this.inflight++;
|
|
1690
1078
|
xhr.addEventListener("abort", () => m.aborted = true);
|
|
1691
1079
|
xhr.addEventListener("timeout", () => m.timedOut = true);
|
|
1080
|
+
let finishCapture;
|
|
1081
|
+
this.track(new Promise((resolve) => {
|
|
1082
|
+
finishCapture = resolve;
|
|
1083
|
+
}));
|
|
1692
1084
|
xhr.addEventListener("loadend", () => {
|
|
1693
1085
|
this.inflight = Math.max(0, this.inflight - 1);
|
|
1694
1086
|
guard(this.sink, this.name, () => this.endXhr(xhr, m, ctx));
|
|
1087
|
+
finishCapture();
|
|
1695
1088
|
});
|
|
1696
1089
|
}
|
|
1697
1090
|
endXhr(xhr, m, ctx) {
|
|
@@ -1735,6 +1128,7 @@ var HTTPRecorder = class {
|
|
|
1735
1128
|
};
|
|
1736
1129
|
}
|
|
1737
1130
|
emit(data) {
|
|
1131
|
+
if (!this.delivering) return;
|
|
1738
1132
|
this.sink.emit("http", data, { priority: "normal", at: data.startedAt });
|
|
1739
1133
|
}
|
|
1740
1134
|
describeBody(body, contentType) {
|
|
@@ -1984,11 +1378,12 @@ var LifecycleManager = class {
|
|
|
1984
1378
|
this.lastUrl = location.href;
|
|
1985
1379
|
const d = this.d;
|
|
1986
1380
|
d.listen(window, "pagehide", () => safe(() => this.h.onPageHide()), { capture: true });
|
|
1987
|
-
d.listen(window, "
|
|
1381
|
+
d.listen(window, "pageshow", (event) => {
|
|
1382
|
+
if (event.persisted) safe(() => this.h.onPageShow?.());
|
|
1383
|
+
});
|
|
1988
1384
|
d.listen(document, "visibilitychange", () => {
|
|
1989
1385
|
const hidden = document.visibilityState === "hidden";
|
|
1990
1386
|
safe(() => this.h.onVisibility(hidden));
|
|
1991
|
-
if (hidden) safe(() => this.h.onPageHide());
|
|
1992
1387
|
});
|
|
1993
1388
|
d.listen(window, "online", () => safe(() => this.h.onConnectivity(true)));
|
|
1994
1389
|
d.listen(window, "offline", () => safe(() => this.h.onConnectivity(false)));
|
|
@@ -2025,282 +1420,120 @@ var LifecycleManager = class {
|
|
|
2025
1420
|
}
|
|
2026
1421
|
};
|
|
2027
1422
|
|
|
2028
|
-
// src/core/
|
|
2029
|
-
var
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
this.
|
|
2033
|
-
this.
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
return
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
try {
|
|
2058
|
-
const s = JSON.parse(raw);
|
|
2059
|
-
if (s.version !== STATE_VERSION || s.platformId !== this.cfg.platformId || !s.sessionId || !s.sessionToken) return null;
|
|
2060
|
-
if (s.expiresAt && s.expiresAt < Date.now()) return null;
|
|
2061
|
-
const seq = Number(safe(() => sessionStorage.getItem(this.seqKey)) ?? 0);
|
|
2062
|
-
s.sequence = Math.max(s.sequence ?? 0, Number.isFinite(seq) ? seq : 0);
|
|
2063
|
-
return s;
|
|
2064
|
-
} catch {
|
|
2065
|
-
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);
|
|
2066
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
|
+
});
|
|
2067
1463
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
this.
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
testCaseId: this.info.testCaseId,
|
|
2076
|
-
platformId: this.cfg.platformId,
|
|
2077
|
-
sessionToken: this.token,
|
|
2078
|
-
status,
|
|
2079
|
-
sequence: this.seq,
|
|
2080
|
-
pageCount: this.state?.pageCount ?? 0,
|
|
2081
|
-
heartbeatIntervalMs: this.state?.heartbeatIntervalMs,
|
|
2082
|
-
expiresAt: this.state?.expiresAt,
|
|
2083
|
-
updatedAt: Date.now(),
|
|
2084
|
-
...extra
|
|
2085
|
-
};
|
|
2086
|
-
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
|
+
});
|
|
2087
1471
|
}
|
|
2088
1472
|
setStatus(status) {
|
|
2089
|
-
this.
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
safe(() => sessionStorage.removeItem(this.storageKey));
|
|
2093
|
-
safe(() => sessionStorage.removeItem(this.seqKey));
|
|
2094
|
-
this.state = null;
|
|
2095
|
-
}
|
|
2096
|
-
// ---- sequência ----------------------------------------------------------
|
|
2097
|
-
nextSequence() {
|
|
2098
|
-
this.seq++;
|
|
2099
|
-
safe(() => sessionStorage.setItem(this.seqKey, String(this.seq)));
|
|
2100
|
-
return this.seq;
|
|
2101
|
-
}
|
|
2102
|
-
get lastSequence() {
|
|
2103
|
-
return this.seq;
|
|
2104
|
-
}
|
|
2105
|
-
/** Sobe a sequência até pelo menos `n` (ex.: maior sequência encontrada no buffer). */
|
|
2106
|
-
ensureSequenceAtLeast(n) {
|
|
2107
|
-
if (n > this.seq) this.seq = n;
|
|
2108
|
-
}
|
|
2109
|
-
newPage() {
|
|
2110
|
-
if (!this.info) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
2111
|
-
this.info = { ...this.info, pageId: uid("pg_") };
|
|
2112
|
-
this.persist(this.state?.status ?? "recording", { pageCount: (this.state?.pageCount ?? 0) + 1 });
|
|
2113
|
-
return this.info.pageId;
|
|
2114
|
-
}
|
|
2115
|
-
get pageCount() {
|
|
2116
|
-
return this.state?.pageCount ?? 0;
|
|
2117
|
-
}
|
|
2118
|
-
// ---- ciclo de vida ------------------------------------------------------
|
|
2119
|
-
/** Cria (ou obtém, de forma idempotente) a sessão no backend. */
|
|
2120
|
-
async create(opts, context) {
|
|
2121
|
-
const queryToken = consumeQueryToken();
|
|
2122
|
-
const sessionToken = opts?.sessionToken || this.token || queryToken;
|
|
2123
|
-
if (!sessionToken || !opts.executionId || !opts.testCaseId)
|
|
2124
|
-
throw new AuthError("sessionToken, executionId e testCaseId s\xE3o obrigat\xF3rios");
|
|
2125
|
-
this.token = sessionToken;
|
|
2126
|
-
const tabId = await resolveTabId(this.cfg.storagePrefix, this.loadPersisted()?.tabId);
|
|
2127
|
-
const res = await this.api.createSession({
|
|
2128
|
-
platformId: this.cfg.platformId,
|
|
2129
|
-
testCaseId: opts.testCaseId,
|
|
2130
|
-
executionId: opts.executionId,
|
|
2131
|
-
tabId,
|
|
2132
|
-
sdkVersion: SDK_VERSION,
|
|
2133
|
-
appVersion: this.cfg.appVersion,
|
|
2134
|
-
context
|
|
1473
|
+
this.status = status;
|
|
1474
|
+
return this.write(["state"], (transaction) => {
|
|
1475
|
+
transaction.objectStore("state").put(status, "status");
|
|
2135
1476
|
});
|
|
2136
|
-
if (!res.recording?.enabled) {
|
|
2137
|
-
this.disabledReason = res.recording?.reason;
|
|
2138
|
-
throw new SessionDisabledError(res.recording?.reason);
|
|
2139
|
-
}
|
|
2140
|
-
if (res.sessionToken) this.token = res.sessionToken;
|
|
2141
|
-
this.seq = 0;
|
|
2142
|
-
this.info = {
|
|
2143
|
-
sessionId: res.sessionId,
|
|
2144
|
-
executionId: opts.executionId,
|
|
2145
|
-
testCaseId: opts.testCaseId,
|
|
2146
|
-
platformId: this.cfg.platformId,
|
|
2147
|
-
tabId,
|
|
2148
|
-
pageId: uid("pg_"),
|
|
2149
|
-
startedAt: Date.now()
|
|
2150
|
-
};
|
|
2151
|
-
safe(() => sessionStorage.removeItem(this.seqKey));
|
|
2152
|
-
this.persist("recording", {
|
|
2153
|
-
heartbeatIntervalMs: res.heartbeatIntervalMs,
|
|
2154
|
-
expiresAt: res.expiresAt,
|
|
2155
|
-
pageCount: 1,
|
|
2156
|
-
sequence: 0
|
|
2157
|
-
});
|
|
2158
|
-
return res;
|
|
2159
|
-
}
|
|
2160
|
-
/** Reidrata sessão persistida (reload/navegação MPA) após validar no backend. */
|
|
2161
|
-
async resume(state) {
|
|
2162
|
-
this.token = state.sessionToken;
|
|
2163
|
-
const tabId = await resolveTabId(this.cfg.storagePrefix, state.tabId);
|
|
2164
|
-
const res = await this.api.status(state.sessionId);
|
|
2165
|
-
if (res.status === "expired") throw new SessionExpiredError();
|
|
2166
|
-
if (res.status === "disabled") throw new SessionDisabledError();
|
|
2167
|
-
if (res.sessionToken) this.token = res.sessionToken;
|
|
2168
|
-
this.state = { ...state, tabId };
|
|
2169
|
-
this.seq = tabId === state.tabId ? state.sequence : 0;
|
|
2170
|
-
this.info = {
|
|
2171
|
-
sessionId: state.sessionId,
|
|
2172
|
-
executionId: state.executionId,
|
|
2173
|
-
testCaseId: state.testCaseId,
|
|
2174
|
-
platformId: state.platformId,
|
|
2175
|
-
tabId,
|
|
2176
|
-
pageId: uid("pg_"),
|
|
2177
|
-
startedAt: Date.now()
|
|
2178
|
-
};
|
|
2179
|
-
this.persist(state.status, { sessionToken: this.token, expiresAt: res.expiresAt ?? state.expiresAt, pageCount: state.pageCount + 1, tabId });
|
|
2180
|
-
return res;
|
|
2181
|
-
}
|
|
2182
|
-
startHeartbeat(onResult, onError, pending, status) {
|
|
2183
|
-
this.stopHeartbeat();
|
|
2184
|
-
const every = this.state?.heartbeatIntervalMs ?? this.cfg.heartbeatIntervalMs;
|
|
2185
|
-
if (!every || every <= 0) return;
|
|
2186
|
-
this.heartbeatTimer = setInterval(async () => {
|
|
2187
|
-
if (!this.info) return;
|
|
2188
|
-
try {
|
|
2189
|
-
const r = await this.api.heartbeat(this.info.sessionId, {
|
|
2190
|
-
tabId: this.info.tabId,
|
|
2191
|
-
pageId: this.info.pageId,
|
|
2192
|
-
status: status(),
|
|
2193
|
-
pending: pending(),
|
|
2194
|
-
lastSequence: this.seq
|
|
2195
|
-
});
|
|
2196
|
-
if (r.sessionToken) {
|
|
2197
|
-
this.token = r.sessionToken;
|
|
2198
|
-
this.persist(this.state?.status ?? "recording", { sessionToken: r.sessionToken, expiresAt: r.expiresAt });
|
|
2199
|
-
}
|
|
2200
|
-
onResult(r);
|
|
2201
|
-
} catch (e) {
|
|
2202
|
-
if (e instanceof AuthError || e instanceof SessionExpiredError) onError(e);
|
|
2203
|
-
}
|
|
2204
|
-
}, every);
|
|
2205
|
-
}
|
|
2206
|
-
stopHeartbeat() {
|
|
2207
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
2208
|
-
this.heartbeatTimer = void 0;
|
|
2209
1477
|
}
|
|
2210
|
-
async
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
1478
|
+
async clear() {
|
|
1479
|
+
await this.write(["events", "state"], (transaction) => {
|
|
1480
|
+
transaction.objectStore("events").clear();
|
|
1481
|
+
transaction.objectStore("state").clear();
|
|
1482
|
+
});
|
|
1483
|
+
this.failure = void 0;
|
|
1484
|
+
this.status = void 0;
|
|
1485
|
+
this.uncommitted.clear();
|
|
1486
|
+
sessionStorage.removeItem(this.checkpointKey);
|
|
2214
1487
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
this.
|
|
2218
|
-
this.
|
|
2219
|
-
this.token = "";
|
|
2220
|
-
this.seq = 0;
|
|
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()] }));
|
|
2221
1492
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
if (typeof window === "undefined") return "";
|
|
2225
|
-
try {
|
|
2226
|
-
const token = readQueryToken();
|
|
2227
|
-
if (!token) return "";
|
|
2228
|
-
const url = new URL(window.location.href);
|
|
2229
|
-
url.searchParams.delete("offscript_token");
|
|
2230
|
-
window.history.replaceState(window.history.state, typeof document === "undefined" ? "" : document.title, `${url.pathname}${url.search}${url.hash}`);
|
|
2231
|
-
return token;
|
|
2232
|
-
} catch {
|
|
2233
|
-
return "";
|
|
1493
|
+
get checkpointKey() {
|
|
1494
|
+
return `${this.database.name}-handoff`;
|
|
2234
1495
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
try {
|
|
2239
|
-
return new URL(window.location.href).searchParams.get("offscript_token")?.trim() ?? "";
|
|
2240
|
-
} catch {
|
|
2241
|
-
return "";
|
|
1496
|
+
async flush() {
|
|
1497
|
+
await Promise.allSettled([...this.pending]);
|
|
1498
|
+
if (this.failure) throw this.failure;
|
|
2242
1499
|
}
|
|
2243
|
-
|
|
2244
|
-
var heldLocks = [];
|
|
2245
|
-
async function resolveTabId(prefix, previous) {
|
|
2246
|
-
const candidate = previous ?? uid("tab_");
|
|
2247
|
-
const locks = globalThis.navigator?.locks;
|
|
2248
|
-
if (!locks || !previous) {
|
|
2249
|
-
if (locks) void claim(locks, `${prefix}:tab:${candidate}`);
|
|
2250
|
-
return candidate;
|
|
2251
|
-
}
|
|
2252
|
-
if (heldLocks.includes(candidate)) return candidate;
|
|
2253
|
-
const ok = await claim(locks, `${prefix}:tab:${candidate}`);
|
|
2254
|
-
if (ok) return candidate;
|
|
2255
|
-
const fresh = uid("tab_");
|
|
2256
|
-
void claim(locks, `${prefix}:tab:${fresh}`);
|
|
2257
|
-
return fresh;
|
|
2258
|
-
}
|
|
2259
|
-
function claim(locks, name) {
|
|
2260
|
-
return new Promise((resolve) => {
|
|
1500
|
+
async close() {
|
|
2261
1501
|
try {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
return void 0;
|
|
2266
|
-
}
|
|
2267
|
-
heldLocks.push(name.split(":tab:")[1]);
|
|
2268
|
-
resolve(true);
|
|
2269
|
-
return new Promise(() => void 0);
|
|
2270
|
-
}).catch(() => resolve(true));
|
|
2271
|
-
} catch {
|
|
2272
|
-
resolve(true);
|
|
1502
|
+
await this.flush();
|
|
1503
|
+
} finally {
|
|
1504
|
+
this.database.close();
|
|
2273
1505
|
}
|
|
2274
|
-
}
|
|
2275
|
-
|
|
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
|
+
};
|
|
2276
1526
|
|
|
2277
1527
|
// src/core/config.ts
|
|
2278
1528
|
function resolveConfig(input) {
|
|
2279
|
-
if (!input || typeof input !== "object") throw new ConfigError("Configura\xE7\xE3o ausente");
|
|
2280
|
-
if (!input.platformId) throw new ConfigError("platformId \xE9 obrigat\xF3rio");
|
|
2281
|
-
if (!input.apiUrl) throw new ConfigError("apiUrl \xE9 obrigat\xF3rio");
|
|
2282
|
-
let url;
|
|
2283
|
-
try {
|
|
2284
|
-
url = new URL(input.apiUrl);
|
|
2285
|
-
} catch {
|
|
2286
|
-
throw new ConfigError(`apiUrl inv\xE1lida: ${input.apiUrl}`);
|
|
2287
|
-
}
|
|
2288
|
-
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
2289
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
2290
|
-
throw new ConfigError("apiUrl deve usar HTTPS (HTTP s\xF3 \xE9 aceito em localhost)");
|
|
2291
1529
|
return {
|
|
2292
|
-
apiUrl: url.origin + url.pathname.replace(/\/+$/, ""),
|
|
2293
|
-
platformId: input.platformId,
|
|
2294
|
-
appVersion: input.appVersion,
|
|
2295
1530
|
recording: deepMerge(DEFAULT_RECORDING, input.recording),
|
|
2296
1531
|
privacy: resolvePrivacy(input.privacy),
|
|
2297
|
-
transport: deepMerge(DEFAULT_TRANSPORT, input.transport),
|
|
2298
1532
|
correlationWindowMs: input.correlationWindowMs ?? 3e3,
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
controls: input.controls ?? true
|
|
1533
|
+
controls: input.controls ?? true,
|
|
1534
|
+
endpoint: input.endpoint,
|
|
1535
|
+
persistenceKey: input.persistenceKey,
|
|
1536
|
+
onComplete: input.onComplete
|
|
2304
1537
|
};
|
|
2305
1538
|
}
|
|
2306
1539
|
|
|
@@ -2369,6 +1602,22 @@ var RecordingControls = class {
|
|
|
2369
1602
|
this.render();
|
|
2370
1603
|
this.startEyes();
|
|
2371
1604
|
}
|
|
1605
|
+
/** Independent notice so the host may destroy the controls after successful upload. */
|
|
1606
|
+
showSuccess() {
|
|
1607
|
+
const notice = document.createElement("div");
|
|
1608
|
+
notice.setAttribute("data-offscript-success", "");
|
|
1609
|
+
notice.setAttribute("data-offscript-ignore", "");
|
|
1610
|
+
notice.className = "offscript-ignore";
|
|
1611
|
+
notice.style.cssText = "position:fixed;right:16px;bottom:16px;max-width:calc(100vw - 32px);z-index:2147483647;";
|
|
1612
|
+
const shadow = notice.attachShadow({ mode: "closed" });
|
|
1613
|
+
shadow.innerHTML = SUCCESS_TEMPLATE;
|
|
1614
|
+
document.body.appendChild(notice);
|
|
1615
|
+
const timer = window.setTimeout(() => notice.remove(), 8e3);
|
|
1616
|
+
shadow.querySelector("button").addEventListener("click", () => {
|
|
1617
|
+
window.clearTimeout(timer);
|
|
1618
|
+
notice.remove();
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
2372
1621
|
unmount() {
|
|
2373
1622
|
this.unsubscribe?.();
|
|
2374
1623
|
this.unsubscribe = void 0;
|
|
@@ -2420,15 +1669,15 @@ var RecordingControls = class {
|
|
|
2420
1669
|
} else if (action === "close") {
|
|
2421
1670
|
this.expanded = false;
|
|
2422
1671
|
this.render();
|
|
2423
|
-
} else if (action === "record") void this.run(() => this.sdk.
|
|
1672
|
+
} else if (action === "record") void this.run(() => this.sdk.startRecording());
|
|
2424
1673
|
else if (action === "pause") void this.run(() => this.sdk.pauseRecording());
|
|
2425
1674
|
else if (action === "resume") void this.run(() => this.sdk.resumeRecording());
|
|
2426
|
-
else if (action === "stop") void this.run(() => this.sdk.
|
|
1675
|
+
else if (action === "stop") void this.run(() => this.sdk.finishRecording());
|
|
2427
1676
|
else if (action === "new") this.openConfirm();
|
|
2428
1677
|
else if (action === "cancel") this.closeConfirm();
|
|
2429
1678
|
else if (action === "confirm") {
|
|
2430
1679
|
this.closeConfirm();
|
|
2431
|
-
void this.run(() => this.sdk.
|
|
1680
|
+
void this.run(() => this.sdk.discardRecording());
|
|
2432
1681
|
} else if (action === "privacy-show") {
|
|
2433
1682
|
this.privacyOpen = true;
|
|
2434
1683
|
this.render();
|
|
@@ -2496,23 +1745,24 @@ var RecordingControls = class {
|
|
|
2496
1745
|
}
|
|
2497
1746
|
render() {
|
|
2498
1747
|
this.place();
|
|
2499
|
-
const status = this.sdk.getState()
|
|
1748
|
+
const { status, pending } = this.sdk.getState();
|
|
2500
1749
|
const shell = this.shadow?.querySelector("[data-shell]");
|
|
2501
1750
|
shell?.setAttribute("data-open", this.expanded ? "true" : "false");
|
|
2502
1751
|
shell?.setAttribute("data-state", status);
|
|
2503
1752
|
if (this.note) this.note.textContent = this.failure;
|
|
2504
1753
|
const live = status === "recording" || status === "paused";
|
|
1754
|
+
const canFinish = live || status === "stopped" && pending > 0;
|
|
2505
1755
|
if (!live) this.closeConfirm();
|
|
2506
1756
|
this.paintPrivacy();
|
|
2507
1757
|
this.setHidden("record", live || status === "initializing" || status === "stopping");
|
|
2508
1758
|
this.setHidden("pause", status !== "recording");
|
|
2509
1759
|
this.setHidden("resume", status !== "paused");
|
|
2510
1760
|
this.setHidden("new", !live);
|
|
2511
|
-
this.setHidden("stop", !
|
|
1761
|
+
this.setHidden("stop", !canFinish);
|
|
2512
1762
|
this.setEnabled("record", !this.busy && !live && status !== "initializing" && status !== "stopping");
|
|
2513
1763
|
this.setEnabled("pause", !this.busy && status === "recording");
|
|
2514
1764
|
this.setEnabled("resume", !this.busy && status === "paused");
|
|
2515
|
-
this.setEnabled("stop", !this.busy &&
|
|
1765
|
+
this.setEnabled("stop", !this.busy && canFinish);
|
|
2516
1766
|
this.setEnabled("new", !this.busy && live);
|
|
2517
1767
|
}
|
|
2518
1768
|
paintPrivacy() {
|
|
@@ -2902,7 +2152,7 @@ var TEMPLATE = `
|
|
|
2902
2152
|
<button type="button" class="text record" data-action="record">Iniciar</button>
|
|
2903
2153
|
<button type="button" data-action="pause" aria-label="Pausar" title="Pausar">${ICON.pause}</button>
|
|
2904
2154
|
<button type="button" data-action="resume" aria-label="Retomar" title="Retomar">${ICON.play}</button>
|
|
2905
|
-
<button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova
|
|
2155
|
+
<button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova grava\xE7\xE3o</button>
|
|
2906
2156
|
<button type="button" class="text finish" data-action="stop" title="Encerra a coleta e envia o que foi gravado">Finalizar</button>
|
|
2907
2157
|
<button type="button" data-action="close" aria-label="Fechar" title="Fechar">${ICON.close}</button>
|
|
2908
2158
|
</div>
|
|
@@ -2910,7 +2160,7 @@ var TEMPLATE = `
|
|
|
2910
2160
|
<div class="speak" data-confirm hidden>
|
|
2911
2161
|
<div class="bubble" role="dialog" aria-labelledby="offscript-confirm-title">
|
|
2912
2162
|
${mark("offscript-trail-speak")}
|
|
2913
|
-
<p id="offscript-confirm-title">Descartar esta
|
|
2163
|
+
<p id="offscript-confirm-title">Descartar esta grava\xE7\xE3o? O que foi gravado ser\xE1 ignorado.</p>
|
|
2914
2164
|
<div class="choices">
|
|
2915
2165
|
<button type="button" class="choice secondary" data-action="cancel">Cancelar</button>
|
|
2916
2166
|
<button type="button" class="choice danger" data-action="confirm">Descartar</button>
|
|
@@ -2920,325 +2170,387 @@ var TEMPLATE = `
|
|
|
2920
2170
|
<p class="note" data-note></p>
|
|
2921
2171
|
</div>
|
|
2922
2172
|
`;
|
|
2173
|
+
var SUCCESS_TEMPLATE = `
|
|
2174
|
+
<style>
|
|
2175
|
+
:host { color-scheme: dark; }
|
|
2176
|
+
.success {
|
|
2177
|
+
display: flex;
|
|
2178
|
+
align-items: center;
|
|
2179
|
+
gap: 12px;
|
|
2180
|
+
padding: 12px 14px;
|
|
2181
|
+
border: 1px solid #377c65;
|
|
2182
|
+
border-radius: 16px;
|
|
2183
|
+
background: #152c25;
|
|
2184
|
+
color: #dcfce7;
|
|
2185
|
+
box-shadow: 0 12px 32px rgba(3, 5, 12, 0.35);
|
|
2186
|
+
font: 500 13px/1.5 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2187
|
+
}
|
|
2188
|
+
p { margin: 0; }
|
|
2189
|
+
button {
|
|
2190
|
+
flex: none;
|
|
2191
|
+
display: grid;
|
|
2192
|
+
place-items: center;
|
|
2193
|
+
width: 28px;
|
|
2194
|
+
height: 28px;
|
|
2195
|
+
padding: 0;
|
|
2196
|
+
border: 0;
|
|
2197
|
+
border-radius: 999px;
|
|
2198
|
+
background: transparent;
|
|
2199
|
+
color: inherit;
|
|
2200
|
+
font: 20px/1 ui-sans-serif, system-ui, sans-serif;
|
|
2201
|
+
cursor: pointer;
|
|
2202
|
+
}
|
|
2203
|
+
button:hover { background: #244a3c; }
|
|
2204
|
+
button:focus-visible { outline: 2px solid #86efac; outline-offset: 2px; }
|
|
2205
|
+
</style>
|
|
2206
|
+
<div class="success">
|
|
2207
|
+
<p role="status">Grava\xE7\xE3o enviada com sucesso!</p>
|
|
2208
|
+
<button type="button" aria-label="Fechar confirma\xE7\xE3o">\xD7</button>
|
|
2209
|
+
</div>
|
|
2210
|
+
`;
|
|
2923
2211
|
|
|
2924
2212
|
// src/core/OffscriptSDK.ts
|
|
2925
|
-
var
|
|
2213
|
+
var TOKEN_STORAGE_KEY = "offscript-session-token";
|
|
2214
|
+
var DRAFT_STORAGE_KEY = "offscript-draft-key";
|
|
2215
|
+
var COMPLETED_TOKEN_STORAGE_KEY = "offscript-completed-token";
|
|
2216
|
+
function tokenFingerprint(token) {
|
|
2217
|
+
let first = 2166136261;
|
|
2218
|
+
let second = 2654435769;
|
|
2219
|
+
for (let index = 0; index < token.length; index++) {
|
|
2220
|
+
const character = token.charCodeAt(index);
|
|
2221
|
+
first = Math.imul(first ^ character, 16777619);
|
|
2222
|
+
second = Math.imul(second ^ character, 2246822519);
|
|
2223
|
+
}
|
|
2224
|
+
return `${first >>> 0}.${second >>> 0}`;
|
|
2225
|
+
}
|
|
2926
2226
|
var Offscript = class {
|
|
2927
|
-
constructor(config) {
|
|
2227
|
+
constructor(config = {}) {
|
|
2928
2228
|
this.bus = new Emitter();
|
|
2929
2229
|
this.status = "idle";
|
|
2930
2230
|
this.recorders = [];
|
|
2931
2231
|
this.accepting = false;
|
|
2932
2232
|
this.destroyed = false;
|
|
2233
|
+
this.suspended = false;
|
|
2234
|
+
this.events = [];
|
|
2933
2235
|
this.recent = [];
|
|
2934
2236
|
this.actionLog = [];
|
|
2935
2237
|
this.contextLog = [];
|
|
2936
|
-
this.
|
|
2937
|
-
this.
|
|
2238
|
+
this.sequence = 0;
|
|
2239
|
+
this.pageId = uid("page_");
|
|
2240
|
+
this.submitted = false;
|
|
2241
|
+
this.launchTokenChanged = false;
|
|
2938
2242
|
this.config = resolveConfig(config);
|
|
2939
2243
|
this.sanitizer = new DataSanitizer(this.config.privacy);
|
|
2940
|
-
this.sessions = new SessionManager(this.config);
|
|
2941
2244
|
}
|
|
2942
|
-
// ------------------------------------------------------------------ API pública
|
|
2943
2245
|
getState() {
|
|
2944
|
-
return { status: this.status,
|
|
2246
|
+
return { status: this.status, pending: this.submitted ? 0 : this.events.length, lastError: this.lastError };
|
|
2945
2247
|
}
|
|
2946
2248
|
capturesSensitiveValues() {
|
|
2947
|
-
return this.config.privacy.captureSensitiveValues
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
if (this.
|
|
2955
|
-
this.
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
async doInit() {
|
|
2959
|
-
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um ambiente de navegador");
|
|
2960
|
-
this.ensureControls();
|
|
2961
|
-
this.setStatus("initializing");
|
|
2962
|
-
try {
|
|
2963
|
-
const buffer = await openBuffer(`${this.config.storagePrefix}-${this.config.platformId}`, this.config.transport.persist);
|
|
2964
|
-
this.transport = new EventTransport(this.config.transport, this.sessions.api, buffer);
|
|
2965
|
-
this.transport.on("flushed", (e) => this.bus.emit("flush", e));
|
|
2966
|
-
this.transport.on("overflow", (e) => this.bus.emit("overflow", e));
|
|
2967
|
-
this.transport.on("fatal", ({ error }) => void this.onFatal(error));
|
|
2968
|
-
this.transport.on("failed", ({ error }) => this.debug("transport failed", error));
|
|
2969
|
-
await this.transport.purgeExpired(this.config.privacy.localRetentionMs);
|
|
2970
|
-
this.setStatus("idle");
|
|
2971
|
-
const persisted = this.config.autoResume ? this.sessions.loadPersisted() : null;
|
|
2972
|
-
if (persisted) await this.resumePersisted(persisted);
|
|
2973
|
-
} catch (e) {
|
|
2974
|
-
this.fail(e);
|
|
2975
|
-
throw e;
|
|
2249
|
+
return this.config.privacy.captureSensitiveValues;
|
|
2250
|
+
}
|
|
2251
|
+
async init() {
|
|
2252
|
+
this.assertAlive();
|
|
2253
|
+
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um navegador");
|
|
2254
|
+
this.initPromise ?? (this.initPromise = this.initialize());
|
|
2255
|
+
await this.initPromise;
|
|
2256
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2257
|
+
if (this.config.controls && this.launchToken && !this.controls) {
|
|
2258
|
+
this.controls = new RecordingControls(this);
|
|
2259
|
+
this.controls.mount();
|
|
2976
2260
|
}
|
|
2977
2261
|
}
|
|
2978
|
-
async
|
|
2979
|
-
|
|
2980
|
-
if (this.
|
|
2981
|
-
|
|
2982
|
-
this.startPromise = p;
|
|
2983
|
-
return p;
|
|
2984
|
-
}
|
|
2985
|
-
async doStart(opts) {
|
|
2986
|
-
await this.init();
|
|
2987
|
-
const cur = this.sessions.session;
|
|
2988
|
-
if ((this.status === "recording" || this.status === "paused") && cur) {
|
|
2989
|
-
if (cur.executionId === opts.executionId) return cur;
|
|
2990
|
-
throw new InvalidStateError(`J\xE1 existe grava\xE7\xE3o ativa para a execu\xE7\xE3o ${cur.executionId}. Encerre-a antes.`);
|
|
2262
|
+
async initialize() {
|
|
2263
|
+
this.resolveLaunchToken();
|
|
2264
|
+
if (this.launchToken && !this.config.persistenceKey) {
|
|
2265
|
+
this.config.persistenceKey = this.getOrCreateDraftKey();
|
|
2991
2266
|
}
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2267
|
+
await this.initializeDraft();
|
|
2268
|
+
}
|
|
2269
|
+
resolveLaunchToken() {
|
|
2270
|
+
const incomingToken = new URLSearchParams(window.location.search).get("offscript_token")?.trim();
|
|
2271
|
+
const previousToken = this.readSessionValue(TOKEN_STORAGE_KEY);
|
|
2272
|
+
const completedToken = this.readSessionValue(COMPLETED_TOKEN_STORAGE_KEY);
|
|
2273
|
+
const legacyCompletion = completedToken === "1" && Boolean(previousToken);
|
|
2274
|
+
const completedFingerprint = legacyCompletion && previousToken ? tokenFingerprint(previousToken) : completedToken;
|
|
2275
|
+
const candidateToken = incomingToken || previousToken || void 0;
|
|
2276
|
+
this.launchTokenChanged = Boolean(incomingToken && previousToken && incomingToken !== previousToken) || Boolean(incomingToken && completedFingerprint && completedFingerprint !== "1" && tokenFingerprint(incomingToken) !== completedFingerprint);
|
|
2277
|
+
if (this.launchTokenChanged) {
|
|
2278
|
+
this.removeSessionValue(COMPLETED_TOKEN_STORAGE_KEY);
|
|
2279
|
+
}
|
|
2280
|
+
if (legacyCompletion && previousToken && !this.launchTokenChanged) {
|
|
2281
|
+
this.writeSessionValue(COMPLETED_TOKEN_STORAGE_KEY, tokenFingerprint(previousToken));
|
|
2282
|
+
this.removeSessionValue(TOKEN_STORAGE_KEY);
|
|
2283
|
+
}
|
|
2284
|
+
const isCompleted = Boolean(candidateToken && completedFingerprint === tokenFingerprint(candidateToken));
|
|
2285
|
+
this.launchToken = isCompleted ? void 0 : candidateToken;
|
|
2286
|
+
if (incomingToken && !isCompleted) this.writeSessionValue(TOKEN_STORAGE_KEY, incomingToken);
|
|
2287
|
+
}
|
|
2288
|
+
getOrCreateDraftKey() {
|
|
2289
|
+
const saved = this.readSessionValue(DRAFT_STORAGE_KEY);
|
|
2290
|
+
if (saved) return saved;
|
|
2291
|
+
const key = uid("draft_");
|
|
2292
|
+
this.writeSessionValue(DRAFT_STORAGE_KEY, key);
|
|
2293
|
+
return key;
|
|
2294
|
+
}
|
|
2295
|
+
readSessionValue(key) {
|
|
2995
2296
|
try {
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
3000
|
-
this.attach();
|
|
3001
|
-
this.accepting = true;
|
|
3002
|
-
this.setStatus("recording");
|
|
3003
|
-
this.emitLifecycle("session-start", { pageCount: 1 }, "critical");
|
|
3004
|
-
await this.startRecorders();
|
|
3005
|
-
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
3006
|
-
this.bus.emit("sessionStart", s);
|
|
3007
|
-
return s;
|
|
3008
|
-
} catch (e) {
|
|
3009
|
-
await this.teardown();
|
|
3010
|
-
this.sessions.clearPersisted();
|
|
3011
|
-
this.sessions.reset();
|
|
3012
|
-
this.fail(e);
|
|
3013
|
-
throw e;
|
|
2297
|
+
return window.sessionStorage.getItem(key);
|
|
2298
|
+
} catch {
|
|
2299
|
+
return null;
|
|
3014
2300
|
}
|
|
3015
2301
|
}
|
|
3016
|
-
|
|
3017
|
-
this.setStatus("initializing");
|
|
2302
|
+
writeSessionValue(key, value) {
|
|
3018
2303
|
try {
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
3022
|
-
this.attach();
|
|
3023
|
-
this.accepting = state.status === "recording";
|
|
3024
|
-
this.setStatus(state.status);
|
|
3025
|
-
this.emitLifecycle("session-resume", { pageCount: this.sessions.pageCount }, "critical");
|
|
3026
|
-
this.emitLifecycle("page-start", { navigationType: safe(() => performance.getEntriesByType("navigation")[0]?.type) }, "critical");
|
|
3027
|
-
await this.startRecorders();
|
|
3028
|
-
if (state.status === "paused") this.recorders.forEach((r) => r.pause?.());
|
|
3029
|
-
this.actions?.recordPageLoad(safe(() => performance.getEntriesByType("navigation")[0]?.type), document.referrer);
|
|
3030
|
-
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
3031
|
-
const token = this.sessions.currentToken;
|
|
3032
|
-
if (token) {
|
|
3033
|
-
this.lastStart = { testCaseId: s.testCaseId, executionId: s.executionId, sessionToken: token };
|
|
3034
|
-
this.armed ?? (this.armed = { testCaseId: s.testCaseId, sessionToken: token });
|
|
3035
|
-
}
|
|
3036
|
-
this.bus.emit("sessionStart", s);
|
|
3037
|
-
} catch (e) {
|
|
3038
|
-
await this.teardown();
|
|
3039
|
-
this.sessions.clearPersisted();
|
|
3040
|
-
this.sessions.reset();
|
|
3041
|
-
this.fail(e);
|
|
3042
|
-
if (!(e instanceof SessionExpiredError || e instanceof SessionDisabledError || e instanceof AuthError)) throw e;
|
|
2304
|
+
window.sessionStorage.setItem(key, value);
|
|
2305
|
+
} catch {
|
|
3043
2306
|
}
|
|
3044
2307
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
startArmedRecording() {
|
|
3051
|
-
if ((this.status === "recording" || this.status === "paused") && this.sessions.session) return Promise.resolve(this.sessions.session);
|
|
3052
|
-
return this.startRecording(this.freshStart());
|
|
2308
|
+
removeSessionValue(key) {
|
|
2309
|
+
try {
|
|
2310
|
+
window.sessionStorage.removeItem(key);
|
|
2311
|
+
} catch {
|
|
2312
|
+
}
|
|
3053
2313
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
await this.
|
|
2314
|
+
async initializeDraft() {
|
|
2315
|
+
if (!this.config.persistenceKey) return;
|
|
2316
|
+
this.store = await RecordingStore.open(this.config.persistenceKey);
|
|
2317
|
+
if (this.launchTokenChanged) await this.store.clear();
|
|
2318
|
+
await this.restoreDraft();
|
|
2319
|
+
}
|
|
2320
|
+
async restoreDraft() {
|
|
2321
|
+
const saved = await this.store.load();
|
|
2322
|
+
if (this.suspended) return;
|
|
2323
|
+
this.clearRecording();
|
|
2324
|
+
if (!saved) {
|
|
2325
|
+
this.setStatus("idle");
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
this.events = saved.events;
|
|
2329
|
+
this.sequence = saved.events[saved.events.length - 1]?.sequence ?? 0;
|
|
2330
|
+
this.actionLog = saved.events.filter((event) => event.kind === "action").map((event) => event.data);
|
|
2331
|
+
this.contextLog = saved.events.filter((event) => event.kind === "context").map((event) => event.data);
|
|
2332
|
+
this.setStatus(saved.status);
|
|
2333
|
+
if (saved.status === "stopped") {
|
|
2334
|
+
this.payload = this.buildPayload();
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
this.accepting = saved.status === "recording";
|
|
2338
|
+
this.attach();
|
|
2339
|
+
await this.startRecorders();
|
|
2340
|
+
if (saved.status === "paused") this.recorders.forEach((recorder) => recorder.pause?.());
|
|
2341
|
+
}
|
|
2342
|
+
save(operation) {
|
|
2343
|
+
void operation.catch((error) => {
|
|
2344
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2345
|
+
this.lastError = { code: "LOCAL_STORAGE_FAILED", message: err.message };
|
|
2346
|
+
this.bus.emit("error", err);
|
|
2347
|
+
});
|
|
3058
2348
|
}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
2349
|
+
startRecording() {
|
|
2350
|
+
if (this.startPromise) return this.startPromise;
|
|
2351
|
+
this.startPromise = this.doStart().finally(() => {
|
|
2352
|
+
this.startPromise = void 0;
|
|
2353
|
+
});
|
|
2354
|
+
return this.startPromise;
|
|
3063
2355
|
}
|
|
3064
|
-
async
|
|
3065
|
-
|
|
3066
|
-
this.
|
|
3067
|
-
this.
|
|
3068
|
-
this.
|
|
3069
|
-
this.
|
|
3070
|
-
|
|
3071
|
-
this.
|
|
3072
|
-
this.
|
|
3073
|
-
this.
|
|
3074
|
-
this.
|
|
3075
|
-
|
|
2356
|
+
async doStart() {
|
|
2357
|
+
await this.init();
|
|
2358
|
+
if (this.status === "recording" || this.status === "paused") return;
|
|
2359
|
+
if (this.completePromise || this.stopPromise) throw new InvalidStateError("Finaliza\xE7\xE3o em andamento");
|
|
2360
|
+
await this.store?.clear();
|
|
2361
|
+
this.clearRecording();
|
|
2362
|
+
this.setStatus("initializing");
|
|
2363
|
+
await this.store?.setStatus("recording");
|
|
2364
|
+
this.accepting = true;
|
|
2365
|
+
this.attach();
|
|
2366
|
+
this.emitLifecycle("recording-start");
|
|
2367
|
+
await this.startRecorders();
|
|
2368
|
+
this.setStatus("recording");
|
|
2369
|
+
await this.store?.flush();
|
|
2370
|
+
this.bus.emit("recordingStart", void 0);
|
|
3076
2371
|
}
|
|
3077
2372
|
async pauseRecording() {
|
|
2373
|
+
this.assertAlive();
|
|
2374
|
+
if (this.restorePromise) await this.restorePromise;
|
|
3078
2375
|
if (this.status === "paused") return;
|
|
3079
|
-
if (this.status !== "recording") throw new InvalidStateError(
|
|
2376
|
+
if (this.status !== "recording") throw new InvalidStateError("Sem grava\xE7\xE3o ativa");
|
|
3080
2377
|
this.actions?.flushAll();
|
|
3081
|
-
this.emitLifecycle("pause"
|
|
2378
|
+
this.emitLifecycle("pause");
|
|
3082
2379
|
this.accepting = false;
|
|
3083
2380
|
this.recorders.forEach((r) => safe(() => r.pause?.()));
|
|
3084
|
-
this.sessions.setStatus("paused");
|
|
3085
2381
|
this.setStatus("paused");
|
|
3086
|
-
|
|
2382
|
+
await this.store?.flush();
|
|
3087
2383
|
}
|
|
3088
2384
|
async resumeRecording() {
|
|
2385
|
+
this.assertAlive();
|
|
2386
|
+
if (this.restorePromise) await this.restorePromise;
|
|
3089
2387
|
if (this.status === "recording") return;
|
|
3090
|
-
if (this.status !== "paused") throw new InvalidStateError(
|
|
2388
|
+
if (this.status !== "paused") throw new InvalidStateError("Grava\xE7\xE3o n\xE3o est\xE1 pausada");
|
|
3091
2389
|
this.accepting = true;
|
|
3092
|
-
this.
|
|
2390
|
+
this.emitLifecycle("resume");
|
|
2391
|
+
for (const r of this.recorders) await r.resume?.();
|
|
3093
2392
|
this.setStatus("recording");
|
|
3094
|
-
this.
|
|
3095
|
-
for (const r of this.recorders) await Promise.resolve(safe(() => r.resume?.()));
|
|
2393
|
+
await this.store?.flush();
|
|
3096
2394
|
}
|
|
3097
2395
|
stopRecording() {
|
|
3098
2396
|
if (this.stopPromise) return this.stopPromise;
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
return Promise.resolve({ sessionId: s?.sessionId ?? "", flushed: true, pendingEvents: 0, completed: true });
|
|
3103
|
-
return Promise.reject(new InvalidStateError(`N\xE3o \xE9 poss\xEDvel encerrar no estado ${this.status}`));
|
|
3104
|
-
}
|
|
3105
|
-
this.stopPromise = this.doStop().finally(() => this.stopPromise = void 0);
|
|
2397
|
+
this.stopPromise = this.doStop().finally(() => {
|
|
2398
|
+
this.stopPromise = void 0;
|
|
2399
|
+
});
|
|
3106
2400
|
return this.stopPromise;
|
|
3107
2401
|
}
|
|
3108
2402
|
async doStop() {
|
|
3109
|
-
|
|
2403
|
+
this.assertAlive();
|
|
2404
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2405
|
+
if (this.startPromise) await this.startPromise;
|
|
2406
|
+
if (this.payload) return this.payload;
|
|
2407
|
+
if (this.status !== "recording" && this.status !== "paused") throw new InvalidStateError("Sem grava\xE7\xE3o ativa");
|
|
3110
2408
|
this.setStatus("stopping");
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
2409
|
+
this.actions?.flushAll();
|
|
2410
|
+
this.emitLifecycle("stop");
|
|
2411
|
+
const recorders = this.recorders.splice(0).reverse();
|
|
2412
|
+
this.lifecycle?.uninstall();
|
|
2413
|
+
for (const recorder of recorders) {
|
|
2414
|
+
if (!(recorder instanceof HTTPRecorder)) safe(() => recorder.stop());
|
|
2415
|
+
}
|
|
2416
|
+
for (const recorder of recorders) {
|
|
2417
|
+
if (recorder instanceof HTTPRecorder) await recorder.drain();
|
|
3114
2418
|
}
|
|
3115
2419
|
this.accepting = false;
|
|
3116
|
-
this.
|
|
3117
|
-
this.
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
2420
|
+
this.rrweb = this.actions = this.context = void 0;
|
|
2421
|
+
this.payload = this.buildPayload();
|
|
2422
|
+
this.setStatus("stopped");
|
|
2423
|
+
await this.store?.flush();
|
|
2424
|
+
this.bus.emit("recordingEnd", this.payload);
|
|
2425
|
+
return this.payload;
|
|
2426
|
+
}
|
|
2427
|
+
complete(options = {}) {
|
|
2428
|
+
this.assertAlive();
|
|
2429
|
+
if (this.completePromise) return this.completePromise;
|
|
2430
|
+
if (this.submitted) return Promise.resolve(this.response);
|
|
2431
|
+
this.completePromise = this.doComplete(options).finally(() => {
|
|
2432
|
+
this.completePromise = void 0;
|
|
2433
|
+
});
|
|
2434
|
+
return this.completePromise;
|
|
2435
|
+
}
|
|
2436
|
+
async doComplete(options) {
|
|
2437
|
+
const endpoint = options.endpoint ?? this.config.endpoint;
|
|
2438
|
+
const token = options.token?.trim() || this.launchToken;
|
|
2439
|
+
if (!endpoint) throw new ConfigError("Endpoint HTTP obrigat\xF3rio para envio");
|
|
2440
|
+
if (!token) throw new ConfigError("Token obrigat\xF3rio para envio");
|
|
2441
|
+
const url = new URL(endpoint, location.href);
|
|
2442
|
+
if (!["https:", "http:"].includes(url.protocol)) throw new ConfigError("Endpoint HTTP inv\xE1lido");
|
|
2443
|
+
const payload = await this.stopRecording();
|
|
2444
|
+
try {
|
|
2445
|
+
const response = await fetch(url.href, {
|
|
2446
|
+
method: "POST",
|
|
2447
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${token}` },
|
|
2448
|
+
body: JSON.stringify(payload)
|
|
2449
|
+
});
|
|
2450
|
+
if (!response.ok) throw new OffscriptError("TRANSPORT_FAILED", `Falha ao enviar grava\xE7\xE3o: HTTP ${response.status}`, { status: response.status });
|
|
2451
|
+
this.submitted = true;
|
|
2452
|
+
const completedToken = this.launchToken ?? token;
|
|
2453
|
+
this.writeSessionValue(COMPLETED_TOKEN_STORAGE_KEY, tokenFingerprint(completedToken));
|
|
2454
|
+
this.removeSessionValue(TOKEN_STORAGE_KEY);
|
|
2455
|
+
this.removeSessionValue(DRAFT_STORAGE_KEY);
|
|
2456
|
+
this.launchToken = void 0;
|
|
2457
|
+
await this.store?.clear();
|
|
2458
|
+
const text = await response.text();
|
|
3121
2459
|
try {
|
|
3122
|
-
|
|
3123
|
-
} catch
|
|
3124
|
-
this.
|
|
2460
|
+
this.response = text ? JSON.parse(text) : null;
|
|
2461
|
+
} catch {
|
|
2462
|
+
this.response = text;
|
|
3125
2463
|
}
|
|
2464
|
+
this.lastError = void 0;
|
|
2465
|
+
this.controls?.showSuccess();
|
|
2466
|
+
this.controls?.unmount();
|
|
2467
|
+
this.controls = void 0;
|
|
2468
|
+
return this.response;
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2471
|
+
this.lastError = { code: err instanceof OffscriptError ? err.code : "INTERNAL", message: err.message };
|
|
2472
|
+
this.bus.emit("error", err);
|
|
2473
|
+
throw err;
|
|
3126
2474
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
2475
|
+
}
|
|
2476
|
+
async finishRecording() {
|
|
2477
|
+
if (this.config.onComplete) return this.config.onComplete();
|
|
2478
|
+
if (this.launchToken) return this.complete();
|
|
2479
|
+
return this.stopRecording();
|
|
2480
|
+
}
|
|
2481
|
+
async discardRecording() {
|
|
2482
|
+
this.assertAlive();
|
|
2483
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2484
|
+
if (this.startPromise) await this.startPromise;
|
|
2485
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2486
|
+
if (this.completePromise) throw new InvalidStateError("Envio em andamento");
|
|
2487
|
+
this.accepting = false;
|
|
2488
|
+
this.stopRecorders();
|
|
3129
2489
|
this.lifecycle?.uninstall();
|
|
3130
|
-
this.
|
|
3131
|
-
this.
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
return summary;
|
|
3136
|
-
}
|
|
3137
|
-
/**
|
|
3138
|
-
* Libera todos os recursos. Por padrão encerra a gravação ativa; com `{ complete: false }` apenas
|
|
3139
|
-
* desanexa a instrumentação e mantém a sessão persistida para ser retomada por outra instância.
|
|
3140
|
-
*/
|
|
3141
|
-
async destroy(opts = {}) {
|
|
2490
|
+
await this.store?.clear();
|
|
2491
|
+
this.clearRecording();
|
|
2492
|
+
this.setStatus("idle");
|
|
2493
|
+
}
|
|
2494
|
+
async destroy() {
|
|
3142
2495
|
if (this.destroyed) return;
|
|
3143
|
-
if (
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
}
|
|
3151
|
-
this.destroyed = true;
|
|
3152
|
-
await this.teardown();
|
|
2496
|
+
if (this.restorePromise) await this.restorePromise;
|
|
2497
|
+
if (this.startPromise) await this.startPromise;
|
|
2498
|
+
if (this.completePromise) await this.completePromise.catch(() => void 0);
|
|
2499
|
+
if (this.stopPromise) await this.stopPromise;
|
|
2500
|
+
this.accepting = false;
|
|
2501
|
+
this.stopRecorders();
|
|
2502
|
+
this.lifecycle?.uninstall();
|
|
3153
2503
|
this.controls?.unmount();
|
|
3154
|
-
this.
|
|
3155
|
-
this.sessions.reset();
|
|
3156
|
-
this.transport?.close();
|
|
2504
|
+
await this.store?.close();
|
|
3157
2505
|
this.bus.clear();
|
|
2506
|
+
this.clearRecording();
|
|
2507
|
+
this.destroyed = true;
|
|
3158
2508
|
}
|
|
3159
|
-
/** Dados estruturados para o motor Playwright, construídos a partir do que foi gravado nesta página. */
|
|
3160
2509
|
exportPlaywrightData() {
|
|
3161
|
-
|
|
3162
|
-
if (!s) throw new InvalidStateError("Sem sess\xE3o ativa");
|
|
3163
|
-
return buildPlaywrightContract({
|
|
3164
|
-
session: { sessionId: s.sessionId, executionId: s.executionId, testCaseId: s.testCaseId, platformId: s.platformId },
|
|
3165
|
-
actions: this.actionLog,
|
|
3166
|
-
contexts: this.contextLog.map((e) => e.data)
|
|
3167
|
-
});
|
|
2510
|
+
return buildPlaywrightContract({ actions: this.actionLog, contexts: this.contextLog });
|
|
3168
2511
|
}
|
|
3169
2512
|
getRecordedActions() {
|
|
3170
2513
|
return this.actionLog;
|
|
3171
2514
|
}
|
|
3172
|
-
// ------------------------------------------------------------------ EventSink
|
|
3173
2515
|
on(type, fn) {
|
|
3174
2516
|
return this.bus.on(type, fn);
|
|
3175
2517
|
}
|
|
3176
2518
|
off(type, fn) {
|
|
3177
2519
|
this.bus.off(type, fn);
|
|
3178
2520
|
}
|
|
3179
|
-
/** Usado pelos recorders. Não confundir com os eventos públicos (`on`). */
|
|
3180
2521
|
emit(kind, data, opts = {}) {
|
|
3181
2522
|
return this.record(kind, data, opts);
|
|
3182
2523
|
}
|
|
3183
2524
|
record(kind, data, opts) {
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
}
|
|
3195
|
-
this.
|
|
3196
|
-
const sequence = this.sessions.nextSequence();
|
|
3197
|
-
const timestamp = opts.at ?? Date.now();
|
|
3198
|
-
const id = `${session.tabId}:${sequence}`;
|
|
3199
|
-
let payload = data;
|
|
3200
|
-
let relatedActionId;
|
|
3201
|
-
let relatedActionDeltaMs;
|
|
3202
|
-
if (kind === "action") {
|
|
3203
|
-
const action = { ...data, id, sessionId: session.sessionId, executionId: session.executionId, sequence, timestamp };
|
|
3204
|
-
payload = action;
|
|
3205
|
-
this.recent.push({ id, t: timestamp });
|
|
3206
|
-
if (this.recent.length > 50) this.recent.shift();
|
|
3207
|
-
this.actionLog.push(action);
|
|
3208
|
-
if (this.actionLog.length > MAX_LOG) this.actionLog.shift();
|
|
3209
|
-
} else if (kind === "http" || kind === "console" || kind === "error" || kind === "iframe") {
|
|
3210
|
-
const rel = this.correlate(timestamp);
|
|
3211
|
-
relatedActionId = rel?.id;
|
|
3212
|
-
relatedActionDeltaMs = rel ? timestamp - rel.t : void 0;
|
|
3213
|
-
}
|
|
3214
|
-
const event = {
|
|
3215
|
-
id,
|
|
3216
|
-
kind,
|
|
3217
|
-
sessionId: session.sessionId,
|
|
3218
|
-
executionId: session.executionId,
|
|
3219
|
-
tabId: session.tabId,
|
|
3220
|
-
pageId: session.pageId,
|
|
3221
|
-
sequence,
|
|
3222
|
-
timestamp,
|
|
3223
|
-
relatedActionId,
|
|
3224
|
-
relatedActionDeltaMs,
|
|
3225
|
-
data: payload
|
|
3226
|
-
};
|
|
3227
|
-
if (kind === "context") {
|
|
3228
|
-
this.contextLog.push(event);
|
|
3229
|
-
if (this.contextLog.length > 50) this.contextLog.shift();
|
|
3230
|
-
}
|
|
3231
|
-
this.eventCount++;
|
|
3232
|
-
transport.enqueue(event);
|
|
3233
|
-
this.metrics.emitted++;
|
|
3234
|
-
if (this.config.debug) this.debug("event", kind, sequence);
|
|
3235
|
-
return event;
|
|
3236
|
-
} catch (e) {
|
|
3237
|
-
this.debug("emit failed", e);
|
|
3238
|
-
return null;
|
|
3239
|
-
} finally {
|
|
3240
|
-
if (t0) this.metrics.emitMs += performance.now() - t0;
|
|
2525
|
+
if (!this.accepting) return null;
|
|
2526
|
+
this.rrweb?.recoverIfNeeded();
|
|
2527
|
+
const sequence = ++this.sequence;
|
|
2528
|
+
const timestamp = opts.at ?? Date.now();
|
|
2529
|
+
const id = `${this.pageId}:${sequence}`;
|
|
2530
|
+
let payload = data;
|
|
2531
|
+
if (kind === "action") {
|
|
2532
|
+
const action = { ...data, id, sequence, timestamp };
|
|
2533
|
+
payload = action;
|
|
2534
|
+
this.actionLog.push(action);
|
|
2535
|
+
this.recent.push({ id, t: timestamp });
|
|
2536
|
+
if (this.recent.length > 50) this.recent.shift();
|
|
3241
2537
|
}
|
|
2538
|
+
const related = ["http", "console", "error", "iframe"].includes(kind) ? this.correlate(timestamp) : void 0;
|
|
2539
|
+
const event = {
|
|
2540
|
+
id,
|
|
2541
|
+
kind,
|
|
2542
|
+
tabId: this.pageId,
|
|
2543
|
+
pageId: this.pageId,
|
|
2544
|
+
sequence,
|
|
2545
|
+
timestamp,
|
|
2546
|
+
relatedActionId: related?.id,
|
|
2547
|
+
relatedActionDeltaMs: related ? timestamp - related.t : void 0,
|
|
2548
|
+
data: payload
|
|
2549
|
+
};
|
|
2550
|
+
if (kind === "context") this.contextLog.push(data);
|
|
2551
|
+
this.events.push(event);
|
|
2552
|
+
if (this.store) this.save(this.store.append(event));
|
|
2553
|
+
return event;
|
|
3242
2554
|
}
|
|
3243
2555
|
correlate(at) {
|
|
3244
2556
|
for (let i = this.recent.length - 1; i >= 0; i--) {
|
|
@@ -3254,18 +2566,27 @@ var Offscript = class {
|
|
|
3254
2566
|
attach() {
|
|
3255
2567
|
this.lifecycle ?? (this.lifecycle = new LifecycleManager({
|
|
3256
2568
|
onPageHide: () => {
|
|
3257
|
-
if (!this.accepting) return;
|
|
3258
2569
|
this.actions?.flushAll();
|
|
3259
|
-
|
|
3260
|
-
this.
|
|
2570
|
+
if (!this.store) return;
|
|
2571
|
+
this.accepting = false;
|
|
2572
|
+
this.stopRecorders();
|
|
2573
|
+
this.suspended = true;
|
|
2574
|
+
if (!this.restorePromise) this.store.checkpoint();
|
|
2575
|
+
},
|
|
2576
|
+
onPageShow: () => {
|
|
2577
|
+
if (!this.suspended) return;
|
|
2578
|
+
this.suspended = false;
|
|
2579
|
+
this.setStatus("initializing");
|
|
2580
|
+
this.restorePromise = this.restoreDraft().finally(() => {
|
|
2581
|
+
this.restorePromise = void 0;
|
|
2582
|
+
});
|
|
2583
|
+
this.save(this.restorePromise);
|
|
3261
2584
|
},
|
|
3262
2585
|
onVisibility: (hidden) => {
|
|
3263
|
-
if (hidden
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
}
|
|
2586
|
+
if (hidden) this.actions?.flushAll();
|
|
2587
|
+
},
|
|
2588
|
+
onConnectivity: () => {
|
|
3267
2589
|
},
|
|
3268
|
-
onConnectivity: (online) => this.transport?.setOnline(online),
|
|
3269
2590
|
onNavigation: ({ from, to, kind }) => {
|
|
3270
2591
|
if (!this.accepting) return;
|
|
3271
2592
|
this.actions?.recordNavigation(from, to, kind);
|
|
@@ -3274,16 +2595,14 @@ var Offscript = class {
|
|
|
3274
2595
|
}
|
|
3275
2596
|
}));
|
|
3276
2597
|
this.lifecycle.install();
|
|
3277
|
-
this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
|
|
3278
2598
|
}
|
|
3279
2599
|
async startRecorders() {
|
|
3280
|
-
this.ensureControls();
|
|
3281
2600
|
const r = this.config.recording;
|
|
3282
2601
|
const list = [];
|
|
3283
2602
|
if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
|
|
3284
|
-
if (r.context) list.push(this.context = new ContextRecorder(this
|
|
2603
|
+
if (r.context) list.push(this.context = new ContextRecorder(this));
|
|
3285
2604
|
if (r.actions) list.push(this.actions = new ActionRecorder(this));
|
|
3286
|
-
if (r.http) list.push(new HTTPRecorder(this
|
|
2605
|
+
if (r.http) list.push(new HTTPRecorder(this));
|
|
3287
2606
|
if (r.console || r.errors) list.push(new ErrorRecorder(this));
|
|
3288
2607
|
if (r.iframes) list.push(new IframeRecorder(this));
|
|
3289
2608
|
this.recorders = list;
|
|
@@ -3296,70 +2615,42 @@ var Offscript = class {
|
|
|
3296
2615
|
}
|
|
3297
2616
|
}
|
|
3298
2617
|
}
|
|
2618
|
+
buildPayload() {
|
|
2619
|
+
return {
|
|
2620
|
+
recording: this.events.filter((event) => event.kind !== "http"),
|
|
2621
|
+
request: this.events.filter((event) => event.kind === "http"),
|
|
2622
|
+
regression: this.exportPlaywrightData()
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
3299
2625
|
stopRecorders() {
|
|
3300
2626
|
for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
|
|
3301
2627
|
this.rrweb = this.actions = this.context = void 0;
|
|
3302
2628
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
this.
|
|
3312
|
-
this.
|
|
3313
|
-
this.
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
this.
|
|
3317
|
-
}
|
|
3318
|
-
onHeartbeat(status) {
|
|
3319
|
-
if (status === "disabled") void this.shutdownRemote(new SessionDisabledError());
|
|
3320
|
-
else if (status === "expired") void this.onFatal(new SessionExpiredError());
|
|
3321
|
-
}
|
|
3322
|
-
/** Backend desativou a gravação: encerra com flush de melhor esforço. */
|
|
3323
|
-
async shutdownRemote(err) {
|
|
3324
|
-
if (this.status !== "recording" && this.status !== "paused") return;
|
|
3325
|
-
this.accepting = false;
|
|
3326
|
-
this.stopRecorders();
|
|
3327
|
-
this.sessions.stopHeartbeat();
|
|
3328
|
-
const session = this.sessions.session;
|
|
3329
|
-
await this.transport?.flushFinal(3e3).catch(() => void 0);
|
|
3330
|
-
this.transport?.stop();
|
|
3331
|
-
this.lifecycle?.uninstall();
|
|
3332
|
-
this.sessions.clearPersisted();
|
|
3333
|
-
this.sessions.reset();
|
|
3334
|
-
this.setStatus("stopped");
|
|
3335
|
-
this.bus.emit("disabled", { reason: err.message });
|
|
3336
|
-
if (session) this.bus.emit("sessionEnd", { sessionId: session.sessionId, flushed: false, pendingEvents: this.transport?.pending ?? 0, completed: false });
|
|
3337
|
-
}
|
|
3338
|
-
async onFatal(err) {
|
|
3339
|
-
if (this.status === "error" || this.status === "stopped") return;
|
|
3340
|
-
await this.teardown();
|
|
3341
|
-
this.sessions.clearPersisted();
|
|
3342
|
-
this.sessions.reset();
|
|
3343
|
-
this.fail(err);
|
|
3344
|
-
}
|
|
3345
|
-
fail(e) {
|
|
3346
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
3347
|
-
this.lastError = { code: err.code ?? "INTERNAL", message: err.message };
|
|
3348
|
-
this.setStatus("error");
|
|
3349
|
-
this.bus.emit("error", err);
|
|
2629
|
+
clearRecording() {
|
|
2630
|
+
this.events = [];
|
|
2631
|
+
this.actionLog = [];
|
|
2632
|
+
this.contextLog = [];
|
|
2633
|
+
this.recent = [];
|
|
2634
|
+
this.sequence = 0;
|
|
2635
|
+
this.pageId = uid("page_");
|
|
2636
|
+
this.payload = void 0;
|
|
2637
|
+
this.submitted = false;
|
|
2638
|
+
this.response = void 0;
|
|
2639
|
+
this.lastError = void 0;
|
|
2640
|
+
}
|
|
2641
|
+
assertAlive() {
|
|
2642
|
+
if (this.destroyed) throw new InvalidStateError("SDK destru\xEDdo");
|
|
3350
2643
|
}
|
|
3351
2644
|
setStatus(to) {
|
|
3352
2645
|
const from = this.status;
|
|
3353
2646
|
if (from === to) return;
|
|
3354
2647
|
this.status = to;
|
|
2648
|
+
if (this.store && (to === "recording" || to === "paused" || to === "stopped")) this.save(this.store.setStatus(to));
|
|
3355
2649
|
this.bus.emit("statusChange", { from, to });
|
|
3356
2650
|
}
|
|
3357
|
-
debug(...args) {
|
|
3358
|
-
if (this.config.debug) console.debug("[offscript]", ...args);
|
|
3359
|
-
}
|
|
3360
2651
|
};
|
|
3361
2652
|
Offscript.version = SDK_VERSION;
|
|
3362
2653
|
|
|
3363
|
-
export {
|
|
3364
|
-
//# sourceMappingURL=chunk-
|
|
3365
|
-
//# sourceMappingURL=chunk-
|
|
2654
|
+
export { ConfigError, DEFAULT_PRIVACY, DEFAULT_RECORDING, DataSanitizer, InvalidStateError, Offscript, OffscriptError };
|
|
2655
|
+
//# sourceMappingURL=chunk-5ZBR6FI5.js.map
|
|
2656
|
+
//# sourceMappingURL=chunk-5ZBR6FI5.js.map
|