@leanbase.com/js 0.1.0 → 0.1.2
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 +9 -21
- package/dist/index.cjs +2974 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +760 -11
- package/dist/index.mjs +2975 -61
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +3091 -134
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +1 -2
- package/src/autocapture-utils.ts +550 -0
- package/src/autocapture.ts +415 -0
- package/src/config.ts +8 -0
- package/src/constants.ts +98 -0
- package/src/extensions/rageclick.ts +34 -0
- package/src/iife.ts +31 -27
- package/src/index.ts +1 -1
- package/src/leanbase-logger.ts +7 -4
- package/src/leanbase-persistence.ts +374 -0
- package/src/leanbase.ts +366 -71
- package/src/page-view.ts +124 -0
- package/src/scroll-manager.ts +103 -0
- package/src/session-props.ts +114 -0
- package/src/sessionid.ts +330 -0
- package/src/storage.ts +410 -0
- package/src/types.ts +634 -0
- package/src/utils/blocked-uas.ts +162 -0
- package/src/utils/element-utils.ts +50 -0
- package/src/utils/event-utils.ts +304 -0
- package/src/utils/index.ts +222 -0
- package/src/utils/request-utils.ts +128 -0
- package/src/utils/simple-event-emitter.ts +27 -0
- package/src/utils/user-agent-utils.ts +357 -0
- package/src/uuidv7.ts +268 -0
- package/src/version.ts +1 -1
package/dist/leanbase.iife.js
CHANGED
|
@@ -102,8 +102,8 @@ var leanbase = (function () {
|
|
|
102
102
|
* @license Apache-2.0
|
|
103
103
|
* @copyright 2021-2023 LiosK
|
|
104
104
|
* @packageDocumentation
|
|
105
|
-
*/ const DIGITS = "0123456789abcdef";
|
|
106
|
-
class UUID {
|
|
105
|
+
*/ const DIGITS$1 = "0123456789abcdef";
|
|
106
|
+
let UUID$1 = class UUID {
|
|
107
107
|
constructor(bytes){
|
|
108
108
|
this.bytes = bytes;
|
|
109
109
|
}
|
|
@@ -164,8 +164,8 @@ var leanbase = (function () {
|
|
|
164
164
|
toString() {
|
|
165
165
|
let text = "";
|
|
166
166
|
for(let i = 0; i < this.bytes.length; i++){
|
|
167
|
-
text += DIGITS.charAt(this.bytes[i] >>> 4);
|
|
168
|
-
text += DIGITS.charAt(0xf & this.bytes[i]);
|
|
167
|
+
text += DIGITS$1.charAt(this.bytes[i] >>> 4);
|
|
168
|
+
text += DIGITS$1.charAt(0xf & this.bytes[i]);
|
|
169
169
|
if (3 === i || 5 === i || 7 === i || 9 === i) text += "-";
|
|
170
170
|
}
|
|
171
171
|
return text;
|
|
@@ -173,8 +173,8 @@ var leanbase = (function () {
|
|
|
173
173
|
toHex() {
|
|
174
174
|
let text = "";
|
|
175
175
|
for(let i = 0; i < this.bytes.length; i++){
|
|
176
|
-
text += DIGITS.charAt(this.bytes[i] >>> 4);
|
|
177
|
-
text += DIGITS.charAt(0xf & this.bytes[i]);
|
|
176
|
+
text += DIGITS$1.charAt(this.bytes[i] >>> 4);
|
|
177
|
+
text += DIGITS$1.charAt(0xf & this.bytes[i]);
|
|
178
178
|
}
|
|
179
179
|
return text;
|
|
180
180
|
}
|
|
@@ -206,8 +206,8 @@ var leanbase = (function () {
|
|
|
206
206
|
}
|
|
207
207
|
return 0;
|
|
208
208
|
}
|
|
209
|
-
}
|
|
210
|
-
class V7Generator {
|
|
209
|
+
};
|
|
210
|
+
let V7Generator$1 = class V7Generator {
|
|
211
211
|
constructor(randomNumberGenerator){
|
|
212
212
|
this.timestamp = 0;
|
|
213
213
|
this.counter = 0;
|
|
@@ -242,7 +242,7 @@ var leanbase = (function () {
|
|
|
242
242
|
this.resetCounter();
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & 2 ** 30 - 1, this.random.nextUint32());
|
|
245
|
+
return UUID$1.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & 2 ** 30 - 1, this.random.nextUint32());
|
|
246
246
|
}
|
|
247
247
|
resetCounter() {
|
|
248
248
|
this.counter = 0x400 * this.random.nextUint32() + (0x3ff & this.random.nextUint32());
|
|
@@ -251,15 +251,15 @@ var leanbase = (function () {
|
|
|
251
251
|
const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
|
|
252
252
|
bytes[6] = 0x40 | bytes[6] >>> 4;
|
|
253
253
|
bytes[8] = 0x80 | bytes[8] >>> 2;
|
|
254
|
-
return UUID.ofInner(bytes);
|
|
254
|
+
return UUID$1.ofInner(bytes);
|
|
255
255
|
}
|
|
256
|
-
}
|
|
256
|
+
};
|
|
257
257
|
const getDefaultRandom = ()=>({
|
|
258
258
|
nextUint32: ()=>0x10000 * Math.trunc(0x10000 * Math.random()) + Math.trunc(0x10000 * Math.random())
|
|
259
259
|
});
|
|
260
|
-
let defaultGenerator;
|
|
261
|
-
const uuidv7 = ()=>uuidv7obj().toString();
|
|
262
|
-
const uuidv7obj = ()=>(defaultGenerator || (defaultGenerator = new V7Generator())).generate();
|
|
260
|
+
let defaultGenerator$1;
|
|
261
|
+
const uuidv7$1 = ()=>uuidv7obj$1().toString();
|
|
262
|
+
const uuidv7obj$1 = ()=>(defaultGenerator$1 || (defaultGenerator$1 = new V7Generator$1())).generate();
|
|
263
263
|
|
|
264
264
|
var types_PostHogPersistedProperty = /*#__PURE__*/ function(PostHogPersistedProperty) {
|
|
265
265
|
PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
|
|
@@ -295,19 +295,58 @@ var leanbase = (function () {
|
|
|
295
295
|
return Compression;
|
|
296
296
|
}({});
|
|
297
297
|
|
|
298
|
+
function includes(str, needle) {
|
|
299
|
+
return -1 !== str.indexOf(needle);
|
|
300
|
+
}
|
|
301
|
+
const trim = function(str) {
|
|
302
|
+
return str.trim();
|
|
303
|
+
};
|
|
304
|
+
const stripLeadingDollar = function(s) {
|
|
305
|
+
return s.replace(/^\$/, '');
|
|
306
|
+
};
|
|
307
|
+
|
|
298
308
|
const nativeIsArray = Array.isArray;
|
|
299
309
|
const ObjProto = Object.prototype;
|
|
310
|
+
const type_utils_hasOwnProperty = ObjProto.hasOwnProperty;
|
|
300
311
|
const type_utils_toString = ObjProto.toString;
|
|
301
312
|
const isArray = nativeIsArray || function(obj) {
|
|
302
313
|
return '[object Array]' === type_utils_toString.call(obj);
|
|
303
314
|
};
|
|
304
315
|
const isFunction = (x)=>'function' == typeof x;
|
|
316
|
+
const isObject = (x)=>x === Object(x) && !isArray(x);
|
|
317
|
+
const isEmptyObject = (x)=>{
|
|
318
|
+
if (isObject(x)) {
|
|
319
|
+
for(const key in x)if (type_utils_hasOwnProperty.call(x, key)) return false;
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
};
|
|
324
|
+
const isUndefined = (x)=>void 0 === x;
|
|
325
|
+
const isString = (x)=>'[object String]' == type_utils_toString.call(x);
|
|
326
|
+
const isEmptyString = (x)=>isString(x) && 0 === x.trim().length;
|
|
305
327
|
const isNull = (x)=>null === x;
|
|
328
|
+
const isNullish = (x)=>isUndefined(x) || isNull(x);
|
|
329
|
+
const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
|
|
330
|
+
const isBoolean = (x)=>'[object Boolean]' === type_utils_toString.call(x);
|
|
331
|
+
const isFormData = (x)=>x instanceof FormData;
|
|
306
332
|
const isPlainError = (x)=>x instanceof Error;
|
|
307
333
|
|
|
334
|
+
function clampToRange(value, min, max, logger, fallbackValue) {
|
|
335
|
+
if (isNumber(value)) if (value > max) {
|
|
336
|
+
logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
|
|
337
|
+
return max;
|
|
338
|
+
} else {
|
|
339
|
+
if (!(value < min)) return value;
|
|
340
|
+
logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
|
|
341
|
+
return min;
|
|
342
|
+
}
|
|
343
|
+
logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
|
|
344
|
+
return clampToRange(max, min, max, logger);
|
|
345
|
+
}
|
|
346
|
+
|
|
308
347
|
class PromiseQueue {
|
|
309
348
|
add(promise) {
|
|
310
|
-
const promiseUUID = uuidv7();
|
|
349
|
+
const promiseUUID = uuidv7$1();
|
|
311
350
|
this.promiseByIds[promiseUUID] = promise;
|
|
312
351
|
promise.catch(()=>{}).finally(()=>{
|
|
313
352
|
delete this.promiseByIds[promiseUUID];
|
|
@@ -906,7 +945,7 @@ var leanbase = (function () {
|
|
|
906
945
|
library: this.getLibraryId(),
|
|
907
946
|
library_version: this.getLibraryVersion(),
|
|
908
947
|
timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
|
|
909
|
-
uuid: options?.uuid ? options.uuid : uuidv7()
|
|
948
|
+
uuid: options?.uuid ? options.uuid : uuidv7$1()
|
|
910
949
|
};
|
|
911
950
|
const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
|
|
912
951
|
if (addGeoipDisableProperty) {
|
|
@@ -1169,7 +1208,7 @@ var leanbase = (function () {
|
|
|
1169
1208
|
const sessionLastDif = now - sessionLastTimestamp;
|
|
1170
1209
|
const sessionStartDif = now - sessionStartTimestamp;
|
|
1171
1210
|
if (!sessionId || sessionLastDif > 1000 * this._sessionExpirationTimeSeconds || sessionStartDif > 1000 * this._sessionMaxLengthSeconds) {
|
|
1172
|
-
sessionId = uuidv7();
|
|
1211
|
+
sessionId = uuidv7$1();
|
|
1173
1212
|
this.setPersistedProperty(types_PostHogPersistedProperty.SessionId, sessionId);
|
|
1174
1213
|
this.setPersistedProperty(types_PostHogPersistedProperty.SessionStartTimestamp, now);
|
|
1175
1214
|
}
|
|
@@ -1187,7 +1226,7 @@ var leanbase = (function () {
|
|
|
1187
1226
|
if (!this._isInitialized) return '';
|
|
1188
1227
|
let anonId = this.getPersistedProperty(types_PostHogPersistedProperty.AnonymousId);
|
|
1189
1228
|
if (!anonId) {
|
|
1190
|
-
anonId = uuidv7();
|
|
1229
|
+
anonId = uuidv7$1();
|
|
1191
1230
|
this.setPersistedProperty(types_PostHogPersistedProperty.AnonymousId, anonId);
|
|
1192
1231
|
}
|
|
1193
1232
|
return anonId;
|
|
@@ -1593,173 +1632,3091 @@ var leanbase = (function () {
|
|
|
1593
1632
|
}
|
|
1594
1633
|
}
|
|
1595
1634
|
|
|
1596
|
-
const
|
|
1635
|
+
const breaker = {};
|
|
1636
|
+
const ArrayProto = Array.prototype;
|
|
1637
|
+
const nativeForEach = ArrayProto.forEach;
|
|
1638
|
+
const nativeIndexOf = ArrayProto.indexOf;
|
|
1639
|
+
const win = typeof window !== 'undefined' ? window : undefined;
|
|
1640
|
+
const global = typeof globalThis !== 'undefined' ? globalThis : win;
|
|
1641
|
+
const navigator$1 = global?.navigator;
|
|
1642
|
+
const document = global?.document;
|
|
1643
|
+
const location = global?.location;
|
|
1644
|
+
global?.fetch;
|
|
1645
|
+
global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined;
|
|
1646
|
+
global?.AbortController;
|
|
1647
|
+
const userAgent = navigator$1?.userAgent;
|
|
1648
|
+
function eachArray(obj, iterator, thisArg) {
|
|
1649
|
+
if (isArray(obj)) {
|
|
1650
|
+
if (nativeForEach && obj.forEach === nativeForEach) {
|
|
1651
|
+
obj.forEach(iterator, thisArg);
|
|
1652
|
+
} else if ('length' in obj && obj.length === +obj.length) {
|
|
1653
|
+
for (let i = 0, l = obj.length; i < l; i++) {
|
|
1654
|
+
if (i in obj && iterator.call(thisArg, obj[i], i) === breaker) {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* @param {*=} obj
|
|
1663
|
+
* @param {function(...*)=} iterator
|
|
1664
|
+
* @param {Object=} thisArg
|
|
1665
|
+
*/
|
|
1666
|
+
function each(obj, iterator, thisArg) {
|
|
1667
|
+
if (isNullish(obj)) {
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (isArray(obj)) {
|
|
1671
|
+
return eachArray(obj, iterator, thisArg);
|
|
1672
|
+
}
|
|
1673
|
+
if (isFormData(obj)) {
|
|
1674
|
+
for (const pair of obj.entries()) {
|
|
1675
|
+
if (iterator.call(thisArg, pair[1], pair[0]) === breaker) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
for (const key in obj) {
|
|
1682
|
+
if (type_utils_hasOwnProperty.call(obj, key)) {
|
|
1683
|
+
if (iterator.call(thisArg, obj[key], key) === breaker) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const extend = function (obj, ...args) {
|
|
1690
|
+
eachArray(args, function (source) {
|
|
1691
|
+
for (const prop in source) {
|
|
1692
|
+
if (source[prop] !== void 0) {
|
|
1693
|
+
obj[prop] = source[prop];
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
return obj;
|
|
1698
|
+
};
|
|
1699
|
+
const extendArray = function (obj, ...args) {
|
|
1700
|
+
eachArray(args, function (source) {
|
|
1701
|
+
eachArray(source, function (item) {
|
|
1702
|
+
obj.push(item);
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1705
|
+
return obj;
|
|
1706
|
+
};
|
|
1707
|
+
const include = function (obj, target) {
|
|
1708
|
+
let found = false;
|
|
1709
|
+
if (isNull(obj)) {
|
|
1710
|
+
return found;
|
|
1711
|
+
}
|
|
1712
|
+
if (nativeIndexOf && obj.indexOf === nativeIndexOf) {
|
|
1713
|
+
return obj.indexOf(target) != -1;
|
|
1714
|
+
}
|
|
1715
|
+
each(obj, function (value) {
|
|
1716
|
+
if (found || (found = value === target)) {
|
|
1717
|
+
return breaker;
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
});
|
|
1721
|
+
return found;
|
|
1722
|
+
};
|
|
1723
|
+
/**
|
|
1724
|
+
* Object.entries() polyfill
|
|
1725
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
|
|
1726
|
+
*/
|
|
1727
|
+
function entries(obj) {
|
|
1728
|
+
const ownProps = Object.keys(obj);
|
|
1729
|
+
let i = ownProps.length;
|
|
1730
|
+
const resArray = new Array(i); // preallocate the Array
|
|
1731
|
+
while (i--) {
|
|
1732
|
+
resArray[i] = [ownProps[i], obj[ownProps[i]]];
|
|
1733
|
+
}
|
|
1734
|
+
return resArray;
|
|
1735
|
+
}
|
|
1736
|
+
function addEventListener(element, event, callback, options) {
|
|
1737
|
+
const {
|
|
1738
|
+
capture = false,
|
|
1739
|
+
passive = true
|
|
1740
|
+
} = options ?? {};
|
|
1741
|
+
// This is the only place where we are allowed to call this function
|
|
1742
|
+
// because the whole idea is that we should be calling this instead of the built-in one
|
|
1743
|
+
// eslint-disable-next-line posthog-js/no-add-event-listener
|
|
1744
|
+
element?.addEventListener(event, callback, {
|
|
1745
|
+
capture,
|
|
1746
|
+
passive
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
const stripEmptyProperties = function (p) {
|
|
1750
|
+
const ret = {};
|
|
1751
|
+
each(p, function (v, k) {
|
|
1752
|
+
if (isString(v) && v.length > 0 || isNumber(v)) {
|
|
1753
|
+
ret[k] = v;
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
return ret;
|
|
1757
|
+
};
|
|
1758
|
+
const EXCLUDED_FROM_CROSS_SUBDOMAIN_COOKIE = ['herokuapp.com', 'vercel.app', 'netlify.app'];
|
|
1759
|
+
function isCrossDomainCookie(documentLocation) {
|
|
1760
|
+
const hostname = documentLocation?.hostname;
|
|
1761
|
+
if (!isString(hostname)) {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
// split and slice isn't a great way to match arbitrary domains,
|
|
1765
|
+
// but it's good enough for ensuring we only match herokuapp.com when it is the TLD
|
|
1766
|
+
// for the hostname
|
|
1767
|
+
const lastTwoParts = hostname.split('.').slice(-2).join('.');
|
|
1768
|
+
for (const excluded of EXCLUDED_FROM_CROSS_SUBDOMAIN_COOKIE) {
|
|
1769
|
+
if (lastTwoParts === excluded) {
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Deep copies an object.
|
|
1777
|
+
* It handles cycles by replacing all references to them with `undefined`
|
|
1778
|
+
* Also supports customizing native values
|
|
1779
|
+
*
|
|
1780
|
+
* @param value
|
|
1781
|
+
* @param customizer
|
|
1782
|
+
* @returns {{}|undefined|*}
|
|
1783
|
+
*/
|
|
1784
|
+
function deepCircularCopy(value, customizer) {
|
|
1785
|
+
const COPY_IN_PROGRESS_SET = new Set();
|
|
1786
|
+
function internalDeepCircularCopy(value, key) {
|
|
1787
|
+
if (value !== Object(value)) return customizer ? customizer(value, key) : value; // primitive value
|
|
1788
|
+
if (COPY_IN_PROGRESS_SET.has(value)) return undefined;
|
|
1789
|
+
COPY_IN_PROGRESS_SET.add(value);
|
|
1790
|
+
let result;
|
|
1791
|
+
if (isArray(value)) {
|
|
1792
|
+
result = [];
|
|
1793
|
+
eachArray(value, it => {
|
|
1794
|
+
result.push(internalDeepCircularCopy(it));
|
|
1795
|
+
});
|
|
1796
|
+
} else {
|
|
1797
|
+
result = {};
|
|
1798
|
+
each(value, (val, key) => {
|
|
1799
|
+
if (!COPY_IN_PROGRESS_SET.has(val)) {
|
|
1800
|
+
result[key] = internalDeepCircularCopy(val, key);
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return result;
|
|
1805
|
+
}
|
|
1806
|
+
return internalDeepCircularCopy(value);
|
|
1807
|
+
}
|
|
1808
|
+
function copyAndTruncateStrings(object, maxStringLength) {
|
|
1809
|
+
return deepCircularCopy(object, value => {
|
|
1810
|
+
if (isString(value) && !isNull(maxStringLength)) {
|
|
1811
|
+
return value.slice(0, maxStringLength);
|
|
1812
|
+
}
|
|
1813
|
+
return value;
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/*
|
|
1818
|
+
* Constants
|
|
1819
|
+
*/
|
|
1820
|
+
/* PROPERTY KEYS */
|
|
1821
|
+
// This key is deprecated, but we want to check for it to see whether aliasing is allowed.
|
|
1822
|
+
const PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id';
|
|
1823
|
+
const DISTINCT_ID = 'distinct_id';
|
|
1824
|
+
const ALIAS_ID_KEY = '__alias';
|
|
1825
|
+
const CAMPAIGN_IDS_KEY = '__cmpns';
|
|
1826
|
+
const EVENT_TIMERS_KEY = '__timers';
|
|
1827
|
+
const AUTOCAPTURE_DISABLED_SERVER_SIDE = '$autocapture_disabled_server_side';
|
|
1828
|
+
const HEATMAPS_ENABLED_SERVER_SIDE = '$heatmaps_enabled_server_side';
|
|
1829
|
+
const ERROR_TRACKING_SUPPRESSION_RULES = '$error_tracking_suppression_rules';
|
|
1830
|
+
// @deprecated can be removed along with eager loaded replay
|
|
1831
|
+
const SESSION_RECORDING_ENABLED_SERVER_SIDE = '$session_recording_enabled_server_side';
|
|
1832
|
+
const SESSION_ID = '$sesid';
|
|
1833
|
+
const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled';
|
|
1834
|
+
const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags';
|
|
1835
|
+
const PERSISTENCE_EARLY_ACCESS_FEATURES = '$early_access_features';
|
|
1836
|
+
const PERSISTENCE_FEATURE_FLAG_DETAILS = '$feature_flag_details';
|
|
1837
|
+
const STORED_PERSON_PROPERTIES_KEY = '$stored_person_properties';
|
|
1838
|
+
const STORED_GROUP_PROPERTIES_KEY = '$stored_group_properties';
|
|
1839
|
+
const SURVEYS = '$surveys';
|
|
1840
|
+
const FLAG_CALL_REPORTED = '$flag_call_reported';
|
|
1841
|
+
const USER_STATE = '$user_state';
|
|
1842
|
+
const CLIENT_SESSION_PROPS = '$client_session_props';
|
|
1843
|
+
const CAPTURE_RATE_LIMIT = '$capture_rate_limit';
|
|
1844
|
+
/** @deprecated Delete this when INITIAL_PERSON_INFO has been around for long enough to ignore backwards compat */
|
|
1845
|
+
const INITIAL_CAMPAIGN_PARAMS = '$initial_campaign_params';
|
|
1846
|
+
/** @deprecated Delete this when INITIAL_PERSON_INFO has been around for long enough to ignore backwards compat */
|
|
1847
|
+
const INITIAL_REFERRER_INFO = '$initial_referrer_info';
|
|
1848
|
+
const INITIAL_PERSON_INFO = '$initial_person_info';
|
|
1849
|
+
const ENABLE_PERSON_PROCESSING = '$epp';
|
|
1850
|
+
const COOKIELESS_MODE_FLAG_PROPERTY = '$cookieless_mode';
|
|
1851
|
+
// These are properties that are reserved and will not be automatically included in events
|
|
1852
|
+
const PERSISTENCE_RESERVED_PROPERTIES = [PEOPLE_DISTINCT_ID_KEY, ALIAS_ID_KEY, CAMPAIGN_IDS_KEY, EVENT_TIMERS_KEY, SESSION_RECORDING_ENABLED_SERVER_SIDE, HEATMAPS_ENABLED_SERVER_SIDE, SESSION_ID, ENABLED_FEATURE_FLAGS, ERROR_TRACKING_SUPPRESSION_RULES, USER_STATE, PERSISTENCE_EARLY_ACCESS_FEATURES, PERSISTENCE_FEATURE_FLAG_DETAILS, STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY, SURVEYS, FLAG_CALL_REPORTED, CLIENT_SESSION_PROPS, CAPTURE_RATE_LIMIT, INITIAL_CAMPAIGN_PARAMS, INITIAL_REFERRER_INFO, ENABLE_PERSON_PROCESSING, INITIAL_PERSON_INFO];
|
|
1597
1853
|
|
|
1598
|
-
|
|
1854
|
+
/* eslint-disable no-console */
|
|
1599
1855
|
const PREFIX = '[Leanbase]';
|
|
1600
1856
|
const logger = {
|
|
1601
1857
|
info: (...args) => {
|
|
1602
1858
|
if (typeof console !== 'undefined') {
|
|
1603
|
-
// eslint-disable-next-line no-console
|
|
1604
1859
|
console.log(PREFIX, ...args);
|
|
1605
1860
|
}
|
|
1606
1861
|
},
|
|
1607
1862
|
warn: (...args) => {
|
|
1608
1863
|
if (typeof console !== 'undefined') {
|
|
1609
|
-
// eslint-disable-next-line no-console
|
|
1610
1864
|
console.warn(PREFIX, ...args);
|
|
1611
1865
|
}
|
|
1612
1866
|
},
|
|
1613
1867
|
error: (...args) => {
|
|
1614
1868
|
if (typeof console !== 'undefined') {
|
|
1615
|
-
// eslint-disable-next-line no-console
|
|
1616
1869
|
console.error(PREFIX, ...args);
|
|
1617
1870
|
}
|
|
1871
|
+
},
|
|
1872
|
+
critical: (...args) => {
|
|
1873
|
+
if (typeof console !== 'undefined') {
|
|
1874
|
+
console.error(PREFIX, 'CRITICAL:', ...args);
|
|
1875
|
+
}
|
|
1618
1876
|
}
|
|
1619
1877
|
};
|
|
1620
1878
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if (options?.preloadFeatureFlags !== false) {
|
|
1651
|
-
this.reloadFeatureFlags();
|
|
1879
|
+
/**
|
|
1880
|
+
* uuidv7: An experimental implementation of the proposed UUID Version 7
|
|
1881
|
+
*
|
|
1882
|
+
* @license Apache-2.0
|
|
1883
|
+
* @copyright 2021-2023 LiosK
|
|
1884
|
+
* @packageDocumentation
|
|
1885
|
+
*
|
|
1886
|
+
* from https://github.com/LiosK/uuidv7/blob/e501462ea3d23241de13192ceae726956f9b3b7d/src/index.ts
|
|
1887
|
+
*/
|
|
1888
|
+
// polyfill for IE11
|
|
1889
|
+
if (!Math.trunc) {
|
|
1890
|
+
Math.trunc = function (v) {
|
|
1891
|
+
return v < 0 ? Math.ceil(v) : Math.floor(v);
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
// polyfill for IE11
|
|
1895
|
+
if (!Number.isInteger) {
|
|
1896
|
+
Number.isInteger = function (value) {
|
|
1897
|
+
return isNumber(value) && isFinite(value) && Math.floor(value) === value;
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
const DIGITS = '0123456789abcdef';
|
|
1901
|
+
/** Represents a UUID as a 16-byte byte array. */
|
|
1902
|
+
class UUID {
|
|
1903
|
+
/** @param bytes - The 16-byte byte array representation. */
|
|
1904
|
+
constructor(bytes) {
|
|
1905
|
+
this.bytes = bytes;
|
|
1906
|
+
if (bytes.length !== 16) {
|
|
1907
|
+
throw new TypeError('not 128-bit length');
|
|
1652
1908
|
}
|
|
1653
1909
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1910
|
+
/**
|
|
1911
|
+
* Builds a byte array from UUIDv7 field values.
|
|
1912
|
+
*
|
|
1913
|
+
* @param unixTsMs - A 48-bit `unix_ts_ms` field value.
|
|
1914
|
+
* @param randA - A 12-bit `rand_a` field value.
|
|
1915
|
+
* @param randBHi - The higher 30 bits of 62-bit `rand_b` field value.
|
|
1916
|
+
* @param randBLo - The lower 32 bits of 62-bit `rand_b` field value.
|
|
1917
|
+
*/
|
|
1918
|
+
static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
|
|
1919
|
+
if (!Number.isInteger(unixTsMs) || !Number.isInteger(randA) || !Number.isInteger(randBHi) || !Number.isInteger(randBLo) || unixTsMs < 0 || randA < 0 || randBHi < 0 || randBLo < 0 || unixTsMs > 281474976710655 || randA > 0xfff || randBHi > 1073741823 || randBLo > 4294967295) {
|
|
1920
|
+
throw new RangeError('invalid field value');
|
|
1921
|
+
}
|
|
1922
|
+
const bytes = new Uint8Array(16);
|
|
1923
|
+
bytes[0] = unixTsMs / 2 ** 40;
|
|
1924
|
+
bytes[1] = unixTsMs / 2 ** 32;
|
|
1925
|
+
bytes[2] = unixTsMs / 2 ** 24;
|
|
1926
|
+
bytes[3] = unixTsMs / 2 ** 16;
|
|
1927
|
+
bytes[4] = unixTsMs / 2 ** 8;
|
|
1928
|
+
bytes[5] = unixTsMs;
|
|
1929
|
+
bytes[6] = 0x70 | randA >>> 8;
|
|
1930
|
+
bytes[7] = randA;
|
|
1931
|
+
bytes[8] = 0x80 | randBHi >>> 24;
|
|
1932
|
+
bytes[9] = randBHi >>> 16;
|
|
1933
|
+
bytes[10] = randBHi >>> 8;
|
|
1934
|
+
bytes[11] = randBHi;
|
|
1935
|
+
bytes[12] = randBLo >>> 24;
|
|
1936
|
+
bytes[13] = randBLo >>> 16;
|
|
1937
|
+
bytes[14] = randBLo >>> 8;
|
|
1938
|
+
bytes[15] = randBLo;
|
|
1939
|
+
return new UUID(bytes);
|
|
1940
|
+
}
|
|
1941
|
+
/** @returns The 8-4-4-4-12 canonical hexadecimal string representation. */
|
|
1942
|
+
toString() {
|
|
1943
|
+
let text = '';
|
|
1944
|
+
for (let i = 0; i < this.bytes.length; i++) {
|
|
1945
|
+
text = text + DIGITS.charAt(this.bytes[i] >>> 4) + DIGITS.charAt(this.bytes[i] & 0xf);
|
|
1946
|
+
if (i === 3 || i === 5 || i === 7 || i === 9) {
|
|
1947
|
+
text += '-';
|
|
1948
|
+
}
|
|
1659
1949
|
}
|
|
1660
|
-
|
|
1950
|
+
if (text.length !== 36) {
|
|
1951
|
+
// We saw one customer whose bundling code was mangling the UUID generation
|
|
1952
|
+
// rather than accept a bad UUID, we throw an error here.
|
|
1953
|
+
throw new Error('Invalid UUIDv7 was generated');
|
|
1954
|
+
}
|
|
1955
|
+
return text;
|
|
1661
1956
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1957
|
+
/** Creates an object from `this`. */
|
|
1958
|
+
clone() {
|
|
1959
|
+
return new UUID(this.bytes.slice(0));
|
|
1664
1960
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1961
|
+
/** Returns true if `this` is equivalent to `other`. */
|
|
1962
|
+
equals(other) {
|
|
1963
|
+
return this.compareTo(other) === 0;
|
|
1667
1964
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1965
|
+
/**
|
|
1966
|
+
* Returns a negative integer, zero, or positive integer if `this` is less
|
|
1967
|
+
* than, equal to, or greater than `other`, respectively.
|
|
1968
|
+
*/
|
|
1969
|
+
compareTo(other) {
|
|
1970
|
+
for (let i = 0; i < 16; i++) {
|
|
1971
|
+
const diff = this.bytes[i] - other.bytes[i];
|
|
1972
|
+
if (diff !== 0) {
|
|
1973
|
+
return Math.sign(diff);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return 0;
|
|
1670
1977
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1978
|
+
}
|
|
1979
|
+
/** Encapsulates the monotonic counter state. */
|
|
1980
|
+
class V7Generator {
|
|
1981
|
+
constructor() {
|
|
1982
|
+
this._timestamp = 0;
|
|
1983
|
+
this._counter = 0;
|
|
1984
|
+
this._random = new DefaultRandom();
|
|
1673
1985
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1986
|
+
/**
|
|
1987
|
+
* Generates a new UUIDv7 object from the current timestamp, or resets the
|
|
1988
|
+
* generator upon significant timestamp rollback.
|
|
1989
|
+
*
|
|
1990
|
+
* This method returns monotonically increasing UUIDs unless the up-to-date
|
|
1991
|
+
* timestamp is significantly (by ten seconds or more) smaller than the one
|
|
1992
|
+
* embedded in the immediately preceding UUID. If such a significant clock
|
|
1993
|
+
* rollback is detected, this method resets the generator and returns a new
|
|
1994
|
+
* UUID based on the current timestamp.
|
|
1995
|
+
*/
|
|
1996
|
+
generate() {
|
|
1997
|
+
const value = this.generateOrAbort();
|
|
1998
|
+
if (!isUndefined(value)) {
|
|
1999
|
+
return value;
|
|
1677
2000
|
} else {
|
|
1678
|
-
|
|
2001
|
+
// reset state and resume
|
|
2002
|
+
this._timestamp = 0;
|
|
2003
|
+
const valueAfterReset = this.generateOrAbort();
|
|
2004
|
+
if (isUndefined(valueAfterReset)) {
|
|
2005
|
+
throw new Error('Could not generate UUID after timestamp reset');
|
|
2006
|
+
}
|
|
2007
|
+
return valueAfterReset;
|
|
1679
2008
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Generates a new UUIDv7 object from the current timestamp, or returns
|
|
2012
|
+
* `undefined` upon significant timestamp rollback.
|
|
2013
|
+
*
|
|
2014
|
+
* This method returns monotonically increasing UUIDs unless the up-to-date
|
|
2015
|
+
* timestamp is significantly (by ten seconds or more) smaller than the one
|
|
2016
|
+
* embedded in the immediately preceding UUID. If such a significant clock
|
|
2017
|
+
* rollback is detected, this method aborts and returns `undefined`.
|
|
2018
|
+
*/
|
|
2019
|
+
generateOrAbort() {
|
|
2020
|
+
const MAX_COUNTER = 4398046511103;
|
|
2021
|
+
const ROLLBACK_ALLOWANCE = 10000; // 10 seconds
|
|
2022
|
+
const ts = Date.now();
|
|
2023
|
+
if (ts > this._timestamp) {
|
|
2024
|
+
this._timestamp = ts;
|
|
2025
|
+
this._resetCounter();
|
|
2026
|
+
} else if (ts + ROLLBACK_ALLOWANCE > this._timestamp) {
|
|
2027
|
+
// go on with previous timestamp if new one is not much smaller
|
|
2028
|
+
this._counter++;
|
|
2029
|
+
if (this._counter > MAX_COUNTER) {
|
|
2030
|
+
// increment timestamp at counter overflow
|
|
2031
|
+
this._timestamp++;
|
|
2032
|
+
this._resetCounter();
|
|
1690
2033
|
}
|
|
2034
|
+
} else {
|
|
2035
|
+
// abort if clock went backwards to unbearable extent
|
|
2036
|
+
return undefined;
|
|
1691
2037
|
}
|
|
2038
|
+
return UUID.fromFieldsV7(this._timestamp, Math.trunc(this._counter / 2 ** 30), this._counter & 2 ** 30 - 1, this._random.nextUint32());
|
|
1692
2039
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2040
|
+
/** Initializes the counter at a 42-bit random integer. */
|
|
2041
|
+
_resetCounter() {
|
|
2042
|
+
this._counter = this._random.nextUint32() * 0x400 + (this._random.nextUint32() & 0x3ff);
|
|
1696
2043
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2044
|
+
}
|
|
2045
|
+
/** Stores `crypto.getRandomValues()` available in the environment. */
|
|
2046
|
+
let getRandomValues = buffer => {
|
|
2047
|
+
// fall back on Math.random() unless the flag is set to true
|
|
2048
|
+
// TRICKY: don't use the isUndefined method here as can't pass the reference
|
|
2049
|
+
if (typeof UUIDV7_DENY_WEAK_RNG !== 'undefined' && UUIDV7_DENY_WEAK_RNG) {
|
|
2050
|
+
throw new Error('no cryptographically strong RNG available');
|
|
1700
2051
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2052
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
2053
|
+
buffer[i] = Math.trunc(Math.random() * 65536) * 65536 + Math.trunc(Math.random() * 65536);
|
|
2054
|
+
}
|
|
2055
|
+
return buffer;
|
|
2056
|
+
};
|
|
2057
|
+
// detect Web Crypto API
|
|
2058
|
+
if (win && !isUndefined(win.crypto) && crypto.getRandomValues) {
|
|
2059
|
+
getRandomValues = buffer => crypto.getRandomValues(buffer);
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Wraps `crypto.getRandomValues()` and compatibles to enable buffering; this
|
|
2063
|
+
* uses a small buffer by default to avoid unbearable throughput decline in some
|
|
2064
|
+
* environments as well as the waste of time and space for unused values.
|
|
2065
|
+
*/
|
|
2066
|
+
class DefaultRandom {
|
|
2067
|
+
constructor() {
|
|
2068
|
+
this._buffer = new Uint32Array(8);
|
|
2069
|
+
this._cursor = Infinity;
|
|
2070
|
+
}
|
|
2071
|
+
nextUint32() {
|
|
2072
|
+
if (this._cursor >= this._buffer.length) {
|
|
2073
|
+
getRandomValues(this._buffer);
|
|
2074
|
+
this._cursor = 0;
|
|
2075
|
+
}
|
|
2076
|
+
return this._buffer[this._cursor++];
|
|
1705
2077
|
}
|
|
1706
2078
|
}
|
|
2079
|
+
let defaultGenerator;
|
|
2080
|
+
/**
|
|
2081
|
+
* Generates a UUIDv7 string.
|
|
2082
|
+
*
|
|
2083
|
+
* @returns The 8-4-4-4-12 canonical hexadecimal string representation
|
|
2084
|
+
* ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
|
|
2085
|
+
*/
|
|
2086
|
+
const uuidv7 = () => uuidv7obj().toString();
|
|
2087
|
+
/** Generates a UUIDv7 object. */
|
|
2088
|
+
const uuidv7obj = () => (defaultGenerator || (defaultGenerator = new V7Generator())).generate();
|
|
1707
2089
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2090
|
+
// we store the discovered subdomain in memory because it might be read multiple times
|
|
2091
|
+
let firstNonPublicSubDomain = '';
|
|
2092
|
+
/**
|
|
2093
|
+
* Browsers don't offer a way to check if something is a public suffix
|
|
2094
|
+
* e.g. `.com.au`, `.io`, `.org.uk`
|
|
2095
|
+
*
|
|
2096
|
+
* But they do reject cookies set on public suffixes
|
|
2097
|
+
* Setting a cookie on `.co.uk` would mean it was sent for every `.co.uk` site visited
|
|
2098
|
+
*
|
|
2099
|
+
* So, we can use this to check if a domain is a public suffix
|
|
2100
|
+
* by trying to set a cookie on a subdomain of the provided hostname
|
|
2101
|
+
* until the browser accepts it
|
|
2102
|
+
*
|
|
2103
|
+
* inspired by https://github.com/AngusFu/browser-root-domain
|
|
2104
|
+
*/
|
|
2105
|
+
function seekFirstNonPublicSubDomain(hostname, cookieJar = document) {
|
|
2106
|
+
if (firstNonPublicSubDomain) {
|
|
2107
|
+
return firstNonPublicSubDomain;
|
|
2108
|
+
}
|
|
2109
|
+
if (!cookieJar) {
|
|
2110
|
+
return '';
|
|
2111
|
+
}
|
|
2112
|
+
if (['localhost', '127.0.0.1'].includes(hostname)) return '';
|
|
2113
|
+
const list = hostname.split('.');
|
|
2114
|
+
let len = Math.min(list.length, 8); // paranoia - we know this number should be small
|
|
2115
|
+
const key = 'dmn_chk_' + uuidv7();
|
|
2116
|
+
while (!firstNonPublicSubDomain && len--) {
|
|
2117
|
+
const candidate = list.slice(len).join('.');
|
|
2118
|
+
const candidateCookieValue = key + '=1;domain=.' + candidate + ';path=/';
|
|
2119
|
+
// try to set cookie, include a short expiry in seconds since we'll check immediately
|
|
2120
|
+
cookieJar.cookie = candidateCookieValue + ';max-age=3';
|
|
2121
|
+
if (cookieJar.cookie.includes(key)) {
|
|
2122
|
+
// the cookie was accepted by the browser, remove the test cookie
|
|
2123
|
+
cookieJar.cookie = candidateCookieValue + ';max-age=0';
|
|
2124
|
+
firstNonPublicSubDomain = candidate;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return firstNonPublicSubDomain;
|
|
2128
|
+
}
|
|
2129
|
+
const DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z]{2,}$/i;
|
|
2130
|
+
const originalCookieDomainFn = hostname => {
|
|
2131
|
+
const matches = hostname.match(DOMAIN_MATCH_REGEX);
|
|
2132
|
+
return matches ? matches[0] : '';
|
|
2133
|
+
};
|
|
2134
|
+
function chooseCookieDomain(hostname, cross_subdomain) {
|
|
2135
|
+
if (cross_subdomain) {
|
|
2136
|
+
// NOTE: Could we use this for cross domain tracking?
|
|
2137
|
+
let matchedSubDomain = seekFirstNonPublicSubDomain(hostname);
|
|
2138
|
+
if (!matchedSubDomain) {
|
|
2139
|
+
const originalMatch = originalCookieDomainFn(hostname);
|
|
2140
|
+
if (originalMatch !== matchedSubDomain) {
|
|
2141
|
+
logger.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
|
|
2142
|
+
}
|
|
2143
|
+
matchedSubDomain = originalMatch;
|
|
1722
2144
|
}
|
|
2145
|
+
return matchedSubDomain ? '; domain=.' + matchedSubDomain : '';
|
|
2146
|
+
}
|
|
2147
|
+
return '';
|
|
2148
|
+
}
|
|
2149
|
+
// Methods partially borrowed from quirksmode.org/js/cookies.html
|
|
2150
|
+
const cookieStore = {
|
|
2151
|
+
_is_supported: () => !!document,
|
|
2152
|
+
_error: function (msg) {
|
|
2153
|
+
logger.error('cookieStore error: ' + msg);
|
|
1723
2154
|
},
|
|
1724
|
-
|
|
1725
|
-
if (
|
|
1726
|
-
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2155
|
+
_get: function (name) {
|
|
2156
|
+
if (!document) {
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
try {
|
|
2160
|
+
const nameEQ = name + '=';
|
|
2161
|
+
const ca = document.cookie.split(';').filter(x => x.length);
|
|
2162
|
+
for (let i = 0; i < ca.length; i++) {
|
|
2163
|
+
let c = ca[i];
|
|
2164
|
+
while (c.charAt(0) == ' ') {
|
|
2165
|
+
c = c.substring(1, c.length);
|
|
2166
|
+
}
|
|
2167
|
+
if (c.indexOf(nameEQ) === 0) {
|
|
2168
|
+
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
} catch {}
|
|
2172
|
+
return null;
|
|
2173
|
+
},
|
|
2174
|
+
_parse: function (name) {
|
|
2175
|
+
let cookie;
|
|
2176
|
+
try {
|
|
2177
|
+
cookie = JSON.parse(cookieStore._get(name)) || {};
|
|
2178
|
+
} catch {
|
|
2179
|
+
// noop
|
|
1732
2180
|
}
|
|
2181
|
+
return cookie;
|
|
1733
2182
|
},
|
|
1734
|
-
|
|
1735
|
-
if (
|
|
1736
|
-
|
|
2183
|
+
_set: function (name, value, days, cross_subdomain, is_secure) {
|
|
2184
|
+
if (!document) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
let expires = '',
|
|
2189
|
+
secure = '';
|
|
2190
|
+
const cdomain = chooseCookieDomain(document.location.hostname, cross_subdomain);
|
|
2191
|
+
if (days) {
|
|
2192
|
+
const date = new Date();
|
|
2193
|
+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
2194
|
+
expires = '; expires=' + date.toUTCString();
|
|
2195
|
+
}
|
|
2196
|
+
if (is_secure) {
|
|
2197
|
+
secure = '; secure';
|
|
2198
|
+
}
|
|
2199
|
+
const new_cookie_val = name + '=' + encodeURIComponent(JSON.stringify(value)) + expires + '; SameSite=Lax; path=/' + cdomain + secure;
|
|
2200
|
+
// 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
|
|
2201
|
+
if (new_cookie_val.length > 4096 * 0.9) {
|
|
2202
|
+
logger.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
|
|
2203
|
+
}
|
|
2204
|
+
document.cookie = new_cookie_val;
|
|
2205
|
+
return new_cookie_val;
|
|
2206
|
+
} catch {
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
},
|
|
2210
|
+
_remove: function (name, cross_subdomain) {
|
|
2211
|
+
if (!document?.cookie) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
try {
|
|
2215
|
+
cookieStore._set(name, '', -1, cross_subdomain);
|
|
2216
|
+
} catch {
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
let _localStorage_supported = null;
|
|
2222
|
+
const localStore = {
|
|
2223
|
+
_is_supported: function () {
|
|
2224
|
+
if (!isNull(_localStorage_supported)) {
|
|
2225
|
+
return _localStorage_supported;
|
|
2226
|
+
}
|
|
2227
|
+
let supported = true;
|
|
2228
|
+
if (!isUndefined(win)) {
|
|
2229
|
+
try {
|
|
2230
|
+
const key = '__mplssupport__',
|
|
2231
|
+
val = 'xyz';
|
|
2232
|
+
localStore._set(key, val);
|
|
2233
|
+
if (localStore._get(key) !== '"xyz"') {
|
|
2234
|
+
supported = false;
|
|
2235
|
+
}
|
|
2236
|
+
localStore._remove(key);
|
|
2237
|
+
} catch {
|
|
2238
|
+
supported = false;
|
|
2239
|
+
}
|
|
1737
2240
|
} else {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2241
|
+
supported = false;
|
|
2242
|
+
}
|
|
2243
|
+
if (!supported) {
|
|
2244
|
+
logger.error('localStorage unsupported; falling back to cookie store');
|
|
2245
|
+
}
|
|
2246
|
+
_localStorage_supported = supported;
|
|
2247
|
+
return supported;
|
|
2248
|
+
},
|
|
2249
|
+
_error: function (msg) {
|
|
2250
|
+
logger.error('localStorage error: ' + msg);
|
|
2251
|
+
},
|
|
2252
|
+
_get: function (name) {
|
|
2253
|
+
try {
|
|
2254
|
+
return win?.localStorage.getItem(name);
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
localStore._error(err);
|
|
2257
|
+
}
|
|
2258
|
+
return null;
|
|
2259
|
+
},
|
|
2260
|
+
_parse: function (name) {
|
|
2261
|
+
try {
|
|
2262
|
+
return JSON.parse(localStore._get(name)) || {};
|
|
2263
|
+
} catch {
|
|
2264
|
+
// noop
|
|
2265
|
+
}
|
|
2266
|
+
return null;
|
|
2267
|
+
},
|
|
2268
|
+
_set: function (name, value) {
|
|
2269
|
+
try {
|
|
2270
|
+
win?.localStorage.setItem(name, JSON.stringify(value));
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
localStore._error(err);
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
_remove: function (name) {
|
|
2276
|
+
try {
|
|
2277
|
+
win?.localStorage.removeItem(name);
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
localStore._error(err);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
// Use localstorage for most data but still use cookie for COOKIE_PERSISTED_PROPERTIES
|
|
2284
|
+
// This solves issues with cookies having too much data in them causing headers too large
|
|
2285
|
+
// Also makes sure we don't have to send a ton of data to the server
|
|
2286
|
+
const COOKIE_PERSISTED_PROPERTIES = [DISTINCT_ID, SESSION_ID, SESSION_RECORDING_IS_SAMPLED, ENABLE_PERSON_PROCESSING, INITIAL_PERSON_INFO];
|
|
2287
|
+
const localPlusCookieStore = {
|
|
2288
|
+
...localStore,
|
|
2289
|
+
_parse: function (name) {
|
|
2290
|
+
try {
|
|
2291
|
+
let cookieProperties = {};
|
|
2292
|
+
try {
|
|
2293
|
+
// See if there's a cookie stored with data.
|
|
2294
|
+
cookieProperties = cookieStore._parse(name) || {};
|
|
2295
|
+
} catch {}
|
|
2296
|
+
const value = extend(cookieProperties, JSON.parse(localStore._get(name) || '{}'));
|
|
2297
|
+
localStore._set(name, value);
|
|
2298
|
+
return value;
|
|
2299
|
+
} catch {
|
|
2300
|
+
// noop
|
|
2301
|
+
}
|
|
2302
|
+
return null;
|
|
2303
|
+
},
|
|
2304
|
+
_set: function (name, value, days, cross_subdomain, is_secure, debug) {
|
|
2305
|
+
try {
|
|
2306
|
+
localStore._set(name, value, undefined, undefined, debug);
|
|
2307
|
+
const cookiePersistedProperties = {};
|
|
2308
|
+
COOKIE_PERSISTED_PROPERTIES.forEach(key => {
|
|
2309
|
+
if (value[key]) {
|
|
2310
|
+
cookiePersistedProperties[key] = value[key];
|
|
2311
|
+
}
|
|
1741
2312
|
});
|
|
2313
|
+
if (Object.keys(cookiePersistedProperties).length) {
|
|
2314
|
+
cookieStore._set(name, cookiePersistedProperties, days, cross_subdomain, is_secure, debug);
|
|
2315
|
+
}
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
localStore._error(err);
|
|
1742
2318
|
}
|
|
1743
2319
|
},
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2320
|
+
_remove: function (name, cross_subdomain) {
|
|
2321
|
+
try {
|
|
2322
|
+
win?.localStorage.removeItem(name);
|
|
2323
|
+
cookieStore._remove(name, cross_subdomain);
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
localStore._error(err);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
const memoryStorage = {};
|
|
2330
|
+
// Storage that only lasts the length of the pageview if we don't want to use cookies
|
|
2331
|
+
const memoryStore = {
|
|
2332
|
+
_is_supported: function () {
|
|
2333
|
+
return true;
|
|
2334
|
+
},
|
|
2335
|
+
_error: function (msg) {
|
|
2336
|
+
logger.error('memoryStorage error: ' + msg);
|
|
2337
|
+
},
|
|
2338
|
+
_get: function (name) {
|
|
2339
|
+
return memoryStorage[name] || null;
|
|
2340
|
+
},
|
|
2341
|
+
_parse: function (name) {
|
|
2342
|
+
return memoryStorage[name] || null;
|
|
2343
|
+
},
|
|
2344
|
+
_set: function (name, value) {
|
|
2345
|
+
memoryStorage[name] = value;
|
|
2346
|
+
},
|
|
2347
|
+
_remove: function (name) {
|
|
2348
|
+
delete memoryStorage[name];
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
let sessionStorageSupported = null;
|
|
2352
|
+
// Storage that only lasts the length of a tab/window. Survives page refreshes
|
|
2353
|
+
const sessionStore = {
|
|
2354
|
+
_is_supported: function () {
|
|
2355
|
+
if (!isNull(sessionStorageSupported)) {
|
|
2356
|
+
return sessionStorageSupported;
|
|
2357
|
+
}
|
|
2358
|
+
sessionStorageSupported = true;
|
|
2359
|
+
if (!isUndefined(win)) {
|
|
2360
|
+
try {
|
|
2361
|
+
const key = '__support__',
|
|
2362
|
+
val = 'xyz';
|
|
2363
|
+
sessionStore._set(key, val);
|
|
2364
|
+
if (sessionStore._get(key) !== '"xyz"') {
|
|
2365
|
+
sessionStorageSupported = false;
|
|
2366
|
+
}
|
|
2367
|
+
sessionStore._remove(key);
|
|
2368
|
+
} catch {
|
|
2369
|
+
sessionStorageSupported = false;
|
|
2370
|
+
}
|
|
1747
2371
|
} else {
|
|
1748
|
-
|
|
1749
|
-
fn: 'group',
|
|
1750
|
-
args
|
|
1751
|
-
});
|
|
2372
|
+
sessionStorageSupported = false;
|
|
1752
2373
|
}
|
|
2374
|
+
return sessionStorageSupported;
|
|
1753
2375
|
},
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2376
|
+
_error: function (msg) {
|
|
2377
|
+
logger.error('sessionStorage error: ', msg);
|
|
2378
|
+
},
|
|
2379
|
+
_get: function (name) {
|
|
2380
|
+
try {
|
|
2381
|
+
return win?.sessionStorage.getItem(name);
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
sessionStore._error(err);
|
|
2384
|
+
}
|
|
2385
|
+
return null;
|
|
2386
|
+
},
|
|
2387
|
+
_parse: function (name) {
|
|
2388
|
+
try {
|
|
2389
|
+
return JSON.parse(sessionStore._get(name)) || null;
|
|
2390
|
+
} catch {
|
|
2391
|
+
// noop
|
|
2392
|
+
}
|
|
2393
|
+
return null;
|
|
2394
|
+
},
|
|
2395
|
+
_set: function (name, value) {
|
|
2396
|
+
try {
|
|
2397
|
+
win?.sessionStorage.setItem(name, JSON.stringify(value));
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
sessionStore._error(err);
|
|
2400
|
+
}
|
|
2401
|
+
},
|
|
2402
|
+
_remove: function (name) {
|
|
2403
|
+
try {
|
|
2404
|
+
win?.sessionStorage.removeItem(name);
|
|
2405
|
+
} catch (err) {
|
|
2406
|
+
sessionStore._error(err);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* IE11 doesn't support `new URL`
|
|
2413
|
+
* so we can create an anchor element and use that to parse the URL
|
|
2414
|
+
* there's a lot of overlap between HTMLHyperlinkElementUtils and URL
|
|
2415
|
+
* meaning useful properties like `pathname` are available on both
|
|
2416
|
+
*/
|
|
2417
|
+
const convertToURL = url => {
|
|
2418
|
+
const location = document?.createElement('a');
|
|
2419
|
+
if (isUndefined(location)) {
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
2422
|
+
location.href = url;
|
|
2423
|
+
return location;
|
|
2424
|
+
};
|
|
2425
|
+
// NOTE: Once we get rid of IE11/op_mini we can start using URLSearchParams
|
|
2426
|
+
const getQueryParam = function (url, param) {
|
|
2427
|
+
const withoutHash = url.split('#')[0] || '';
|
|
2428
|
+
// Split only on the first ? to sort problem out for those with multiple ?s
|
|
2429
|
+
// and then remove them
|
|
2430
|
+
const queryParams = withoutHash.split(/\?(.*)/)[1] || '';
|
|
2431
|
+
const cleanedQueryParams = queryParams.replace(/^\?+/g, '');
|
|
2432
|
+
const queryParts = cleanedQueryParams.split('&');
|
|
2433
|
+
let keyValuePair;
|
|
2434
|
+
for (let i = 0; i < queryParts.length; i++) {
|
|
2435
|
+
const parts = queryParts[i].split('=');
|
|
2436
|
+
if (parts[0] === param) {
|
|
2437
|
+
keyValuePair = parts;
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (!isArray(keyValuePair) || keyValuePair.length < 2) {
|
|
2442
|
+
return '';
|
|
2443
|
+
} else {
|
|
2444
|
+
let result = keyValuePair[1];
|
|
2445
|
+
try {
|
|
2446
|
+
result = decodeURIComponent(result);
|
|
2447
|
+
} catch {
|
|
2448
|
+
logger.error('Skipping decoding for malformed query param: ' + result);
|
|
2449
|
+
}
|
|
2450
|
+
return result.replace(/\+/g, ' ');
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
// replace any query params in the url with the provided mask value. Tries to keep the URL as instant as possible,
|
|
2454
|
+
// including preserving malformed text in most cases
|
|
2455
|
+
const maskQueryParams = function (url, maskedParams, mask) {
|
|
2456
|
+
if (!url || !maskedParams || !maskedParams.length) {
|
|
2457
|
+
return url;
|
|
2458
|
+
}
|
|
2459
|
+
const splitHash = url.split('#');
|
|
2460
|
+
const withoutHash = splitHash[0] || '';
|
|
2461
|
+
const hash = splitHash[1];
|
|
2462
|
+
const splitQuery = withoutHash.split('?');
|
|
2463
|
+
const queryString = splitQuery[1];
|
|
2464
|
+
const urlWithoutQueryAndHash = splitQuery[0];
|
|
2465
|
+
const queryParts = (queryString || '').split('&');
|
|
2466
|
+
// use an array of strings rather than an object to preserve ordering and duplicates
|
|
2467
|
+
const paramStrings = [];
|
|
2468
|
+
for (let i = 0; i < queryParts.length; i++) {
|
|
2469
|
+
const keyValuePair = queryParts[i].split('=');
|
|
2470
|
+
if (!isArray(keyValuePair)) {
|
|
2471
|
+
continue;
|
|
2472
|
+
} else if (maskedParams.includes(keyValuePair[0])) {
|
|
2473
|
+
paramStrings.push(keyValuePair[0] + '=' + mask);
|
|
1757
2474
|
} else {
|
|
1758
|
-
|
|
1759
|
-
fn: 'alias',
|
|
1760
|
-
args
|
|
1761
|
-
});
|
|
2475
|
+
paramStrings.push(queryParts[i]);
|
|
1762
2476
|
}
|
|
2477
|
+
}
|
|
2478
|
+
let result = urlWithoutQueryAndHash;
|
|
2479
|
+
if (queryString != null) {
|
|
2480
|
+
result += '?' + paramStrings.join('&');
|
|
2481
|
+
}
|
|
2482
|
+
if (hash != null) {
|
|
2483
|
+
result += '#' + hash;
|
|
2484
|
+
}
|
|
2485
|
+
return result;
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* this device detection code is (at time of writing) about 3% of the size of the entire library
|
|
2490
|
+
* this is mostly because the identifiers user in regexes and results can't be minified away since
|
|
2491
|
+
* they have meaning
|
|
2492
|
+
*
|
|
2493
|
+
* so, there are some pre-uglifying choices in the code to help reduce the size
|
|
2494
|
+
* e.g. many repeated strings are stored as variables and then old-fashioned concatenated together
|
|
2495
|
+
*
|
|
2496
|
+
* TL;DR here be dragons
|
|
2497
|
+
*/
|
|
2498
|
+
const FACEBOOK = 'Facebook';
|
|
2499
|
+
const MOBILE = 'Mobile';
|
|
2500
|
+
const IOS = 'iOS';
|
|
2501
|
+
const ANDROID = 'Android';
|
|
2502
|
+
const TABLET = 'Tablet';
|
|
2503
|
+
const ANDROID_TABLET = ANDROID + ' ' + TABLET;
|
|
2504
|
+
const IPAD = 'iPad';
|
|
2505
|
+
const APPLE = 'Apple';
|
|
2506
|
+
const APPLE_WATCH = APPLE + ' Watch';
|
|
2507
|
+
const SAFARI = 'Safari';
|
|
2508
|
+
const BLACKBERRY = 'BlackBerry';
|
|
2509
|
+
const SAMSUNG = 'Samsung';
|
|
2510
|
+
const SAMSUNG_BROWSER = SAMSUNG + 'Browser';
|
|
2511
|
+
const SAMSUNG_INTERNET = SAMSUNG + ' Internet';
|
|
2512
|
+
const CHROME = 'Chrome';
|
|
2513
|
+
const CHROME_OS = CHROME + ' OS';
|
|
2514
|
+
const CHROME_IOS = CHROME + ' ' + IOS;
|
|
2515
|
+
const INTERNET_EXPLORER = 'Internet Explorer';
|
|
2516
|
+
const INTERNET_EXPLORER_MOBILE = INTERNET_EXPLORER + ' ' + MOBILE;
|
|
2517
|
+
const OPERA = 'Opera';
|
|
2518
|
+
const OPERA_MINI = OPERA + ' Mini';
|
|
2519
|
+
const EDGE = 'Edge';
|
|
2520
|
+
const MICROSOFT_EDGE = 'Microsoft ' + EDGE;
|
|
2521
|
+
const FIREFOX = 'Firefox';
|
|
2522
|
+
const FIREFOX_IOS = FIREFOX + ' ' + IOS;
|
|
2523
|
+
const NINTENDO = 'Nintendo';
|
|
2524
|
+
const PLAYSTATION = 'PlayStation';
|
|
2525
|
+
const XBOX = 'Xbox';
|
|
2526
|
+
const ANDROID_MOBILE = ANDROID + ' ' + MOBILE;
|
|
2527
|
+
const MOBILE_SAFARI = MOBILE + ' ' + SAFARI;
|
|
2528
|
+
const WINDOWS = 'Windows';
|
|
2529
|
+
const WINDOWS_PHONE = WINDOWS + ' Phone';
|
|
2530
|
+
const NOKIA = 'Nokia';
|
|
2531
|
+
const OUYA = 'Ouya';
|
|
2532
|
+
const GENERIC = 'Generic';
|
|
2533
|
+
const GENERIC_MOBILE = GENERIC + ' ' + MOBILE.toLowerCase();
|
|
2534
|
+
const GENERIC_TABLET = GENERIC + ' ' + TABLET.toLowerCase();
|
|
2535
|
+
const KONQUEROR = 'Konqueror';
|
|
2536
|
+
const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)';
|
|
2537
|
+
const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX);
|
|
2538
|
+
const XBOX_REGEX = new RegExp(XBOX, 'i');
|
|
2539
|
+
const PLAYSTATION_REGEX = new RegExp(PLAYSTATION + ' \\w+', 'i');
|
|
2540
|
+
const NINTENDO_REGEX = new RegExp(NINTENDO + ' \\w+', 'i');
|
|
2541
|
+
const BLACKBERRY_REGEX = new RegExp(BLACKBERRY + '|PlayBook|BB10', 'i');
|
|
2542
|
+
const windowsVersionMap = {
|
|
2543
|
+
'NT3.51': 'NT 3.11',
|
|
2544
|
+
'NT4.0': 'NT 4.0',
|
|
2545
|
+
'5.0': '2000',
|
|
2546
|
+
'5.1': 'XP',
|
|
2547
|
+
'5.2': 'XP',
|
|
2548
|
+
'6.0': 'Vista',
|
|
2549
|
+
'6.1': '7',
|
|
2550
|
+
'6.2': '8',
|
|
2551
|
+
'6.3': '8.1',
|
|
2552
|
+
'6.4': '10',
|
|
2553
|
+
'10.0': '10'
|
|
2554
|
+
};
|
|
2555
|
+
/**
|
|
2556
|
+
* Safari detection turns out to be complicated. For e.g. https://stackoverflow.com/a/29696509
|
|
2557
|
+
* We can be slightly loose because some options have been ruled out (e.g. firefox on iOS)
|
|
2558
|
+
* before this check is made
|
|
2559
|
+
*/
|
|
2560
|
+
function isSafari(userAgent) {
|
|
2561
|
+
return includes(userAgent, SAFARI) && !includes(userAgent, CHROME) && !includes(userAgent, ANDROID);
|
|
2562
|
+
}
|
|
2563
|
+
const safariCheck = (ua, vendor) => vendor && includes(vendor, APPLE) || isSafari(ua);
|
|
2564
|
+
/**
|
|
2565
|
+
* This function detects which browser is running this script.
|
|
2566
|
+
* The order of the checks are important since many user agents
|
|
2567
|
+
* include keywords used in later checks.
|
|
2568
|
+
*/
|
|
2569
|
+
const detectBrowser = function (user_agent, vendor) {
|
|
2570
|
+
vendor = vendor || ''; // vendor is undefined for at least IE9
|
|
2571
|
+
if (includes(user_agent, ' OPR/') && includes(user_agent, 'Mini')) {
|
|
2572
|
+
return OPERA_MINI;
|
|
2573
|
+
} else if (includes(user_agent, ' OPR/')) {
|
|
2574
|
+
return OPERA;
|
|
2575
|
+
} else if (BLACKBERRY_REGEX.test(user_agent)) {
|
|
2576
|
+
return BLACKBERRY;
|
|
2577
|
+
} else if (includes(user_agent, 'IE' + MOBILE) || includes(user_agent, 'WPDesktop')) {
|
|
2578
|
+
return INTERNET_EXPLORER_MOBILE;
|
|
2579
|
+
}
|
|
2580
|
+
// https://developer.samsung.com/internet/user-agent-string-format
|
|
2581
|
+
else if (includes(user_agent, SAMSUNG_BROWSER)) {
|
|
2582
|
+
return SAMSUNG_INTERNET;
|
|
2583
|
+
} else if (includes(user_agent, EDGE) || includes(user_agent, 'Edg/')) {
|
|
2584
|
+
return MICROSOFT_EDGE;
|
|
2585
|
+
} else if (includes(user_agent, 'FBIOS')) {
|
|
2586
|
+
return FACEBOOK + ' ' + MOBILE;
|
|
2587
|
+
} else if (includes(user_agent, 'UCWEB') || includes(user_agent, 'UCBrowser')) {
|
|
2588
|
+
return 'UC Browser';
|
|
2589
|
+
} else if (includes(user_agent, 'CriOS')) {
|
|
2590
|
+
return CHROME_IOS; // why not just Chrome?
|
|
2591
|
+
} else if (includes(user_agent, 'CrMo')) {
|
|
2592
|
+
return CHROME;
|
|
2593
|
+
} else if (includes(user_agent, CHROME)) {
|
|
2594
|
+
return CHROME;
|
|
2595
|
+
} else if (includes(user_agent, ANDROID) && includes(user_agent, SAFARI)) {
|
|
2596
|
+
return ANDROID_MOBILE;
|
|
2597
|
+
} else if (includes(user_agent, 'FxiOS')) {
|
|
2598
|
+
return FIREFOX_IOS;
|
|
2599
|
+
} else if (includes(user_agent.toLowerCase(), KONQUEROR.toLowerCase())) {
|
|
2600
|
+
return KONQUEROR;
|
|
2601
|
+
} else if (safariCheck(user_agent, vendor)) {
|
|
2602
|
+
return includes(user_agent, MOBILE) ? MOBILE_SAFARI : SAFARI;
|
|
2603
|
+
} else if (includes(user_agent, FIREFOX)) {
|
|
2604
|
+
return FIREFOX;
|
|
2605
|
+
} else if (includes(user_agent, 'MSIE') || includes(user_agent, 'Trident/')) {
|
|
2606
|
+
return INTERNET_EXPLORER;
|
|
2607
|
+
} else if (includes(user_agent, 'Gecko')) {
|
|
2608
|
+
return FIREFOX;
|
|
2609
|
+
}
|
|
2610
|
+
return '';
|
|
2611
|
+
};
|
|
2612
|
+
const versionRegexes = {
|
|
2613
|
+
[INTERNET_EXPLORER_MOBILE]: [new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2614
|
+
[MICROSOFT_EDGE]: [new RegExp(EDGE + '?\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2615
|
+
[CHROME]: [new RegExp('(' + CHROME + '|CrMo)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2616
|
+
[CHROME_IOS]: [new RegExp('CriOS\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2617
|
+
'UC Browser': [new RegExp('(UCBrowser|UCWEB)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2618
|
+
[SAFARI]: [DEFAULT_BROWSER_VERSION_REGEX],
|
|
2619
|
+
[MOBILE_SAFARI]: [DEFAULT_BROWSER_VERSION_REGEX],
|
|
2620
|
+
[OPERA]: [new RegExp('(' + OPERA + '|OPR)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2621
|
+
[FIREFOX]: [new RegExp(FIREFOX + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2622
|
+
[FIREFOX_IOS]: [new RegExp('FxiOS\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2623
|
+
[KONQUEROR]: [new RegExp('Konqueror[:/]?' + BROWSER_VERSION_REGEX_SUFFIX, 'i')],
|
|
2624
|
+
// not every blackberry user agent has the version after the name
|
|
2625
|
+
[BLACKBERRY]: [new RegExp(BLACKBERRY + ' ' + BROWSER_VERSION_REGEX_SUFFIX), DEFAULT_BROWSER_VERSION_REGEX],
|
|
2626
|
+
[ANDROID_MOBILE]: [new RegExp('android\\s' + BROWSER_VERSION_REGEX_SUFFIX, 'i')],
|
|
2627
|
+
[SAMSUNG_INTERNET]: [new RegExp(SAMSUNG_BROWSER + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2628
|
+
[INTERNET_EXPLORER]: [new RegExp('(rv:|MSIE )' + BROWSER_VERSION_REGEX_SUFFIX)],
|
|
2629
|
+
Mozilla: [new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)]
|
|
2630
|
+
};
|
|
2631
|
+
/**
|
|
2632
|
+
* This function detects which browser version is running this script,
|
|
2633
|
+
* parsing major and minor version (e.g., 42.1). User agent strings from:
|
|
2634
|
+
* http://www.useragentstring.com/pages/useragentstring.php
|
|
2635
|
+
*
|
|
2636
|
+
* `navigator.vendor` is passed in and used to help with detecting certain browsers
|
|
2637
|
+
* NB `navigator.vendor` is deprecated and not present in every browser
|
|
2638
|
+
*/
|
|
2639
|
+
const detectBrowserVersion = function (userAgent, vendor) {
|
|
2640
|
+
const browser = detectBrowser(userAgent, vendor);
|
|
2641
|
+
const regexes = versionRegexes[browser];
|
|
2642
|
+
if (isUndefined(regexes)) {
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
for (let i = 0; i < regexes.length; i++) {
|
|
2646
|
+
const regex = regexes[i];
|
|
2647
|
+
const matches = userAgent.match(regex);
|
|
2648
|
+
if (matches) {
|
|
2649
|
+
return parseFloat(matches[matches.length - 2]);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return null;
|
|
2653
|
+
};
|
|
2654
|
+
// to avoid repeating regexes or calling them twice, we have an array of matches
|
|
2655
|
+
// the first regex that matches uses its matcher function to return the result
|
|
2656
|
+
const osMatchers = [[new RegExp(XBOX + '; ' + XBOX + ' (.*?)[);]', 'i'), match => {
|
|
2657
|
+
return [XBOX, match && match[1] || ''];
|
|
2658
|
+
}], [new RegExp(NINTENDO, 'i'), [NINTENDO, '']], [new RegExp(PLAYSTATION, 'i'), [PLAYSTATION, '']], [BLACKBERRY_REGEX, [BLACKBERRY, '']], [new RegExp(WINDOWS, 'i'), (_, user_agent) => {
|
|
2659
|
+
if (/Phone/.test(user_agent) || /WPDesktop/.test(user_agent)) {
|
|
2660
|
+
return [WINDOWS_PHONE, ''];
|
|
2661
|
+
}
|
|
2662
|
+
// not all JS versions support negative lookbehind, so we need two checks here
|
|
2663
|
+
if (new RegExp(MOBILE).test(user_agent) && !/IEMobile\b/.test(user_agent)) {
|
|
2664
|
+
return [WINDOWS + ' ' + MOBILE, ''];
|
|
2665
|
+
}
|
|
2666
|
+
const match = /Windows NT ([0-9.]+)/i.exec(user_agent);
|
|
2667
|
+
if (match && match[1]) {
|
|
2668
|
+
const version = match[1];
|
|
2669
|
+
let osVersion = windowsVersionMap[version] || '';
|
|
2670
|
+
if (/arm/i.test(user_agent)) {
|
|
2671
|
+
osVersion = 'RT';
|
|
2672
|
+
}
|
|
2673
|
+
return [WINDOWS, osVersion];
|
|
2674
|
+
}
|
|
2675
|
+
return [WINDOWS, ''];
|
|
2676
|
+
}], [/((iPhone|iPad|iPod).*?OS (\d+)_(\d+)_?(\d+)?|iPhone)/, match => {
|
|
2677
|
+
if (match && match[3]) {
|
|
2678
|
+
const versionParts = [match[3], match[4], match[5] || '0'];
|
|
2679
|
+
return [IOS, versionParts.join('.')];
|
|
2680
|
+
}
|
|
2681
|
+
return [IOS, ''];
|
|
2682
|
+
}], [/(watch.*\/(\d+\.\d+\.\d+)|watch os,(\d+\.\d+),)/i, match => {
|
|
2683
|
+
// e.g. Watch4,3/5.3.8 (16U680)
|
|
2684
|
+
let version = '';
|
|
2685
|
+
if (match && match.length >= 3) {
|
|
2686
|
+
version = isUndefined(match[2]) ? match[3] : match[2];
|
|
2687
|
+
}
|
|
2688
|
+
return ['watchOS', version];
|
|
2689
|
+
}], [new RegExp('(' + ANDROID + ' (\\d+)\\.(\\d+)\\.?(\\d+)?|' + ANDROID + ')', 'i'), match => {
|
|
2690
|
+
if (match && match[2]) {
|
|
2691
|
+
const versionParts = [match[2], match[3], match[4] || '0'];
|
|
2692
|
+
return [ANDROID, versionParts.join('.')];
|
|
2693
|
+
}
|
|
2694
|
+
return [ANDROID, ''];
|
|
2695
|
+
}], [/Mac OS X (\d+)[_.](\d+)[_.]?(\d+)?/i, match => {
|
|
2696
|
+
const result = ['Mac OS X', ''];
|
|
2697
|
+
if (match && match[1]) {
|
|
2698
|
+
const versionParts = [match[1], match[2], match[3] || '0'];
|
|
2699
|
+
result[1] = versionParts.join('.');
|
|
2700
|
+
}
|
|
2701
|
+
return result;
|
|
2702
|
+
}], [/Mac/i,
|
|
2703
|
+
// mop up a few non-standard UAs that should match mac
|
|
2704
|
+
['Mac OS X', '']], [/CrOS/, [CHROME_OS, '']], [/Linux|debian/i, ['Linux', '']]];
|
|
2705
|
+
const detectOS = function (user_agent) {
|
|
2706
|
+
for (let i = 0; i < osMatchers.length; i++) {
|
|
2707
|
+
const [rgex, resultOrFn] = osMatchers[i];
|
|
2708
|
+
const match = rgex.exec(user_agent);
|
|
2709
|
+
const result = match && (isFunction(resultOrFn) ? resultOrFn(match, user_agent) : resultOrFn);
|
|
2710
|
+
if (result) {
|
|
2711
|
+
return result;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
return ['', ''];
|
|
2715
|
+
};
|
|
2716
|
+
const detectDevice = function (user_agent) {
|
|
2717
|
+
if (NINTENDO_REGEX.test(user_agent)) {
|
|
2718
|
+
return NINTENDO;
|
|
2719
|
+
} else if (PLAYSTATION_REGEX.test(user_agent)) {
|
|
2720
|
+
return PLAYSTATION;
|
|
2721
|
+
} else if (XBOX_REGEX.test(user_agent)) {
|
|
2722
|
+
return XBOX;
|
|
2723
|
+
} else if (new RegExp(OUYA, 'i').test(user_agent)) {
|
|
2724
|
+
return OUYA;
|
|
2725
|
+
} else if (new RegExp('(' + WINDOWS_PHONE + '|WPDesktop)', 'i').test(user_agent)) {
|
|
2726
|
+
return WINDOWS_PHONE;
|
|
2727
|
+
} else if (/iPad/.test(user_agent)) {
|
|
2728
|
+
return IPAD;
|
|
2729
|
+
} else if (/iPod/.test(user_agent)) {
|
|
2730
|
+
return 'iPod Touch';
|
|
2731
|
+
} else if (/iPhone/.test(user_agent)) {
|
|
2732
|
+
return 'iPhone';
|
|
2733
|
+
} else if (/(watch)(?: ?os[,/]|\d,\d\/)[\d.]+/i.test(user_agent)) {
|
|
2734
|
+
return APPLE_WATCH;
|
|
2735
|
+
} else if (BLACKBERRY_REGEX.test(user_agent)) {
|
|
2736
|
+
return BLACKBERRY;
|
|
2737
|
+
} else if (/(kobo)\s(ereader|touch)/i.test(user_agent)) {
|
|
2738
|
+
return 'Kobo';
|
|
2739
|
+
} else if (new RegExp(NOKIA, 'i').test(user_agent)) {
|
|
2740
|
+
return NOKIA;
|
|
2741
|
+
} else if (
|
|
2742
|
+
// Kindle Fire without Silk / Echo Show
|
|
2743
|
+
/(kf[a-z]{2}wi|aeo[c-r]{2})( bui|\))/i.test(user_agent) ||
|
|
2744
|
+
// Kindle Fire HD
|
|
2745
|
+
/(kf[a-z]+)( bui|\)).+silk\//i.test(user_agent)) {
|
|
2746
|
+
return 'Kindle Fire';
|
|
2747
|
+
} else if (/(Android|ZTE)/i.test(user_agent)) {
|
|
2748
|
+
if (!new RegExp(MOBILE).test(user_agent) || /(9138B|TB782B|Nexus [97]|pixel c|HUAWEISHT|BTV|noble nook|smart ultra 6)/i.test(user_agent)) {
|
|
2749
|
+
if (/pixel[\daxl ]{1,6}/i.test(user_agent) && !/pixel c/i.test(user_agent) || /(huaweimed-al00|tah-|APA|SM-G92|i980|zte|U304AA)/i.test(user_agent) || /lmy47v/i.test(user_agent) && !/QTAQZ3/i.test(user_agent)) {
|
|
2750
|
+
return ANDROID;
|
|
2751
|
+
}
|
|
2752
|
+
return ANDROID_TABLET;
|
|
2753
|
+
} else {
|
|
2754
|
+
return ANDROID;
|
|
2755
|
+
}
|
|
2756
|
+
} else if (new RegExp('(pda|' + MOBILE + ')', 'i').test(user_agent)) {
|
|
2757
|
+
return GENERIC_MOBILE;
|
|
2758
|
+
} else if (new RegExp(TABLET, 'i').test(user_agent) && !new RegExp(TABLET + ' pc', 'i').test(user_agent)) {
|
|
2759
|
+
return GENERIC_TABLET;
|
|
2760
|
+
} else {
|
|
2761
|
+
return '';
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
const detectDeviceType = function (user_agent) {
|
|
2765
|
+
const device = detectDevice(user_agent);
|
|
2766
|
+
if (device === IPAD || device === ANDROID_TABLET || device === 'Kobo' || device === 'Kindle Fire' || device === GENERIC_TABLET) {
|
|
2767
|
+
return TABLET;
|
|
2768
|
+
} else if (device === NINTENDO || device === XBOX || device === PLAYSTATION || device === OUYA) {
|
|
2769
|
+
return 'Console';
|
|
2770
|
+
} else if (device === APPLE_WATCH) {
|
|
2771
|
+
return 'Wearable';
|
|
2772
|
+
} else if (device) {
|
|
2773
|
+
return MOBILE;
|
|
2774
|
+
} else {
|
|
2775
|
+
return 'Desktop';
|
|
2776
|
+
}
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
var version = "0.1.2";
|
|
2780
|
+
var packageInfo = {
|
|
2781
|
+
version: version};
|
|
2782
|
+
|
|
2783
|
+
const Config = {
|
|
2784
|
+
LIB_VERSION: packageInfo.version
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
const URL_REGEX_PREFIX = 'https?://(.*)';
|
|
2788
|
+
// CAMPAIGN_PARAMS and EVENT_TO_PERSON_PROPERTIES should be kept in sync with
|
|
2789
|
+
// https://github.com/PostHog/posthog/blob/master/plugin-server/src/utils/db/utils.ts#L60
|
|
2790
|
+
// The list of campaign parameters that could be considered personal data under e.g. GDPR.
|
|
2791
|
+
// These can be masked in URLs and properties before being sent to posthog.
|
|
2792
|
+
const PERSONAL_DATA_CAMPAIGN_PARAMS = ['gclid',
|
|
2793
|
+
// google ads
|
|
2794
|
+
'gclsrc',
|
|
2795
|
+
// google ads 360
|
|
2796
|
+
'dclid',
|
|
2797
|
+
// google display ads
|
|
2798
|
+
'gbraid',
|
|
2799
|
+
// google ads, web to app
|
|
2800
|
+
'wbraid',
|
|
2801
|
+
// google ads, app to web
|
|
2802
|
+
'fbclid',
|
|
2803
|
+
// facebook
|
|
2804
|
+
'msclkid',
|
|
2805
|
+
// microsoft
|
|
2806
|
+
'twclid',
|
|
2807
|
+
// twitter
|
|
2808
|
+
'li_fat_id',
|
|
2809
|
+
// linkedin
|
|
2810
|
+
'igshid',
|
|
2811
|
+
// instagram
|
|
2812
|
+
'ttclid',
|
|
2813
|
+
// tiktok
|
|
2814
|
+
'rdt_cid',
|
|
2815
|
+
// reddit
|
|
2816
|
+
'epik',
|
|
2817
|
+
// pinterest
|
|
2818
|
+
'qclid',
|
|
2819
|
+
// quora
|
|
2820
|
+
'sccid',
|
|
2821
|
+
// snapchat
|
|
2822
|
+
'irclid',
|
|
2823
|
+
// impact
|
|
2824
|
+
'_kx' // klaviyo
|
|
2825
|
+
];
|
|
2826
|
+
const CAMPAIGN_PARAMS = extendArray(['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'gad_source',
|
|
2827
|
+
// google ads source
|
|
2828
|
+
'mc_cid' // mailchimp campaign id
|
|
2829
|
+
], PERSONAL_DATA_CAMPAIGN_PARAMS);
|
|
2830
|
+
const MASKED = '<masked>';
|
|
2831
|
+
// Campaign params that can be read from the cookie store
|
|
2832
|
+
const COOKIE_CAMPAIGN_PARAMS = ['li_fat_id' // linkedin
|
|
2833
|
+
];
|
|
2834
|
+
function getCampaignParams(customTrackedParams, maskPersonalDataProperties, customPersonalDataProperties) {
|
|
2835
|
+
if (!document) {
|
|
2836
|
+
return {};
|
|
2837
|
+
}
|
|
2838
|
+
const paramsToMask = maskPersonalDataProperties ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || []) : [];
|
|
2839
|
+
// Initially get campaign params from the URL
|
|
2840
|
+
const urlCampaignParams = _getCampaignParamsFromUrl(maskQueryParams(document.URL, paramsToMask, MASKED), customTrackedParams);
|
|
2841
|
+
// But we can also get some of them from the cookie store
|
|
2842
|
+
// For example: https://learn.microsoft.com/en-us/linkedin/marketing/conversions/enabling-first-party-cookies?view=li-lms-2025-05#reading-li_fat_id-from-cookies
|
|
2843
|
+
const cookieCampaignParams = _getCampaignParamsFromCookie();
|
|
2844
|
+
// Prefer the values found in the urlCampaignParams if possible
|
|
2845
|
+
// `extend` will override the values if found in the second argument
|
|
2846
|
+
return extend(cookieCampaignParams, urlCampaignParams);
|
|
2847
|
+
}
|
|
2848
|
+
function _getCampaignParamsFromUrl(url, customParams) {
|
|
2849
|
+
const campaign_keywords = CAMPAIGN_PARAMS.concat(customParams || []);
|
|
2850
|
+
const params = {};
|
|
2851
|
+
each(campaign_keywords, function (kwkey) {
|
|
2852
|
+
const kw = getQueryParam(url, kwkey);
|
|
2853
|
+
params[kwkey] = kw ? kw : null;
|
|
2854
|
+
});
|
|
2855
|
+
return params;
|
|
2856
|
+
}
|
|
2857
|
+
function _getCampaignParamsFromCookie() {
|
|
2858
|
+
const params = {};
|
|
2859
|
+
each(COOKIE_CAMPAIGN_PARAMS, function (kwkey) {
|
|
2860
|
+
const kw = cookieStore._get(kwkey);
|
|
2861
|
+
params[kwkey] = kw ? kw : null;
|
|
2862
|
+
});
|
|
2863
|
+
return params;
|
|
2864
|
+
}
|
|
2865
|
+
function _getSearchEngine(referrer) {
|
|
2866
|
+
if (!referrer) {
|
|
2867
|
+
return null;
|
|
2868
|
+
} else {
|
|
2869
|
+
if (referrer.search(URL_REGEX_PREFIX + 'google.([^/?]*)') === 0) {
|
|
2870
|
+
return 'google';
|
|
2871
|
+
} else if (referrer.search(URL_REGEX_PREFIX + 'bing.com') === 0) {
|
|
2872
|
+
return 'bing';
|
|
2873
|
+
} else if (referrer.search(URL_REGEX_PREFIX + 'yahoo.com') === 0) {
|
|
2874
|
+
return 'yahoo';
|
|
2875
|
+
} else if (referrer.search(URL_REGEX_PREFIX + 'duckduckgo.com') === 0) {
|
|
2876
|
+
return 'duckduckgo';
|
|
2877
|
+
} else {
|
|
2878
|
+
return null;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function _getSearchInfoFromReferrer(referrer) {
|
|
2883
|
+
const search = _getSearchEngine(referrer);
|
|
2884
|
+
const param = search != 'yahoo' ? 'q' : 'p';
|
|
2885
|
+
const ret = {};
|
|
2886
|
+
if (!isNull(search)) {
|
|
2887
|
+
ret['$search_engine'] = search;
|
|
2888
|
+
const keyword = document ? getQueryParam(document.referrer, param) : '';
|
|
2889
|
+
if (keyword.length) {
|
|
2890
|
+
ret['ph_keyword'] = keyword;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return ret;
|
|
2894
|
+
}
|
|
2895
|
+
function getSearchInfo() {
|
|
2896
|
+
const referrer = document?.referrer;
|
|
2897
|
+
if (!referrer) {
|
|
2898
|
+
return {};
|
|
2899
|
+
}
|
|
2900
|
+
return _getSearchInfoFromReferrer(referrer);
|
|
2901
|
+
}
|
|
2902
|
+
function getBrowserLanguage() {
|
|
2903
|
+
return navigator.language ||
|
|
2904
|
+
// Any modern browser
|
|
2905
|
+
navigator.userLanguage // IE11
|
|
2906
|
+
;
|
|
2907
|
+
}
|
|
2908
|
+
function getBrowserLanguagePrefix() {
|
|
2909
|
+
const lang = getBrowserLanguage();
|
|
2910
|
+
return typeof lang === 'string' ? lang.split('-')[0] : undefined;
|
|
2911
|
+
}
|
|
2912
|
+
function getReferrer() {
|
|
2913
|
+
return document?.referrer || '$direct';
|
|
2914
|
+
}
|
|
2915
|
+
function getReferringDomain() {
|
|
2916
|
+
if (!document?.referrer) {
|
|
2917
|
+
return '$direct';
|
|
2918
|
+
}
|
|
2919
|
+
return convertToURL(document.referrer)?.host || '$direct';
|
|
2920
|
+
}
|
|
2921
|
+
function getReferrerInfo() {
|
|
2922
|
+
return {
|
|
2923
|
+
$referrer: getReferrer(),
|
|
2924
|
+
$referring_domain: getReferringDomain()
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
function getPersonInfo(maskPersonalDataProperties, customPersonalDataProperties) {
|
|
2928
|
+
const paramsToMask = maskPersonalDataProperties ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || []) : [];
|
|
2929
|
+
const url = location?.href.substring(0, 1000);
|
|
2930
|
+
// we're being a bit more economical with bytes here because this is stored in the cookie
|
|
2931
|
+
return {
|
|
2932
|
+
r: getReferrer().substring(0, 1000),
|
|
2933
|
+
u: url ? maskQueryParams(url, paramsToMask, MASKED) : undefined
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
function getPersonPropsFromInfo(info) {
|
|
2937
|
+
const {
|
|
2938
|
+
r: referrer,
|
|
2939
|
+
u: url
|
|
2940
|
+
} = info;
|
|
2941
|
+
const referring_domain = referrer == null ? undefined : referrer == '$direct' ? '$direct' : convertToURL(referrer)?.host;
|
|
2942
|
+
const props = {
|
|
2943
|
+
$referrer: referrer,
|
|
2944
|
+
$referring_domain: referring_domain
|
|
2945
|
+
};
|
|
2946
|
+
if (url) {
|
|
2947
|
+
props['$current_url'] = url;
|
|
2948
|
+
const location = convertToURL(url);
|
|
2949
|
+
props['$host'] = location?.host;
|
|
2950
|
+
props['$pathname'] = location?.pathname;
|
|
2951
|
+
const campaignParams = _getCampaignParamsFromUrl(url);
|
|
2952
|
+
extend(props, campaignParams);
|
|
2953
|
+
}
|
|
2954
|
+
if (referrer) {
|
|
2955
|
+
const searchInfo = _getSearchInfoFromReferrer(referrer);
|
|
2956
|
+
extend(props, searchInfo);
|
|
2957
|
+
}
|
|
2958
|
+
return props;
|
|
2959
|
+
}
|
|
2960
|
+
function getInitialPersonPropsFromInfo(info) {
|
|
2961
|
+
const personProps = getPersonPropsFromInfo(info);
|
|
2962
|
+
const props = {};
|
|
2963
|
+
each(personProps, function (val, key) {
|
|
2964
|
+
props[`$initial_${stripLeadingDollar(key)}`] = val;
|
|
2965
|
+
});
|
|
2966
|
+
return props;
|
|
2967
|
+
}
|
|
2968
|
+
function getTimezone() {
|
|
2969
|
+
try {
|
|
2970
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2971
|
+
} catch {
|
|
2972
|
+
return undefined;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
function getTimezoneOffset() {
|
|
2976
|
+
try {
|
|
2977
|
+
return new Date().getTimezoneOffset();
|
|
2978
|
+
} catch {
|
|
2979
|
+
return undefined;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
function getEventProperties(maskPersonalDataProperties, customPersonalDataProperties) {
|
|
2983
|
+
if (!userAgent) {
|
|
2984
|
+
return {};
|
|
2985
|
+
}
|
|
2986
|
+
const paramsToMask = maskPersonalDataProperties ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || []) : [];
|
|
2987
|
+
const [os_name, os_version] = detectOS(userAgent);
|
|
2988
|
+
return extend(stripEmptyProperties({
|
|
2989
|
+
$os: os_name,
|
|
2990
|
+
$os_version: os_version,
|
|
2991
|
+
$browser: detectBrowser(userAgent, navigator.vendor),
|
|
2992
|
+
$device: detectDevice(userAgent),
|
|
2993
|
+
$device_type: detectDeviceType(userAgent),
|
|
2994
|
+
$timezone: getTimezone(),
|
|
2995
|
+
$timezone_offset: getTimezoneOffset()
|
|
2996
|
+
}), {
|
|
2997
|
+
$current_url: maskQueryParams(location?.href, paramsToMask, MASKED),
|
|
2998
|
+
$host: location?.host,
|
|
2999
|
+
$pathname: location?.pathname,
|
|
3000
|
+
$raw_user_agent: userAgent.length > 1000 ? userAgent.substring(0, 997) + '...' : userAgent,
|
|
3001
|
+
$browser_version: detectBrowserVersion(userAgent, navigator.vendor),
|
|
3002
|
+
$browser_language: getBrowserLanguage(),
|
|
3003
|
+
$browser_language_prefix: getBrowserLanguagePrefix(),
|
|
3004
|
+
$screen_height: win?.screen.height,
|
|
3005
|
+
$screen_width: win?.screen.width,
|
|
3006
|
+
$viewport_height: win?.innerHeight,
|
|
3007
|
+
$viewport_width: win?.innerWidth,
|
|
3008
|
+
$lib: 'web',
|
|
3009
|
+
$lib_version: Config.LIB_VERSION,
|
|
3010
|
+
$insert_id: Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10),
|
|
3011
|
+
$time: Date.now() / 1000 // epoch time in seconds
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/* eslint camelcase: "off" */
|
|
3016
|
+
const CASE_INSENSITIVE_PERSISTENCE_TYPES = ['cookie', 'localstorage', 'localstorage+cookie', 'sessionstorage', 'memory'];
|
|
3017
|
+
const parseName = config => {
|
|
3018
|
+
let token = '';
|
|
3019
|
+
if (config['token']) {
|
|
3020
|
+
token = config['token'].replace(/\+/g, 'PL').replace(/\//g, 'SL').replace(/=/g, 'EQ');
|
|
3021
|
+
}
|
|
3022
|
+
if (config['persistence_name']) {
|
|
3023
|
+
return config['persistence_name'];
|
|
3024
|
+
}
|
|
3025
|
+
return 'leanbase_' + token;
|
|
3026
|
+
};
|
|
3027
|
+
/**
|
|
3028
|
+
* Leanbase Persistence Object
|
|
3029
|
+
* @constructor
|
|
3030
|
+
*/
|
|
3031
|
+
class LeanbasePersistence {
|
|
3032
|
+
/**
|
|
3033
|
+
* @param {LeanbaseConfig} config initial PostHog configuration
|
|
3034
|
+
* @param {boolean=} isDisabled should persistence be disabled (e.g. because of consent management)
|
|
3035
|
+
*/
|
|
3036
|
+
constructor(config, isDisabled) {
|
|
3037
|
+
this._config = config;
|
|
3038
|
+
this.props = {};
|
|
3039
|
+
this._campaign_params_saved = false;
|
|
3040
|
+
this._name = parseName(config);
|
|
3041
|
+
this._storage = this._buildStorage(config);
|
|
3042
|
+
this.load();
|
|
3043
|
+
if (config.debug) {
|
|
3044
|
+
logger.info('Persistence loaded', config['persistence'], {
|
|
3045
|
+
...this.props
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
this.update_config(config, config, isDisabled);
|
|
3049
|
+
this.save();
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Returns whether persistence is disabled. Only available in SDKs > 1.257.1. Do not use on extensions, otherwise
|
|
3053
|
+
* it'll break backwards compatibility for any version before 1.257.1.
|
|
3054
|
+
*/
|
|
3055
|
+
isDisabled() {
|
|
3056
|
+
return !!this._disabled;
|
|
3057
|
+
}
|
|
3058
|
+
_buildStorage(config) {
|
|
3059
|
+
if (CASE_INSENSITIVE_PERSISTENCE_TYPES.indexOf(config['persistence'].toLowerCase()) === -1) {
|
|
3060
|
+
logger.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
|
|
3061
|
+
config['persistence'] = 'localStorage+cookie';
|
|
3062
|
+
}
|
|
3063
|
+
let store;
|
|
3064
|
+
const storage_type = config['persistence'].toLowerCase();
|
|
3065
|
+
if (storage_type === 'localstorage' && localStore._is_supported()) {
|
|
3066
|
+
store = localStore;
|
|
3067
|
+
} else if (storage_type === 'localstorage+cookie' && localPlusCookieStore._is_supported()) {
|
|
3068
|
+
store = localPlusCookieStore;
|
|
3069
|
+
} else if (storage_type === 'sessionstorage' && sessionStore._is_supported()) {
|
|
3070
|
+
store = sessionStore;
|
|
3071
|
+
} else if (storage_type === 'memory') {
|
|
3072
|
+
store = memoryStore;
|
|
3073
|
+
} else if (storage_type === 'cookie') {
|
|
3074
|
+
store = cookieStore;
|
|
3075
|
+
} else if (localPlusCookieStore._is_supported()) {
|
|
3076
|
+
store = localPlusCookieStore;
|
|
3077
|
+
} else {
|
|
3078
|
+
store = cookieStore;
|
|
3079
|
+
}
|
|
3080
|
+
return store;
|
|
3081
|
+
}
|
|
3082
|
+
properties() {
|
|
3083
|
+
const p = {};
|
|
3084
|
+
// Filter out reserved properties
|
|
3085
|
+
each(this.props, function (v, k) {
|
|
3086
|
+
if (k === ENABLED_FEATURE_FLAGS && isObject(v)) {
|
|
3087
|
+
const keys = Object.keys(v);
|
|
3088
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3089
|
+
p[`$feature/${keys[i]}`] = v[keys[i]];
|
|
3090
|
+
}
|
|
3091
|
+
} else if (!include(PERSISTENCE_RESERVED_PROPERTIES, k)) {
|
|
3092
|
+
p[k] = v;
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
return p;
|
|
3096
|
+
}
|
|
3097
|
+
load() {
|
|
3098
|
+
if (this._disabled) {
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const entry = this._storage._parse(this._name);
|
|
3102
|
+
if (entry) {
|
|
3103
|
+
this.props = extend({}, entry);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* NOTE: Saving frequently causes issues with Recordings and Consent Management Platform (CMP) tools which
|
|
3108
|
+
* observe cookie changes, and modify their UI, often causing infinite loops.
|
|
3109
|
+
* As such callers of this should ideally check that the data has changed beforehand
|
|
3110
|
+
*/
|
|
3111
|
+
save() {
|
|
3112
|
+
if (this._disabled) {
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
this._storage._set(this._name, this.props, this._expire_days, this._cross_subdomain, this._secure, this._config.debug);
|
|
3116
|
+
}
|
|
3117
|
+
remove() {
|
|
3118
|
+
this._storage._remove(this._name, false);
|
|
3119
|
+
this._storage._remove(this._name, true);
|
|
3120
|
+
}
|
|
3121
|
+
clear() {
|
|
3122
|
+
this.remove();
|
|
3123
|
+
this.props = {};
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* @param {Object} props
|
|
3127
|
+
* @param {*=} default_value
|
|
3128
|
+
* @param {number=} days
|
|
3129
|
+
*/
|
|
3130
|
+
register_once(props, default_value, days) {
|
|
3131
|
+
if (isObject(props)) {
|
|
3132
|
+
if (isUndefined(default_value)) {
|
|
3133
|
+
default_value = 'None';
|
|
3134
|
+
}
|
|
3135
|
+
this._expire_days = isUndefined(days) ? this._default_expiry : days;
|
|
3136
|
+
let hasChanges = false;
|
|
3137
|
+
each(props, (val, prop) => {
|
|
3138
|
+
if (!this.props.hasOwnProperty(prop) || this.props[prop] === default_value) {
|
|
3139
|
+
this.props[prop] = val;
|
|
3140
|
+
hasChanges = true;
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
if (hasChanges) {
|
|
3144
|
+
this.save();
|
|
3145
|
+
return true;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
return false;
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* @param {Object} props
|
|
3152
|
+
* @param {number=} days
|
|
3153
|
+
*/
|
|
3154
|
+
register(props, days) {
|
|
3155
|
+
if (isObject(props)) {
|
|
3156
|
+
this._expire_days = isUndefined(days) ? this._default_expiry : days;
|
|
3157
|
+
let hasChanges = false;
|
|
3158
|
+
each(props, (val, prop) => {
|
|
3159
|
+
if (props.hasOwnProperty(prop) && this.props[prop] !== val) {
|
|
3160
|
+
this.props[prop] = val;
|
|
3161
|
+
hasChanges = true;
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
3164
|
+
if (hasChanges) {
|
|
3165
|
+
this.save();
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
return false;
|
|
3170
|
+
}
|
|
3171
|
+
unregister(prop) {
|
|
3172
|
+
if (prop in this.props) {
|
|
3173
|
+
delete this.props[prop];
|
|
3174
|
+
this.save();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
update_campaign_params() {
|
|
3178
|
+
if (!this._campaign_params_saved) {
|
|
3179
|
+
const campaignParams = getCampaignParams(this._config.custom_campaign_params, this._config.mask_personal_data_properties, this._config.custom_personal_data_properties);
|
|
3180
|
+
if (!isEmptyObject(stripEmptyProperties(campaignParams))) {
|
|
3181
|
+
this.register(campaignParams);
|
|
3182
|
+
}
|
|
3183
|
+
this._campaign_params_saved = true;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
update_search_keyword() {
|
|
3187
|
+
this.register(getSearchInfo());
|
|
3188
|
+
}
|
|
3189
|
+
update_referrer_info() {
|
|
3190
|
+
this.register_once(getReferrerInfo(), undefined);
|
|
3191
|
+
}
|
|
3192
|
+
set_initial_person_info() {
|
|
3193
|
+
if (this.props[INITIAL_CAMPAIGN_PARAMS] || this.props[INITIAL_REFERRER_INFO]) {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
this.register_once({
|
|
3197
|
+
[INITIAL_PERSON_INFO]: getPersonInfo(this._config.mask_personal_data_properties, this._config.custom_personal_data_properties)
|
|
3198
|
+
}, undefined);
|
|
3199
|
+
}
|
|
3200
|
+
get_initial_props() {
|
|
3201
|
+
const p = {};
|
|
3202
|
+
each([INITIAL_REFERRER_INFO, INITIAL_CAMPAIGN_PARAMS], key => {
|
|
3203
|
+
const initialReferrerInfo = this.props[key];
|
|
3204
|
+
if (initialReferrerInfo) {
|
|
3205
|
+
each(initialReferrerInfo, function (v, k) {
|
|
3206
|
+
p['$initial_' + stripLeadingDollar(k)] = v;
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
});
|
|
3210
|
+
const initialPersonInfo = this.props[INITIAL_PERSON_INFO];
|
|
3211
|
+
if (initialPersonInfo) {
|
|
3212
|
+
const initialPersonProps = getInitialPersonPropsFromInfo(initialPersonInfo);
|
|
3213
|
+
extend(p, initialPersonProps);
|
|
3214
|
+
}
|
|
3215
|
+
return p;
|
|
3216
|
+
}
|
|
3217
|
+
safe_merge(props) {
|
|
3218
|
+
each(this.props, function (val, prop) {
|
|
3219
|
+
if (!(prop in props)) {
|
|
3220
|
+
props[prop] = val;
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3223
|
+
return props;
|
|
3224
|
+
}
|
|
3225
|
+
update_config(config, oldConfig, isDisabled) {
|
|
3226
|
+
this._default_expiry = this._expire_days = config['cookie_expiration'];
|
|
3227
|
+
this.set_disabled(config['disable_persistence'] || !!isDisabled);
|
|
3228
|
+
this.set_cross_subdomain(config['cross_subdomain_cookie']);
|
|
3229
|
+
this.set_secure(config['secure_cookie']);
|
|
3230
|
+
if (config.persistence !== oldConfig.persistence) {
|
|
3231
|
+
const newStore = this._buildStorage(config);
|
|
3232
|
+
const props = this.props;
|
|
3233
|
+
this.clear();
|
|
3234
|
+
this._storage = newStore;
|
|
3235
|
+
this.props = props;
|
|
3236
|
+
this.save();
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
set_disabled(disabled) {
|
|
3240
|
+
this._disabled = disabled;
|
|
3241
|
+
if (this._disabled) {
|
|
3242
|
+
return this.remove();
|
|
3243
|
+
}
|
|
3244
|
+
this.save();
|
|
3245
|
+
}
|
|
3246
|
+
set_cross_subdomain(cross_subdomain) {
|
|
3247
|
+
if (cross_subdomain !== this._cross_subdomain) {
|
|
3248
|
+
this._cross_subdomain = cross_subdomain;
|
|
3249
|
+
this.remove();
|
|
3250
|
+
this.save();
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
set_secure(secure) {
|
|
3254
|
+
if (secure !== this._secure) {
|
|
3255
|
+
this._secure = secure;
|
|
3256
|
+
this.remove();
|
|
3257
|
+
this.save();
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
set_event_timer(event_name, timestamp) {
|
|
3261
|
+
const timers = this.props[EVENT_TIMERS_KEY] || {};
|
|
3262
|
+
timers[event_name] = timestamp;
|
|
3263
|
+
this.props[EVENT_TIMERS_KEY] = timers;
|
|
3264
|
+
this.save();
|
|
3265
|
+
}
|
|
3266
|
+
remove_event_timer(event_name) {
|
|
3267
|
+
const timers = this.props[EVENT_TIMERS_KEY] || {};
|
|
3268
|
+
const timestamp = timers[event_name];
|
|
3269
|
+
if (!isUndefined(timestamp)) {
|
|
3270
|
+
delete this.props[EVENT_TIMERS_KEY][event_name];
|
|
3271
|
+
this.save();
|
|
3272
|
+
}
|
|
3273
|
+
return timestamp;
|
|
3274
|
+
}
|
|
3275
|
+
get_property(prop) {
|
|
3276
|
+
return this.props[prop];
|
|
3277
|
+
}
|
|
3278
|
+
set_property(prop, to) {
|
|
3279
|
+
this.props[prop] = to;
|
|
3280
|
+
this.save();
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
/*
|
|
3285
|
+
* Check whether an element has nodeType Node.ELEMENT_NODE
|
|
3286
|
+
* @param {Element} el - element to check
|
|
3287
|
+
* @returns {boolean} whether el is of the correct nodeType
|
|
3288
|
+
*/
|
|
3289
|
+
function isElementNode(el) {
|
|
3290
|
+
return !!el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
|
|
3291
|
+
}
|
|
3292
|
+
/*
|
|
3293
|
+
* Check whether an element is of a given tag type.
|
|
3294
|
+
* Due to potential reference discrepancies (such as the webcomponents.js polyfill),
|
|
3295
|
+
* we want to match tagNames instead of specific references because something like
|
|
3296
|
+
* element === document.body won't always work because element might not be a native
|
|
3297
|
+
* element.
|
|
3298
|
+
* @param {Element} el - element to check
|
|
3299
|
+
* @param {string} tag - tag name (e.g., "div")
|
|
3300
|
+
* @returns {boolean} whether el is of the given tag type
|
|
3301
|
+
*/
|
|
3302
|
+
function isTag(el, tag) {
|
|
3303
|
+
return !!el && !!el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
|
|
3304
|
+
}
|
|
3305
|
+
/*
|
|
3306
|
+
* Check whether an element has nodeType Node.TEXT_NODE
|
|
3307
|
+
* @param {Element} el - element to check
|
|
3308
|
+
* @returns {boolean} whether el is of the correct nodeType
|
|
3309
|
+
*/
|
|
3310
|
+
function isTextNode(el) {
|
|
3311
|
+
return !!el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
|
|
3312
|
+
}
|
|
3313
|
+
/*
|
|
3314
|
+
* Check whether an element has nodeType Node.DOCUMENT_FRAGMENT_NODE
|
|
3315
|
+
* @param {Element} el - element to check
|
|
3316
|
+
* @returns {boolean} whether el is of the correct nodeType
|
|
3317
|
+
*/
|
|
3318
|
+
function isDocumentFragment(el) {
|
|
3319
|
+
return !!el && el.nodeType === 11; // Node.DOCUMENT_FRAGMENT_NODE - use integer constant for browser portability
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
function splitClassString(s) {
|
|
3323
|
+
return s ? trim(s).split(/\s+/) : [];
|
|
3324
|
+
}
|
|
3325
|
+
function checkForURLMatches(urlsList) {
|
|
3326
|
+
const url = window?.location.href;
|
|
3327
|
+
return !!(url && urlsList && urlsList.some(regex => url.match(regex)));
|
|
3328
|
+
}
|
|
3329
|
+
/*
|
|
3330
|
+
* Get the className of an element, accounting for edge cases where element.className is an object
|
|
3331
|
+
*
|
|
3332
|
+
* Because this is a string it can contain unexpected characters
|
|
3333
|
+
* So, this method safely splits the className and returns that array.
|
|
3334
|
+
*/
|
|
3335
|
+
function getClassNames(el) {
|
|
3336
|
+
let className = '';
|
|
3337
|
+
switch (typeof el.className) {
|
|
3338
|
+
case 'string':
|
|
3339
|
+
className = el.className;
|
|
3340
|
+
break;
|
|
3341
|
+
// TODO: when is this ever used?
|
|
3342
|
+
case 'object':
|
|
3343
|
+
// handle cases where className might be SVGAnimatedString or some other type
|
|
3344
|
+
className = (el.className && 'baseVal' in el.className ? el.className.baseVal : null) || el.getAttribute('class') || '';
|
|
3345
|
+
break;
|
|
3346
|
+
default:
|
|
3347
|
+
className = '';
|
|
3348
|
+
}
|
|
3349
|
+
return splitClassString(className);
|
|
3350
|
+
}
|
|
3351
|
+
function makeSafeText(s) {
|
|
3352
|
+
if (isNullish(s)) {
|
|
3353
|
+
return null;
|
|
3354
|
+
}
|
|
3355
|
+
return trim(s)
|
|
3356
|
+
// scrub potentially sensitive values
|
|
3357
|
+
.split(/(\s+)/).filter(s => shouldCaptureValue(s)).join('')
|
|
3358
|
+
// normalize whitespace
|
|
3359
|
+
.replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
|
|
3360
|
+
// truncate
|
|
3361
|
+
.substring(0, 255);
|
|
3362
|
+
}
|
|
3363
|
+
/*
|
|
3364
|
+
* Get the direct text content of an element, protecting against sensitive data collection.
|
|
3365
|
+
* Concats textContent of each of the element's text node children; this avoids potential
|
|
3366
|
+
* collection of sensitive data that could happen if we used element.textContent and the
|
|
3367
|
+
* element had sensitive child elements, since element.textContent includes child content.
|
|
3368
|
+
* Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
|
|
3369
|
+
* @param {Element} el - element to get the text of
|
|
3370
|
+
* @returns {string} the element's direct text content
|
|
3371
|
+
*/
|
|
3372
|
+
function getSafeText(el) {
|
|
3373
|
+
let elText = '';
|
|
3374
|
+
if (shouldCaptureElement(el) && !isSensitiveElement(el) && el.childNodes && el.childNodes.length) {
|
|
3375
|
+
each(el.childNodes, function (child) {
|
|
3376
|
+
if (isTextNode(child) && child.textContent) {
|
|
3377
|
+
elText += makeSafeText(child.textContent) ?? '';
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
return trim(elText);
|
|
3382
|
+
}
|
|
3383
|
+
function getEventTarget(e) {
|
|
3384
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes
|
|
3385
|
+
if (isUndefined(e.target)) {
|
|
3386
|
+
return e.srcElement || null;
|
|
3387
|
+
} else {
|
|
3388
|
+
if (e.target?.shadowRoot) {
|
|
3389
|
+
return e.composedPath()[0] || null;
|
|
3390
|
+
}
|
|
3391
|
+
return e.target || null;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
const autocaptureCompatibleElements = ['a', 'button', 'form', 'input', 'select', 'textarea', 'label'];
|
|
3395
|
+
/*
|
|
3396
|
+
if there is no config, then all elements are allowed
|
|
3397
|
+
if there is a config, and there is an allow list, then only elements in the allow list are allowed
|
|
3398
|
+
assumes that some other code is checking this element's parents
|
|
3399
|
+
*/
|
|
3400
|
+
function checkIfElementTreePassesElementAllowList(elements, autocaptureConfig) {
|
|
3401
|
+
const allowlist = autocaptureConfig?.element_allowlist;
|
|
3402
|
+
if (isUndefined(allowlist)) {
|
|
3403
|
+
// everything is allowed, when there is no allow list
|
|
3404
|
+
return true;
|
|
3405
|
+
}
|
|
3406
|
+
// check each element in the tree
|
|
3407
|
+
// if any of the elements are in the allow list, then the tree is allowed
|
|
3408
|
+
for (const el of elements) {
|
|
3409
|
+
if (allowlist.some(elementType => el.tagName.toLowerCase() === elementType)) {
|
|
3410
|
+
return true;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
// otherwise there is an allow list and this element tree didn't match it
|
|
3414
|
+
return false;
|
|
3415
|
+
}
|
|
3416
|
+
/*
|
|
3417
|
+
if there is no selector list (i.e. it is undefined), then any elements matches
|
|
3418
|
+
if there is an empty list, then no elements match
|
|
3419
|
+
if there is a selector list, then check it against each element provided
|
|
3420
|
+
*/
|
|
3421
|
+
function checkIfElementsMatchCSSSelector(elements, selectorList) {
|
|
3422
|
+
if (isUndefined(selectorList)) {
|
|
3423
|
+
// everything is allowed, when there is no selector list
|
|
3424
|
+
return true;
|
|
3425
|
+
}
|
|
3426
|
+
for (const el of elements) {
|
|
3427
|
+
if (selectorList.some(selector => el.matches(selector))) {
|
|
3428
|
+
return true;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return false;
|
|
3432
|
+
}
|
|
3433
|
+
function getParentElement(curEl) {
|
|
3434
|
+
const parentNode = curEl.parentNode;
|
|
3435
|
+
if (!parentNode || !isElementNode(parentNode)) return false;
|
|
3436
|
+
return parentNode;
|
|
3437
|
+
}
|
|
3438
|
+
// autocapture check will already filter for ph-no-capture,
|
|
3439
|
+
// but we include it here to protect against future changes accidentally removing that check
|
|
3440
|
+
const DEFAULT_RAGE_CLICK_IGNORE_LIST = ['.ph-no-rageclick', '.ph-no-capture'];
|
|
3441
|
+
function shouldCaptureRageclick(el, _config) {
|
|
3442
|
+
if (!window || cannotCheckForAutocapture(el)) {
|
|
3443
|
+
return false;
|
|
3444
|
+
}
|
|
3445
|
+
let selectorIgnoreList;
|
|
3446
|
+
if (isBoolean(_config)) {
|
|
3447
|
+
selectorIgnoreList = _config ? DEFAULT_RAGE_CLICK_IGNORE_LIST : false;
|
|
3448
|
+
} else {
|
|
3449
|
+
selectorIgnoreList = _config?.css_selector_ignorelist ?? DEFAULT_RAGE_CLICK_IGNORE_LIST;
|
|
3450
|
+
}
|
|
3451
|
+
if (selectorIgnoreList === false) {
|
|
3452
|
+
return false;
|
|
3453
|
+
}
|
|
3454
|
+
const {
|
|
3455
|
+
targetElementList
|
|
3456
|
+
} = getElementAndParentsForElement(el, false);
|
|
3457
|
+
// we don't capture if we match the ignore list
|
|
3458
|
+
return !checkIfElementsMatchCSSSelector(targetElementList, selectorIgnoreList);
|
|
3459
|
+
}
|
|
3460
|
+
const cannotCheckForAutocapture = el => {
|
|
3461
|
+
return !el || isTag(el, 'html') || !isElementNode(el);
|
|
3462
|
+
};
|
|
3463
|
+
const getElementAndParentsForElement = (el, captureOnAnyElement) => {
|
|
3464
|
+
if (!window || cannotCheckForAutocapture(el)) {
|
|
3465
|
+
return {
|
|
3466
|
+
parentIsUsefulElement: false,
|
|
3467
|
+
targetElementList: []
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
let parentIsUsefulElement = false;
|
|
3471
|
+
const targetElementList = [el];
|
|
3472
|
+
let curEl = el;
|
|
3473
|
+
while (curEl.parentNode && !isTag(curEl, 'body')) {
|
|
3474
|
+
// If element is a shadow root, we skip it
|
|
3475
|
+
if (isDocumentFragment(curEl.parentNode)) {
|
|
3476
|
+
targetElementList.push(curEl.parentNode.host);
|
|
3477
|
+
curEl = curEl.parentNode.host;
|
|
3478
|
+
continue;
|
|
3479
|
+
}
|
|
3480
|
+
const parentNode = getParentElement(curEl);
|
|
3481
|
+
if (!parentNode) break;
|
|
3482
|
+
if (captureOnAnyElement || autocaptureCompatibleElements.indexOf(parentNode.tagName.toLowerCase()) > -1) {
|
|
3483
|
+
parentIsUsefulElement = true;
|
|
3484
|
+
} else {
|
|
3485
|
+
const compStyles = window.getComputedStyle(parentNode);
|
|
3486
|
+
if (compStyles && compStyles.getPropertyValue('cursor') === 'pointer') {
|
|
3487
|
+
parentIsUsefulElement = true;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
targetElementList.push(parentNode);
|
|
3491
|
+
curEl = parentNode;
|
|
3492
|
+
}
|
|
3493
|
+
return {
|
|
3494
|
+
parentIsUsefulElement,
|
|
3495
|
+
targetElementList
|
|
3496
|
+
};
|
|
3497
|
+
};
|
|
3498
|
+
/*
|
|
3499
|
+
* Check whether a DOM event should be "captured" or if it may contain sensitive data
|
|
3500
|
+
* using a variety of heuristics.
|
|
3501
|
+
* @param {Element} el - element to check
|
|
3502
|
+
* @param {Event} event - event to check
|
|
3503
|
+
* @param {Object} autocaptureConfig - autocapture config
|
|
3504
|
+
* @param {boolean} captureOnAnyElement - whether to capture on any element, clipboard autocapture doesn't restrict to "clickable" elements
|
|
3505
|
+
* @param {string[]} allowedEventTypes - event types to capture, normally just 'click', but some autocapture types react to different events, some elements have fixed events (e.g., form has "submit")
|
|
3506
|
+
* @returns {boolean} whether the event should be captured
|
|
3507
|
+
*/
|
|
3508
|
+
function shouldCaptureDomEvent(el, event, autocaptureConfig = undefined, captureOnAnyElement, allowedEventTypes) {
|
|
3509
|
+
if (!window || cannotCheckForAutocapture(el)) {
|
|
3510
|
+
return false;
|
|
3511
|
+
}
|
|
3512
|
+
if (autocaptureConfig?.url_allowlist) {
|
|
3513
|
+
// if the current URL is not in the allow list, don't capture
|
|
3514
|
+
if (!checkForURLMatches(autocaptureConfig.url_allowlist)) {
|
|
3515
|
+
return false;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
if (autocaptureConfig?.url_ignorelist) {
|
|
3519
|
+
// if the current URL is in the ignore list, don't capture
|
|
3520
|
+
if (checkForURLMatches(autocaptureConfig.url_ignorelist)) {
|
|
3521
|
+
return false;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (autocaptureConfig?.dom_event_allowlist) {
|
|
3525
|
+
const allowlist = autocaptureConfig.dom_event_allowlist;
|
|
3526
|
+
if (allowlist && !allowlist.some(eventType => event.type === eventType)) {
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
const {
|
|
3531
|
+
parentIsUsefulElement,
|
|
3532
|
+
targetElementList
|
|
3533
|
+
} = getElementAndParentsForElement(el, captureOnAnyElement);
|
|
3534
|
+
if (!checkIfElementTreePassesElementAllowList(targetElementList, autocaptureConfig)) {
|
|
3535
|
+
return false;
|
|
3536
|
+
}
|
|
3537
|
+
if (!checkIfElementsMatchCSSSelector(targetElementList, autocaptureConfig?.css_selector_allowlist)) {
|
|
3538
|
+
return false;
|
|
3539
|
+
}
|
|
3540
|
+
const compStyles = window.getComputedStyle(el);
|
|
3541
|
+
if (compStyles && compStyles.getPropertyValue('cursor') === 'pointer' && event.type === 'click') {
|
|
3542
|
+
return true;
|
|
3543
|
+
}
|
|
3544
|
+
const tag = el.tagName.toLowerCase();
|
|
3545
|
+
switch (tag) {
|
|
3546
|
+
case 'html':
|
|
3547
|
+
return false;
|
|
3548
|
+
case 'form':
|
|
3549
|
+
return (allowedEventTypes || ['submit']).indexOf(event.type) >= 0;
|
|
3550
|
+
case 'input':
|
|
3551
|
+
case 'select':
|
|
3552
|
+
case 'textarea':
|
|
3553
|
+
return (allowedEventTypes || ['change', 'click']).indexOf(event.type) >= 0;
|
|
3554
|
+
default:
|
|
3555
|
+
if (parentIsUsefulElement) return (allowedEventTypes || ['click']).indexOf(event.type) >= 0;
|
|
3556
|
+
return (allowedEventTypes || ['click']).indexOf(event.type) >= 0 && (autocaptureCompatibleElements.indexOf(tag) > -1 || el.getAttribute('contenteditable') === 'true');
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
/*
|
|
3560
|
+
* Check whether a DOM element should be "captured" or if it may contain sensitive data
|
|
3561
|
+
* using a variety of heuristics.
|
|
3562
|
+
* @param {Element} el - element to check
|
|
3563
|
+
* @returns {boolean} whether the element should be captured
|
|
3564
|
+
*/
|
|
3565
|
+
function shouldCaptureElement(el) {
|
|
3566
|
+
for (let curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
|
|
3567
|
+
const classes = getClassNames(curEl);
|
|
3568
|
+
if (includes(classes, 'ph-sensitive') || includes(classes, 'ph-no-capture')) {
|
|
3569
|
+
return false;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
if (includes(getClassNames(el), 'ph-include')) {
|
|
3573
|
+
return true;
|
|
3574
|
+
}
|
|
3575
|
+
// don't include hidden or password fields
|
|
3576
|
+
const type = el.type || '';
|
|
3577
|
+
if (isString(type)) {
|
|
3578
|
+
// it's possible for el.type to be a DOM element if el is a form with a child input[name="type"]
|
|
3579
|
+
switch (type.toLowerCase()) {
|
|
3580
|
+
case 'hidden':
|
|
3581
|
+
return false;
|
|
3582
|
+
case 'password':
|
|
3583
|
+
return false;
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
// filter out data from fields that look like sensitive fields
|
|
3587
|
+
const name = el.name || el.id || '';
|
|
3588
|
+
// See https://github.com/posthog/posthog-js/issues/165
|
|
3589
|
+
// Under specific circumstances a bug caused .replace to be called on a DOM element
|
|
3590
|
+
// instead of a string, removing the element from the page. Ensure this issue is mitigated.
|
|
3591
|
+
if (isString(name)) {
|
|
3592
|
+
// it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
|
|
3593
|
+
const sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
3594
|
+
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
3595
|
+
return false;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3600
|
+
/*
|
|
3601
|
+
* Check whether a DOM element is 'sensitive' and we should only capture limited data
|
|
3602
|
+
* @param {Element} el - element to check
|
|
3603
|
+
* @returns {boolean} whether the element should be captured
|
|
3604
|
+
*/
|
|
3605
|
+
function isSensitiveElement(el) {
|
|
3606
|
+
// don't send data from inputs or similar elements since there will always be
|
|
3607
|
+
// a risk of clientside javascript placing sensitive data in attributes
|
|
3608
|
+
const allowedInputTypes = ['button', 'checkbox', 'submit', 'reset'];
|
|
3609
|
+
if (isTag(el, 'input') && !allowedInputTypes.includes(el.type) || isTag(el, 'select') || isTag(el, 'textarea') || el.getAttribute('contenteditable') === 'true') {
|
|
3610
|
+
return true;
|
|
3611
|
+
}
|
|
3612
|
+
return false;
|
|
3613
|
+
}
|
|
3614
|
+
// Define the core pattern for matching credit card numbers
|
|
3615
|
+
const coreCCPattern = `(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11})`;
|
|
3616
|
+
// Create the Anchored version of the regex by adding '^' at the start and '$' at the end
|
|
3617
|
+
const anchoredCCRegex = new RegExp(`^(?:${coreCCPattern})$`);
|
|
3618
|
+
// The Unanchored version is essentially the core pattern, usable as is for partial matches
|
|
3619
|
+
const unanchoredCCRegex = new RegExp(coreCCPattern);
|
|
3620
|
+
// Define the core pattern for matching SSNs with optional dashes
|
|
3621
|
+
const coreSSNPattern = `\\d{3}-?\\d{2}-?\\d{4}`;
|
|
3622
|
+
// Create the Anchored version of the regex by adding '^' at the start and '$' at the end
|
|
3623
|
+
const anchoredSSNRegex = new RegExp(`^(${coreSSNPattern})$`);
|
|
3624
|
+
// The Unanchored version is essentially the core pattern itself, usable for partial matches
|
|
3625
|
+
const unanchoredSSNRegex = new RegExp(`(${coreSSNPattern})`);
|
|
3626
|
+
/*
|
|
3627
|
+
* Check whether a string value should be "captured" or if it may contain sensitive data
|
|
3628
|
+
* using a variety of heuristics.
|
|
3629
|
+
* @param {string} value - string value to check
|
|
3630
|
+
* @param {boolean} anchorRegexes - whether to anchor the regexes to the start and end of the string
|
|
3631
|
+
* @returns {boolean} whether the element should be captured
|
|
3632
|
+
*/
|
|
3633
|
+
function shouldCaptureValue(value, anchorRegexes = true) {
|
|
3634
|
+
if (isNullish(value)) {
|
|
3635
|
+
return false;
|
|
3636
|
+
}
|
|
3637
|
+
if (isString(value)) {
|
|
3638
|
+
value = trim(value);
|
|
3639
|
+
// check to see if input value looks like a credit card number
|
|
3640
|
+
// see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
|
|
3641
|
+
const ccRegex = anchorRegexes ? anchoredCCRegex : unanchoredCCRegex;
|
|
3642
|
+
if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
|
|
3643
|
+
return false;
|
|
3644
|
+
}
|
|
3645
|
+
// check to see if input value looks like a social security number
|
|
3646
|
+
const ssnRegex = anchorRegexes ? anchoredSSNRegex : unanchoredSSNRegex;
|
|
3647
|
+
if (ssnRegex.test(value)) {
|
|
3648
|
+
return false;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
return true;
|
|
3652
|
+
}
|
|
3653
|
+
/*
|
|
3654
|
+
* Check whether an attribute name is an Angular style attr (either _ngcontent or _nghost)
|
|
3655
|
+
* These update on each build and lead to noise in the element chain
|
|
3656
|
+
* More details on the attributes here: https://angular.io/guide/view-encapsulation
|
|
3657
|
+
* @param {string} attributeName - string value to check
|
|
3658
|
+
* @returns {boolean} whether the element is an angular tag
|
|
3659
|
+
*/
|
|
3660
|
+
function isAngularStyleAttr(attributeName) {
|
|
3661
|
+
if (isString(attributeName)) {
|
|
3662
|
+
return attributeName.substring(0, 10) === '_ngcontent' || attributeName.substring(0, 7) === '_nghost';
|
|
3663
|
+
}
|
|
3664
|
+
return false;
|
|
3665
|
+
}
|
|
3666
|
+
/*
|
|
3667
|
+
* Iterate through children of a target element looking for span tags
|
|
3668
|
+
* and return the text content of the span tags, separated by spaces,
|
|
3669
|
+
* along with the direct text content of the target element
|
|
3670
|
+
* @param {Element} target - element to check
|
|
3671
|
+
* @returns {string} text content of the target element and its child span tags
|
|
3672
|
+
*/
|
|
3673
|
+
function getDirectAndNestedSpanText(target) {
|
|
3674
|
+
let text = getSafeText(target);
|
|
3675
|
+
text = `${text} ${getNestedSpanText(target)}`.trim();
|
|
3676
|
+
return shouldCaptureValue(text) ? text : '';
|
|
3677
|
+
}
|
|
3678
|
+
/*
|
|
3679
|
+
* Iterate through children of a target element looking for span tags
|
|
3680
|
+
* and return the text content of the span tags, separated by spaces
|
|
3681
|
+
* @param {Element} target - element to check
|
|
3682
|
+
* @returns {string} text content of span tags
|
|
3683
|
+
*/
|
|
3684
|
+
function getNestedSpanText(target) {
|
|
3685
|
+
let text = '';
|
|
3686
|
+
if (target && target.childNodes && target.childNodes.length) {
|
|
3687
|
+
each(target.childNodes, function (child) {
|
|
3688
|
+
if (child && child.tagName?.toLowerCase() === 'span') {
|
|
3689
|
+
try {
|
|
3690
|
+
const spanText = getSafeText(child);
|
|
3691
|
+
text = `${text} ${spanText}`.trim();
|
|
3692
|
+
if (child.childNodes && child.childNodes.length) {
|
|
3693
|
+
text = `${text} ${getNestedSpanText(child)}`.trim();
|
|
3694
|
+
}
|
|
3695
|
+
} catch (e) {
|
|
3696
|
+
logger.error('[AutoCapture]', e);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
});
|
|
3700
|
+
}
|
|
3701
|
+
return text;
|
|
3702
|
+
}
|
|
3703
|
+
/*
|
|
3704
|
+
Back in the day storing events in Postgres we use Elements for autocapture events.
|
|
3705
|
+
Now we're using elements_chain. We used to do this parsing/processing during ingestion.
|
|
3706
|
+
This code is just copied over from ingestion, but we should optimize it
|
|
3707
|
+
to create elements_chain string directly.
|
|
3708
|
+
*/
|
|
3709
|
+
function getElementsChainString(elements) {
|
|
3710
|
+
return elementsToString(extractElements(elements));
|
|
3711
|
+
}
|
|
3712
|
+
function escapeQuotes(input) {
|
|
3713
|
+
return input.replace(/"|\\"/g, '\\"');
|
|
3714
|
+
}
|
|
3715
|
+
function elementsToString(elements) {
|
|
3716
|
+
const ret = elements.map(element => {
|
|
3717
|
+
let el_string = '';
|
|
3718
|
+
if (element.tag_name) {
|
|
3719
|
+
el_string += element.tag_name;
|
|
3720
|
+
}
|
|
3721
|
+
if (element.attr_class) {
|
|
3722
|
+
element.attr_class.sort();
|
|
3723
|
+
for (const single_class of element.attr_class) {
|
|
3724
|
+
el_string += `.${single_class.replace(/"/g, '')}`;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
const attributes = {
|
|
3728
|
+
...(element.text ? {
|
|
3729
|
+
text: element.text
|
|
3730
|
+
} : {}),
|
|
3731
|
+
'nth-child': element.nth_child ?? 0,
|
|
3732
|
+
'nth-of-type': element.nth_of_type ?? 0,
|
|
3733
|
+
...(element.href ? {
|
|
3734
|
+
href: element.href
|
|
3735
|
+
} : {}),
|
|
3736
|
+
...(element.attr_id ? {
|
|
3737
|
+
attr_id: element.attr_id
|
|
3738
|
+
} : {}),
|
|
3739
|
+
...element.attributes
|
|
3740
|
+
};
|
|
3741
|
+
const sortedAttributes = {};
|
|
3742
|
+
entries(attributes).sort(([a], [b]) => a.localeCompare(b)).forEach(([key, value]) => sortedAttributes[escapeQuotes(key.toString())] = escapeQuotes(value.toString()));
|
|
3743
|
+
el_string += ':';
|
|
3744
|
+
el_string += entries(sortedAttributes).map(([key, value]) => `${key}="${value}"`).join('');
|
|
3745
|
+
return el_string;
|
|
3746
|
+
});
|
|
3747
|
+
return ret.join(';');
|
|
3748
|
+
}
|
|
3749
|
+
function extractElements(elements) {
|
|
3750
|
+
return elements.map(el => {
|
|
3751
|
+
const response = {
|
|
3752
|
+
text: el['$el_text']?.slice(0, 400),
|
|
3753
|
+
tag_name: el['tag_name'],
|
|
3754
|
+
href: el['attr__href']?.slice(0, 2048),
|
|
3755
|
+
attr_class: extractAttrClass(el),
|
|
3756
|
+
attr_id: el['attr__id'],
|
|
3757
|
+
nth_child: el['nth_child'],
|
|
3758
|
+
nth_of_type: el['nth_of_type'],
|
|
3759
|
+
attributes: {}
|
|
3760
|
+
};
|
|
3761
|
+
entries(el).filter(([key]) => key.indexOf('attr__') === 0).forEach(([key, value]) => response.attributes[key] = value);
|
|
3762
|
+
return response;
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
function extractAttrClass(el) {
|
|
3766
|
+
const attr_class = el['attr__class'];
|
|
3767
|
+
if (!attr_class) {
|
|
3768
|
+
return undefined;
|
|
3769
|
+
} else if (isArray(attr_class)) {
|
|
3770
|
+
return attr_class;
|
|
3771
|
+
} else {
|
|
3772
|
+
return splitClassString(attr_class);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
// Naive rage click implementation: If mouse has not moved further than RAGE_CLICK_THRESHOLD_PX
|
|
3777
|
+
// over RAGE_CLICK_CLICK_COUNT clicks with max RAGE_CLICK_TIMEOUT_MS between clicks, it's
|
|
3778
|
+
// counted as a rage click
|
|
3779
|
+
const RAGE_CLICK_THRESHOLD_PX = 30;
|
|
3780
|
+
const RAGE_CLICK_TIMEOUT_MS = 1000;
|
|
3781
|
+
const RAGE_CLICK_CLICK_COUNT = 3;
|
|
3782
|
+
class RageClick {
|
|
3783
|
+
constructor() {
|
|
3784
|
+
this.clicks = [];
|
|
3785
|
+
}
|
|
3786
|
+
isRageClick(x, y, timestamp) {
|
|
3787
|
+
const lastClick = this.clicks[this.clicks.length - 1];
|
|
3788
|
+
if (lastClick && Math.abs(x - lastClick.x) + Math.abs(y - lastClick.y) < RAGE_CLICK_THRESHOLD_PX && timestamp - lastClick.timestamp < RAGE_CLICK_TIMEOUT_MS) {
|
|
3789
|
+
this.clicks.push({
|
|
3790
|
+
x,
|
|
3791
|
+
y,
|
|
3792
|
+
timestamp
|
|
3793
|
+
});
|
|
3794
|
+
if (this.clicks.length === RAGE_CLICK_CLICK_COUNT) {
|
|
3795
|
+
return true;
|
|
3796
|
+
}
|
|
3797
|
+
} else {
|
|
3798
|
+
this.clicks = [{
|
|
3799
|
+
x,
|
|
3800
|
+
y,
|
|
3801
|
+
timestamp
|
|
3802
|
+
}];
|
|
3803
|
+
}
|
|
3804
|
+
return false;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
const COPY_AUTOCAPTURE_EVENT = '$copy_autocapture';
|
|
3809
|
+
var Compression;
|
|
3810
|
+
(function (Compression) {
|
|
3811
|
+
Compression["GZipJS"] = "gzip-js";
|
|
3812
|
+
Compression["Base64"] = "base64";
|
|
3813
|
+
})(Compression || (Compression = {}));
|
|
3814
|
+
|
|
3815
|
+
function limitText(length, text) {
|
|
3816
|
+
if (text.length > length) {
|
|
3817
|
+
return text.slice(0, length) + '...';
|
|
3818
|
+
}
|
|
3819
|
+
return text;
|
|
3820
|
+
}
|
|
3821
|
+
function getAugmentPropertiesFromElement(elem) {
|
|
3822
|
+
const shouldCaptureEl = shouldCaptureElement(elem);
|
|
3823
|
+
if (!shouldCaptureEl) {
|
|
3824
|
+
return {};
|
|
3825
|
+
}
|
|
3826
|
+
const props = {};
|
|
3827
|
+
each(elem.attributes, function (attr) {
|
|
3828
|
+
if (attr.name && attr.name.indexOf('data-ph-capture-attribute') === 0) {
|
|
3829
|
+
const propertyKey = attr.name.replace('data-ph-capture-attribute-', '');
|
|
3830
|
+
const propertyValue = attr.value;
|
|
3831
|
+
if (propertyKey && propertyValue && shouldCaptureValue(propertyValue)) {
|
|
3832
|
+
props[propertyKey] = propertyValue;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
});
|
|
3836
|
+
return props;
|
|
3837
|
+
}
|
|
3838
|
+
function previousElementSibling(el) {
|
|
3839
|
+
if (el.previousElementSibling) {
|
|
3840
|
+
return el.previousElementSibling;
|
|
3841
|
+
}
|
|
3842
|
+
let _el = el;
|
|
3843
|
+
do {
|
|
3844
|
+
_el = _el.previousSibling; // resolves to ChildNode->Node, which is Element's parent class
|
|
3845
|
+
} while (_el && !isElementNode(_el));
|
|
3846
|
+
return _el;
|
|
3847
|
+
}
|
|
3848
|
+
function getDefaultProperties(eventType) {
|
|
3849
|
+
return {
|
|
3850
|
+
$event_type: eventType,
|
|
3851
|
+
$ce_version: 1
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function getPropertiesFromElement(elem, maskAllAttributes, maskText, elementAttributeIgnorelist) {
|
|
3855
|
+
const tag_name = elem.tagName.toLowerCase();
|
|
3856
|
+
const props = {
|
|
3857
|
+
tag_name: tag_name
|
|
3858
|
+
};
|
|
3859
|
+
if (autocaptureCompatibleElements.indexOf(tag_name) > -1 && !maskText) {
|
|
3860
|
+
if (tag_name.toLowerCase() === 'a' || tag_name.toLowerCase() === 'button') {
|
|
3861
|
+
props['$el_text'] = limitText(1024, getDirectAndNestedSpanText(elem));
|
|
3862
|
+
} else {
|
|
3863
|
+
props['$el_text'] = limitText(1024, getSafeText(elem));
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
const classes = getClassNames(elem);
|
|
3867
|
+
if (classes.length > 0) props['classes'] = classes.filter(function (c) {
|
|
3868
|
+
return c !== '';
|
|
3869
|
+
});
|
|
3870
|
+
// capture the deny list here because this not-a-class class makes it tricky to use this.config in the function below
|
|
3871
|
+
each(elem.attributes, function (attr) {
|
|
3872
|
+
// Only capture attributes we know are safe
|
|
3873
|
+
if (isSensitiveElement(elem) && ['name', 'id', 'class', 'aria-label'].indexOf(attr.name) === -1) return;
|
|
3874
|
+
if (elementAttributeIgnorelist?.includes(attr.name)) return;
|
|
3875
|
+
if (!maskAllAttributes && shouldCaptureValue(attr.value) && !isAngularStyleAttr(attr.name)) {
|
|
3876
|
+
let value = attr.value;
|
|
3877
|
+
if (attr.name === 'class') {
|
|
3878
|
+
// html attributes can _technically_ contain linebreaks,
|
|
3879
|
+
// but we're very intolerant of them in the class string,
|
|
3880
|
+
// so we strip them.
|
|
3881
|
+
value = splitClassString(value).join(' ');
|
|
3882
|
+
}
|
|
3883
|
+
props['attr__' + attr.name] = limitText(1024, value);
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
let nthChild = 1;
|
|
3887
|
+
let nthOfType = 1;
|
|
3888
|
+
let currentElem = elem;
|
|
3889
|
+
while (currentElem = previousElementSibling(currentElem)) {
|
|
3890
|
+
// eslint-disable-line no-cond-assign
|
|
3891
|
+
nthChild++;
|
|
3892
|
+
if (currentElem.tagName === elem.tagName) {
|
|
3893
|
+
nthOfType++;
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
props['nth_child'] = nthChild;
|
|
3897
|
+
props['nth_of_type'] = nthOfType;
|
|
3898
|
+
return props;
|
|
3899
|
+
}
|
|
3900
|
+
function autocapturePropertiesForElement(target, {
|
|
3901
|
+
e,
|
|
3902
|
+
maskAllElementAttributes,
|
|
3903
|
+
maskAllText,
|
|
3904
|
+
elementAttributeIgnoreList,
|
|
3905
|
+
elementsChainAsString
|
|
3906
|
+
}) {
|
|
3907
|
+
const targetElementList = [target];
|
|
3908
|
+
let curEl = target;
|
|
3909
|
+
while (curEl.parentNode && !isTag(curEl, 'body')) {
|
|
3910
|
+
if (isDocumentFragment(curEl.parentNode)) {
|
|
3911
|
+
targetElementList.push(curEl.parentNode.host);
|
|
3912
|
+
curEl = curEl.parentNode.host;
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
targetElementList.push(curEl.parentNode);
|
|
3916
|
+
curEl = curEl.parentNode;
|
|
3917
|
+
}
|
|
3918
|
+
const elementsJson = [];
|
|
3919
|
+
const autocaptureAugmentProperties = {};
|
|
3920
|
+
let href = false;
|
|
3921
|
+
let explicitNoCapture = false;
|
|
3922
|
+
each(targetElementList, el => {
|
|
3923
|
+
const shouldCaptureEl = shouldCaptureElement(el);
|
|
3924
|
+
// if the element or a parent element is an anchor tag
|
|
3925
|
+
// include the href as a property
|
|
3926
|
+
if (el.tagName.toLowerCase() === 'a') {
|
|
3927
|
+
href = el.getAttribute('href');
|
|
3928
|
+
href = shouldCaptureEl && href && shouldCaptureValue(href) && href;
|
|
3929
|
+
}
|
|
3930
|
+
// allow users to programmatically prevent capturing of elements by adding class 'ph-no-capture'
|
|
3931
|
+
const classes = getClassNames(el);
|
|
3932
|
+
if (includes(classes, 'ph-no-capture')) {
|
|
3933
|
+
explicitNoCapture = true;
|
|
3934
|
+
}
|
|
3935
|
+
elementsJson.push(getPropertiesFromElement(el, maskAllElementAttributes, maskAllText, elementAttributeIgnoreList));
|
|
3936
|
+
const augmentProperties = getAugmentPropertiesFromElement(el);
|
|
3937
|
+
extend(autocaptureAugmentProperties, augmentProperties);
|
|
3938
|
+
});
|
|
3939
|
+
if (explicitNoCapture) {
|
|
3940
|
+
return {
|
|
3941
|
+
props: {},
|
|
3942
|
+
explicitNoCapture
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
if (!maskAllText) {
|
|
3946
|
+
// if the element is a button or anchor tag get the span text from any
|
|
3947
|
+
// children and include it as/with the text property on the parent element
|
|
3948
|
+
if (target.tagName.toLowerCase() === 'a' || target.tagName.toLowerCase() === 'button') {
|
|
3949
|
+
elementsJson[0]['$el_text'] = getDirectAndNestedSpanText(target);
|
|
3950
|
+
} else {
|
|
3951
|
+
elementsJson[0]['$el_text'] = getSafeText(target);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
let externalHref;
|
|
3955
|
+
if (href) {
|
|
3956
|
+
elementsJson[0]['attr__href'] = href;
|
|
3957
|
+
const hrefHost = convertToURL(href)?.host;
|
|
3958
|
+
const locationHost = win?.location?.host;
|
|
3959
|
+
if (hrefHost && locationHost && hrefHost !== locationHost) {
|
|
3960
|
+
externalHref = href;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
const props = extend(getDefaultProperties(e.type),
|
|
3964
|
+
// Sending "$elements" is deprecated. Only one client on US cloud uses this.
|
|
3965
|
+
!elementsChainAsString ? {
|
|
3966
|
+
$elements: elementsJson
|
|
3967
|
+
} : {},
|
|
3968
|
+
// Always send $elements_chain, as it's needed downstream in site app filtering
|
|
3969
|
+
{
|
|
3970
|
+
$elements_chain: getElementsChainString(elementsJson)
|
|
3971
|
+
}, elementsJson[0]?.['$el_text'] ? {
|
|
3972
|
+
$el_text: elementsJson[0]?.['$el_text']
|
|
3973
|
+
} : {}, externalHref && e.type === 'click' ? {
|
|
3974
|
+
$external_click_url: externalHref
|
|
3975
|
+
} : {}, autocaptureAugmentProperties);
|
|
3976
|
+
return {
|
|
3977
|
+
props
|
|
3978
|
+
};
|
|
3979
|
+
}
|
|
3980
|
+
class Autocapture {
|
|
3981
|
+
constructor(instance) {
|
|
3982
|
+
this._initialized = false;
|
|
3983
|
+
this._isDisabledServerSide = null;
|
|
3984
|
+
this.rageclicks = new RageClick();
|
|
3985
|
+
this._elementsChainAsString = false;
|
|
3986
|
+
this.instance = instance;
|
|
3987
|
+
this._elementSelectors = null;
|
|
3988
|
+
}
|
|
3989
|
+
get _config() {
|
|
3990
|
+
const config = isObject(this.instance.config.autocapture) ? this.instance.config.autocapture : {};
|
|
3991
|
+
// precompile the regex
|
|
3992
|
+
config.url_allowlist = config.url_allowlist?.map(url => new RegExp(url));
|
|
3993
|
+
config.url_ignorelist = config.url_ignorelist?.map(url => new RegExp(url));
|
|
3994
|
+
return config;
|
|
3995
|
+
}
|
|
3996
|
+
_addDomEventHandlers() {
|
|
3997
|
+
if (!this.isBrowserSupported()) {
|
|
3998
|
+
logger.info('Disabling Automatic Event Collection because this browser is not supported');
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
if (!win || !document) {
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
const handler = e => {
|
|
4005
|
+
e = e || win?.event;
|
|
4006
|
+
try {
|
|
4007
|
+
this._captureEvent(e);
|
|
4008
|
+
} catch (error) {
|
|
4009
|
+
logger.error('Failed to capture event', error);
|
|
4010
|
+
}
|
|
4011
|
+
};
|
|
4012
|
+
addEventListener(document, 'submit', handler, {
|
|
4013
|
+
capture: true
|
|
4014
|
+
});
|
|
4015
|
+
addEventListener(document, 'change', handler, {
|
|
4016
|
+
capture: true
|
|
4017
|
+
});
|
|
4018
|
+
addEventListener(document, 'click', handler, {
|
|
4019
|
+
capture: true
|
|
4020
|
+
});
|
|
4021
|
+
if (this._config.capture_copied_text) {
|
|
4022
|
+
const copiedTextHandler = e => {
|
|
4023
|
+
e = e || win?.event;
|
|
4024
|
+
this._captureEvent(e, COPY_AUTOCAPTURE_EVENT);
|
|
4025
|
+
};
|
|
4026
|
+
addEventListener(document, 'copy', copiedTextHandler, {
|
|
4027
|
+
capture: true
|
|
4028
|
+
});
|
|
4029
|
+
addEventListener(document, 'cut', copiedTextHandler, {
|
|
4030
|
+
capture: true
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
startIfEnabled() {
|
|
4035
|
+
if (this.isEnabled && !this._initialized) {
|
|
4036
|
+
this._addDomEventHandlers();
|
|
4037
|
+
this._initialized = true;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
onRemoteConfig(response) {
|
|
4041
|
+
if (response.elementsChainAsString) {
|
|
4042
|
+
this._elementsChainAsString = response.elementsChainAsString;
|
|
4043
|
+
}
|
|
4044
|
+
if (this.instance.persistence) {
|
|
4045
|
+
this.instance.persistence.register({
|
|
4046
|
+
[AUTOCAPTURE_DISABLED_SERVER_SIDE]: !!response['autocapture_opt_out']
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
this._isDisabledServerSide = !!response['autocapture_opt_out'];
|
|
4050
|
+
this.startIfEnabled();
|
|
4051
|
+
}
|
|
4052
|
+
setElementSelectors(selectors) {
|
|
4053
|
+
this._elementSelectors = selectors;
|
|
4054
|
+
}
|
|
4055
|
+
getElementSelectors(element) {
|
|
4056
|
+
const elementSelectors = [];
|
|
4057
|
+
this._elementSelectors?.forEach(selector => {
|
|
4058
|
+
const matchedElements = document?.querySelectorAll(selector);
|
|
4059
|
+
matchedElements?.forEach(matchedElement => {
|
|
4060
|
+
if (element === matchedElement) {
|
|
4061
|
+
elementSelectors.push(selector);
|
|
4062
|
+
}
|
|
4063
|
+
});
|
|
4064
|
+
});
|
|
4065
|
+
return elementSelectors;
|
|
4066
|
+
}
|
|
4067
|
+
get isEnabled() {
|
|
4068
|
+
const persistedServerDisabled = this.instance.persistence?.props[AUTOCAPTURE_DISABLED_SERVER_SIDE];
|
|
4069
|
+
const memoryDisabled = this._isDisabledServerSide;
|
|
4070
|
+
if (isNull(memoryDisabled) && !isBoolean(persistedServerDisabled)) {
|
|
4071
|
+
return false;
|
|
4072
|
+
}
|
|
4073
|
+
const disabledServer = this._isDisabledServerSide ?? !!persistedServerDisabled;
|
|
4074
|
+
const disabledClient = !this.instance.config.autocapture;
|
|
4075
|
+
return !disabledClient && !disabledServer;
|
|
4076
|
+
}
|
|
4077
|
+
_captureEvent(e, eventName = '$autocapture') {
|
|
4078
|
+
if (!this.isEnabled) {
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
/*** Don't mess with this code without running IE8 tests on it ***/
|
|
4082
|
+
let target = getEventTarget(e);
|
|
4083
|
+
if (isTextNode(target)) {
|
|
4084
|
+
// defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
|
|
4085
|
+
target = target.parentNode || null;
|
|
4086
|
+
}
|
|
4087
|
+
if (eventName === '$autocapture' && e.type === 'click' && e instanceof MouseEvent) {
|
|
4088
|
+
if (!!this.instance.config.rageclick && this.rageclicks?.isRageClick(e.clientX, e.clientY, new Date().getTime())) {
|
|
4089
|
+
if (shouldCaptureRageclick(target, this.instance.config.rageclick)) {
|
|
4090
|
+
this._captureEvent(e, '$rageclick');
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
const isCopyAutocapture = eventName === COPY_AUTOCAPTURE_EVENT;
|
|
4095
|
+
if (target && shouldCaptureDomEvent(target, e, this._config,
|
|
4096
|
+
// mostly this method cares about the target element, but in the case of copy events,
|
|
4097
|
+
// we want some of the work this check does without insisting on the target element's type
|
|
4098
|
+
isCopyAutocapture,
|
|
4099
|
+
// we also don't want to restrict copy checks to clicks,
|
|
4100
|
+
// so we pass that knowledge in here, rather than add the logic inside the check
|
|
4101
|
+
isCopyAutocapture ? ['copy', 'cut'] : undefined)) {
|
|
4102
|
+
const {
|
|
4103
|
+
props,
|
|
4104
|
+
explicitNoCapture
|
|
4105
|
+
} = autocapturePropertiesForElement(target, {
|
|
4106
|
+
e,
|
|
4107
|
+
maskAllElementAttributes: this.instance.config.mask_all_element_attributes,
|
|
4108
|
+
maskAllText: this.instance.config.mask_all_text,
|
|
4109
|
+
elementAttributeIgnoreList: this._config.element_attribute_ignorelist,
|
|
4110
|
+
elementsChainAsString: this._elementsChainAsString
|
|
4111
|
+
});
|
|
4112
|
+
if (explicitNoCapture) {
|
|
4113
|
+
return false;
|
|
4114
|
+
}
|
|
4115
|
+
const elementSelectors = this.getElementSelectors(target);
|
|
4116
|
+
if (elementSelectors && elementSelectors.length > 0) {
|
|
4117
|
+
props['$element_selectors'] = elementSelectors;
|
|
4118
|
+
}
|
|
4119
|
+
if (eventName === COPY_AUTOCAPTURE_EVENT) {
|
|
4120
|
+
// you can't read the data from the clipboard event,
|
|
4121
|
+
// but you can guess that you can read it from the window's current selection
|
|
4122
|
+
const selectedContent = makeSafeText(win?.getSelection()?.toString());
|
|
4123
|
+
const clipType = e.type || 'clipboard';
|
|
4124
|
+
if (!selectedContent) {
|
|
4125
|
+
return false;
|
|
4126
|
+
}
|
|
4127
|
+
props['$selected_content'] = selectedContent;
|
|
4128
|
+
props['$copy_type'] = clipType;
|
|
4129
|
+
}
|
|
4130
|
+
this.instance.capture(eventName, props);
|
|
4131
|
+
return true;
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
isBrowserSupported() {
|
|
4135
|
+
return isFunction(document?.querySelectorAll);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// This keeps track of the PageView state (such as the previous PageView's path, timestamp, id, and scroll properties).
|
|
4140
|
+
// We store the state in memory, which means that for non-SPA sites, the state will be lost on page reload. This means
|
|
4141
|
+
// that non-SPA sites should always send a $pageleave event on any navigation, before the page unloads. For SPA sites,
|
|
4142
|
+
// they only need to send a $pageleave event when the user navigates away from the site, as the information is not lost
|
|
4143
|
+
// on an internal navigation, and is included as the $prev_pageview_ properties in the next $pageview event.
|
|
4144
|
+
// Practically, this means that to find the scroll properties for a given pageview, you need to find the event where
|
|
4145
|
+
// event name is $pageview or $pageleave and where $prev_pageview_id matches the original pageview event's id.
|
|
4146
|
+
class PageViewManager {
|
|
4147
|
+
constructor(instance) {
|
|
4148
|
+
this._instance = instance;
|
|
4149
|
+
}
|
|
4150
|
+
doPageView(timestamp, pageViewId) {
|
|
4151
|
+
const response = this._previousPageViewProperties(timestamp, pageViewId);
|
|
4152
|
+
// On a pageview we reset the contexts
|
|
4153
|
+
this._currentPageview = {
|
|
4154
|
+
pathname: win?.location.pathname ?? '',
|
|
4155
|
+
pageViewId,
|
|
4156
|
+
timestamp
|
|
4157
|
+
};
|
|
4158
|
+
this._instance.scrollManager.resetContext();
|
|
4159
|
+
return response;
|
|
4160
|
+
}
|
|
4161
|
+
doPageLeave(timestamp) {
|
|
4162
|
+
return this._previousPageViewProperties(timestamp, this._currentPageview?.pageViewId);
|
|
4163
|
+
}
|
|
4164
|
+
doEvent() {
|
|
4165
|
+
return {
|
|
4166
|
+
$pageview_id: this._currentPageview?.pageViewId
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
_previousPageViewProperties(timestamp, pageviewId) {
|
|
4170
|
+
const previousPageView = this._currentPageview;
|
|
4171
|
+
if (!previousPageView) {
|
|
4172
|
+
return {
|
|
4173
|
+
$pageview_id: pageviewId
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
let properties = {
|
|
4177
|
+
$pageview_id: pageviewId,
|
|
4178
|
+
$prev_pageview_id: previousPageView.pageViewId
|
|
4179
|
+
};
|
|
4180
|
+
const scrollContext = this._instance.scrollManager.getContext();
|
|
4181
|
+
if (scrollContext && !this._instance.config.disable_scroll_properties) {
|
|
4182
|
+
let {
|
|
4183
|
+
maxScrollHeight,
|
|
4184
|
+
lastScrollY,
|
|
4185
|
+
maxScrollY,
|
|
4186
|
+
maxContentHeight,
|
|
4187
|
+
lastContentY,
|
|
4188
|
+
maxContentY
|
|
4189
|
+
} = scrollContext;
|
|
4190
|
+
if (!isUndefined(maxScrollHeight) && !isUndefined(lastScrollY) && !isUndefined(maxScrollY) && !isUndefined(maxContentHeight) && !isUndefined(lastContentY) && !isUndefined(maxContentY)) {
|
|
4191
|
+
// Use ceil, so that e.g. scrolling 999.5px of a 1000px page is considered 100% scrolled
|
|
4192
|
+
maxScrollHeight = Math.ceil(maxScrollHeight);
|
|
4193
|
+
lastScrollY = Math.ceil(lastScrollY);
|
|
4194
|
+
maxScrollY = Math.ceil(maxScrollY);
|
|
4195
|
+
maxContentHeight = Math.ceil(maxContentHeight);
|
|
4196
|
+
lastContentY = Math.ceil(lastContentY);
|
|
4197
|
+
maxContentY = Math.ceil(maxContentY);
|
|
4198
|
+
// if the maximum scroll height is near 0, then the percentage is 1
|
|
4199
|
+
const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger);
|
|
4200
|
+
const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger);
|
|
4201
|
+
const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger);
|
|
4202
|
+
const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger);
|
|
4203
|
+
properties = extend(properties, {
|
|
4204
|
+
$prev_pageview_last_scroll: lastScrollY,
|
|
4205
|
+
$prev_pageview_last_scroll_percentage: lastScrollPercentage,
|
|
4206
|
+
$prev_pageview_max_scroll: maxScrollY,
|
|
4207
|
+
$prev_pageview_max_scroll_percentage: maxScrollPercentage,
|
|
4208
|
+
$prev_pageview_last_content: lastContentY,
|
|
4209
|
+
$prev_pageview_last_content_percentage: lastContentPercentage,
|
|
4210
|
+
$prev_pageview_max_content: maxContentY,
|
|
4211
|
+
$prev_pageview_max_content_percentage: maxContentPercentage
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
if (previousPageView.pathname) {
|
|
4216
|
+
properties.$prev_pageview_pathname = previousPageView.pathname;
|
|
4217
|
+
}
|
|
4218
|
+
if (previousPageView.timestamp) {
|
|
4219
|
+
// Use seconds, for consistency with our other duration-related properties like $duration
|
|
4220
|
+
properties.$prev_pageview_duration = (timestamp.getTime() - previousPageView.timestamp.getTime()) / 1000;
|
|
4221
|
+
}
|
|
4222
|
+
return properties;
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
// This class is responsible for tracking scroll events and maintaining the scroll context
|
|
4227
|
+
class ScrollManager {
|
|
4228
|
+
constructor(_instance) {
|
|
4229
|
+
this._instance = _instance;
|
|
4230
|
+
this._updateScrollData = () => {
|
|
4231
|
+
if (!this._context) {
|
|
4232
|
+
this._context = {};
|
|
4233
|
+
}
|
|
4234
|
+
const el = this.scrollElement();
|
|
4235
|
+
const scrollY = this.scrollY();
|
|
4236
|
+
const scrollHeight = el ? Math.max(0, el.scrollHeight - el.clientHeight) : 0;
|
|
4237
|
+
const contentY = scrollY + (el?.clientHeight || 0);
|
|
4238
|
+
const contentHeight = el?.scrollHeight || 0;
|
|
4239
|
+
this._context.lastScrollY = Math.ceil(scrollY);
|
|
4240
|
+
this._context.maxScrollY = Math.max(scrollY, this._context.maxScrollY ?? 0);
|
|
4241
|
+
this._context.maxScrollHeight = Math.max(scrollHeight, this._context.maxScrollHeight ?? 0);
|
|
4242
|
+
this._context.lastContentY = contentY;
|
|
4243
|
+
this._context.maxContentY = Math.max(contentY, this._context.maxContentY ?? 0);
|
|
4244
|
+
this._context.maxContentHeight = Math.max(contentHeight, this._context.maxContentHeight ?? 0);
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
getContext() {
|
|
4248
|
+
return this._context;
|
|
4249
|
+
}
|
|
4250
|
+
resetContext() {
|
|
4251
|
+
const ctx = this._context;
|
|
4252
|
+
// update the scroll properties for the new page, but wait until the next tick
|
|
4253
|
+
// of the event loop
|
|
4254
|
+
setTimeout(this._updateScrollData, 0);
|
|
4255
|
+
return ctx;
|
|
4256
|
+
}
|
|
4257
|
+
// `capture: true` is required to get scroll events for other scrollable elements
|
|
4258
|
+
// on the page, not just the window
|
|
4259
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
|
|
4260
|
+
startMeasuringScrollPosition() {
|
|
4261
|
+
addEventListener(win, 'scroll', this._updateScrollData, {
|
|
4262
|
+
capture: true
|
|
4263
|
+
});
|
|
4264
|
+
addEventListener(win, 'scrollend', this._updateScrollData, {
|
|
4265
|
+
capture: true
|
|
4266
|
+
});
|
|
4267
|
+
addEventListener(win, 'resize', this._updateScrollData);
|
|
4268
|
+
}
|
|
4269
|
+
scrollElement() {
|
|
4270
|
+
if (this._instance.config.scroll_root_selector) {
|
|
4271
|
+
const selectors = isArray(this._instance.config.scroll_root_selector) ? this._instance.config.scroll_root_selector : [this._instance.config.scroll_root_selector];
|
|
4272
|
+
for (const selector of selectors) {
|
|
4273
|
+
const element = win?.document.querySelector(selector);
|
|
4274
|
+
if (element) {
|
|
4275
|
+
return element;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
return undefined;
|
|
4279
|
+
} else {
|
|
4280
|
+
return win?.document.documentElement;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
scrollY() {
|
|
4284
|
+
if (this._instance.config.scroll_root_selector) {
|
|
4285
|
+
const element = this.scrollElement();
|
|
4286
|
+
return element && element.scrollTop || 0;
|
|
4287
|
+
} else {
|
|
4288
|
+
return win ? win.scrollY || win.pageYOffset || win.document.documentElement.scrollTop || 0 : 0;
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
scrollX() {
|
|
4292
|
+
if (this._instance.config.scroll_root_selector) {
|
|
4293
|
+
const element = this.scrollElement();
|
|
4294
|
+
return element && element.scrollLeft || 0;
|
|
4295
|
+
} else {
|
|
4296
|
+
return win ? win.scrollX || win.pageXOffset || win.document.documentElement.scrollLeft || 0 : 0;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
const DEFAULT_BLOCKED_UA_STRS = [
|
|
4302
|
+
// Random assortment of bots
|
|
4303
|
+
'amazonbot', 'amazonproductbot', 'app.hypefactors.com',
|
|
4304
|
+
// Buck, but "buck" is too short to be safe to block (https://app.hypefactors.com/media-monitoring/about.htm)
|
|
4305
|
+
'applebot', 'archive.org_bot', 'awariobot', 'backlinksextendedbot', 'baiduspider', 'bingbot', 'bingpreview', 'chrome-lighthouse', 'dataforseobot', 'deepscan', 'duckduckbot', 'facebookexternal', 'facebookcatalog', 'http://yandex.com/bots', 'hubspot', 'ia_archiver', 'leikibot', 'linkedinbot', 'meta-externalagent', 'mj12bot', 'msnbot', 'nessus', 'petalbot', 'pinterest', 'prerender', 'rogerbot', 'screaming frog', 'sebot-wa', 'sitebulb', 'slackbot', 'slurp', 'trendictionbot', 'turnitin', 'twitterbot', 'vercel-screenshot', 'vercelbot', 'yahoo! slurp', 'yandexbot', 'zoombot',
|
|
4306
|
+
// Bot-like words, maybe we should block `bot` entirely?
|
|
4307
|
+
'bot.htm', 'bot.php', '(bot;', 'bot/', 'crawler',
|
|
4308
|
+
// Ahrefs: https://ahrefs.com/seo/glossary/ahrefsbot
|
|
4309
|
+
'ahrefsbot', 'ahrefssiteaudit',
|
|
4310
|
+
// Semrush bots: https://www.semrush.com/bot/
|
|
4311
|
+
'semrushbot', 'siteauditbot', 'splitsignalbot',
|
|
4312
|
+
// AI Crawlers
|
|
4313
|
+
'gptbot', 'oai-searchbot', 'chatgpt-user', 'perplexitybot',
|
|
4314
|
+
// Uptime-like stuff
|
|
4315
|
+
'better uptime bot', 'sentryuptimebot', 'uptimerobot',
|
|
4316
|
+
// headless browsers
|
|
4317
|
+
'headlesschrome', 'cypress',
|
|
4318
|
+
// we don't block electron here, as many customers use posthog-js in electron apps
|
|
4319
|
+
// a whole bunch of goog-specific crawlers
|
|
4320
|
+
// https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
|
|
4321
|
+
'google-hoteladsverifier', 'adsbot-google', 'apis-google', 'duplexweb-google', 'feedfetcher-google', 'google favicon', 'google web preview', 'google-read-aloud', 'googlebot', 'googleother', 'google-cloudvertexbot', 'googleweblight', 'mediapartners-google', 'storebot-google', 'google-inspectiontool', 'bytespider'];
|
|
4322
|
+
/**
|
|
4323
|
+
* Block various web spiders from executing our JS and sending false capturing data
|
|
4324
|
+
*/
|
|
4325
|
+
const isBlockedUA = function (ua, customBlockedUserAgents) {
|
|
4326
|
+
if (!ua) {
|
|
4327
|
+
return false;
|
|
4328
|
+
}
|
|
4329
|
+
const uaLower = ua.toLowerCase();
|
|
4330
|
+
return DEFAULT_BLOCKED_UA_STRS.concat(customBlockedUserAgents || []).some(blockedUA => {
|
|
4331
|
+
const blockedUaLower = blockedUA.toLowerCase();
|
|
4332
|
+
// can't use includes because IE 11 :/
|
|
4333
|
+
return uaLower.indexOf(blockedUaLower) !== -1;
|
|
4334
|
+
});
|
|
4335
|
+
};
|
|
4336
|
+
const isLikelyBot = function (navigator, customBlockedUserAgents) {
|
|
4337
|
+
if (!navigator) {
|
|
4338
|
+
return false;
|
|
4339
|
+
}
|
|
4340
|
+
const ua = navigator.userAgent;
|
|
4341
|
+
if (ua) {
|
|
4342
|
+
if (isBlockedUA(ua, customBlockedUserAgents)) {
|
|
4343
|
+
return true;
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
try {
|
|
4347
|
+
// eslint-disable-next-line compat/compat
|
|
4348
|
+
const uaData = navigator?.userAgentData;
|
|
4349
|
+
if (uaData?.brands && uaData.brands.some(brandObj => isBlockedUA(brandObj?.brand, customBlockedUserAgents))) {
|
|
4350
|
+
return true;
|
|
4351
|
+
}
|
|
4352
|
+
} catch {
|
|
4353
|
+
// ignore the error, we were using experimental browser features
|
|
4354
|
+
}
|
|
4355
|
+
return !!navigator.webdriver;
|
|
4356
|
+
// There's some more enhancements we could make in this area, e.g. it's possible to check if Chrome dev tools are
|
|
4357
|
+
// open, which will detect some bots that are trying to mask themselves and might get past the checks above.
|
|
4358
|
+
// However, this would give false positives for actual humans who have dev tools open.
|
|
4359
|
+
// We could also use the data in navigator.userAgentData.getHighEntropyValues() to detect bots, but we should wait
|
|
4360
|
+
// until this stops being experimental. The MDN docs imply that this might eventually require user permission.
|
|
4361
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues
|
|
4362
|
+
// It would be very bad if posthog-js caused a permission prompt to appear on every page load.
|
|
4363
|
+
};
|
|
4364
|
+
|
|
4365
|
+
const defaultConfig = () => ({
|
|
4366
|
+
host: 'https://i.leanbase.co',
|
|
4367
|
+
token: '',
|
|
4368
|
+
autocapture: true,
|
|
4369
|
+
rageclick: true,
|
|
4370
|
+
persistence: 'localStorage+cookie',
|
|
4371
|
+
capture_pageview: 'history_change',
|
|
4372
|
+
capture_pageleave: 'if_capture_pageview',
|
|
4373
|
+
persistence_name: '',
|
|
4374
|
+
mask_all_element_attributes: false,
|
|
4375
|
+
cookie_expiration: 365,
|
|
4376
|
+
cross_subdomain_cookie: isCrossDomainCookie(document?.location),
|
|
4377
|
+
custom_campaign_params: [],
|
|
4378
|
+
custom_personal_data_properties: [],
|
|
4379
|
+
disable_persistence: false,
|
|
4380
|
+
mask_personal_data_properties: false,
|
|
4381
|
+
secure_cookie: window?.location?.protocol === 'https:',
|
|
4382
|
+
mask_all_text: false,
|
|
4383
|
+
bootstrap: {},
|
|
4384
|
+
session_idle_timeout_seconds: 30 * 60,
|
|
4385
|
+
save_campaign_params: true,
|
|
4386
|
+
save_referrer: true,
|
|
4387
|
+
opt_out_useragent_filter: false,
|
|
4388
|
+
properties_string_max_length: 65535,
|
|
4389
|
+
loaded: () => {}
|
|
4390
|
+
});
|
|
4391
|
+
class Leanbase extends PostHogCore {
|
|
4392
|
+
constructor(token, config) {
|
|
4393
|
+
const mergedConfig = extend(defaultConfig(), config || {}, {
|
|
4394
|
+
token
|
|
4395
|
+
});
|
|
4396
|
+
super(token, mergedConfig);
|
|
4397
|
+
this.personProcessingSetOncePropertiesSent = false;
|
|
4398
|
+
this.isLoaded = false;
|
|
4399
|
+
this.config = mergedConfig;
|
|
4400
|
+
this.visibilityStateListener = null;
|
|
4401
|
+
this.initialPageviewCaptured = false;
|
|
4402
|
+
this.scrollManager = new ScrollManager(this);
|
|
4403
|
+
this.pageViewManager = new PageViewManager(this);
|
|
4404
|
+
this.init(token, mergedConfig);
|
|
4405
|
+
}
|
|
4406
|
+
init(token, config) {
|
|
4407
|
+
this.setConfig(extend(defaultConfig(), config, {
|
|
4408
|
+
token
|
|
4409
|
+
}));
|
|
4410
|
+
this.isLoaded = true;
|
|
4411
|
+
this.persistence = new LeanbasePersistence(this.config);
|
|
4412
|
+
this.replayAutocapture = new Autocapture(this);
|
|
4413
|
+
this.replayAutocapture.startIfEnabled();
|
|
4414
|
+
if (this.config.preloadFeatureFlags !== false) {
|
|
4415
|
+
this.reloadFeatureFlags();
|
|
4416
|
+
}
|
|
4417
|
+
this.config.loaded?.(this);
|
|
4418
|
+
if (this.config.capture_pageview) {
|
|
4419
|
+
setTimeout(() => {
|
|
4420
|
+
if (this.config.cookieless_mode === 'always') {
|
|
4421
|
+
this.captureInitialPageview();
|
|
4422
|
+
}
|
|
4423
|
+
}, 1);
|
|
4424
|
+
}
|
|
4425
|
+
addEventListener(document, 'DOMContentLoaded', () => {
|
|
4426
|
+
this.loadRemoteConfig();
|
|
4427
|
+
});
|
|
4428
|
+
addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
|
|
4429
|
+
passive: false
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
captureInitialPageview() {
|
|
4433
|
+
if (!document) {
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
if (document.visibilityState !== 'visible') {
|
|
4437
|
+
if (!this.visibilityStateListener) {
|
|
4438
|
+
this.visibilityStateListener = this.captureInitialPageview.bind(this);
|
|
4439
|
+
addEventListener(document, 'visibilitychange', this.visibilityStateListener);
|
|
4440
|
+
}
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
if (!this.initialPageviewCaptured) {
|
|
4444
|
+
this.initialPageviewCaptured = true;
|
|
4445
|
+
this.capture('$pageview', {
|
|
4446
|
+
title: document.title
|
|
4447
|
+
});
|
|
4448
|
+
if (this.visibilityStateListener) {
|
|
4449
|
+
document.removeEventListener('visibilitychange', this.visibilityStateListener);
|
|
4450
|
+
this.visibilityStateListener = null;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
capturePageLeave() {
|
|
4455
|
+
const {
|
|
4456
|
+
capture_pageleave,
|
|
4457
|
+
capture_pageview
|
|
4458
|
+
} = this.config;
|
|
4459
|
+
if (capture_pageleave === true || capture_pageleave === 'if_capture_pageview' && (capture_pageview === true || capture_pageview === 'history_change')) {
|
|
4460
|
+
this.capture('$pageleave');
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
async loadRemoteConfig() {
|
|
4464
|
+
if (!this.isRemoteConfigLoaded) {
|
|
4465
|
+
const remoteConfig = await this.reloadRemoteConfigAsync();
|
|
4466
|
+
if (remoteConfig) {
|
|
4467
|
+
this.onRemoteConfig(remoteConfig);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
onRemoteConfig(config) {
|
|
4472
|
+
if (!(document && document.body)) {
|
|
4473
|
+
setTimeout(() => {
|
|
4474
|
+
this.onRemoteConfig(config);
|
|
4475
|
+
}, 500);
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
this.isRemoteConfigLoaded = true;
|
|
4479
|
+
this.replayAutocapture?.onRemoteConfig(config);
|
|
4480
|
+
}
|
|
4481
|
+
fetch(url, options) {
|
|
4482
|
+
const fetchFn = getFetch();
|
|
4483
|
+
if (!fetchFn) {
|
|
4484
|
+
return Promise.reject(new Error('Fetch API is not available in this environment.'));
|
|
4485
|
+
}
|
|
4486
|
+
return fetchFn(url, options);
|
|
4487
|
+
}
|
|
4488
|
+
setConfig(config) {
|
|
4489
|
+
const oldConfig = {
|
|
4490
|
+
...this.config
|
|
4491
|
+
};
|
|
4492
|
+
if (isObject(config)) {
|
|
4493
|
+
extend(this.config, config);
|
|
4494
|
+
this.persistence?.update_config(this.config, oldConfig);
|
|
4495
|
+
this.replayAutocapture?.startIfEnabled();
|
|
4496
|
+
}
|
|
4497
|
+
const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory';
|
|
4498
|
+
this.sessionPersistence = isTempStorage ? this.persistence : new LeanbasePersistence({
|
|
4499
|
+
...this.config,
|
|
4500
|
+
persistence: 'sessionStorage'
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
getLibraryId() {
|
|
4504
|
+
return 'leanbase';
|
|
4505
|
+
}
|
|
4506
|
+
getLibraryVersion() {
|
|
4507
|
+
return Config.LIB_VERSION;
|
|
4508
|
+
}
|
|
4509
|
+
getCustomUserAgent() {
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
getPersistedProperty(key) {
|
|
4513
|
+
return this.persistence?.get_property(key);
|
|
4514
|
+
}
|
|
4515
|
+
setPersistedProperty(key, value) {
|
|
4516
|
+
this.persistence?.set_property(key, value);
|
|
4517
|
+
}
|
|
4518
|
+
calculateEventProperties(eventName, eventProperties, timestamp, uuid, readOnly) {
|
|
4519
|
+
if (!this.persistence || !this.sessionPersistence) {
|
|
4520
|
+
return eventProperties;
|
|
4521
|
+
}
|
|
4522
|
+
timestamp = timestamp || new Date();
|
|
4523
|
+
const startTimestamp = readOnly ? undefined : this.persistence?.remove_event_timer(eventName);
|
|
4524
|
+
let properties = {
|
|
4525
|
+
...eventProperties
|
|
4526
|
+
};
|
|
4527
|
+
properties['token'] = this.config.token;
|
|
4528
|
+
if (this.config.cookieless_mode == 'always' || this.config.cookieless_mode == 'on_reject') {
|
|
4529
|
+
properties[COOKIELESS_MODE_FLAG_PROPERTY] = true;
|
|
4530
|
+
}
|
|
4531
|
+
if (eventName === '$snapshot') {
|
|
4532
|
+
const persistenceProps = {
|
|
4533
|
+
...this.persistence.properties()
|
|
4534
|
+
};
|
|
4535
|
+
properties['distinct_id'] = persistenceProps.distinct_id;
|
|
4536
|
+
if (!(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id'])) {
|
|
4537
|
+
logger.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
|
|
4538
|
+
}
|
|
4539
|
+
return properties;
|
|
4540
|
+
}
|
|
4541
|
+
const infoProperties = getEventProperties(this.config.mask_personal_data_properties, this.config.custom_personal_data_properties);
|
|
4542
|
+
if (this.sessionManager) {
|
|
4543
|
+
const {
|
|
4544
|
+
sessionId,
|
|
4545
|
+
windowId
|
|
4546
|
+
} = this.sessionManager.checkAndGetSessionAndWindowId(readOnly, timestamp.getTime());
|
|
4547
|
+
properties['$session_id'] = sessionId;
|
|
4548
|
+
properties['$window_id'] = windowId;
|
|
4549
|
+
}
|
|
4550
|
+
if (this.sessionPropsManager) {
|
|
4551
|
+
extend(properties, this.sessionPropsManager.getSessionProps());
|
|
4552
|
+
}
|
|
4553
|
+
let pageviewProperties = this.pageViewManager.doEvent();
|
|
4554
|
+
if (eventName === '$pageview' && !readOnly) {
|
|
4555
|
+
pageviewProperties = this.pageViewManager.doPageView(timestamp, uuid);
|
|
4556
|
+
}
|
|
4557
|
+
if (eventName === '$pageleave' && !readOnly) {
|
|
4558
|
+
pageviewProperties = this.pageViewManager.doPageLeave(timestamp);
|
|
4559
|
+
}
|
|
4560
|
+
properties = extend(properties, pageviewProperties);
|
|
4561
|
+
if (eventName === '$pageview' && document) {
|
|
4562
|
+
properties['title'] = document.title;
|
|
4563
|
+
}
|
|
4564
|
+
if (!isUndefined(startTimestamp)) {
|
|
4565
|
+
const duration_in_ms = timestamp.getTime() - startTimestamp;
|
|
4566
|
+
properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3));
|
|
4567
|
+
}
|
|
4568
|
+
if (userAgent && this.config.opt_out_useragent_filter) {
|
|
4569
|
+
properties['$browser_type'] = isLikelyBot(navigator$1, []) ? 'bot' : 'browser';
|
|
4570
|
+
}
|
|
4571
|
+
properties = extend({}, infoProperties, this.persistence.properties(), this.sessionPersistence.properties(), properties);
|
|
4572
|
+
properties['$is_identified'] = this.isIdentified();
|
|
4573
|
+
return properties;
|
|
4574
|
+
}
|
|
4575
|
+
isIdentified() {
|
|
4576
|
+
return this.persistence?.get_property(USER_STATE) === 'identified' || this.sessionPersistence?.get_property(USER_STATE) === 'identified';
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Add additional set_once properties to the event when creating a person profile. This allows us to create the
|
|
4580
|
+
* profile with mostly-accurate properties, despite earlier events not setting them. We do this by storing them in
|
|
4581
|
+
* persistence.
|
|
4582
|
+
* @param dataSetOnce
|
|
4583
|
+
*/
|
|
4584
|
+
calculateSetOnceProperties(dataSetOnce) {
|
|
4585
|
+
if (!this.persistence) {
|
|
4586
|
+
return dataSetOnce;
|
|
4587
|
+
}
|
|
4588
|
+
if (this.personProcessingSetOncePropertiesSent) {
|
|
4589
|
+
return dataSetOnce;
|
|
4590
|
+
}
|
|
4591
|
+
const initialProps = this.persistence.get_initial_props();
|
|
4592
|
+
const sessionProps = this.sessionPropsManager?.getSetOnceProps();
|
|
4593
|
+
const setOnceProperties = extend({}, initialProps, sessionProps || {}, dataSetOnce || {});
|
|
4594
|
+
this.personProcessingSetOncePropertiesSent = true;
|
|
4595
|
+
if (isEmptyObject(setOnceProperties)) {
|
|
4596
|
+
return undefined;
|
|
4597
|
+
}
|
|
4598
|
+
return setOnceProperties;
|
|
4599
|
+
}
|
|
4600
|
+
capture(event, properties, options) {
|
|
4601
|
+
if (!this.isLoaded || !this.sessionPersistence || !this.persistence) {
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
if (isUndefined(event) || !isString(event)) {
|
|
4605
|
+
logger.error('No event name provided to posthog.capture');
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
if (properties?.$current_url && !isString(properties?.$current_url)) {
|
|
4609
|
+
logger.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
|
|
4610
|
+
delete properties?.$current_url;
|
|
4611
|
+
}
|
|
4612
|
+
this.sessionPersistence.update_search_keyword();
|
|
4613
|
+
if (this.config.save_campaign_params) {
|
|
4614
|
+
this.sessionPersistence.update_campaign_params();
|
|
4615
|
+
}
|
|
4616
|
+
if (this.config.save_referrer) {
|
|
4617
|
+
this.sessionPersistence.update_referrer_info();
|
|
4618
|
+
}
|
|
4619
|
+
if (this.config.save_campaign_params || this.config.save_referrer) {
|
|
4620
|
+
this.persistence.set_initial_person_info();
|
|
4621
|
+
}
|
|
4622
|
+
const systemTime = new Date();
|
|
4623
|
+
const timestamp = options?.timestamp || systemTime;
|
|
4624
|
+
const uuid = uuidv7();
|
|
4625
|
+
let data = {
|
|
4626
|
+
uuid,
|
|
4627
|
+
event,
|
|
4628
|
+
properties: this.calculateEventProperties(event, properties || {}, timestamp, uuid)
|
|
4629
|
+
};
|
|
4630
|
+
const setProperties = options?.$set;
|
|
4631
|
+
if (setProperties) {
|
|
4632
|
+
data.$set = options?.$set;
|
|
4633
|
+
}
|
|
4634
|
+
const setOnceProperties = this.calculateSetOnceProperties(options?.$set_once);
|
|
4635
|
+
if (setOnceProperties) {
|
|
4636
|
+
data.$set_once = setOnceProperties;
|
|
4637
|
+
}
|
|
4638
|
+
data = copyAndTruncateStrings(data, options?._noTruncate ? null : this.config.properties_string_max_length);
|
|
4639
|
+
data.timestamp = timestamp;
|
|
4640
|
+
if (!isUndefined(options?.timestamp)) {
|
|
4641
|
+
data.properties['$event_time_override_provided'] = true;
|
|
4642
|
+
data.properties['$event_time_override_system_time'] = systemTime;
|
|
4643
|
+
}
|
|
4644
|
+
const finalSet = {
|
|
4645
|
+
...data.properties['$set'],
|
|
4646
|
+
...data['$set']
|
|
4647
|
+
};
|
|
4648
|
+
if (!isEmptyObject(finalSet)) {
|
|
4649
|
+
this.setPersonPropertiesForFlags(finalSet);
|
|
4650
|
+
}
|
|
4651
|
+
super.capture(data.event, data.properties, options);
|
|
4652
|
+
}
|
|
4653
|
+
identify(distinctId, properties, options) {
|
|
4654
|
+
super.identify(distinctId, properties, options);
|
|
4655
|
+
}
|
|
4656
|
+
destroy() {
|
|
4657
|
+
this.persistence?.clear();
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
const api = {
|
|
4662
|
+
_instance: null,
|
|
4663
|
+
_queue: [],
|
|
4664
|
+
init(apiKey, options) {
|
|
4665
|
+
this._instance = new Leanbase(apiKey, options);
|
|
4666
|
+
const q = this._queue;
|
|
4667
|
+
this._queue = [];
|
|
4668
|
+
for (const {
|
|
4669
|
+
fn,
|
|
4670
|
+
args
|
|
4671
|
+
} of q) {
|
|
4672
|
+
// @ts-expect-error dynamic dispatch to API methods
|
|
4673
|
+
this[fn](...args);
|
|
4674
|
+
}
|
|
4675
|
+
},
|
|
4676
|
+
capture(...args) {
|
|
4677
|
+
if (this._instance) {
|
|
4678
|
+
return this._instance.capture(...args);
|
|
4679
|
+
}
|
|
4680
|
+
this._queue.push({
|
|
4681
|
+
fn: 'capture',
|
|
4682
|
+
args
|
|
4683
|
+
});
|
|
4684
|
+
},
|
|
4685
|
+
captureException(...args) {
|
|
4686
|
+
if (this._instance) {
|
|
4687
|
+
return this._instance.captureException(...args);
|
|
4688
|
+
}
|
|
4689
|
+
this._queue.push({
|
|
4690
|
+
fn: 'captureException',
|
|
4691
|
+
args
|
|
4692
|
+
});
|
|
4693
|
+
},
|
|
4694
|
+
identify(...args) {
|
|
4695
|
+
if (this._instance) {
|
|
4696
|
+
return this._instance.identify(...args);
|
|
4697
|
+
}
|
|
4698
|
+
this._queue.push({
|
|
4699
|
+
fn: 'identify',
|
|
4700
|
+
args
|
|
4701
|
+
});
|
|
4702
|
+
},
|
|
4703
|
+
group(...args) {
|
|
4704
|
+
if (this._instance) {
|
|
4705
|
+
return this._instance.group(...args);
|
|
4706
|
+
}
|
|
4707
|
+
this._queue.push({
|
|
4708
|
+
fn: 'group',
|
|
4709
|
+
args
|
|
4710
|
+
});
|
|
4711
|
+
},
|
|
4712
|
+
alias(...args) {
|
|
4713
|
+
if (this._instance) {
|
|
4714
|
+
return this._instance.alias(...args);
|
|
4715
|
+
}
|
|
4716
|
+
this._queue.push({
|
|
4717
|
+
fn: 'alias',
|
|
4718
|
+
args
|
|
4719
|
+
});
|
|
1763
4720
|
},
|
|
1764
4721
|
reset() {
|
|
1765
4722
|
this._instance?.reset();
|