mixpanel-browser 2.71.0 → 2.72.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/.claude/settings.local.json +9 -0
- package/.github/workflows/tests.yml +1 -0
- package/CHANGELOG.md +10 -0
- package/dist/mixpanel-core.cjs.d.ts +47 -10
- package/dist/mixpanel-core.cjs.js +57 -21
- package/dist/mixpanel-recorder.js +681 -113
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +47 -10
- package/dist/mixpanel-with-async-recorder.cjs.js +57 -21
- package/dist/mixpanel-with-recorder.d.ts +47 -10
- package/dist/mixpanel-with-recorder.js +737 -133
- package/dist/mixpanel-with-recorder.min.d.ts +47 -10
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +47 -10
- package/dist/mixpanel.amd.js +737 -133
- package/dist/mixpanel.cjs.d.ts +47 -10
- package/dist/mixpanel.cjs.js +737 -133
- package/dist/mixpanel.globals.js +57 -21
- package/dist/mixpanel.min.js +149 -149
- package/dist/mixpanel.module.d.ts +47 -10
- package/dist/mixpanel.module.js +737 -133
- package/dist/mixpanel.umd.d.ts +47 -10
- package/dist/mixpanel.umd.js +737 -133
- package/dist/rrweb-bundled.js +12760 -0
- package/dist/rrweb-compiled.js +2496 -7176
- package/package.json +3 -2
- package/rollup.config.mjs +15 -4
- package/src/autocapture/index.js +7 -5
- package/src/autocapture/rageclick.js +20 -1
- package/src/autocapture/shadow-dom-observer.js +3 -15
- package/src/autocapture/utils.js +30 -0
- package/src/config.js +1 -1
- package/src/index.d.ts +47 -10
- package/src/mixpanel-core.js +1 -0
- package/src/recorder/recorder.js +1 -1
- package/src/recorder/rrweb-entrypoint.js +6 -0
- package/src/recorder/session-recording.js +69 -12
- package/src/utils.js +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixpanel-browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.72.0",
|
|
4
4
|
"description": "The official Mixpanel JavaScript browser client library",
|
|
5
5
|
"main": "dist/mixpanel.cjs.js",
|
|
6
6
|
"module": "dist/mixpanel.module.js",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"webpack": "1.12.2"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@mixpanel/rrweb": "2.0.0-alpha.18.2"
|
|
69
|
+
"@mixpanel/rrweb": "2.0.0-alpha.18.2",
|
|
70
|
+
"@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.2"
|
|
70
71
|
}
|
|
71
72
|
}
|
package/rollup.config.mjs
CHANGED
|
@@ -7,10 +7,11 @@ import fs from 'fs';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
|
|
9
9
|
const COMPILED_RRWEB_PATH = 'build/rrweb-compiled.js';
|
|
10
|
+
const BUNDLED_RRWEB_PATH = 'build/rrweb-bundled.js';
|
|
10
11
|
|
|
11
12
|
const aliasRrweb = () => alias({
|
|
12
13
|
entries: [
|
|
13
|
-
{ find:
|
|
14
|
+
{ find: /rrweb-entrypoint(?:\.js)?$/, replacement: COMPILED_RRWEB_PATH },
|
|
14
15
|
]
|
|
15
16
|
});
|
|
16
17
|
|
|
@@ -42,15 +43,25 @@ const MINIFY = process.env.MINIFY || process.env.FULL;
|
|
|
42
43
|
|
|
43
44
|
// Main builds used to develop / iterate quickly
|
|
44
45
|
const MAIN_BUILDS = [
|
|
45
|
-
// compile rrweb first to es5 with swc, we'll replace the import later on
|
|
46
46
|
{
|
|
47
|
-
'input': '
|
|
47
|
+
'input': 'src/recorder/rrweb-entrypoint.js',
|
|
48
|
+
'output': [
|
|
49
|
+
{
|
|
50
|
+
file: BUNDLED_RRWEB_PATH,
|
|
51
|
+
format: 'es',
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
plugins: [nodeResolve({browser: true})]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
'input': BUNDLED_RRWEB_PATH,
|
|
48
58
|
'output': [
|
|
49
59
|
{
|
|
50
60
|
file: COMPILED_RRWEB_PATH,
|
|
61
|
+
format: 'es',
|
|
51
62
|
}
|
|
52
63
|
],
|
|
53
|
-
plugins: [
|
|
64
|
+
plugins: [swc({swc: {jsc: {target: 'es5'}}})]
|
|
54
65
|
},
|
|
55
66
|
|
|
56
67
|
// IIFE recorder bundle that is loaded asynchronously
|
package/src/autocapture/index.js
CHANGED
|
@@ -249,6 +249,11 @@ Autocapture.prototype._trackPageLeave = function(ev, currentUrl, currentScrollHe
|
|
|
249
249
|
// User has navigated away already ending their impression.
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
|
+
|
|
253
|
+
if (!this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.is_recording_heatmap_data()) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
252
257
|
this.hasTrackedScrollSession = true;
|
|
253
258
|
var viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
|
|
254
259
|
var scrollPercentage = Math.round(Math.max(this.maxScrollViewDepth - viewportHeight, 0) / (currentScrollHeight - viewportHeight) * 100);
|
|
@@ -268,12 +273,9 @@ Autocapture.prototype._trackPageLeave = function(ev, currentUrl, currentScrollHe
|
|
|
268
273
|
'$current_url': currentUrl || _.info.currentUrl(),
|
|
269
274
|
'$viewportHeight': viewportHeight, // This is the fold line
|
|
270
275
|
'$viewportWidth': Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
|
|
276
|
+
'$captured_for_heatmap': this.mp.is_recording_heatmap_data()
|
|
271
277
|
}, DEFAULT_PROPS);
|
|
272
278
|
|
|
273
|
-
if (this.mp.is_recording_heatmap_data() && !this.getConfig(CONFIG_TRACK_PAGE_LEAVE)) {
|
|
274
|
-
props['$captured_for_heatmap'] = true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
279
|
// Send with beacon transport to ensure event is sent before unload
|
|
278
280
|
this.mp.track(MP_EV_PAGE_LEAVE, props, {transport: 'sendBeacon'});
|
|
279
281
|
};
|
|
@@ -447,7 +449,7 @@ Autocapture.prototype.initRageClickTracking = function() {
|
|
|
447
449
|
return;
|
|
448
450
|
}
|
|
449
451
|
|
|
450
|
-
if (this._rageClickTracker.isRageClick(ev
|
|
452
|
+
if (this._rageClickTracker.isRageClick(ev, currentRageClickConfig)) {
|
|
451
453
|
this.trackDomEvent(ev, MP_EV_RAGE_CLICK);
|
|
452
454
|
}
|
|
453
455
|
}.bind(this);
|
|
@@ -1,17 +1,36 @@
|
|
|
1
|
+
import { getClickEventTargetElement, isDefinitelyNonInteractive } from './utils';
|
|
2
|
+
|
|
1
3
|
/** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
|
|
2
4
|
/** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
|
|
3
5
|
/** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
|
|
6
|
+
/** @const */ var DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY = false;
|
|
4
7
|
|
|
5
8
|
function RageClickTracker() {
|
|
6
9
|
this.clicks = [];
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Determines if a click event is part of a rage click sequence.
|
|
14
|
+
* @param {Event} event - the original click event.
|
|
15
|
+
* @param {import('../index.d.ts').RageClickConfig} options - configuration options for rage click detection.
|
|
16
|
+
* @returns {boolean} - true if the click is considered a rage click, false otherwise.
|
|
17
|
+
*/
|
|
18
|
+
RageClickTracker.prototype.isRageClick = function(event, options) {
|
|
10
19
|
options = options || {};
|
|
11
20
|
var thresholdPx = options['threshold_px'] || DEFAULT_RAGE_CLICK_THRESHOLD_PX;
|
|
12
21
|
var timeoutMs = options['timeout_ms'] || DEFAULT_RAGE_CLICK_TIMEOUT_MS;
|
|
13
22
|
var clickCount = options['click_count'] || DEFAULT_RAGE_CLICK_CLICK_COUNT;
|
|
23
|
+
var interactiveElementsOnly = options['interactive_elements_only'] || DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY;
|
|
24
|
+
|
|
25
|
+
if (interactiveElementsOnly) {
|
|
26
|
+
var target = getClickEventTargetElement(event);
|
|
27
|
+
if (!target || isDefinitelyNonInteractive(target)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
var timestamp = Date.now();
|
|
33
|
+
var x = event['pageX'], y = event['pageY'];
|
|
15
34
|
|
|
16
35
|
var lastClick = this.clicks[this.clicks.length - 1];
|
|
17
36
|
if (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger, weakSetSupported } from './utils';
|
|
1
|
+
import { getClickEventComposedPath, getClickEventTargetElement, logger, weakSetSupported } from './utils';
|
|
2
2
|
|
|
3
3
|
function ShadowDOMObserver(changeCallback, observerConfig) {
|
|
4
4
|
this.changeCallback = changeCallback || function() {};
|
|
@@ -12,28 +12,16 @@ ShadowDOMObserver.prototype.getEventTarget = function(event) {
|
|
|
12
12
|
if (!this.observedShadowRoots) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
var path = this.getComposedPath(event);
|
|
16
|
-
if (path && path.length) {
|
|
17
|
-
return path[0];
|
|
18
|
-
}
|
|
19
15
|
|
|
20
|
-
return event
|
|
16
|
+
return getClickEventTargetElement(event);
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
ShadowDOMObserver.prototype.getComposedPath = function(event) {
|
|
25
|
-
if ('composedPath' in event) {
|
|
26
|
-
return event['composedPath']();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return [];
|
|
30
|
-
};
|
|
31
19
|
ShadowDOMObserver.prototype.observeFromEvent = function(event) {
|
|
32
20
|
if (!this.observedShadowRoots) {
|
|
33
21
|
return;
|
|
34
22
|
}
|
|
35
23
|
|
|
36
|
-
var path =
|
|
24
|
+
var path = getClickEventComposedPath(event);
|
|
37
25
|
|
|
38
26
|
// Check each element in path for shadow roots
|
|
39
27
|
for (var i = 0; i < path.length; i++) {
|
package/src/autocapture/utils.js
CHANGED
|
@@ -744,9 +744,39 @@ function isDefinitelyNonInteractive(element) {
|
|
|
744
744
|
return false;
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Get the composed path of a click event for elements embedded in shadow DOM.
|
|
749
|
+
* @param {Event} event - event to get the composed path from
|
|
750
|
+
* @returns {Array} the composed path of the click event
|
|
751
|
+
*/
|
|
752
|
+
function getClickEventComposedPath(event) {
|
|
753
|
+
if ('composedPath' in event) {
|
|
754
|
+
return event['composedPath']();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Get the element from a click event, accounting for elements embedded in shadow DOM.
|
|
762
|
+
* @param {Event} event - event to get the target from
|
|
763
|
+
* @returns {Element | null} the element that was the target of the click event
|
|
764
|
+
*/
|
|
765
|
+
function getClickEventTargetElement(event) {
|
|
766
|
+
var path = getClickEventComposedPath(event);
|
|
767
|
+
|
|
768
|
+
if (path && path.length > 0) {
|
|
769
|
+
return path[0];
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return event['target'] || event['srcElement'];
|
|
773
|
+
}
|
|
774
|
+
|
|
747
775
|
export {
|
|
748
776
|
EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_INPUT, EV_LOAD,EV_MP_LOCATION_CHANGE, EV_POPSTATE,
|
|
749
777
|
EV_SCROLL, EV_SCROLLEND, EV_SELECT, EV_SUBMIT, EV_TOGGLE, EV_VISIBILITYCHANGE,
|
|
778
|
+
getClickEventComposedPath,
|
|
779
|
+
getClickEventTargetElement,
|
|
750
780
|
getPolyfillScrollEndFunction,
|
|
751
781
|
getPropsForDOMEvent,
|
|
752
782
|
getSafeText,
|
package/src/config.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -43,20 +43,22 @@ export interface OutTrackingOptions extends ClearOptOutInOutOptions {
|
|
|
43
43
|
export type RageClickConfig =
|
|
44
44
|
| boolean
|
|
45
45
|
| {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
/** Distance threshold in pixels for clicks to be considered within the same area (default: 30) */
|
|
47
|
+
threshold_px?: number;
|
|
48
|
+
/** Time window in milliseconds for clicks to be considered rapid (default: 1000) */
|
|
49
|
+
timeout_ms?: number;
|
|
50
|
+
/** Number of clicks required to trigger a rage click event (default: 3) */
|
|
51
|
+
click_count?: number;
|
|
52
|
+
/** Whether to only track rage clicks on interactive elements like buttons, links, inputs (default: false) */
|
|
53
|
+
interactive_elements_only?: boolean;
|
|
54
|
+
};
|
|
53
55
|
|
|
54
56
|
export type DeadClickConfig =
|
|
55
57
|
| boolean
|
|
56
58
|
| {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
/** Time in milliseconds to wait after a click before qualifying it as dead (default: 500) */
|
|
60
|
+
timeout_ms?: number;
|
|
61
|
+
};
|
|
60
62
|
|
|
61
63
|
export interface RegisterOptions {
|
|
62
64
|
persistent: boolean;
|
|
@@ -149,6 +151,10 @@ export interface AutocaptureConfig {
|
|
|
149
151
|
block_element_callback?: (element: Element, event: Event) => boolean;
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
export interface FlagsConfig {
|
|
155
|
+
context: Dict;
|
|
156
|
+
}
|
|
157
|
+
|
|
152
158
|
export interface Config {
|
|
153
159
|
api_host: string;
|
|
154
160
|
api_routes: {
|
|
@@ -165,6 +171,7 @@ export interface Config {
|
|
|
165
171
|
cookie_domain: string;
|
|
166
172
|
cross_site_cookie: boolean;
|
|
167
173
|
cross_subdomain_cookie: boolean;
|
|
174
|
+
flags: boolean | FlagsConfig;
|
|
168
175
|
persistence: Persistence;
|
|
169
176
|
persistence_name: string;
|
|
170
177
|
cookie_name: string;
|
|
@@ -277,11 +284,41 @@ export interface Group {
|
|
|
277
284
|
unset(prop: string, callback?: Callback): void;
|
|
278
285
|
}
|
|
279
286
|
|
|
287
|
+
export interface FlagsVariant {
|
|
288
|
+
key: string;
|
|
289
|
+
value: any;
|
|
290
|
+
experiment_id?: string;
|
|
291
|
+
is_experiment_active?: boolean;
|
|
292
|
+
is_qa_tester?: boolean;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface FlagsUpdateContextOptions {
|
|
296
|
+
replace?: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface FlagsManager {
|
|
300
|
+
are_flags_ready(): boolean;
|
|
301
|
+
get_variant(
|
|
302
|
+
featureName: string,
|
|
303
|
+
fallback: FlagsVariant
|
|
304
|
+
): Promise<FlagsVariant>;
|
|
305
|
+
get_variant_sync(featureName: string, fallback: FlagsVariant): FlagsVariant;
|
|
306
|
+
get_variant_value(featureName: string, fallbackValue: any): Promise<any>;
|
|
307
|
+
get_variant_value_sync(featureName: string, fallbackValue: any): any;
|
|
308
|
+
is_enabled(featureName: string, fallbackValue?: boolean): Promise<boolean>;
|
|
309
|
+
is_enabled_sync(featureName: string, fallbackValue?: boolean): boolean;
|
|
310
|
+
update_context(
|
|
311
|
+
context: Dict,
|
|
312
|
+
options?: FlagsUpdateContextOptions
|
|
313
|
+
): Promise<void>;
|
|
314
|
+
}
|
|
315
|
+
|
|
280
316
|
export interface Mixpanel {
|
|
281
317
|
add_group(group_key: string, group_id: string, callback?: Callback): void;
|
|
282
318
|
alias(alias: string, original?: string): void;
|
|
283
319
|
clear_opt_in_out_tracking(options?: Partial<ClearOptOutInOutOptions>): void;
|
|
284
320
|
disable(events?: string[]): void;
|
|
321
|
+
flags: FlagsManager;
|
|
285
322
|
get_config(prop_name?: string): any;
|
|
286
323
|
get_distinct_id(): any;
|
|
287
324
|
get_group(group_key: string, group_id: string): Group;
|
package/src/mixpanel-core.js
CHANGED
|
@@ -152,6 +152,7 @@ var DEFAULT_CONFIG = {
|
|
|
152
152
|
'record_block_selector': 'img, video, audio',
|
|
153
153
|
'record_canvas': false,
|
|
154
154
|
'record_collect_fonts': false,
|
|
155
|
+
'record_console': true,
|
|
155
156
|
'record_heatmap_data': false,
|
|
156
157
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
157
158
|
'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
|
package/src/recorder/recorder.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// this file exists as an entry point to be able to transpile rrweb packages to es5
|
|
2
|
+
// compatible code without needing to transpile the entire mixpanel-js codebase
|
|
3
|
+
import {record, EventType, IncrementalSource} from '@mixpanel/rrweb';
|
|
4
|
+
import {getRecordConsolePlugin} from '@mixpanel/rrweb-plugin-console-record';
|
|
5
|
+
|
|
6
|
+
export { record, EventType, IncrementalSource, getRecordConsolePlugin };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { window } from '../window';
|
|
2
|
-
import { IncrementalSource, EventType } from '
|
|
3
|
-
import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, NOOP_FUNC, _, localStorageSupported} from '../utils'; // eslint-disable-line camelcase
|
|
2
|
+
import { IncrementalSource, EventType, getRecordConsolePlugin } from './rrweb-entrypoint';
|
|
3
|
+
import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, NOOP_FUNC, _, localStorageSupported, canUseCompressionStream, navigator, userAgent, windowOpera} from '../utils'; // eslint-disable-line camelcase
|
|
4
4
|
import { IDBStorageWrapper, RECORDING_EVENTS_STORE_NAME } from '../storage/indexed-db';
|
|
5
5
|
import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
|
|
6
6
|
import { RequestBatcher } from '../request-batcher';
|
|
@@ -38,6 +38,7 @@ function isUserEvent(ev) {
|
|
|
38
38
|
* @property {number} idleExpires
|
|
39
39
|
* @property {number} maxExpires
|
|
40
40
|
* @property {number} replayStartTime
|
|
41
|
+
* @property {number} lastEventTimestamp
|
|
41
42
|
* @property {number} seqNo
|
|
42
43
|
* @property {string} batchStartUrl
|
|
43
44
|
* @property {string} replayId
|
|
@@ -58,6 +59,7 @@ function isUserEvent(ev) {
|
|
|
58
59
|
* @property {number} idleExpires
|
|
59
60
|
* @property {number} maxExpires
|
|
60
61
|
* @property {number} replayStartTime
|
|
62
|
+
* @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
|
|
61
63
|
* @property {number} seqNo
|
|
62
64
|
* @property {string} batchStartUrl
|
|
63
65
|
* @property {string} replayStartUrl
|
|
@@ -91,6 +93,7 @@ var SessionRecording = function(options) {
|
|
|
91
93
|
this.idleExpires = options.idleExpires || null;
|
|
92
94
|
this.maxExpires = options.maxExpires || null;
|
|
93
95
|
this.replayStartTime = options.replayStartTime || null;
|
|
96
|
+
this.lastEventTimestamp = options.lastEventTimestamp || null;
|
|
94
97
|
this.seqNo = options.seqNo || 0;
|
|
95
98
|
|
|
96
99
|
this.idleTimeoutId = null;
|
|
@@ -150,10 +153,20 @@ SessionRecording.prototype.getUserIdInfo = function () {
|
|
|
150
153
|
|
|
151
154
|
SessionRecording.prototype.unloadPersistedData = function () {
|
|
152
155
|
this.batcher.stop();
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
|
|
157
|
+
return this.queueStorage.init().catch(function () {
|
|
158
|
+
this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
|
|
159
|
+
}.bind(this)).then(function () {
|
|
160
|
+
// if the recording is too short, just delete any stored events without flushing
|
|
161
|
+
if (this.getDurationMs() < this._getRecordMinMs()) {
|
|
155
162
|
return this.queueStorage.removeItem(this.batcherKey);
|
|
156
|
-
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.batcher.flush()
|
|
166
|
+
.then(function () {
|
|
167
|
+
return this.queueStorage.removeItem(this.batcherKey);
|
|
168
|
+
}.bind(this));
|
|
169
|
+
}.bind(this));
|
|
157
170
|
};
|
|
158
171
|
|
|
159
172
|
SessionRecording.prototype.getConfig = function(configVar) {
|
|
@@ -188,11 +201,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
188
201
|
this.maxExpires = new Date().getTime() + this.recordMaxMs;
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
this.recordMinMs = this.
|
|
192
|
-
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
193
|
-
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
194
|
-
logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
195
|
-
}
|
|
204
|
+
this.recordMinMs = this._getRecordMinMs();
|
|
196
205
|
|
|
197
206
|
if (!this.replayStartTime) {
|
|
198
207
|
this.replayStartTime = new Date().getTime();
|
|
@@ -240,6 +249,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
240
249
|
}
|
|
241
250
|
// promise only used to await during tests
|
|
242
251
|
this.__enqueuePromise = this.batcher.enqueue(ev);
|
|
252
|
+
|
|
253
|
+
// Capture the timestamp of the last event for duration calculation.
|
|
254
|
+
if (this.lastEventTimestamp === null || ev.timestamp > this.lastEventTimestamp) {
|
|
255
|
+
this.lastEventTimestamp = ev.timestamp;
|
|
256
|
+
}
|
|
243
257
|
}.bind(this),
|
|
244
258
|
'blockClass': this.getConfig('record_block_class'),
|
|
245
259
|
'blockSelector': blockSelector,
|
|
@@ -254,7 +268,16 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
254
268
|
'recordCanvas': this.getConfig('record_canvas'),
|
|
255
269
|
'sampling': {
|
|
256
270
|
'canvas': 15
|
|
257
|
-
}
|
|
271
|
+
},
|
|
272
|
+
'plugins': this.getConfig('record_console') ? [
|
|
273
|
+
getRecordConsolePlugin({
|
|
274
|
+
stringifyOptions: {
|
|
275
|
+
stringLengthLimit: 1000,
|
|
276
|
+
numOfKeysLimit: 50,
|
|
277
|
+
depthOfLimit: 2
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
] : []
|
|
258
281
|
});
|
|
259
282
|
} catch (err) {
|
|
260
283
|
this.reportError('Unexpected error when starting rrweb recording.', err);
|
|
@@ -339,6 +362,7 @@ SessionRecording.prototype.serialize = function () {
|
|
|
339
362
|
'replayStartTime': this.replayStartTime,
|
|
340
363
|
'batchStartUrl': this.batchStartUrl,
|
|
341
364
|
'replayStartUrl': this.replayStartUrl,
|
|
365
|
+
'lastEventTimestamp': this.lastEventTimestamp,
|
|
342
366
|
'idleExpires': this.idleExpires,
|
|
343
367
|
'maxExpires': this.maxExpires,
|
|
344
368
|
'tabId': tabId,
|
|
@@ -360,6 +384,7 @@ SessionRecording.deserialize = function (serializedRecording, options) {
|
|
|
360
384
|
idleExpires: serializedRecording['idleExpires'],
|
|
361
385
|
maxExpires: serializedRecording['maxExpires'],
|
|
362
386
|
replayStartTime: serializedRecording['replayStartTime'],
|
|
387
|
+
lastEventTimestamp: serializedRecording['lastEventTimestamp'],
|
|
363
388
|
seqNo: serializedRecording['seqNo'],
|
|
364
389
|
sharedLockStorage: options.sharedLockStorage,
|
|
365
390
|
}));
|
|
@@ -450,7 +475,7 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
|
|
|
450
475
|
var eventsJson = JSON.stringify(data);
|
|
451
476
|
Object.assign(reqParams, this.getUserIdInfo());
|
|
452
477
|
|
|
453
|
-
if (
|
|
478
|
+
if (canUseCompressionStream(userAgent, navigator.vendor, windowOpera)) {
|
|
454
479
|
var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
|
|
455
480
|
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
456
481
|
new Response(gzipStream)
|
|
@@ -479,4 +504,36 @@ SessionRecording.prototype.reportError = function(msg, err) {
|
|
|
479
504
|
}
|
|
480
505
|
};
|
|
481
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Calculates the duration of the recording in milliseconds, based on the start time and time of last recorded event.
|
|
509
|
+
* @returns {number} The duration of the recording in milliseconds. Returns 0 if recording hasn't started.
|
|
510
|
+
*/
|
|
511
|
+
SessionRecording.prototype.getDurationMs = function() {
|
|
512
|
+
if (this.replayStartTime === null) {
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// If the recording has no events, assume it is in progress and use the current time as the end time.
|
|
517
|
+
if (this.lastEventTimestamp === null) {
|
|
518
|
+
return new Date().getTime() - this.replayStartTime;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return this.lastEventTimestamp - this.replayStartTime;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Lazily loads the minimum recording length config in milliseconds, respecting the maximum limit.
|
|
526
|
+
* @returns {number} The minimum recording length in milliseconds.
|
|
527
|
+
*/
|
|
528
|
+
SessionRecording.prototype._getRecordMinMs = function() {
|
|
529
|
+
var configValue = this.getConfig('record_min_ms');
|
|
530
|
+
|
|
531
|
+
if (configValue > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
532
|
+
logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
533
|
+
return MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return configValue;
|
|
537
|
+
};
|
|
538
|
+
|
|
482
539
|
export { SessionRecording };
|
package/src/utils.js
CHANGED
|
@@ -1738,6 +1738,28 @@ if (typeof JSON !== 'undefined') {
|
|
|
1738
1738
|
JSONStringify = JSONStringify || _.JSONEncode;
|
|
1739
1739
|
JSONParse = JSONParse || _.JSONDecode;
|
|
1740
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* Determines if CompressionStream API should be used.
|
|
1743
|
+
* Returns false for Safari 16.4 and 16.5 which have breaking CompressionStream bugs.
|
|
1744
|
+
* https://bugs.webkit.org/show_bug.cgi?id=254021
|
|
1745
|
+
* fixed in 16.6 https://developer.apple.com/documentation/safari-release-notes/safari-16_6-release-notes
|
|
1746
|
+
*/
|
|
1747
|
+
var canUseCompressionStream = function(userAgent, vendor, opera) {
|
|
1748
|
+
if (!window.CompressionStream) {
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
var browser = _.info.browser(userAgent, vendor, opera);
|
|
1753
|
+
var version = _.info.browserVersion(userAgent, vendor, opera);
|
|
1754
|
+
if (browser === 'Safari' || browser === 'Mobile Safari') {
|
|
1755
|
+
if (version >= 16.4 && version < 16.6) {
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
return true;
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1741
1763
|
// UNMINIFIED EXPORTS (for closure compiler)
|
|
1742
1764
|
_['info'] = _.info;
|
|
1743
1765
|
_['info']['browser'] = _.info.browser;
|
|
@@ -1755,6 +1777,7 @@ _['NPO'] = NpoPromise;
|
|
|
1755
1777
|
export {
|
|
1756
1778
|
_,
|
|
1757
1779
|
batchedThrottle,
|
|
1780
|
+
canUseCompressionStream,
|
|
1758
1781
|
cheap_guid,
|
|
1759
1782
|
console_with_prefix,
|
|
1760
1783
|
console,
|
|
@@ -1773,4 +1796,5 @@ export {
|
|
|
1773
1796
|
safewrapClass,
|
|
1774
1797
|
slice,
|
|
1775
1798
|
userAgent,
|
|
1799
|
+
windowOpera,
|
|
1776
1800
|
};
|