@offscript/sdk 0.1.1 → 0.1.5

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