@leanbase.com/js 0.2.2-alpha.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/leanbase.d.ts +0 -3
  2. package/dist/leanbase.iife.js +1 -1
  3. package/dist/leanbase.iife.js.map +1 -1
  4. package/dist/main.js +1 -1
  5. package/dist/main.js.map +1 -1
  6. package/dist/module.d.ts +0 -708
  7. package/dist/module.js +1 -1
  8. package/dist/module.js.map +1 -1
  9. package/dist/types.d.ts +0 -135
  10. package/dist/version.d.ts +1 -1
  11. package/lib/leanbase.d.ts +0 -3
  12. package/lib/leanbase.js +0 -25
  13. package/lib/leanbase.js.map +1 -1
  14. package/lib/types.d.ts +0 -135
  15. package/lib/types.js.map +1 -1
  16. package/lib/version.d.ts +1 -1
  17. package/lib/version.js +1 -1
  18. package/lib/version.js.map +1 -1
  19. package/package.json +1 -6
  20. package/dist/extensions/replay/external/config.d.ts +0 -9
  21. package/dist/extensions/replay/external/denylist.d.ts +0 -5
  22. package/dist/extensions/replay/external/lazy-loaded-session-recorder.d.ts +0 -153
  23. package/dist/extensions/replay/external/mutation-throttler.d.ts +0 -20
  24. package/dist/extensions/replay/external/network-plugin.d.ts +0 -15
  25. package/dist/extensions/replay/external/sessionrecording-utils.d.ts +0 -19
  26. package/dist/extensions/replay/external/triggerMatching.d.ts +0 -102
  27. package/dist/extensions/replay/rrweb-plugins/patch.d.ts +0 -3
  28. package/dist/extensions/replay/session-recording.d.ts +0 -75
  29. package/dist/extensions/replay/types/rrweb-types.d.ts +0 -439
  30. package/dist/extensions/replay/types/rrweb.d.ts +0 -82
  31. package/dist/extensions/sampling.d.ts +0 -4
  32. package/dist/utils/logger.d.ts +0 -10
  33. package/lib/extensions/replay/external/config.d.ts +0 -9
  34. package/lib/extensions/replay/external/config.js +0 -221
  35. package/lib/extensions/replay/external/config.js.map +0 -1
  36. package/lib/extensions/replay/external/denylist.d.ts +0 -5
  37. package/lib/extensions/replay/external/denylist.js +0 -28
  38. package/lib/extensions/replay/external/denylist.js.map +0 -1
  39. package/lib/extensions/replay/external/lazy-loaded-session-recorder.d.ts +0 -153
  40. package/lib/extensions/replay/external/lazy-loaded-session-recorder.js +0 -1042
  41. package/lib/extensions/replay/external/lazy-loaded-session-recorder.js.map +0 -1
  42. package/lib/extensions/replay/external/mutation-throttler.d.ts +0 -20
  43. package/lib/extensions/replay/external/mutation-throttler.js +0 -77
  44. package/lib/extensions/replay/external/mutation-throttler.js.map +0 -1
  45. package/lib/extensions/replay/external/network-plugin.d.ts +0 -15
  46. package/lib/extensions/replay/external/network-plugin.js +0 -503
  47. package/lib/extensions/replay/external/network-plugin.js.map +0 -1
  48. package/lib/extensions/replay/external/sessionrecording-utils.d.ts +0 -19
  49. package/lib/extensions/replay/external/sessionrecording-utils.js +0 -125
  50. package/lib/extensions/replay/external/sessionrecording-utils.js.map +0 -1
  51. package/lib/extensions/replay/external/triggerMatching.d.ts +0 -102
  52. package/lib/extensions/replay/external/triggerMatching.js +0 -342
  53. package/lib/extensions/replay/external/triggerMatching.js.map +0 -1
  54. package/lib/extensions/replay/rrweb-plugins/patch.d.ts +0 -3
  55. package/lib/extensions/replay/rrweb-plugins/patch.js +0 -32
  56. package/lib/extensions/replay/rrweb-plugins/patch.js.map +0 -1
  57. package/lib/extensions/replay/session-recording.d.ts +0 -75
  58. package/lib/extensions/replay/session-recording.js +0 -279
  59. package/lib/extensions/replay/session-recording.js.map +0 -1
  60. package/lib/extensions/replay/types/rrweb-types.d.ts +0 -439
  61. package/lib/extensions/replay/types/rrweb-types.js +0 -83
  62. package/lib/extensions/replay/types/rrweb-types.js.map +0 -1
  63. package/lib/extensions/replay/types/rrweb.d.ts +0 -82
  64. package/lib/extensions/replay/types/rrweb.js +0 -9
  65. package/lib/extensions/replay/types/rrweb.js.map +0 -1
  66. package/lib/extensions/sampling.d.ts +0 -4
  67. package/lib/extensions/sampling.js +0 -23
  68. package/lib/extensions/sampling.js.map +0 -1
  69. package/lib/utils/logger.d.ts +0 -10
  70. package/lib/utils/logger.js +0 -15
  71. package/lib/utils/logger.js.map +0 -1
@@ -1,1042 +0,0 @@
1
- import { clampToRange, includes, isBoolean, isFunction, isNullish, isNumber, isObject, isString, isUndefined, } from '@posthog/core';
2
- import { EventType, IncrementalSource, } from '../types/rrweb-types';
3
- import { buildNetworkRequestOptions } from './config';
4
- import { ACTIVE, allMatchSessionRecordingStatus, AndTriggerMatching, anyMatchSessionRecordingStatus, BUFFERING, DISABLED, EventTriggerMatching, LinkedFlagMatching, nullMatchSessionRecordingStatus, OrTriggerMatching, PAUSED, PendingTriggerMatching, SAMPLED, TRIGGER_PENDING, URLTriggerMatching, } from './triggerMatching';
5
- import { estimateSize, INCREMENTAL_SNAPSHOT_EVENT_TYPE, truncateLargeConsoleLogs } from './sessionrecording-utils';
6
- import { gzipSync, strFromU8, strToU8 } from 'fflate';
7
- import { window, document, addEventListener, assignableWindow } from '../../../utils';
8
- import { MutationThrottler } from './mutation-throttler';
9
- import { createLogger } from '../../../utils/logger';
10
- import { SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION, SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_REMOTE_CONFIG, SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION, } from '../../../constants';
11
- import { isLocalhost } from '../../../utils/request-utils';
12
- import Config from '../../../config';
13
- import { sampleOnProperty } from '../../sampling';
14
- const BASE_ENDPOINT = '/s/';
15
- const DEFAULT_CANVAS_QUALITY = 0.4;
16
- const DEFAULT_CANVAS_FPS = 4;
17
- const MAX_CANVAS_FPS = 12;
18
- const MAX_CANVAS_QUALITY = 1;
19
- const TWO_SECONDS = 2000;
20
- const ONE_KB = 1024;
21
- const ONE_MINUTE = 1000 * 60;
22
- const FIVE_MINUTES = ONE_MINUTE * 5;
23
- export const RECORDING_IDLE_THRESHOLD_MS = FIVE_MINUTES;
24
- export const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9; // ~1mb (with some wiggle room)
25
- export const RECORDING_BUFFER_TIMEOUT = 2000; // 2 seconds
26
- export const SESSION_RECORDING_BATCH_KEY = 'recordings';
27
- const LOGGER_PREFIX = '[SessionRecording]';
28
- const logger = createLogger(LOGGER_PREFIX);
29
- const ACTIVE_SOURCES = [
30
- IncrementalSource.MouseMove,
31
- IncrementalSource.MouseInteraction,
32
- IncrementalSource.Scroll,
33
- IncrementalSource.ViewportResize,
34
- IncrementalSource.Input,
35
- IncrementalSource.TouchMove,
36
- IncrementalSource.MediaInteraction,
37
- IncrementalSource.Drag,
38
- ];
39
- const newQueuedEvent = (rrwebMethod) => ({
40
- rrwebMethod,
41
- enqueuedAt: Date.now(),
42
- attempt: 1,
43
- });
44
- function getRRWebRecord() {
45
- return assignableWindow?.__PosthogExtensions__?.rrweb?.record;
46
- }
47
- function gzipToString(data) {
48
- return strFromU8(gzipSync(strToU8(JSON.stringify(data))), true);
49
- }
50
- /**
51
- * rrweb's packer takes an event and returns a string or the reverse on `unpack`.
52
- * but we want to be able to inspect metadata during ingestion.
53
- * and don't want to compress the entire event,
54
- * so we have a custom packer that only compresses part of some events
55
- */
56
- function compressEvent(event) {
57
- try {
58
- if (event.type === EventType.FullSnapshot) {
59
- return {
60
- ...event,
61
- data: gzipToString(event.data),
62
- cv: '2024-10',
63
- };
64
- }
65
- if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.Mutation) {
66
- return {
67
- ...event,
68
- cv: '2024-10',
69
- data: {
70
- ...event.data,
71
- texts: gzipToString(event.data.texts),
72
- attributes: gzipToString(event.data.attributes),
73
- removes: gzipToString(event.data.removes),
74
- adds: gzipToString(event.data.adds),
75
- },
76
- };
77
- }
78
- if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.StyleSheetRule) {
79
- return {
80
- ...event,
81
- cv: '2024-10',
82
- data: {
83
- ...event.data,
84
- adds: event.data.adds ? gzipToString(event.data.adds) : undefined,
85
- removes: event.data.removes ? gzipToString(event.data.removes) : undefined,
86
- },
87
- };
88
- }
89
- }
90
- catch (e) {
91
- logger.error('could not compress event - will use uncompressed event', e);
92
- }
93
- return event;
94
- }
95
- function isSessionIdleEvent(e) {
96
- return e.type === EventType.Custom && e.data.tag === 'sessionIdle';
97
- }
98
- /** When we put the recording into a paused state, we add a custom event.
99
- * However, in the paused state, events are dropped and never make it to the buffer,
100
- * so we need to manually let this one through */
101
- function isRecordingPausedEvent(e) {
102
- return e.type === EventType.Custom && e.data.tag === 'recording paused';
103
- }
104
- export const SEVEN_MEGABYTES = 1024 * 1024 * 7 * 0.9; // ~7mb (with some wiggle room)
105
- // recursively splits large buffers into smaller ones
106
- // uses a pretty high size limit to avoid splitting too much
107
- export function splitBuffer(buffer, sizeLimit = SEVEN_MEGABYTES) {
108
- if (buffer.size >= sizeLimit && buffer.data.length > 1) {
109
- const half = Math.floor(buffer.data.length / 2);
110
- const firstHalf = buffer.data.slice(0, half);
111
- const secondHalf = buffer.data.slice(half);
112
- return [
113
- splitBuffer({
114
- size: estimateSize(firstHalf),
115
- data: firstHalf,
116
- sessionId: buffer.sessionId,
117
- windowId: buffer.windowId,
118
- }),
119
- splitBuffer({
120
- size: estimateSize(secondHalf),
121
- data: secondHalf,
122
- sessionId: buffer.sessionId,
123
- windowId: buffer.windowId,
124
- }),
125
- ].flatMap((x) => x);
126
- }
127
- else {
128
- return [buffer];
129
- }
130
- }
131
- export class LazyLoadedSessionRecording {
132
- get sessionId() {
133
- return this._sessionId;
134
- }
135
- get _sessionManager() {
136
- if (!this._instance.sessionManager) {
137
- throw new Error(LOGGER_PREFIX + ' must be started with a valid sessionManager.');
138
- }
139
- return this._instance.sessionManager;
140
- }
141
- get _sessionIdleThresholdMilliseconds() {
142
- return this._instance.config.session_recording?.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS;
143
- }
144
- get _isSampled() {
145
- const currentValue = this._instance.getPersistedProperty(SESSION_RECORDING_IS_SAMPLED);
146
- // originally we would store `true` or `false` or nothing,
147
- // but that would mean sometimes we would carry on recording on session id change
148
- return isBoolean(currentValue) ? currentValue : isString(currentValue) ? currentValue === this.sessionId : null;
149
- }
150
- get _sampleRate() {
151
- const rate = this._remoteConfig?.sampleRate;
152
- return isNumber(rate) ? rate : null;
153
- }
154
- get _minimumDuration() {
155
- const duration = this._remoteConfig?.minimumDurationMilliseconds;
156
- return isNumber(duration) ? duration : null;
157
- }
158
- constructor(_instance) {
159
- this._instance = _instance;
160
- this._endpoint = BASE_ENDPOINT;
161
- /**
162
- * Util to help developers working on this feature manually override
163
- */
164
- this._forceAllowLocalhostNetworkCapture = false;
165
- this._stopRrweb = undefined;
166
- this._lastActivityTimestamp = Date.now();
167
- /**
168
- * and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet
169
- */
170
- this._queuedRRWebEvents = [];
171
- this._isIdle = 'unknown';
172
- this._warnedMissingConsolePlugin = false;
173
- // we need to be able to check the state of the event and url triggers separately
174
- // as we make some decisions based on them without referencing LinkedFlag etc
175
- this._triggerMatching = new PendingTriggerMatching();
176
- this._removePageViewCaptureHook = undefined;
177
- this._removeEventTriggerCaptureHook = undefined;
178
- this._statusMatcher = nullMatchSessionRecordingStatus;
179
- this._onSessionIdListener = undefined;
180
- this._onSessionIdleResetForcedListener = undefined;
181
- this._samplingSessionListener = undefined;
182
- this._forceIdleSessionIdListener = undefined;
183
- this._onSessionIdCallback = (sessionId, windowId, changeReason) => {
184
- if (!changeReason) {
185
- return;
186
- }
187
- const nextWindowId = windowId ?? this._windowId;
188
- if (sessionId === this._sessionId && nextWindowId === this._windowId) {
189
- return;
190
- }
191
- const previousSessionId = this._sessionId;
192
- this._sessionId = sessionId;
193
- this._windowId = nextWindowId;
194
- this._buffer.sessionId = sessionId;
195
- this._buffer.windowId = nextWindowId;
196
- this._tryAddCustomEvent('$session_id_change', { sessionId, windowId: nextWindowId, changeReason });
197
- this._clearConditionalRecordingPersistence();
198
- if (!this._stopRrweb) {
199
- this.start('session_id_changed');
200
- }
201
- if (isNumber(this._sampleRate) && isNullish(this._samplingSessionListener)) {
202
- this._makeSamplingDecision(sessionId, previousSessionId);
203
- }
204
- };
205
- this._onBeforeUnload = () => {
206
- this._flushBuffer();
207
- };
208
- this._onOffline = () => {
209
- this._tryAddCustomEvent('browser offline', {});
210
- };
211
- this._onOnline = () => {
212
- this._tryAddCustomEvent('browser online', {});
213
- };
214
- this._onVisibilityChange = () => {
215
- if (document?.visibilityState) {
216
- const label = 'window ' + document.visibilityState;
217
- this._tryAddCustomEvent(label, {});
218
- }
219
- };
220
- // we know there's a sessionManager, so don't need to start without a session id
221
- const { sessionId, windowId } = this._sessionManager.checkAndGetSessionAndWindowId();
222
- this._sessionId = sessionId;
223
- this._windowId = windowId;
224
- this._linkedFlagMatching = new LinkedFlagMatching(this._instance);
225
- this._urlTriggerMatching = new URLTriggerMatching(this._instance);
226
- this._eventTriggerMatching = new EventTriggerMatching(this._instance);
227
- this._buffer = this._clearBuffer();
228
- if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
229
- logger.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
230
- }
231
- }
232
- get _masking() {
233
- const masking_server_side = this._remoteConfig?.masking;
234
- const masking_client_side = {
235
- maskAllInputs: this._instance.config.session_recording?.maskAllInputs,
236
- maskTextSelector: this._instance.config.session_recording?.maskTextSelector,
237
- blockSelector: this._instance.config.session_recording?.blockSelector,
238
- };
239
- const maskAllInputs = masking_client_side?.maskAllInputs ?? masking_server_side?.maskAllInputs;
240
- const maskTextSelector = masking_client_side?.maskTextSelector ?? masking_server_side?.maskTextSelector;
241
- const blockSelector = masking_client_side?.blockSelector ?? masking_server_side?.blockSelector;
242
- return !isUndefined(maskAllInputs) || !isUndefined(maskTextSelector) || !isUndefined(blockSelector)
243
- ? {
244
- maskAllInputs: maskAllInputs ?? true,
245
- maskTextSelector,
246
- blockSelector,
247
- }
248
- : undefined;
249
- }
250
- get _canvasRecording() {
251
- const canvasRecording_client_side = this._instance.config.session_recording?.captureCanvas;
252
- const canvasRecording_server_side = this._remoteConfig?.canvasRecording;
253
- const enabled = canvasRecording_client_side?.recordCanvas ?? canvasRecording_server_side?.enabled ?? false;
254
- const fps = canvasRecording_client_side?.canvasFps ?? canvasRecording_server_side?.fps ?? DEFAULT_CANVAS_FPS;
255
- let quality = canvasRecording_client_side?.canvasQuality ?? canvasRecording_server_side?.quality ?? DEFAULT_CANVAS_QUALITY;
256
- if (typeof quality === 'string') {
257
- const parsed = parseFloat(quality);
258
- quality = isNaN(parsed) ? 0.4 : parsed;
259
- }
260
- return {
261
- enabled,
262
- fps: clampToRange(fps, 0, MAX_CANVAS_FPS, createLogger('canvas recording fps'), DEFAULT_CANVAS_FPS),
263
- quality: clampToRange(quality, 0, MAX_CANVAS_QUALITY, createLogger('canvas recording quality'), DEFAULT_CANVAS_QUALITY),
264
- };
265
- }
266
- get _isConsoleLogCaptureEnabled() {
267
- const enabled_server_side = !!this._remoteConfig?.consoleLogRecordingEnabled;
268
- const enabled_client_side = this._instance.config.enable_recording_console_log;
269
- return enabled_client_side ?? enabled_server_side;
270
- }
271
- // network payload capture config has three parts
272
- // each can be configured server side or client side
273
- get _networkPayloadCapture() {
274
- const networkPayloadCapture_server_side = this._remoteConfig?.networkPayloadCapture;
275
- const networkPayloadCapture_client_side = {
276
- recordHeaders: this._instance.config.session_recording?.recordHeaders,
277
- recordBody: this._instance.config.session_recording?.recordBody,
278
- };
279
- const headersEnabled = networkPayloadCapture_client_side?.recordHeaders || networkPayloadCapture_server_side?.recordHeaders;
280
- const bodyEnabled = networkPayloadCapture_client_side?.recordBody || networkPayloadCapture_server_side?.recordBody;
281
- const clientConfigForPerformanceCapture = isObject(this._instance.config.capture_performance)
282
- ? this._instance.config.capture_performance?.network_timing
283
- : this._instance.config.capture_performance;
284
- const networkTimingEnabled = !!(isBoolean(clientConfigForPerformanceCapture)
285
- ? clientConfigForPerformanceCapture
286
- : networkPayloadCapture_server_side?.capturePerformance);
287
- return headersEnabled || bodyEnabled || networkTimingEnabled
288
- ? { recordHeaders: headersEnabled, recordBody: bodyEnabled, recordPerformance: networkTimingEnabled }
289
- : undefined;
290
- }
291
- _gatherRRWebPlugins() {
292
- const plugins = [];
293
- const recordConsolePlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordConsolePlugin;
294
- if (recordConsolePlugin && this._isConsoleLogCaptureEnabled) {
295
- plugins.push(recordConsolePlugin());
296
- }
297
- else if (this._isConsoleLogCaptureEnabled && !this._warnedMissingConsolePlugin) {
298
- logger.warn('Console log recording enabled but plugin unavailable; continuing without console capture.');
299
- this._warnedMissingConsolePlugin = true;
300
- }
301
- const networkPlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin;
302
- if (!!this._networkPayloadCapture && isFunction(networkPlugin)) {
303
- const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
304
- if (canRecordNetwork) {
305
- plugins.push(networkPlugin(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
306
- }
307
- else {
308
- logger.info('NetworkCapture not started because we are on localhost.');
309
- }
310
- }
311
- return plugins;
312
- }
313
- _maskUrl(url) {
314
- const userSessionRecordingOptions = this._instance.config.session_recording || {};
315
- if (userSessionRecordingOptions.maskNetworkRequestFn) {
316
- let networkRequest = {
317
- url,
318
- };
319
- // TODO we should deprecate this and use the same function for this masking and the rrweb/network plugin
320
- // TODO or deprecate this and provide a new clearer name so this would be `maskURLPerformanceFn` or similar
321
- networkRequest = userSessionRecordingOptions.maskNetworkRequestFn(networkRequest);
322
- return networkRequest?.url;
323
- }
324
- return url;
325
- }
326
- _tryRRWebMethod(queuedRRWebEvent) {
327
- try {
328
- queuedRRWebEvent.rrwebMethod();
329
- return true;
330
- }
331
- catch (e) {
332
- // Sometimes a race can occur where the recorder is not fully started yet
333
- if (this._queuedRRWebEvents.length < 10) {
334
- this._queuedRRWebEvents.push({
335
- enqueuedAt: queuedRRWebEvent.enqueuedAt || Date.now(),
336
- attempt: queuedRRWebEvent.attempt + 1,
337
- rrwebMethod: queuedRRWebEvent.rrwebMethod,
338
- });
339
- }
340
- else {
341
- logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
342
- }
343
- return false;
344
- }
345
- }
346
- _tryAddCustomEvent(tag, payload) {
347
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
348
- }
349
- _pageViewFallBack() {
350
- try {
351
- if (this._instance.config.capture_pageview || !window) {
352
- return;
353
- }
354
- // Strip hash parameters from URL since they often aren't helpful
355
- // Use URL constructor for proper parsing to handle edge cases
356
- // recording doesn't run in IE11, so we don't need compat here
357
- // eslint-disable-next-line compat/compat
358
- const url = new URL(window.location.href);
359
- const hrefWithoutHash = url.origin + url.pathname + url.search;
360
- const currentUrl = this._maskUrl(hrefWithoutHash);
361
- if (this._lastHref !== currentUrl) {
362
- this._lastHref = currentUrl;
363
- this._tryAddCustomEvent('$url_changed', { href: currentUrl });
364
- }
365
- }
366
- catch {
367
- // If URL processing fails, don't capture anything
368
- }
369
- }
370
- _processQueuedEvents() {
371
- if (this._queuedRRWebEvents.length) {
372
- // if rrweb isn't ready to accept events earlier, then we queued them up.
373
- // now that `emit` has been called rrweb should be ready to accept them.
374
- // so, before we process this event, we try our queued events _once_ each
375
- // we don't want to risk queuing more things and never exiting this loop!
376
- // if they fail here, they'll be pushed into a new queue
377
- // and tried on the next loop.
378
- // there is a risk of this queue growing in an uncontrolled manner.
379
- // so its length is limited elsewhere
380
- // for now this is to help us ensure we can capture events that happen
381
- // and try to identify more about when it is failing
382
- const itemsToProcess = [...this._queuedRRWebEvents];
383
- this._queuedRRWebEvents = [];
384
- itemsToProcess.forEach((queuedRRWebEvent) => {
385
- if (Date.now() - queuedRRWebEvent.enqueuedAt <= TWO_SECONDS) {
386
- this._tryRRWebMethod(queuedRRWebEvent);
387
- }
388
- });
389
- }
390
- }
391
- _tryTakeFullSnapshot() {
392
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
393
- }
394
- get _fullSnapshotIntervalMillis() {
395
- if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING &&
396
- !['sampled', 'active'].includes(this.status)) {
397
- return ONE_MINUTE;
398
- }
399
- return this._instance.config.session_recording?.full_snapshot_interval_millis ?? FIVE_MINUTES;
400
- }
401
- _scheduleFullSnapshot() {
402
- if (this._fullSnapshotTimer) {
403
- clearInterval(this._fullSnapshotTimer);
404
- }
405
- // we don't schedule snapshots while idle
406
- if (this._isIdle === true) {
407
- return;
408
- }
409
- const interval = this._fullSnapshotIntervalMillis;
410
- if (!interval) {
411
- return;
412
- }
413
- this._fullSnapshotTimer = setInterval(() => {
414
- this._tryTakeFullSnapshot();
415
- }, interval);
416
- }
417
- _pauseRecording() {
418
- // we check _urlBlocked not status, since more than one thing can affect status
419
- if (this._urlTriggerMatching.urlBlocked) {
420
- return;
421
- }
422
- // we can't flush the buffer here since someone might be starting on a blocked page.
423
- // and we need to be sure that we don't record that page,
424
- // so we might not get the below custom event, but events will report the paused status.
425
- // which will allow debugging of sessions that start on blocked pages
426
- this._urlTriggerMatching.urlBlocked = true;
427
- // Clear the snapshot timer since we don't want new snapshots while paused
428
- clearInterval(this._fullSnapshotTimer);
429
- logger.info('recording paused due to URL blocker');
430
- this._tryAddCustomEvent('recording paused', { reason: 'url blocker' });
431
- }
432
- _resumeRecording() {
433
- // we check _urlBlocked not status, since more than one thing can affect status
434
- if (!this._urlTriggerMatching.urlBlocked) {
435
- return;
436
- }
437
- this._urlTriggerMatching.urlBlocked = false;
438
- this._tryTakeFullSnapshot();
439
- this._scheduleFullSnapshot();
440
- this._tryAddCustomEvent('recording resumed', { reason: 'left blocked url' });
441
- logger.info('recording resumed');
442
- }
443
- _activateTrigger(triggerType) {
444
- if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
445
- // status is stored separately for URL and event triggers
446
- this._instance?.persistence?.register({
447
- [triggerType === 'url'
448
- ? SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION
449
- : SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION]: this._sessionId,
450
- });
451
- this._flushBuffer();
452
- this._reportStarted((triggerType + '_trigger_matched'));
453
- }
454
- }
455
- get isStarted() {
456
- return !!this._stopRrweb;
457
- }
458
- get _remoteConfig() {
459
- const persistedConfig = this._instance.getPersistedProperty(SESSION_RECORDING_REMOTE_CONFIG);
460
- if (!persistedConfig) {
461
- return undefined;
462
- }
463
- const parsedConfig = isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig);
464
- return parsedConfig;
465
- }
466
- start(startReason) {
467
- const config = this._remoteConfig;
468
- if (!config) {
469
- logger.info('remote config must be stored in persistence before recording can start');
470
- return;
471
- }
472
- // We want to ensure the sessionManager is reset if necessary on loading the recorder
473
- this._sessionManager.checkAndGetSessionAndWindowId();
474
- if (config?.endpoint) {
475
- this._endpoint = config?.endpoint;
476
- }
477
- if (config?.triggerMatchType === 'any') {
478
- this._statusMatcher = anyMatchSessionRecordingStatus;
479
- this._triggerMatching = new OrTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching]);
480
- }
481
- else {
482
- // either the setting is "ALL"
483
- // or we default to the most restrictive
484
- this._statusMatcher = allMatchSessionRecordingStatus;
485
- this._triggerMatching = new AndTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching]);
486
- }
487
- this._instance.registerForSession({
488
- $sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType ?? null,
489
- });
490
- this._urlTriggerMatching.onConfig(config);
491
- this._eventTriggerMatching.onConfig(config);
492
- this._removeEventTriggerCaptureHook?.();
493
- this._addEventTriggerListener();
494
- this._linkedFlagMatching.onConfig(config, (flag, variant) => {
495
- this._reportStarted('linked_flag_matched', {
496
- flag,
497
- variant,
498
- });
499
- });
500
- this._makeSamplingDecision(this.sessionId);
501
- this._startRecorder();
502
- // calling addEventListener multiple times is safe and will not add duplicates
503
- addEventListener(window, 'beforeunload', this._onBeforeUnload);
504
- addEventListener(window, 'offline', this._onOffline);
505
- addEventListener(window, 'online', this._onOnline);
506
- addEventListener(window, 'visibilitychange', this._onVisibilityChange);
507
- if (!this._onSessionIdListener) {
508
- this._onSessionIdListener = this._sessionManager.onSessionId(this._onSessionIdCallback);
509
- }
510
- if (!this._onSessionIdleResetForcedListener) {
511
- this._onSessionIdleResetForcedListener = this._sessionManager.on('forcedIdleReset', () => {
512
- // a session was forced to reset due to idle timeout and lack of activity
513
- this._clearConditionalRecordingPersistence();
514
- this._isIdle = 'unknown';
515
- this.stop();
516
- // then we want a session id listener to restart the recording when a new session starts
517
- this._forceIdleSessionIdListener = this._sessionManager.onSessionId((sessionId, windowId, changeReason) => {
518
- // this should first unregister itself
519
- this._forceIdleSessionIdListener?.();
520
- this._forceIdleSessionIdListener = undefined;
521
- this._onSessionIdCallback(sessionId, windowId, changeReason);
522
- });
523
- });
524
- }
525
- if (isNullish(this._removePageViewCaptureHook)) {
526
- // :TRICKY: rrweb does not capture navigation within SPA-s, so hook into our $pageview events to get access to all events.
527
- // Dropping the initial event is fine (it's always captured by rrweb).
528
- this._removePageViewCaptureHook = this._instance.on('eventCaptured', (event) => {
529
- // If anything could go wrong here,
530
- // it has the potential to block the main loop,
531
- // so we catch all errors.
532
- try {
533
- if (event.event === '$pageview') {
534
- const href = event?.properties.$current_url ? this._maskUrl(event?.properties.$current_url) : '';
535
- if (!href) {
536
- return;
537
- }
538
- this._tryAddCustomEvent('$pageview', { href });
539
- }
540
- }
541
- catch (e) {
542
- logger.error('Could not add $pageview to rrweb session', e);
543
- }
544
- });
545
- }
546
- if (this.status === ACTIVE) {
547
- this._reportStarted(startReason || 'recording_initialized');
548
- }
549
- }
550
- stop() {
551
- window?.removeEventListener('beforeunload', this._onBeforeUnload);
552
- window?.removeEventListener('offline', this._onOffline);
553
- window?.removeEventListener('online', this._onOnline);
554
- window?.removeEventListener('visibilitychange', this._onVisibilityChange);
555
- this._clearBuffer();
556
- clearInterval(this._fullSnapshotTimer);
557
- this._clearFlushBufferTimer();
558
- this._removePageViewCaptureHook?.();
559
- this._removePageViewCaptureHook = undefined;
560
- this._removeEventTriggerCaptureHook?.();
561
- this._removeEventTriggerCaptureHook = undefined;
562
- this._onSessionIdListener?.();
563
- this._onSessionIdListener = undefined;
564
- this._onSessionIdleResetForcedListener?.();
565
- this._onSessionIdleResetForcedListener = undefined;
566
- this._samplingSessionListener?.();
567
- this._samplingSessionListener = undefined;
568
- this._forceIdleSessionIdListener?.();
569
- this._forceIdleSessionIdListener = undefined;
570
- this._eventTriggerMatching.stop();
571
- this._urlTriggerMatching.stop();
572
- this._linkedFlagMatching.stop();
573
- this._mutationThrottler?.stop();
574
- // Clear any queued rrweb events to prevent memory leaks from closures
575
- this._queuedRRWebEvents = [];
576
- this._stopRrweb?.();
577
- this._stopRrweb = undefined;
578
- logger.info('stopped');
579
- }
580
- onRRwebEmit(rawEvent) {
581
- this._processQueuedEvents();
582
- if (!rawEvent || !isObject(rawEvent)) {
583
- return;
584
- }
585
- if (rawEvent.type === EventType.Meta) {
586
- const href = this._maskUrl(rawEvent.data.href);
587
- this._lastHref = href;
588
- if (!href) {
589
- return;
590
- }
591
- rawEvent.data.href = href;
592
- }
593
- else {
594
- this._pageViewFallBack();
595
- }
596
- // Check if the URL matches any trigger patterns
597
- this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), (triggerType) => this._activateTrigger(triggerType));
598
- // always have to check if the URL is blocked really early,
599
- // or you risk getting stuck in a loop
600
- if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
601
- return;
602
- }
603
- // we're processing a full snapshot, so we should reset the timer
604
- if (rawEvent.type === EventType.FullSnapshot) {
605
- this._scheduleFullSnapshot();
606
- // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
607
- this._mutationThrottler?.reset();
608
- }
609
- // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
610
- // we always start trigger pending so need to wait for flags before we know if we're really pending
611
- if (rawEvent.type === EventType.FullSnapshot &&
612
- this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
613
- this._clearBufferBeforeMostRecentMeta();
614
- }
615
- const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
616
- if (!throttledEvent) {
617
- return;
618
- }
619
- // TODO: Re-add ensureMaxMessageSize once we are confident in it
620
- const event = truncateLargeConsoleLogs(throttledEvent);
621
- this._updateWindowAndSessionIds(event);
622
- // When in an idle state we keep recording but don't capture the events,
623
- // we don't want to return early if idle is 'unknown'
624
- if (this._isIdle === true && !isSessionIdleEvent(event)) {
625
- return;
626
- }
627
- if (isSessionIdleEvent(event)) {
628
- // session idle events have a timestamp when rrweb sees them
629
- // which can artificially lengthen a session
630
- // we know when we detected it based on the payload and can correct the timestamp
631
- const payload = event.data.payload;
632
- if (payload) {
633
- const lastActivity = payload.lastActivityTimestamp;
634
- const threshold = payload.threshold;
635
- event.timestamp = lastActivity + threshold;
636
- }
637
- }
638
- const eventToSend = (this._instance.config.session_recording?.compress_events ?? true) ? compressEvent(event) : event;
639
- const size = estimateSize(eventToSend);
640
- const properties = {
641
- $snapshot_bytes: size,
642
- $snapshot_data: eventToSend,
643
- $session_id: this._sessionId,
644
- $window_id: this._windowId,
645
- };
646
- if (this.status === DISABLED) {
647
- this._clearBuffer();
648
- return;
649
- }
650
- this._captureSnapshotBuffered(properties);
651
- }
652
- get status() {
653
- return this._statusMatcher({
654
- // can't get here without recording being enabled...
655
- receivedFlags: true,
656
- isRecordingEnabled: true,
657
- // things that do still vary
658
- isSampled: this._isSampled,
659
- urlTriggerMatching: this._urlTriggerMatching,
660
- eventTriggerMatching: this._eventTriggerMatching,
661
- linkedFlagMatching: this._linkedFlagMatching,
662
- sessionId: this.sessionId,
663
- });
664
- }
665
- log(message, level = 'log') {
666
- this._instance.sessionRecording?.onRRwebEmit({
667
- type: 6,
668
- data: {
669
- plugin: 'rrweb/console@1',
670
- payload: {
671
- level,
672
- trace: [],
673
- // Even though it is a string, we stringify it as that's what rrweb expects
674
- payload: [JSON.stringify(message)],
675
- },
676
- },
677
- timestamp: Date.now(),
678
- });
679
- }
680
- overrideLinkedFlag() {
681
- this._linkedFlagMatching.linkedFlagSeen = true;
682
- this._tryTakeFullSnapshot();
683
- this._reportStarted('linked_flag_overridden');
684
- }
685
- /**
686
- * this ignores the sampling config and (if other conditions are met) causes capture to start
687
- *
688
- * It is not usual to call this directly,
689
- * instead call `posthog.startSessionRecording({sampling: true})`
690
- * */
691
- overrideSampling() {
692
- this._instance.persistence?.register({
693
- // short-circuits the `makeSamplingDecision` function in the session recording module
694
- [SESSION_RECORDING_IS_SAMPLED]: this.sessionId,
695
- });
696
- this._tryTakeFullSnapshot();
697
- this._reportStarted('sampling_overridden');
698
- }
699
- /**
700
- * this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
701
- *
702
- * It is not usual to call this directly,
703
- * instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
704
- * */
705
- overrideTrigger(triggerType) {
706
- this._activateTrigger(triggerType);
707
- }
708
- _clearFlushBufferTimer() {
709
- if (this._flushBufferTimer) {
710
- clearTimeout(this._flushBufferTimer);
711
- this._flushBufferTimer = undefined;
712
- }
713
- }
714
- _flushBuffer() {
715
- this._clearFlushBufferTimer();
716
- const minimumDuration = this._minimumDuration;
717
- const sessionDuration = this._sessionDuration;
718
- // if we have old data in the buffer but the session has rotated, then the
719
- // session duration might be negative. In that case we want to flush the buffer
720
- const isPositiveSessionDuration = isNumber(sessionDuration) && sessionDuration >= 0;
721
- const isBelowMinimumDuration = isNumber(minimumDuration) && isPositiveSessionDuration && sessionDuration < minimumDuration;
722
- if (this.status === BUFFERING || this.status === PAUSED || this.status === DISABLED || isBelowMinimumDuration) {
723
- this._flushBufferTimer = setTimeout(() => {
724
- this._flushBuffer();
725
- }, RECORDING_BUFFER_TIMEOUT);
726
- return this._buffer;
727
- }
728
- if (this._buffer.data.length > 0) {
729
- const snapshotEvents = splitBuffer(this._buffer);
730
- snapshotEvents.forEach((snapshotBuffer) => {
731
- this._captureSnapshot({
732
- $snapshot_bytes: snapshotBuffer.size,
733
- $snapshot_data: snapshotBuffer.data,
734
- $session_id: snapshotBuffer.sessionId,
735
- $window_id: snapshotBuffer.windowId,
736
- $lib: 'web',
737
- $lib_version: Config.LIB_VERSION,
738
- });
739
- });
740
- }
741
- // buffer is empty, we clear it in case the session id has changed
742
- return this._clearBuffer();
743
- }
744
- _captureSnapshotBuffered(properties) {
745
- const additionalBytes = 2 + (this._buffer?.data.length || 0); // 2 bytes for the array brackets and 1 byte for each comma
746
- if (!this._isIdle && // we never want to flush when idle
747
- (this._buffer.size + properties.$snapshot_bytes + additionalBytes > RECORDING_MAX_EVENT_SIZE ||
748
- this._buffer.sessionId !== this._sessionId)) {
749
- this._buffer = this._flushBuffer();
750
- }
751
- this._buffer.size += properties.$snapshot_bytes;
752
- this._buffer.data.push(properties.$snapshot_data);
753
- if (!this._flushBufferTimer && !this._isIdle) {
754
- this._flushBufferTimer = setTimeout(() => {
755
- this._flushBuffer();
756
- }, RECORDING_BUFFER_TIMEOUT);
757
- }
758
- }
759
- _captureSnapshot(properties) {
760
- // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
761
- this._instance.capture('$snapshot', properties, {
762
- _url: this._snapshotUrl(),
763
- _noTruncate: true,
764
- _batchKey: SESSION_RECORDING_BATCH_KEY,
765
- skip_client_rate_limiting: true,
766
- });
767
- }
768
- _snapshotUrl() {
769
- const router = this._instance.requestRouter;
770
- if (router?.endpointFor) {
771
- return router.endpointFor('api', this._endpoint);
772
- }
773
- return this._endpoint;
774
- }
775
- get _sessionDuration() {
776
- const mostRecentSnapshot = this._buffer?.data[this._buffer?.data.length - 1];
777
- const { sessionStartTimestamp } = this._sessionManager.checkAndGetSessionAndWindowId(true);
778
- return mostRecentSnapshot ? mostRecentSnapshot.timestamp - sessionStartTimestamp : null;
779
- }
780
- _clearBufferBeforeMostRecentMeta() {
781
- if (!this._buffer || this._buffer.data.length === 0) {
782
- return this._clearBuffer();
783
- }
784
- // Find the last meta event index by iterating backwards
785
- let lastMetaIndex = -1;
786
- for (let i = this._buffer.data.length - 1; i >= 0; i--) {
787
- if (this._buffer.data[i].type === EventType.Meta) {
788
- lastMetaIndex = i;
789
- break;
790
- }
791
- }
792
- if (lastMetaIndex >= 0) {
793
- this._buffer.data = this._buffer.data.slice(lastMetaIndex);
794
- this._buffer.size = this._buffer.data.reduce((acc, curr) => acc + estimateSize(curr), 0);
795
- return this._buffer;
796
- }
797
- else {
798
- return this._clearBuffer();
799
- }
800
- }
801
- _clearBuffer() {
802
- this._buffer = {
803
- size: 0,
804
- data: [],
805
- sessionId: this._sessionId,
806
- windowId: this._windowId,
807
- };
808
- return this._buffer;
809
- }
810
- _reportStarted(startReason, tagPayload) {
811
- this._instance.registerForSession({
812
- $session_recording_start_reason: startReason,
813
- });
814
- logger.info(startReason.replace('_', ' '), tagPayload);
815
- if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
816
- this._tryAddCustomEvent(startReason, tagPayload);
817
- }
818
- }
819
- _isInteractiveEvent(event) {
820
- return (event.type === INCREMENTAL_SNAPSHOT_EVENT_TYPE &&
821
- ACTIVE_SOURCES.indexOf(event.data?.source) !== -1);
822
- }
823
- _updateWindowAndSessionIds(event) {
824
- // Some recording events are triggered by non-user events (e.g. "X minutes ago" text updating on the screen).
825
- // We don't want to extend the session or trigger a new session in these cases. These events are designated by event
826
- // type -> incremental update, and source -> mutation.
827
- const isUserInteraction = this._isInteractiveEvent(event);
828
- if (!isUserInteraction && !this._isIdle) {
829
- // We check if the lastActivityTimestamp is old enough to go idle
830
- const timeSinceLastActivity = event.timestamp - this._lastActivityTimestamp;
831
- if (timeSinceLastActivity > this._sessionIdleThresholdMilliseconds) {
832
- // we mark as idle right away,
833
- // or else we get multiple idle events
834
- // if there are lots of non-user activity events being emitted
835
- this._isIdle = true;
836
- // don't take full snapshots while idle
837
- clearInterval(this._fullSnapshotTimer);
838
- this._tryAddCustomEvent('sessionIdle', {
839
- eventTimestamp: event.timestamp,
840
- lastActivityTimestamp: this._lastActivityTimestamp,
841
- threshold: this._sessionIdleThresholdMilliseconds,
842
- bufferLength: this._buffer.data.length,
843
- bufferSize: this._buffer.size,
844
- });
845
- // proactively flush the buffer in case the session is idle for a long time
846
- this._flushBuffer();
847
- }
848
- }
849
- let returningFromIdle = false;
850
- if (isUserInteraction) {
851
- this._lastActivityTimestamp = event.timestamp;
852
- if (this._isIdle) {
853
- const idleWasUnknown = this._isIdle === 'unknown';
854
- // Remove the idle state
855
- this._isIdle = false;
856
- // if the idle state was unknown, we don't want to add an event, since we're just in bootup
857
- // whereas if it was true, we know we've been idle for a while, and we can mark ourselves as returning from idle
858
- if (!idleWasUnknown) {
859
- this._tryAddCustomEvent('sessionNoLongerIdle', {
860
- reason: 'user activity',
861
- type: event.type,
862
- });
863
- returningFromIdle = true;
864
- }
865
- }
866
- }
867
- if (this._isIdle) {
868
- return;
869
- }
870
- // We only want to extend the session if it is an interactive event.
871
- const { windowId, sessionId } = this._sessionManager.checkAndGetSessionAndWindowId(!isUserInteraction, event.timestamp);
872
- const sessionIdChanged = this._sessionId !== sessionId;
873
- const windowIdChanged = this._windowId !== windowId;
874
- this._windowId = windowId;
875
- this._sessionId = sessionId;
876
- if (sessionIdChanged || windowIdChanged) {
877
- this.stop();
878
- this.start('session_id_changed');
879
- }
880
- else if (returningFromIdle) {
881
- this._scheduleFullSnapshot();
882
- }
883
- }
884
- _clearConditionalRecordingPersistence() {
885
- this._instance?.persistence?.unregister(SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION);
886
- this._instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION);
887
- this._instance?.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
888
- }
889
- _makeSamplingDecision(sessionId, previousSessionId) {
890
- const sessionIdChanged = (previousSessionId ?? this._sessionId) !== sessionId;
891
- // capture the current sample rate
892
- // because it is re-used multiple times
893
- // and the bundler won't minimize any of the references
894
- const currentSampleRate = this._sampleRate;
895
- if (!isNumber(currentSampleRate)) {
896
- this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
897
- return;
898
- }
899
- const storedIsSampled = this._isSampled;
900
- /**
901
- * if we get this far, then we should make a sampling decision.
902
- * When the session id changes or there is no stored sampling decision for this session id
903
- * then we should make a new decision.
904
- *
905
- * Otherwise, we should use the stored decision.
906
- */
907
- const makeDecision = sessionIdChanged || !isBoolean(storedIsSampled);
908
- const shouldSample = makeDecision ? sampleOnProperty(sessionId, currentSampleRate) : storedIsSampled;
909
- if (makeDecision) {
910
- if (shouldSample) {
911
- this._reportStarted(SAMPLED);
912
- }
913
- else {
914
- logger.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
915
- }
916
- this._tryAddCustomEvent('samplingDecisionMade', {
917
- sampleRate: currentSampleRate,
918
- isSampled: shouldSample,
919
- });
920
- }
921
- this._instance.persistence?.register({
922
- [SESSION_RECORDING_IS_SAMPLED]: shouldSample ? sessionId : false,
923
- });
924
- }
925
- _addEventTriggerListener() {
926
- if (this._eventTriggerMatching._eventTriggers.length === 0 || !isNullish(this._removeEventTriggerCaptureHook)) {
927
- return;
928
- }
929
- this._removeEventTriggerCaptureHook = this._instance.on('eventCaptured', (event) => {
930
- // If anything could go wrong here, it has the potential to block the main loop,
931
- // so we catch all errors.
932
- try {
933
- if (this._eventTriggerMatching._eventTriggers.includes(event.event)) {
934
- this._activateTrigger('event');
935
- }
936
- }
937
- catch (e) {
938
- logger.error('Could not activate event trigger', e);
939
- }
940
- });
941
- }
942
- get sdkDebugProperties() {
943
- const { sessionStartTimestamp } = this._sessionManager.checkAndGetSessionAndWindowId(true);
944
- return {
945
- $recording_status: this.status,
946
- $sdk_debug_replay_internal_buffer_length: this._buffer.data.length,
947
- $sdk_debug_replay_internal_buffer_size: this._buffer.size,
948
- $sdk_debug_current_session_duration: this._sessionDuration,
949
- $sdk_debug_session_start: sessionStartTimestamp,
950
- };
951
- }
952
- _startRecorder() {
953
- if (this._stopRrweb) {
954
- return;
955
- }
956
- // rrweb config info: https://github.com/rrweb-io/rrweb/blob/7d5d0033258d6c29599fb08412202d9a2c7b9413/src/record/index.ts#L28
957
- const sessionRecordingOptions = {
958
- // a limited set of the rrweb config options that we expose to our users.
959
- // see https://github.com/rrweb-io/rrweb/blob/master/guide.md
960
- blockClass: 'ph-no-capture',
961
- blockSelector: undefined,
962
- ignoreClass: 'ph-ignore-input',
963
- maskTextClass: 'ph-mask',
964
- maskTextSelector: undefined,
965
- maskTextFn: undefined,
966
- maskAllInputs: true,
967
- maskInputOptions: { password: true },
968
- maskInputFn: undefined,
969
- slimDOMOptions: {},
970
- collectFonts: false,
971
- inlineStylesheet: true,
972
- recordCrossOriginIframes: false,
973
- };
974
- // only allows user to set our allowlisted options
975
- const userSessionRecordingOptions = this._instance.config.session_recording;
976
- for (const [key, value] of Object.entries(userSessionRecordingOptions || {})) {
977
- if (key in sessionRecordingOptions) {
978
- if (key === 'maskInputOptions') {
979
- // ensure password config is set if not included
980
- sessionRecordingOptions.maskInputOptions = { password: true, ...value };
981
- }
982
- else {
983
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
984
- // @ts-ignore
985
- sessionRecordingOptions[key] = value;
986
- }
987
- }
988
- }
989
- if (this._canvasRecording && this._canvasRecording.enabled) {
990
- sessionRecordingOptions.recordCanvas = true;
991
- sessionRecordingOptions.sampling = { canvas: this._canvasRecording.fps };
992
- sessionRecordingOptions.dataURLOptions = { type: 'image/webp', quality: this._canvasRecording.quality };
993
- }
994
- if (this._masking) {
995
- sessionRecordingOptions.maskAllInputs = this._masking.maskAllInputs ?? true;
996
- sessionRecordingOptions.maskTextSelector = this._masking.maskTextSelector ?? undefined;
997
- sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined;
998
- }
999
- const rrwebRecord = getRRWebRecord();
1000
- if (!rrwebRecord) {
1001
- logger.error('_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.');
1002
- return;
1003
- }
1004
- this._mutationThrottler =
1005
- this._mutationThrottler ??
1006
- new MutationThrottler(rrwebRecord, {
1007
- refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
1008
- bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
1009
- onBlockedNode: (id, node) => {
1010
- const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
1011
- logger.info(message, {
1012
- node: node,
1013
- });
1014
- this.log(LOGGER_PREFIX + ' ' + message, 'warn');
1015
- },
1016
- });
1017
- const activePlugins = this._gatherRRWebPlugins();
1018
- this._stopRrweb = rrwebRecord({
1019
- emit: (event) => {
1020
- this.onRRwebEmit(event);
1021
- },
1022
- plugins: activePlugins,
1023
- ...sessionRecordingOptions,
1024
- });
1025
- // We reset the last activity timestamp, resetting the idle timer
1026
- this._lastActivityTimestamp = Date.now();
1027
- // stay unknown if we're not sure if we're idle or not
1028
- this._isIdle = isBoolean(this._isIdle) ? this._isIdle : 'unknown';
1029
- this.tryAddCustomEvent('$remote_config_received', this._remoteConfig);
1030
- this._tryAddCustomEvent('$session_options', {
1031
- sessionRecordingOptions,
1032
- activePlugins: activePlugins.map((p) => p?.name),
1033
- });
1034
- this._tryAddCustomEvent('$posthog_config', {
1035
- config: this._instance.config,
1036
- });
1037
- }
1038
- tryAddCustomEvent(tag, payload) {
1039
- return this._tryAddCustomEvent(tag, payload);
1040
- }
1041
- }
1042
- //# sourceMappingURL=lazy-loaded-session-recorder.js.map