@kharko/dozor 0.1.0 → 0.2.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 +386 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# @kharko/dozor
|
|
2
|
+
|
|
3
|
+
Lightweight session recording SDK for [Kharko Dozor](https://github.com/kolia-zamnius/kharko-dozor) — an open-source session replay platform.
|
|
4
|
+
|
|
5
|
+
Captures DOM mutations via [rrweb](https://github.com/rrweb-io/rrweb), batches them, compresses with gzip, and ships to the Dozor ingest endpoint. Framework-agnostic — works with any JavaScript app. For React, see [`@kharko/dozor-react`](https://www.npmjs.com/package/@kharko/dozor-react).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @kharko/dozor
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @kharko/dozor
|
|
13
|
+
# or
|
|
14
|
+
yarn add @kharko/dozor
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Dozor } from "@kharko/dozor";
|
|
21
|
+
|
|
22
|
+
Dozor.init({ apiKey: "dp_your_public_key" });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's it. Recording starts immediately and events are sent to Dozor automatically.
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
### `Dozor.init(options)`
|
|
30
|
+
|
|
31
|
+
Creates and returns a singleton recorder instance. Calling `init()` multiple times returns the existing instance — it does **not** re-initialize.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const dozor = Dozor.init({
|
|
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
|
+
userId: "user_123",
|
|
42
|
+
pauseOnHidden: true,
|
|
43
|
+
recordConsole: true,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Options
|
|
48
|
+
|
|
49
|
+
| Option | Type | Default | Description |
|
|
50
|
+
| --------------- | --------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `apiKey` | `string` | **required** | Public project API key (`dp_...`). Found in Project Settings. |
|
|
52
|
+
| `endpoint` | `string` | `https://dozor.kharko.dev/api/ingest` | Ingest endpoint URL. Override for self-hosted setups. |
|
|
53
|
+
| `flushInterval` | `number` | `10000` | How often to flush buffered events (ms). |
|
|
54
|
+
| `batchSize` | `number` | `500` | Max events in the buffer before an automatic flush. |
|
|
55
|
+
| `autoStart` | `boolean` | `true` | Start recording immediately on init. Set `false` to defer until `start()` is called. |
|
|
56
|
+
| `hold` | `boolean` | `false` | Start with transport held — events are recorded and buffered in memory but not sent to the server until `release()` is called. |
|
|
57
|
+
| `userId` | `string` | — | Stable user identifier. Sent with session metadata to link multiple sessions to the same user. Can also be set later via `setUserId()`. |
|
|
58
|
+
| `pauseOnHidden` | `boolean` | `true` | Automatically pause recording when the tab is hidden and resume when the user returns. Saves bandwidth and avoids capturing inactivity. |
|
|
59
|
+
| `recordConsole` | `boolean` | `true` | Record `console.log`, `warn`, `error`, `info`, `debug` calls. Console entries appear as events and can be viewed in the Dozor dashboard replay. |
|
|
60
|
+
|
|
61
|
+
### Instance properties
|
|
62
|
+
|
|
63
|
+
| Property | Type | Description |
|
|
64
|
+
| ------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `sessionId` | `string` | Current session ID (UUID v4, stored in `sessionStorage`). |
|
|
66
|
+
| `state` | `DozorState` | Current lifecycle state: `"idle"`, `"recording"`, `"paused"`, or `"stopped"`. |
|
|
67
|
+
| `isRecording` | `boolean` | `true` when actively recording. |
|
|
68
|
+
| `isPaused` | `boolean` | `true` when paused via `pause()`. |
|
|
69
|
+
| `isHeld` | `boolean` | `true` when transport is held — events are buffered but not sent. |
|
|
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()`. |
|
|
72
|
+
|
|
73
|
+
### Instance methods
|
|
74
|
+
|
|
75
|
+
#### `dozor.start()`
|
|
76
|
+
|
|
77
|
+
Starts recording manually. Only needed when `autoStart: false`. No-op if not in `"idle"` state.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const dozor = Dozor.init({ apiKey: "dp_...", autoStart: false });
|
|
81
|
+
|
|
82
|
+
// later, when ready
|
|
83
|
+
dozor.start();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `dozor.pause()`
|
|
87
|
+
|
|
88
|
+
Pauses recording without destroying the session. Stops the rrweb recorder and the flush timer, but keeps the session ID and buffered events alive.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
dozor.pause();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use `resume()` to continue recording the same session.
|
|
95
|
+
|
|
96
|
+
#### `dozor.resume()`
|
|
97
|
+
|
|
98
|
+
Resumes recording after a `pause()`. Continues the same session with the same session ID. No-op if not paused.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
dozor.resume();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `dozor.stop()`
|
|
105
|
+
|
|
106
|
+
Stops recording permanently, flushes all remaining events (including held ones), and destroys the singleton. After `stop()`, call `Dozor.init()` to create a new instance with a new session.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
dozor.stop();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Note:** `stop()` always flushes — even if transport is held. This ensures no data is lost when you explicitly end a session.
|
|
113
|
+
|
|
114
|
+
#### `dozor.cancel()`
|
|
115
|
+
|
|
116
|
+
Discards the current session entirely. Stops recording, drops all buffered events without flushing, and sends a delete request to remove the session from the server. Destroys the singleton.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
dozor.cancel();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `dozor.hold()`
|
|
123
|
+
|
|
124
|
+
Holds the transport — recording continues but events are buffered in memory without being sent to the server. The rrweb recorder keeps capturing DOM mutations normally.
|
|
125
|
+
|
|
126
|
+
No-op if already held or stopped.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
dozor.hold();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `dozor.release(options?)`
|
|
133
|
+
|
|
134
|
+
Releases the transport hold. By default, flushes all buffered events and resumes normal sending. Pass `{ discard: true }` to drop the held events without sending them.
|
|
135
|
+
|
|
136
|
+
No-op if not held.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// Flush buffered events and resume normal sending
|
|
140
|
+
dozor.release();
|
|
141
|
+
|
|
142
|
+
// Drop buffered events and resume normal sending
|
|
143
|
+
dozor.release({ discard: true });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Option | Type | Default | Description |
|
|
147
|
+
| --------- | --------- | ------- | ------------------------------------------ |
|
|
148
|
+
| `discard` | `boolean` | `false` | Drop held events instead of flushing them. |
|
|
149
|
+
|
|
150
|
+
#### `dozor.setUserId(id)`
|
|
151
|
+
|
|
152
|
+
Sets or updates the user ID after init. The ID is included in session metadata sent to the server.
|
|
153
|
+
|
|
154
|
+
If metadata has already been sent (i.e., at least one batch was flushed), calling `setUserId()` triggers a metadata re-send on the next flush so the server receives the updated user ID.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
dozor.setUserId("user_123");
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Lifecycle
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
init(autoStart: true) ──> RECORDING
|
|
164
|
+
init(autoStart: false) ──> IDLE
|
|
165
|
+
|
|
166
|
+
IDLE ────── start() ───> RECORDING
|
|
167
|
+
RECORDING ─ pause() ───> PAUSED
|
|
168
|
+
PAUSED ──── resume() ──> RECORDING
|
|
169
|
+
RECORDING ─ stop() ────> STOPPED (flush + destroy)
|
|
170
|
+
PAUSED ──── stop() ────> STOPPED (flush + destroy)
|
|
171
|
+
IDLE ────── stop() ────> STOPPED (destroy)
|
|
172
|
+
RECORDING ─ cancel() ──> STOPPED (drop buffer + delete session)
|
|
173
|
+
PAUSED ──── cancel() ──> STOPPED (drop buffer + delete session)
|
|
174
|
+
IDLE ────── cancel() ──> STOPPED (delete session)
|
|
175
|
+
|
|
176
|
+
STOPPED ── init() ────> (new instance, new session)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Transport hold is orthogonal to lifecycle state — you can `hold()` and `release()` in any active state (`idle`, `recording`, or `paused`):
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
ANY ACTIVE ── hold() ─────> transport held (recording continues)
|
|
183
|
+
HELD ──────── release() ──> transport resumed (flush buffer)
|
|
184
|
+
HELD ──────── stop() ─────> STOPPED (force flush + destroy)
|
|
185
|
+
HELD ──────── cancel() ───> STOPPED (drop buffer + delete session)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Use cases
|
|
189
|
+
|
|
190
|
+
### Basic recording
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { Dozor } from "@kharko/dozor";
|
|
194
|
+
|
|
195
|
+
// Starts recording immediately. Events are batched and sent automatically.
|
|
196
|
+
Dozor.init({ apiKey: "dp_your_key" });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Deferred start
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const dozor = Dozor.init({ apiKey: "dp_your_key", autoStart: false });
|
|
203
|
+
|
|
204
|
+
// Start only when the user enters a specific section
|
|
205
|
+
onEnterCheckout(() => {
|
|
206
|
+
dozor.start();
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Conditional recording
|
|
211
|
+
|
|
212
|
+
Record a session but only save it if the user completes a valuable action (e.g., purchase, sign-up). If they don't, discard everything.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const dozor = Dozor.init({ apiKey: "dp_your_key", hold: true });
|
|
216
|
+
|
|
217
|
+
// Recording is active, but nothing is sent to the server.
|
|
218
|
+
// The user interacts with the page...
|
|
219
|
+
|
|
220
|
+
if (userCompletedPurchase) {
|
|
221
|
+
dozor.release(); // flush all buffered events, resume normal sending
|
|
222
|
+
} else {
|
|
223
|
+
dozor.cancel(); // discard the session entirely
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Network-aware buffering
|
|
228
|
+
|
|
229
|
+
Pause sending during heavy network activity so the tracker doesn't compete with business-critical requests.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const dozor = Dozor.init({ apiKey: "dp_your_key" });
|
|
233
|
+
|
|
234
|
+
// About to fire many parallel requests
|
|
235
|
+
dozor.hold();
|
|
236
|
+
await Promise.all([...heavyApiCalls]);
|
|
237
|
+
dozor.release(); // flush everything that accumulated during the hold
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Identify users
|
|
241
|
+
|
|
242
|
+
Link multiple sessions to the same user for cross-session analytics.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// Option A: known at init time
|
|
246
|
+
Dozor.init({ apiKey: "dp_your_key", userId: currentUser.id });
|
|
247
|
+
|
|
248
|
+
// Option B: user logs in after recording started
|
|
249
|
+
const dozor = Dozor.init({ apiKey: "dp_your_key" });
|
|
250
|
+
onLogin((user) => {
|
|
251
|
+
dozor.setUserId(user.id);
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Pause during sensitive input
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// User enters credit card details
|
|
259
|
+
dozor.pause();
|
|
260
|
+
|
|
261
|
+
// Done — resume recording
|
|
262
|
+
dozor.resume();
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Disable console recording
|
|
266
|
+
|
|
267
|
+
Console recording is enabled by default. Disable it if your app logs sensitive data or you want to reduce event volume.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
Dozor.init({ apiKey: "dp_your_key", recordConsole: false });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Disable auto-pause on hidden
|
|
274
|
+
|
|
275
|
+
By default, recording pauses when the tab is hidden and resumes when visible. Disable this if you want to keep recording in the background (e.g., long-running workflows where the user switches tabs).
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
Dozor.init({ apiKey: "dp_your_key", pauseOnHidden: false });
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Edge cases
|
|
282
|
+
|
|
283
|
+
| Scenario | Behavior |
|
|
284
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
+
| `init()` called multiple times | Returns the existing singleton. Does not re-initialize. |
|
|
286
|
+
| `start()` when already recording | No-op. |
|
|
287
|
+
| `pause()` when not recording | No-op. |
|
|
288
|
+
| `resume()` when not paused | No-op. |
|
|
289
|
+
| `stop()` when already stopped | No-op. |
|
|
290
|
+
| `cancel()` when already stopped | No-op. |
|
|
291
|
+
| `hold()` when already held | No-op. |
|
|
292
|
+
| `hold()` when stopped | No-op. |
|
|
293
|
+
| `release()` when not held | No-op. |
|
|
294
|
+
| `stop()` while held | Releases hold, flushes all events, destroys instance. No data is lost. |
|
|
295
|
+
| `cancel()` while held | Drops buffer, deletes session. Held events are discarded. |
|
|
296
|
+
| Tab hidden with `pauseOnHidden: true` (default) | Recording pauses automatically, resumes when the tab becomes visible. |
|
|
297
|
+
| Tab hidden after manual `pause()` | Recording was already paused by the user — auto-resume does **not** kick in when the tab becomes visible. Only `resume()` can resume. |
|
|
298
|
+
| Tab hidden with `pauseOnHidden: false` | No auto-pause. Events keep being recorded and flushed normally. |
|
|
299
|
+
| Page unload while held | Events are **not** sent. The user explicitly held transport, so the hold is respected. |
|
|
300
|
+
| Page unload while recording | Final events are sent via `fetch()` with `keepalive: true`. |
|
|
301
|
+
| Tab goes to background | Buffer is flushed immediately (unless held). |
|
|
302
|
+
| `setUserId()` after metadata was already sent | Triggers a metadata re-send on the next flush. |
|
|
303
|
+
| `setUserId()` before any flush | User ID is included in the first metadata payload. |
|
|
304
|
+
| `sessionStorage` unavailable | Session ID is generated in memory but not persisted across reloads. |
|
|
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). |
|
|
308
|
+
|
|
309
|
+
## How it works
|
|
310
|
+
|
|
311
|
+
### Recording
|
|
312
|
+
|
|
313
|
+
Uses [rrweb](https://github.com/rrweb-io/rrweb) to capture a full DOM snapshot on init, then records incremental mutations (DOM changes, mouse moves, scroll, input, etc.) as events.
|
|
314
|
+
|
|
315
|
+
### Sessions
|
|
316
|
+
|
|
317
|
+
Each session gets a UUID stored in `sessionStorage`. The ID persists across SPA navigations and page reloads within the same tab, but a new tab starts a new session. `stop()` and `cancel()` clear the stored ID.
|
|
318
|
+
|
|
319
|
+
### Batching
|
|
320
|
+
|
|
321
|
+
Events accumulate in an in-memory buffer. The buffer is flushed when:
|
|
322
|
+
|
|
323
|
+
- `flushInterval` ms have passed (default: 10s)
|
|
324
|
+
- The buffer reaches `batchSize` events (default: 500)
|
|
325
|
+
- The tab goes to the background (`visibilitychange`)
|
|
326
|
+
- The page is closing (`beforeunload`)
|
|
327
|
+
|
|
328
|
+
When transport is held, all flush triggers are suppressed — events stay in the buffer.
|
|
329
|
+
|
|
330
|
+
### Compression
|
|
331
|
+
|
|
332
|
+
Payloads larger than 1 KB are compressed with gzip via the browser-native [CompressionStream API](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream). Falls back to uncompressed JSON in environments without `CompressionStream`.
|
|
333
|
+
|
|
334
|
+
### Transport
|
|
335
|
+
|
|
336
|
+
- Regular flushes use `fetch()` with retry (3 attempts, exponential backoff: 1s → 2s → 4s). Client errors (4xx) are not retried.
|
|
337
|
+
- Page-unload flushes use `fetch()` with `keepalive: true` for best-effort delivery.
|
|
338
|
+
- All requests include an `X-Dozor-Public-Key` header for authentication and a `Content-Encoding: gzip` header when compressed.
|
|
339
|
+
|
|
340
|
+
### Metadata
|
|
341
|
+
|
|
342
|
+
The first batch of each session includes browser metadata:
|
|
343
|
+
|
|
344
|
+
| Field | Example |
|
|
345
|
+
| -------------- | -------------------------- |
|
|
346
|
+
| `url` | `https://example.com/page` |
|
|
347
|
+
| `referrer` | `https://google.com` |
|
|
348
|
+
| `userAgent` | `Mozilla/5.0 ...` |
|
|
349
|
+
| `screenWidth` | `1920` |
|
|
350
|
+
| `screenHeight` | `1080` |
|
|
351
|
+
| `language` | `en-US` |
|
|
352
|
+
| `userId` | `user_123` (if set) |
|
|
353
|
+
|
|
354
|
+
## Types
|
|
355
|
+
|
|
356
|
+
The package exports TypeScript types for use in your backend or tooling:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import type { DozorOptions, DozorState, IngestPayload, SessionMetadata } from "@kharko/dozor";
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Browser support
|
|
363
|
+
|
|
364
|
+
Works in all modern browsers that support:
|
|
365
|
+
|
|
366
|
+
- [`MutationObserver`](https://caniuse.com/mutationobserver) (rrweb requirement)
|
|
367
|
+
- [`crypto.randomUUID()`](https://caniuse.com/mdn-api_crypto_randomuuid)
|
|
368
|
+
- [`fetch()`](https://caniuse.com/fetch) with `keepalive`
|
|
369
|
+
- [`CompressionStream`](https://caniuse.com/mdn-api_compressionstream) (optional — falls back to uncompressed)
|
|
370
|
+
|
|
371
|
+
## Self-hosting
|
|
372
|
+
|
|
373
|
+
Point the SDK to your own ingest endpoint:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
Dozor.init({
|
|
377
|
+
apiKey: "dp_your_key",
|
|
378
|
+
endpoint: "https://your-server.com/api/ingest",
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
See the [Kharko Dozor repository](https://github.com/kolia-zamnius/kharko-dozor) for the full self-hosted setup.
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
'use strict';var rrweb=require('rrweb');function
|
|
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 m(){try{sessionStorage.removeItem(u);}catch{}}async function v(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,a){let n={sessionId:t,events:e};a&&(n.metadata=a);let r=JSON.stringify(n),o,c={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&r.length>1024?(o=await v(r),c["Content-Encoding"]="gzip"):o=r;for(let d=0;d<3;d++){try{let l=await fetch(this.endpoint,{method:"POST",headers:c,body:o,keepalive:!0});if(l.ok)return !0;if(l.status>=400&&l.status<500)return !1}catch{}d<2&&await S(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}),r,o={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&n.length>1024?(r=await v(n),o["Content-Encoding"]="gzip"):r=n;try{fetch(this.endpoint,{method:"POST",headers:o,body:r,keepalive:!0}).catch(()=>{});}catch{}}};function S(s){return new Promise(t=>setTimeout(t,s))}var b="https://dozor.kharko.dev/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??b;this.batchSize=t.batchSize??T,this.flushInterval=t.flushInterval??R;let a=t.autoStart??true;this._isHeld=t.hold??false,this._userId=t.userId??null,this._pauseOnHidden=t.pauseOnHidden??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");}),a?(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(){this.stopRecording=rrweb.record({emit:t=>this.onEvent(t),plugins:this._plugins})??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(){m(),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 p=i;
|
|
2
2
|
exports.Dozor=p;//# 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","SESSION_KEY","getSessionId","existing","id","Transport","endpoint","apiKey","sessionId","events","metadata","payload","body","attempt","res","sleep","ms","resolve","DEFAULT_ENDPOINT","DEFAULT_FLUSH_INTERVAL","DEFAULT_BATCH_SIZE","_Dozor","options","flushInterval","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(): SessionMetadata {\n return {\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}\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","import type { eventWithTime } from \"rrweb\";\nimport type { IngestPayload, SessionMetadata } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\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 body = JSON.stringify(payload);\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 \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\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 send via fetch with keepalive (for page unload). */\n sendKeepalive(sessionId: string, events: eventWithTime[]): void {\n if (events.length === 0) return;\n\n const payload: IngestPayload = { sessionId, events };\n\n try {\n fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\n body: JSON.stringify(payload),\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 { DozorOptions, SessionMetadata } from \"./types.js\";\nimport { collectMetadata } from \"./metadata.js\";\nimport { getSessionId } from \"./session.js\";\nimport { Transport } from \"./transport.js\";\n\nconst DEFAULT_ENDPOINT = \"https://dozor.kharko.dev/api/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 5_000;\nconst DEFAULT_BATCH_SIZE = 50;\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\n private constructor(options: DozorOptions) {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n const flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n\n this.transport = new Transport(endpoint, options.apiKey);\n this.sessionId = getSessionId();\n this.metadata = collectMetadata();\n\n this.stopRecording = record({\n emit: (event) => this.onEvent(event),\n }) ?? null;\n\n this.flushTimer = setInterval(() => this.flush(), flushInterval);\n\n addEventListener(\"beforeunload\", () => this.onBeforeUnload());\n addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") this.flush();\n });\n }\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 /** Stop recording and flush remaining events. */\n stop(): void {\n if (this.stopRecording) {\n this.stopRecording();\n this.stopRecording = null;\n }\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n this.flush();\n Dozor.instance = null;\n }\n\n private onEvent(event: eventWithTime): void {\n this.buffer.push(event);\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n private flush(): void {\n if (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.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","record","event","Dozor"],"mappings":"+GAGO,SAASA,CAAAA,CAAgBC,CAAAA,CAAyC,CACvE,IAAMC,CAAAA,CAAwB,CAC5B,GAAA,CAAK,QAAA,CAAS,IAAA,CACd,QAAA,CAAU,SAAS,QAAA,CACnB,SAAA,CAAW,SAAA,CAAU,SAAA,CACrB,WAAA,CAAa,MAAA,CAAO,MACpB,YAAA,CAAc,MAAA,CAAO,MAAA,CACrB,QAAA,CAAU,SAAA,CAAU,QACtB,EACA,OAAID,CAAAA,GAAQC,CAAAA,CAAK,MAAA,CAASD,CAAAA,CAAAA,CACnBC,CACT,CCdA,IAAMC,CAAAA,CAAc,kBAAA,CAGb,SAASC,CAAAA,EAAuB,CACrC,GAAI,CACF,IAAMC,CAAAA,CAAW,cAAA,CAAe,OAAA,CAAQF,CAAW,EACnD,GAAIE,CAAAA,CAAU,OAAOA,CACvB,CAAA,KAAQ,CAER,CAEA,IAAMC,CAAAA,CAAK,MAAA,CAAO,UAAA,EAAW,CAE7B,GAAI,CACF,cAAA,CAAe,OAAA,CAAQH,CAAAA,CAAaG,CAAE,EACxC,MAAQ,CAER,CAEA,OAAOA,CACT,CAGO,SAASC,GAAuB,CACrC,GAAI,CACF,cAAA,CAAe,UAAA,CAAWJ,CAAW,EACvC,CAAA,KAAQ,CAER,CACF,CCrBA,eAAeK,CAAAA,CAAaC,EAA8B,CACxD,IAAMC,CAAAA,CAAS,IAAI,IAAA,CAAK,CAACD,CAAK,CAAC,CAAA,CAAE,MAAA,EAAO,CAAE,WAAA,CAAY,IAAI,kBAAkB,MAAM,CAAC,EACnF,OAAO,IAAI,SAASC,CAAM,CAAA,CAAE,IAAA,EAC9B,CAGA,IAAMC,EAAsB,OAAO,iBAAA,CAAsB,GAAA,CAE5CC,CAAAA,CAAN,KAAgB,CAIrB,YAAYC,CAAAA,CAAkBC,CAAAA,CAAgB,CAC5C,IAAA,CAAK,QAAA,CAAWD,CAAAA,CAChB,KAAK,MAAA,CAASC,EAChB,CAGA,MAAM,IAAA,CAAKC,CAAAA,CAAmBC,EAAyBC,CAAAA,CAA8C,CACnG,IAAMC,CAAAA,CAAyB,CAAE,SAAA,CAAAH,EAAW,MAAA,CAAAC,CAAO,CAAA,CAC/CC,CAAAA,GAAUC,CAAAA,CAAQ,QAAA,CAAWD,GAEjC,IAAME,CAAAA,CAAO,IAAA,CAAK,SAAA,CAAUD,CAAO,CAAA,CAG/BE,EACEC,CAAAA,CAAkC,CACtC,cAAA,CAAgB,kBAAA,CAChB,oBAAA,CAAsB,IAAA,CAAK,MAC7B,CAAA,CAEIV,CAAAA,EAAuBQ,CAAAA,CAAK,MAAA,CAAS,IAAA,EACvCC,CAAAA,CAAO,MAAMZ,CAAAA,CAAaW,CAAI,CAAA,CAC9BE,CAAAA,CAAQ,kBAAkB,CAAA,CAAI,QAE9BD,CAAAA,CAAOD,CAAAA,CAGT,IAAA,IAASG,CAAAA,CAAU,CAAA,CAAGA,CAAAA,CAAU,EAAaA,CAAAA,EAAAA,CAAW,CACtD,GAAI,CACF,IAAMC,CAAAA,CAAM,MAAM,KAAA,CAAM,IAAA,CAAK,QAAA,CAAU,CACrC,MAAA,CAAQ,MAAA,CACR,QAAAF,CAAAA,CACA,IAAA,CAAAD,CAAAA,CACA,SAAA,CAAW,CAAA,CACb,CAAC,EAED,GAAIG,CAAAA,CAAI,EAAA,CAAI,OAAO,CAAA,CAAA,CAGnB,GAAIA,EAAI,MAAA,EAAU,GAAA,EAAOA,EAAI,MAAA,CAAS,GAAA,CAAK,OAAO,CAAA,CACpD,CAAA,KAAQ,CAER,CAEID,CAAAA,CAAU,CAAA,EACZ,MAAME,CAAAA,CAAM,GAAA,CAAgB,CAAA,EAAKF,CAAO,EAE5C,CAEA,OAAO,MACT,CAGA,aAAA,CAAcP,CAAAA,CAAyB,CACrC,IAAMU,EAAY,IAAA,CAAK,QAAA,CAAS,OAAA,CAAQ,SAAA,CAAW,kBAAkB,CAAA,CACrE,GAAI,CACF,KAAA,CAAMA,CAAAA,CAAW,CACf,MAAA,CAAQ,MAAA,CACR,QAAS,CACP,cAAA,CAAgB,kBAAA,CAChB,oBAAA,CAAsB,IAAA,CAAK,MAC7B,EACA,IAAA,CAAM,IAAA,CAAK,SAAA,CAAU,CAAE,SAAA,CAAAV,CAAU,CAAC,CACpC,CAAC,EAAE,KAAA,CAAM,IAAM,CAAC,CAAC,EACnB,CAAA,KAAQ,CAER,CACF,CAGA,MAAM,aAAA,CAAcA,CAAAA,CAAmBC,CAAAA,CAAwC,CAC7E,GAAIA,CAAAA,CAAO,SAAW,CAAA,CAAG,OAGzB,IAAMG,CAAAA,CAAO,IAAA,CAAK,SAAA,CADa,CAAE,SAAA,CAAAJ,CAAAA,CAAW,MAAA,CAAAC,CAAO,CAChB,CAAA,CAE/BI,EACEC,CAAAA,CAAkC,CACtC,cAAA,CAAgB,kBAAA,CAChB,oBAAA,CAAsB,IAAA,CAAK,MAC7B,CAAA,CAEIV,CAAAA,EAAuBQ,CAAAA,CAAK,MAAA,CAAS,IAAA,EACvCC,CAAAA,CAAO,MAAMZ,CAAAA,CAAaW,CAAI,CAAA,CAC9BE,CAAAA,CAAQ,kBAAkB,CAAA,CAAI,QAE9BD,CAAAA,CAAOD,CAAAA,CAGT,GAAI,CACF,KAAA,CAAM,KAAK,QAAA,CAAU,CACnB,MAAA,CAAQ,MAAA,CACR,OAAA,CAAAE,CAAAA,CACA,KAAAD,CAAAA,CACA,SAAA,CAAW,CAAA,CACb,CAAC,CAAA,CAAE,KAAA,CAAM,IAAM,CAAC,CAAC,EACnB,CAAA,KAAQ,CAER,CACF,CACF,CAAA,CAEA,SAASI,CAAAA,CAAME,CAAAA,CAA2B,CACxC,OAAO,IAAI,OAAA,CAASC,CAAAA,EAAY,UAAA,CAAWA,CAAAA,CAASD,CAAE,CAAC,CACzD,CClHA,IAAME,CAAAA,CAAmB,qCAAA,CACnBC,CAAAA,CAAyB,GAAA,CACzBC,EAAqB,GAAA,CAEdC,CAAAA,CAAN,MAAMA,CAAM,CAmBT,WAAA,CAAYC,EAAuB,CAd3C,IAAA,CAAQ,MAAA,CAA0B,EAAC,CAEnC,IAAA,CAAQ,aAAe,KAAA,CACvB,IAAA,CAAQ,UAAA,CAAoD,IAAA,CAC5D,IAAA,CAAQ,aAAA,CAAqC,KAO7C,IAAA,CAAQ,WAAA,CAAc,KAAA,CAIpB,IAAMnB,CAAAA,CAAWmB,CAAAA,CAAQ,UAAYJ,CAAAA,CACrC,IAAA,CAAK,SAAA,CAAYI,CAAAA,CAAQ,SAAA,EAAaF,CAAAA,CACtC,KAAK,aAAA,CAAgBE,CAAAA,CAAQ,aAAA,EAAiBH,CAAAA,CAC9C,IAAMI,CAAAA,CAAYD,EAAQ,SAAA,EAAa,IAAA,CACvC,IAAA,CAAK,OAAA,CAAUA,CAAAA,CAAQ,IAAA,EAAQ,MAC/B,IAAA,CAAK,OAAA,CAAUA,CAAAA,CAAQ,MAAA,EAAU,IAAA,CACjC,IAAA,CAAK,eAAiBA,CAAAA,CAAQ,aAAA,EAAiB,IAAA,CAE/C,IAAA,CAAK,QAAA,CAAW,GACZA,CAAAA,CAAQ,aAAA,GAAkB,OAC5B,IAAA,CAAK,QAAA,CAAS,KAAKE,+CAAAA,EAAwB,CAAA,CAG7C,IAAA,CAAK,SAAA,CAAY,IAAItB,EAAUC,CAAAA,CAAUmB,CAAAA,CAAQ,MAAM,CAAA,CACvD,IAAA,CAAK,UAAA,CAAa5B,GAAa,CAC/B,IAAA,CAAK,QAAA,CAAWJ,CAAAA,CAAgB,IAAA,CAAK,OAAO,EAG5C,gBAAA,CAAiB,cAAA,CAAgB,IAAM,IAAA,CAAK,cAAA,EAAgB,EAC5D,gBAAA,CAAiB,kBAAA,CAAoB,IAAM,CACrC,QAAA,CAAS,eAAA,GAAoB,UAC/B,IAAA,CAAK,KAAA,EAAM,CACP,IAAA,CAAK,cAAA,EAAkB,IAAA,CAAK,SAAW,WAAA,GACzC,IAAA,CAAK,iBAAA,EAAkB,CACvB,IAAA,CAAK,MAAA,CAAS,SACd,IAAA,CAAK,WAAA,CAAc,OAEZ,IAAA,CAAK,cAAA,EAAkB,KAAK,WAAA,EAAe,IAAA,CAAK,MAAA,GAAW,QAAA,GACpE,IAAA,CAAK,WAAA,CAAc,MACnB,IAAA,CAAK,cAAA,EAAe,CACpB,IAAA,CAAK,MAAA,CAAS,WAAA,EAElB,CAAC,CAAA,CAEGiC,CAAAA,EACF,IAAA,CAAK,cAAA,EAAe,CACpB,IAAA,CAAK,OAAS,WAAA,EAEd,IAAA,CAAK,MAAA,CAAS,OAElB,CAKA,OAAO,KAAKD,CAAAA,CAA8B,CACxC,OAAID,CAAAA,CAAM,QAAA,GACVA,CAAAA,CAAM,SAAW,IAAIA,CAAAA,CAAMC,CAAO,CAAA,CAAA,CAC3BD,CAAAA,CAAM,QACf,CAKA,IAAI,SAAA,EAAoB,CACtB,OAAO,IAAA,CAAK,UACd,CAGA,IAAI,WAAA,EAAuB,CACzB,OAAO,IAAA,CAAK,SAAW,WACzB,CAGA,IAAI,QAAA,EAAoB,CACtB,OAAO,KAAK,MAAA,GAAW,QACzB,CAGA,IAAI,KAAA,EAAoB,CACtB,OAAO,IAAA,CAAK,MACd,CAGA,IAAI,MAAA,EAAkB,CACpB,OAAO,IAAA,CAAK,OACd,CAGA,IAAI,MAAA,EAAwB,CAC1B,OAAO,IAAA,CAAK,OACd,CAGA,IAAI,UAAA,EAAqB,CACvB,OAAO,IAAA,CAAK,MAAA,CAAO,MACrB,CAKA,KAAA,EAAc,CACR,KAAK,MAAA,GAAW,MAAA,GACpB,IAAA,CAAK,cAAA,EAAe,CACpB,IAAA,CAAK,OAAS,WAAA,EAChB,CAGA,KAAA,EAAc,CACR,IAAA,CAAK,MAAA,GAAW,cACpB,IAAA,CAAK,iBAAA,EAAkB,CACvB,IAAA,CAAK,MAAA,CAAS,QAAA,CACd,KAAK,WAAA,CAAc,KAAA,EACrB,CAGA,MAAA,EAAe,CACT,IAAA,CAAK,SAAW,QAAA,GACpB,IAAA,CAAK,WAAA,CAAc,KAAA,CACnB,IAAA,CAAK,cAAA,GACL,IAAA,CAAK,MAAA,CAAS,WAAA,EAChB,CAGA,IAAA,EAAa,CACP,KAAK,MAAA,GAAW,SAAA,GACpB,IAAA,CAAK,OAAA,CAAU,KAAA,CACf,IAAA,CAAK,mBAAkB,CACvB,IAAA,CAAK,KAAA,EAAM,CACX,IAAA,CAAK,OAAA,IACP,CAGA,MAAA,EAAe,CACT,IAAA,CAAK,MAAA,GAAW,SAAA,GACpB,KAAK,iBAAA,EAAkB,CACvB,KAAK,MAAA,CAAS,GACd,IAAA,CAAK,SAAA,CAAU,aAAA,CAAc,IAAA,CAAK,UAAU,CAAA,CAC5C,KAAK,OAAA,EAAQ,EACf,CAOA,IAAA,EAAa,CACP,IAAA,CAAK,SAAW,SAAA,EAAa,IAAA,CAAK,OAAA,GACtC,IAAA,CAAK,OAAA,CAAU,IAAA,EACjB,CAOA,OAAA,CAAQC,CAAAA,CAAuC,CACxC,IAAA,CAAK,OAAA,GACV,IAAA,CAAK,QAAU,KAAA,CAEXA,CAAAA,EAAS,OAAA,CACX,IAAA,CAAK,MAAA,CAAS,GAEd,IAAA,CAAK,KAAA,EAAM,EAEf,CAOA,SAAA,CAAU1B,CAAAA,CAAkB,CAC1B,IAAA,CAAK,OAAA,CAAUA,CAAAA,CAEX,IAAA,CAAK,QAAA,GACP,IAAA,CAAK,SAAS,MAAA,CAASA,CAAAA,CAAAA,CAIrB,KAAK,YAAA,GACP,IAAA,CAAK,aAAe,KAAA,EAExB,CAKQ,cAAA,EAAuB,CAC7B,IAAA,CAAK,aAAA,CACH6B,aAAO,CACL,IAAA,CAAOC,CAAAA,EAAU,IAAA,CAAK,OAAA,CAAQA,CAAK,EACnC,OAAA,CAAS,IAAA,CAAK,QAChB,CAAC,CAAA,EAAK,IAAA,CAER,KAAK,UAAA,CAAa,WAAA,CAAY,IAAM,IAAA,CAAK,KAAA,EAAM,CAAG,KAAK,aAAa,EACtE,CAGQ,iBAAA,EAA0B,CAC5B,IAAA,CAAK,gBACP,IAAA,CAAK,aAAA,EAAc,CACnB,IAAA,CAAK,aAAA,CAAgB,IAAA,CAAA,CAEnB,KAAK,UAAA,GACP,aAAA,CAAc,IAAA,CAAK,UAAU,CAAA,CAC7B,IAAA,CAAK,WAAa,IAAA,EAEtB,CAGQ,SAAgB,CACtB7B,CAAAA,GACAwB,CAAAA,CAAM,QAAA,CAAW,IAAA,CACjB,IAAA,CAAK,MAAA,CAAS,UAChB,CAEQ,OAAA,CAAQK,CAAAA,CAA4B,CAC1C,IAAA,CAAK,MAAA,CAAO,IAAA,CAAKA,CAAK,CAAA,CAClB,IAAA,CAAK,MAAA,CAAO,MAAA,EAAU,IAAA,CAAK,SAAA,EAC7B,KAAK,KAAA,GAET,CAEQ,KAAA,EAAc,CACpB,GAAI,KAAK,OAAA,EAAW,IAAA,CAAK,MAAA,CAAO,MAAA,GAAW,CAAA,CAAG,OAE9C,IAAMpB,CAAAA,CAAS,IAAA,CAAK,MAAA,CACpB,IAAA,CAAK,MAAA,CAAS,GAEd,IAAMC,CAAAA,CAAY,IAAA,CAAK,YAAA,CAA4C,MAAA,CAA7B,IAAA,CAAK,UAAY,MAAA,CACnDA,CAAAA,GAAU,IAAA,CAAK,YAAA,CAAe,IAAA,CAAA,CAElC,IAAA,CAAK,UAAU,IAAA,CAAK,IAAA,CAAK,UAAA,CAAYD,CAAAA,CAAQC,CAAQ,CAAA,CAAE,MAAM,IAAM,CAAC,CAAC,EACvE,CAEQ,cAAA,EAAuB,CAC7B,GAAI,IAAA,CAAK,MAAA,GAAW,SAAA,EAAa,IAAA,CAAK,OAAA,EAAW,KAAK,MAAA,CAAO,MAAA,GAAW,CAAA,CAAG,OAE3E,IAAMD,CAAAA,CAAS,KAAK,MAAA,CACpB,IAAA,CAAK,MAAA,CAAS,EAAC,CAEf,IAAA,CAAK,UAAU,aAAA,CAAc,IAAA,CAAK,UAAA,CAAYA,CAAM,EACtD,CACF,EA9Pae,CAAAA,CACI,QAAA,CAAyB,IAAA,CADnC,IAAMM,CAAAA,CAANN","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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { eventWithTime } from 'rrweb';
|
|
2
2
|
|
|
3
|
+
type DozorState = "idle" | "recording" | "paused" | "stopped";
|
|
3
4
|
interface DozorOptions {
|
|
4
5
|
/** Public API key (dp_...) */
|
|
5
6
|
apiKey: string;
|
|
6
7
|
/** Ingest endpoint URL. Defaults to https://dozor.kharko.dev/api/ingest */
|
|
7
8
|
endpoint?: string;
|
|
8
|
-
/** Flush interval in ms. Default:
|
|
9
|
+
/** Flush interval in ms. Default: 10000 */
|
|
9
10
|
flushInterval?: number;
|
|
10
|
-
/** Max events per batch before auto-flush. Default:
|
|
11
|
+
/** Max events per batch before auto-flush. Default: 500 */
|
|
11
12
|
batchSize?: number;
|
|
13
|
+
/** Start recording immediately on init. Default: true */
|
|
14
|
+
autoStart?: boolean;
|
|
15
|
+
/** Start with transport held — events are buffered locally but not sent until `release()` is called. Default: false */
|
|
16
|
+
hold?: boolean;
|
|
17
|
+
/** Optional stable user identifier. Sent with metadata to link multiple sessions to the same user. Can also be set later via `setUserId()`. */
|
|
18
|
+
userId?: string;
|
|
19
|
+
/** Automatically pause recording when the tab is hidden and resume when visible. Default: true */
|
|
20
|
+
pauseOnHidden?: boolean;
|
|
21
|
+
/** Record console.log/warn/error/info/debug calls. Default: true */
|
|
22
|
+
recordConsole?: boolean;
|
|
12
23
|
}
|
|
13
24
|
interface SessionMetadata {
|
|
14
25
|
url: string;
|
|
@@ -17,6 +28,7 @@ interface SessionMetadata {
|
|
|
17
28
|
screenWidth: number;
|
|
18
29
|
screenHeight: number;
|
|
19
30
|
language: string;
|
|
31
|
+
userId?: string;
|
|
20
32
|
}
|
|
21
33
|
interface IngestPayload {
|
|
22
34
|
sessionId: string;
|
|
@@ -27,21 +39,76 @@ interface IngestPayload {
|
|
|
27
39
|
declare class Dozor {
|
|
28
40
|
private static instance;
|
|
29
41
|
private transport;
|
|
30
|
-
private
|
|
42
|
+
private _sessionId;
|
|
31
43
|
private buffer;
|
|
32
44
|
private metadata;
|
|
33
45
|
private metadataSent;
|
|
34
46
|
private flushTimer;
|
|
35
47
|
private stopRecording;
|
|
36
48
|
private batchSize;
|
|
49
|
+
private flushInterval;
|
|
50
|
+
private _state;
|
|
51
|
+
private _isHeld;
|
|
52
|
+
private _userId;
|
|
53
|
+
private _pauseOnHidden;
|
|
54
|
+
private _autoPaused;
|
|
55
|
+
private _plugins;
|
|
37
56
|
private constructor();
|
|
38
57
|
/** Initialize the Dozor recorder. Returns the singleton instance. */
|
|
39
58
|
static init(options: DozorOptions): Dozor;
|
|
40
|
-
/**
|
|
59
|
+
/** Current session ID (UUID v4). */
|
|
60
|
+
get sessionId(): string;
|
|
61
|
+
/** `true` when actively recording. */
|
|
62
|
+
get isRecording(): boolean;
|
|
63
|
+
/** `true` when paused via `pause()`. */
|
|
64
|
+
get isPaused(): boolean;
|
|
65
|
+
/** Current lifecycle state. */
|
|
66
|
+
get state(): DozorState;
|
|
67
|
+
/** `true` when transport is held — events are buffered locally but not sent. */
|
|
68
|
+
get isHeld(): boolean;
|
|
69
|
+
/** Current user ID, or `null` if not set. */
|
|
70
|
+
get userId(): string | null;
|
|
71
|
+
/** Number of events currently buffered in memory (not yet sent). */
|
|
72
|
+
get bufferSize(): number;
|
|
73
|
+
/** Start recording manually. Only needed when `autoStart: false`. No-op if already recording. */
|
|
74
|
+
start(): void;
|
|
75
|
+
/** Pause recording without destroying the session. Keeps the session ID and buffered events alive. */
|
|
76
|
+
pause(): void;
|
|
77
|
+
/** Resume recording after a `pause()`. Continues the same session. */
|
|
78
|
+
resume(): void;
|
|
79
|
+
/** Stop recording permanently, flush remaining events (even if held), and destroy the singleton. */
|
|
41
80
|
stop(): void;
|
|
81
|
+
/** Discard the current session. Drops buffered events and sends a delete request to the server. */
|
|
82
|
+
cancel(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Hold the transport — recording continues but events are buffered locally without being sent.
|
|
85
|
+
* Use `release()` to flush the buffer and resume normal sending, or `cancel()` to discard everything.
|
|
86
|
+
* No-op if already held or stopped.
|
|
87
|
+
*/
|
|
88
|
+
hold(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Release the transport hold — flush buffered events and resume normal sending.
|
|
91
|
+
* Pass `{ discard: true }` to drop held events without sending them.
|
|
92
|
+
* No-op if not held.
|
|
93
|
+
*/
|
|
94
|
+
release(options?: {
|
|
95
|
+
discard?: boolean;
|
|
96
|
+
}): void;
|
|
97
|
+
/**
|
|
98
|
+
* Set or update the user ID after init.
|
|
99
|
+
* Useful when the user logs in after recording has already started.
|
|
100
|
+
* The ID will be included in the next metadata/batch sent to the server.
|
|
101
|
+
*/
|
|
102
|
+
setUserId(id: string): void;
|
|
103
|
+
/** Start rrweb recording and the flush timer. */
|
|
104
|
+
private startRecording;
|
|
105
|
+
/** Stop rrweb and clear the flush timer (without flushing). */
|
|
106
|
+
private teardownRecording;
|
|
107
|
+
/** Clear session and destroy the singleton. */
|
|
108
|
+
private destroy;
|
|
42
109
|
private onEvent;
|
|
43
110
|
private flush;
|
|
44
111
|
private onBeforeUnload;
|
|
45
112
|
}
|
|
46
113
|
|
|
47
|
-
export { Dozor, type DozorOptions, type IngestPayload, type SessionMetadata };
|
|
114
|
+
export { Dozor, type DozorOptions, type DozorState, type IngestPayload, type SessionMetadata };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { eventWithTime } from 'rrweb';
|
|
2
2
|
|
|
3
|
+
type DozorState = "idle" | "recording" | "paused" | "stopped";
|
|
3
4
|
interface DozorOptions {
|
|
4
5
|
/** Public API key (dp_...) */
|
|
5
6
|
apiKey: string;
|
|
6
7
|
/** Ingest endpoint URL. Defaults to https://dozor.kharko.dev/api/ingest */
|
|
7
8
|
endpoint?: string;
|
|
8
|
-
/** Flush interval in ms. Default:
|
|
9
|
+
/** Flush interval in ms. Default: 10000 */
|
|
9
10
|
flushInterval?: number;
|
|
10
|
-
/** Max events per batch before auto-flush. Default:
|
|
11
|
+
/** Max events per batch before auto-flush. Default: 500 */
|
|
11
12
|
batchSize?: number;
|
|
13
|
+
/** Start recording immediately on init. Default: true */
|
|
14
|
+
autoStart?: boolean;
|
|
15
|
+
/** Start with transport held — events are buffered locally but not sent until `release()` is called. Default: false */
|
|
16
|
+
hold?: boolean;
|
|
17
|
+
/** Optional stable user identifier. Sent with metadata to link multiple sessions to the same user. Can also be set later via `setUserId()`. */
|
|
18
|
+
userId?: string;
|
|
19
|
+
/** Automatically pause recording when the tab is hidden and resume when visible. Default: true */
|
|
20
|
+
pauseOnHidden?: boolean;
|
|
21
|
+
/** Record console.log/warn/error/info/debug calls. Default: true */
|
|
22
|
+
recordConsole?: boolean;
|
|
12
23
|
}
|
|
13
24
|
interface SessionMetadata {
|
|
14
25
|
url: string;
|
|
@@ -17,6 +28,7 @@ interface SessionMetadata {
|
|
|
17
28
|
screenWidth: number;
|
|
18
29
|
screenHeight: number;
|
|
19
30
|
language: string;
|
|
31
|
+
userId?: string;
|
|
20
32
|
}
|
|
21
33
|
interface IngestPayload {
|
|
22
34
|
sessionId: string;
|
|
@@ -27,21 +39,76 @@ interface IngestPayload {
|
|
|
27
39
|
declare class Dozor {
|
|
28
40
|
private static instance;
|
|
29
41
|
private transport;
|
|
30
|
-
private
|
|
42
|
+
private _sessionId;
|
|
31
43
|
private buffer;
|
|
32
44
|
private metadata;
|
|
33
45
|
private metadataSent;
|
|
34
46
|
private flushTimer;
|
|
35
47
|
private stopRecording;
|
|
36
48
|
private batchSize;
|
|
49
|
+
private flushInterval;
|
|
50
|
+
private _state;
|
|
51
|
+
private _isHeld;
|
|
52
|
+
private _userId;
|
|
53
|
+
private _pauseOnHidden;
|
|
54
|
+
private _autoPaused;
|
|
55
|
+
private _plugins;
|
|
37
56
|
private constructor();
|
|
38
57
|
/** Initialize the Dozor recorder. Returns the singleton instance. */
|
|
39
58
|
static init(options: DozorOptions): Dozor;
|
|
40
|
-
/**
|
|
59
|
+
/** Current session ID (UUID v4). */
|
|
60
|
+
get sessionId(): string;
|
|
61
|
+
/** `true` when actively recording. */
|
|
62
|
+
get isRecording(): boolean;
|
|
63
|
+
/** `true` when paused via `pause()`. */
|
|
64
|
+
get isPaused(): boolean;
|
|
65
|
+
/** Current lifecycle state. */
|
|
66
|
+
get state(): DozorState;
|
|
67
|
+
/** `true` when transport is held — events are buffered locally but not sent. */
|
|
68
|
+
get isHeld(): boolean;
|
|
69
|
+
/** Current user ID, or `null` if not set. */
|
|
70
|
+
get userId(): string | null;
|
|
71
|
+
/** Number of events currently buffered in memory (not yet sent). */
|
|
72
|
+
get bufferSize(): number;
|
|
73
|
+
/** Start recording manually. Only needed when `autoStart: false`. No-op if already recording. */
|
|
74
|
+
start(): void;
|
|
75
|
+
/** Pause recording without destroying the session. Keeps the session ID and buffered events alive. */
|
|
76
|
+
pause(): void;
|
|
77
|
+
/** Resume recording after a `pause()`. Continues the same session. */
|
|
78
|
+
resume(): void;
|
|
79
|
+
/** Stop recording permanently, flush remaining events (even if held), and destroy the singleton. */
|
|
41
80
|
stop(): void;
|
|
81
|
+
/** Discard the current session. Drops buffered events and sends a delete request to the server. */
|
|
82
|
+
cancel(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Hold the transport — recording continues but events are buffered locally without being sent.
|
|
85
|
+
* Use `release()` to flush the buffer and resume normal sending, or `cancel()` to discard everything.
|
|
86
|
+
* No-op if already held or stopped.
|
|
87
|
+
*/
|
|
88
|
+
hold(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Release the transport hold — flush buffered events and resume normal sending.
|
|
91
|
+
* Pass `{ discard: true }` to drop held events without sending them.
|
|
92
|
+
* No-op if not held.
|
|
93
|
+
*/
|
|
94
|
+
release(options?: {
|
|
95
|
+
discard?: boolean;
|
|
96
|
+
}): void;
|
|
97
|
+
/**
|
|
98
|
+
* Set or update the user ID after init.
|
|
99
|
+
* Useful when the user logs in after recording has already started.
|
|
100
|
+
* The ID will be included in the next metadata/batch sent to the server.
|
|
101
|
+
*/
|
|
102
|
+
setUserId(id: string): void;
|
|
103
|
+
/** Start rrweb recording and the flush timer. */
|
|
104
|
+
private startRecording;
|
|
105
|
+
/** Stop rrweb and clear the flush timer (without flushing). */
|
|
106
|
+
private teardownRecording;
|
|
107
|
+
/** Clear session and destroy the singleton. */
|
|
108
|
+
private destroy;
|
|
42
109
|
private onEvent;
|
|
43
110
|
private flush;
|
|
44
111
|
private onBeforeUnload;
|
|
45
112
|
}
|
|
46
113
|
|
|
47
|
-
export { Dozor, type DozorOptions, type IngestPayload, type SessionMetadata };
|
|
114
|
+
export { Dozor, type DozorOptions, type DozorState, type IngestPayload, type SessionMetadata };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {record}from'rrweb';function
|
|
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 m(){try{sessionStorage.removeItem(u);}catch{}}async function v(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,a){let n={sessionId:t,events:e};a&&(n.metadata=a);let r=JSON.stringify(n),o,c={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&r.length>1024?(o=await v(r),c["Content-Encoding"]="gzip"):o=r;for(let d=0;d<3;d++){try{let l=await fetch(this.endpoint,{method:"POST",headers:c,body:o,keepalive:!0});if(l.ok)return !0;if(l.status>=400&&l.status<500)return !1}catch{}d<2&&await S(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}),r,o={"Content-Type":"application/json","X-Dozor-Public-Key":this.apiKey};_&&n.length>1024?(r=await v(n),o["Content-Encoding"]="gzip"):r=n;try{fetch(this.endpoint,{method:"POST",headers:o,body:r,keepalive:!0}).catch(()=>{});}catch{}}};function S(s){return new Promise(t=>setTimeout(t,s))}var b="https://dozor.kharko.dev/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??b;this.batchSize=t.batchSize??T,this.flushInterval=t.flushInterval??R;let a=t.autoStart??true;this._isHeld=t.hold??false,this._userId=t.userId??null,this._pauseOnHidden=t.pauseOnHidden??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");}),a?(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(){this.stopRecording=record({emit:t=>this.onEvent(t),plugins:this._plugins})??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(){m(),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 p=i;
|
|
2
2
|
export{p 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","SESSION_KEY","getSessionId","existing","id","Transport","endpoint","apiKey","sessionId","events","metadata","payload","body","attempt","res","sleep","ms","resolve","DEFAULT_ENDPOINT","DEFAULT_FLUSH_INTERVAL","DEFAULT_BATCH_SIZE","_Dozor","options","flushInterval","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(): SessionMetadata {\n return {\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}\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","import type { eventWithTime } from \"rrweb\";\nimport type { IngestPayload, SessionMetadata } from \"./types.js\";\n\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\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 body = JSON.stringify(payload);\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 \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\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 send via fetch with keepalive (for page unload). */\n sendKeepalive(sessionId: string, events: eventWithTime[]): void {\n if (events.length === 0) return;\n\n const payload: IngestPayload = { sessionId, events };\n\n try {\n fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Dozor-Public-Key\": this.apiKey,\n },\n body: JSON.stringify(payload),\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 { DozorOptions, SessionMetadata } from \"./types.js\";\nimport { collectMetadata } from \"./metadata.js\";\nimport { getSessionId } from \"./session.js\";\nimport { Transport } from \"./transport.js\";\n\nconst DEFAULT_ENDPOINT = \"https://dozor.kharko.dev/api/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 5_000;\nconst DEFAULT_BATCH_SIZE = 50;\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\n private constructor(options: DozorOptions) {\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n const flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n\n this.transport = new Transport(endpoint, options.apiKey);\n this.sessionId = getSessionId();\n this.metadata = collectMetadata();\n\n this.stopRecording = record({\n emit: (event) => this.onEvent(event),\n }) ?? null;\n\n this.flushTimer = setInterval(() => this.flush(), flushInterval);\n\n addEventListener(\"beforeunload\", () => this.onBeforeUnload());\n addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") this.flush();\n });\n }\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 /** Stop recording and flush remaining events. */\n stop(): void {\n if (this.stopRecording) {\n this.stopRecording();\n this.stopRecording = null;\n }\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n this.flush();\n Dozor.instance = null;\n }\n\n private onEvent(event: eventWithTime): void {\n this.buffer.push(event);\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n private flush(): void {\n if (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.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","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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kharko/dozor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lightweight session recording SDK for Kharko Dozor",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dev": "tsup --watch"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@rrweb/rrweb-plugin-console-record": "2.0.0-alpha.20",
|
|
30
31
|
"rrweb": "2.0.0-alpha.20"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|