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