@openreplay/tracker 18.0.17-beta.0 → 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/lib/index.js
CHANGED
|
@@ -74,7 +74,7 @@ class ConditionsManager {
|
|
|
74
74
|
return resultCondition;
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
|
-
this.
|
|
77
|
+
this.durationInts = [];
|
|
78
78
|
}
|
|
79
79
|
setConditions(conditions) {
|
|
80
80
|
this.conditions = conditions;
|
|
@@ -167,18 +167,22 @@ class ConditionsManager {
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
clearDurationInt(int) {
|
|
171
|
+
clearInterval(int);
|
|
172
|
+
this.durationInts = this.durationInts.filter((i) => i !== int);
|
|
173
|
+
}
|
|
170
174
|
processDuration(durationMs, condName) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
// Track each interval independently: with more than one duration condition a
|
|
176
|
+
// single shared field would leak every interval but the last, and the timer
|
|
177
|
+
// would keep firing after it triggered.
|
|
178
|
+
const int = setInterval(() => {
|
|
179
|
+
if (performance.now() > durationMs) {
|
|
180
|
+
this.clearDurationInt(int);
|
|
174
181
|
this.trigger(condName);
|
|
175
182
|
}
|
|
176
183
|
}, 1000);
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
clearInterval(this.durationInt);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
184
|
+
this.durationInts.push(int);
|
|
185
|
+
this.app.attachStopCallback(() => this.clearDurationInt(int));
|
|
182
186
|
}
|
|
183
187
|
networkRequest(message) {
|
|
184
188
|
// method - 2, url - 3, status - 6, duration - 8
|
|
@@ -697,13 +701,18 @@ function throttleWithTrailing(fn, interval) {
|
|
|
697
701
|
timeouts.delete(key);
|
|
698
702
|
}
|
|
699
703
|
lastCalls.set(key, now);
|
|
704
|
+
// args are consumed now; don't retain them keyed by every id we ever saw
|
|
705
|
+
lastArgs.delete(key);
|
|
700
706
|
fn(key, ...args);
|
|
701
707
|
}
|
|
702
708
|
else if (!timeouts.has(key)) {
|
|
703
709
|
const timeoutId = setTimeout(() => {
|
|
704
|
-
lastCalls.set(key, Date.now());
|
|
705
710
|
timeouts.delete(key);
|
|
706
711
|
const finalArgs = lastArgs.get(key);
|
|
712
|
+
// Trailing call closes the throttle window for this key: drop its
|
|
713
|
+
// bookkeeping so the maps don't grow one permanent entry per key.
|
|
714
|
+
lastArgs.delete(key);
|
|
715
|
+
lastCalls.delete(key);
|
|
707
716
|
fn(key, ...finalArgs);
|
|
708
717
|
}, remaining);
|
|
709
718
|
timeouts.set(key, timeoutId);
|
|
@@ -2950,10 +2959,18 @@ class Sanitizer {
|
|
|
2950
2959
|
data = data.replace(/\d/g, '0');
|
|
2951
2960
|
}
|
|
2952
2961
|
if (this.options.obscureTextEmails) {
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2962
|
+
// Match every email-like substring anywhere in the text (not just when the
|
|
2963
|
+
// whole node is a single email), and preserve every domain label including
|
|
2964
|
+
// multi-part TLDs (e.g. .co.uk) so nothing leaks through unmasked.
|
|
2965
|
+
data = data.replace(/[^\s@]+@[^\s@]+\.[^\s@]+/g, (email) => {
|
|
2966
|
+
const atIdx = email.lastIndexOf('@');
|
|
2967
|
+
const name = email.slice(0, atIdx);
|
|
2968
|
+
const domain = email.slice(atIdx + 1);
|
|
2969
|
+
const maskedDomain = domain
|
|
2970
|
+
.split('.')
|
|
2971
|
+
.map((part) => stars(part))
|
|
2972
|
+
.join('.');
|
|
2973
|
+
return `${stars(name)}@${maskedDomain}`;
|
|
2957
2974
|
});
|
|
2958
2975
|
}
|
|
2959
2976
|
return data;
|
|
@@ -2990,8 +3007,14 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
2990
3007
|
console.warn('Openreplay: Invalid xlink:href or href found on <use>.');
|
|
2991
3008
|
return;
|
|
2992
3009
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
3010
|
+
// Key the cache by url + symbol + mode: the same symbol id lives in different
|
|
3011
|
+
// sprite files, and different modes store different value shapes (object vs
|
|
3012
|
+
// string vs data-url), so keying by symbolId alone returns the wrong asset.
|
|
3013
|
+
// Namespace remote vs in-document sprites so a relative sprite file literally
|
|
3014
|
+
// named "local" can't collide with the in-document bucket.
|
|
3015
|
+
const cacheKey = url ? `url:${url}#${symbolId}#${mode}` : `doc:${symbolId}#${mode}`;
|
|
3016
|
+
if (iconCache[cacheKey]) {
|
|
3017
|
+
return iconCache[cacheKey];
|
|
2995
3018
|
}
|
|
2996
3019
|
// happens if svg spritemap is local, fastest case for us
|
|
2997
3020
|
if (!url && symbolId) {
|
|
@@ -3010,7 +3033,7 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
3010
3033
|
${symbol.innerHTML}
|
|
3011
3034
|
</svg>
|
|
3012
3035
|
`;
|
|
3013
|
-
iconCache[
|
|
3036
|
+
iconCache[cacheKey] = inlineSvg;
|
|
3014
3037
|
return inlineSvg;
|
|
3015
3038
|
}
|
|
3016
3039
|
else {
|
|
@@ -3045,10 +3068,21 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
3045
3068
|
}
|
|
3046
3069
|
else {
|
|
3047
3070
|
svgUrlCache[url] = 1;
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3071
|
+
try {
|
|
3072
|
+
const response = await fetch(url);
|
|
3073
|
+
const svgText = await response.text();
|
|
3074
|
+
svgDoc = domParser.parseFromString(svgText, 'image/svg+xml');
|
|
3075
|
+
svgUrlCache[url] = svgDoc;
|
|
3076
|
+
}
|
|
3077
|
+
catch (e) {
|
|
3078
|
+
// Clear the in-flight sentinel so a failed fetch doesn't wedge every
|
|
3079
|
+
// future <use> for this url into a 10s poll-then-give-up loop.
|
|
3080
|
+
delete svgUrlCache[url];
|
|
3081
|
+
throw e;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
if (!svgDoc) {
|
|
3085
|
+
return;
|
|
3052
3086
|
}
|
|
3053
3087
|
// @ts-ignore
|
|
3054
3088
|
const symbol = svgDoc.getElementById(symbolId);
|
|
@@ -3063,7 +3097,7 @@ async function parseUseEl(useElement, mode, domParser) {
|
|
|
3063
3097
|
${symbol.innerHTML}
|
|
3064
3098
|
</svg>
|
|
3065
3099
|
`;
|
|
3066
|
-
iconCache[
|
|
3100
|
+
iconCache[cacheKey] = inlineSvg;
|
|
3067
3101
|
return inlineSvg;
|
|
3068
3102
|
}
|
|
3069
3103
|
if (mode === 'dataurl') ;
|
|
@@ -3249,6 +3283,7 @@ class Observer {
|
|
|
3249
3283
|
}
|
|
3250
3284
|
if (value === null) {
|
|
3251
3285
|
this.app.send(RemoveNodeAttribute(id, name));
|
|
3286
|
+
return;
|
|
3252
3287
|
}
|
|
3253
3288
|
if (isUseElement(node) && name === 'href' && !this.disableSprites) {
|
|
3254
3289
|
parseUseEl(node, 'svgtext', this.domParser)
|
|
@@ -4272,7 +4307,7 @@ class App {
|
|
|
4272
4307
|
this.stopCallbacks = [];
|
|
4273
4308
|
this.commitCallbacks = [];
|
|
4274
4309
|
this.activityState = ActivityState.NotActive;
|
|
4275
|
-
this.version = '18.0
|
|
4310
|
+
this.version = '18.1.0-beta.0'; // TODO: version compatability check inside each plugin.
|
|
4276
4311
|
this.socketMode = false;
|
|
4277
4312
|
this.compressionThreshold = 24 * 1000;
|
|
4278
4313
|
this.bc = null;
|
|
@@ -4284,6 +4319,7 @@ class App {
|
|
|
4284
4319
|
this.frameOderNumber = 0;
|
|
4285
4320
|
this.frameLevel = 0;
|
|
4286
4321
|
this.emptyBatchCounter = 0;
|
|
4322
|
+
this.lastHiddenAt = null;
|
|
4287
4323
|
/** used by child iframes for crossdomain only */
|
|
4288
4324
|
this.parentActive = false;
|
|
4289
4325
|
this.checkStatus = () => {
|
|
@@ -4728,12 +4764,18 @@ class App {
|
|
|
4728
4764
|
}
|
|
4729
4765
|
};
|
|
4730
4766
|
this.startTimeout = null;
|
|
4731
|
-
this.restart = () => {
|
|
4767
|
+
this.restart = (forceNew = false) => {
|
|
4732
4768
|
this.stop(false);
|
|
4733
4769
|
this.waitStatus(ActivityState.NotActive).then(() => {
|
|
4734
4770
|
this.allowAppStart();
|
|
4735
|
-
|
|
4771
|
+
// Force a brand-new session when asked (e.g. a stale bfcache restore) without
|
|
4772
|
+
// letting forceNew leak into later restarts through prevOpts.
|
|
4773
|
+
const startOpts = forceNew ? { ...this.prevOpts, forceNew: true } : this.prevOpts;
|
|
4774
|
+
this.start(startOpts, true)
|
|
4736
4775
|
.then((r) => {
|
|
4776
|
+
if (forceNew) {
|
|
4777
|
+
this.prevOpts = { ...this.prevOpts, forceNew: false };
|
|
4778
|
+
}
|
|
4737
4779
|
this.debug.info('Session restart', r);
|
|
4738
4780
|
})
|
|
4739
4781
|
.catch((e) => {
|
|
@@ -4741,6 +4783,34 @@ class App {
|
|
|
4741
4783
|
});
|
|
4742
4784
|
});
|
|
4743
4785
|
};
|
|
4786
|
+
this.handlePageShow = (e) => {
|
|
4787
|
+
// Only back-forward cache restores set persisted === true; a normal navigation is false.
|
|
4788
|
+
if (!e || !e.persisted) {
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
// Don't resurrect a tracker that was intentionally stopped.
|
|
4792
|
+
if (this.activityState === ActivityState.NotActive) {
|
|
4793
|
+
return;
|
|
4794
|
+
}
|
|
4795
|
+
try {
|
|
4796
|
+
const frozenMs = this.lastHiddenAt != null ? Date.now() - this.lastHiddenAt : null;
|
|
4797
|
+
this.lastHiddenAt = null;
|
|
4798
|
+
const threshold = this.options.bfcacheRestartThreshold ?? 30 * 60 * 1000;
|
|
4799
|
+
// If we couldn't measure the freeze, or it outlasted the threshold, the session has
|
|
4800
|
+
// likely expired server-side - start a fresh one (forceNew). Otherwise let the
|
|
4801
|
+
// resumed page continue on the existing session (preserving continuity).
|
|
4802
|
+
if (frozenMs === null || frozenMs >= threshold) {
|
|
4803
|
+
this.debug.info('OpenReplay: restarting session after bfcache restore', { frozenMs });
|
|
4804
|
+
this.restart(true);
|
|
4805
|
+
}
|
|
4806
|
+
else {
|
|
4807
|
+
this.debug.info('OpenReplay: resuming session after bfcache restore', { frozenMs });
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
catch (err) {
|
|
4811
|
+
this._debug('bfcache_restore', err);
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4744
4814
|
this.send = (message, urgent = false) => {
|
|
4745
4815
|
if (this.activityState === ActivityState.NotActive) {
|
|
4746
4816
|
return;
|
|
@@ -4868,6 +4938,7 @@ class App {
|
|
|
4868
4938
|
localStorage: null,
|
|
4869
4939
|
sessionStorage: null,
|
|
4870
4940
|
forceSingleTab: false,
|
|
4941
|
+
bfcacheRestartThreshold: 30 * 60 * 1000,
|
|
4871
4942
|
assistSocketHost: '',
|
|
4872
4943
|
captureIFrames: true,
|
|
4873
4944
|
obscureTextEmails: false,
|
|
@@ -5189,7 +5260,16 @@ class App {
|
|
|
5189
5260
|
}
|
|
5190
5261
|
};
|
|
5191
5262
|
this.attachEventListener(document.body, 'mouseleave', alertWorker, false, false);
|
|
5192
|
-
this.attachEventListener(window, 'pagehide',
|
|
5263
|
+
this.attachEventListener(window, 'pagehide', () => {
|
|
5264
|
+
// pagehide is the entry point into the back-forward cache; remember when
|
|
5265
|
+
// it fired so pageshow can measure how long the page stayed frozen.
|
|
5266
|
+
this.lastHiddenAt = Date.now();
|
|
5267
|
+
alertWorker();
|
|
5268
|
+
}, false, false);
|
|
5269
|
+
// Detect restores from the back-forward cache. Registered through
|
|
5270
|
+
// attachEventListener so it's removed on stop (no leak) and a tracker that was
|
|
5271
|
+
// intentionally stopped does not resurrect itself on a back/forward restore.
|
|
5272
|
+
this.attachEventListener(window, 'pageshow', this.handlePageShow, false, false);
|
|
5193
5273
|
// TODO: stop session after inactivity timeout (make configurable)
|
|
5194
5274
|
this.attachEventListener(document, 'visibilitychange', (e) => document.visibilityState === 'hidden' && alertWorker(), false);
|
|
5195
5275
|
}
|
|
@@ -5479,9 +5559,12 @@ class App {
|
|
|
5479
5559
|
this.sessionStorage.removeItem(this.options.session_reset_key);
|
|
5480
5560
|
}
|
|
5481
5561
|
}
|
|
5562
|
+
getSessionVersionHash() {
|
|
5563
|
+
const PROTO_VERSION = '2';
|
|
5564
|
+
return `${PROTO_VERSION}x${this.version}`;
|
|
5565
|
+
}
|
|
5482
5566
|
checkSessionToken(forceNew) {
|
|
5483
|
-
const
|
|
5484
|
-
const versionHash = `${PROTO_VERSION}x${this.version}`;
|
|
5567
|
+
const versionHash = this.getSessionVersionHash();
|
|
5485
5568
|
const needReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
|
|
5486
5569
|
let needNewSessionID = forceNew || needReset;
|
|
5487
5570
|
const sessionToken = this.session.getSessionToken(this.projectKey);
|
|
@@ -5812,6 +5895,7 @@ class App {
|
|
|
5812
5895
|
}
|
|
5813
5896
|
this.delay = delay;
|
|
5814
5897
|
this.session.setSessionToken(token, this.projectKey);
|
|
5898
|
+
this.sessionStorage.setItem(`${this.options.session_token_key}_version`, this.getSessionVersionHash());
|
|
5815
5899
|
if (sessionToken && sessionToken !== token) {
|
|
5816
5900
|
this.bc?.postMessage({
|
|
5817
5901
|
type: proto.reset,
|
|
@@ -6151,6 +6235,9 @@ function Console (app, opts) {
|
|
|
6151
6235
|
};
|
|
6152
6236
|
app.attachStartCallback(reset);
|
|
6153
6237
|
app.ticker.attach(reset, 33, false);
|
|
6238
|
+
// Restorers for every method we patch, so we can put the host's console back
|
|
6239
|
+
// when the session stops instead of leaving it proxied for the page's lifetime.
|
|
6240
|
+
const restores = [];
|
|
6154
6241
|
const patchConsole = (console, ctx) => {
|
|
6155
6242
|
const handler = {
|
|
6156
6243
|
apply: function (target, thisArg, argumentsList) {
|
|
@@ -6171,10 +6258,30 @@ function Console (app, opts) {
|
|
|
6171
6258
|
}
|
|
6172
6259
|
const fn = ctx.console[method];
|
|
6173
6260
|
console[method] = new Proxy(fn, handler);
|
|
6261
|
+
restores.push(() => {
|
|
6262
|
+
console[method] = fn;
|
|
6263
|
+
});
|
|
6174
6264
|
});
|
|
6175
6265
|
};
|
|
6176
6266
|
const patchContext = app.safe((context) => patchConsole(context.console, context));
|
|
6177
|
-
|
|
6267
|
+
let patched = false;
|
|
6268
|
+
const startPatching = () => {
|
|
6269
|
+
if (patched) {
|
|
6270
|
+
return;
|
|
6271
|
+
}
|
|
6272
|
+
patched = true;
|
|
6273
|
+
patchContext(window);
|
|
6274
|
+
};
|
|
6275
|
+
// Patch immediately, and re-patch on any later start; restore on stop so the
|
|
6276
|
+
// host console isn't left wrapped for the page's lifetime and capture is
|
|
6277
|
+
// re-established symmetrically across a stop/restart.
|
|
6278
|
+
startPatching();
|
|
6279
|
+
app.attachStartCallback(startPatching);
|
|
6280
|
+
app.attachStopCallback(() => {
|
|
6281
|
+
patched = false;
|
|
6282
|
+
restores.forEach((restore) => restore());
|
|
6283
|
+
restores.length = 0;
|
|
6284
|
+
});
|
|
6178
6285
|
app.observer.attachContextCallback(patchContext);
|
|
6179
6286
|
}
|
|
6180
6287
|
|
|
@@ -6495,7 +6602,7 @@ function Img (app) {
|
|
|
6495
6602
|
const target = mutation.target;
|
|
6496
6603
|
const id = app.nodes.getID(target);
|
|
6497
6604
|
if (id === undefined) {
|
|
6498
|
-
|
|
6605
|
+
continue;
|
|
6499
6606
|
}
|
|
6500
6607
|
if (mutation.attributeName === 'src') {
|
|
6501
6608
|
sendSrc(id, target);
|
|
@@ -7435,15 +7542,12 @@ function CSSRules (app, opts) {
|
|
|
7435
7542
|
if (!sheetID)
|
|
7436
7543
|
continue;
|
|
7437
7544
|
if (options.checkLimit) {
|
|
7438
|
-
|
|
7439
|
-
checkIterations[sheetID] = 0;
|
|
7440
|
-
}
|
|
7441
|
-
else {
|
|
7442
|
-
checkIterations[sheetID]++;
|
|
7443
|
-
}
|
|
7545
|
+
checkIterations[sheetID] = (checkIterations[sheetID] ?? 0) + 1;
|
|
7444
7546
|
if (checkIterations[sheetID] > options.checkLimit) {
|
|
7445
7547
|
trackedSheets.delete(sheet);
|
|
7446
|
-
return
|
|
7548
|
+
// continue, not return: skipping one exhausted sheet must not
|
|
7549
|
+
// abandon the remaining tracked sheets in this scan.
|
|
7550
|
+
continue;
|
|
7447
7551
|
}
|
|
7448
7552
|
}
|
|
7449
7553
|
for (let j = 0; j < sheet.cssRules.length; j++) {
|
|
@@ -9239,7 +9343,7 @@ function webAnimations(app, options = {}) {
|
|
|
9239
9343
|
/**
|
|
9240
9344
|
* Detects client browser, OS, and device information
|
|
9241
9345
|
*/
|
|
9242
|
-
function uaParse(sWindow) {
|
|
9346
|
+
function uaParse(sWindow, onOsVersionUpdate) {
|
|
9243
9347
|
const unknown = '-';
|
|
9244
9348
|
// Screen detection
|
|
9245
9349
|
let width = 0;
|
|
@@ -9398,7 +9502,11 @@ function uaParse(sWindow) {
|
|
|
9398
9502
|
.getHighEntropyValues(['platformVersion'])
|
|
9399
9503
|
.then((ua) => {
|
|
9400
9504
|
const version = parseInt(ua.platformVersion.split('.')[0], 10);
|
|
9401
|
-
|
|
9505
|
+
// uaParse has already returned by the time this resolves, so a local
|
|
9506
|
+
// assignment would be lost - notify the caller to update its state.
|
|
9507
|
+
const refined = version < 13 ? '10' : '11';
|
|
9508
|
+
osVersion = refined;
|
|
9509
|
+
onOsVersionUpdate?.(refined);
|
|
9402
9510
|
})
|
|
9403
9511
|
.catch(() => {
|
|
9404
9512
|
// ignore errors and keep osVersion as is
|
|
@@ -9574,7 +9682,12 @@ class ConstantProperties {
|
|
|
9574
9682
|
this.clearSuperProperties = () => {
|
|
9575
9683
|
this.localStorage.setItem(superPropKey, JSON.stringify({}));
|
|
9576
9684
|
};
|
|
9577
|
-
const { width, height, browser, browserVersion, browserMajorVersion, os, osVersion, mobile } = uaParse(win)
|
|
9685
|
+
const { width, height, browser, browserVersion, browserMajorVersion, os, osVersion, mobile } = uaParse(win, (refinedOsVersion) => {
|
|
9686
|
+
// Windows 10/11 are indistinguishable from the UA string; the async
|
|
9687
|
+
// high-entropy result lands after construction, so update it live here.
|
|
9688
|
+
// `all` is a getter, so later events pick up the corrected value.
|
|
9689
|
+
this.osVersion = refinedOsVersion;
|
|
9690
|
+
});
|
|
9578
9691
|
const storedUserId = this.sessionStorage.getItem(userIdKey);
|
|
9579
9692
|
if (storedUserId) {
|
|
9580
9693
|
this.user_id = storedUserId;
|
|
@@ -9607,7 +9720,7 @@ class ConstantProperties {
|
|
|
9607
9720
|
user_id: this.user_id,
|
|
9608
9721
|
distinct_id: this.deviceId,
|
|
9609
9722
|
sdk_edition: 'web',
|
|
9610
|
-
sdk_version: '18.0
|
|
9723
|
+
sdk_version: '18.1.0-beta.0',
|
|
9611
9724
|
timezone: getUTCOffsetString(),
|
|
9612
9725
|
search_engine: this.searchEngine,
|
|
9613
9726
|
};
|
|
@@ -9957,7 +10070,15 @@ class Batcher {
|
|
|
9957
10070
|
this.stopped = false;
|
|
9958
10071
|
this.paused = false;
|
|
9959
10072
|
this.onVisibilityChange = () => {
|
|
9960
|
-
|
|
10073
|
+
if (document.hidden) {
|
|
10074
|
+
// The tab may be closing; flush now with keepalive so buffered events
|
|
10075
|
+
// survive the transition instead of waiting for the (paused) interval.
|
|
10076
|
+
this.flush(true);
|
|
10077
|
+
this.paused = true;
|
|
10078
|
+
}
|
|
10079
|
+
else {
|
|
10080
|
+
this.paused = false;
|
|
10081
|
+
}
|
|
9961
10082
|
};
|
|
9962
10083
|
}
|
|
9963
10084
|
getBatches() {
|
|
@@ -10042,8 +10163,9 @@ class Batcher {
|
|
|
10042
10163
|
}
|
|
10043
10164
|
return Array.from(uniqueEventsByType.values());
|
|
10044
10165
|
}
|
|
10045
|
-
sendBatch(batch) {
|
|
10166
|
+
sendBatch(batch, keepalive = false, onTerminalFailure) {
|
|
10046
10167
|
if (this.stopped) {
|
|
10168
|
+
onTerminalFailure?.();
|
|
10047
10169
|
return;
|
|
10048
10170
|
}
|
|
10049
10171
|
const sentBatch = batch;
|
|
@@ -10051,11 +10173,14 @@ class Batcher {
|
|
|
10051
10173
|
const send = () => {
|
|
10052
10174
|
const token = this.getToken();
|
|
10053
10175
|
if (!token) {
|
|
10176
|
+
// No token yet: keep the events so they go out once one is available.
|
|
10177
|
+
onTerminalFailure?.();
|
|
10054
10178
|
return;
|
|
10055
10179
|
}
|
|
10056
10180
|
attempts++;
|
|
10057
10181
|
return fetch(`${this.backendUrl}${this.apiEdp}`, {
|
|
10058
10182
|
method: 'POST',
|
|
10183
|
+
keepalive,
|
|
10059
10184
|
headers: {
|
|
10060
10185
|
'Content-Type': 'application/json',
|
|
10061
10186
|
Authorization: `Bearer ${token}`,
|
|
@@ -10074,6 +10199,7 @@ class Batcher {
|
|
|
10074
10199
|
send();
|
|
10075
10200
|
});
|
|
10076
10201
|
}
|
|
10202
|
+
onTerminalFailure?.();
|
|
10077
10203
|
return;
|
|
10078
10204
|
}
|
|
10079
10205
|
if (response.status === 403) {
|
|
@@ -10082,6 +10208,7 @@ class Batcher {
|
|
|
10082
10208
|
send();
|
|
10083
10209
|
});
|
|
10084
10210
|
}
|
|
10211
|
+
onTerminalFailure?.();
|
|
10085
10212
|
return;
|
|
10086
10213
|
}
|
|
10087
10214
|
if (!response.ok) {
|
|
@@ -10092,6 +10219,9 @@ class Batcher {
|
|
|
10092
10219
|
if (attempts < this.retryLimit) {
|
|
10093
10220
|
setTimeout(() => void send(), this.retryTimeout);
|
|
10094
10221
|
}
|
|
10222
|
+
else {
|
|
10223
|
+
onTerminalFailure?.();
|
|
10224
|
+
}
|
|
10095
10225
|
});
|
|
10096
10226
|
};
|
|
10097
10227
|
void send();
|
|
@@ -10108,16 +10238,38 @@ class Batcher {
|
|
|
10108
10238
|
}
|
|
10109
10239
|
}, this.autosendInterval);
|
|
10110
10240
|
}
|
|
10111
|
-
flush() {
|
|
10112
|
-
const
|
|
10113
|
-
const isEmpty =
|
|
10241
|
+
flush(keepalive = false) {
|
|
10242
|
+
const cats = Object.keys(this.batch);
|
|
10243
|
+
const isEmpty = cats.every((category) => this.batch[category].length === 0);
|
|
10114
10244
|
if (isEmpty) {
|
|
10115
10245
|
return;
|
|
10116
10246
|
}
|
|
10117
|
-
this.
|
|
10118
|
-
|
|
10247
|
+
const batches = this.getBatches();
|
|
10248
|
+
// Snapshot what we're about to send so we can restore it if the request
|
|
10249
|
+
// ultimately fails, instead of dropping events the moment we clear the buffer.
|
|
10250
|
+
const snapshot = {
|
|
10251
|
+
[categories.people]: [...batches.data[categories.people]],
|
|
10252
|
+
[categories.events]: [...batches.data[categories.events]],
|
|
10253
|
+
};
|
|
10254
|
+
cats.forEach((key) => {
|
|
10119
10255
|
this.batch[key] = [];
|
|
10120
10256
|
});
|
|
10257
|
+
this.sendBatch(batches, keepalive, () => this.requeue(snapshot));
|
|
10258
|
+
}
|
|
10259
|
+
requeue(snapshot) {
|
|
10260
|
+
if (this.stopped) {
|
|
10261
|
+
return;
|
|
10262
|
+
}
|
|
10263
|
+
// Put the failed events back at the front, ahead of anything queued since,
|
|
10264
|
+
// so they're retried on the next flush rather than lost.
|
|
10265
|
+
this.batch[categories.people] = [
|
|
10266
|
+
...snapshot[categories.people],
|
|
10267
|
+
...this.batch[categories.people],
|
|
10268
|
+
];
|
|
10269
|
+
this.batch[categories.events] = [
|
|
10270
|
+
...snapshot[categories.events],
|
|
10271
|
+
...this.batch[categories.events],
|
|
10272
|
+
];
|
|
10121
10273
|
}
|
|
10122
10274
|
stop() {
|
|
10123
10275
|
this.flush();
|
|
@@ -10309,7 +10461,7 @@ class API {
|
|
|
10309
10461
|
this.signalStartIssue = (reason, missingApi) => {
|
|
10310
10462
|
const doNotTrack = this.checkDoNotTrack();
|
|
10311
10463
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
10312
|
-
trackerVersion: '18.0
|
|
10464
|
+
trackerVersion: '18.1.0-beta.0',
|
|
10313
10465
|
projectKey: this.options.projectKey,
|
|
10314
10466
|
doNotTrack,
|
|
10315
10467
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|