@offscript/sdk 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,36 +1,26 @@
1
1
  # @offscript/sdk
2
2
 
3
- Biblioteca de captura no navegador: DOM com rrweb, ações, navegação SPA, HTTP, console, erros, contexto e iframes. Não depende de backend para começar. Por padrão, os dados ficam em memória. Para continuar a mesma gravação entre documentos da mesma origem, o host pode fornecer uma `persistenceKey` estável e exclusiva da aba.
3
+ Biblioteca de captura no navegador: DOM com rrweb, ações, navegação SPA, HTTP, console, erros, contexto e iframes. O SDK lê `offscript_token` da query string, mantém o token e a gravação entre documentos da mesma origem e preserva a URL. O overlay só aparece quando há um token de execução.
4
4
 
5
5
  ```ts
6
6
  import { Offscript } from "@offscript/sdk";
7
7
 
8
8
  const sdk = new Offscript({
9
+ endpoint: "https://painel.example/api/recordings",
9
10
  recording: { rrweb: true, actions: true, http: true, errors: true, iframes: true },
10
11
  });
12
+ await sdk.init();
11
13
  await sdk.startRecording();
12
- const result = await sdk.complete({
13
- endpoint: "https://painel.example/api/recordings",
14
- token: tokenForUpload,
15
- });
14
+ const result = await sdk.complete();
16
15
  ```
17
16
 
18
- `complete` para os recorders e faz um único POST JSON com `Authorization: Bearer <token>`. O destino e o token são fornecidos pelo front-end apenas nesta operação. O SDK não lê tokens da URL nem conhece IDs do domínio do painel.
17
+ `complete()` para os recorders e faz um único POST JSON com `Authorization: Bearer <token>`. O host configura o endpoint; o SDK obtém o token da URL ou da sessão da aba. Também e possível passar `endpoint` e `token` explícitamente a `complete` para integrações personalizadas.
19
18
 
20
- O payload contém `recording` (envelopes rrweb, ações, console, erros, contexto, iframes e ciclo de vida), `request` (envelopes HTTP) e `regression` (contrato Playwright com condições iniciais e passos). Não há transporte durante a captura nem reenvio automático. Chamadas concorrentes de `complete` compartilham o envio; após sucesso, chamadas repetidas retornam o resultado. Após falha, os dados continuam disponíveis para uma nova chamada explícita.
19
+ O payload contém `recording` (envelopes rrweb, ações, console, erros, contexto, iframes e ciclo de vida), `request` (envelopes HTTP) e `regression` (contrato Playwright com condições iniciais e passos). Não ha transporte durante a captura nem reenvio automático. Chamadas concorrentes de `complete` compartilham o envio; após sucesso, chamadas repetidas retornam o resultado. Após falha, os dados continuam disponíveis para uma nova chamada explícita.
21
20
 
22
21
  `stopRecording()` encerra localmente e devolve o payload sem publicar. `pauseRecording()` e `resumeRecording()` controlam a captura; retomar gera um snapshot completo. `discardRecording()` limpa os dados e volta ao estado inicial. `destroy()` libera a instrumentação sem enviar dados.
23
22
 
24
- O overlay aparece em `init()` quando `controls` está habilitado (padrão). Iniciar grava localmente. Para publicar ao clicar em Finalizar, configure o callback do host:
25
-
26
- ```ts
27
- const sdk = new Offscript({
28
- onComplete: () => sdk.complete({ endpoint: destination, token: uploadToken }),
29
- });
30
- await sdk.init();
31
- ```
32
-
33
- Sem callback, Finalizar apenas encerra localmente. `getState()` retorna `{ status, pending, lastError }`. Eventos públicos: `statusChange`, `error`, `recordingStart` e `recordingEnd` (captura encerrada, antes do upload).
23
+ Com `controls` habilitado (padrão), o overlay aparece em `init()` somente quando existe um token de execução na URL ou na sessão da aba. Iniciar grava localmente; Finalizar envia a gravação ao endpoint configurado usando o token do SDK. `onComplete` continua disponível para integrações que precisem substituir esse comportamento.
34
24
 
35
25
  As opções `recording` e `privacy` estão tipadas em `src/types/config.ts`. `captureSensitiveValues` vem desabilitada; habilitá-la inclui valores sensíveis na captura. Configure máscaras, seletores bloqueados e URLs ignoradas conforme a aplicação.
36
26
 
@@ -40,8 +30,8 @@ Validação: `npm run typecheck`, `npm test`, `npm run build`, `npm run test:e2e
40
30
 
41
31
  ## Continuidade entre páginas
42
32
 
43
- Configure `persistenceKey` com uma chave opaca criada pelo host e mantida em `sessionStorage` na aba. Com a mesma chave em cada página, `init()` restaura o rascunho e retoma a captura quando ela estava ativa, preserva a pausa ou disponibiliza a gravação encerrada para envio. Sem essa opção, a captura continua apenas em memória.
33
+ Em sessões iniciadas com token, o SDK cria e mantém automaticamente uma chave opaca de rascunho em `sessionStorage`. Com ela, `init()` restaura o rascunho e retoma a captura quando ela estava ativa, preserva a pausa ou disponibiliza a gravação encerrada para envio. `persistenceKey` continua opcional para integrações que queiram controlar essa chave.
44
34
 
45
- Os eventos ficam no IndexedDB da origem. No descarregamento do documento, os últimos eventos ainda não confirmados são guardados em `sessionStorage` para a próxima página. `complete()` limpa o rascunho após o HTTP de sucesso; `discardRecording()` também o remove. `destroy()` fecha o armazenamento e libera a instrumentação, preservando o rascunho local.
35
+ Os eventos ficam no IndexedDB da origem. No descarregamento do documento, os últimos eventos ainda não confirmados são guardados em `sessionStorage` para a próxima pagina. `complete()` limpa o rascunho após HTTP de sucesso; `discardRecording()` também o remove. `destroy()` fecha o armazenamento e libera a instrumentação, preservando o rascunho local.
46
36
 
47
- O token continua sob responsabilidade do host. A bancada o recebe pela query, guarda na aba e remove o parâmetro da URL; páginas seguintes usam o valor da aba. Após envio bem-sucedido, a bancada apaga token e chave e fecha o overlay. Falhas mantêm os dados para nova tentativa. A continuidade exige a mesma origem e SDK inicializado em cada documento; armazenamento de navegador não é compartilhado entre domínios ou portas diferentes.
37
+ O SDK lê o token de `offscript_token`, mantém a query na URL e guarda a credencial na sessão da aba para continuar após navegações. Após envio bem-sucedido, o SDK limpa o rascunho e não reabre o overlay nessa sessão; falhas mantém os dados para nova tentativa. A continuidade exige a mesma origem e o SDK inicializado em cada documento; armazenamento de navegador não é compartilhado entre domínios ou portas diferentes.
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var chunkVYCDQLP7_cjs = require('../chunk-VYCDQLP7.cjs');
4
- require('../chunk-HPDLEAAL.cjs');
3
+ var chunkPCJRHW66_cjs = require('../chunk-PCJRHW66.cjs');
4
+ require('../chunk-MZXF2LF2.cjs');
5
5
 
6
6
  // src/adapters/nuxt.ts
7
7
  function createNuxtOffscript(config) {
8
8
  if (typeof window === "undefined") return null;
9
- const sdk = new chunkVYCDQLP7_cjs.Offscript(config);
9
+ const sdk = new chunkPCJRHW66_cjs.Offscript(config);
10
10
  void sdk.init().catch(() => void 0);
11
11
  return sdk;
12
12
  }
@@ -1,5 +1,5 @@
1
- import { Offscript } from '../chunk-XBXFGCUT.js';
2
- import '../chunk-6ROP37WI.js';
1
+ import { Offscript } from '../chunk-5ZBR6FI5.js';
2
+ import '../chunk-KHKIWCLM.js';
3
3
 
4
4
  // src/adapters/nuxt.ts
5
5
  function createNuxtOffscript(config) {
@@ -1,18 +1,18 @@
1
1
  'use strict';
2
2
 
3
- var chunk2MY2FWIA_cjs = require('../chunk-2MY2FWIA.cjs');
4
- require('../chunk-VYCDQLP7.cjs');
5
- require('../chunk-HPDLEAAL.cjs');
3
+ var chunkRAUSXUGS_cjs = require('../chunk-RAUSXUGS.cjs');
4
+ require('../chunk-PCJRHW66.cjs');
5
+ require('../chunk-MZXF2LF2.cjs');
6
6
 
7
7
 
8
8
 
9
9
  Object.defineProperty(exports, "createOffscript", {
10
10
  enumerable: true,
11
- get: function () { return chunk2MY2FWIA_cjs.createOffscript; }
11
+ get: function () { return chunkRAUSXUGS_cjs.createOffscript; }
12
12
  });
13
13
  Object.defineProperty(exports, "startOffscript", {
14
14
  enumerable: true,
15
- get: function () { return chunk2MY2FWIA_cjs.startOffscript; }
15
+ get: function () { return chunkRAUSXUGS_cjs.startOffscript; }
16
16
  });
17
17
  //# sourceMappingURL=vanilla.cjs.map
18
18
  //# sourceMappingURL=vanilla.cjs.map
@@ -1,5 +1,5 @@
1
- export { createOffscript, startOffscript } from '../chunk-V2YYWGIA.js';
2
- import '../chunk-XBXFGCUT.js';
3
- import '../chunk-6ROP37WI.js';
1
+ export { createOffscript, startOffscript } from '../chunk-R2PPPR4K.js';
2
+ import '../chunk-5ZBR6FI5.js';
3
+ import '../chunk-KHKIWCLM.js';
4
4
  //# sourceMappingURL=vanilla.js.map
5
5
  //# sourceMappingURL=vanilla.js.map
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var chunkVYCDQLP7_cjs = require('../chunk-VYCDQLP7.cjs');
4
- require('../chunk-HPDLEAAL.cjs');
3
+ var chunkPCJRHW66_cjs = require('../chunk-PCJRHW66.cjs');
4
+ require('../chunk-MZXF2LF2.cjs');
5
5
 
6
6
  // src/adapters/vue.ts
7
7
  var OFFSCRIPT_KEY = "$offscript";
8
8
  var OffscriptPlugin = {
9
9
  install(app, config) {
10
- const sdk = new chunkVYCDQLP7_cjs.Offscript(config);
10
+ const sdk = new chunkPCJRHW66_cjs.Offscript(config);
11
11
  void sdk.init().catch(() => void 0);
12
12
  if (app.config?.globalProperties) {
13
13
  app.config.globalProperties[OFFSCRIPT_KEY] = sdk;
@@ -1,5 +1,5 @@
1
- import { Offscript } from '../chunk-XBXFGCUT.js';
2
- import '../chunk-6ROP37WI.js';
1
+ import { Offscript } from '../chunk-5ZBR6FI5.js';
2
+ import '../chunk-KHKIWCLM.js';
3
3
 
4
4
  // src/adapters/vue.ts
5
5
  var OFFSCRIPT_KEY = "$offscript";
@@ -1,4 +1,4 @@
1
- import { buildPlaywrightContract, SDK_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-6ROP37WI.js';
1
+ import { buildPlaywrightContract, SDK_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-KHKIWCLM.js';
2
2
 
3
3
  // src/types/config.ts
4
4
  var DEFAULT_RECORDING = {
@@ -1531,6 +1531,7 @@ function resolveConfig(input) {
1531
1531
  privacy: resolvePrivacy(input.privacy),
1532
1532
  correlationWindowMs: input.correlationWindowMs ?? 3e3,
1533
1533
  controls: input.controls ?? true,
1534
+ endpoint: input.endpoint,
1534
1535
  persistenceKey: input.persistenceKey,
1535
1536
  onComplete: input.onComplete
1536
1537
  };
@@ -2209,6 +2210,19 @@ var SUCCESS_TEMPLATE = `
2209
2210
  `;
2210
2211
 
2211
2212
  // src/core/OffscriptSDK.ts
2213
+ var TOKEN_STORAGE_KEY = "offscript-session-token";
2214
+ var DRAFT_STORAGE_KEY = "offscript-draft-key";
2215
+ var COMPLETED_TOKEN_STORAGE_KEY = "offscript-completed-token";
2216
+ function tokenFingerprint(token) {
2217
+ let first = 2166136261;
2218
+ let second = 2654435769;
2219
+ for (let index = 0; index < token.length; index++) {
2220
+ const character = token.charCodeAt(index);
2221
+ first = Math.imul(first ^ character, 16777619);
2222
+ second = Math.imul(second ^ character, 2246822519);
2223
+ }
2224
+ return `${first >>> 0}.${second >>> 0}`;
2225
+ }
2212
2226
  var Offscript = class {
2213
2227
  constructor(config = {}) {
2214
2228
  this.bus = new Emitter();
@@ -2224,6 +2238,7 @@ var Offscript = class {
2224
2238
  this.sequence = 0;
2225
2239
  this.pageId = uid("page_");
2226
2240
  this.submitted = false;
2241
+ this.launchTokenChanged = false;
2227
2242
  this.config = resolveConfig(config);
2228
2243
  this.sanitizer = new DataSanitizer(this.config.privacy);
2229
2244
  }
@@ -2236,17 +2251,70 @@ var Offscript = class {
2236
2251
  async init() {
2237
2252
  this.assertAlive();
2238
2253
  if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um navegador");
2239
- this.initPromise ?? (this.initPromise = this.initializeDraft());
2254
+ this.initPromise ?? (this.initPromise = this.initialize());
2240
2255
  await this.initPromise;
2241
2256
  if (this.restorePromise) await this.restorePromise;
2242
- if (this.config.controls && !this.controls) {
2257
+ if (this.config.controls && this.launchToken && !this.controls) {
2243
2258
  this.controls = new RecordingControls(this);
2244
2259
  this.controls.mount();
2245
2260
  }
2246
2261
  }
2262
+ async initialize() {
2263
+ this.resolveLaunchToken();
2264
+ if (this.launchToken && !this.config.persistenceKey) {
2265
+ this.config.persistenceKey = this.getOrCreateDraftKey();
2266
+ }
2267
+ await this.initializeDraft();
2268
+ }
2269
+ resolveLaunchToken() {
2270
+ const incomingToken = new URLSearchParams(window.location.search).get("offscript_token")?.trim();
2271
+ const previousToken = this.readSessionValue(TOKEN_STORAGE_KEY);
2272
+ const completedToken = this.readSessionValue(COMPLETED_TOKEN_STORAGE_KEY);
2273
+ const legacyCompletion = completedToken === "1" && Boolean(previousToken);
2274
+ const completedFingerprint = legacyCompletion && previousToken ? tokenFingerprint(previousToken) : completedToken;
2275
+ const candidateToken = incomingToken || previousToken || void 0;
2276
+ this.launchTokenChanged = Boolean(incomingToken && previousToken && incomingToken !== previousToken) || Boolean(incomingToken && completedFingerprint && completedFingerprint !== "1" && tokenFingerprint(incomingToken) !== completedFingerprint);
2277
+ if (this.launchTokenChanged) {
2278
+ this.removeSessionValue(COMPLETED_TOKEN_STORAGE_KEY);
2279
+ }
2280
+ if (legacyCompletion && previousToken && !this.launchTokenChanged) {
2281
+ this.writeSessionValue(COMPLETED_TOKEN_STORAGE_KEY, tokenFingerprint(previousToken));
2282
+ this.removeSessionValue(TOKEN_STORAGE_KEY);
2283
+ }
2284
+ const isCompleted = Boolean(candidateToken && completedFingerprint === tokenFingerprint(candidateToken));
2285
+ this.launchToken = isCompleted ? void 0 : candidateToken;
2286
+ if (incomingToken && !isCompleted) this.writeSessionValue(TOKEN_STORAGE_KEY, incomingToken);
2287
+ }
2288
+ getOrCreateDraftKey() {
2289
+ const saved = this.readSessionValue(DRAFT_STORAGE_KEY);
2290
+ if (saved) return saved;
2291
+ const key = uid("draft_");
2292
+ this.writeSessionValue(DRAFT_STORAGE_KEY, key);
2293
+ return key;
2294
+ }
2295
+ readSessionValue(key) {
2296
+ try {
2297
+ return window.sessionStorage.getItem(key);
2298
+ } catch {
2299
+ return null;
2300
+ }
2301
+ }
2302
+ writeSessionValue(key, value) {
2303
+ try {
2304
+ window.sessionStorage.setItem(key, value);
2305
+ } catch {
2306
+ }
2307
+ }
2308
+ removeSessionValue(key) {
2309
+ try {
2310
+ window.sessionStorage.removeItem(key);
2311
+ } catch {
2312
+ }
2313
+ }
2247
2314
  async initializeDraft() {
2248
2315
  if (!this.config.persistenceKey) return;
2249
2316
  this.store = await RecordingStore.open(this.config.persistenceKey);
2317
+ if (this.launchTokenChanged) await this.store.clear();
2250
2318
  await this.restoreDraft();
2251
2319
  }
2252
2320
  async restoreDraft() {
@@ -2356,7 +2424,7 @@ var Offscript = class {
2356
2424
  this.bus.emit("recordingEnd", this.payload);
2357
2425
  return this.payload;
2358
2426
  }
2359
- complete(options) {
2427
+ complete(options = {}) {
2360
2428
  this.assertAlive();
2361
2429
  if (this.completePromise) return this.completePromise;
2362
2430
  if (this.submitted) return Promise.resolve(this.response);
@@ -2365,19 +2433,27 @@ var Offscript = class {
2365
2433
  });
2366
2434
  return this.completePromise;
2367
2435
  }
2368
- async doComplete({ endpoint, token }) {
2436
+ async doComplete(options) {
2437
+ const endpoint = options.endpoint ?? this.config.endpoint;
2438
+ const token = options.token?.trim() || this.launchToken;
2439
+ if (!endpoint) throw new ConfigError("Endpoint HTTP obrigat\xF3rio para envio");
2440
+ if (!token) throw new ConfigError("Token obrigat\xF3rio para envio");
2369
2441
  const url = new URL(endpoint, location.href);
2370
2442
  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
2443
  const payload = await this.stopRecording();
2373
2444
  try {
2374
2445
  const response = await fetch(url.href, {
2375
2446
  method: "POST",
2376
- headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${token.trim()}` },
2447
+ headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${token}` },
2377
2448
  body: JSON.stringify(payload)
2378
2449
  });
2379
2450
  if (!response.ok) throw new OffscriptError("TRANSPORT_FAILED", `Falha ao enviar grava\xE7\xE3o: HTTP ${response.status}`, { status: response.status });
2380
2451
  this.submitted = true;
2452
+ const completedToken = this.launchToken ?? token;
2453
+ this.writeSessionValue(COMPLETED_TOKEN_STORAGE_KEY, tokenFingerprint(completedToken));
2454
+ this.removeSessionValue(TOKEN_STORAGE_KEY);
2455
+ this.removeSessionValue(DRAFT_STORAGE_KEY);
2456
+ this.launchToken = void 0;
2381
2457
  await this.store?.clear();
2382
2458
  const text = await response.text();
2383
2459
  try {
@@ -2387,6 +2463,8 @@ var Offscript = class {
2387
2463
  }
2388
2464
  this.lastError = void 0;
2389
2465
  this.controls?.showSuccess();
2466
+ this.controls?.unmount();
2467
+ this.controls = void 0;
2390
2468
  return this.response;
2391
2469
  } catch (error) {
2392
2470
  const err = error instanceof Error ? error : new Error(String(error));
@@ -2397,6 +2475,7 @@ var Offscript = class {
2397
2475
  }
2398
2476
  async finishRecording() {
2399
2477
  if (this.config.onComplete) return this.config.onComplete();
2478
+ if (this.launchToken) return this.complete();
2400
2479
  return this.stopRecording();
2401
2480
  }
2402
2481
  async discardRecording() {
@@ -2573,5 +2652,5 @@ var Offscript = class {
2573
2652
  Offscript.version = SDK_VERSION;
2574
2653
 
2575
2654
  export { ConfigError, DEFAULT_PRIVACY, DEFAULT_RECORDING, DataSanitizer, InvalidStateError, Offscript, OffscriptError };
2576
- //# sourceMappingURL=chunk-XBXFGCUT.js.map
2577
- //# sourceMappingURL=chunk-XBXFGCUT.js.map
2655
+ //# sourceMappingURL=chunk-5ZBR6FI5.js.map
2656
+ //# sourceMappingURL=chunk-5ZBR6FI5.js.map