@kharko/dozor 0.2.0 → 0.3.0
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 +94 -64
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,42 +33,36 @@ Creates and returns a singleton recorder instance. Calling `init()` multiple tim
|
|
|
33
33
|
```ts
|
|
34
34
|
const dozor = Dozor.init({
|
|
35
35
|
apiKey: "dp_your_public_key",
|
|
36
|
-
endpoint: "https://dozor.kharko.dev/api/ingest",
|
|
37
|
-
flushInterval: 10_000,
|
|
38
|
-
batchSize: 500,
|
|
39
|
-
autoStart: true,
|
|
40
|
-
hold: false,
|
|
41
36
|
userId: "user_123",
|
|
42
|
-
|
|
43
|
-
recordConsole: true,
|
|
37
|
+
privacyBlockMedia: true,
|
|
44
38
|
});
|
|
45
39
|
```
|
|
46
40
|
|
|
47
41
|
#### Options
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
- `apiKey` (`string`) — **Required.** Public project API key (`dp_...`).
|
|
44
|
+
- `endpoint` (`string`) — Ingest endpoint URL. Default: production endpoint. Override for self-hosted setups.
|
|
45
|
+
- `flushInterval` (`number`) — How often to flush buffered events (ms). Default: `10000`.
|
|
46
|
+
- `batchSize` (`number`) — Max events in the buffer before an automatic flush. Default: `500`.
|
|
47
|
+
- `autoStart` (`boolean`) — Start recording immediately on init. Default: `true`.
|
|
48
|
+
- `hold` (`boolean`) — Start with transport held — events are buffered but not sent until `release()`. Default: `false`.
|
|
49
|
+
- `userId` (`string`) — Stable user identifier for cross-session analytics. Can also be set later via `setUserId()`.
|
|
50
|
+
- `pauseOnHidden` (`boolean`) — Auto-pause when the tab is hidden, resume when visible. Default: `true`.
|
|
51
|
+
- `recordConsole` (`boolean`) — Record `console.log/warn/error/info/debug` calls. Default: `true`.
|
|
52
|
+
- `privacyMaskAttribute` (`string`) — HTML attribute for text masking. Elements and descendants have text replaced with `***`. Default: `"data-dozor-mask"`.
|
|
53
|
+
- `privacyBlockAttribute` (`string`) — HTML attribute for element blocking. Element is replaced with a same-size placeholder. Default: `"data-dozor-block"`.
|
|
54
|
+
- `privacyBlockMedia` (`boolean`) — Replace all media (`img`, `video`, `audio`, etc.) with placeholders. Default: `false`.
|
|
55
|
+
- `privacyMaskInputs` (`boolean`) — Mask all input/textarea/select values with `*`. Default: `true`.
|
|
60
56
|
|
|
61
57
|
### Instance properties
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
| `userId` | `string \| null` | Current user ID, or `null` if not set. |
|
|
71
|
-
| `bufferSize` | `number` | Number of events currently buffered in memory (not yet sent). Useful for debugging and monitoring buffer growth during `hold()`. |
|
|
59
|
+
- `sessionId` (`string`) — Current session ID (UUID v4, stored in `sessionStorage`).
|
|
60
|
+
- `state` (`DozorState`) — Current lifecycle state: `"idle"`, `"recording"`, `"paused"`, or `"stopped"`.
|
|
61
|
+
- `isRecording` (`boolean`) — `true` when actively recording.
|
|
62
|
+
- `isPaused` (`boolean`) — `true` when paused via `pause()`.
|
|
63
|
+
- `isHeld` (`boolean`) — `true` when transport is held — events are buffered but not sent.
|
|
64
|
+
- `userId` (`string | null`) — Current user ID, or `null` if not set.
|
|
65
|
+
- `bufferSize` (`number`) — Number of events currently buffered in memory (not yet sent).
|
|
72
66
|
|
|
73
67
|
### Instance methods
|
|
74
68
|
|
|
@@ -143,9 +137,7 @@ dozor.release();
|
|
|
143
137
|
dozor.release({ discard: true });
|
|
144
138
|
```
|
|
145
139
|
|
|
146
|
-
|
|
147
|
-
| --------- | --------- | ------- | ------------------------------------------ |
|
|
148
|
-
| `discard` | `boolean` | `false` | Drop held events instead of flushing them. |
|
|
140
|
+
- `discard` (`boolean`) — Drop held events instead of flushing them. Default: `false`.
|
|
149
141
|
|
|
150
142
|
#### `dozor.setUserId(id)`
|
|
151
143
|
|
|
@@ -252,6 +244,50 @@ onLogin((user) => {
|
|
|
252
244
|
});
|
|
253
245
|
```
|
|
254
246
|
|
|
247
|
+
### Mask sensitive text
|
|
248
|
+
|
|
249
|
+
Add the `data-dozor-mask` attribute to any element whose text content should be replaced with asterisks in the recording. All descendant text is masked too.
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<div data-dozor-mask>
|
|
253
|
+
<p>John Doe</p> <!-- recorded as "********" -->
|
|
254
|
+
<span>+1 555-0123</span> <!-- recorded as "************" -->
|
|
255
|
+
</div>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
You can customize the attribute name:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
Dozor.init({ apiKey: "dp_your_key", privacyMaskAttribute: "data-private" });
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Block elements entirely
|
|
265
|
+
|
|
266
|
+
Add the `data-dozor-block` attribute to elements that should be completely hidden from the recording. The element is replaced with an empty placeholder of the same size — no content is captured.
|
|
267
|
+
|
|
268
|
+
```html
|
|
269
|
+
<img data-dozor-block src="/user-avatar.jpg" />
|
|
270
|
+
<div data-dozor-block>
|
|
271
|
+
<p>This content will not appear in the replay.</p>
|
|
272
|
+
</div>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Block all media
|
|
276
|
+
|
|
277
|
+
Replace all images, videos, and other media with placeholders. Useful when the recorded site serves media behind auth cookies or CORS restrictions that break during replay.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
Dozor.init({ apiKey: "dp_your_key", privacyBlockMedia: true });
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Allow input recording
|
|
284
|
+
|
|
285
|
+
Input values are masked by default. If you need to capture what users type (e.g., a search box in an internal tool), disable input masking:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
Dozor.init({ apiKey: "dp_your_key", privacyMaskInputs: false });
|
|
289
|
+
```
|
|
290
|
+
|
|
255
291
|
### Pause during sensitive input
|
|
256
292
|
|
|
257
293
|
```ts
|
|
@@ -280,31 +316,28 @@ Dozor.init({ apiKey: "dp_your_key", pauseOnHidden: false });
|
|
|
280
316
|
|
|
281
317
|
## Edge cases
|
|
282
318
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
| `CompressionStream` unavailable | Falls back to uncompressed JSON. No errors thrown. |
|
|
306
|
-
| Server returns 4xx | Request is not retried (invalid key, bad payload, etc.). |
|
|
307
|
-
| Server returns 5xx or network error | Retried up to 3 times with exponential backoff (1s, 2s, 4s). |
|
|
319
|
+
- `init()` called multiple times — returns the existing singleton, does not re-initialize.
|
|
320
|
+
- `start()` when already recording — no-op.
|
|
321
|
+
- `pause()` when not recording — no-op.
|
|
322
|
+
- `resume()` when not paused — no-op.
|
|
323
|
+
- `stop()` when already stopped — no-op.
|
|
324
|
+
- `cancel()` when already stopped — no-op.
|
|
325
|
+
- `hold()` when already held or stopped — no-op.
|
|
326
|
+
- `release()` when not held — no-op.
|
|
327
|
+
- `stop()` while held — releases hold, flushes all events, destroys instance. No data is lost.
|
|
328
|
+
- `cancel()` while held — drops buffer, deletes session. Held events are discarded.
|
|
329
|
+
- Tab hidden with `pauseOnHidden: true` (default) — recording pauses automatically, resumes when the tab becomes visible.
|
|
330
|
+
- Tab hidden after manual `pause()` — auto-resume does **not** kick in. Only `resume()` can resume.
|
|
331
|
+
- Tab hidden with `pauseOnHidden: false` — no auto-pause, events keep being recorded and flushed normally.
|
|
332
|
+
- Page unload while held — events are **not** sent. The hold is respected.
|
|
333
|
+
- Page unload while recording — final events are sent via `fetch()` with `keepalive: true`.
|
|
334
|
+
- Tab goes to background — buffer is flushed immediately (unless held).
|
|
335
|
+
- `setUserId()` after metadata was already sent — triggers a metadata re-send on the next flush.
|
|
336
|
+
- `setUserId()` before any flush — user ID is included in the first metadata payload.
|
|
337
|
+
- `sessionStorage` unavailable — session ID is generated in memory but not persisted across reloads.
|
|
338
|
+
- `CompressionStream` unavailable — falls back to uncompressed JSON. No errors thrown.
|
|
339
|
+
- Server returns 4xx — request is not retried (invalid key, bad payload, etc.).
|
|
340
|
+
- Server returns 5xx or network error — retried up to 3 times with exponential backoff (1s, 2s, 4s).
|
|
308
341
|
|
|
309
342
|
## How it works
|
|
310
343
|
|
|
@@ -341,15 +374,12 @@ Payloads larger than 1 KB are compressed with gzip via the browser-native [Compr
|
|
|
341
374
|
|
|
342
375
|
The first batch of each session includes browser metadata:
|
|
343
376
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
| `screenHeight` | `1080` |
|
|
351
|
-
| `language` | `en-US` |
|
|
352
|
-
| `userId` | `user_123` (if set) |
|
|
377
|
+
- `url` — current page URL
|
|
378
|
+
- `referrer` — referrer URL
|
|
379
|
+
- `userAgent` — browser user agent string
|
|
380
|
+
- `screenWidth` / `screenHeight` — screen dimensions
|
|
381
|
+
- `language` — browser language
|
|
382
|
+
- `userId` — user ID (if set)
|
|
353
383
|
|
|
354
384
|
## Types
|
|
355
385
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
'use strict';var rrweb=require('rrweb'),rrwebPluginConsoleRecord=require('@rrweb/rrweb-plugin-console-record');function f(s){let t={url:location.href,referrer:document.referrer,userAgent:navigator.userAgent,screenWidth:screen.width,screenHeight:screen.height,language:navigator.language};return s&&(t.userId=s),t}var u="dozor_session_id";function g(){try{let t=sessionStorage.getItem(u);if(t)return t}catch{}let s=crypto.randomUUID();try{sessionStorage.setItem(u,s);}catch{}return s}function
|
|
2
|
-
exports.Dozor=
|
|
1
|
+
'use strict';var rrweb=require('rrweb'),rrwebPluginConsoleRecord=require('@rrweb/rrweb-plugin-console-record');function f(s){let t={url:location.href,referrer:document.referrer,userAgent:navigator.userAgent,screenWidth:screen.width,screenHeight:screen.height,language:navigator.language};return s&&(t.userId=s),t}var u="dozor_session_id";function g(){try{let t=sessionStorage.getItem(u);if(t)return t}catch{}let s=crypto.randomUUID();try{sessionStorage.setItem(u,s);}catch{}return s}function v(){try{sessionStorage.removeItem(u);}catch{}}async function m(s){let t=new Blob([s]).stream().pipeThrough(new CompressionStream("gzip"));return new Response(t).blob()}var _=typeof CompressionStream<"u",h=class{constructor(t,e){this.endpoint=t,this.apiKey=e;}async send(t,e,r){let n={sessionId:t,events:e};r&&(n.metadata=r);let a=JSON.stringify(n),o,p={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&a.length>1024?(o=await m(a),p["Content-Encoding"]="gzip"):o=a;for(let d=0;d<3;d++){try{let l=await fetch(this.endpoint,{method:"POST",headers:p,body:o,keepalive:!0});if(l.ok)return !0;if(l.status>=400&&l.status<500)return !1}catch{}d<2&&await y(1e3*2**d);}return false}deleteSession(t){let e=this.endpoint.replace("/ingest","/sessions/cancel");try{fetch(e,{method:"POST",headers:{"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey},body:JSON.stringify({sessionId:t})}).catch(()=>{});}catch{}}async sendKeepalive(t,e){if(e.length===0)return;let n=JSON.stringify({sessionId:t,events:e}),a,o={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&n.length>1024?(a=await m(n),o["Content-Encoding"]="gzip"):a=n;try{fetch(this.endpoint,{method:"POST",headers:o,body:a,keepalive:!0}).catch(()=>{});}catch{}}};function y(s){return new Promise(t=>setTimeout(t,s))}var I="https://kharko-dozor.vercel.app/api/ingest",R=1e4,T=500,i=class i{constructor(t){this.buffer=[];this.metadataSent=false;this.flushTimer=null;this.stopRecording=null;this._autoPaused=false;let e=t.endpoint??I;this.batchSize=t.batchSize??T,this.flushInterval=t.flushInterval??R;let r=t.autoStart??true;this._isHeld=t.hold??false,this._userId=t.userId??null,this._pauseOnHidden=t.pauseOnHidden??true,this._privacyMaskAttribute=t.privacyMaskAttribute??"data-dozor-mask",this._privacyBlockAttribute=t.privacyBlockAttribute??"data-dozor-block",this._privacyBlockMedia=t.privacyBlockMedia??false,this._privacyMaskInputs=t.privacyMaskInputs??true,this._plugins=[],t.recordConsole!==false&&this._plugins.push(rrwebPluginConsoleRecord.getRecordConsolePlugin()),this.transport=new h(e,t.apiKey),this._sessionId=g(),this.metadata=f(this._userId),addEventListener("beforeunload",()=>this.onBeforeUnload()),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.flush(),this._pauseOnHidden&&this._state==="recording"&&(this.teardownRecording(),this._state="paused",this._autoPaused=true)):this._pauseOnHidden&&this._autoPaused&&this._state==="paused"&&(this._autoPaused=false,this.startRecording(),this._state="recording");}),r?(this.startRecording(),this._state="recording"):this._state="idle";}static init(t){return i.instance||(i.instance=new i(t)),i.instance}get sessionId(){return this._sessionId}get isRecording(){return this._state==="recording"}get isPaused(){return this._state==="paused"}get state(){return this._state}get isHeld(){return this._isHeld}get userId(){return this._userId}get bufferSize(){return this.buffer.length}start(){this._state==="idle"&&(this.startRecording(),this._state="recording");}pause(){this._state==="recording"&&(this.teardownRecording(),this._state="paused",this._autoPaused=false);}resume(){this._state==="paused"&&(this._autoPaused=false,this.startRecording(),this._state="recording");}stop(){this._state!=="stopped"&&(this._isHeld=false,this.teardownRecording(),this.flush(),this.destroy());}cancel(){this._state!=="stopped"&&(this.teardownRecording(),this.buffer=[],this.transport.deleteSession(this._sessionId),this.destroy());}hold(){this._state==="stopped"||this._isHeld||(this._isHeld=true);}release(t){this._isHeld&&(this._isHeld=false,t?.discard?this.buffer=[]:this.flush());}setUserId(t){this._userId=t,this.metadata&&(this.metadata.userId=t),this.metadataSent&&(this.metadataSent=false);}startRecording(){let t=[`[${this._privacyBlockAttribute}]`];this._privacyBlockMedia&&t.push("img","video","audio","picture","canvas","embed","object");let e=this._privacyMaskAttribute;this.stopRecording=rrweb.record({emit:r=>this.onEvent(r),plugins:this._plugins,maskTextSelector:`[${e}], [${e}] *`,blockSelector:t.join(","),maskAllInputs:this._privacyMaskInputs})??null,this.flushTimer=setInterval(()=>this.flush(),this.flushInterval);}teardownRecording(){this.stopRecording&&(this.stopRecording(),this.stopRecording=null),this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null);}destroy(){v(),i.instance=null,this._state="stopped";}onEvent(t){this.buffer.push(t),this.buffer.length>=this.batchSize&&this.flush();}flush(){if(this._isHeld||this.buffer.length===0)return;let t=this.buffer;this.buffer=[];let e=this.metadataSent?void 0:this.metadata??void 0;e&&(this.metadataSent=true),this.transport.send(this._sessionId,t,e).catch(()=>{});}onBeforeUnload(){if(this._state==="stopped"||this._isHeld||this.buffer.length===0)return;let t=this.buffer;this.buffer=[],this.transport.sendKeepalive(this._sessionId,t);}};i.instance=null;var c=i;
|
|
2
|
+
exports.Dozor=c;//# sourceMappingURL=index.cjs.map
|
|
3
3
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/metadata.ts","../src/session.ts","../src/transport.ts","../src/recorder.ts"],"names":["collectMetadata","userId","meta","SESSION_KEY","getSessionId","existing","id","clearSessionId","gzipCompress","input","stream","supportsCompression","Transport","endpoint","apiKey","sessionId","events","metadata","payload","json","body","headers","attempt","res","sleep","cancelUrl","ms","resolve","DEFAULT_ENDPOINT","DEFAULT_FLUSH_INTERVAL","DEFAULT_BATCH_SIZE","_Dozor","options","autoStart","getRecordConsolePlugin","record","event","Dozor"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { SessionMetadata } from \"./types.js\";\n\n/** Collect session metadata from the browser environment. */\nexport function collectMetadata(userId?: string | null): SessionMetadata {\n const meta: SessionMetadata = {\n url: location.href,\n referrer: document.referrer,\n userAgent: navigator.userAgent,\n screenWidth: screen.width,\n screenHeight: screen.height,\n language: navigator.language,\n };\n if (userId) meta.userId = userId;\n return meta;\n}\n","const SESSION_KEY = \"dozor_session_id\";\n\n/** Get or create a session ID persisted in sessionStorage for SPA continuity. */\nexport function getSessionId(): string {\n try {\n const existing = sessionStorage.getItem(SESSION_KEY);\n if (existing) return existing;\n } catch {\n // sessionStorage unavailable (SSR, iframe sandbox, etc.)\n }\n\n const id = crypto.randomUUID();\n\n try {\n sessionStorage.setItem(SESSION_KEY, id);\n } catch {\n // best-effort persistence\n }\n\n return id;\n}\n\n/** Remove the session ID from sessionStorage so the next init() creates a fresh session. */\nexport function clearSessionId(): void {\n try {\n sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // best-effort\n }\n}\n","import type { eventWithTime } from \"rrweb\";\nimport type { IngestPayload, SessionMetadata } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst COMPRESSION_THRESHOLD = 1_024;\n\n/** Compress a string to gzip using CompressionStream API. */\nasync function gzipCompress(input: string): Promise<Blob> {\n const stream = new Blob([input]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Response(stream).blob();\n}\n\n/** Check if CompressionStream is available in this environment. */\nconst supportsCompression = typeof CompressionStream !== \"undefined\";\n\nexport class Transport {\n private endpoint: string;\n private apiKey: string;\n\n constructor(endpoint: string, apiKey: string) {\n this.endpoint = endpoint;\n this.apiKey = apiKey;\n }\n\n /** Send a batch of events via fetch with retry. */\n async send(sessionId: string, events: eventWithTime[], metadata?: SessionMetadata): Promise<boolean> {\n const payload: IngestPayload = { sessionId, events };\n if (metadata) payload.metadata = metadata;\n\n const json = JSON.stringify(payload);\n\n // Compress if available and payload is large enough to benefit\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n });\n\n if (res.ok) return true;\n\n // Don't retry client errors (400, 401, etc.)\n if (res.status >= 400 && res.status < 500) return false;\n } catch {\n // Network error — retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n }\n\n return false;\n }\n\n /** Best-effort DELETE to remove a cancelled session from the server. */\n deleteSession(sessionId: string): void {\n const cancelUrl = this.endpoint.replace(\"/ingest\", \"/sessions/cancel\");\n try {\n fetch(cancelUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\n body: JSON.stringify({ sessionId }),\n }).catch(() => {});\n } catch {\n // fire-and-forget\n }\n }\n\n /** Best-effort send via fetch with keepalive (for page unload). */\n async sendKeepalive(sessionId: string, events: eventWithTime[]): Promise<void> {\n if (events.length === 0) return;\n\n const payload: IngestPayload = { sessionId, events };\n const json = JSON.stringify(payload);\n\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n try {\n fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n }).catch(() => {});\n } catch {\n // best-effort, ignore failures during unload\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { record } from \"rrweb\";\nimport type { eventWithTime } from \"rrweb\";\nimport type { RecordPlugin } from \"@rrweb/types\";\nimport { getRecordConsolePlugin } from \"@rrweb/rrweb-plugin-console-record\";\nimport type { DozorOptions, DozorState, SessionMetadata } from \"./types.js\";\nimport { collectMetadata } from \"./metadata.js\";\nimport { getSessionId, clearSessionId } from \"./session.js\";\nimport { Transport } from \"./transport.js\";\n\nconst DEFAULT_ENDPOINT = \"https://dozor.kharko.dev/api/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 10_000;\nconst DEFAULT_BATCH_SIZE = 500;\n\nexport class Dozor {\n private static instance: Dozor | null = null;\n\n private transport: Transport;\n private _sessionId: string;\n private buffer: eventWithTime[] = [];\n private metadata: SessionMetadata | null;\n private metadataSent = false;\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private stopRecording: (() => void) | null = null;\n private batchSize: number;\n private flushInterval: number;\n private _state: DozorState;\n private _isHeld: boolean;\n private _userId: string | null;\n private _pauseOnHidden: boolean;\n private _autoPaused = false;\n private _plugins: RecordPlugin[];\n\n private constructor(options: DozorOptions) {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n const autoStart = options.autoStart ?? true;\n this._isHeld = options.hold ?? false;\n this._userId = options.userId ?? null;\n this._pauseOnHidden = options.pauseOnHidden ?? true;\n\n this._plugins = [];\n if (options.recordConsole !== false) {\n this._plugins.push(getRecordConsolePlugin());\n }\n\n this.transport = new Transport(endpoint, options.apiKey);\n this._sessionId = getSessionId();\n this.metadata = collectMetadata(this._userId);\n\n // Register global listeners once — they check state internally\n addEventListener(\"beforeunload\", () => this.onBeforeUnload());\n addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n this.flush();\n if (this._pauseOnHidden && this._state === \"recording\") {\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = true;\n }\n } else if (this._pauseOnHidden && this._autoPaused && this._state === \"paused\") {\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n });\n\n if (autoStart) {\n this.startRecording();\n this._state = \"recording\";\n } else {\n this._state = \"idle\";\n }\n }\n\n // ── Static ──────────────────────────────────────────────\n\n /** Initialize the Dozor recorder. Returns the singleton instance. */\n static init(options: DozorOptions): Dozor {\n if (Dozor.instance) return Dozor.instance;\n Dozor.instance = new Dozor(options);\n return Dozor.instance;\n }\n\n // ── Public properties ───────────────────────────────────\n\n /** Current session ID (UUID v4). */\n get sessionId(): string {\n return this._sessionId;\n }\n\n /** `true` when actively recording. */\n get isRecording(): boolean {\n return this._state === \"recording\";\n }\n\n /** `true` when paused via `pause()`. */\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n /** Current lifecycle state. */\n get state(): DozorState {\n return this._state;\n }\n\n /** `true` when transport is held — events are buffered locally but not sent. */\n get isHeld(): boolean {\n return this._isHeld;\n }\n\n /** Current user ID, or `null` if not set. */\n get userId(): string | null {\n return this._userId;\n }\n\n /** Number of events currently buffered in memory (not yet sent). */\n get bufferSize(): number {\n return this.buffer.length;\n }\n\n // ── Lifecycle methods ───────────────────────────────────\n\n /** Start recording manually. Only needed when `autoStart: false`. No-op if already recording. */\n start(): void {\n if (this._state !== \"idle\") return;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Pause recording without destroying the session. Keeps the session ID and buffered events alive. */\n pause(): void {\n if (this._state !== \"recording\") return;\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = false;\n }\n\n /** Resume recording after a `pause()`. Continues the same session. */\n resume(): void {\n if (this._state !== \"paused\") return;\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Stop recording permanently, flush remaining events (even if held), and destroy the singleton. */\n stop(): void {\n if (this._state === \"stopped\") return;\n this._isHeld = false;\n this.teardownRecording();\n this.flush();\n this.destroy();\n }\n\n /** Discard the current session. Drops buffered events and sends a delete request to the server. */\n cancel(): void {\n if (this._state === \"stopped\") return;\n this.teardownRecording();\n this.buffer = [];\n this.transport.deleteSession(this._sessionId);\n this.destroy();\n }\n\n /**\n * Hold the transport — recording continues but events are buffered locally without being sent.\n * Use `release()` to flush the buffer and resume normal sending, or `cancel()` to discard everything.\n * No-op if already held or stopped.\n */\n hold(): void {\n if (this._state === \"stopped\" || this._isHeld) return;\n this._isHeld = true;\n }\n\n /**\n * Release the transport hold — flush buffered events and resume normal sending.\n * Pass `{ discard: true }` to drop held events without sending them.\n * No-op if not held.\n */\n release(options?: { discard?: boolean }): void {\n if (!this._isHeld) return;\n this._isHeld = false;\n\n if (options?.discard) {\n this.buffer = [];\n } else {\n this.flush();\n }\n }\n\n /**\n * Set or update the user ID after init.\n * Useful when the user logs in after recording has already started.\n * The ID will be included in the next metadata/batch sent to the server.\n */\n setUserId(id: string): void {\n this._userId = id;\n // Update metadata so the next flush includes the userId\n if (this.metadata) {\n this.metadata.userId = id;\n }\n // If metadata was already sent, force re-send with userId on next flush\n // by resetting the flag — metadata is small, re-sending is fine\n if (this.metadataSent) {\n this.metadataSent = false;\n }\n }\n\n // ── Private ─────────────────────────────────────────────\n\n /** Start rrweb recording and the flush timer. */\n private startRecording(): void {\n this.stopRecording =\n record({\n emit: (event) => this.onEvent(event),\n plugins: this._plugins,\n }) ?? null;\n\n this.flushTimer = setInterval(() => this.flush(), this.flushInterval);\n }\n\n /** Stop rrweb and clear the flush timer (without flushing). */\n private teardownRecording(): void {\n if (this.stopRecording) {\n this.stopRecording();\n this.stopRecording = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n /** Clear session and destroy the singleton. */\n private destroy(): void {\n clearSessionId();\n Dozor.instance = null;\n this._state = \"stopped\";\n }\n\n private onEvent(event: eventWithTime): void {\n this.buffer.push(event);\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n private flush(): void {\n if (this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n const metadata = !this.metadataSent ? this.metadata ?? undefined : undefined;\n if (metadata) this.metadataSent = true;\n\n this.transport.send(this._sessionId, events, metadata).catch(() => {});\n }\n\n private onBeforeUnload(): void {\n if (this._state === \"stopped\" || this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n this.transport.sendKeepalive(this._sessionId, events);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/metadata.ts","../src/session.ts","../src/transport.ts","../src/recorder.ts"],"names":["collectMetadata","userId","meta","SESSION_KEY","getSessionId","existing","id","clearSessionId","gzipCompress","input","stream","supportsCompression","Transport","endpoint","apiKey","sessionId","events","metadata","payload","json","body","headers","attempt","res","sleep","cancelUrl","ms","resolve","DEFAULT_ENDPOINT","DEFAULT_FLUSH_INTERVAL","DEFAULT_BATCH_SIZE","_Dozor","options","autoStart","getRecordConsolePlugin","blockParts","maskAttr","record","event","Dozor"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { SessionMetadata } from \"./types.js\";\n\n/** Collect session metadata from the browser environment. */\nexport function collectMetadata(userId?: string | null): SessionMetadata {\n const meta: SessionMetadata = {\n url: location.href,\n referrer: document.referrer,\n userAgent: navigator.userAgent,\n screenWidth: screen.width,\n screenHeight: screen.height,\n language: navigator.language,\n };\n if (userId) meta.userId = userId;\n return meta;\n}\n","const SESSION_KEY = \"dozor_session_id\";\n\n/** Get or create a session ID persisted in sessionStorage for SPA continuity. */\nexport function getSessionId(): string {\n try {\n const existing = sessionStorage.getItem(SESSION_KEY);\n if (existing) return existing;\n } catch {\n // sessionStorage unavailable (SSR, iframe sandbox, etc.)\n }\n\n const id = crypto.randomUUID();\n\n try {\n sessionStorage.setItem(SESSION_KEY, id);\n } catch {\n // best-effort persistence\n }\n\n return id;\n}\n\n/** Remove the session ID from sessionStorage so the next init() creates a fresh session. */\nexport function clearSessionId(): void {\n try {\n sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // best-effort\n }\n}\n","import type { eventWithTime } from \"rrweb\";\nimport type { IngestPayload, SessionMetadata } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst COMPRESSION_THRESHOLD = 1_024;\n\n/** Compress a string to gzip using CompressionStream API. */\nasync function gzipCompress(input: string): Promise<Blob> {\n const stream = new Blob([input]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Response(stream).blob();\n}\n\n/** Check if CompressionStream is available in this environment. */\nconst supportsCompression = typeof CompressionStream !== \"undefined\";\n\nexport class Transport {\n private endpoint: string;\n private apiKey: string;\n\n constructor(endpoint: string, apiKey: string) {\n this.endpoint = endpoint;\n this.apiKey = apiKey;\n }\n\n /** Send a batch of events via fetch with retry. */\n async send(sessionId: string, events: eventWithTime[], metadata?: SessionMetadata): Promise<boolean> {\n const payload: IngestPayload = { sessionId, events };\n if (metadata) payload.metadata = metadata;\n\n const json = JSON.stringify(payload);\n\n // Compress if available and payload is large enough to benefit\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n });\n\n if (res.ok) return true;\n\n // Don't retry client errors (400, 401, etc.)\n if (res.status >= 400 && res.status < 500) return false;\n } catch {\n // Network error — retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n }\n\n return false;\n }\n\n /** Best-effort DELETE to remove a cancelled session from the server. */\n deleteSession(sessionId: string): void {\n const cancelUrl = this.endpoint.replace(\"/ingest\", \"/sessions/cancel\");\n try {\n fetch(cancelUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\n body: JSON.stringify({ sessionId }),\n }).catch(() => {});\n } catch {\n // fire-and-forget\n }\n }\n\n /** Best-effort send via fetch with keepalive (for page unload). */\n async sendKeepalive(sessionId: string, events: eventWithTime[]): Promise<void> {\n if (events.length === 0) return;\n\n const payload: IngestPayload = { sessionId, events };\n const json = JSON.stringify(payload);\n\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n try {\n fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n }).catch(() => {});\n } catch {\n // best-effort, ignore failures during unload\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { record } from \"rrweb\";\nimport type { eventWithTime } from \"rrweb\";\nimport type { RecordPlugin } from \"@rrweb/types\";\nimport { getRecordConsolePlugin } from \"@rrweb/rrweb-plugin-console-record\";\nimport type { DozorOptions, DozorState, SessionMetadata } from \"./types.js\";\nimport { collectMetadata } from \"./metadata.js\";\nimport { getSessionId, clearSessionId } from \"./session.js\";\nimport { Transport } from \"./transport.js\";\n\nconst DEFAULT_ENDPOINT = \"https://kharko-dozor.vercel.app/api/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 10_000;\nconst DEFAULT_BATCH_SIZE = 500;\n\nexport class Dozor {\n private static instance: Dozor | null = null;\n\n private transport: Transport;\n private _sessionId: string;\n private buffer: eventWithTime[] = [];\n private metadata: SessionMetadata | null;\n private metadataSent = false;\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private stopRecording: (() => void) | null = null;\n private batchSize: number;\n private flushInterval: number;\n private _state: DozorState;\n private _isHeld: boolean;\n private _userId: string | null;\n private _pauseOnHidden: boolean;\n private _autoPaused = false;\n private _plugins: RecordPlugin[];\n private _privacyMaskAttribute: string;\n private _privacyBlockAttribute: string;\n private _privacyBlockMedia: boolean;\n private _privacyMaskInputs: boolean;\n\n private constructor(options: DozorOptions) {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n const autoStart = options.autoStart ?? true;\n this._isHeld = options.hold ?? false;\n this._userId = options.userId ?? null;\n this._pauseOnHidden = options.pauseOnHidden ?? true;\n this._privacyMaskAttribute = options.privacyMaskAttribute ?? \"data-dozor-mask\";\n this._privacyBlockAttribute = options.privacyBlockAttribute ?? \"data-dozor-block\";\n this._privacyBlockMedia = options.privacyBlockMedia ?? false;\n this._privacyMaskInputs = options.privacyMaskInputs ?? true;\n\n this._plugins = [];\n if (options.recordConsole !== false) {\n this._plugins.push(getRecordConsolePlugin());\n }\n\n this.transport = new Transport(endpoint, options.apiKey);\n this._sessionId = getSessionId();\n this.metadata = collectMetadata(this._userId);\n\n // Register global listeners once — they check state internally\n addEventListener(\"beforeunload\", () => this.onBeforeUnload());\n addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n this.flush();\n if (this._pauseOnHidden && this._state === \"recording\") {\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = true;\n }\n } else if (this._pauseOnHidden && this._autoPaused && this._state === \"paused\") {\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n });\n\n if (autoStart) {\n this.startRecording();\n this._state = \"recording\";\n } else {\n this._state = \"idle\";\n }\n }\n\n // ── Static ──────────────────────────────────────────────\n\n /** Initialize the Dozor recorder. Returns the singleton instance. */\n static init(options: DozorOptions): Dozor {\n if (Dozor.instance) return Dozor.instance;\n Dozor.instance = new Dozor(options);\n return Dozor.instance;\n }\n\n // ── Public properties ───────────────────────────────────\n\n /** Current session ID (UUID v4). */\n get sessionId(): string {\n return this._sessionId;\n }\n\n /** `true` when actively recording. */\n get isRecording(): boolean {\n return this._state === \"recording\";\n }\n\n /** `true` when paused via `pause()`. */\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n /** Current lifecycle state. */\n get state(): DozorState {\n return this._state;\n }\n\n /** `true` when transport is held — events are buffered locally but not sent. */\n get isHeld(): boolean {\n return this._isHeld;\n }\n\n /** Current user ID, or `null` if not set. */\n get userId(): string | null {\n return this._userId;\n }\n\n /** Number of events currently buffered in memory (not yet sent). */\n get bufferSize(): number {\n return this.buffer.length;\n }\n\n // ── Lifecycle methods ───────────────────────────────────\n\n /** Start recording manually. Only needed when `autoStart: false`. No-op if already recording. */\n start(): void {\n if (this._state !== \"idle\") return;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Pause recording without destroying the session. Keeps the session ID and buffered events alive. */\n pause(): void {\n if (this._state !== \"recording\") return;\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = false;\n }\n\n /** Resume recording after a `pause()`. Continues the same session. */\n resume(): void {\n if (this._state !== \"paused\") return;\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Stop recording permanently, flush remaining events (even if held), and destroy the singleton. */\n stop(): void {\n if (this._state === \"stopped\") return;\n this._isHeld = false;\n this.teardownRecording();\n this.flush();\n this.destroy();\n }\n\n /** Discard the current session. Drops buffered events and sends a delete request to the server. */\n cancel(): void {\n if (this._state === \"stopped\") return;\n this.teardownRecording();\n this.buffer = [];\n this.transport.deleteSession(this._sessionId);\n this.destroy();\n }\n\n /**\n * Hold the transport — recording continues but events are buffered locally without being sent.\n * Use `release()` to flush the buffer and resume normal sending, or `cancel()` to discard everything.\n * No-op if already held or stopped.\n */\n hold(): void {\n if (this._state === \"stopped\" || this._isHeld) return;\n this._isHeld = true;\n }\n\n /**\n * Release the transport hold — flush buffered events and resume normal sending.\n * Pass `{ discard: true }` to drop held events without sending them.\n * No-op if not held.\n */\n release(options?: { discard?: boolean }): void {\n if (!this._isHeld) return;\n this._isHeld = false;\n\n if (options?.discard) {\n this.buffer = [];\n } else {\n this.flush();\n }\n }\n\n /**\n * Set or update the user ID after init.\n * Useful when the user logs in after recording has already started.\n * The ID will be included in the next metadata/batch sent to the server.\n */\n setUserId(id: string): void {\n this._userId = id;\n // Update metadata so the next flush includes the userId\n if (this.metadata) {\n this.metadata.userId = id;\n }\n // If metadata was already sent, force re-send with userId on next flush\n // by resetting the flag — metadata is small, re-sending is fine\n if (this.metadataSent) {\n this.metadataSent = false;\n }\n }\n\n // ── Private ─────────────────────────────────────────────\n\n /** Start rrweb recording and the flush timer. */\n private startRecording(): void {\n // Build block selector from attribute + optional media elements\n const blockParts: string[] = [`[${this._privacyBlockAttribute}]`];\n if (this._privacyBlockMedia) {\n blockParts.push(\"img\", \"video\", \"audio\", \"picture\", \"canvas\", \"embed\", \"object\");\n }\n\n // Mask text selector — include descendants so nested text is masked too\n const maskAttr = this._privacyMaskAttribute;\n\n this.stopRecording =\n record({\n emit: (event) => this.onEvent(event),\n plugins: this._plugins,\n maskTextSelector: `[${maskAttr}], [${maskAttr}] *`,\n blockSelector: blockParts.join(\",\"),\n maskAllInputs: this._privacyMaskInputs,\n }) ?? null;\n\n this.flushTimer = setInterval(() => this.flush(), this.flushInterval);\n }\n\n /** Stop rrweb and clear the flush timer (without flushing). */\n private teardownRecording(): void {\n if (this.stopRecording) {\n this.stopRecording();\n this.stopRecording = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n /** Clear session and destroy the singleton. */\n private destroy(): void {\n clearSessionId();\n Dozor.instance = null;\n this._state = \"stopped\";\n }\n\n private onEvent(event: eventWithTime): void {\n this.buffer.push(event);\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n private flush(): void {\n if (this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n const metadata = !this.metadataSent ? this.metadata ?? undefined : undefined;\n if (metadata) this.metadataSent = true;\n\n this.transport.send(this._sessionId, events, metadata).catch(() => {});\n }\n\n private onBeforeUnload(): void {\n if (this._state === \"stopped\" || this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n this.transport.sendKeepalive(this._sessionId, events);\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -4,7 +4,7 @@ type DozorState = "idle" | "recording" | "paused" | "stopped";
|
|
|
4
4
|
interface DozorOptions {
|
|
5
5
|
/** Public API key (dp_...) */
|
|
6
6
|
apiKey: string;
|
|
7
|
-
/** Ingest endpoint URL. Defaults to https://dozor.
|
|
7
|
+
/** Ingest endpoint URL. Defaults to https://kharko-dozor.vercel.app/api/ingest */
|
|
8
8
|
endpoint?: string;
|
|
9
9
|
/** Flush interval in ms. Default: 10000 */
|
|
10
10
|
flushInterval?: number;
|
|
@@ -20,6 +20,14 @@ interface DozorOptions {
|
|
|
20
20
|
pauseOnHidden?: boolean;
|
|
21
21
|
/** Record console.log/warn/error/info/debug calls. Default: true */
|
|
22
22
|
recordConsole?: boolean;
|
|
23
|
+
/** HTML attribute name for text masking. Elements (and their descendants) with this attribute will have text content replaced with asterisks in the recording. Default: `"data-dozor-mask"` */
|
|
24
|
+
privacyMaskAttribute?: string;
|
|
25
|
+
/** HTML attribute name for element blocking. Elements with this attribute are completely removed from the recording and replaced with a same-size placeholder. Default: `"data-dozor-block"` */
|
|
26
|
+
privacyBlockAttribute?: string;
|
|
27
|
+
/** Replace all media elements (`img`, `video`, `audio`, `picture`, `canvas`, `embed`, `object`) with same-size placeholders. Useful when the recorded site blocks cross-origin media access during replay. Default: `false` */
|
|
28
|
+
privacyBlockMedia?: boolean;
|
|
29
|
+
/** Mask all input, textarea, and select values with asterisks. Default: `true` */
|
|
30
|
+
privacyMaskInputs?: boolean;
|
|
23
31
|
}
|
|
24
32
|
interface SessionMetadata {
|
|
25
33
|
url: string;
|
|
@@ -53,6 +61,10 @@ declare class Dozor {
|
|
|
53
61
|
private _pauseOnHidden;
|
|
54
62
|
private _autoPaused;
|
|
55
63
|
private _plugins;
|
|
64
|
+
private _privacyMaskAttribute;
|
|
65
|
+
private _privacyBlockAttribute;
|
|
66
|
+
private _privacyBlockMedia;
|
|
67
|
+
private _privacyMaskInputs;
|
|
56
68
|
private constructor();
|
|
57
69
|
/** Initialize the Dozor recorder. Returns the singleton instance. */
|
|
58
70
|
static init(options: DozorOptions): Dozor;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ type DozorState = "idle" | "recording" | "paused" | "stopped";
|
|
|
4
4
|
interface DozorOptions {
|
|
5
5
|
/** Public API key (dp_...) */
|
|
6
6
|
apiKey: string;
|
|
7
|
-
/** Ingest endpoint URL. Defaults to https://dozor.
|
|
7
|
+
/** Ingest endpoint URL. Defaults to https://kharko-dozor.vercel.app/api/ingest */
|
|
8
8
|
endpoint?: string;
|
|
9
9
|
/** Flush interval in ms. Default: 10000 */
|
|
10
10
|
flushInterval?: number;
|
|
@@ -20,6 +20,14 @@ interface DozorOptions {
|
|
|
20
20
|
pauseOnHidden?: boolean;
|
|
21
21
|
/** Record console.log/warn/error/info/debug calls. Default: true */
|
|
22
22
|
recordConsole?: boolean;
|
|
23
|
+
/** HTML attribute name for text masking. Elements (and their descendants) with this attribute will have text content replaced with asterisks in the recording. Default: `"data-dozor-mask"` */
|
|
24
|
+
privacyMaskAttribute?: string;
|
|
25
|
+
/** HTML attribute name for element blocking. Elements with this attribute are completely removed from the recording and replaced with a same-size placeholder. Default: `"data-dozor-block"` */
|
|
26
|
+
privacyBlockAttribute?: string;
|
|
27
|
+
/** Replace all media elements (`img`, `video`, `audio`, `picture`, `canvas`, `embed`, `object`) with same-size placeholders. Useful when the recorded site blocks cross-origin media access during replay. Default: `false` */
|
|
28
|
+
privacyBlockMedia?: boolean;
|
|
29
|
+
/** Mask all input, textarea, and select values with asterisks. Default: `true` */
|
|
30
|
+
privacyMaskInputs?: boolean;
|
|
23
31
|
}
|
|
24
32
|
interface SessionMetadata {
|
|
25
33
|
url: string;
|
|
@@ -53,6 +61,10 @@ declare class Dozor {
|
|
|
53
61
|
private _pauseOnHidden;
|
|
54
62
|
private _autoPaused;
|
|
55
63
|
private _plugins;
|
|
64
|
+
private _privacyMaskAttribute;
|
|
65
|
+
private _privacyBlockAttribute;
|
|
66
|
+
private _privacyBlockMedia;
|
|
67
|
+
private _privacyMaskInputs;
|
|
56
68
|
private constructor();
|
|
57
69
|
/** Initialize the Dozor recorder. Returns the singleton instance. */
|
|
58
70
|
static init(options: DozorOptions): Dozor;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {record}from'rrweb';import {getRecordConsolePlugin}from'@rrweb/rrweb-plugin-console-record';function f(s){let t={url:location.href,referrer:document.referrer,userAgent:navigator.userAgent,screenWidth:screen.width,screenHeight:screen.height,language:navigator.language};return s&&(t.userId=s),t}var u="dozor_session_id";function g(){try{let t=sessionStorage.getItem(u);if(t)return t}catch{}let s=crypto.randomUUID();try{sessionStorage.setItem(u,s);}catch{}return s}function
|
|
2
|
-
export{
|
|
1
|
+
import {record}from'rrweb';import {getRecordConsolePlugin}from'@rrweb/rrweb-plugin-console-record';function f(s){let t={url:location.href,referrer:document.referrer,userAgent:navigator.userAgent,screenWidth:screen.width,screenHeight:screen.height,language:navigator.language};return s&&(t.userId=s),t}var u="dozor_session_id";function g(){try{let t=sessionStorage.getItem(u);if(t)return t}catch{}let s=crypto.randomUUID();try{sessionStorage.setItem(u,s);}catch{}return s}function v(){try{sessionStorage.removeItem(u);}catch{}}async function m(s){let t=new Blob([s]).stream().pipeThrough(new CompressionStream("gzip"));return new Response(t).blob()}var _=typeof CompressionStream<"u",h=class{constructor(t,e){this.endpoint=t,this.apiKey=e;}async send(t,e,r){let n={sessionId:t,events:e};r&&(n.metadata=r);let a=JSON.stringify(n),o,p={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&a.length>1024?(o=await m(a),p["Content-Encoding"]="gzip"):o=a;for(let d=0;d<3;d++){try{let l=await fetch(this.endpoint,{method:"POST",headers:p,body:o,keepalive:!0});if(l.ok)return !0;if(l.status>=400&&l.status<500)return !1}catch{}d<2&&await y(1e3*2**d);}return false}deleteSession(t){let e=this.endpoint.replace("/ingest","/sessions/cancel");try{fetch(e,{method:"POST",headers:{"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey},body:JSON.stringify({sessionId:t})}).catch(()=>{});}catch{}}async sendKeepalive(t,e){if(e.length===0)return;let n=JSON.stringify({sessionId:t,events:e}),a,o={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&n.length>1024?(a=await m(n),o["Content-Encoding"]="gzip"):a=n;try{fetch(this.endpoint,{method:"POST",headers:o,body:a,keepalive:!0}).catch(()=>{});}catch{}}};function y(s){return new Promise(t=>setTimeout(t,s))}var I="https://kharko-dozor.vercel.app/api/ingest",R=1e4,T=500,i=class i{constructor(t){this.buffer=[];this.metadataSent=false;this.flushTimer=null;this.stopRecording=null;this._autoPaused=false;let e=t.endpoint??I;this.batchSize=t.batchSize??T,this.flushInterval=t.flushInterval??R;let r=t.autoStart??true;this._isHeld=t.hold??false,this._userId=t.userId??null,this._pauseOnHidden=t.pauseOnHidden??true,this._privacyMaskAttribute=t.privacyMaskAttribute??"data-dozor-mask",this._privacyBlockAttribute=t.privacyBlockAttribute??"data-dozor-block",this._privacyBlockMedia=t.privacyBlockMedia??false,this._privacyMaskInputs=t.privacyMaskInputs??true,this._plugins=[],t.recordConsole!==false&&this._plugins.push(getRecordConsolePlugin()),this.transport=new h(e,t.apiKey),this._sessionId=g(),this.metadata=f(this._userId),addEventListener("beforeunload",()=>this.onBeforeUnload()),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.flush(),this._pauseOnHidden&&this._state==="recording"&&(this.teardownRecording(),this._state="paused",this._autoPaused=true)):this._pauseOnHidden&&this._autoPaused&&this._state==="paused"&&(this._autoPaused=false,this.startRecording(),this._state="recording");}),r?(this.startRecording(),this._state="recording"):this._state="idle";}static init(t){return i.instance||(i.instance=new i(t)),i.instance}get sessionId(){return this._sessionId}get isRecording(){return this._state==="recording"}get isPaused(){return this._state==="paused"}get state(){return this._state}get isHeld(){return this._isHeld}get userId(){return this._userId}get bufferSize(){return this.buffer.length}start(){this._state==="idle"&&(this.startRecording(),this._state="recording");}pause(){this._state==="recording"&&(this.teardownRecording(),this._state="paused",this._autoPaused=false);}resume(){this._state==="paused"&&(this._autoPaused=false,this.startRecording(),this._state="recording");}stop(){this._state!=="stopped"&&(this._isHeld=false,this.teardownRecording(),this.flush(),this.destroy());}cancel(){this._state!=="stopped"&&(this.teardownRecording(),this.buffer=[],this.transport.deleteSession(this._sessionId),this.destroy());}hold(){this._state==="stopped"||this._isHeld||(this._isHeld=true);}release(t){this._isHeld&&(this._isHeld=false,t?.discard?this.buffer=[]:this.flush());}setUserId(t){this._userId=t,this.metadata&&(this.metadata.userId=t),this.metadataSent&&(this.metadataSent=false);}startRecording(){let t=[`[${this._privacyBlockAttribute}]`];this._privacyBlockMedia&&t.push("img","video","audio","picture","canvas","embed","object");let e=this._privacyMaskAttribute;this.stopRecording=record({emit:r=>this.onEvent(r),plugins:this._plugins,maskTextSelector:`[${e}], [${e}] *`,blockSelector:t.join(","),maskAllInputs:this._privacyMaskInputs})??null,this.flushTimer=setInterval(()=>this.flush(),this.flushInterval);}teardownRecording(){this.stopRecording&&(this.stopRecording(),this.stopRecording=null),this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null);}destroy(){v(),i.instance=null,this._state="stopped";}onEvent(t){this.buffer.push(t),this.buffer.length>=this.batchSize&&this.flush();}flush(){if(this._isHeld||this.buffer.length===0)return;let t=this.buffer;this.buffer=[];let e=this.metadataSent?void 0:this.metadata??void 0;e&&(this.metadataSent=true),this.transport.send(this._sessionId,t,e).catch(()=>{});}onBeforeUnload(){if(this._state==="stopped"||this._isHeld||this.buffer.length===0)return;let t=this.buffer;this.buffer=[],this.transport.sendKeepalive(this._sessionId,t);}};i.instance=null;var c=i;
|
|
2
|
+
export{c as Dozor};//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/metadata.ts","../src/session.ts","../src/transport.ts","../src/recorder.ts"],"names":["collectMetadata","userId","meta","SESSION_KEY","getSessionId","existing","id","clearSessionId","gzipCompress","input","stream","supportsCompression","Transport","endpoint","apiKey","sessionId","events","metadata","payload","json","body","headers","attempt","res","sleep","cancelUrl","ms","resolve","DEFAULT_ENDPOINT","DEFAULT_FLUSH_INTERVAL","DEFAULT_BATCH_SIZE","_Dozor","options","autoStart","getRecordConsolePlugin","record","event","Dozor"],"mappings":"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","file":"index.js","sourcesContent":["import type { SessionMetadata } from \"./types.js\";\n\n/** Collect session metadata from the browser environment. */\nexport function collectMetadata(userId?: string | null): SessionMetadata {\n const meta: SessionMetadata = {\n url: location.href,\n referrer: document.referrer,\n userAgent: navigator.userAgent,\n screenWidth: screen.width,\n screenHeight: screen.height,\n language: navigator.language,\n };\n if (userId) meta.userId = userId;\n return meta;\n}\n","const SESSION_KEY = \"dozor_session_id\";\n\n/** Get or create a session ID persisted in sessionStorage for SPA continuity. */\nexport function getSessionId(): string {\n try {\n const existing = sessionStorage.getItem(SESSION_KEY);\n if (existing) return existing;\n } catch {\n // sessionStorage unavailable (SSR, iframe sandbox, etc.)\n }\n\n const id = crypto.randomUUID();\n\n try {\n sessionStorage.setItem(SESSION_KEY, id);\n } catch {\n // best-effort persistence\n }\n\n return id;\n}\n\n/** Remove the session ID from sessionStorage so the next init() creates a fresh session. */\nexport function clearSessionId(): void {\n try {\n sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // best-effort\n }\n}\n","import type { eventWithTime } from \"rrweb\";\nimport type { IngestPayload, SessionMetadata } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst COMPRESSION_THRESHOLD = 1_024;\n\n/** Compress a string to gzip using CompressionStream API. */\nasync function gzipCompress(input: string): Promise<Blob> {\n const stream = new Blob([input]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Response(stream).blob();\n}\n\n/** Check if CompressionStream is available in this environment. */\nconst supportsCompression = typeof CompressionStream !== \"undefined\";\n\nexport class Transport {\n private endpoint: string;\n private apiKey: string;\n\n constructor(endpoint: string, apiKey: string) {\n this.endpoint = endpoint;\n this.apiKey = apiKey;\n }\n\n /** Send a batch of events via fetch with retry. */\n async send(sessionId: string, events: eventWithTime[], metadata?: SessionMetadata): Promise<boolean> {\n const payload: IngestPayload = { sessionId, events };\n if (metadata) payload.metadata = metadata;\n\n const json = JSON.stringify(payload);\n\n // Compress if available and payload is large enough to benefit\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n });\n\n if (res.ok) return true;\n\n // Don't retry client errors (400, 401, etc.)\n if (res.status >= 400 && res.status < 500) return false;\n } catch {\n // Network error — retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n }\n\n return false;\n }\n\n /** Best-effort DELETE to remove a cancelled session from the server. */\n deleteSession(sessionId: string): void {\n const cancelUrl = this.endpoint.replace(\"/ingest\", \"/sessions/cancel\");\n try {\n fetch(cancelUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\n body: JSON.stringify({ sessionId }),\n }).catch(() => {});\n } catch {\n // fire-and-forget\n }\n }\n\n /** Best-effort send via fetch with keepalive (for page unload). */\n async sendKeepalive(sessionId: string, events: eventWithTime[]): Promise<void> {\n if (events.length === 0) return;\n\n const payload: IngestPayload = { sessionId, events };\n const json = JSON.stringify(payload);\n\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n try {\n fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n }).catch(() => {});\n } catch {\n // best-effort, ignore failures during unload\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { record } from \"rrweb\";\nimport type { eventWithTime } from \"rrweb\";\nimport type { RecordPlugin } from \"@rrweb/types\";\nimport { getRecordConsolePlugin } from \"@rrweb/rrweb-plugin-console-record\";\nimport type { DozorOptions, DozorState, SessionMetadata } from \"./types.js\";\nimport { collectMetadata } from \"./metadata.js\";\nimport { getSessionId, clearSessionId } from \"./session.js\";\nimport { Transport } from \"./transport.js\";\n\nconst DEFAULT_ENDPOINT = \"https://dozor.kharko.dev/api/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 10_000;\nconst DEFAULT_BATCH_SIZE = 500;\n\nexport class Dozor {\n private static instance: Dozor | null = null;\n\n private transport: Transport;\n private _sessionId: string;\n private buffer: eventWithTime[] = [];\n private metadata: SessionMetadata | null;\n private metadataSent = false;\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private stopRecording: (() => void) | null = null;\n private batchSize: number;\n private flushInterval: number;\n private _state: DozorState;\n private _isHeld: boolean;\n private _userId: string | null;\n private _pauseOnHidden: boolean;\n private _autoPaused = false;\n private _plugins: RecordPlugin[];\n\n private constructor(options: DozorOptions) {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n const autoStart = options.autoStart ?? true;\n this._isHeld = options.hold ?? false;\n this._userId = options.userId ?? null;\n this._pauseOnHidden = options.pauseOnHidden ?? true;\n\n this._plugins = [];\n if (options.recordConsole !== false) {\n this._plugins.push(getRecordConsolePlugin());\n }\n\n this.transport = new Transport(endpoint, options.apiKey);\n this._sessionId = getSessionId();\n this.metadata = collectMetadata(this._userId);\n\n // Register global listeners once — they check state internally\n addEventListener(\"beforeunload\", () => this.onBeforeUnload());\n addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n this.flush();\n if (this._pauseOnHidden && this._state === \"recording\") {\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = true;\n }\n } else if (this._pauseOnHidden && this._autoPaused && this._state === \"paused\") {\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n });\n\n if (autoStart) {\n this.startRecording();\n this._state = \"recording\";\n } else {\n this._state = \"idle\";\n }\n }\n\n // ── Static ──────────────────────────────────────────────\n\n /** Initialize the Dozor recorder. Returns the singleton instance. */\n static init(options: DozorOptions): Dozor {\n if (Dozor.instance) return Dozor.instance;\n Dozor.instance = new Dozor(options);\n return Dozor.instance;\n }\n\n // ── Public properties ───────────────────────────────────\n\n /** Current session ID (UUID v4). */\n get sessionId(): string {\n return this._sessionId;\n }\n\n /** `true` when actively recording. */\n get isRecording(): boolean {\n return this._state === \"recording\";\n }\n\n /** `true` when paused via `pause()`. */\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n /** Current lifecycle state. */\n get state(): DozorState {\n return this._state;\n }\n\n /** `true` when transport is held — events are buffered locally but not sent. */\n get isHeld(): boolean {\n return this._isHeld;\n }\n\n /** Current user ID, or `null` if not set. */\n get userId(): string | null {\n return this._userId;\n }\n\n /** Number of events currently buffered in memory (not yet sent). */\n get bufferSize(): number {\n return this.buffer.length;\n }\n\n // ── Lifecycle methods ───────────────────────────────────\n\n /** Start recording manually. Only needed when `autoStart: false`. No-op if already recording. */\n start(): void {\n if (this._state !== \"idle\") return;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Pause recording without destroying the session. Keeps the session ID and buffered events alive. */\n pause(): void {\n if (this._state !== \"recording\") return;\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = false;\n }\n\n /** Resume recording after a `pause()`. Continues the same session. */\n resume(): void {\n if (this._state !== \"paused\") return;\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Stop recording permanently, flush remaining events (even if held), and destroy the singleton. */\n stop(): void {\n if (this._state === \"stopped\") return;\n this._isHeld = false;\n this.teardownRecording();\n this.flush();\n this.destroy();\n }\n\n /** Discard the current session. Drops buffered events and sends a delete request to the server. */\n cancel(): void {\n if (this._state === \"stopped\") return;\n this.teardownRecording();\n this.buffer = [];\n this.transport.deleteSession(this._sessionId);\n this.destroy();\n }\n\n /**\n * Hold the transport — recording continues but events are buffered locally without being sent.\n * Use `release()` to flush the buffer and resume normal sending, or `cancel()` to discard everything.\n * No-op if already held or stopped.\n */\n hold(): void {\n if (this._state === \"stopped\" || this._isHeld) return;\n this._isHeld = true;\n }\n\n /**\n * Release the transport hold — flush buffered events and resume normal sending.\n * Pass `{ discard: true }` to drop held events without sending them.\n * No-op if not held.\n */\n release(options?: { discard?: boolean }): void {\n if (!this._isHeld) return;\n this._isHeld = false;\n\n if (options?.discard) {\n this.buffer = [];\n } else {\n this.flush();\n }\n }\n\n /**\n * Set or update the user ID after init.\n * Useful when the user logs in after recording has already started.\n * The ID will be included in the next metadata/batch sent to the server.\n */\n setUserId(id: string): void {\n this._userId = id;\n // Update metadata so the next flush includes the userId\n if (this.metadata) {\n this.metadata.userId = id;\n }\n // If metadata was already sent, force re-send with userId on next flush\n // by resetting the flag — metadata is small, re-sending is fine\n if (this.metadataSent) {\n this.metadataSent = false;\n }\n }\n\n // ── Private ─────────────────────────────────────────────\n\n /** Start rrweb recording and the flush timer. */\n private startRecording(): void {\n this.stopRecording =\n record({\n emit: (event) => this.onEvent(event),\n plugins: this._plugins,\n }) ?? null;\n\n this.flushTimer = setInterval(() => this.flush(), this.flushInterval);\n }\n\n /** Stop rrweb and clear the flush timer (without flushing). */\n private teardownRecording(): void {\n if (this.stopRecording) {\n this.stopRecording();\n this.stopRecording = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n /** Clear session and destroy the singleton. */\n private destroy(): void {\n clearSessionId();\n Dozor.instance = null;\n this._state = \"stopped\";\n }\n\n private onEvent(event: eventWithTime): void {\n this.buffer.push(event);\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n private flush(): void {\n if (this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n const metadata = !this.metadataSent ? this.metadata ?? undefined : undefined;\n if (metadata) this.metadataSent = true;\n\n this.transport.send(this._sessionId, events, metadata).catch(() => {});\n }\n\n private onBeforeUnload(): void {\n if (this._state === \"stopped\" || this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n this.transport.sendKeepalive(this._sessionId, events);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/metadata.ts","../src/session.ts","../src/transport.ts","../src/recorder.ts"],"names":["collectMetadata","userId","meta","SESSION_KEY","getSessionId","existing","id","clearSessionId","gzipCompress","input","stream","supportsCompression","Transport","endpoint","apiKey","sessionId","events","metadata","payload","json","body","headers","attempt","res","sleep","cancelUrl","ms","resolve","DEFAULT_ENDPOINT","DEFAULT_FLUSH_INTERVAL","DEFAULT_BATCH_SIZE","_Dozor","options","autoStart","getRecordConsolePlugin","blockParts","maskAttr","record","event","Dozor"],"mappings":"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","file":"index.js","sourcesContent":["import type { SessionMetadata } from \"./types.js\";\n\n/** Collect session metadata from the browser environment. */\nexport function collectMetadata(userId?: string | null): SessionMetadata {\n const meta: SessionMetadata = {\n url: location.href,\n referrer: document.referrer,\n userAgent: navigator.userAgent,\n screenWidth: screen.width,\n screenHeight: screen.height,\n language: navigator.language,\n };\n if (userId) meta.userId = userId;\n return meta;\n}\n","const SESSION_KEY = \"dozor_session_id\";\n\n/** Get or create a session ID persisted in sessionStorage for SPA continuity. */\nexport function getSessionId(): string {\n try {\n const existing = sessionStorage.getItem(SESSION_KEY);\n if (existing) return existing;\n } catch {\n // sessionStorage unavailable (SSR, iframe sandbox, etc.)\n }\n\n const id = crypto.randomUUID();\n\n try {\n sessionStorage.setItem(SESSION_KEY, id);\n } catch {\n // best-effort persistence\n }\n\n return id;\n}\n\n/** Remove the session ID from sessionStorage so the next init() creates a fresh session. */\nexport function clearSessionId(): void {\n try {\n sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // best-effort\n }\n}\n","import type { eventWithTime } from \"rrweb\";\nimport type { IngestPayload, SessionMetadata } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst COMPRESSION_THRESHOLD = 1_024;\n\n/** Compress a string to gzip using CompressionStream API. */\nasync function gzipCompress(input: string): Promise<Blob> {\n const stream = new Blob([input]).stream().pipeThrough(new CompressionStream(\"gzip\"));\n return new Response(stream).blob();\n}\n\n/** Check if CompressionStream is available in this environment. */\nconst supportsCompression = typeof CompressionStream !== \"undefined\";\n\nexport class Transport {\n private endpoint: string;\n private apiKey: string;\n\n constructor(endpoint: string, apiKey: string) {\n this.endpoint = endpoint;\n this.apiKey = apiKey;\n }\n\n /** Send a batch of events via fetch with retry. */\n async send(sessionId: string, events: eventWithTime[], metadata?: SessionMetadata): Promise<boolean> {\n const payload: IngestPayload = { sessionId, events };\n if (metadata) payload.metadata = metadata;\n\n const json = JSON.stringify(payload);\n\n // Compress if available and payload is large enough to benefit\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n try {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n });\n\n if (res.ok) return true;\n\n // Don't retry client errors (400, 401, etc.)\n if (res.status >= 400 && res.status < 500) return false;\n } catch {\n // Network error — retry\n }\n\n if (attempt < MAX_RETRIES - 1) {\n await sleep(BASE_DELAY_MS * 2 ** attempt);\n }\n }\n\n return false;\n }\n\n /** Best-effort DELETE to remove a cancelled session from the server. */\n deleteSession(sessionId: string): void {\n const cancelUrl = this.endpoint.replace(\"/ingest\", \"/sessions/cancel\");\n try {\n fetch(cancelUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\n body: JSON.stringify({ sessionId }),\n }).catch(() => {});\n } catch {\n // fire-and-forget\n }\n }\n\n /** Best-effort send via fetch with keepalive (for page unload). */\n async sendKeepalive(sessionId: string, events: eventWithTime[]): Promise<void> {\n if (events.length === 0) return;\n\n const payload: IngestPayload = { sessionId, events };\n const json = JSON.stringify(payload);\n\n let body: BodyInit;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n };\n\n if (supportsCompression && json.length > COMPRESSION_THRESHOLD) {\n body = await gzipCompress(json);\n headers[\"Content-Encoding\"] = \"gzip\";\n } else {\n body = json;\n }\n\n try {\n fetch(this.endpoint, {\n method: \"POST\",\n headers,\n body,\n keepalive: true,\n }).catch(() => {});\n } catch {\n // best-effort, ignore failures during unload\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { record } from \"rrweb\";\nimport type { eventWithTime } from \"rrweb\";\nimport type { RecordPlugin } from \"@rrweb/types\";\nimport { getRecordConsolePlugin } from \"@rrweb/rrweb-plugin-console-record\";\nimport type { DozorOptions, DozorState, SessionMetadata } from \"./types.js\";\nimport { collectMetadata } from \"./metadata.js\";\nimport { getSessionId, clearSessionId } from \"./session.js\";\nimport { Transport } from \"./transport.js\";\n\nconst DEFAULT_ENDPOINT = \"https://kharko-dozor.vercel.app/api/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 10_000;\nconst DEFAULT_BATCH_SIZE = 500;\n\nexport class Dozor {\n private static instance: Dozor | null = null;\n\n private transport: Transport;\n private _sessionId: string;\n private buffer: eventWithTime[] = [];\n private metadata: SessionMetadata | null;\n private metadataSent = false;\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private stopRecording: (() => void) | null = null;\n private batchSize: number;\n private flushInterval: number;\n private _state: DozorState;\n private _isHeld: boolean;\n private _userId: string | null;\n private _pauseOnHidden: boolean;\n private _autoPaused = false;\n private _plugins: RecordPlugin[];\n private _privacyMaskAttribute: string;\n private _privacyBlockAttribute: string;\n private _privacyBlockMedia: boolean;\n private _privacyMaskInputs: boolean;\n\n private constructor(options: DozorOptions) {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n const autoStart = options.autoStart ?? true;\n this._isHeld = options.hold ?? false;\n this._userId = options.userId ?? null;\n this._pauseOnHidden = options.pauseOnHidden ?? true;\n this._privacyMaskAttribute = options.privacyMaskAttribute ?? \"data-dozor-mask\";\n this._privacyBlockAttribute = options.privacyBlockAttribute ?? \"data-dozor-block\";\n this._privacyBlockMedia = options.privacyBlockMedia ?? false;\n this._privacyMaskInputs = options.privacyMaskInputs ?? true;\n\n this._plugins = [];\n if (options.recordConsole !== false) {\n this._plugins.push(getRecordConsolePlugin());\n }\n\n this.transport = new Transport(endpoint, options.apiKey);\n this._sessionId = getSessionId();\n this.metadata = collectMetadata(this._userId);\n\n // Register global listeners once — they check state internally\n addEventListener(\"beforeunload\", () => this.onBeforeUnload());\n addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n this.flush();\n if (this._pauseOnHidden && this._state === \"recording\") {\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = true;\n }\n } else if (this._pauseOnHidden && this._autoPaused && this._state === \"paused\") {\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n });\n\n if (autoStart) {\n this.startRecording();\n this._state = \"recording\";\n } else {\n this._state = \"idle\";\n }\n }\n\n // ── Static ──────────────────────────────────────────────\n\n /** Initialize the Dozor recorder. Returns the singleton instance. */\n static init(options: DozorOptions): Dozor {\n if (Dozor.instance) return Dozor.instance;\n Dozor.instance = new Dozor(options);\n return Dozor.instance;\n }\n\n // ── Public properties ───────────────────────────────────\n\n /** Current session ID (UUID v4). */\n get sessionId(): string {\n return this._sessionId;\n }\n\n /** `true` when actively recording. */\n get isRecording(): boolean {\n return this._state === \"recording\";\n }\n\n /** `true` when paused via `pause()`. */\n get isPaused(): boolean {\n return this._state === \"paused\";\n }\n\n /** Current lifecycle state. */\n get state(): DozorState {\n return this._state;\n }\n\n /** `true` when transport is held — events are buffered locally but not sent. */\n get isHeld(): boolean {\n return this._isHeld;\n }\n\n /** Current user ID, or `null` if not set. */\n get userId(): string | null {\n return this._userId;\n }\n\n /** Number of events currently buffered in memory (not yet sent). */\n get bufferSize(): number {\n return this.buffer.length;\n }\n\n // ── Lifecycle methods ───────────────────────────────────\n\n /** Start recording manually. Only needed when `autoStart: false`. No-op if already recording. */\n start(): void {\n if (this._state !== \"idle\") return;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Pause recording without destroying the session. Keeps the session ID and buffered events alive. */\n pause(): void {\n if (this._state !== \"recording\") return;\n this.teardownRecording();\n this._state = \"paused\";\n this._autoPaused = false;\n }\n\n /** Resume recording after a `pause()`. Continues the same session. */\n resume(): void {\n if (this._state !== \"paused\") return;\n this._autoPaused = false;\n this.startRecording();\n this._state = \"recording\";\n }\n\n /** Stop recording permanently, flush remaining events (even if held), and destroy the singleton. */\n stop(): void {\n if (this._state === \"stopped\") return;\n this._isHeld = false;\n this.teardownRecording();\n this.flush();\n this.destroy();\n }\n\n /** Discard the current session. Drops buffered events and sends a delete request to the server. */\n cancel(): void {\n if (this._state === \"stopped\") return;\n this.teardownRecording();\n this.buffer = [];\n this.transport.deleteSession(this._sessionId);\n this.destroy();\n }\n\n /**\n * Hold the transport — recording continues but events are buffered locally without being sent.\n * Use `release()` to flush the buffer and resume normal sending, or `cancel()` to discard everything.\n * No-op if already held or stopped.\n */\n hold(): void {\n if (this._state === \"stopped\" || this._isHeld) return;\n this._isHeld = true;\n }\n\n /**\n * Release the transport hold — flush buffered events and resume normal sending.\n * Pass `{ discard: true }` to drop held events without sending them.\n * No-op if not held.\n */\n release(options?: { discard?: boolean }): void {\n if (!this._isHeld) return;\n this._isHeld = false;\n\n if (options?.discard) {\n this.buffer = [];\n } else {\n this.flush();\n }\n }\n\n /**\n * Set or update the user ID after init.\n * Useful when the user logs in after recording has already started.\n * The ID will be included in the next metadata/batch sent to the server.\n */\n setUserId(id: string): void {\n this._userId = id;\n // Update metadata so the next flush includes the userId\n if (this.metadata) {\n this.metadata.userId = id;\n }\n // If metadata was already sent, force re-send with userId on next flush\n // by resetting the flag — metadata is small, re-sending is fine\n if (this.metadataSent) {\n this.metadataSent = false;\n }\n }\n\n // ── Private ─────────────────────────────────────────────\n\n /** Start rrweb recording and the flush timer. */\n private startRecording(): void {\n // Build block selector from attribute + optional media elements\n const blockParts: string[] = [`[${this._privacyBlockAttribute}]`];\n if (this._privacyBlockMedia) {\n blockParts.push(\"img\", \"video\", \"audio\", \"picture\", \"canvas\", \"embed\", \"object\");\n }\n\n // Mask text selector — include descendants so nested text is masked too\n const maskAttr = this._privacyMaskAttribute;\n\n this.stopRecording =\n record({\n emit: (event) => this.onEvent(event),\n plugins: this._plugins,\n maskTextSelector: `[${maskAttr}], [${maskAttr}] *`,\n blockSelector: blockParts.join(\",\"),\n maskAllInputs: this._privacyMaskInputs,\n }) ?? null;\n\n this.flushTimer = setInterval(() => this.flush(), this.flushInterval);\n }\n\n /** Stop rrweb and clear the flush timer (without flushing). */\n private teardownRecording(): void {\n if (this.stopRecording) {\n this.stopRecording();\n this.stopRecording = null;\n }\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n }\n\n /** Clear session and destroy the singleton. */\n private destroy(): void {\n clearSessionId();\n Dozor.instance = null;\n this._state = \"stopped\";\n }\n\n private onEvent(event: eventWithTime): void {\n this.buffer.push(event);\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n private flush(): void {\n if (this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n const metadata = !this.metadataSent ? this.metadata ?? undefined : undefined;\n if (metadata) this.metadataSent = true;\n\n this.transport.send(this._sessionId, events, metadata).catch(() => {});\n }\n\n private onBeforeUnload(): void {\n if (this._state === \"stopped\" || this._isHeld || this.buffer.length === 0) return;\n\n const events = this.buffer;\n this.buffer = [];\n\n this.transport.sendKeepalive(this._sessionId, events);\n }\n}\n"]}
|