@openreplay/tracker 18.0.17 → 18.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/entry.js +201 -49
- package/dist/cjs/entry.js.map +1 -1
- package/dist/cjs/index.js +201 -49
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/main/app/index.d.ts +11 -0
- package/dist/cjs/main/modules/analytics/batcher.d.ts +2 -1
- package/dist/cjs/main/modules/analytics/utils.d.ts +1 -1
- package/dist/cjs/main/modules/conditionsManager.d.ts +2 -1
- package/dist/lib/entry.js +201 -49
- package/dist/lib/entry.js.map +1 -1
- package/dist/lib/index.js +201 -49
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/main/app/index.d.ts +11 -0
- package/dist/lib/main/modules/analytics/batcher.d.ts +2 -1
- package/dist/lib/main/modules/analytics/utils.d.ts +1 -1
- package/dist/lib/main/modules/conditionsManager.d.ts +2 -1
- package/dist/types/main/app/index.d.ts +11 -0
- package/dist/types/main/modules/analytics/batcher.d.ts +2 -1
- package/dist/types/main/modules/analytics/utils.d.ts +1 -1
- package/dist/types/main/modules/conditionsManager.d.ts +2 -1
- package/package.json +1 -1
package/dist/cjs/entry.js
CHANGED
|
@@ -78,7 +78,7 @@ class ConditionsManager {
|
|
|
78
78
|
return resultCondition;
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
|
-
this.
|
|
81
|
+
this.durationInts = [];
|
|
82
82
|
}
|
|
83
83
|
setConditions(conditions) {
|
|
84
84
|
this.conditions = conditions;
|
|
@@ -171,18 +171,22 @@ class ConditionsManager {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
clearDurationInt(int) {
|
|
175
|
+
clearInterval(int);
|
|
176
|
+
this.durationInts = this.durationInts.filter((i) => i !== int);
|
|
177
|
+
}
|
|
174
178
|
processDuration(durationMs, condName) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
// Track each interval independently: with more than one duration condition a
|
|
180
|
+
// single shared field would leak every interval but the last, and the timer
|
|
181
|
+
// would keep firing after it triggered.
|
|
182
|
+
const int = setInterval(() => {
|
|
183
|
+
if (performance.now() > durationMs) {
|
|
184
|
+
this.clearDurationInt(int);
|
|
178
185
|
this.trigger(condName);
|
|
179
186
|
}
|
|
180
187
|
}, 1000);
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
clearInterval(this.durationInt);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
188
|
+
this.durationInts.push(int);
|
|
189
|
+
this.app.attachStopCallback(() => this.clearDurationInt(int));
|
|
186
190
|
}
|
|
187
191
|
networkRequest(message) {
|
|
188
192
|
// method - 2, url - 3, status - 6, duration - 8
|
|
@@ -701,13 +705,18 @@ function throttleWithTrailing(fn, interval) {
|
|
|
701
705
|
timeouts.delete(key);
|
|
702
706
|
}
|
|
703
707
|
lastCalls.set(key, now);
|
|
708
|
+
// args are consumed now; don't retain them keyed by every id we ever saw
|
|
709
|
+
lastArgs.delete(key);
|
|
704
710
|
fn(key, ...args);
|
|
705
711
|
}
|
|
706
712
|
else if (!timeouts.has(key)) {
|
|
707
713
|
const timeoutId = setTimeout(() => {
|
|
708
|
-
lastCalls.set(key, Date.now());
|
|
709
714
|
timeouts.delete(key);
|
|
710
715
|
const finalArgs = lastArgs.get(key);
|
|
716
|
+
// Trailing call closes the throttle window for this key: drop its
|
|
717
|
+
// bookkeeping so the maps don't grow one permanent entry per key.
|
|
718
|
+
lastArgs.delete(key);
|
|
719
|
+
lastCalls.delete(key);
|
|
711
720
|
fn(key, ...finalArgs);
|
|
712
721
|
}, remaining);
|
|
713
722
|
timeouts.set(key, timeoutId);
|
|
@@ -2954,10 +2963,18 @@ class Sanitizer {
|
|
|
2954
2963
|
data = data.replace(/\d/g, '0');
|
|
2955
2964
|
}
|
|
2956
2965
|
if (this.options.obscureTextEmails) {
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2966
|
+
// Match every email-like substring anywhere in the text (not just when the
|
|
2967
|
+
// whole node is a single email), and preserve every domain label including
|
|
2968
|
+
// multi-part TLDs (e.g. .co.uk) so nothing leaks through unmasked.
|
|
2969
|
+
data = data.replace(/[^\s@]+@[^\s@]+\.[^\s@]+/g, (email) => {
|
|
2970
|
+
const atIdx = email.lastIndexOf('@');
|
|
2971
|
+
const name = email.slice(0, atIdx);
|
|
2972
|
+
const domain = email.slice(atIdx + 1);
|
|
2973
|
+
const maskedDomain = domain
|
|
2974
|
+
.split('.')
|
|
2975
|
+
.map((part) => stars(part))
|
|
2976
|
+
.join('.');
|
|
2977
|
+
return `${stars(name)}@${maskedDomain}`;
|
|
2961
2978
|
});
|
|
2962
2979
|
}
|
|
2963
2980
|
return data;
|
|
@@ -2994,8 +3011,14 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
2994
3011
|
console.warn('Openreplay: Invalid xlink:href or href found on <use>.');
|
|
2995
3012
|
return;
|
|
2996
3013
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
3014
|
+
// Key the cache by url + symbol + mode: the same symbol id lives in different
|
|
3015
|
+
// sprite files, and different modes store different value shapes (object vs
|
|
3016
|
+
// string vs data-url), so keying by symbolId alone returns the wrong asset.
|
|
3017
|
+
// Namespace remote vs in-document sprites so a relative sprite file literally
|
|
3018
|
+
// named "local" can't collide with the in-document bucket.
|
|
3019
|
+
const cacheKey = url ? `url:${url}#${symbolId}#${mode}` : `doc:${symbolId}#${mode}`;
|
|
3020
|
+
if (iconCache[cacheKey]) {
|
|
3021
|
+
return iconCache[cacheKey];
|
|
2999
3022
|
}
|
|
3000
3023
|
// happens if svg spritemap is local, fastest case for us
|
|
3001
3024
|
if (!url && symbolId) {
|
|
@@ -3014,7 +3037,7 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
3014
3037
|
${symbol.innerHTML}
|
|
3015
3038
|
</svg>
|
|
3016
3039
|
`;
|
|
3017
|
-
iconCache[
|
|
3040
|
+
iconCache[cacheKey] = inlineSvg;
|
|
3018
3041
|
return inlineSvg;
|
|
3019
3042
|
}
|
|
3020
3043
|
else {
|
|
@@ -3049,10 +3072,21 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
3049
3072
|
}
|
|
3050
3073
|
else {
|
|
3051
3074
|
svgUrlCache[url] = 1;
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3075
|
+
try {
|
|
3076
|
+
const response = await fetch(url);
|
|
3077
|
+
const svgText = await response.text();
|
|
3078
|
+
svgDoc = domParser.parseFromString(svgText, 'image/svg+xml');
|
|
3079
|
+
svgUrlCache[url] = svgDoc;
|
|
3080
|
+
}
|
|
3081
|
+
catch (e) {
|
|
3082
|
+
// Clear the in-flight sentinel so a failed fetch doesn't wedge every
|
|
3083
|
+
// future <use> for this url into a 10s poll-then-give-up loop.
|
|
3084
|
+
delete svgUrlCache[url];
|
|
3085
|
+
throw e;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (!svgDoc) {
|
|
3089
|
+
return;
|
|
3056
3090
|
}
|
|
3057
3091
|
// @ts-ignore
|
|
3058
3092
|
const symbol = svgDoc.getElementById(symbolId);
|
|
@@ -3067,7 +3101,7 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
3067
3101
|
${symbol.innerHTML}
|
|
3068
3102
|
</svg>
|
|
3069
3103
|
`;
|
|
3070
|
-
iconCache[
|
|
3104
|
+
iconCache[cacheKey] = inlineSvg;
|
|
3071
3105
|
return inlineSvg;
|
|
3072
3106
|
}
|
|
3073
3107
|
if (mode === 'dataurl') ;
|
|
@@ -3253,6 +3287,7 @@ class Observer {
|
|
|
3253
3287
|
}
|
|
3254
3288
|
if (value === null) {
|
|
3255
3289
|
this.app.send(RemoveNodeAttribute(id, name));
|
|
3290
|
+
return;
|
|
3256
3291
|
}
|
|
3257
3292
|
if (isUseElement(node) && name === 'href' && !this.disableSprites) {
|
|
3258
3293
|
parseUseEl(node, 'svgtext', this.domParser)
|
|
@@ -4276,7 +4311,7 @@ class App {
|
|
|
4276
4311
|
this.stopCallbacks = [];
|
|
4277
4312
|
this.commitCallbacks = [];
|
|
4278
4313
|
this.activityState = ActivityState.NotActive;
|
|
4279
|
-
this.version = '18.0.
|
|
4314
|
+
this.version = '18.1.0-beta.0'; // TODO: version compatability check inside each plugin.
|
|
4280
4315
|
this.socketMode = false;
|
|
4281
4316
|
this.compressionThreshold = 24 * 1000;
|
|
4282
4317
|
this.bc = null;
|
|
@@ -4288,6 +4323,7 @@ class App {
|
|
|
4288
4323
|
this.frameOderNumber = 0;
|
|
4289
4324
|
this.frameLevel = 0;
|
|
4290
4325
|
this.emptyBatchCounter = 0;
|
|
4326
|
+
this.lastHiddenAt = null;
|
|
4291
4327
|
/** used by child iframes for crossdomain only */
|
|
4292
4328
|
this.parentActive = false;
|
|
4293
4329
|
this.checkStatus = () => {
|
|
@@ -4732,12 +4768,18 @@ class App {
|
|
|
4732
4768
|
}
|
|
4733
4769
|
};
|
|
4734
4770
|
this.startTimeout = null;
|
|
4735
|
-
this.restart = () => {
|
|
4771
|
+
this.restart = (forceNew = false) => {
|
|
4736
4772
|
this.stop(false);
|
|
4737
4773
|
this.waitStatus(ActivityState.NotActive).then(() => {
|
|
4738
4774
|
this.allowAppStart();
|
|
4739
|
-
|
|
4775
|
+
// Force a brand-new session when asked (e.g. a stale bfcache restore) without
|
|
4776
|
+
// letting forceNew leak into later restarts through prevOpts.
|
|
4777
|
+
const startOpts = forceNew ? { ...this.prevOpts, forceNew: true } : this.prevOpts;
|
|
4778
|
+
this.start(startOpts, true)
|
|
4740
4779
|
.then((r) => {
|
|
4780
|
+
if (forceNew) {
|
|
4781
|
+
this.prevOpts = { ...this.prevOpts, forceNew: false };
|
|
4782
|
+
}
|
|
4741
4783
|
this.debug.info('Session restart', r);
|
|
4742
4784
|
})
|
|
4743
4785
|
.catch((e) => {
|
|
@@ -4745,6 +4787,34 @@ class App {
|
|
|
4745
4787
|
});
|
|
4746
4788
|
});
|
|
4747
4789
|
};
|
|
4790
|
+
this.handlePageShow = (e) => {
|
|
4791
|
+
// Only back-forward cache restores set persisted === true; a normal navigation is false.
|
|
4792
|
+
if (!e || !e.persisted) {
|
|
4793
|
+
return;
|
|
4794
|
+
}
|
|
4795
|
+
// Don't resurrect a tracker that was intentionally stopped.
|
|
4796
|
+
if (this.activityState === ActivityState.NotActive) {
|
|
4797
|
+
return;
|
|
4798
|
+
}
|
|
4799
|
+
try {
|
|
4800
|
+
const frozenMs = this.lastHiddenAt != null ? Date.now() - this.lastHiddenAt : null;
|
|
4801
|
+
this.lastHiddenAt = null;
|
|
4802
|
+
const threshold = this.options.bfcacheRestartThreshold ?? 30 * 60 * 1000;
|
|
4803
|
+
// If we couldn't measure the freeze, or it outlasted the threshold, the session has
|
|
4804
|
+
// likely expired server-side - start a fresh one (forceNew). Otherwise let the
|
|
4805
|
+
// resumed page continue on the existing session (preserving continuity).
|
|
4806
|
+
if (frozenMs === null || frozenMs >= threshold) {
|
|
4807
|
+
this.debug.info('OpenReplay: restarting session after bfcache restore', { frozenMs });
|
|
4808
|
+
this.restart(true);
|
|
4809
|
+
}
|
|
4810
|
+
else {
|
|
4811
|
+
this.debug.info('OpenReplay: resuming session after bfcache restore', { frozenMs });
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
catch (err) {
|
|
4815
|
+
this._debug('bfcache_restore', err);
|
|
4816
|
+
}
|
|
4817
|
+
};
|
|
4748
4818
|
this.send = (message, urgent = false) => {
|
|
4749
4819
|
if (this.activityState === ActivityState.NotActive) {
|
|
4750
4820
|
return;
|
|
@@ -4872,6 +4942,7 @@ class App {
|
|
|
4872
4942
|
localStorage: null,
|
|
4873
4943
|
sessionStorage: null,
|
|
4874
4944
|
forceSingleTab: false,
|
|
4945
|
+
bfcacheRestartThreshold: 30 * 60 * 1000,
|
|
4875
4946
|
assistSocketHost: '',
|
|
4876
4947
|
captureIFrames: true,
|
|
4877
4948
|
obscureTextEmails: false,
|
|
@@ -5193,7 +5264,16 @@ class App {
|
|
|
5193
5264
|
}
|
|
5194
5265
|
};
|
|
5195
5266
|
this.attachEventListener(document.body, 'mouseleave', alertWorker, false, false);
|
|
5196
|
-
this.attachEventListener(window, 'pagehide',
|
|
5267
|
+
this.attachEventListener(window, 'pagehide', () => {
|
|
5268
|
+
// pagehide is the entry point into the back-forward cache; remember when
|
|
5269
|
+
// it fired so pageshow can measure how long the page stayed frozen.
|
|
5270
|
+
this.lastHiddenAt = Date.now();
|
|
5271
|
+
alertWorker();
|
|
5272
|
+
}, false, false);
|
|
5273
|
+
// Detect restores from the back-forward cache. Registered through
|
|
5274
|
+
// attachEventListener so it's removed on stop (no leak) and a tracker that was
|
|
5275
|
+
// intentionally stopped does not resurrect itself on a back/forward restore.
|
|
5276
|
+
this.attachEventListener(window, 'pageshow', this.handlePageShow, false, false);
|
|
5197
5277
|
// TODO: stop session after inactivity timeout (make configurable)
|
|
5198
5278
|
this.attachEventListener(document, 'visibilitychange', (e) => document.visibilityState === 'hidden' && alertWorker(), false);
|
|
5199
5279
|
}
|
|
@@ -5483,9 +5563,12 @@ class App {
|
|
|
5483
5563
|
this.sessionStorage.removeItem(this.options.session_reset_key);
|
|
5484
5564
|
}
|
|
5485
5565
|
}
|
|
5566
|
+
getSessionVersionHash() {
|
|
5567
|
+
const PROTO_VERSION = '2';
|
|
5568
|
+
return `${PROTO_VERSION}x${this.version}`;
|
|
5569
|
+
}
|
|
5486
5570
|
checkSessionToken(forceNew) {
|
|
5487
|
-
const
|
|
5488
|
-
const versionHash = `${PROTO_VERSION}x${this.version}`;
|
|
5571
|
+
const versionHash = this.getSessionVersionHash();
|
|
5489
5572
|
const needReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
|
|
5490
5573
|
let needNewSessionID = forceNew || needReset;
|
|
5491
5574
|
const sessionToken = this.session.getSessionToken(this.projectKey);
|
|
@@ -5816,6 +5899,7 @@ class App {
|
|
|
5816
5899
|
}
|
|
5817
5900
|
this.delay = delay;
|
|
5818
5901
|
this.session.setSessionToken(token, this.projectKey);
|
|
5902
|
+
this.sessionStorage.setItem(`${this.options.session_token_key}_version`, this.getSessionVersionHash());
|
|
5819
5903
|
if (sessionToken && sessionToken !== token) {
|
|
5820
5904
|
this.bc?.postMessage({
|
|
5821
5905
|
type: proto.reset,
|
|
@@ -6155,6 +6239,9 @@ function Console (app, opts) {
|
|
|
6155
6239
|
};
|
|
6156
6240
|
app.attachStartCallback(reset);
|
|
6157
6241
|
app.ticker.attach(reset, 33, false);
|
|
6242
|
+
// Restorers for every method we patch, so we can put the host's console back
|
|
6243
|
+
// when the session stops instead of leaving it proxied for the page's lifetime.
|
|
6244
|
+
const restores = [];
|
|
6158
6245
|
const patchConsole = (console, ctx) => {
|
|
6159
6246
|
const handler = {
|
|
6160
6247
|
apply: function (target, thisArg, argumentsList) {
|
|
@@ -6175,10 +6262,30 @@ function Console (app, opts) {
|
|
|
6175
6262
|
}
|
|
6176
6263
|
const fn = ctx.console[method];
|
|
6177
6264
|
console[method] = new Proxy(fn, handler);
|
|
6265
|
+
restores.push(() => {
|
|
6266
|
+
console[method] = fn;
|
|
6267
|
+
});
|
|
6178
6268
|
});
|
|
6179
6269
|
};
|
|
6180
6270
|
const patchContext = app.safe((context) => patchConsole(context.console, context));
|
|
6181
|
-
|
|
6271
|
+
let patched = false;
|
|
6272
|
+
const startPatching = () => {
|
|
6273
|
+
if (patched) {
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
6276
|
+
patched = true;
|
|
6277
|
+
patchContext(window);
|
|
6278
|
+
};
|
|
6279
|
+
// Patch immediately, and re-patch on any later start; restore on stop so the
|
|
6280
|
+
// host console isn't left wrapped for the page's lifetime and capture is
|
|
6281
|
+
// re-established symmetrically across a stop/restart.
|
|
6282
|
+
startPatching();
|
|
6283
|
+
app.attachStartCallback(startPatching);
|
|
6284
|
+
app.attachStopCallback(() => {
|
|
6285
|
+
patched = false;
|
|
6286
|
+
restores.forEach((restore) => restore());
|
|
6287
|
+
restores.length = 0;
|
|
6288
|
+
});
|
|
6182
6289
|
app.observer.attachContextCallback(patchContext);
|
|
6183
6290
|
}
|
|
6184
6291
|
|
|
@@ -6499,7 +6606,7 @@ function Img (app) {
|
|
|
6499
6606
|
const target = mutation.target;
|
|
6500
6607
|
const id = app.nodes.getID(target);
|
|
6501
6608
|
if (id === undefined) {
|
|
6502
|
-
|
|
6609
|
+
continue;
|
|
6503
6610
|
}
|
|
6504
6611
|
if (mutation.attributeName === 'src') {
|
|
6505
6612
|
sendSrc(id, target);
|
|
@@ -7439,15 +7546,12 @@ function CSSRules (app, opts) {
|
|
|
7439
7546
|
if (!sheetID)
|
|
7440
7547
|
continue;
|
|
7441
7548
|
if (options.checkLimit) {
|
|
7442
|
-
|
|
7443
|
-
checkIterations[sheetID] = 0;
|
|
7444
|
-
}
|
|
7445
|
-
else {
|
|
7446
|
-
checkIterations[sheetID]++;
|
|
7447
|
-
}
|
|
7549
|
+
checkIterations[sheetID] = (checkIterations[sheetID] ?? 0) + 1;
|
|
7448
7550
|
if (checkIterations[sheetID] > options.checkLimit) {
|
|
7449
7551
|
trackedSheets.delete(sheet);
|
|
7450
|
-
return
|
|
7552
|
+
// continue, not return: skipping one exhausted sheet must not
|
|
7553
|
+
// abandon the remaining tracked sheets in this scan.
|
|
7554
|
+
continue;
|
|
7451
7555
|
}
|
|
7452
7556
|
}
|
|
7453
7557
|
for (let j = 0; j < sheet.cssRules.length; j++) {
|
|
@@ -9243,7 +9347,7 @@ function webAnimations(app, options = {}) {
|
|
|
9243
9347
|
/**
|
|
9244
9348
|
* Detects client browser, OS, and device information
|
|
9245
9349
|
*/
|
|
9246
|
-
function uaParse(sWindow) {
|
|
9350
|
+
function uaParse(sWindow, onOsVersionUpdate) {
|
|
9247
9351
|
const unknown = '-';
|
|
9248
9352
|
// Screen detection
|
|
9249
9353
|
let width = 0;
|
|
@@ -9402,7 +9506,11 @@ function uaParse(sWindow) {
|
|
|
9402
9506
|
.getHighEntropyValues(['platformVersion'])
|
|
9403
9507
|
.then((ua) => {
|
|
9404
9508
|
const version = parseInt(ua.platformVersion.split('.')[0], 10);
|
|
9405
|
-
|
|
9509
|
+
// uaParse has already returned by the time this resolves, so a local
|
|
9510
|
+
// assignment would be lost - notify the caller to update its state.
|
|
9511
|
+
const refined = version < 13 ? '10' : '11';
|
|
9512
|
+
osVersion = refined;
|
|
9513
|
+
onOsVersionUpdate?.(refined);
|
|
9406
9514
|
})
|
|
9407
9515
|
.catch(() => {
|
|
9408
9516
|
// ignore errors and keep osVersion as is
|
|
@@ -9578,7 +9686,12 @@ class ConstantProperties {
|
|
|
9578
9686
|
this.clearSuperProperties = () => {
|
|
9579
9687
|
this.localStorage.setItem(superPropKey, JSON.stringify({}));
|
|
9580
9688
|
};
|
|
9581
|
-
const { width, height, browser, browserVersion, browserMajorVersion, os, osVersion, mobile } = uaParse(win)
|
|
9689
|
+
const { width, height, browser, browserVersion, browserMajorVersion, os, osVersion, mobile } = uaParse(win, (refinedOsVersion) => {
|
|
9690
|
+
// Windows 10/11 are indistinguishable from the UA string; the async
|
|
9691
|
+
// high-entropy result lands after construction, so update it live here.
|
|
9692
|
+
// `all` is a getter, so later events pick up the corrected value.
|
|
9693
|
+
this.osVersion = refinedOsVersion;
|
|
9694
|
+
});
|
|
9582
9695
|
const storedUserId = this.sessionStorage.getItem(userIdKey);
|
|
9583
9696
|
if (storedUserId) {
|
|
9584
9697
|
this.user_id = storedUserId;
|
|
@@ -9611,7 +9724,7 @@ class ConstantProperties {
|
|
|
9611
9724
|
user_id: this.user_id,
|
|
9612
9725
|
distinct_id: this.deviceId,
|
|
9613
9726
|
sdk_edition: 'web',
|
|
9614
|
-
sdk_version: '18.0.
|
|
9727
|
+
sdk_version: '18.1.0-beta.0',
|
|
9615
9728
|
timezone: getUTCOffsetString(),
|
|
9616
9729
|
search_engine: this.searchEngine,
|
|
9617
9730
|
};
|
|
@@ -9961,7 +10074,15 @@ class Batcher {
|
|
|
9961
10074
|
this.stopped = false;
|
|
9962
10075
|
this.paused = false;
|
|
9963
10076
|
this.onVisibilityChange = () => {
|
|
9964
|
-
|
|
10077
|
+
if (document.hidden) {
|
|
10078
|
+
// The tab may be closing; flush now with keepalive so buffered events
|
|
10079
|
+
// survive the transition instead of waiting for the (paused) interval.
|
|
10080
|
+
this.flush(true);
|
|
10081
|
+
this.paused = true;
|
|
10082
|
+
}
|
|
10083
|
+
else {
|
|
10084
|
+
this.paused = false;
|
|
10085
|
+
}
|
|
9965
10086
|
};
|
|
9966
10087
|
}
|
|
9967
10088
|
getBatches() {
|
|
@@ -10046,8 +10167,9 @@ class Batcher {
|
|
|
10046
10167
|
}
|
|
10047
10168
|
return Array.from(uniqueEventsByType.values());
|
|
10048
10169
|
}
|
|
10049
|
-
sendBatch(batch) {
|
|
10170
|
+
sendBatch(batch, keepalive = false, onTerminalFailure) {
|
|
10050
10171
|
if (this.stopped) {
|
|
10172
|
+
onTerminalFailure?.();
|
|
10051
10173
|
return;
|
|
10052
10174
|
}
|
|
10053
10175
|
const sentBatch = batch;
|
|
@@ -10055,11 +10177,14 @@ class Batcher {
|
|
|
10055
10177
|
const send = () => {
|
|
10056
10178
|
const token = this.getToken();
|
|
10057
10179
|
if (!token) {
|
|
10180
|
+
// No token yet: keep the events so they go out once one is available.
|
|
10181
|
+
onTerminalFailure?.();
|
|
10058
10182
|
return;
|
|
10059
10183
|
}
|
|
10060
10184
|
attempts++;
|
|
10061
10185
|
return fetch(`${this.backendUrl}${this.apiEdp}`, {
|
|
10062
10186
|
method: 'POST',
|
|
10187
|
+
keepalive,
|
|
10063
10188
|
headers: {
|
|
10064
10189
|
'Content-Type': 'application/json',
|
|
10065
10190
|
Authorization: `Bearer ${token}`,
|
|
@@ -10078,6 +10203,7 @@ class Batcher {
|
|
|
10078
10203
|
send();
|
|
10079
10204
|
});
|
|
10080
10205
|
}
|
|
10206
|
+
onTerminalFailure?.();
|
|
10081
10207
|
return;
|
|
10082
10208
|
}
|
|
10083
10209
|
if (response.status === 403) {
|
|
@@ -10086,6 +10212,7 @@ class Batcher {
|
|
|
10086
10212
|
send();
|
|
10087
10213
|
});
|
|
10088
10214
|
}
|
|
10215
|
+
onTerminalFailure?.();
|
|
10089
10216
|
return;
|
|
10090
10217
|
}
|
|
10091
10218
|
if (!response.ok) {
|
|
@@ -10096,6 +10223,9 @@ class Batcher {
|
|
|
10096
10223
|
if (attempts < this.retryLimit) {
|
|
10097
10224
|
setTimeout(() => void send(), this.retryTimeout);
|
|
10098
10225
|
}
|
|
10226
|
+
else {
|
|
10227
|
+
onTerminalFailure?.();
|
|
10228
|
+
}
|
|
10099
10229
|
});
|
|
10100
10230
|
};
|
|
10101
10231
|
void send();
|
|
@@ -10112,16 +10242,38 @@ class Batcher {
|
|
|
10112
10242
|
}
|
|
10113
10243
|
}, this.autosendInterval);
|
|
10114
10244
|
}
|
|
10115
|
-
flush() {
|
|
10116
|
-
const
|
|
10117
|
-
const isEmpty =
|
|
10245
|
+
flush(keepalive = false) {
|
|
10246
|
+
const cats = Object.keys(this.batch);
|
|
10247
|
+
const isEmpty = cats.every((category) => this.batch[category].length === 0);
|
|
10118
10248
|
if (isEmpty) {
|
|
10119
10249
|
return;
|
|
10120
10250
|
}
|
|
10121
|
-
this.
|
|
10122
|
-
|
|
10251
|
+
const batches = this.getBatches();
|
|
10252
|
+
// Snapshot what we're about to send so we can restore it if the request
|
|
10253
|
+
// ultimately fails, instead of dropping events the moment we clear the buffer.
|
|
10254
|
+
const snapshot = {
|
|
10255
|
+
[categories.people]: [...batches.data[categories.people]],
|
|
10256
|
+
[categories.events]: [...batches.data[categories.events]],
|
|
10257
|
+
};
|
|
10258
|
+
cats.forEach((key) => {
|
|
10123
10259
|
this.batch[key] = [];
|
|
10124
10260
|
});
|
|
10261
|
+
this.sendBatch(batches, keepalive, () => this.requeue(snapshot));
|
|
10262
|
+
}
|
|
10263
|
+
requeue(snapshot) {
|
|
10264
|
+
if (this.stopped) {
|
|
10265
|
+
return;
|
|
10266
|
+
}
|
|
10267
|
+
// Put the failed events back at the front, ahead of anything queued since,
|
|
10268
|
+
// so they're retried on the next flush rather than lost.
|
|
10269
|
+
this.batch[categories.people] = [
|
|
10270
|
+
...snapshot[categories.people],
|
|
10271
|
+
...this.batch[categories.people],
|
|
10272
|
+
];
|
|
10273
|
+
this.batch[categories.events] = [
|
|
10274
|
+
...snapshot[categories.events],
|
|
10275
|
+
...this.batch[categories.events],
|
|
10276
|
+
];
|
|
10125
10277
|
}
|
|
10126
10278
|
stop() {
|
|
10127
10279
|
this.flush();
|
|
@@ -10313,7 +10465,7 @@ class API {
|
|
|
10313
10465
|
this.signalStartIssue = (reason, missingApi) => {
|
|
10314
10466
|
const doNotTrack = this.checkDoNotTrack();
|
|
10315
10467
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
10316
|
-
trackerVersion: '18.0.
|
|
10468
|
+
trackerVersion: '18.1.0-beta.0',
|
|
10317
10469
|
projectKey: this.options.projectKey,
|
|
10318
10470
|
doNotTrack,
|
|
10319
10471
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|