swetrix 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -14
- package/dist/esnext/Lib.d.ts +79 -11
- package/dist/esnext/Lib.js +417 -12
- package/dist/esnext/Lib.js.map +1 -1
- package/dist/esnext/index.d.ts +18 -15
- package/dist/esnext/index.js +23 -15
- package/dist/esnext/index.js.map +1 -1
- package/dist/esnext/utils.d.ts +9 -0
- package/dist/esnext/utils.js +20 -0
- package/dist/esnext/utils.js.map +1 -1
- package/dist/replaylibrary.min.js +173 -0
- package/dist/swetrix.cjs.js +463 -25
- package/dist/swetrix.cjs.js.map +1 -1
- package/dist/swetrix.es5.js +463 -26
- package/dist/swetrix.es5.js.map +1 -1
- package/dist/swetrix.js +1 -1
- package/dist/swetrix.js.map +1 -1
- package/jest.config.js +3 -1
- package/package.json +43 -40
- package/rollup.config.mjs +20 -0
- package/src/Lib.ts +589 -12
- package/src/index.ts +29 -14
- package/src/types/rrweb-shim.d.ts +11 -0
- package/src/utils.ts +22 -0
- package/tests/errors.test.ts +2 -9
- package/tests/events.test.ts +2 -9
- package/tests/experiments.test.ts +2 -9
- package/tests/initialisation.test.ts +5 -18
- package/tests/jsdomEnvironment.ts +20 -0
- package/tests/pageview.test.ts +3 -9
- package/tests/sessionReplay.test.ts +389 -0
- package/tests/testUtils.ts +27 -0
- package/tests/utils.test.ts +37 -114
- package/tsconfig.esnext.json +5 -1
- package/tsconfig.json +6 -1
- package/tsconfig.test.json +7 -0
- package/.github/funding.yml +0 -2
- package/.github/workflows/test.yml +0 -32
package/dist/swetrix.cjs.js
CHANGED
|
@@ -36,6 +36,26 @@ const getTimezone = () => {
|
|
|
36
36
|
const getReferrer = () => {
|
|
37
37
|
return document.referrer || undefined;
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Returns the URL query string (without the leading `?`) of the current
|
|
41
|
+
* page, or `undefined` if there is none.
|
|
42
|
+
*
|
|
43
|
+
* Falls back to a query string embedded in `location.hash` (e.g. when a
|
|
44
|
+
* hash router uses `/#/path?foo=bar`) so we still capture click IDs in
|
|
45
|
+
* SPA hash-routed setups.
|
|
46
|
+
*/
|
|
47
|
+
const getQueryString = () => {
|
|
48
|
+
if (location.search && location.search.length > 1) {
|
|
49
|
+
return location.search.slice(1);
|
|
50
|
+
}
|
|
51
|
+
const hashIndex = location.hash.indexOf('?');
|
|
52
|
+
if (hashIndex > -1) {
|
|
53
|
+
const hashQuery = location.hash.slice(hashIndex + 1);
|
|
54
|
+
if (hashQuery)
|
|
55
|
+
return hashQuery;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
39
59
|
const getUTMSource = () => findInSearch(utmSourceRegex);
|
|
40
60
|
const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();
|
|
41
61
|
const getUTMCampaign = () => findInSearch(utmCampaignRegex);
|
|
@@ -70,11 +90,29 @@ const getPath = (options) => {
|
|
|
70
90
|
return result;
|
|
71
91
|
};
|
|
72
92
|
|
|
93
|
+
const SESSION_REPLAY_PRIVACY_VALUES = ['total', 'normal', 'none'];
|
|
73
94
|
const defaultActions = {
|
|
74
95
|
stop() { },
|
|
75
96
|
};
|
|
97
|
+
const defaultSessionReplayActions = {
|
|
98
|
+
async stop() { },
|
|
99
|
+
async flush() { },
|
|
100
|
+
};
|
|
76
101
|
const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
|
|
77
102
|
const DEFAULT_API_BASE = 'https://api.swetrix.com';
|
|
103
|
+
const DEFAULT_RRWEB_FILE = 'replaylibrary.min.js';
|
|
104
|
+
const DEFAULT_RRWEB_URL = `https://cdn.jsdelivr.net/npm/swetrix@latest/dist/${DEFAULT_RRWEB_FILE}`;
|
|
105
|
+
const DEFAULT_SESSION_REPLAY_FLUSH_INTERVAL = 5000;
|
|
106
|
+
const DEFAULT_SESSION_REPLAY_MAX_EVENTS = 100;
|
|
107
|
+
const DEFAULT_SESSION_REPLAY_PRIVACY = 'total';
|
|
108
|
+
const SESSION_REPLAY_ACTIVITY_EVENTS = [
|
|
109
|
+
'click',
|
|
110
|
+
'keydown',
|
|
111
|
+
'mousedown',
|
|
112
|
+
'mousemove',
|
|
113
|
+
'scroll',
|
|
114
|
+
'touchstart',
|
|
115
|
+
];
|
|
78
116
|
// Default cache duration: 5 minutes
|
|
79
117
|
const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
|
|
80
118
|
class Lib {
|
|
@@ -88,9 +126,15 @@ class Lib {
|
|
|
88
126
|
this.activePage = null;
|
|
89
127
|
this.errorListenerExists = false;
|
|
90
128
|
this.cachedData = null;
|
|
129
|
+
this.rrwebLoader = null;
|
|
130
|
+
this.sessionReplayActions = null;
|
|
131
|
+
this.sessionReplayInitPromise = null;
|
|
91
132
|
this.trackPathChange = this.trackPathChange.bind(this);
|
|
92
133
|
this.heartbeat = this.heartbeat.bind(this);
|
|
93
134
|
this.captureError = this.captureError.bind(this);
|
|
135
|
+
if (this.getSessionReplayPreloadOption()) {
|
|
136
|
+
void this.preloadSessionReplay().catch(() => undefined);
|
|
137
|
+
}
|
|
94
138
|
}
|
|
95
139
|
captureError(event) {
|
|
96
140
|
var _a, _b, _c, _d;
|
|
@@ -176,6 +220,7 @@ class Lib {
|
|
|
176
220
|
ca: getUTMCampaign(),
|
|
177
221
|
te: getUTMTerm(),
|
|
178
222
|
co: getUTMContent(),
|
|
223
|
+
qs: getQueryString(),
|
|
179
224
|
profileId: (_c = event.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId,
|
|
180
225
|
};
|
|
181
226
|
await this.sendRequest('custom', data);
|
|
@@ -237,10 +282,10 @@ class Lib {
|
|
|
237
282
|
};
|
|
238
283
|
}
|
|
239
284
|
/**
|
|
240
|
-
* Fetches all feature flags
|
|
241
|
-
* Results are cached for 5 minutes by default.
|
|
285
|
+
* Fetches all feature flags for the project.
|
|
286
|
+
* Results are cached for 5 minutes by default and share a cache with experiments.
|
|
242
287
|
*
|
|
243
|
-
* @param options - Options for evaluating feature flags.
|
|
288
|
+
* @param options - Options for evaluating feature flags (`profileId` only).
|
|
244
289
|
* @param forceRefresh - If true, bypasses the cache and fetches fresh data.
|
|
245
290
|
* @returns A promise that resolves to a record of flag keys to boolean values.
|
|
246
291
|
*/
|
|
@@ -307,8 +352,8 @@ class Lib {
|
|
|
307
352
|
* Gets the value of a single feature flag.
|
|
308
353
|
*
|
|
309
354
|
* @param key - The feature flag key.
|
|
310
|
-
* @param options - Options for evaluating the feature flag.
|
|
311
|
-
* @param defaultValue -
|
|
355
|
+
* @param options - Options for evaluating the feature flag (`profileId` only).
|
|
356
|
+
* @param defaultValue - Optional default value to return if the flag is not found. Defaults to false.
|
|
312
357
|
* @returns A promise that resolves to the boolean value of the flag.
|
|
313
358
|
*/
|
|
314
359
|
async getFeatureFlag(key, options, defaultValue = false) {
|
|
@@ -323,16 +368,16 @@ class Lib {
|
|
|
323
368
|
this.cachedData = null;
|
|
324
369
|
}
|
|
325
370
|
/**
|
|
326
|
-
* Fetches
|
|
371
|
+
* Fetches variant assignments for running A/B test experiments returned by feature flag evaluation.
|
|
327
372
|
* Results are cached for 5 minutes by default (shared cache with feature flags).
|
|
328
373
|
*
|
|
329
|
-
* @param options - Options for evaluating experiments.
|
|
374
|
+
* @param options - Options for evaluating experiments (`profileId` only).
|
|
330
375
|
* @param forceRefresh - If true, bypasses the cache and fetches fresh data.
|
|
331
376
|
* @returns A promise that resolves to a record of experiment IDs to variant keys.
|
|
332
377
|
*
|
|
333
378
|
* @example
|
|
334
379
|
* ```typescript
|
|
335
|
-
* const experiments = await getExperiments()
|
|
380
|
+
* const experiments = await getExperiments({ profileId: 'user-123' })
|
|
336
381
|
* // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
|
|
337
382
|
* ```
|
|
338
383
|
*/
|
|
@@ -363,13 +408,16 @@ class Lib {
|
|
|
363
408
|
* Gets the variant key for a specific A/B test experiment.
|
|
364
409
|
*
|
|
365
410
|
* @param experimentId - The experiment ID.
|
|
366
|
-
* @param options - Options for evaluating the experiment.
|
|
367
|
-
* @param defaultVariant -
|
|
411
|
+
* @param options - Options for evaluating the experiment (`profileId` only).
|
|
412
|
+
* @param defaultVariant - Optional default variant key to return if the experiment is not found. Defaults to null.
|
|
368
413
|
* @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
|
|
369
414
|
*
|
|
370
415
|
* @example
|
|
371
416
|
* ```typescript
|
|
372
|
-
* const variant = await getExperiment('checkout-redesign')
|
|
417
|
+
* const variant = await getExperiment('checkout-redesign', { profileId: 'user-123' })
|
|
418
|
+
*
|
|
419
|
+
* // Optional fallback variant:
|
|
420
|
+
* const variantWithFallback = await getExperiment('checkout-redesign', undefined, 'control')
|
|
373
421
|
*
|
|
374
422
|
* if (variant === 'new-checkout') {
|
|
375
423
|
* // Show new checkout flow
|
|
@@ -485,6 +533,167 @@ class Lib {
|
|
|
485
533
|
return null;
|
|
486
534
|
}
|
|
487
535
|
}
|
|
536
|
+
async startSessionReplay(options = {}) {
|
|
537
|
+
if (this.sessionReplayActions) {
|
|
538
|
+
return this.sessionReplayActions;
|
|
539
|
+
}
|
|
540
|
+
if (this.sessionReplayInitPromise) {
|
|
541
|
+
return this.sessionReplayInitPromise;
|
|
542
|
+
}
|
|
543
|
+
const initPromise = this.initialiseSessionReplay(options);
|
|
544
|
+
this.sessionReplayInitPromise = initPromise;
|
|
545
|
+
try {
|
|
546
|
+
return await initPromise;
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
if (this.sessionReplayInitPromise === initPromise) {
|
|
550
|
+
this.sessionReplayInitPromise = null;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async initialiseSessionReplay(options) {
|
|
555
|
+
var _a;
|
|
556
|
+
if (this.sessionReplayActions) {
|
|
557
|
+
return this.sessionReplayActions;
|
|
558
|
+
}
|
|
559
|
+
if (!this.canTrack()) {
|
|
560
|
+
return defaultSessionReplayActions;
|
|
561
|
+
}
|
|
562
|
+
if (!this.shouldSampleSessionReplay(options.sampleRate)) {
|
|
563
|
+
return defaultSessionReplayActions;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
await this.preloadSessionReplay();
|
|
567
|
+
}
|
|
568
|
+
catch (_b) {
|
|
569
|
+
return defaultSessionReplayActions;
|
|
570
|
+
}
|
|
571
|
+
const rrweb = window.rrweb;
|
|
572
|
+
if (!(rrweb === null || rrweb === void 0 ? void 0 : rrweb.record)) {
|
|
573
|
+
return defaultSessionReplayActions;
|
|
574
|
+
}
|
|
575
|
+
const privacy = this.getSessionReplayPrivacy(options.privacy);
|
|
576
|
+
const replayId = this.createReplayId();
|
|
577
|
+
const started = await this.sendSessionReplayStart(replayId, privacy);
|
|
578
|
+
if (!started) {
|
|
579
|
+
return defaultSessionReplayActions;
|
|
580
|
+
}
|
|
581
|
+
const flushIntervalMs = typeof options.flushIntervalMs === 'number' && options.flushIntervalMs > 0
|
|
582
|
+
? options.flushIntervalMs
|
|
583
|
+
: DEFAULT_SESSION_REPLAY_FLUSH_INTERVAL;
|
|
584
|
+
const maxEventsPerChunk = typeof options.maxEventsPerChunk === 'number' &&
|
|
585
|
+
options.maxEventsPerChunk > 0
|
|
586
|
+
? Math.floor(options.maxEventsPerChunk)
|
|
587
|
+
: DEFAULT_SESSION_REPLAY_MAX_EVENTS;
|
|
588
|
+
const maxDurationMs = typeof options.maxDurationMs === 'number' && options.maxDurationMs > 0
|
|
589
|
+
? options.maxDurationMs
|
|
590
|
+
: null;
|
|
591
|
+
const idleTimeoutMs = typeof options.idleTimeoutMs === 'number' && options.idleTimeoutMs > 0
|
|
592
|
+
? options.idleTimeoutMs
|
|
593
|
+
: null;
|
|
594
|
+
let chunkIndex = 0;
|
|
595
|
+
let stopped = false;
|
|
596
|
+
let events = [];
|
|
597
|
+
let flushing = Promise.resolve();
|
|
598
|
+
let maxDurationTimer;
|
|
599
|
+
let idleTimer;
|
|
600
|
+
const flush = async (useBeacon = false) => {
|
|
601
|
+
if (!events.length)
|
|
602
|
+
return;
|
|
603
|
+
const chunk = events;
|
|
604
|
+
events = [];
|
|
605
|
+
const currentChunkIndex = chunkIndex++;
|
|
606
|
+
flushing = flushing
|
|
607
|
+
.catch(() => undefined)
|
|
608
|
+
.then(() => this.sendSessionReplayChunk(replayId, privacy, currentChunkIndex, chunk, useBeacon));
|
|
609
|
+
await flushing;
|
|
610
|
+
};
|
|
611
|
+
const userEmit = (_a = options.rrweb) === null || _a === void 0 ? void 0 : _a.emit;
|
|
612
|
+
const recordOptions = this.getSessionReplayRecordOptions(privacy, options.rrweb, (event) => {
|
|
613
|
+
try {
|
|
614
|
+
userEmit === null || userEmit === void 0 ? void 0 : userEmit(event);
|
|
615
|
+
}
|
|
616
|
+
catch (_a) { }
|
|
617
|
+
events.push(event);
|
|
618
|
+
if (events.length >= maxEventsPerChunk) {
|
|
619
|
+
void flush();
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
const stopRecording = rrweb.record(recordOptions);
|
|
623
|
+
const timer = setInterval(() => void flush(), flushIntervalMs);
|
|
624
|
+
const flushOnPageExit = () => void flush(true);
|
|
625
|
+
const flushOnHidden = () => {
|
|
626
|
+
if (document.visibilityState === 'hidden') {
|
|
627
|
+
void flush(true);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const clearIdleTimer = () => {
|
|
631
|
+
if (idleTimer) {
|
|
632
|
+
clearTimeout(idleTimer);
|
|
633
|
+
idleTimer = undefined;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const stopSessionReplay = async () => {
|
|
637
|
+
if (stopped)
|
|
638
|
+
return;
|
|
639
|
+
stopped = true;
|
|
640
|
+
clearInterval(timer);
|
|
641
|
+
if (maxDurationTimer) {
|
|
642
|
+
clearTimeout(maxDurationTimer);
|
|
643
|
+
}
|
|
644
|
+
clearIdleTimer();
|
|
645
|
+
window.removeEventListener('pagehide', flushOnPageExit);
|
|
646
|
+
document.removeEventListener('visibilitychange', flushOnHidden);
|
|
647
|
+
SESSION_REPLAY_ACTIVITY_EVENTS.forEach((eventName) => {
|
|
648
|
+
window.removeEventListener(eventName, resetIdleTimer);
|
|
649
|
+
});
|
|
650
|
+
stopRecording === null || stopRecording === void 0 ? void 0 : stopRecording();
|
|
651
|
+
await flush();
|
|
652
|
+
this.sessionReplayActions = null;
|
|
653
|
+
this.sessionReplayInitPromise = null;
|
|
654
|
+
};
|
|
655
|
+
const resetIdleTimer = () => {
|
|
656
|
+
if (!idleTimeoutMs || stopped)
|
|
657
|
+
return;
|
|
658
|
+
clearIdleTimer();
|
|
659
|
+
idleTimer = setTimeout(() => void stopSessionReplay(), idleTimeoutMs);
|
|
660
|
+
};
|
|
661
|
+
window.addEventListener('pagehide', flushOnPageExit);
|
|
662
|
+
document.addEventListener('visibilitychange', flushOnHidden);
|
|
663
|
+
if (maxDurationMs) {
|
|
664
|
+
maxDurationTimer = setTimeout(() => void stopSessionReplay(), maxDurationMs);
|
|
665
|
+
}
|
|
666
|
+
if (idleTimeoutMs) {
|
|
667
|
+
SESSION_REPLAY_ACTIVITY_EVENTS.forEach((eventName) => {
|
|
668
|
+
window.addEventListener(eventName, resetIdleTimer, { passive: true });
|
|
669
|
+
});
|
|
670
|
+
resetIdleTimer();
|
|
671
|
+
}
|
|
672
|
+
this.sessionReplayActions = {
|
|
673
|
+
stop: stopSessionReplay,
|
|
674
|
+
flush: async () => {
|
|
675
|
+
await flush();
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
return this.sessionReplayActions;
|
|
679
|
+
}
|
|
680
|
+
shouldSampleSessionReplay(sampleRate) {
|
|
681
|
+
if (typeof sampleRate !== 'number') {
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
if (sampleRate <= 0) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
if (sampleRate >= 1) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
return Math.random() < sampleRate;
|
|
691
|
+
}
|
|
692
|
+
getSessionReplayPrivacy(privacy) {
|
|
693
|
+
return SESSION_REPLAY_PRIVACY_VALUES.includes(privacy)
|
|
694
|
+
? privacy
|
|
695
|
+
: DEFAULT_SESSION_REPLAY_PRIVACY;
|
|
696
|
+
}
|
|
488
697
|
/**
|
|
489
698
|
* Gets the API base URL (without /log suffix).
|
|
490
699
|
*/
|
|
@@ -547,6 +756,7 @@ class Lib {
|
|
|
547
756
|
ca: getUTMCampaign(),
|
|
548
757
|
te: getUTMTerm(),
|
|
549
758
|
co: getUTMContent(),
|
|
759
|
+
qs: getQueryString(),
|
|
550
760
|
profileId: (_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId,
|
|
551
761
|
...payload,
|
|
552
762
|
};
|
|
@@ -573,6 +783,225 @@ class Lib {
|
|
|
573
783
|
}
|
|
574
784
|
return true;
|
|
575
785
|
}
|
|
786
|
+
getSessionReplayUrl() {
|
|
787
|
+
const replayOption = this.getSessionReplayPreloadOption();
|
|
788
|
+
if (replayOption &&
|
|
789
|
+
typeof replayOption === 'object' &&
|
|
790
|
+
replayOption.rrwebUrl) {
|
|
791
|
+
return replayOption.rrwebUrl;
|
|
792
|
+
}
|
|
793
|
+
return this.getDefaultSessionReplayUrl();
|
|
794
|
+
}
|
|
795
|
+
getSessionReplayPreloadOption() {
|
|
796
|
+
var _a;
|
|
797
|
+
return (_a = this.options) === null || _a === void 0 ? void 0 : _a.preloadSessionReplay;
|
|
798
|
+
}
|
|
799
|
+
getDefaultSessionReplayUrl() {
|
|
800
|
+
if (!isInBrowser()) {
|
|
801
|
+
return DEFAULT_RRWEB_URL;
|
|
802
|
+
}
|
|
803
|
+
const trackerScript = this.getTrackerScript();
|
|
804
|
+
if (trackerScript === null || trackerScript === void 0 ? void 0 : trackerScript.src) {
|
|
805
|
+
const { hostname, pathname } = new URL(trackerScript.src);
|
|
806
|
+
if (hostname === 'swetrix.org' &&
|
|
807
|
+
/^\/swetrix(\.min)?\.js$/i.test(pathname)) {
|
|
808
|
+
return DEFAULT_RRWEB_URL;
|
|
809
|
+
}
|
|
810
|
+
return new URL(DEFAULT_RRWEB_FILE, trackerScript.src).toString();
|
|
811
|
+
}
|
|
812
|
+
return DEFAULT_RRWEB_URL;
|
|
813
|
+
}
|
|
814
|
+
getTrackerScript() {
|
|
815
|
+
const trackerScript = Array.from(document.scripts).find((script) => {
|
|
816
|
+
if (!script.src) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const { pathname } = new URL(script.src);
|
|
821
|
+
return /(^|\/)swetrix(\.min)?\.js$/i.test(pathname);
|
|
822
|
+
}
|
|
823
|
+
catch (_a) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
return trackerScript;
|
|
828
|
+
}
|
|
829
|
+
preloadSessionReplay() {
|
|
830
|
+
var _a;
|
|
831
|
+
if (!isInBrowser()) {
|
|
832
|
+
return Promise.resolve();
|
|
833
|
+
}
|
|
834
|
+
if ((_a = window.rrweb) === null || _a === void 0 ? void 0 : _a.record) {
|
|
835
|
+
return Promise.resolve();
|
|
836
|
+
}
|
|
837
|
+
if (this.rrwebLoader) {
|
|
838
|
+
return this.rrwebLoader;
|
|
839
|
+
}
|
|
840
|
+
if (window.__SWETRIX_RRWEB_LOADING__) {
|
|
841
|
+
this.rrwebLoader = window.__SWETRIX_RRWEB_LOADING__;
|
|
842
|
+
void this.rrwebLoader.catch(() => {
|
|
843
|
+
if (window.__SWETRIX_RRWEB_LOADING__ === this.rrwebLoader) {
|
|
844
|
+
delete window.__SWETRIX_RRWEB_LOADING__;
|
|
845
|
+
}
|
|
846
|
+
this.rrwebLoader = null;
|
|
847
|
+
});
|
|
848
|
+
return this.rrwebLoader;
|
|
849
|
+
}
|
|
850
|
+
this.rrwebLoader = this.loadSessionReplayRecorder();
|
|
851
|
+
window.__SWETRIX_RRWEB_LOADING__ = this.rrwebLoader;
|
|
852
|
+
const loader = this.rrwebLoader;
|
|
853
|
+
void loader.catch(() => {
|
|
854
|
+
if (window.__SWETRIX_RRWEB_LOADING__ === loader) {
|
|
855
|
+
delete window.__SWETRIX_RRWEB_LOADING__;
|
|
856
|
+
}
|
|
857
|
+
if (this.rrwebLoader === loader) {
|
|
858
|
+
this.rrwebLoader = null;
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
return this.rrwebLoader;
|
|
862
|
+
}
|
|
863
|
+
async loadSessionReplayRecorder() {
|
|
864
|
+
const replayOption = this.getSessionReplayPreloadOption();
|
|
865
|
+
const hasCustomReplayUrl = replayOption && typeof replayOption === 'object' && replayOption.rrwebUrl;
|
|
866
|
+
if (hasCustomReplayUrl || this.getTrackerScript()) {
|
|
867
|
+
await this.loadSessionReplayScript(this.getSessionReplayUrl());
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (await this.loadSessionReplayPackage()) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
await this.loadSessionReplayScript(this.getDefaultSessionReplayUrl());
|
|
874
|
+
}
|
|
875
|
+
async loadSessionReplayPackage() {
|
|
876
|
+
try {
|
|
877
|
+
const rrwebModule = (await import('rrweb'));
|
|
878
|
+
const rrweb = rrwebModule.record ? rrwebModule : rrwebModule.default;
|
|
879
|
+
if (!(rrweb === null || rrweb === void 0 ? void 0 : rrweb.record)) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
window.rrweb = rrweb;
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
catch (_a) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
loadSessionReplayScript(url) {
|
|
890
|
+
return new Promise((resolve, reject) => {
|
|
891
|
+
const script = document.createElement('script');
|
|
892
|
+
script.async = true;
|
|
893
|
+
script.src = url;
|
|
894
|
+
script.crossOrigin = 'anonymous';
|
|
895
|
+
script.onload = () => resolve();
|
|
896
|
+
script.onerror = () => reject(new Error('Failed to load rrweb'));
|
|
897
|
+
document.head.appendChild(script);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
getSessionReplayRecordOptions(privacy, userOptions, emit) {
|
|
901
|
+
const options = {
|
|
902
|
+
...userOptions,
|
|
903
|
+
emit,
|
|
904
|
+
};
|
|
905
|
+
const maskInputOptions = typeof options.maskInputOptions === 'object' &&
|
|
906
|
+
options.maskInputOptions !== null
|
|
907
|
+
? options.maskInputOptions
|
|
908
|
+
: {};
|
|
909
|
+
const resolvedPrivacy = this.getSessionReplayPrivacy(privacy);
|
|
910
|
+
if (resolvedPrivacy === 'total') {
|
|
911
|
+
return {
|
|
912
|
+
...options,
|
|
913
|
+
maskAllInputs: true,
|
|
914
|
+
maskTextSelector: '*',
|
|
915
|
+
blockSelector: this.mergeSelectors(options.blockSelector, 'img, picture, video, audio, canvas, svg'),
|
|
916
|
+
recordCanvas: false,
|
|
917
|
+
inlineImages: false,
|
|
918
|
+
emit,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (resolvedPrivacy === 'normal') {
|
|
922
|
+
return {
|
|
923
|
+
...options,
|
|
924
|
+
maskAllInputs: true,
|
|
925
|
+
emit,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
...options,
|
|
930
|
+
maskInputOptions: {
|
|
931
|
+
...maskInputOptions,
|
|
932
|
+
password: true,
|
|
933
|
+
},
|
|
934
|
+
emit,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
mergeSelectors(existing, required) {
|
|
938
|
+
if (typeof existing === 'string' && existing.trim()) {
|
|
939
|
+
return `${existing}, ${required}`;
|
|
940
|
+
}
|
|
941
|
+
return required;
|
|
942
|
+
}
|
|
943
|
+
createReplayId() {
|
|
944
|
+
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
|
945
|
+
return crypto.randomUUID();
|
|
946
|
+
}
|
|
947
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
948
|
+
}
|
|
949
|
+
async sendSessionReplayStart(replayId, privacy) {
|
|
950
|
+
var _a, _b, _c;
|
|
951
|
+
try {
|
|
952
|
+
const apiBase = this.getApiBase();
|
|
953
|
+
const response = await fetch(`${apiBase}/log/session-replay/start`, {
|
|
954
|
+
method: 'POST',
|
|
955
|
+
headers: {
|
|
956
|
+
'Content-Type': 'application/json',
|
|
957
|
+
},
|
|
958
|
+
body: JSON.stringify({
|
|
959
|
+
pid: this.projectID,
|
|
960
|
+
replayId,
|
|
961
|
+
privacy,
|
|
962
|
+
pg: this.activePage ||
|
|
963
|
+
getPath({
|
|
964
|
+
hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
|
|
965
|
+
search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
|
|
966
|
+
}),
|
|
967
|
+
lc: getLocale(),
|
|
968
|
+
tz: getTimezone(),
|
|
969
|
+
profileId: (_c = this.options) === null || _c === void 0 ? void 0 : _c.profileId,
|
|
970
|
+
}),
|
|
971
|
+
});
|
|
972
|
+
return response.ok;
|
|
973
|
+
}
|
|
974
|
+
catch (_d) {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async sendSessionReplayChunk(replayId, privacy, chunkIndex, events, useBeacon) {
|
|
979
|
+
const apiBase = this.getApiBase();
|
|
980
|
+
const url = `${apiBase}/log/session-replay/chunk`;
|
|
981
|
+
const payload = JSON.stringify({
|
|
982
|
+
pid: this.projectID,
|
|
983
|
+
replayId,
|
|
984
|
+
privacy,
|
|
985
|
+
chunkIndex,
|
|
986
|
+
events,
|
|
987
|
+
});
|
|
988
|
+
if (useBeacon && typeof navigator.sendBeacon === 'function') {
|
|
989
|
+
const sent = navigator.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
|
|
990
|
+
if (sent)
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
await fetch(url, {
|
|
995
|
+
method: 'POST',
|
|
996
|
+
headers: {
|
|
997
|
+
'Content-Type': 'application/json',
|
|
998
|
+
},
|
|
999
|
+
keepalive: useBeacon,
|
|
1000
|
+
body: payload,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
catch (_a) { }
|
|
1004
|
+
}
|
|
576
1005
|
async sendRequest(path, body) {
|
|
577
1006
|
var _a;
|
|
578
1007
|
const host = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) || DEFAULT_API_HOST;
|
|
@@ -648,6 +1077,12 @@ function trackErrors(options) {
|
|
|
648
1077
|
}
|
|
649
1078
|
return exports.LIB_INSTANCE.trackErrors(options);
|
|
650
1079
|
}
|
|
1080
|
+
function startSessionReplay(options) {
|
|
1081
|
+
if (!exports.LIB_INSTANCE) {
|
|
1082
|
+
return Promise.resolve(defaultSessionReplayActions);
|
|
1083
|
+
}
|
|
1084
|
+
return exports.LIB_INSTANCE.startSessionReplay(options);
|
|
1085
|
+
}
|
|
651
1086
|
/**
|
|
652
1087
|
* This function is used to manually track an error event.
|
|
653
1088
|
* It's useful if you want to track specific errors in your application.
|
|
@@ -682,17 +1117,16 @@ function pageview(options) {
|
|
|
682
1117
|
}
|
|
683
1118
|
/**
|
|
684
1119
|
* Fetches all feature flags for the project.
|
|
685
|
-
* Results are cached for 5 minutes by default.
|
|
1120
|
+
* Results are cached for 5 minutes by default and share a cache with experiments.
|
|
686
1121
|
*
|
|
687
|
-
* @param options - Options for evaluating feature flags (
|
|
1122
|
+
* @param options - Options for evaluating feature flags (`profileId` only).
|
|
688
1123
|
* @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
|
|
689
1124
|
* @returns A promise that resolves to a record of flag keys to boolean values.
|
|
690
1125
|
*
|
|
691
1126
|
* @example
|
|
692
1127
|
* ```typescript
|
|
693
1128
|
* const flags = await getFeatureFlags({
|
|
694
|
-
*
|
|
695
|
-
* attributes: { cc: 'US', dv: 'desktop' }
|
|
1129
|
+
* profileId: 'user-123'
|
|
696
1130
|
* })
|
|
697
1131
|
*
|
|
698
1132
|
* if (flags['new-checkout']) {
|
|
@@ -709,13 +1143,13 @@ async function getFeatureFlags(options, forceRefresh) {
|
|
|
709
1143
|
* Gets the value of a single feature flag.
|
|
710
1144
|
*
|
|
711
1145
|
* @param key - The feature flag key.
|
|
712
|
-
* @param options - Options for evaluating the feature flag (
|
|
713
|
-
* @param defaultValue -
|
|
1146
|
+
* @param options - Options for evaluating the feature flag (`profileId` only).
|
|
1147
|
+
* @param defaultValue - Optional default value to return if the flag is not found. Defaults to false.
|
|
714
1148
|
* @returns A promise that resolves to the boolean value of the flag.
|
|
715
1149
|
*
|
|
716
1150
|
* @example
|
|
717
1151
|
* ```typescript
|
|
718
|
-
* const isEnabled = await getFeatureFlag('dark-mode', {
|
|
1152
|
+
* const isEnabled = await getFeatureFlag('dark-mode', { profileId: 'user-123' })
|
|
719
1153
|
*
|
|
720
1154
|
* if (isEnabled) {
|
|
721
1155
|
* // Enable dark mode
|
|
@@ -728,8 +1162,8 @@ async function getFeatureFlag(key, options, defaultValue = false) {
|
|
|
728
1162
|
return exports.LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);
|
|
729
1163
|
}
|
|
730
1164
|
/**
|
|
731
|
-
* Clears the cached feature flags, forcing a fresh fetch on the next call.
|
|
732
|
-
* Useful when you know the user's
|
|
1165
|
+
* Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
|
|
1166
|
+
* Useful when you know the user's profile has changed.
|
|
733
1167
|
*/
|
|
734
1168
|
function clearFeatureFlagsCache() {
|
|
735
1169
|
if (!exports.LIB_INSTANCE)
|
|
@@ -737,10 +1171,10 @@ function clearFeatureFlagsCache() {
|
|
|
737
1171
|
exports.LIB_INSTANCE.clearFeatureFlagsCache();
|
|
738
1172
|
}
|
|
739
1173
|
/**
|
|
740
|
-
* Fetches
|
|
1174
|
+
* Fetches variant assignments for running A/B test experiments returned by feature flag evaluation.
|
|
741
1175
|
* Results are cached for 5 minutes by default (shared cache with feature flags).
|
|
742
1176
|
*
|
|
743
|
-
* @param options - Options for evaluating experiments.
|
|
1177
|
+
* @param options - Options for evaluating experiments (`profileId` only).
|
|
744
1178
|
* @param forceRefresh - If true, bypasses the cache and fetches fresh data.
|
|
745
1179
|
* @returns A promise that resolves to a record of experiment IDs to variant keys.
|
|
746
1180
|
*
|
|
@@ -767,13 +1201,16 @@ async function getExperiments(options, forceRefresh) {
|
|
|
767
1201
|
* Gets the variant key for a specific A/B test experiment.
|
|
768
1202
|
*
|
|
769
1203
|
* @param experimentId - The experiment ID.
|
|
770
|
-
* @param options - Options for evaluating the experiment.
|
|
771
|
-
* @param defaultVariant -
|
|
1204
|
+
* @param options - Options for evaluating the experiment (`profileId` only).
|
|
1205
|
+
* @param defaultVariant - Optional default variant key to return if the experiment is not found. Defaults to null.
|
|
772
1206
|
* @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
|
|
773
1207
|
*
|
|
774
1208
|
* @example
|
|
775
1209
|
* ```typescript
|
|
776
|
-
* const variant = await getExperiment('checkout-redesign-experiment-id')
|
|
1210
|
+
* const variant = await getExperiment('checkout-redesign-experiment-id', { profileId: 'user-123' })
|
|
1211
|
+
*
|
|
1212
|
+
* // Optional fallback variant:
|
|
1213
|
+
* const variantWithFallback = await getExperiment('checkout-redesign-experiment-id', undefined, 'control')
|
|
777
1214
|
*
|
|
778
1215
|
* if (variant === 'new-checkout') {
|
|
779
1216
|
* // Show new checkout flow
|
|
@@ -867,6 +1304,7 @@ exports.getProfileId = getProfileId;
|
|
|
867
1304
|
exports.getSessionId = getSessionId;
|
|
868
1305
|
exports.init = init;
|
|
869
1306
|
exports.pageview = pageview;
|
|
1307
|
+
exports.startSessionReplay = startSessionReplay;
|
|
870
1308
|
exports.track = track;
|
|
871
1309
|
exports.trackError = trackError;
|
|
872
1310
|
exports.trackErrors = trackErrors;
|