@cedarai/session-replay-sdk 0.1.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/dist/global.global.js +4076 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +420 -0
- package/package.json +31 -0
- package/src/console.test.ts +148 -0
- package/src/console.ts +54 -0
- package/src/errors.test.ts +146 -0
- package/src/errors.ts +99 -0
- package/src/global.ts +3 -0
- package/src/index.test.ts +207 -0
- package/src/index.ts +122 -0
- package/src/network.test.ts +135 -0
- package/src/network.ts +112 -0
- package/src/recorder.test.ts +187 -0
- package/src/recorder.ts +47 -0
- package/src/transport.test.ts +256 -0
- package/src/transport.ts +114 -0
- package/src/types.ts +101 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { Transport } from './transport.js';
|
|
3
|
+
import type { SessionEvent, SDKConfig } from './types.js';
|
|
4
|
+
|
|
5
|
+
function makeConfig(overrides: Partial<SDKConfig> = {}): SDKConfig {
|
|
6
|
+
return {
|
|
7
|
+
serverUrl: 'https://replay.getcedar.ai',
|
|
8
|
+
cedarSessionId: 'test-session-123',
|
|
9
|
+
batchIntervalMs: 5000,
|
|
10
|
+
batchMaxSize: 1024 * 512,
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeRRWebEvent(timestamp = Date.now()): SessionEvent {
|
|
16
|
+
return { type: 'rrweb', data: { type: 2, timestamp, data: { node: { type: 0 } } } };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeConsoleEvent(timestamp = Date.now()): SessionEvent {
|
|
20
|
+
return { type: 'console', timestamp, data: { level: 'log', args: ['test'] } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Transport', () => {
|
|
24
|
+
let transport: Transport;
|
|
25
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.useFakeTimers();
|
|
29
|
+
// Mock fetch globally
|
|
30
|
+
fetchSpy = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
|
31
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
transport?.stop();
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── Event queuing ────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe('event queuing', () => {
|
|
43
|
+
it('queues events without immediately sending', () => {
|
|
44
|
+
transport = new Transport(makeConfig());
|
|
45
|
+
transport.start();
|
|
46
|
+
transport.enqueue(makeRRWebEvent());
|
|
47
|
+
transport.enqueue(makeConsoleEvent());
|
|
48
|
+
|
|
49
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not send events before start() is called', () => {
|
|
53
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 100 }));
|
|
54
|
+
transport.enqueue(makeRRWebEvent());
|
|
55
|
+
|
|
56
|
+
vi.advanceTimersByTime(200);
|
|
57
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── Flush on interval ────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe('flush on interval', () => {
|
|
64
|
+
it('flushes after batchIntervalMs', () => {
|
|
65
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 5000 }));
|
|
66
|
+
transport.start();
|
|
67
|
+
transport.enqueue(makeRRWebEvent());
|
|
68
|
+
|
|
69
|
+
vi.advanceTimersByTime(5000);
|
|
70
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not flush when queue is empty', () => {
|
|
74
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 5000 }));
|
|
75
|
+
transport.start();
|
|
76
|
+
|
|
77
|
+
vi.advanceTimersByTime(5000);
|
|
78
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('flushes repeatedly on each interval', () => {
|
|
82
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 1000 }));
|
|
83
|
+
transport.start();
|
|
84
|
+
|
|
85
|
+
transport.enqueue(makeRRWebEvent());
|
|
86
|
+
vi.advanceTimersByTime(1000);
|
|
87
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
88
|
+
|
|
89
|
+
transport.enqueue(makeConsoleEvent());
|
|
90
|
+
vi.advanceTimersByTime(1000);
|
|
91
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── Flush on size ────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe('flush on size', () => {
|
|
98
|
+
it('flushes when batch exceeds batchMaxSize', () => {
|
|
99
|
+
transport = new Transport(makeConfig({ batchMaxSize: 100 })); // tiny limit
|
|
100
|
+
transport.start();
|
|
101
|
+
|
|
102
|
+
// Each event is ~60+ bytes JSON, so 2 should exceed 100 bytes
|
|
103
|
+
transport.enqueue(makeRRWebEvent());
|
|
104
|
+
transport.enqueue(makeRRWebEvent());
|
|
105
|
+
|
|
106
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── Payload format ───────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describe('payload format', () => {
|
|
113
|
+
it('sends correct payload structure to serverUrl/api/ingest', () => {
|
|
114
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 1000 }));
|
|
115
|
+
transport.start();
|
|
116
|
+
transport.enqueue(makeRRWebEvent(1000));
|
|
117
|
+
transport.enqueue(makeConsoleEvent(1100));
|
|
118
|
+
|
|
119
|
+
vi.advanceTimersByTime(1000);
|
|
120
|
+
|
|
121
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
122
|
+
const [url, options] = fetchSpy.mock.calls[0];
|
|
123
|
+
expect(url).toBe('https://replay.getcedar.ai/api/ingest');
|
|
124
|
+
expect(options.method).toBe('POST');
|
|
125
|
+
expect(options.headers['Content-Type']).toBe('application/json');
|
|
126
|
+
|
|
127
|
+
const body = JSON.parse(options.body);
|
|
128
|
+
expect(body.sessionId).toBe('test-session-123');
|
|
129
|
+
expect(body.events).toHaveLength(2);
|
|
130
|
+
expect(body.events[0].type).toBe('rrweb');
|
|
131
|
+
expect(body.events[1].type).toBe('console');
|
|
132
|
+
expect(typeof body.batchTimestamp).toBe('number');
|
|
133
|
+
expect(typeof body.startedAt).toBe('number');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('clears the queue after successful flush', () => {
|
|
137
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 1000 }));
|
|
138
|
+
transport.start();
|
|
139
|
+
transport.enqueue(makeRRWebEvent());
|
|
140
|
+
|
|
141
|
+
vi.advanceTimersByTime(1000);
|
|
142
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
143
|
+
|
|
144
|
+
// Next interval: no events, should not send
|
|
145
|
+
vi.advanceTimersByTime(1000);
|
|
146
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── Manual flush ─────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe('flush()', () => {
|
|
153
|
+
it('immediately sends queued events', () => {
|
|
154
|
+
transport = new Transport(makeConfig());
|
|
155
|
+
transport.start();
|
|
156
|
+
transport.enqueue(makeRRWebEvent());
|
|
157
|
+
|
|
158
|
+
transport.flush();
|
|
159
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('does nothing if queue is empty', () => {
|
|
163
|
+
transport = new Transport(makeConfig());
|
|
164
|
+
transport.start();
|
|
165
|
+
|
|
166
|
+
transport.flush();
|
|
167
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── stop() ───────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe('stop()', () => {
|
|
174
|
+
it('flushes remaining events on stop', () => {
|
|
175
|
+
transport = new Transport(makeConfig());
|
|
176
|
+
transport.start();
|
|
177
|
+
transport.enqueue(makeRRWebEvent());
|
|
178
|
+
|
|
179
|
+
transport.stop();
|
|
180
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('stops the interval timer', () => {
|
|
184
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 1000 }));
|
|
185
|
+
transport.start();
|
|
186
|
+
transport.stop();
|
|
187
|
+
|
|
188
|
+
transport.enqueue(makeRRWebEvent());
|
|
189
|
+
vi.advanceTimersByTime(2000);
|
|
190
|
+
// Only the flush from stop(), not from interval
|
|
191
|
+
expect(fetchSpy).toHaveBeenCalledTimes(0); // stop flushed empty, enqueue after stop is ignored
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Self-interception guard ──────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
describe('self-interception', () => {
|
|
198
|
+
it('uses the provided originalFetch, not window.fetch', () => {
|
|
199
|
+
const originalFetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
|
200
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 1000 }), originalFetch);
|
|
201
|
+
transport.start();
|
|
202
|
+
transport.enqueue(makeRRWebEvent());
|
|
203
|
+
|
|
204
|
+
vi.advanceTimersByTime(1000);
|
|
205
|
+
|
|
206
|
+
// Should call originalFetch, not window.fetch
|
|
207
|
+
expect(originalFetch).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ── sendBeacon on unload ─────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
describe('sendBeacon fallback', () => {
|
|
215
|
+
it('uses sendBeacon when flushOnUnload is called', () => {
|
|
216
|
+
const sendBeaconSpy = vi.fn().mockReturnValue(true);
|
|
217
|
+
vi.stubGlobal('navigator', { sendBeacon: sendBeaconSpy });
|
|
218
|
+
|
|
219
|
+
transport = new Transport(makeConfig());
|
|
220
|
+
transport.start();
|
|
221
|
+
transport.enqueue(makeRRWebEvent());
|
|
222
|
+
|
|
223
|
+
transport.flushOnUnload();
|
|
224
|
+
|
|
225
|
+
expect(sendBeaconSpy).toHaveBeenCalledTimes(1);
|
|
226
|
+
const [url] = sendBeaconSpy.mock.calls[0];
|
|
227
|
+
expect(url).toContain('https://replay.getcedar.ai/api/ingest');
|
|
228
|
+
expect(url).toContain('encoding=gzip');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('does nothing on unload if queue is empty', () => {
|
|
232
|
+
const sendBeaconSpy = vi.fn().mockReturnValue(true);
|
|
233
|
+
vi.stubGlobal('navigator', { sendBeacon: sendBeaconSpy });
|
|
234
|
+
|
|
235
|
+
transport = new Transport(makeConfig());
|
|
236
|
+
transport.start();
|
|
237
|
+
|
|
238
|
+
transport.flushOnUnload();
|
|
239
|
+
expect(sendBeaconSpy).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ── Error resilience ─────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
describe('error resilience', () => {
|
|
246
|
+
it('does not throw if fetch fails', () => {
|
|
247
|
+
fetchSpy.mockRejectedValue(new Error('Network error'));
|
|
248
|
+
|
|
249
|
+
transport = new Transport(makeConfig({ batchIntervalMs: 1000 }));
|
|
250
|
+
transport.start();
|
|
251
|
+
transport.enqueue(makeRRWebEvent());
|
|
252
|
+
|
|
253
|
+
expect(() => vi.advanceTimersByTime(1000)).not.toThrow();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { SessionEvent, SDKConfig, BatchPayload } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class Transport {
|
|
4
|
+
private config: SDKConfig;
|
|
5
|
+
private queue: SessionEvent[] = [];
|
|
6
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
7
|
+
private started = false;
|
|
8
|
+
private startedAt: number = Date.now();
|
|
9
|
+
private fetchFn: typeof fetch;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param config SDK configuration
|
|
13
|
+
* @param originalFetch Original fetch reference to avoid self-interception.
|
|
14
|
+
* If not provided, uses globalThis.fetch.
|
|
15
|
+
*/
|
|
16
|
+
constructor(config: SDKConfig, originalFetch?: typeof fetch) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
const fn = originalFetch ?? globalThis.fetch;
|
|
19
|
+
this.fetchFn = fn.bind(globalThis);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start(): void {
|
|
23
|
+
if (this.started) return;
|
|
24
|
+
this.started = true;
|
|
25
|
+
this.startedAt = Date.now();
|
|
26
|
+
|
|
27
|
+
const interval = this.config.batchIntervalMs ?? 5000;
|
|
28
|
+
this.timer = setInterval(() => {
|
|
29
|
+
this.flush();
|
|
30
|
+
}, interval);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stop(): void {
|
|
34
|
+
if (!this.started) return;
|
|
35
|
+
this.started = false;
|
|
36
|
+
|
|
37
|
+
if (this.timer !== null) {
|
|
38
|
+
clearInterval(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.flush();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
enqueue(event: SessionEvent): void {
|
|
46
|
+
if (!this.started) return;
|
|
47
|
+
|
|
48
|
+
this.queue.push(event);
|
|
49
|
+
|
|
50
|
+
// Check if batch exceeds max size
|
|
51
|
+
const maxSize = this.config.batchMaxSize ?? 1024 * 512;
|
|
52
|
+
const estimatedSize = this.estimateQueueSize();
|
|
53
|
+
if (estimatedSize >= maxSize) {
|
|
54
|
+
this.flush();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
flush(): void {
|
|
59
|
+
if (this.queue.length === 0) return;
|
|
60
|
+
|
|
61
|
+
const events = this.queue.splice(0);
|
|
62
|
+
const payload = this.buildPayload(events);
|
|
63
|
+
|
|
64
|
+
// Fire and forget — don't block on the response
|
|
65
|
+
this.fetchFn(`${this.config.serverUrl}/api/ingest`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify(payload),
|
|
69
|
+
}).catch(() => {
|
|
70
|
+
// Silently ignore send failures — don't crash the host app
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Flush via sendBeacon for page unload. sendBeacon cannot set custom headers,
|
|
76
|
+
* so we send as a Blob and indicate gzip via query param.
|
|
77
|
+
*/
|
|
78
|
+
flushOnUnload(): void {
|
|
79
|
+
if (this.queue.length === 0) return;
|
|
80
|
+
|
|
81
|
+
const events = this.queue.splice(0);
|
|
82
|
+
const payload = this.buildPayload(events);
|
|
83
|
+
const body = JSON.stringify(payload);
|
|
84
|
+
|
|
85
|
+
// sendBeacon has a ~64KB limit. For now, send uncompressed JSON as a Blob.
|
|
86
|
+
// TODO: Add gzip compression (pako) and truncation for large payloads.
|
|
87
|
+
const blob = new Blob([body], { type: 'application/json' });
|
|
88
|
+
const url = `${this.config.serverUrl}/api/ingest?encoding=gzip`;
|
|
89
|
+
|
|
90
|
+
navigator.sendBeacon(url, blob);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private buildPayload(events: SessionEvent[]): BatchPayload {
|
|
94
|
+
return {
|
|
95
|
+
sessionId: this.config.cedarSessionId,
|
|
96
|
+
batchTimestamp: Date.now(),
|
|
97
|
+
startedAt: this.startedAt,
|
|
98
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
99
|
+
viewportWidth: typeof window !== 'undefined' ? window.innerWidth : 0,
|
|
100
|
+
viewportHeight: typeof window !== 'undefined' ? window.innerHeight : 0,
|
|
101
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
102
|
+
events,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private estimateQueueSize(): number {
|
|
107
|
+
// Rough estimate: JSON.stringify each event and sum lengths
|
|
108
|
+
let size = 0;
|
|
109
|
+
for (const event of this.queue) {
|
|
110
|
+
size += JSON.stringify(event).length;
|
|
111
|
+
}
|
|
112
|
+
return size;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export interface SDKConfig {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
cedarSessionId: string;
|
|
4
|
+
batchIntervalMs?: number; // default: 5000
|
|
5
|
+
batchMaxSize?: number; // default: 512KB
|
|
6
|
+
console?: ConsoleConfig;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ConsoleConfig {
|
|
10
|
+
log?: boolean; // default: true
|
|
11
|
+
info?: boolean; // default: true
|
|
12
|
+
warn?: boolean; // default: true
|
|
13
|
+
error?: boolean; // default: true
|
|
14
|
+
debug?: boolean; // default: true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RecorderConfig {
|
|
18
|
+
checkoutEveryNms?: number;
|
|
19
|
+
blockSelector?: string;
|
|
20
|
+
maskAllInputs?: boolean;
|
|
21
|
+
inlineStylesheet?: boolean;
|
|
22
|
+
sampling?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Events the SDK produces
|
|
26
|
+
export type SessionEvent =
|
|
27
|
+
| RRWebSessionEvent
|
|
28
|
+
| NetworkSessionEvent
|
|
29
|
+
| ConsoleSessionEvent
|
|
30
|
+
| ErrorSessionEvent
|
|
31
|
+
| CustomSessionEvent;
|
|
32
|
+
|
|
33
|
+
export interface RRWebSessionEvent {
|
|
34
|
+
type: 'rrweb';
|
|
35
|
+
data: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NetworkSessionEvent {
|
|
39
|
+
type: 'network';
|
|
40
|
+
timestamp: number;
|
|
41
|
+
data: {
|
|
42
|
+
id: string;
|
|
43
|
+
method: string;
|
|
44
|
+
url: string;
|
|
45
|
+
graphqlOperationName?: string;
|
|
46
|
+
requestHeaders?: Record<string, string>;
|
|
47
|
+
requestBody?: string;
|
|
48
|
+
status?: number;
|
|
49
|
+
responseHeaders?: Record<string, string>;
|
|
50
|
+
responseBody?: string;
|
|
51
|
+
startTime: number;
|
|
52
|
+
endTime?: number;
|
|
53
|
+
duration?: number;
|
|
54
|
+
error?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ConsoleSessionEvent {
|
|
59
|
+
type: 'console';
|
|
60
|
+
timestamp: number;
|
|
61
|
+
data: {
|
|
62
|
+
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
63
|
+
args: string[];
|
|
64
|
+
trace?: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ErrorSessionEvent {
|
|
69
|
+
type: 'error';
|
|
70
|
+
timestamp: number;
|
|
71
|
+
data: {
|
|
72
|
+
message: string;
|
|
73
|
+
stack?: string;
|
|
74
|
+
source?: string;
|
|
75
|
+
lineno?: number;
|
|
76
|
+
colno?: number;
|
|
77
|
+
type: 'uncaught' | 'unhandledrejection' | 'manual';
|
|
78
|
+
tags?: Record<string, string>;
|
|
79
|
+
extra?: Record<string, unknown>;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CustomSessionEvent {
|
|
84
|
+
type: 'custom';
|
|
85
|
+
timestamp: number;
|
|
86
|
+
data: {
|
|
87
|
+
name: string;
|
|
88
|
+
properties?: Record<string, unknown>;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface BatchPayload {
|
|
93
|
+
sessionId: string;
|
|
94
|
+
batchTimestamp: number;
|
|
95
|
+
startedAt: number;
|
|
96
|
+
userAgent: string;
|
|
97
|
+
viewportWidth: number;
|
|
98
|
+
viewportHeight: number;
|
|
99
|
+
url: string;
|
|
100
|
+
events: SessionEvent[];
|
|
101
|
+
}
|
package/tsconfig.json
ADDED