swetrix 4.2.0 → 4.4.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.
@@ -1,11 +1,58 @@
1
1
  import { isInBrowser, isLocalhost, isAutomated, getLocale, getTimezone, getReferrer, getQueryString, getUTMCampaign, getUTMMedium, getUTMSource, getUTMTerm, getUTMContent, getPath, } from './utils.js';
2
+ const SESSION_REPLAY_PRIVACY_VALUES = ['total', 'normal', 'none'];
2
3
  export const defaultActions = {
3
4
  stop() { },
4
5
  };
6
+ export const defaultSessionReplayActions = {
7
+ async stop() { },
8
+ async flush() { },
9
+ };
5
10
  const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
6
11
  const DEFAULT_API_BASE = 'https://api.swetrix.com';
12
+ const DEFAULT_RRWEB_FILE = 'replaylibrary.min.js';
13
+ const DEFAULT_RRWEB_URL = `https://cdn.jsdelivr.net/npm/swetrix@latest/dist/${DEFAULT_RRWEB_FILE}`;
14
+ const DEFAULT_SESSION_REPLAY_FLUSH_INTERVAL = 5000;
15
+ const DEFAULT_SESSION_REPLAY_MAX_EVENTS = 100;
16
+ const DEFAULT_SESSION_REPLAY_MAX_CHUNK_BYTES = 512 * 1024;
17
+ const DEFAULT_SESSION_REPLAY_MAX_EVENT_BYTES = 5 * 1024 * 1024;
18
+ const DEFAULT_SESSION_REPLAY_MAX_DURATION_MS = 30 * 60 * 1000;
19
+ const DEFAULT_SESSION_REPLAY_PRIVACY = 'total';
20
+ const DEFAULT_SESSION_REPLAY_SAMPLING = {
21
+ mousemove: 50,
22
+ scroll: 150,
23
+ input: 'last',
24
+ };
25
+ const DEFAULT_SESSION_REPLAY_SLIM_DOM_OPTIONS = {
26
+ script: true,
27
+ comment: true,
28
+ headFavicon: true,
29
+ headWhitespace: true,
30
+ headMetaDescKeywords: true,
31
+ headMetaSocial: true,
32
+ headMetaRobots: true,
33
+ headMetaHttpEquiv: true,
34
+ headMetaAuthorship: true,
35
+ headMetaVerification: true,
36
+ };
37
+ const SESSION_REPLAY_ACTIVITY_EVENTS = [
38
+ 'click',
39
+ 'keydown',
40
+ 'mousedown',
41
+ 'mousemove',
42
+ 'scroll',
43
+ 'touchstart',
44
+ ];
7
45
  // Default cache duration: 5 minutes
8
46
  const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
47
+ const getStringByteLength = (value) => {
48
+ if (typeof TextEncoder !== 'undefined') {
49
+ return new TextEncoder().encode(value).length;
50
+ }
51
+ if (typeof Blob !== 'undefined') {
52
+ return new Blob([value]).size;
53
+ }
54
+ return value.length;
55
+ };
9
56
  export class Lib {
10
57
  constructor(projectID, options) {
11
58
  this.projectID = projectID;
@@ -17,9 +64,15 @@ export class Lib {
17
64
  this.activePage = null;
18
65
  this.errorListenerExists = false;
19
66
  this.cachedData = null;
67
+ this.rrwebLoader = null;
68
+ this.sessionReplayActions = null;
69
+ this.sessionReplayInitPromise = null;
20
70
  this.trackPathChange = this.trackPathChange.bind(this);
21
71
  this.heartbeat = this.heartbeat.bind(this);
22
72
  this.captureError = this.captureError.bind(this);
73
+ if (this.getSessionReplayPreloadOption()) {
74
+ void this.preloadSessionReplay().catch(() => undefined);
75
+ }
23
76
  }
24
77
  captureError(event) {
25
78
  if (typeof this.errorsOptions?.sampleRate === 'number' && this.errorsOptions.sampleRate >= Math.random()) {
@@ -163,10 +216,10 @@ export class Lib {
163
216
  };
164
217
  }
165
218
  /**
166
- * Fetches all feature flags and experiments for the project.
167
- * Results are cached for 5 minutes by default.
219
+ * Fetches all feature flags for the project.
220
+ * Results are cached for 5 minutes by default and share a cache with experiments.
168
221
  *
169
- * @param options - Options for evaluating feature flags.
222
+ * @param options - Options for evaluating feature flags (`profileId` only).
170
223
  * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
171
224
  * @returns A promise that resolves to a record of flag keys to boolean values.
172
225
  */
@@ -231,8 +284,8 @@ export class Lib {
231
284
  * Gets the value of a single feature flag.
232
285
  *
233
286
  * @param key - The feature flag key.
234
- * @param options - Options for evaluating the feature flag.
235
- * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
287
+ * @param options - Options for evaluating the feature flag (`profileId` only).
288
+ * @param defaultValue - Optional default value to return if the flag is not found. Defaults to false.
236
289
  * @returns A promise that resolves to the boolean value of the flag.
237
290
  */
238
291
  async getFeatureFlag(key, options, defaultValue = false) {
@@ -246,16 +299,16 @@ export class Lib {
246
299
  this.cachedData = null;
247
300
  }
248
301
  /**
249
- * Fetches all A/B test experiments for the project.
302
+ * Fetches variant assignments for running A/B test experiments returned by feature flag evaluation.
250
303
  * Results are cached for 5 minutes by default (shared cache with feature flags).
251
304
  *
252
- * @param options - Options for evaluating experiments.
305
+ * @param options - Options for evaluating experiments (`profileId` only).
253
306
  * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
254
307
  * @returns A promise that resolves to a record of experiment IDs to variant keys.
255
308
  *
256
309
  * @example
257
310
  * ```typescript
258
- * const experiments = await getExperiments()
311
+ * const experiments = await getExperiments({ profileId: 'user-123' })
259
312
  * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
260
313
  * ```
261
314
  */
@@ -285,13 +338,16 @@ export class Lib {
285
338
  * Gets the variant key for a specific A/B test experiment.
286
339
  *
287
340
  * @param experimentId - The experiment ID.
288
- * @param options - Options for evaluating the experiment.
289
- * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
341
+ * @param options - Options for evaluating the experiment (`profileId` only).
342
+ * @param defaultVariant - Optional default variant key to return if the experiment is not found. Defaults to null.
290
343
  * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
291
344
  *
292
345
  * @example
293
346
  * ```typescript
294
- * const variant = await getExperiment('checkout-redesign')
347
+ * const variant = await getExperiment('checkout-redesign', { profileId: 'user-123' })
348
+ *
349
+ * // Optional fallback variant:
350
+ * const variantWithFallback = await getExperiment('checkout-redesign', undefined, 'control')
295
351
  *
296
352
  * if (variant === 'new-checkout') {
297
353
  * // Show new checkout flow
@@ -405,6 +461,195 @@ export class Lib {
405
461
  return null;
406
462
  }
407
463
  }
464
+ async startSessionReplay(options = {}) {
465
+ if (this.sessionReplayActions) {
466
+ return this.sessionReplayActions;
467
+ }
468
+ if (this.sessionReplayInitPromise) {
469
+ return this.sessionReplayInitPromise;
470
+ }
471
+ const initPromise = this.initialiseSessionReplay(options);
472
+ this.sessionReplayInitPromise = initPromise;
473
+ try {
474
+ return await initPromise;
475
+ }
476
+ finally {
477
+ if (this.sessionReplayInitPromise === initPromise) {
478
+ this.sessionReplayInitPromise = null;
479
+ }
480
+ }
481
+ }
482
+ async initialiseSessionReplay(options) {
483
+ if (this.sessionReplayActions) {
484
+ return this.sessionReplayActions;
485
+ }
486
+ if (!this.canTrack()) {
487
+ return defaultSessionReplayActions;
488
+ }
489
+ const maxDurationMs = typeof options.maxDurationMs === 'number' && options.maxDurationMs > 0
490
+ ? options.maxDurationMs
491
+ : DEFAULT_SESSION_REPLAY_MAX_DURATION_MS;
492
+ if (!this.shouldSampleSessionReplay(options.sampleRate)) {
493
+ return defaultSessionReplayActions;
494
+ }
495
+ try {
496
+ await this.preloadSessionReplay();
497
+ }
498
+ catch {
499
+ return defaultSessionReplayActions;
500
+ }
501
+ const rrweb = window.rrweb;
502
+ if (!rrweb?.record) {
503
+ return defaultSessionReplayActions;
504
+ }
505
+ const privacy = this.getSessionReplayPrivacy(options.privacy);
506
+ const proposedReplayId = this.createReplayId();
507
+ const started = await this.sendSessionReplayStart(proposedReplayId, privacy);
508
+ if (!started) {
509
+ return defaultSessionReplayActions;
510
+ }
511
+ const replayId = started.replayId;
512
+ const flushIntervalMs = typeof options.flushIntervalMs === 'number' && options.flushIntervalMs > 0
513
+ ? options.flushIntervalMs
514
+ : DEFAULT_SESSION_REPLAY_FLUSH_INTERVAL;
515
+ const maxEventsPerChunk = typeof options.maxEventsPerChunk === 'number' &&
516
+ options.maxEventsPerChunk > 0
517
+ ? Math.floor(options.maxEventsPerChunk)
518
+ : DEFAULT_SESSION_REPLAY_MAX_EVENTS;
519
+ const maxBytesPerChunkCandidate = typeof options.maxBytesPerChunk === 'number'
520
+ ? Math.floor(options.maxBytesPerChunk)
521
+ : Number.NaN;
522
+ const maxBytesPerChunk = maxBytesPerChunkCandidate >= 1
523
+ ? maxBytesPerChunkCandidate
524
+ : DEFAULT_SESSION_REPLAY_MAX_CHUNK_BYTES;
525
+ const maxBytesPerEventCandidate = typeof options.maxBytesPerEvent === 'number'
526
+ ? Math.floor(options.maxBytesPerEvent)
527
+ : Number.NaN;
528
+ const maxBytesPerEvent = maxBytesPerEventCandidate >= 1
529
+ ? maxBytesPerEventCandidate
530
+ : DEFAULT_SESSION_REPLAY_MAX_EVENT_BYTES;
531
+ const idleTimeoutMs = typeof options.idleTimeoutMs === 'number' && options.idleTimeoutMs > 0
532
+ ? options.idleTimeoutMs
533
+ : null;
534
+ let chunkIndex = started.nextChunkIndex;
535
+ let stopped = false;
536
+ let events = [];
537
+ let eventsByteLength = 0;
538
+ let flushing = Promise.resolve();
539
+ let maxDurationTimer;
540
+ let idleTimer;
541
+ const flush = async (useBeacon = false) => {
542
+ if (!events.length)
543
+ return;
544
+ const chunk = events;
545
+ events = [];
546
+ eventsByteLength = 0;
547
+ const currentChunkIndex = chunkIndex++;
548
+ flushing = flushing
549
+ .catch(() => undefined)
550
+ .then(() => this.sendSessionReplayChunk(replayId, privacy, currentChunkIndex, chunk, useBeacon));
551
+ await flushing;
552
+ };
553
+ const userEmit = options.rrweb?.emit;
554
+ const recordOptions = this.getSessionReplayRecordOptions(privacy, options.rrweb, (event) => {
555
+ try {
556
+ userEmit?.(event);
557
+ }
558
+ catch { }
559
+ let eventByteLength = 0;
560
+ try {
561
+ eventByteLength = getStringByteLength(JSON.stringify(event));
562
+ }
563
+ catch {
564
+ return;
565
+ }
566
+ if (eventByteLength > maxBytesPerEvent) {
567
+ return;
568
+ }
569
+ if (events.length &&
570
+ eventsByteLength + eventByteLength > maxBytesPerChunk) {
571
+ void flush();
572
+ }
573
+ events.push(event);
574
+ eventsByteLength += eventByteLength;
575
+ if (events.length >= maxEventsPerChunk ||
576
+ eventsByteLength >= maxBytesPerChunk) {
577
+ void flush();
578
+ }
579
+ }, Boolean(options.recordIframes), options.maskAllText);
580
+ const stopRecording = rrweb.record(recordOptions);
581
+ const timer = setInterval(() => void flush(), flushIntervalMs);
582
+ const flushOnPageExit = () => void flush(true);
583
+ const flushOnHidden = () => {
584
+ if (document.visibilityState === 'hidden') {
585
+ void flush(true);
586
+ }
587
+ };
588
+ const clearIdleTimer = () => {
589
+ if (idleTimer) {
590
+ clearTimeout(idleTimer);
591
+ idleTimer = undefined;
592
+ }
593
+ };
594
+ const stopSessionReplay = async () => {
595
+ if (stopped)
596
+ return;
597
+ stopped = true;
598
+ clearInterval(timer);
599
+ if (maxDurationTimer) {
600
+ clearTimeout(maxDurationTimer);
601
+ }
602
+ clearIdleTimer();
603
+ window.removeEventListener('pagehide', flushOnPageExit);
604
+ document.removeEventListener('visibilitychange', flushOnHidden);
605
+ SESSION_REPLAY_ACTIVITY_EVENTS.forEach((eventName) => {
606
+ window.removeEventListener(eventName, resetIdleTimer);
607
+ });
608
+ stopRecording?.();
609
+ await flush();
610
+ this.sessionReplayActions = null;
611
+ this.sessionReplayInitPromise = null;
612
+ };
613
+ const resetIdleTimer = () => {
614
+ if (!idleTimeoutMs || stopped)
615
+ return;
616
+ clearIdleTimer();
617
+ idleTimer = setTimeout(() => void stopSessionReplay(), idleTimeoutMs);
618
+ };
619
+ window.addEventListener('pagehide', flushOnPageExit);
620
+ document.addEventListener('visibilitychange', flushOnHidden);
621
+ maxDurationTimer = setTimeout(() => void stopSessionReplay(), maxDurationMs);
622
+ if (idleTimeoutMs) {
623
+ SESSION_REPLAY_ACTIVITY_EVENTS.forEach((eventName) => {
624
+ window.addEventListener(eventName, resetIdleTimer, { passive: true });
625
+ });
626
+ resetIdleTimer();
627
+ }
628
+ this.sessionReplayActions = {
629
+ stop: stopSessionReplay,
630
+ flush: async () => {
631
+ await flush();
632
+ },
633
+ };
634
+ return this.sessionReplayActions;
635
+ }
636
+ shouldSampleSessionReplay(sampleRate) {
637
+ if (typeof sampleRate !== 'number') {
638
+ return true;
639
+ }
640
+ if (sampleRate <= 0) {
641
+ return false;
642
+ }
643
+ if (sampleRate >= 1) {
644
+ return true;
645
+ }
646
+ return Math.random() < sampleRate;
647
+ }
648
+ getSessionReplayPrivacy(privacy) {
649
+ return SESSION_REPLAY_PRIVACY_VALUES.includes(privacy)
650
+ ? privacy
651
+ : DEFAULT_SESSION_REPLAY_PRIVACY;
652
+ }
408
653
  /**
409
654
  * Gets the API base URL (without /log suffix).
410
655
  */
@@ -489,6 +734,286 @@ export class Lib {
489
734
  }
490
735
  return true;
491
736
  }
737
+ getSessionReplayUrl() {
738
+ const replayOption = this.getSessionReplayPreloadOption();
739
+ if (replayOption &&
740
+ typeof replayOption === 'object' &&
741
+ replayOption.rrwebUrl) {
742
+ return replayOption.rrwebUrl;
743
+ }
744
+ return this.getDefaultSessionReplayUrl();
745
+ }
746
+ getSessionReplayPreloadOption() {
747
+ return this.options?.preloadSessionReplay;
748
+ }
749
+ getDefaultSessionReplayUrl() {
750
+ if (!isInBrowser()) {
751
+ return DEFAULT_RRWEB_URL;
752
+ }
753
+ const trackerScript = this.getTrackerScript();
754
+ if (trackerScript?.src) {
755
+ const { hostname, pathname } = new URL(trackerScript.src);
756
+ if (hostname === 'swetrix.org' &&
757
+ /^\/swetrix(\.min)?\.js$/i.test(pathname)) {
758
+ return DEFAULT_RRWEB_URL;
759
+ }
760
+ return new URL(DEFAULT_RRWEB_FILE, trackerScript.src).toString();
761
+ }
762
+ return DEFAULT_RRWEB_URL;
763
+ }
764
+ getTrackerScript() {
765
+ const trackerScript = Array.from(document.scripts).find((script) => {
766
+ if (!script.src) {
767
+ return false;
768
+ }
769
+ try {
770
+ const { pathname } = new URL(script.src);
771
+ return /(^|\/)swetrix(\.min)?\.js$/i.test(pathname);
772
+ }
773
+ catch {
774
+ return false;
775
+ }
776
+ });
777
+ return trackerScript;
778
+ }
779
+ preloadSessionReplay() {
780
+ if (!isInBrowser()) {
781
+ return Promise.resolve();
782
+ }
783
+ if (window.rrweb?.record) {
784
+ return Promise.resolve();
785
+ }
786
+ if (this.rrwebLoader) {
787
+ return this.rrwebLoader;
788
+ }
789
+ if (window.__SWETRIX_RRWEB_LOADING__) {
790
+ this.rrwebLoader = window.__SWETRIX_RRWEB_LOADING__;
791
+ void this.rrwebLoader.catch(() => {
792
+ if (window.__SWETRIX_RRWEB_LOADING__ === this.rrwebLoader) {
793
+ delete window.__SWETRIX_RRWEB_LOADING__;
794
+ }
795
+ this.rrwebLoader = null;
796
+ });
797
+ return this.rrwebLoader;
798
+ }
799
+ this.rrwebLoader = this.loadSessionReplayRecorder();
800
+ window.__SWETRIX_RRWEB_LOADING__ = this.rrwebLoader;
801
+ const loader = this.rrwebLoader;
802
+ void loader.catch(() => {
803
+ if (window.__SWETRIX_RRWEB_LOADING__ === loader) {
804
+ delete window.__SWETRIX_RRWEB_LOADING__;
805
+ }
806
+ if (this.rrwebLoader === loader) {
807
+ this.rrwebLoader = null;
808
+ }
809
+ });
810
+ return this.rrwebLoader;
811
+ }
812
+ async loadSessionReplayRecorder() {
813
+ const replayOption = this.getSessionReplayPreloadOption();
814
+ const hasCustomReplayUrl = replayOption && typeof replayOption === 'object' && replayOption.rrwebUrl;
815
+ if (hasCustomReplayUrl || this.getTrackerScript()) {
816
+ await this.loadSessionReplayScript(this.getSessionReplayUrl());
817
+ return;
818
+ }
819
+ if (await this.loadSessionReplayPackage()) {
820
+ return;
821
+ }
822
+ await this.loadSessionReplayScript(this.getDefaultSessionReplayUrl());
823
+ }
824
+ async loadSessionReplayPackage() {
825
+ try {
826
+ const rrwebModule = (await import('rrweb'));
827
+ const rrweb = rrwebModule.record ? rrwebModule : rrwebModule.default;
828
+ if (!rrweb?.record) {
829
+ return false;
830
+ }
831
+ window.rrweb = rrweb;
832
+ return true;
833
+ }
834
+ catch {
835
+ return false;
836
+ }
837
+ }
838
+ loadSessionReplayScript(url) {
839
+ return new Promise((resolve, reject) => {
840
+ const script = document.createElement('script');
841
+ script.async = true;
842
+ script.src = url;
843
+ script.crossOrigin = 'anonymous';
844
+ script.onload = () => resolve();
845
+ script.onerror = () => reject(new Error('Failed to load rrweb'));
846
+ document.head.appendChild(script);
847
+ });
848
+ }
849
+ getSessionReplayRecordOptions(privacy, userOptions, emit, recordIframes, maskAllText) {
850
+ const hasUserSampling = userOptions &&
851
+ Object.prototype.hasOwnProperty.call(userOptions, 'sampling');
852
+ const hasUserSlimDOMOptions = userOptions &&
853
+ Object.prototype.hasOwnProperty.call(userOptions, 'slimDOMOptions');
854
+ const sampling = typeof userOptions?.sampling === 'object' && userOptions.sampling !== null
855
+ ? {
856
+ ...DEFAULT_SESSION_REPLAY_SAMPLING,
857
+ ...userOptions.sampling,
858
+ }
859
+ : hasUserSampling
860
+ ? userOptions?.sampling
861
+ : DEFAULT_SESSION_REPLAY_SAMPLING;
862
+ const slimDOMOptions = typeof userOptions?.slimDOMOptions === 'object' &&
863
+ userOptions.slimDOMOptions !== null
864
+ ? {
865
+ ...DEFAULT_SESSION_REPLAY_SLIM_DOM_OPTIONS,
866
+ ...userOptions.slimDOMOptions,
867
+ }
868
+ : hasUserSlimDOMOptions
869
+ ? userOptions?.slimDOMOptions
870
+ : DEFAULT_SESSION_REPLAY_SLIM_DOM_OPTIONS;
871
+ const options = {
872
+ recordCanvas: false,
873
+ recordCrossOriginIframes: false,
874
+ collectFonts: false,
875
+ inlineImages: false,
876
+ ...userOptions,
877
+ sampling,
878
+ slimDOMOptions,
879
+ emit,
880
+ };
881
+ const maskInputOptions = typeof options.maskInputOptions === 'object' &&
882
+ options.maskInputOptions !== null
883
+ ? options.maskInputOptions
884
+ : {};
885
+ const resolvedPrivacy = this.getSessionReplayPrivacy(privacy);
886
+ const defaultBlockSelector = recordIframes ? undefined : 'iframe';
887
+ const resolvedMaskAllText = typeof maskAllText === 'boolean'
888
+ ? maskAllText
889
+ : resolvedPrivacy === 'total';
890
+ const textMaskingOptions = resolvedMaskAllText
891
+ ? { maskTextSelector: '*' }
892
+ : {};
893
+ if (resolvedPrivacy === 'total') {
894
+ return {
895
+ ...options,
896
+ ...textMaskingOptions,
897
+ maskAllInputs: true,
898
+ blockSelector: this.mergeSelectors(this.mergeSelectors(options.blockSelector, defaultBlockSelector), 'img, picture, video, audio, canvas, svg'),
899
+ recordCanvas: false,
900
+ inlineImages: false,
901
+ emit,
902
+ };
903
+ }
904
+ if (resolvedPrivacy === 'normal') {
905
+ return {
906
+ ...options,
907
+ ...textMaskingOptions,
908
+ maskAllInputs: true,
909
+ blockSelector: this.mergeSelectors(options.blockSelector, defaultBlockSelector),
910
+ emit,
911
+ };
912
+ }
913
+ return {
914
+ ...options,
915
+ ...textMaskingOptions,
916
+ blockSelector: this.mergeSelectors(options.blockSelector, defaultBlockSelector),
917
+ maskInputOptions: {
918
+ ...maskInputOptions,
919
+ password: true,
920
+ },
921
+ emit,
922
+ };
923
+ }
924
+ mergeSelectors(existing, required) {
925
+ if (!required) {
926
+ return typeof existing === 'string' ? existing : undefined;
927
+ }
928
+ if (typeof existing === 'string' && existing.trim()) {
929
+ return `${existing}, ${required}`;
930
+ }
931
+ return required;
932
+ }
933
+ createReplayId() {
934
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
935
+ return crypto.randomUUID();
936
+ }
937
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
938
+ }
939
+ async sendSessionReplayStart(replayId, privacy) {
940
+ try {
941
+ const apiBase = this.getApiBase();
942
+ const response = await fetch(`${apiBase}/log/session-replay/start`, {
943
+ method: 'POST',
944
+ headers: {
945
+ 'Content-Type': 'application/json',
946
+ },
947
+ body: JSON.stringify({
948
+ pid: this.projectID,
949
+ replayId,
950
+ privacy,
951
+ pg: this.activePage ||
952
+ getPath({
953
+ hash: this.pageViewsOptions?.hash,
954
+ search: this.pageViewsOptions?.search,
955
+ }),
956
+ lc: getLocale(),
957
+ tz: getTimezone(),
958
+ profileId: this.options?.profileId,
959
+ }),
960
+ });
961
+ if (!response.ok) {
962
+ return null;
963
+ }
964
+ try {
965
+ const result = (await response.json());
966
+ const resolvedReplayId = typeof result.replayId === 'string' && result.replayId
967
+ ? result.replayId
968
+ : replayId;
969
+ const resolvedChunkIndex = typeof result.nextChunkIndex === 'number' &&
970
+ Number.isFinite(result.nextChunkIndex) &&
971
+ result.nextChunkIndex >= 0
972
+ ? Math.floor(result.nextChunkIndex)
973
+ : 0;
974
+ return {
975
+ replayId: resolvedReplayId,
976
+ nextChunkIndex: resolvedChunkIndex,
977
+ };
978
+ }
979
+ catch {
980
+ return {
981
+ replayId,
982
+ nextChunkIndex: 0,
983
+ };
984
+ }
985
+ }
986
+ catch {
987
+ return null;
988
+ }
989
+ }
990
+ async sendSessionReplayChunk(replayId, privacy, chunkIndex, events, useBeacon) {
991
+ const apiBase = this.getApiBase();
992
+ const url = `${apiBase}/log/session-replay/chunk`;
993
+ const payload = JSON.stringify({
994
+ pid: this.projectID,
995
+ replayId,
996
+ privacy,
997
+ chunkIndex,
998
+ events,
999
+ });
1000
+ if (useBeacon && typeof navigator.sendBeacon === 'function') {
1001
+ const sent = navigator.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
1002
+ if (sent)
1003
+ return;
1004
+ }
1005
+ try {
1006
+ await fetch(url, {
1007
+ method: 'POST',
1008
+ headers: {
1009
+ 'Content-Type': 'application/json',
1010
+ },
1011
+ keepalive: useBeacon,
1012
+ body: payload,
1013
+ });
1014
+ }
1015
+ catch { }
1016
+ }
492
1017
  async sendRequest(path, body) {
493
1018
  const host = this.options?.apiURL || DEFAULT_API_HOST;
494
1019
  await fetch(`${host}/${path}`, {