PageSourceSearch

https://bigten.org/mp/lib.js

js bigten.org collected 2026-09-24 08:26:16 UTC 411,784 bytes, 10,458 lines download raw bytes

vendor: 411,784 bytes, lines 1-10458
1(function () {
2    'use strict';
3
4    var Config = {
5        DEBUG: false,
6        LIB_VERSION: '2.83.0'
7    };
8
9    // Window global names for async modules
10    var TARGETING_GLOBAL_NAME = '__mp_targeting';
11    var RECORDER_GLOBAL_NAME = '__mp_recorder';
12
13    // Constants that are injected at build-time for the names of async modules.
14    var RECORDER_FILENAME = 'mixpanel-recorder-CmbaTnek.js';
15    var TARGETING_FILENAME = 'mixpanel-targeting-pUqZ3qpG.js';
16
17    // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
18    var win;
19    if (typeof(window) === 'undefined') {
20        var loc = {
21            hostname: ''
22        };
23        win = {
24            crypto: {randomUUID: function() {throw Error('unsupported');}},
25            navigator: { userAgent: '', onLine: true },
26            document: {
27                createElement: function() { return {}; },
28                location: loc,
29                referrer: ''
30            },
31            screen: { width: 0, height: 0 },
32            location: loc,
33            addEventListener: function() {},
34            removeEventListener: function() {},
35            dispatchEvent: function() {},
36            CustomEvent: function () {}
37        };
38    } else {
39        win = window;
40    }
41
42    var setImmediate = win['setImmediate'];
43    var builtInProp, cycle, schedulingQueue,
44        ToString = Object.prototype.toString,
45        timer = (typeof setImmediate !== 'undefined') ?
46            function timer(fn) { return setImmediate(fn); } :
47            setTimeout;
48
49    // dammit, IE8.
50    try {
51        Object.defineProperty({},'x',{});
52        builtInProp = function builtInProp(obj,name,val,config) {
53            return Object.defineProperty(obj,name,{
54                value: val,
55                writable: true,
56                configurable: config !== false
57            });
58        };
59    }
60    catch (err) {
61        builtInProp = function builtInProp(obj,name,val) {
62            obj[name] = val;
63            return obj;
64        };
65    }
66
67    // Note: using a queue instead of array for efficiency
68    schedulingQueue = (function Queue() {
69        var first, last, item;
70
71        function Item(fn,self) {
72            this.fn = fn;
73            this.self = self;
74            this.next = void 0;
75        }
76
77        return {
78            add: function add(fn,self) {
79                item = new Item(fn,self);
80                if (last) {
81                    last.next = item;
82                }
83                else {
84                    first = item;
85                }
86                last = item;
87                item = void 0;
88            },
89            drain: function drain() {
90                var f = first;
91                first = last = cycle = void 0;
92
93                while (f) {
94                    f.fn.call(f.self);
95                    f = f.next;
96                }
97            }
98        };
99    })();
100
101    function schedule(fn,self) {
102        schedulingQueue.add(fn,self);
103        if (!cycle) {
104            cycle = timer(schedulingQueue.drain);
105        }
106    }
107
108    // promise duck typing
109    function isThenable(o) {
110        var _then, oType = typeof o;
111
112        if (o !== null && (oType === 'object' || oType === 'function')) {
113            _then = o.then;
114        }
115        return typeof _then === 'function' ? _then : false;
116    }
117
118    function notify() {
119        for (var i=0; i<this.chain.length; i++) {
120            notifyIsolated(
121                this,
122                (this.state === 1) ? this.chain[i].success : this.chain[i].failure,
123                this.chain[i]
124            );
125        }
126        this.chain.length = 0;
127    }
128
129    // NOTE: This is a separate function to isolate
130    // the `try..catch` so that other code can be
131    // optimized better
132    function notifyIsolated(self,cb,chain) {
133        var ret, _then;
134        try {
135            if (cb === false) {
136                chain.reject(self.msg);
137            }
138            else {
139                if (cb === true) {
140                    ret = self.msg;
141                }
142                else {
143                    ret = cb.call(void 0,self.msg);
144                }
145
146                if (ret === chain.promise) {
147                    chain.reject(TypeError('Promise-chain cycle'));
148                }
149                // eslint-disable-next-line no-cond-assign
150                else if (_then = isThenable(ret)) {
151                    _then.call(ret,chain.resolve,chain.reject);
152                }
153                else {
154                    chain.resolve(ret);
155                }
156            }
157        }
158        catch (err) {
159            chain.reject(err);
160        }
161    }
162
163    function resolve(msg) {
164        var _then, self = this;
165
166        // already triggered?
167        if (self.triggered) { return; }
168
169        self.triggered = true;
170
171        // unwrap
172        if (self.def) {
173            self = self.def;
174        }
175
176        try {
177            // eslint-disable-next-line no-cond-assign
178            if (_then = isThenable(msg)) {
179                schedule(function(){
180                    var defWrapper = new MakeDefWrapper(self);
181                    try {
182                        _then.call(msg,
183                            function $resolve$(){ resolve.apply(defWrapper,arguments); },
184                            function $reject$(){ reject.apply(defWrapper,arguments); }
185                        );
186                    }
187                    catch (err) {
188                        reject.call(defWrapper,err);
189                    }
190                });
191            }
192            else {
193                self.msg = msg;
194                self.state = 1;
195                if (self.chain.length > 0) {
196                    schedule(notify,self);
197                }
198            }
199        }
200        catch (err) {
201            reject.call(new MakeDefWrapper(self),err);
202        }
203    }
204
205    function reject(msg) {
206        var self = this;
207
208        // already triggered?
209        if (self.triggered) { return; }
210
211        self.triggered = true;
212
213        // unwrap
214        if (self.def) {
215            self = self.def;
216        }
217
218        self.msg = msg;
219        self.state = 2;
220        if (self.chain.length > 0) {
221            schedule(notify,self);
222        }
223    }
224
225    function iteratePromises(Constructor,arr,resolver,rejecter) {
226        for (var idx=0; idx<arr.length; idx++) {
227            (function IIFE(idx){
228                Constructor.resolve(arr[idx])
229                    .then(
230                        function $resolver$(msg){
231                            resolver(idx,msg);
232                        },
233                        rejecter
234                    );
235            })(idx);
236        }
237    }
238
239    function MakeDefWrapper(self) {
240        this.def = self;
241        this.triggered = false;
242    }
243
244    function MakeDef(self) {
245        this.promise = self;
246        this.state = 0;
247        this.triggered = false;
248        this.chain = [];
249        this.msg = void 0;
250    }
251
252    function NpoPromise(executor) {
253        if (typeof executor !== 'function') {
254            throw TypeError('Not a function');
255        }
256
257        if (this['__NPO__'] !== 0) {
258            throw TypeError('Not a promise');
259        }
260
261        // instance shadowing the inherited "brand"
262        // to signal an already "initialized" promise
263        this['__NPO__'] = 1;
264
265        var def = new MakeDef(this);
266
267        this['then'] = function then(success,failure) {
268            var o = {
269                success: typeof success === 'function' ? success : true,
270                failure: typeof failure === 'function' ? failure : false
271            };
272                // Note: `then(..)` itself can be borrowed to be used against
273                // a different promise constructor for making the chained promise,
274                // by substituting a different `this` binding.
275            o.promise = new this.constructor(function extractChain(resolve,reject) {
276                if (typeof resolve !== 'function' || typeof reject !== 'function') {
277                    throw TypeError('Not a function');
278                }
279
280                o.resolve = resolve;
281                o.reject = reject;
282            });
283            def.chain.push(o);
284
285            if (def.state !== 0) {
286                schedule(notify,def);
287            }
288
289            return o.promise;
290        };
291        this['catch'] = function $catch$(failure) {
292            return this.then(void 0,failure);
293        };
294
295        try {
296            executor.call(
297                void 0,
298                function publicResolve(msg){
299                    resolve.call(def,msg);
300                },
301                function publicReject(msg) {
302                    reject.call(def,msg);
303                }
304            );
305        }
306        catch (err) {
307            reject.call(def,err);
308        }
309    }
310
311    var PromisePrototype = builtInProp({},'constructor',NpoPromise,
312        /*configurable=*/false
313    );
314
315        // Note: Android 4 cannot use `Object.defineProperty(..)` here
316    NpoPromise.prototype = PromisePrototype;
317
318    // built-in "brand" to signal an "uninitialized" promise
319    builtInProp(PromisePrototype,'__NPO__',0,
320        /*configurable=*/false
321    );
322
323    builtInProp(NpoPromise,'resolve',function Promise$resolve(msg) {
324        var Constructor = this;
325
326        // spec mandated checks
327        // note: best "isPromise" check that's practical for now
328        if (msg && typeof msg === 'object' && msg['__NPO__'] === 1) {
329            return msg;
330        }
331
332        return new Constructor(function executor(resolve,reject){
333            if (typeof resolve !== 'function' || typeof reject !== 'function') {
334                throw TypeError('Not a function');
335            }
336
337            resolve(msg);
338        });
339    });
340
341    builtInProp(NpoPromise,'reject',function Promise$reject(msg) {
342        return new this(function executor(resolve,reject){
343            if (typeof resolve !== 'function' || typeof reject !== 'function') {
344                throw TypeError('Not a function');
345            }
346
347            reject(msg);
348        });
349    });
350
351    builtInProp(NpoPromise,'all',function Promise$all(arr) {
352        var Constructor = this;
353
354        // spec mandated checks
355        if (ToString.call(arr) !== '[object Array]') {
356            return Constructor.reject(TypeError('Not an array'));
357        }
358        if (arr.length === 0) {
359            return Constructor.resolve([]);
360        }
361
362        return new Constructor(function executor(resolve,reject){
363            if (typeof resolve !== 'function' || typeof reject !== 'function') {
364                throw TypeError('Not a function');
365            }
366
367            var len = arr.length, msgs = Array(len), count = 0;
368
369            iteratePromises(Constructor,arr,function resolver(idx,msg) {
370                msgs[idx] = msg;
371                if (++count === len) {
372                    resolve(msgs);
373                }
374            },reject);
375        });
376    });
377
378    builtInProp(NpoPromise,'race',function Promise$race(arr) {
379        var Constructor = this;
380
381        // spec mandated checks
382        if (ToString.call(arr) !== '[object Array]') {
383            return Constructor.reject(TypeError('Not an array'));
384        }
385
386        return new Constructor(function executor(resolve,reject){
387            if (typeof resolve !== 'function' || typeof reject !== 'function') {
388                throw TypeError('Not a function');
389            }
390
391            iteratePromises(Constructor,arr,function resolver(idx,msg){
392                resolve(msg);
393            },reject);
394        });
395    });
396
397    var PromisePolyfill;
398    if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) {
399        PromisePolyfill = Promise;
400    } else {
401        PromisePolyfill = NpoPromise;
402    }
403
404    /* eslint camelcase: "off", eqeqeq: "off" */
405
406    // Maximum allowed session recording length
407    var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
408
409    /*
410     * Saved references to long variable names, so that closure compiler can
411     * minimize file size.
412     */
413
414    var ArrayProto = Array.prototype,
415        FuncProto = Function.prototype,
416        ObjProto = Object.prototype,
417        slice = ArrayProto.slice,
418        toString = ObjProto.toString,
419        hasOwnProperty = ObjProto.hasOwnProperty,
420        windowConsole = win.console,
421        navigator = win.navigator,
422        document$1 = win.document,
423        windowOpera = win.opera,
424        screen = win.screen,
425        userAgent = navigator.userAgent;
426
427    var nativeBind = FuncProto.bind,
428        nativeForEach = ArrayProto.forEach,
429        nativeIndexOf = ArrayProto.indexOf,
430        nativeMap = ArrayProto.map,
431        nativeIsArray = Array.isArray,
432        breaker = {};
433
434    var _ = {};
435
436    // Console override
437    var console = {
438        /** @type {function(...*)} */
439        log: function() {
440            if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
441                try {
442                    windowConsole.log.apply(windowConsole, arguments);
443                } catch (err) {
444                    _.each(arguments, function(arg) {
445                        windowConsole.log(arg);
446                    });
447                }
448            }
449        },
450        /** @type {function(...*)} */
451        warn: function() {
452            if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
453                var args = ['Mixpanel warning:'].concat(_.toArray(arguments));
454                try {
455                    windowConsole.warn.apply(windowConsole, args);
456                } catch (err) {
457                    _.each(args, function(arg) {
458                        windowConsole.warn(arg);
459                    });
460                }
461            }
462        },
463        /** @type {function(...*)} */
464        error: function() {
465            if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
466                var args = ['Mixpanel error:'].concat(_.toArray(arguments));
467                try {
468                    windowConsole.error.apply(windowConsole, args);
469                } catch (err) {
470                    _.each(args, function(arg) {
471                        windowConsole.error(arg);
472                    });
473                }
474            }
475        },
476        /** @type {function(...*)} */
477        critical: function() {
478            if (!_.isUndefined(windowConsole) && windowConsole) {
479                var args = ['Mixpanel error:'].concat(_.toArray(arguments));
480                try {
481                    windowConsole.error.apply(windowConsole, args);
482                } catch (err) {
483                    _.each(args, function(arg) {
484                        windowConsole.error(arg);
485                    });
486                }
487            }
488        }
489    };
490
491    var log_func_with_prefix = function(func, prefix) {
492        return function() {
493            arguments[0] = '[' + prefix + '] ' + arguments[0];
494            return func.apply(console, arguments);
495        };
496    };
497    var console_with_prefix = function(prefix) {
498        return {
499            log: log_func_with_prefix(console.log, prefix),
500            warn: log_func_with_prefix(console.warn, prefix),
501            error: log_func_with_prefix(console.error, prefix),
502            critical: log_func_with_prefix(console.critical, prefix)
503        };
504    };
505
506
507    var safewrap = function(f) {
508        return function() {
509            try {
510                return f.apply(this, arguments);
511            } catch (e) {
512                console.critical('Implementation error. Please turn on debug and contact [email protected].');
513                if (Config.DEBUG){
514                    console.critical(e);
515                }
516            }
517        };
518    };
519
520    var safewrapClass = function(klass) {
521        var proto = klass.prototype;
522        for (var func in proto) {
523            if (typeof(proto[func]) === 'function') {
524                proto[func] = safewrap(proto[func]);
525            }
526        }
527    };
528
529
530    // UNDERSCORE
531    // Embed part of the Underscore Library
532    _.bind = function(func, context) {
533        var args, bound;
534        if (nativeBind && func.bind === nativeBind) {
535            return nativeBind.apply(func, slice.call(arguments, 1));
536        }
537        if (!_.isFunction(func)) {
538            throw new TypeError();
539        }
540        args = slice.call(arguments, 2);
541        bound = function() {
542            if (!(this instanceof bound)) {
543                return func.apply(context, args.concat(slice.call(arguments)));
544            }
545            var ctor = {};
546            ctor.prototype = func.prototype;
547            var self = new ctor();
548            ctor.prototype = null;
549            var result = func.apply(self, args.concat(slice.call(arguments)));
550            if (Object(result) === result) {
551                return result;
552            }
553            return self;
554        };
555        return bound;
556    };
557
558    /**
559     * @param {*=} obj
560     * @param {function(...*)=} iterator
561     * @param {Object=} context
562     */
563    _.each = function(obj, iterator, context) {
564        if (obj === null || obj === undefined) {
565            return;
566        }
567        if (nativeForEach && obj.forEach === nativeForEach) {
568            obj.forEach(iterator, context);
569        } else if (obj.length === +obj.length) {
570            for (var i = 0, l = obj.length; i < l; i++) {
571                if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) {
572                    return;
573                }
574            }
575        } else {
576            for (var key in obj) {
577                if (hasOwnProperty.call(obj, key)) {
578                    if (iterator.call(context, obj[key], key, obj) === breaker) {
579                        return;
580                    }
581                }
582            }
583        }
584    };
585
586    _.extend = function(obj) {
587        _.each(slice.call(arguments, 1), function(source) {
588            for (var prop in source) {
589                if (source[prop] !== void 0) {
590                    obj[prop] = source[prop];
591                }
592            }
593        });
594        return obj;
595    };
596
597    _.isArray = nativeIsArray || function(obj) {
598        return toString.call(obj) === '[object Array]';
599    };
600
601    _.isFunction = function(f) {
602        return typeof f === 'function';
603    };
604
605    _.isArguments = function(obj) {
606        return !!(obj && hasOwnProperty.call(obj, 'callee'));
607    };
608
609    _.toArray = function(iterable) {
610        if (!iterable) {
611            return [];
612        }
613        if (iterable.toArray) {
614            return iterable.toArray();
615        }
616        if (_.isArray(iterable)) {
617            return slice.call(iterable);
618        }
619        if (_.isArguments(iterable)) {
620            return slice.call(iterable);
621        }
622        return _.values(iterable);
623    };
624
625    _.map = function(arr, callback, context) {
626        if (nativeMap && arr.map === nativeMap) {
627            return arr.map(callback, context);
628        } else {
629            var results = [];
630            _.each(arr, function(item) {
631                results.push(callback.call(context, item));
632            });
633            return results;
634        }
635    };
636
637    _.keys = function(obj) {
638        var results = [];
639        if (obj === null) {
640            return results;
641        }
642        _.each(obj, function(value, key) {
643            results[results.length] = key;
644        });
645        return results;
646    };
647
648    _.values = function(obj) {
649        var results = [];
650        if (obj === null) {
651            return results;
652        }
653        _.each(obj, function(value) {
654            results[results.length] = value;
655        });
656        return results;
657    };
658
659    _.include = function(obj, target) {
660        var found = false;
661        if (obj === null) {
662            return found;
663        }
664        if (nativeIndexOf && obj.indexOf === nativeIndexOf) {
665            return obj.indexOf(target) != -1;
666        }
667        _.each(obj, function(value) {
668            if (found || (found = (value === target))) {
669                return breaker;
670            }
671        });
672        return found;
673    };
674
675    _.includes = function(str, needle) {
676        return str.indexOf(needle) !== -1;
677    };
678
679    // Underscore Addons
680    _.inherit = function(subclass, superclass) {
681        subclass.prototype = new superclass();
682        subclass.prototype.constructor = subclass;
683        subclass.superclass = superclass.prototype;
684        return subclass;
685    };
686
687    _.isObject = function(obj) {
688        return (obj === Object(obj) && !_.isArray(obj));
689    };
690
691    _.isEmptyObject = function(obj) {
692        if (_.isObject(obj)) {
693            for (var key in obj) {
694                if (hasOwnProperty.call(obj, key)) {
695                    return false;
696                }
697            }
698            return true;
699        }
700        return false;
701    };
702
703    _.isUndefined = function(obj) {
704        return obj === void 0;
705    };
706
707    _.isString = function(obj) {
708        return toString.call(obj) == '[object String]';
709    };
710
711    _.isDate = function(obj) {
712        return toString.call(obj) == '[object Date]';
713    };
714
715    _.isNumber = function(obj) {
716        return toString.call(obj) == '[object Number]';
717    };
718
719    _.isElement = function(obj) {
720        return !!(obj && obj.nodeType === 1);
721    };
722
723    _.encodeDates = function(obj) {
724        _.each(obj, function(v, k) {
725            if (_.isDate(v)) {
726                obj[k] = _.formatDate(v);
727            } else if (_.isObject(v)) {
728                obj[k] = _.encodeDates(v); // recurse
729            }
730        });
731        return obj;
732    };
733
734    _.timestamp = function() {
735        Date.now = Date.now || function() {
736            return +new Date;
737        };
738        return Date.now();
739    };
740
741    _.formatDate = function(d) {
742        // YYYY-MM-DDTHH:MM:SS in UTC
743        function pad(n) {
744            return n < 10 ? '0' + n : n;
745        }
746        return d.getUTCFullYear() + '-' +
747            pad(d.getUTCMonth() + 1) + '-' +
748            pad(d.getUTCDate()) + 'T' +
749            pad(d.getUTCHours()) + ':' +
750            pad(d.getUTCMinutes()) + ':' +
751            pad(d.getUTCSeconds());
752    };
753
754    _.strip_empty_properties = function(p) {
755        var ret = {};
756        _.each(p, function(v, k) {
757            if (_.isString(v) && v.length > 0) {
758                ret[k] = v;
759            }
760        });
761        return ret;
762    };
763
764    /*
765     * this function returns a copy of object after truncating it.  If
766     * passed an Array or Object it will iterate through obj and
767     * truncate all the values recursively.
768     */
769    _.truncate = function(obj, length) {
770        var ret;
771
772        if (typeof(obj) === 'string') {
773            ret = obj.slice(0, length);
774        } else if (_.isArray(obj)) {
775            ret = [];
776            _.each(obj, function(val) {
777                ret.push(_.truncate(val, length));
778            });
779        } else if (_.isObject(obj)) {
780            ret = {};
781            _.each(obj, function(val, key) {
782                ret[key] = _.truncate(val, length);
783            });
784        } else {
785            ret = obj;
786        }
787
788        return ret;
789    };
790
791    _.JSONEncode = (function() {
792        return function(mixed_val) {
793            var value = mixed_val;
794            var quote = function(string) {
795                var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex
796                var meta = { // table of character substitutions
797                    '\b': '\\b',
798                    '\t': '\\t',
799                    '\n': '\\n',
800                    '\f': '\\f',
801                    '\r': '\\r',
802                    '"': '\\"',
803                    '\\': '\\\\'
804                };
805
806                escapable.lastIndex = 0;
807                return escapable.test(string) ?
808                    '"' + string.replace(escapable, function(a) {
809                        var c = meta[a];
810                        return typeof c === 'string' ? c :
811                            '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
812                    }) + '"' :
813                    '"' + string + '"';
814            };
815
816            var str = function(key, holder) {
817                var gap = '';
818                var indent = '    ';
819                var i = 0; // The loop counter.
820                var k = ''; // The member key.
821                var v = ''; // The member value.
822                var length = 0;
823                var mind = gap;
824                var partial = [];
825                var value = holder[key];
826
827                // If the value has a toJSON method, call it to obtain a replacement value.
828                if (value && typeof value === 'object' &&
829                    typeof value.toJSON === 'function') {
830                    value = value.toJSON(key);
831                }
832
833                // What happens next depends on the value's type.
834                switch (typeof value) {
835                    case 'string':
836                        return quote(value);
837
838                    case 'number':
839                        // JSON numbers must be finite. Encode non-finite numbers as null.
840                        return isFinite(value) ? String(value) : 'null';
841
842                    case 'boolean':
843                    case 'null':
844                        // If the value is a boolean or null, convert it to a string. Note:
845                        // typeof null does not produce 'null'. The case is included here in
846                        // the remote chance that this gets fixed someday.
847
848                        return String(value);
849
850                    case 'object':
851                        // If the type is 'object', we might be dealing with an object or an array or
852                        // null.
853                        // Due to a specification blunder in ECMAScript, typeof null is 'object',
854                        // so watch out for that case.
855                        if (!value) {
856                            return 'null';
857                        }
858
859                        // Make an array to hold the partial results of stringifying this object value.
860                        gap += indent;
861                        partial = [];
862
863                        // Is the value an array?
864                        if (toString.apply(value) === '[object Array]') {
865                            // The value is an array. Stringify every element. Use null as a placeholder
866                            // for non-JSON values.
867
868                            length = value.length;
869                            for (i = 0; i < length; i += 1) {
870                                partial[i] = str(i, value) || 'null';
871                            }
872
873                            // Join all of the elements together, separated with commas, and wrap them in
874                            // brackets.
875                            v = partial.length === 0 ? '[]' :
876                                gap ? '[\n' + gap +
877                                partial.join(',\n' + gap) + '\n' +
878                                mind + ']' :
879                                    '[' + partial.join(',') + ']';
880                            gap = mind;
881                            return v;
882                        }
883
884                        // Iterate through all of the keys in the object.
885                        for (k in value) {
886                            if (hasOwnProperty.call(value, k)) {
887                                v = str(k, value);
888                                if (v) {
889                                    partial.push(quote(k) + (gap ? ': ' : ':') + v);
890                                }
891                            }
892                        }
893
894                        // Join all of the member texts together, separated with commas,
895                        // and wrap them in braces.
896                        v = partial.length === 0 ? '{}' :
897                            gap ? '{' + partial.join(',') + '' +
898                            mind + '}' : '{' + partial.join(',') + '}';
899                        gap = mind;
900                        return v;
901                }
902            };
903
904            // Make a fake root object containing our value under the key of ''.
905            // Return the result of stringifying the value.
906            return str('', {
907                '': value
908            });
909        };
910    })();
911
912    /**
913     * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
914     * Slightly modified to throw a real Error rather than a POJO
915     */
916    _.JSONDecode = (function() {
917        var at, // The index of the current character
918            ch, // The current character
919            escapee = {
920                '"': '"',
921                '\\': '\\',
922                '/': '/',
923                'b': '\b',
924                'f': '\f',
925                'n': '\n',
926                'r': '\r',
927                't': '\t'
928            },
929            text,
930            error = function(m) {
931                var e = new SyntaxError(m);
932                e.at = at;
933                e.text = text;
934                throw e;
935            },
936            next = function(c) {
937                // If a c parameter is provided, verify that it matches the current character.
938                if (c && c !== ch) {
939                    error('Expected \'' + c + '\' instead of \'' + ch + '\'');
940                }
941                // Get the next character. When there are no more characters,
942                // return the empty string.
943                ch = text.charAt(at);
944                at += 1;
945                return ch;
946            },
947            number = function() {
948                // Parse a number value.
949                var number,
950                    string = '';
951
952                if (ch === '-') {
953                    string = '-';
954                    next('-');
955                }
956                while (ch >= '0' && ch <= '9') {
957                    string += ch;
958                    next();
959                }
960                if (ch === '.') {
961                    string += '.';
962                    while (next() && ch >= '0' && ch <= '9') {
963                        string += ch;
964                    }
965                }
966                if (ch === 'e' || ch === 'E') {
967                    string += ch;
968                    next();
969                    if (ch === '-' || ch === '+') {
970                        string += ch;
971                        next();
972                    }
973                    while (ch >= '0' && ch <= '9') {
974                        string += ch;
975                        next();
976                    }
977                }
978                number = +string;
979                if (!isFinite(number)) {
980                    error('Bad number');
981                } else {
982                    return number;
983                }
984            },
985
986            string = function() {
987                // Parse a string value.
988                var hex,
989                    i,
990                    string = '',
991                    uffff;
992                // When parsing for string values, we must look for " and \ characters.
993                if (ch === '"') {
994                    while (next()) {
995                        if (ch === '"') {
996                            next();
997                            return string;
998                        }
999                        if (ch === '\\') {
1000                            next();
1001                            if (ch === 'u') {
1002                                uffff = 0;
1003                                for (i = 0; i < 4; i += 1) {
1004                                    hex = parseInt(next(), 16);
1005                                    if (!isFinite(hex)) {
1006                                        break;
1007                                    }
1008                                    uffff = uffff * 16 + hex;
1009                                }
1010                                string += String.fromCharCode(uffff);
1011                            } else if (typeof escapee[ch] === 'string') {
1012                                string += escapee[ch];
1013                            } else {
1014                                break;
1015                            }
1016                        } else {
1017                            string += ch;
1018                        }
1019                    }
1020                }
1021                error('Bad string');
1022            },
1023            white = function() {
1024                // Skip whitespace.
1025                while (ch && ch <= ' ') {
1026                    next();
1027                }
1028            },
1029            word = function() {
1030                // true, false, or null.
1031                switch (ch) {
1032                    case 't':
1033                        next('t');
1034                        next('r');
1035                        next('u');
1036                        next('e');
1037                        return true;
1038                    case 'f':
1039                        next('f');
1040                        next('a');
1041                        next('l');
1042                        next('s');
1043                        next('e');
1044                        return false;
1045                    case 'n':
1046                        next('n');
1047                        next('u');
1048                        next('l');
1049                        next('l');
1050                        return null;
1051                }
1052                error('Unexpected "' + ch + '"');
1053            },
1054            value, // Placeholder for the value function.
1055            array = function() {
1056                // Parse an array value.
1057                var array = [];
1058
1059                if (ch === '[') {
1060                    next('[');
1061                    white();
1062                    if (ch === ']') {
1063                        next(']');
1064                        return array; // empty array
1065                    }
1066                    while (ch) {
1067                        array.push(value());
1068                        white();
1069                        if (ch === ']') {
1070                            next(']');
1071                            return array;
1072                        }
1073                        next(',');
1074                        white();
1075                    }
1076                }
1077                error('Bad array');
1078            },
1079            object = function() {
1080                // Parse an object value.
1081                var key,
1082                    object = {};
1083
1084                if (ch === '{') {
1085                    next('{');
1086                    white();
1087                    if (ch === '}') {
1088                        next('}');
1089                        return object; // empty object
1090                    }
1091                    while (ch) {
1092                        key = string();
1093                        white();
1094                        next(':');
1095                        if (Object.hasOwnProperty.call(object, key)) {
1096                            error('Duplicate key "' + key + '"');
1097                        }
1098                        object[key] = value();
1099                        white();
1100                        if (ch === '}') {
1101                            next('}');
1102                            return object;
1103                        }
1104                        next(',');
1105                        white();
1106                    }
1107                }
1108                error('Bad object');
1109            };
1110
1111        value = function() {
1112            // Parse a JSON value. It could be an object, an array, a string,
1113            // a number, or a word.
1114            white();
1115            switch (ch) {
1116                case '{':
1117                    return object();
1118                case '[':
1119                    return array();
1120                case '"':
1121                    return string();
1122                case '-':
1123                    return number();
1124                default:
1125                    return ch >= '0' && ch <= '9' ? number() : word();
1126            }
1127        };
1128
1129        // Return the json_parse function. It will have access to all of the
1130        // above functions and variables.
1131        return function(source) {
1132            var result;
1133
1134            text = source;
1135            at = 0;
1136            ch = ' ';
1137            result = value();
1138            white();
1139            if (ch) {
1140                error('Syntax error');
1141            }
1142
1143            return result;
1144        };
1145    })();
1146
1147    _.base64Encode = function(data) {
1148        var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
1149        var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
1150            ac = 0,
1151            enc = '',
1152            tmp_arr = [];
1153
1154        if (!data) {
1155            return data;
1156        }
1157
1158        data = _.utf8Encode(data);
1159
1160        do { // pack three octets into four hexets
1161            o1 = data.charCodeAt(i++);
1162            o2 = data.charCodeAt(i++);
1163            o3 = data.charCodeAt(i++);
1164
1165            bits = o1 << 16 | o2 << 8 | o3;
1166
1167            h1 = bits >> 18 & 0x3f;
1168            h2 = bits >> 12 & 0x3f;
1169            h3 = bits >> 6 & 0x3f;
1170            h4 = bits & 0x3f;
1171
1172            // use hexets to index into b64, and append result to encoded string
1173            tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
1174        } while (i < data.length);
1175
1176        enc = tmp_arr.join('');
1177
1178        switch (data.length % 3) {
1179            case 1:
1180                enc = enc.slice(0, -2) + '==';
1181                break;
1182            case 2:
1183                enc = enc.slice(0, -1) + '=';
1184                break;
1185        }
1186
1187        return enc;
1188    };
1189
1190    _.utf8Encode = function(string) {
1191        string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
1192
1193        var utftext = '',
1194            start,
1195            end;
1196        var stringl = 0,
1197            n;
1198
1199        start = end = 0;
1200        stringl = string.length;
1201
1202        for (n = 0; n < stringl; n++) {
1203            var c1 = string.charCodeAt(n);
1204            var enc = null;
1205
1206            if (c1 < 128) {
1207                end++;
1208            } else if ((c1 > 127) && (c1 < 2048)) {
1209                enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128);
1210            } else {
1211                enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128);
1212            }
1213            if (enc !== null) {
1214                if (end > start) {
1215                    utftext += string.substring(start, end);
1216                }
1217                utftext += enc;
1218                start = end = n + 1;
1219            }
1220        }
1221
1222        if (end > start) {
1223            utftext += string.substring(start, string.length);
1224        }
1225
1226        return utftext;
1227    };
1228
1229    _.UUID = function() {
1230        try {
1231            // use native Crypto API when available
1232            return win['crypto']['randomUUID']();
1233        } catch (err) {
1234            // fall back to generating our own UUID
1235            // based on https://gist.github.com/scwood/3bff42cc005cc20ab7ec98f0d8e1d59d
1236            var uuid = new Array(36);
1237            for (var i = 0; i < 36; i++) {
1238                uuid[i] = Math.floor(Math.random() * 16);
1239            }
1240            uuid[14] = 4; // set bits 12-15 of time-high-and-version to 0100
1241            uuid[19] = uuid[19] &= -5; // set bit 6 of clock-seq-and-reserved to zero
1242            uuid[19] = uuid[19] |= (1 << 3); // set bit 7 of clock-seq-and-reserved to one
1243            uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
1244
1245            return _.map(uuid, function(x) {
1246                return x.toString(16);
1247            }).join('');
1248        }
1249    };
1250
1251    // _.isBlockedUA()
1252    // This is to block various web spiders from executing our JS and
1253    // sending false tracking data
1254    var BLOCKED_UA_STRS = [
1255        'ahrefsbot',
1256        'ahrefssiteaudit',
1257        'amazonbot',
1258        'baiduspider',
1259        'bingbot',
1260        'bingpreview',
1261        'chrome-lighthouse',
1262        'facebookexternal',
1263        'petalbot',
1264        'pinterest',
1265        'screaming frog',
1266        'yahoo! slurp',
1267        'yandex',
1268
1269        // a whole bunch of goog-specific crawlers
1270        // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
1271        'adsbot-google',
1272        'apis-google',
1273        'duplexweb-google',
1274        'feedfetcher-google',
1275        'google favicon',
1276        'google web preview',
1277        'google-read-aloud',
1278        'googlebot',
1279        'googleweblight',
1280        'mediapartners-google',
1281        'storebot-google'
1282    ];
1283    _.isBlockedUA = function(ua) {
1284        var i;
1285        ua = ua.toLowerCase();
1286        for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
1287            if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
1288                return true;
1289            }
1290        }
1291        return false;
1292    };
1293
1294    /**
1295     * @param {Object=} formdata
1296     * @param {string=} arg_separator
1297     */
1298    _.HTTPBuildQuery = function(formdata, arg_separator) {
1299        var use_val, use_key, tmp_arr = [];
1300
1301        if (_.isUndefined(arg_separator)) {
1302            arg_separator = '&';
1303        }
1304
1305        _.each(formdata, function(val, key) {
1306            use_val = encodeURIComponent(val.toString());
1307            use_key = encodeURIComponent(key);
1308            tmp_arr[tmp_arr.length] = use_key + '=' + use_val;
1309        });
1310
1311        return tmp_arr.join(arg_separator);
1312    };
1313
1314    _.getQueryParam = function(url, param) {
1315        // Expects a raw URL
1316
1317        // Escape the RegExp metacharacters the param name may contain before
1318        // interpolating it into `regexS`. Backslash is escaped in the same pass as
1319        // the brackets (a single character class), so a param name is never able to
1320        // introduce an unintended escape sequence (CodeQL js/incomplete-sanitization).
1321        param = param.replace(/[[\]\\]/g, '\\$&');
1322        var regexS = '[\\?&]' + param + '=([^&#]*)',
1323            regex = new RegExp(regexS),
1324            results = regex.exec(url);
1325        if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) {
1326            return '';
1327        } else {
1328            var result = results[1];
1329            try {
1330                result = decodeURIComponent(result);
1331            } catch(err) {
1332                console.error('Skipping decoding for malformed query param: ' + result);
1333            }
1334            return result.replace(/\+/g, ' ');
1335        }
1336    };
1337
1338
1339    // _.cookie
1340    // Methods partially borrowed from quirksmode.org/js/cookies.html
1341    _.cookie = {
1342        get: function(name) {
1343            var nameEQ = name + '=';
1344            var ca = document$1.cookie.split(';');
1345            for (var i = 0; i < ca.length; i++) {
1346                var c = ca[i];
1347                while (c.charAt(0) == ' ') {
1348                    c = c.substring(1, c.length);
1349                }
1350                if (c.indexOf(nameEQ) === 0) {
1351                    return decodeURIComponent(c.substring(nameEQ.length, c.length));
1352                }
1353            }
1354            return null;
1355        },
1356
1357        parse: function(name) {
1358            var cookie;
1359            try {
1360                cookie = _.JSONDecode(_.cookie.get(name)) || {};
1361            } catch (err) {
1362                // noop
1363            }
1364            return cookie;
1365        },
1366
1367        set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) {
1368            var cdomain = '',
1369                expires = '',
1370                secure = '';
1371
1372            if (domain_override) {
1373                cdomain = '; domain=' + domain_override;
1374            } else if (is_cross_subdomain) {
1375                var domain = extract_domain(document$1.location.hostname);
1376                cdomain = domain ? '; domain=.' + domain : '';
1377            }
1378
1379            if (seconds) {
1380                var date = new Date();
1381                date.setTime(date.getTime() + (seconds * 1000));
1382                expires = '; expires=' + date.toGMTString();
1383            }
1384
1385            if (is_cross_site) {
1386                is_secure = true;
1387                secure = '; SameSite=None';
1388            }
1389            if (is_secure) {
1390                secure += '; secure';
1391            }
1392
1393            document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure;
1394        },
1395
1396        set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) {
1397            var cdomain = '', expires = '', secure = '';
1398
1399            if (domain_override) {
1400                cdomain = '; domain=' + domain_override;
1401            } else if (is_cross_subdomain) {
1402                var domain = extract_domain(document$1.location.hostname);
1403                cdomain = domain ? '; domain=.' + domain : '';
1404            }
1405
1406            if (days) {
1407                var date = new Date();
1408                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
1409                expires = '; expires=' + date.toGMTString();
1410            }
1411
1412            if (is_cross_site) {
1413                is_secure = true;
1414                secure = '; SameSite=None';
1415            }
1416            if (is_secure) {
1417                secure += '; secure';
1418            }
1419
1420            var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure;
1421            document$1.cookie = new_cookie_val;
1422            return new_cookie_val;
1423        },
1424
1425        remove: function(name, is_cross_subdomain, domain_override) {
1426            _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override);
1427        }
1428    };
1429
1430    var _testStorageSupported = function (storage) {
1431        var supported = true;
1432        try {
1433            var key = '__mplss_' + cheap_guid(8),
1434                val = 'xyz';
1435            storage.setItem(key, val);
1436            if (storage.getItem(key) !== val) {
1437                supported = false;
1438            }
1439            storage.removeItem(key);
1440        } catch (err) {
1441            supported = false;
1442        }
1443        return supported;
1444    };
1445
1446    var _localStorageSupported = null;
1447    var localStorageSupported = function(storage, forceCheck) {
1448        if (_localStorageSupported !== null && !forceCheck) {
1449            return _localStorageSupported;
1450        }
1451
1452        return _localStorageSupported = _testStorageSupported(storage);
1453    };
1454
1455    var _sessionStorageSupported = null;
1456    var sessionStorageSupported = function(storage, forceCheck) {
1457        if (_sessionStorageSupported !== null && !forceCheck) {
1458            return _sessionStorageSupported;
1459        }
1460
1461        return _sessionStorageSupported = _testStorageSupported(storage);
1462    };
1463
1464    function _storageWrapper(storage, name, is_supported_fn) {
1465        var log_error = function(msg) {
1466            console.error(name + ' error: ' + msg);
1467        };
1468
1469        return {
1470            is_supported: function(forceCheck) {
1471                var supported = is_supported_fn(storage, forceCheck);
1472                if (!supported) {
1473                    console.error(name + ' unsupported');
1474                }
1475                return supported;
1476            },
1477            error: log_error,
1478            get: function(key) {
1479                try {
1480                    return storage.getItem(key);
1481                } catch (err) {
1482                    log_error(err);
1483                }
1484                return null;
1485            },
1486            parse: function(key) {
1487                try {
1488                    return _.JSONDecode(storage.getItem(key)) || {};
1489                } catch (err) {
1490                    // noop
1491                }
1492                return null;
1493            },
1494            set: function(key, value) {
1495                try {
1496                    storage.setItem(key, value);
1497                } catch (err) {
1498                    log_error(err);
1499                }
1500            },
1501            remove: function(key) {
1502                try {
1503                    storage.removeItem(key);
1504                } catch (err) {
1505                    log_error(err);
1506                }
1507            }
1508        };
1509    }
1510
1511    // Safari and other browsers may error out accessing localStorage/sessionStorage
1512    // when cookies are disabled, so wrap access in a try-catch.
1513    var getLocalStorage = function() {
1514        try {
1515            return win.localStorage; // eslint-disable-line no-restricted-properties
1516        } catch (_err) {
1517            return null;
1518        }
1519    };
1520
1521    var getSessionStorage = function() {
1522        try {
1523            return win.sessionStorage; // eslint-disable-line no-restricted-properties
1524        } catch (_err) {
1525            return null;
1526        }
1527    };
1528
1529    _.localStorage = _storageWrapper(getLocalStorage(), 'localStorage', localStorageSupported);
1530    _.sessionStorage = _storageWrapper(getSessionStorage(), 'sessionStorage', sessionStorageSupported);
1531
1532    _.register_event = (function() {
1533        // written by Dean Edwards, 2005
1534        // with input from Tino Zijdel - [email protected]
1535        // with input from Carl Sverre - [email protected]
1536        // with input from Mixpanel
1537        // http://dean.edwards.name/weblog/2005/10/add-event/
1538        // https://gist.github.com/1930440
1539
1540        /**
1541         * @param {Object} element
1542         * @param {string} type
1543         * @param {function(...*)} handler
1544         * @param {boolean=} oldSchool
1545         * @param {boolean=} useCapture
1546         */
1547        var register_event = function(element, type, handler, oldSchool, useCapture) {
1548            if (!element) {
1549                console.error('No valid element provided to register_event');
1550                return;
1551            }
1552
1553            if (element.addEventListener && !oldSchool) {
1554                element.addEventListener(type, handler, !!useCapture);
1555            } else {
1556                var ontype = 'on' + type;
1557                var old_handler = element[ontype]; // can be undefined
1558                element[ontype] = makeHandler(element, handler, old_handler);
1559            }
1560        };
1561
1562        function makeHandler(element, new_handler, old_handlers) {
1563            var handler = function(event) {
1564                event = event || fixEvent(win.event);
1565
1566                // this basically happens in firefox whenever another script
1567                // overwrites the onload callback and doesn't pass the event
1568                // object to previously defined callbacks.  All the browsers
1569                // that don't define window.event implement addEventListener
1570                // so the dom_loaded handler will still be fired as usual.
1571                if (!event) {
1572                    return undefined;
1573                }
1574
1575                var ret = true;
1576                var old_result, new_result;
1577
1578                if (_.isFunction(old_handlers)) {
1579                    old_result = old_handlers(event);
1580                }
1581                new_result = new_handler.call(element, event);
1582
1583                if ((false === old_result) || (false === new_result)) {
1584                    ret = false;
1585                }
1586
1587                return ret;
1588            };
1589
1590            return handler;
1591        }
1592
1593        function fixEvent(event) {
1594            if (event) {
1595                event.preventDefault = fixEvent.preventDefault;
1596                event.stopPropagation = fixEvent.stopPropagation;
1597            }
1598            return event;
1599        }
1600        fixEvent.preventDefault = function() {
1601            this.returnValue = false;
1602        };
1603        fixEvent.stopPropagation = function() {
1604            this.cancelBubble = true;
1605        };
1606
1607        return register_event;
1608    })();
1609
1610
1611    var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$');
1612
1613    _.dom_query = (function() {
1614        /* document.getElementsBySelector(selector)
1615        - returns an array of element objects from the current document
1616        matching the CSS selector. Selectors can contain element names,
1617        class names and ids and can be nested. For example:
1618
1619        elements = document.getElementsBySelector('div#main p a.external')
1620
1621        Will return an array of all 'a' elements with 'external' in their
1622        class attribute that are contained inside 'p' elements that are
1623        contained inside the 'div' element which has id="main"
1624
1625        New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
1626        See http://www.w3.org/TR/css3-selectors/#attribute-selectors
1627
1628        Version 0.4 - Simon Willison, March 25th 2003
1629        -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
1630        -- Opera 7 fails
1631
1632        Version 0.5 - Carl Sverre, Jan 7th 2013
1633        -- Now uses jQuery-esque `hasClass` for testing class name
1634        equality.  This fixes a bug related to '-' characters being
1635        considered not part of a 'word' in regex.
1636        */
1637
1638        function getAllChildren(e) {
1639            // Returns all children of element. Workaround required for IE5/Windows. Ugh.
1640            return e.all ? e.all : e.getElementsByTagName('*');
1641        }
1642
1643        var bad_whitespace = /[\t\r\n]/g;
1644
1645        function hasClass(elem, selector) {
1646            var className = ' ' + selector + ' ';
1647            return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0);
1648        }
1649
1650        function getElementsBySelector(selector) {
1651            // Attempt to fail gracefully in lesser browsers
1652            if (!document$1.getElementsByTagName) {
1653                return [];
1654            }
1655            // Split selector in to tokens
1656            var tokens = selector.split(' ');
1657            var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex;
1658            var currentContext = [document$1];
1659            for (i = 0; i < tokens.length; i++) {
1660                token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, '');
1661                if (token.indexOf('#') > -1) {
1662                    // Token is an ID selector
1663                    bits = token.split('#');
1664                    tagName = bits[0];
1665                    var id = bits[1];
1666                    var element = document$1.getElementById(id);
1667                    if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
1668                        // element not found or tag with that ID not found, return false
1669                        return [];
1670                    }
1671                    // Set currentContext to contain just this element
1672                    currentContext = [element];
1673                    continue; // Skip to next token
1674                }
1675                if (token.indexOf('.') > -1) {
1676                    // Token contains a class selector
1677                    bits = token.split('.');
1678                    tagName = bits[0];
1679                    var className = bits[1];
1680                    if (!tagName) {
1681                        tagName = '*';
1682                    }
1683                    // Get elements matching tag, filter them for class selector
1684                    found = [];
1685                    foundCount = 0;
1686                    for (j = 0; j < currentContext.length; j++) {
1687                        if (tagName == '*') {
1688                            elements = getAllChildren(currentContext[j]);
1689                        } else {
1690                            elements = currentContext[j].getElementsByTagName(tagName);
1691                        }
1692                        for (k = 0; k < elements.length; k++) {
1693                            found[foundCount++] = elements[k];
1694                        }
1695                    }
1696                    currentContext = [];
1697                    currentContextIndex = 0;
1698                    for (j = 0; j < found.length; j++) {
1699                        if (found[j].className &&
1700                            _.isString(found[j].className) && // some SVG elements have classNames which are not strings
1701                            hasClass(found[j], className)
1702                        ) {
1703                            currentContext[currentContextIndex++] = found[j];
1704                        }
1705                    }
1706                    continue; // Skip to next token
1707                }
1708                // Code to deal with attribute selectors
1709                var token_match = token.match(TOKEN_MATCH_REGEX);
1710                if (token_match) {
1711                    tagName = token_match[1];
1712                    var attrName = token_match[2];
1713                    var attrOperator = token_match[3];
1714                    var attrValue = token_match[4];
1715                    if (!tagName) {
1716                        tagName = '*';
1717                    }
1718                    // Grab all of the tagName elements within current context
1719                    found = [];
1720                    foundCount = 0;
1721                    for (j = 0; j < currentContext.length; j++) {
1722                        if (tagName == '*') {
1723                            elements = getAllChildren(currentContext[j]);
1724                        } else {
1725                            elements = currentContext[j].getElementsByTagName(tagName);
1726                        }
1727                        for (k = 0; k < elements.length; k++) {
1728                            found[foundCount++] = elements[k];
1729                        }
1730                    }
1731                    currentContext = [];
1732                    currentContextIndex = 0;
1733                    var checkFunction; // This function will be used to filter the elements
1734                    switch (attrOperator) {
1735                        case '=': // Equality
1736                            checkFunction = function(e) {
1737                                return (e.getAttribute(attrName) == attrValue);
1738                            };
1739                            break;
1740                        case '~': // Match one of space seperated words
1741                            checkFunction = function(e) {
1742                                return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b')));
1743                            };
1744                            break;
1745                        case '|': // Match start with value followed by optional hyphen
1746                            checkFunction = function(e) {
1747                                return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?')));
1748                            };
1749                            break;
1750                        case '^': // Match starts with value
1751                            checkFunction = function(e) {
1752                                return (e.getAttribute(attrName).indexOf(attrValue) === 0);
1753                            };
1754                            break;
1755                        case '$': // Match ends with value - fails with "Warning" in Opera 7
1756                            checkFunction = function(e) {
1757                                return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length);
1758                            };
1759                            break;
1760                        case '*': // Match ends with value
1761                            checkFunction = function(e) {
1762                                return (e.getAttribute(attrName).indexOf(attrValue) > -1);
1763                            };
1764                            break;
1765                        default:
1766                            // Just test for existence of attribute
1767                            checkFunction = function(e) {
1768                                return e.getAttribute(attrName);
1769                            };
1770                    }
1771                    currentContext = [];
1772                    currentContextIndex = 0;
1773                    for (j = 0; j < found.length; j++) {
1774                        if (checkFunction(found[j])) {
1775                            currentContext[currentContextIndex++] = found[j];
1776                        }
1777                    }
1778                    // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
1779                    continue; // Skip to next token
1780                }
1781                // If we get here, token is JUST an element (not a class or ID selector)
1782                tagName = token;
1783                found = [];
1784                foundCount = 0;
1785                for (j = 0; j < currentContext.length; j++) {
1786                    elements = currentContext[j].getElementsByTagName(tagName);
1787                    for (k = 0; k < elements.length; k++) {
1788                        found[foundCount++] = elements[k];
1789                    }
1790                }
1791                currentContext = found;
1792            }
1793            return currentContext;
1794        }
1795
1796        return function(query) {
1797            if (_.isElement(query)) {
1798                return [query];
1799            } else if (_.isObject(query) && !_.isUndefined(query.length)) {
1800                return query;
1801            } else {
1802                return getElementsBySelector.call(this, query);
1803            }
1804        };
1805    })();
1806
1807    var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic'];
1808    var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
1809
1810    _.info = {
1811        campaignParams: function(default_value) {
1812            var kw = '',
1813                params = {};
1814            _.each(CAMPAIGN_KEYWORDS, function(kwkey) {
1815                kw = _.getQueryParam(document$1.URL, kwkey);
1816                if (kw.length) {
1817                    params[kwkey] = kw;
1818                } else if (default_value !== undefined) {
1819                    params[kwkey] = default_value;
1820                }
1821            });
1822
1823            return params;
1824        },
1825
1826        clickParams: function() {
1827            var id = '',
1828                params = {};
1829            _.each(CLICK_IDS, function(idkey) {
1830                id = _.getQueryParam(document$1.URL, idkey);
1831                if (id.length) {
1832                    params[idkey] = id;
1833                }
1834            });
1835
1836            return params;
1837        },
1838
1839        marketingParams: function() {
1840            return _.extend(_.info.campaignParams(), _.info.clickParams());
1841        },
1842
1843        searchEngine: function(referrer) {
1844            if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
1845                return 'google';
1846            } else if (referrer.search('https?://(.*)bing.com') === 0) {
1847                return 'bing';
1848            } else if (referrer.search('https?://(.*)yahoo.com') === 0) {
1849                return 'yahoo';
1850            } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) {
1851                return 'duckduckgo';
1852            } else {
1853                return null;
1854            }
1855        },
1856
1857        searchInfo: function(referrer) {
1858            var search = _.info.searchEngine(referrer),
1859                param = (search != 'yahoo') ? 'q' : 'p',
1860                ret = {};
1861
1862            if (search !== null) {
1863                ret['$search_engine'] = search;
1864
1865                var keyword = _.getQueryParam(referrer, param);
1866                if (keyword.length) {
1867                    ret['mp_keyword'] = keyword;
1868                }
1869            }
1870
1871            return ret;
1872        },
1873
1874        /**
1875         * This function detects which browser is running this script.
1876         * The order of the checks are important since many user agents
1877         * include key words used in later checks.
1878         */
1879        browser: function(user_agent, vendor, opera) {
1880            vendor = vendor || ''; // vendor is undefined for at least IE9
1881            if (opera || _.includes(user_agent, ' OPR/')) {
1882                if (_.includes(user_agent, 'Mini')) {
1883                    return 'Opera Mini';
1884                }
1885                return 'Opera';
1886            } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
1887                return 'BlackBerry';
1888            } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) {
1889                return 'Internet Explorer Mobile';
1890            } else if (_.includes(user_agent, 'SamsungBrowser/')) {
1891                // https://developer.samsung.com/internet/user-agent-string-format
1892                return 'Samsung Internet';
1893            } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) {
1894                return 'Microsoft Edge';
1895            } else if (_.includes(user_agent, 'FBIOS')) {
1896                return 'Facebook Mobile';
1897            } else if (_.includes(user_agent, 'Whale/')) {
1898                // https://user-agents.net/browsers/whale-browser
1899                return 'Whale Browser';
1900            } else if (_.includes(user_agent, 'Chrome')) {
1901                return 'Chrome';
1902            } else if (_.includes(user_agent, 'CriOS')) {
1903                return 'Chrome iOS';
1904            } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) {
1905                return 'UC Browser';
1906            } else if (_.includes(user_agent, 'FxiOS')) {
1907                return 'Firefox iOS';
1908            } else if (_.includes(vendor, 'Apple')) {
1909                if (_.includes(user_agent, 'Mobile')) {
1910                    return 'Mobile Safari';
1911                }
1912                return 'Safari';
1913            } else if (_.includes(user_agent, 'Android')) {
1914                return 'Android Mobile';
1915            } else if (_.includes(user_agent, 'Konqueror')) {
1916                return 'Konqueror';
1917            } else if (_.includes(user_agent, 'Firefox')) {
1918                return 'Firefox';
1919            } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) {
1920                return 'Internet Explorer';
1921            } else if (_.includes(user_agent, 'Gecko')) {
1922                return 'Mozilla';
1923            } else {
1924                return '';
1925            }
1926        },
1927
1928        /**
1929         * This function detects which browser version is running this script,
1930         * parsing major and minor version (e.g., 42.1). User agent strings from:
1931         * http://www.useragentstring.com/pages/useragentstring.php
1932         */
1933        browserVersion: function(userAgent, vendor, opera) {
1934            var browser = _.info.browser(userAgent, vendor, opera);
1935            var versionRegexs = {
1936                'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/,
1937                'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/,
1938                'Chrome': /Chrome\/(\d+(\.\d+)?)/,
1939                'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/,
1940                'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/,
1941                'Safari': /Version\/(\d+(\.\d+)?)/,
1942                'Mobile Safari': /Version\/(\d+(\.\d+)?)/,
1943                'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/,
1944                'Firefox': /Firefox\/(\d+(\.\d+)?)/,
1945                'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/,
1946                'Konqueror': /Konqueror:(\d+(\.\d+)?)/,
1947                'BlackBerry': /BlackBerry (\d+(\.\d+)?)/,
1948                'Android Mobile': /android\s(\d+(\.\d+)?)/,
1949                'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/,
1950                'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/,
1951                'Mozilla': /rv:(\d+(\.\d+)?)/,
1952                'Whale Browser': /Whale\/(\d+(\.\d+)?)/
1953            };
1954            var regex = versionRegexs[browser];
1955            if (regex === undefined) {
1956                return null;
1957            }
1958            var matches = userAgent.match(regex);
1959            if (!matches) {
1960                return null;
1961            }
1962            return parseFloat(matches[matches.length - 2]);
1963        },
1964
1965        os: function() {
1966            var a = userAgent;
1967            if (/Windows/i.test(a)) {
1968                if (/Phone/.test(a) || /WPDesktop/.test(a)) {
1969                    return 'Windows Phone';
1970                }
1971                return 'Windows';
1972            } else if (/(iPhone|iPad|iPod)/.test(a)) {
1973                return 'iOS';
1974            } else if (/Android/.test(a)) {
1975                return 'Android';
1976            } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) {
1977                return 'BlackBerry';
1978            } else if (/Mac/i.test(a)) {
1979                return 'Mac OS X';
1980            } else if (/Linux/.test(a)) {
1981                return 'Linux';
1982            } else if (/CrOS/.test(a)) {
1983                return 'Chrome OS';
1984            } else {
1985                return '';
1986            }
1987        },
1988
1989        device: function(user_agent) {
1990            if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) {
1991                return 'Windows Phone';
1992            } else if (/iPad/.test(user_agent)) {
1993                return 'iPad';
1994            } else if (/iPod/.test(user_agent)) {
1995                return 'iPod Touch';
1996            } else if (/iPhone/.test(user_agent)) {
1997                return 'iPhone';
1998            } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
1999                return 'BlackBerry';
2000            } else if (/Android/.test(user_agent)) {
2001                return 'Android';
2002            } else {
2003                return '';
2004            }
2005        },
2006
2007        referringDomain: function(referrer) {
2008            var split = referrer.split('/');
2009            if (split.length >= 3) {
2010                return split[2];
2011            }
2012            return '';
2013        },
2014
2015        currentUrl: function() {
2016            return win.location.href;
2017        },
2018
2019        properties: function(extra_props) {
2020            if (typeof extra_props !== 'object') {
2021                extra_props = {};
2022            }
2023            return _.extend(_.strip_empty_properties({
2024                '$os': _.info.os(),
2025                '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera),
2026                '$referrer': document$1.referrer,
2027                '$referring_domain': _.info.referringDomain(document$1.referrer),
2028                '$device': _.info.device(userAgent)
2029            }), {
2030                '$current_url': _.info.currentUrl(),
2031                '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera),
2032                '$screen_height': screen.height,
2033                '$screen_width': screen.width,
2034                'mp_lib': 'web',
2035                '$lib_version': Config.LIB_VERSION,
2036                '$insert_id': cheap_guid(),
2037                'time': _.timestamp() / 1000 // epoch time in seconds
2038            }, _.strip_empty_properties(extra_props));
2039        },
2040
2041        people_properties: function() {
2042            return _.extend(_.strip_empty_properties({
2043                '$os': _.info.os(),
2044                '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera)
2045            }), {
2046                '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera)
2047            });
2048        },
2049
2050        mpPageViewProperties: function() {
2051            return _.strip_empty_properties({
2052                'current_page_title': document$1.title,
2053                'current_domain': win.location.hostname,
2054                'current_url_path': win.location.pathname,
2055                'current_url_protocol': win.location.protocol,
2056                'current_url_search': win.location.search
2057            });
2058        }
2059    };
2060
2061    /**
2062     * Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
2063     * Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
2064     */
2065    var batchedThrottle = function (fn, waitMs) {
2066        var timeoutPromise = null;
2067        var throttledItems = [];
2068        return function (item) {
2069            var self = this;
2070            throttledItems.push(item);
2071
2072            if (!timeoutPromise) {
2073                timeoutPromise = new PromisePolyfill(function (resolve) {
2074                    setTimeout(function () {
2075                        var returnValue = fn.apply(self, [throttledItems]);
2076                        timeoutPromise = null;
2077                        throttledItems = [];
2078                        resolve(returnValue);
2079                    }, waitMs);
2080                });
2081            }
2082            return timeoutPromise;
2083        };
2084    };
2085
2086    var cheap_guid = function(maxlen) {
2087        var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
2088        return maxlen ? guid.substring(0, maxlen) : guid;
2089    };
2090
2091    /**
2092     * Generates a W3C traceparent header for easy interop with distributed tracing systems i.e Open Telemetry
2093     * https://www.w3.org/TR/trace-context/#traceparent-header
2094    */
2095    var generateTraceparent = function() {
2096        var traceID = _.UUID().replace(/-/g, '');
2097        var parentID = _.UUID().replace(/-/g, '').substring(0, 16);
2098
2099        // Sampled trace
2100        var traceFlags = '01';
2101
2102        return '00-' + traceID + '-' + parentID + '-' + traceFlags;
2103    };
2104
2105    // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
2106    var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
2107    // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
2108    var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i;
2109    /**
2110     * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For
2111     * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we
2112     * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX).
2113     * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy
2114     * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel
2115     * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date
2116     * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK
2117     * offers the 'cookie_domain' config option to set it explicitly.
2118     * @example
2119     * extract_domain('my.sub.example.com')
2120     * // 'example.com'
2121     */
2122    var extract_domain = function(hostname) {
2123        var domain_regex = DOMAIN_MATCH_REGEX;
2124        var parts = hostname.split('.');
2125        var tld = parts[parts.length - 1];
2126        if (tld.length > 4 || tld === 'com' || tld === 'org') {
2127            domain_regex = SIMPLE_DOMAIN_MATCH_REGEX;
2128        }
2129        var matches = hostname.match(domain_regex);
2130        return matches ? matches[0] : '';
2131    };
2132
2133    /**
2134     * Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
2135     * @returns {boolean}
2136     */
2137    var isOnline = function() {
2138        var onLine = win.navigator['onLine'];
2139        return _.isUndefined(onLine) || onLine;
2140    };
2141
2142    var NOOP_FUNC = function () {};
2143
2144    var urlMatchesRegexList = function (url, regexList) {
2145        var matches = false;
2146        for (var i = 0; i < regexList.length; i++) {
2147            if (url.match(regexList[i])) {
2148                matches = true;
2149                break;
2150            }
2151        }
2152        return matches;
2153    };
2154
2155    var JSONStringify = null, JSONParse = null;
2156    if (typeof JSON !== 'undefined') {
2157        JSONStringify = JSON.stringify;
2158        JSONParse = JSON.parse;
2159    }
2160    JSONStringify = JSONStringify || _.JSONEncode;
2161    JSONParse = JSONParse || _.JSONDecode;
2162
2163    // UNMINIFIED EXPORTS (for closure compiler)
2164    _['info']                   = _.info;
2165    _['info']['browser']        = _.info.browser;
2166    _['info']['browserVersion'] = _.info.browserVersion;
2167    _['info']['device']         = _.info.device;
2168    _['info']['properties']     = _.info.properties;
2169    _['isBlockedUA']            = _.isBlockedUA;
2170    _['isEmptyObject']          = _.isEmptyObject;
2171    _['isObject']               = _.isObject;
2172    _['JSONDecode']             = _.JSONDecode;
2173    _['JSONEncode']             = _.JSONEncode;
2174    _['toArray']                = _.toArray;
2175    _['NPO']                    = NpoPromise;
2176
2177    // stateless utils
2178    // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
2179
2180
2181    var EV_CHANGE = 'change';
2182    var EV_CLICK = 'click';
2183    var EV_HASHCHANGE = 'hashchange';
2184    var EV_INPUT = 'input';
2185    var EV_LOAD = 'load';
2186    var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
2187    var EV_POPSTATE = 'popstate';
2188    // TODO scrollend isn't available in Safari: document or polyfill?
2189    var EV_SCROLLEND = 'scrollend';
2190    var EV_SCROLL = 'scroll';
2191    var EV_SELECT = 'select';
2192    var EV_SUBMIT = 'submit';
2193    var EV_TOGGLE = 'toggle';
2194    var EV_VISIBILITYCHANGE = 'visibilitychange';
2195
2196    var CLICK_EVENT_PROPS = [
2197        'clientX', 'clientY',
2198        'offsetX', 'offsetY',
2199        'pageX', 'pageY',
2200        'screenX', 'screenY',
2201        'x', 'y'
2202    ];
2203    var OPT_IN_CLASSES = ['mp-include'];
2204    var OPT_OUT_CLASSES = ['mp-no-track'];
2205    var SENSITIVE_DATA_CLASSES = OPT_OUT_CLASSES.concat(['mp-sensitive']);
2206    var TRACKED_ATTRS = [
2207        'aria-label', 'aria-labelledby', 'aria-describedby',
2208        'href', 'name', 'role', 'title', 'type'
2209    ];
2210
2211    var INTERACTIVE_ARIA_ROLES = {
2212        'button': true,
2213        'checkbox': true,
2214        'combobox': true,
2215        'grid': true,
2216        'link': true,
2217        'listbox': true,
2218        'menu': true,
2219        'menubar': true,
2220        'menuitem': true,
2221        'menuitemcheckbox': true,
2222        'menuitemradio': true,
2223        'navigation': true,
2224        'option': true,
2225        'radio': true,
2226        'radiogroup': true,
2227        'searchbox': true,
2228        'slider': true,
2229        'spinbutton': true,
2230        'switch': true,
2231        'tab': true,
2232        'tablist': true,
2233        'textbox': true,
2234        'tree': true,
2235        'treegrid': true,
2236        'treeitem': true
2237    };
2238
2239    var ALWAYS_NON_INTERACTIVE_TAGS = {
2240        // Document metadata
2241        'base': true,
2242        'head': true,
2243        'html': true,
2244        'link': true,
2245        'meta': true,
2246        'script': true,
2247        'style': true,
2248        'title': true,
2249        // Text formatting
2250        'br': true,
2251        'hr': true,
2252        'wbr': true,
2253        // Other
2254        'noscript': true,
2255        'picture': true,
2256        'source': true,
2257        'template': true,
2258        'track': true
2259    };
2260
2261    // Common container tags that need additional checks
2262    var TEXT_CONTAINER_TAGS = {
2263        'article': true,
2264        'div': true,
2265        'h1': true,
2266        'h2': true,
2267        'h3': true,
2268        'h4': true,
2269        'h5': true,
2270        'h6': true,
2271        'p': true,
2272        'section': true,
2273        'span': true
2274    };
2275
2276    var EVENT_HANDLER_ATTRIBUTES = [
2277        'onclick', 'onmousedown', 'onmouseup', 'onpointerdown', 'onpointerup', 'ontouchend', 'ontouchstart'
2278    ];
2279
2280    var MAX_DEPTH = 5;
2281
2282    var logger$6 = console_with_prefix('autocapture');
2283
2284
2285    function getClasses(el) {
2286        var classes = {};
2287        var classList = getClassName(el).split(' ');
2288        for (var i = 0; i < classList.length; i++) {
2289            var cls = classList[i];
2290            if (cls) {
2291                classes[cls] = true;
2292            }
2293        }
2294        return classes;
2295    }
2296
2297    /*
2298     * Get the className of an element, accounting for edge cases where element.className is an object
2299     * @param {Element} el - element to get the className of
2300     * @returns {string} the element's class
2301     */
2302    function getClassName(el) {
2303        switch(typeof el.className) {
2304            case 'string':
2305                return el.className;
2306            case 'object': // handle cases where className might be SVGAnimatedString or some other type
2307                return el.className.baseVal || el.getAttribute('class') || '';
2308            default: // future proof
2309                return '';
2310        }
2311    }
2312
2313    function getPreviousElementSibling(el) {
2314        if (el.previousElementSibling) {
2315            return el.previousElementSibling;
2316        } else {
2317            do {
2318                el = el.previousSibling;
2319            } while (el && !isElementNode(el));
2320            return el;
2321        }
2322    }
2323
2324    function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
2325        var props = {
2326            '$classes': getClassName(el).split(' '),
2327            '$tag_name': el.tagName.toLowerCase()
2328        };
2329        var elId = el.id;
2330        if (elId) {
2331            props['$id'] = elId;
2332        }
2333
2334        if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
2335            _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
2336                if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
2337                    var attrVal = el.getAttribute(attr);
2338                    if (shouldTrackValue(attrVal)) {
2339                        props['$attr-' + attr] = attrVal;
2340                    }
2341                }
2342            });
2343        }
2344
2345        var nthChild = 1;
2346        var nthOfType = 1;
2347        var currentElem = el;
2348        while (currentElem = getPreviousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
2349            nthChild++;
2350            if (currentElem.tagName === el.tagName) {
2351                nthOfType++;
2352            }
2353        }
2354        props['$nth_child'] = nthChild;
2355        props['$nth_of_type'] = nthOfType;
2356
2357        return props;
2358    }
2359
2360    function getPropsForDOMEvent(ev, config) {
2361        var allowElementCallback = config.allowElementCallback;
2362        var allowSelectors = config.allowSelectors || [];
2363        var blockAttrs = config.blockAttrs || [];
2364        var blockElementCallback = config.blockElementCallback;
2365        var blockSelectors = config.blockSelectors || [];
2366        var captureTextContent = config.captureTextContent || false;
2367        var captureExtraAttrs = config.captureExtraAttrs || [];
2368        var capturedForHeatMap = config.capturedForHeatMap || false;
2369
2370        // convert array to set every time, as the config may have changed
2371        var blockAttrsSet = {};
2372        _.each(blockAttrs, function(attr) {
2373            blockAttrsSet[attr] = true;
2374        });
2375
2376        var props = null;
2377
2378        var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
2379        if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
2380            target = target.parentNode;
2381        }
2382
2383        if (
2384            shouldTrackDomEvent(target, ev) &&
2385            isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
2386            !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2387        ) {
2388            var targetElementList = [target];
2389            var curEl = target;
2390            while (curEl.parentNode && !isTag(curEl, 'body')) {
2391                targetElementList.push(curEl.parentNode);
2392                curEl = curEl.parentNode;
2393            }
2394
2395            var elementsJson = [];
2396            var href, explicitNoTrack = false;
2397            _.each(targetElementList, function(el) {
2398                var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
2399
2400                // if the element or a parent element is an anchor tag
2401                // include the href as a property
2402                if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
2403                    href = el.getAttribute('href');
2404                    href = shouldTrackDetails && shouldTrackValue(href) && href;
2405                }
2406
2407                if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
2408                    explicitNoTrack = true;
2409                }
2410
2411                elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
2412            }, this);
2413
2414            if (!explicitNoTrack) {
2415                var docElement = document$1['documentElement'];
2416                props = {
2417                    '$event_type': ev.type,
2418                    '$host': win.location.host,
2419                    '$pathname': win.location.pathname,
2420                    '$elements':  elementsJson,
2421                    '$el_attr__href': href,
2422                    '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
2423                    '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0),
2424                    '$pageHeight': document$1['body']['offsetHeight'] || 0,
2425                    '$pageWidth': document$1['body']['offsetWidth'] || 0,
2426                };
2427                _.each(captureExtraAttrs, function(attr) {
2428                    if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
2429                        var attrVal = target.getAttribute(attr);
2430                        if (shouldTrackValue(attrVal)) {
2431                            props['$el_attr__' + attr] = attrVal;
2432                        }
2433                    }
2434                });
2435
2436                if (captureTextContent) {
2437                    elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2438                    if (elementText && elementText.length) {
2439                        props['$el_text'] = elementText;
2440                    }
2441                }
2442
2443                if (ev.type === EV_CLICK) {
2444                    _.each(CLICK_EVENT_PROPS, function(prop) {
2445                        if (prop in ev) {
2446                            props['$' + prop] = ev[prop];
2447                        }
2448                    });
2449                    if (capturedForHeatMap) {
2450                        props['$captured_for_heatmap'] = true;
2451                    }
2452                    target = guessRealClickTarget(ev);
2453                }
2454                // prioritize text content from "real" click target if different from original target
2455                if (captureTextContent) {
2456                    var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2457                    if (elementText && elementText.length) {
2458                        props['$el_text'] = elementText;
2459                    }
2460                }
2461
2462                if (target) {
2463                    // target may have been recalculated; check allowlists and blocklists again
2464                    if (
2465                        !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
2466                        isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2467                    ) {
2468                        return null;
2469                    }
2470
2471                    var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
2472                    props['$target'] = targetProps;
2473                    // pull up more props onto main event props
2474                    props['$el_classes'] = targetProps['$classes'];
2475                    _.extend(props, _.strip_empty_properties({
2476                        '$el_id': targetProps['$id'],
2477                        '$el_tag_name': targetProps['$tag_name']
2478                    }));
2479                }
2480            }
2481        }
2482
2483        return props;
2484    }
2485
2486
2487    /**
2488     * Get the direct text content of an element, protecting against sensitive data collection.
2489     * Concats textContent of each of the element's text node children; this avoids potential
2490     * collection of sensitive data that could happen if we used element.textContent and the
2491     * element had sensitive child elements, since element.textContent includes child content.
2492     * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
2493     * @param {Element} el - element to get the text of
2494     * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2495     * @returns {string} the element's direct text content
2496     */
2497    function getSafeText(el, ev, allowElementCallback, allowSelectors) {
2498        var elText = '';
2499
2500        if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
2501            _.each(el.childNodes, function(child) {
2502                if (isTextNode(child) && child.textContent) {
2503                    elText += child.textContent.trim()
2504                        // scrub potentially sensitive values
2505                        .split(/(\s+)/).filter(shouldTrackValue).join('')
2506                        // normalize whitespace
2507                        .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
2508                        // truncate
2509                        .substring(0, 255);
2510                }
2511            });
2512        }
2513
2514        return elText.trim();
2515    }
2516
2517    function guessRealClickTarget(ev) {
2518        var target = ev.target;
2519        var composedPath = ev['composedPath']();
2520        for (var i = 0; i < composedPath.length; i++) {
2521            var node = composedPath[i];
2522            if (
2523                isTag(node, 'a') ||
2524                isTag(node, 'button') ||
2525                isTag(node, 'input') ||
2526                isTag(node, 'select') ||
2527                (node.getAttribute && node.getAttribute('role') === 'button')
2528            ) {
2529                target = node;
2530                break;
2531            }
2532            if (node === target) {
2533                break;
2534            }
2535        }
2536        return target;
2537    }
2538
2539    function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
2540        if (allowElementCallback) {
2541            try {
2542                if (!allowElementCallback(el, ev)) {
2543                    return false;
2544                }
2545            } catch (err) {
2546                logger$6.critical('Error while checking element in allowElementCallback', err);
2547                return false;
2548            }
2549        }
2550
2551        if (!allowSelectors.length) {
2552            // no allowlist; all elements are fair game
2553            return true;
2554        }
2555
2556        for (var i = 0; i < allowSelectors.length; i++) {
2557            var sel = allowSelectors[i];
2558            try {
2559                if (el['matches'](sel)) {
2560                    return true;
2561                }
2562            } catch (err) {
2563                logger$6.critical('Error while checking selector: ' + sel, err);
2564            }
2565        }
2566        return false;
2567    }
2568
2569    function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
2570        var i;
2571
2572        if (blockElementCallback) {
2573            try {
2574                if (blockElementCallback(el, ev)) {
2575                    return true;
2576                }
2577            } catch (err) {
2578                logger$6.critical('Error while checking element in blockElementCallback', err);
2579                return true;
2580            }
2581        }
2582
2583        if (blockSelectors && blockSelectors.length) {
2584            // programmatically prevent tracking of elements that match CSS selectors
2585            for (i = 0; i < blockSelectors.length; i++) {
2586                var sel = blockSelectors[i];
2587                try {
2588                    if (el['matches'](sel)) {
2589                        return true;
2590                    }
2591                } catch (err) {
2592                    logger$6.critical('Error while checking selector: ' + sel, err);
2593                }
2594            }
2595        }
2596
2597        // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
2598        var classes = getClasses(el);
2599        for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
2600            if (classes[OPT_OUT_CLASSES[i]]) {
2601                return true;
2602            }
2603        }
2604
2605        return false;
2606    }
2607
2608    /*
2609     * Check whether a DOM node has nodeType Node.ELEMENT_NODE
2610     * @param {Node} node - node to check
2611     * @returns {boolean} whether node is of the correct nodeType
2612     */
2613    function isElementNode(node) {
2614        return node && node.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
2615    }
2616
2617    /*
2618     * Check whether an element is of a given tag type.
2619     * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
2620     * we want to match tagNames instead of specific references because something like
2621     * element === document.body won't always work because element might not be a native
2622     * element.
2623     * @param {Element} el - element to check
2624     * @param {string} tag - tag name (e.g., "div")
2625     * @returns {boolean} whether el is of the given tag type
2626     */
2627    function isTag(el, tag) {
2628        return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
2629    }
2630
2631    /*
2632     * Check whether a DOM node is a TEXT_NODE
2633     * @param {Node} node - node to check
2634     * @returns {boolean} whether node is of type Node.TEXT_NODE
2635     */
2636    function isTextNode(node) {
2637        return node && node.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
2638    }
2639
2640    function minDOMApisSupported() {
2641        try {
2642            var testEl = document$1.createElement('div');
2643            return !!testEl['matches'];
2644        } catch (err) {
2645            return false;
2646        }
2647    }
2648
2649    function weakSetSupported() {
2650        return typeof WeakSet !== 'undefined';
2651    }
2652
2653    /*
2654     * Check whether a DOM event should be "tracked" or if it may contain sensitive data
2655     * using a variety of heuristics.
2656     * @param {Element} el - element to check
2657     * @param {Event} ev - event to check
2658     * @returns {boolean} whether the event should be tracked
2659     */
2660    function shouldTrackDomEvent(el, ev) {
2661        if (!el || isTag(el, 'html') || !isElementNode(el)) {
2662            return false;
2663        }
2664        var tag = el.tagName.toLowerCase();
2665        switch (tag) {
2666            case 'form':
2667                return ev.type === EV_SUBMIT;
2668            case 'input':
2669                if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
2670                    return ev.type === EV_CHANGE;
2671                } else {
2672                    return ev.type === EV_CLICK;
2673                }
2674            case 'select':
2675            case 'textarea':
2676                return ev.type === EV_CHANGE;
2677            default:
2678                return ev.type === EV_CLICK;
2679        }
2680    }
2681
2682    function elementLooksSensitive(el) {
2683        var name = (el.name || el.id || '').toString().toLowerCase();
2684        if (typeof name === 'string') { // 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"]
2685            var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2686            if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2687                return true;
2688            }
2689        }
2690
2691        return false;
2692    }
2693
2694    /*
2695     * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2696     * using a variety of heuristics.
2697     * @param {Element} el - element to check
2698     * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2699     * @returns {boolean} whether the element should be tracked
2700     */
2701    function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
2702        var i;
2703
2704        if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
2705            return false;
2706        }
2707
2708        for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
2709            var classes = getClasses(curEl);
2710            for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
2711                if (classes[SENSITIVE_DATA_CLASSES[i]]) {
2712                    return false;
2713                }
2714            }
2715        }
2716
2717        var elClasses = getClasses(el);
2718        for (i = 0; i < OPT_IN_CLASSES.length; i++) {
2719            if (elClasses[OPT_IN_CLASSES[i]]) {
2720                return true;
2721            }
2722        }
2723
2724        // don't send data from inputs or similar elements since there will always be
2725        // a risk of clientside javascript placing sensitive data in attributes
2726        if (
2727            isTag(el, 'input') ||
2728            isTag(el, 'select') ||
2729            isTag(el, 'textarea') ||
2730            el.getAttribute('contenteditable') === 'true'
2731        ) {
2732            return false;
2733        }
2734
2735        // don't include hidden or password fields
2736        var type = el.type || '';
2737        if (typeof type === 'string') { // it's possible for el.type to be a DOM element if el is a form with a child input[name="type"]
2738            switch(type.toLowerCase()) {
2739                case 'hidden':
2740                    return false;
2741                case 'password':
2742                    return false;
2743            }
2744        }
2745
2746        if (elementLooksSensitive(el)) {
2747            return false;
2748        }
2749
2750        return true;
2751    }
2752
2753
2754    /*
2755     * Check whether a string value should be "tracked" or if it may contain sensitive data
2756     * using a variety of heuristics.
2757     * @param {string} value - string value to check
2758     * @returns {boolean} whether the element should be tracked
2759     */
2760    function shouldTrackValue(value) {
2761        if (value === null || _.isUndefined(value)) {
2762            return false;
2763        }
2764
2765        if (typeof value === 'string') {
2766            value = value.trim();
2767
2768            // check to see if input value looks like a credit card number
2769            // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
2770            var ccRegex = /^(?:(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}))$/;
2771            if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
2772                return false;
2773            }
2774
2775            // check to see if input value looks like a social security number
2776            var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
2777            if (ssnRegex.test(value)) {
2778                return false;
2779            }
2780        }
2781
2782        return true;
2783    }
2784
2785    /**
2786     * Creates a cross-browser compatible scroll end function with appropriate event listener.
2787     * For browsers that support scrollend, returns the original function with scrollend event.
2788     * For browsers without scrollend support, returns a debounced function that triggers
2789     * 100ms after the last scroll event to simulate scrollend behavior.
2790     * @param {Function} originalFunction - The function to call when scrolling ends
2791     * @returns {Object} Object containing listener function and eventType string
2792     * @returns {Function} returns.listener - The wrapped function to use as event listener
2793     * @returns {string} returns.eventType - The event type to listen for ('scrollend' or 'scroll')
2794     */
2795    function getPolyfillScrollEndFunction(originalFunction) {
2796        var supportsScrollEnd = 'onscrollend' in win;
2797        var polyfillFunction = safewrap(originalFunction);
2798        var polyfillEvent = EV_SCROLLEND;
2799        if (!supportsScrollEnd) {
2800            // Polyfill for browsers without scrollend support: wait 100ms after the last scroll event
2801            // https://developer.chrome.com/blog/scrollend-a-new-javascript-event
2802            var scrollTimer = null;
2803            var scrollDelayMs = 100;
2804
2805            polyfillFunction = safewrap(function() {
2806                clearTimeout(scrollTimer);
2807                scrollTimer = setTimeout(originalFunction, scrollDelayMs);
2808            });
2809
2810            polyfillEvent = EV_SCROLL;
2811        }
2812
2813        return {
2814            listener: polyfillFunction,
2815            eventType: polyfillEvent
2816        };
2817    }
2818
2819    function hasInlineEventHandlers(element) {
2820        for (var i = 0; i < EVENT_HANDLER_ATTRIBUTES.length; i++) {
2821            if (element.hasAttribute(EVENT_HANDLER_ATTRIBUTES[i])) {
2822                return true;
2823            }
2824        }
2825        return false;
2826    }
2827
2828    function hasInteractiveAriaRole(element) {
2829        var role = element.getAttribute('role');
2830        if (!role) return false;
2831
2832        // Handle invalid markup where multiple roles might be specified
2833        // Only the first token is recognized per ARIA spec
2834        var primaryRole = role.trim().split(/\s+/)[0].toLowerCase();
2835
2836        return INTERACTIVE_ARIA_ROLES[primaryRole];
2837    }
2838
2839    function hasAnyInteractivityIndicators(element) {
2840        var tagName = element.tagName.toLowerCase();
2841
2842        // Check for interactive HTML elements
2843        if (tagName === 'button' ||
2844            tagName === 'input' ||
2845            tagName === 'select' ||
2846            tagName === 'textarea' ||
2847            tagName === 'details' ||
2848            tagName === 'dialog') {
2849            return true;
2850        }
2851
2852        if (element.isContentEditable) {
2853            return true;
2854        }
2855
2856        if (element.onclick || element.onmousedown || element.onmouseup || element.ontouchstart || element.ontouchend) {
2857            return true;
2858        }
2859
2860        if (hasInlineEventHandlers(element)) {
2861            return true;
2862        }
2863
2864        if (hasInteractiveAriaRole(element)) {
2865            return true;
2866        }
2867
2868        if (tagName === 'a' && element.hasAttribute('href')) {
2869            return true;
2870        }
2871
2872        if (element.hasAttribute('tabindex')) {
2873            return true;
2874        }
2875
2876        return false;
2877    }
2878
2879
2880    function isDefinitelyNonInteractive(element) {
2881        if (!element || !element.tagName) {
2882            return true;
2883        }
2884
2885        var tagName = element.tagName.toLowerCase();
2886
2887        // These tags are definitely non-interactive
2888        if (ALWAYS_NON_INTERACTIVE_TAGS[tagName]) {
2889            return true;
2890        }
2891
2892        // For all other elements, we can only be certain they're non-interactive if they lack ALL indicators of interactivity
2893        // Check for any signs of interactivity
2894        if (hasAnyInteractivityIndicators(element)) {
2895            return false;
2896        }
2897
2898        // Check parent chain for interactive context
2899        var parent = element.parentElement;
2900        var depth = 0;
2901
2902        while (parent && depth < MAX_DEPTH) {
2903            if (hasAnyInteractivityIndicators(parent)) {
2904                return false; // Element is inside an interactive parent
2905            }
2906
2907            if (parent.getRootNode && parent.getRootNode() !== document$1) {
2908                var root = parent.getRootNode();
2909                if (root.host && hasAnyInteractivityIndicators(root.host)) {
2910                    return false; // Inside an interactive shadow host
2911                }
2912            }
2913
2914            parent = parent.parentElement;
2915            depth++;
2916        }
2917
2918        // Pure text containers without any interactive context
2919        if (TEXT_CONTAINER_TAGS[tagName]) {
2920            // These are non-interactive ONLY if they have no interactive indicators (already checked as part of hasAnyInteractivityIndicators)
2921            return true;
2922        }
2923
2924        // Default: we can't be certain it's non-interactive
2925        return false;
2926    }
2927
2928    /**
2929     * Get the composed path of a click event for elements embedded in shadow DOM.
2930     * @param {Event} event - event to get the composed path from
2931     * @returns {Array} the composed path of the click event
2932    */
2933    function getClickEventComposedPath(event) {
2934        if ('composedPath' in event) {
2935            return event['composedPath']();
2936        }
2937
2938        return [];
2939    }
2940
2941    /**
2942     * Get the element from a click event, accounting for elements embedded in shadow DOM.
2943     * @param {Event} event - event to get the target from
2944     * @returns {Element | null} the element that was the target of the click event
2945     */
2946    function getClickEventTargetElement(event) {
2947        var path = getClickEventComposedPath(event);
2948
2949        if (path && path.length > 0) {
2950            return path[0];
2951        }
2952
2953        return event['target'] || event['srcElement'];
2954    }
2955
2956    /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
2957    /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
2958    /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
2959    /** @const */ var DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY = false;
2960
2961    function RageClickTracker() {
2962        this.clicks = [];
2963    }
2964
2965    /**
2966     * Determines if a click event is part of a rage click sequence.
2967     * @param {Event} event - the original click event.
2968     * @param {import('../index.d.ts').RageClickConfig} options - configuration options for rage click detection.
2969     * @returns {boolean} - true if the click is considered a rage click, false otherwise.
2970     */
2971    RageClickTracker.prototype.isRageClick = function(event, options) {
2972        options = options || {};
2973        var thresholdPx = options['threshold_px'] || DEFAULT_RAGE_CLICK_THRESHOLD_PX;
2974        var timeoutMs = options['timeout_ms'] || DEFAULT_RAGE_CLICK_TIMEOUT_MS;
2975        var clickCount = options['click_count'] || DEFAULT_RAGE_CLICK_CLICK_COUNT;
2976        var interactiveElementsOnly = options['interactive_elements_only'] || DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY;
2977
2978        if (interactiveElementsOnly) {
2979            var target = getClickEventTargetElement(event);
2980            if (!target || isDefinitelyNonInteractive(target)) {
2981                return false;
2982            }
2983        }
2984
2985        var timestamp = Date.now();
2986        var x = event['pageX'], y = event['pageY'];
2987
2988        var lastClick = this.clicks[this.clicks.length - 1];
2989        if (
2990            lastClick &&
2991            timestamp - lastClick.timestamp < timeoutMs &&
2992            Math.sqrt(Math.pow(x - lastClick.x, 2) + Math.pow(y - lastClick.y, 2)) < thresholdPx
2993        ) {
2994            this.clicks.push({ x: x, y: y, timestamp: timestamp });
2995            if (this.clicks.length >= clickCount) {
2996                this.clicks = [];
2997                return true;
2998            }
2999        } else {
3000            this.clicks = [{ x: x, y: y, timestamp: timestamp }];
3001        }
3002        return false;
3003    };
3004
3005    function ShadowDOMObserver(changeCallback, observerConfig) {
3006        this.changeCallback = changeCallback || function() {};
3007        this.observerConfig = observerConfig;
3008
3009        this.observedShadowRoots = null;
3010        this.shadowObservers = [];
3011    }
3012
3013    ShadowDOMObserver.prototype.getEventTarget = function(event) {
3014        if (!this.observedShadowRoots) {
3015            return;
3016        }
3017
3018        return getClickEventTargetElement(event);
3019    };
3020
3021    ShadowDOMObserver.prototype.observeFromEvent = function(event) {
3022        if (!this.observedShadowRoots) {
3023            return;
3024        }
3025
3026        var path = getClickEventComposedPath(event);
3027
3028        // Check each element in path for shadow roots
3029        for (var i = 0; i < path.length; i++) {
3030            var element = path[i];
3031
3032            if (element && element.shadowRoot) {
3033                this.observeShadowRoot(element.shadowRoot);
3034            }
3035        }
3036    };
3037
3038
3039    ShadowDOMObserver.prototype.observeShadowRoot = function(shadowRoot) {
3040        if (!this.observedShadowRoots || this.observedShadowRoots.has(shadowRoot)) {
3041            return;
3042        }
3043
3044        var self = this;
3045
3046        try {
3047            this.observedShadowRoots.add(shadowRoot);
3048
3049            var observer = new window.MutationObserver(function() {
3050                self.changeCallback();
3051            });
3052
3053            observer.observe(shadowRoot, this.observerConfig);
3054            this.shadowObservers.push(observer);
3055        } catch (e) {
3056            logger$6.critical('Error while observing shadow root', e);
3057        }
3058    };
3059
3060
3061    ShadowDOMObserver.prototype.start = function() {
3062        if (this.observedShadowRoots) {
3063            return;
3064        }
3065
3066        if (!weakSetSupported()) {
3067            logger$6.critical('Shadow DOM observation unavailable: WeakSet not supported');
3068            return;
3069        }
3070
3071        this.observedShadowRoots = new WeakSet();
3072    };
3073
3074    ShadowDOMObserver.prototype.stop = function() {
3075        if (!this.observedShadowRoots) {
3076            return;
3077        }
3078
3079        for (var i = 0; i < this.shadowObservers.length; i++) {
3080            try {
3081                this.shadowObservers[i].disconnect();
3082            } catch (e) {
3083                logger$6.critical('Error while disconnecting shadow DOM observer', e);
3084            }
3085        }
3086        this.shadowObservers = [];
3087        this.observedShadowRoots = null;
3088    };
3089
3090    /** @const */ var DEFAULT_DEAD_CLICK_TIMEOUT_MS = 500;
3091    /** @const */ var INTERACTION_EVENTS = [EV_CHANGE, EV_INPUT, EV_SUBMIT, EV_SELECT, EV_TOGGLE];
3092    /** @const */ var LAYOUT_EVENTS = [EV_SCROLLEND];
3093    /** @const */ var NAVIGATION_EVENTS = [EV_HASHCHANGE];
3094    /** @const */ var MUTATION_OBSERVER_CONFIG = {
3095        characterData: true,
3096        childList: true,
3097        subtree: true,
3098        attributes: true,
3099        attributeFilter: ['style', 'class', 'hidden', 'checked', 'selected', 'value', 'display', 'visibility']
3100    };
3101
3102
3103    function DeadClickTracker(onDeadClickCallback) {
3104        this.eventListeners = [];
3105        this.mutationObserver = null;
3106        this.shadowDOMObserver = null;
3107
3108        this.isTracking = false;
3109        this.lastChangeEventTimestamp = 0;
3110        this.pendingClicks = [];
3111        this.onDeadClickCallback = onDeadClickCallback;
3112        this.processingActive = false;
3113        this.processingTimeout = null;
3114    }
3115
3116
3117    DeadClickTracker.prototype.addClick = function(event) {
3118        var element = this.shadowDOMObserver && this.shadowDOMObserver.getEventTarget(event);
3119
3120        if (!element) {
3121            element = event['target'] || event['srcElement'];
3122        }
3123
3124        if (!element || isDefinitelyNonInteractive(element)) {
3125            return false;
3126        }
3127
3128        if (this.shadowDOMObserver) {
3129            this.shadowDOMObserver.observeFromEvent(event);
3130        }
3131        this.pendingClicks.push({
3132            element: element,
3133            event: event,
3134            timestamp: Date.now()
3135        });
3136        return true;
3137    };
3138
3139    DeadClickTracker.prototype.trackClick = function(event, config) {
3140        if (!this.isTracking) {
3141            return false;
3142        }
3143
3144        var added = this.addClick(event);
3145        if (added) {
3146            this.triggerProcessing(config);
3147        }
3148        return added;
3149    };
3150
3151    DeadClickTracker.prototype.getDeadClicks = function(config) {
3152        if (this.pendingClicks.length === 0) {
3153            return [];
3154        }
3155
3156        var timeoutMs = config['timeout_ms'];
3157        var now = Date.now();
3158        var clicksToEvaluate = this.pendingClicks.slice(); // Copy array
3159        this.pendingClicks = []; // Clear original
3160
3161        var deadClicks = [];
3162
3163        for (var i = 0; i < clicksToEvaluate.length; i++) {
3164            var click = clicksToEvaluate[i];
3165
3166            if (now - click.timestamp >= timeoutMs) {
3167                // Click has exceeded timeout, check if it's dead by looking for changes after this specific click
3168                if (!this.hasChangesAfter(click.timestamp)) {
3169                    deadClicks.push(click);
3170                }
3171            } else {
3172                // Still pending - add back
3173                this.pendingClicks.push(click);
3174            }
3175        }
3176
3177        return deadClicks;
3178    };
3179
3180    DeadClickTracker.prototype.hasChangesAfter = function(timestamp) {
3181        // 100ms tolerance for race condition between when we record the click and the change event
3182        return this.lastChangeEventTimestamp >= (timestamp - 100);
3183    };
3184
3185    DeadClickTracker.prototype.recordChangeEvent = function() {
3186        this.lastChangeEventTimestamp = Date.now();
3187    };
3188
3189    DeadClickTracker.prototype.triggerProcessing = function(config) {
3190        // Prevent multiple concurrent processing chains
3191        if (this.processingActive) {
3192            return;
3193        }
3194        this.processingActive = true;
3195        this.processRecursively(config);
3196    };
3197
3198    DeadClickTracker.prototype.processRecursively = function(config) {
3199        if (!this.isTracking || !this.onDeadClickCallback) {
3200            this.processingActive = false;
3201            return;
3202        }
3203
3204        var timeoutMs = config['timeout_ms'];
3205        var self = this;
3206
3207        this.processingTimeout = setTimeout(function() {
3208            if (!self.processingActive) {
3209                return;
3210            }
3211
3212            var deadClicks = self.getDeadClicks(config);
3213
3214            for (var i = 0; i < deadClicks.length; i++) {
3215                self.onDeadClickCallback(deadClicks[i].event);
3216            }
3217
3218            if (self.pendingClicks.length > 0) {
3219                self.processRecursively(config);
3220            } else {
3221                self.processingActive = false;
3222            }
3223        }, timeoutMs);
3224    };
3225
3226    DeadClickTracker.prototype.startTracking = function() {
3227        if (this.isTracking) {
3228            return;
3229        }
3230
3231        this.isTracking = true;
3232
3233        var self = this;
3234
3235        INTERACTION_EVENTS.forEach(function(event) {
3236            var handler = function() {
3237                self.recordChangeEvent();
3238            };
3239            document.addEventListener(event, handler, { capture: true, passive: true });
3240            self.eventListeners.push({ target: document, event: event, handler: handler, options: { capture: true, passive: true } });
3241        });
3242        NAVIGATION_EVENTS.forEach(function(event) {
3243            var handler = function() {
3244                self.recordChangeEvent();
3245            };
3246            window.addEventListener(event, handler);
3247            self.eventListeners.push({ target: window, event: event, handler: handler });
3248        });
3249        LAYOUT_EVENTS.forEach(function(event) {
3250            var handler = function() {
3251                self.recordChangeEvent();
3252            };
3253            window.addEventListener(event, handler, { passive: true });
3254            self.eventListeners.push({ target: window, event: event, handler: handler, options: { passive: true } });
3255        });
3256        var selectionHandler = function() {
3257            self.recordChangeEvent();
3258        };
3259        document.addEventListener('selectionchange', selectionHandler);
3260        self.eventListeners.push({ target: document, event: 'selectionchange', handler: selectionHandler });
3261
3262        // Set up MutationObserver
3263        if (window.MutationObserver) {
3264            try {
3265                this.mutationObserver = new window.MutationObserver(function() {
3266                    self.recordChangeEvent();
3267                });
3268
3269                this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
3270            } catch (e) {
3271                logger$6.critical('Error while setting up mutation observer', e);
3272            }
3273        }
3274
3275        // Set up Shadow DOM observer
3276        if (window.customElements) {
3277            try {
3278                this.shadowDOMObserver = new ShadowDOMObserver(
3279                    function() {
3280                        self.recordChangeEvent();
3281                    },
3282                    MUTATION_OBSERVER_CONFIG
3283                );
3284                this.shadowDOMObserver.start();
3285            } catch (e) {
3286                logger$6.critical('Error while setting up shadow DOM observer', e);
3287                this.shadowDOMObserver = null;
3288            }
3289        }
3290    };
3291
3292    DeadClickTracker.prototype.stopTracking = function() {
3293        if (!this.isTracking) {
3294            return;
3295        }
3296
3297        this.isTracking = false;
3298        this.pendingClicks = [];
3299        this.lastChangeEventTimestamp = 0;
3300        this.processingActive = false;
3301
3302        if (this.processingTimeout) {
3303            clearTimeout(this.processingTimeout);
3304            this.processingTimeout = null;
3305        }
3306
3307        // Remove all event listeners
3308        for (var i = 0; i < this.eventListeners.length; i++) {
3309            var listener = this.eventListeners[i];
3310            try {
3311                listener.target.removeEventListener(listener.event, listener.handler, listener.options);
3312            } catch (e) {
3313                logger$6.critical('Error while removing event listener', e);
3314            }
3315        }
3316        this.eventListeners = [];
3317
3318        if (this.mutationObserver) {
3319            try {
3320                this.mutationObserver.disconnect();
3321            } catch (e) {
3322                logger$6.critical('Error while disconnecting mutation observer', e);
3323            }
3324            this.mutationObserver = null;
3325        }
3326
3327        if (this.shadowDOMObserver) {
3328            try {
3329                this.shadowDOMObserver.stop();
3330            } catch (e) {
3331                logger$6.critical('Error while stopping shadow DOM observer', e);
3332            }
3333            this.shadowDOMObserver = null;
3334        }
3335    };
3336
3337    var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
3338    var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
3339
3340    var PAGEVIEW_OPTION_FULL_URL = 'full-url';
3341    var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
3342    var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
3343
3344    var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
3345    var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
3346    var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
3347    var CONFIG_BLOCK_ATTRS = 'block_attrs';
3348    var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
3349    var CONFIG_BLOCK_SELECTORS = 'block_selectors';
3350    var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
3351    var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
3352    var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
3353    var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
3354    var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
3355    var CONFIG_TRACK_CLICK = 'click';
3356    var CONFIG_TRACK_DEAD_CLICK = 'dead_click';
3357    var CONFIG_TRACK_INPUT = 'input';
3358    var CONFIG_TRACK_PAGEVIEW = 'pageview';
3359    var CONFIG_TRACK_RAGE_CLICK = 'rage_click';
3360    var CONFIG_TRACK_SCROLL = 'scroll';
3361    var CONFIG_TRACK_PAGE_LEAVE = 'page_leave';
3362    var CONFIG_TRACK_SUBMIT = 'submit';
3363
3364    var CONFIG_DEFAULTS$1 = {};
3365    CONFIG_DEFAULTS$1[CONFIG_ALLOW_SELECTORS] = [];
3366    CONFIG_DEFAULTS$1[CONFIG_ALLOW_URL_REGEXES] = [];
3367    CONFIG_DEFAULTS$1[CONFIG_BLOCK_ATTRS] = [];
3368    CONFIG_DEFAULTS$1[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
3369    CONFIG_DEFAULTS$1[CONFIG_BLOCK_SELECTORS] = [];
3370    CONFIG_DEFAULTS$1[CONFIG_BLOCK_URL_REGEXES] = [];
3371    CONFIG_DEFAULTS$1[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
3372    CONFIG_DEFAULTS$1[CONFIG_CAPTURE_TEXT_CONTENT] = false;
3373    CONFIG_DEFAULTS$1[CONFIG_SCROLL_CAPTURE_ALL] = false;
3374    CONFIG_DEFAULTS$1[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
3375    CONFIG_DEFAULTS$1[CONFIG_TRACK_CLICK] = true;
3376    CONFIG_DEFAULTS$1[CONFIG_TRACK_DEAD_CLICK] = true;
3377    CONFIG_DEFAULTS$1[CONFIG_TRACK_INPUT] = true;
3378    CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
3379    CONFIG_DEFAULTS$1[CONFIG_TRACK_RAGE_CLICK] = true;
3380    CONFIG_DEFAULTS$1[CONFIG_TRACK_SCROLL] = true;
3381    CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGE_LEAVE] = false;
3382    CONFIG_DEFAULTS$1[CONFIG_TRACK_SUBMIT] = true;
3383
3384    var DEFAULT_PROPS = {
3385        '$mp_autocapture': true
3386    };
3387
3388    var MP_EV_CLICK = '$mp_click';
3389    var MP_EV_DEAD_CLICK = '$mp_dead_click';
3390    var MP_EV_INPUT = '$mp_input_change';
3391    var MP_EV_RAGE_CLICK = '$mp_rage_click';
3392    var MP_EV_SCROLL = '$mp_scroll';
3393    var MP_EV_SUBMIT = '$mp_submit';
3394    var MP_EV_PAGE_LEAVE = '$mp_page_leave';
3395
3396    /**
3397     * Autocapture: manages automatic event tracking
3398     * @constructor
3399     */
3400    var Autocapture = function(mp) {
3401        this.mp = mp;
3402        this.maxScrollViewDepth = 0;
3403        this.hasTrackedScrollSession = false;
3404        this.previousScrollHeight = 0;
3405    };
3406
3407    Autocapture.prototype.init = function() {
3408        if (!minDOMApisSupported()) {
3409            logger$6.critical('Autocapture unavailable: missing required DOM APIs');
3410            return;
3411        }
3412        this.initPageListeners();
3413        this.initPageviewTracking();
3414        this.initClickTracking();
3415        this.initDeadClickTracking();
3416        this.initInputTracking();
3417        this.initScrollTracking();
3418        this.initSubmitTracking();
3419        this.initRageClickTracking();
3420        this.initPageLeaveTracking();
3421    };
3422
3423    Autocapture.prototype.getFullConfig = function() {
3424        var autocaptureConfig = this.mp.get_config(AUTOCAPTURE_CONFIG_KEY);
3425        if (!autocaptureConfig) {
3426            // Autocapture is completely off
3427            return {};
3428        } else if (_.isObject(autocaptureConfig)) {
3429            return _.extend({}, CONFIG_DEFAULTS$1, autocaptureConfig);
3430        } else {
3431            // Autocapture config is non-object truthy value, return default
3432            return CONFIG_DEFAULTS$1;
3433        }
3434    };
3435
3436    Autocapture.prototype.getConfig = function(key) {
3437        return this.getFullConfig()[key];
3438    };
3439
3440    Autocapture.prototype.currentUrlBlocked = function() {
3441        var currentUrl = _.info.currentUrl();
3442
3443        var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
3444        if (allowUrlRegexes.length) {
3445            // we're using an allowlist, only track if current URL matches
3446            try {
3447                return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
3448            } catch (err) {
3449                logger$6.critical('Error while checking block URL regexes: ', err);
3450                return true;
3451            }
3452        }
3453
3454        var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
3455        if (!blockUrlRegexes || !blockUrlRegexes.length) {
3456            return false;
3457        }
3458
3459        try {
3460            return urlMatchesRegexList(currentUrl, blockUrlRegexes);
3461        } catch (err) {
3462            logger$6.critical('Error while checking block URL regexes: ', err);
3463            return true;
3464        }
3465    };
3466
3467    Autocapture.prototype.pageviewTrackingConfig = function() {
3468        // supports both autocapture config and old track_pageview config
3469        if (this.mp.get_config(AUTOCAPTURE_CONFIG_KEY)) {
3470            return this.getConfig(CONFIG_TRACK_PAGEVIEW);
3471        } else {
3472            return this.mp.get_config(LEGACY_PAGEVIEW_CONFIG_KEY);
3473        }
3474    };
3475
3476    // helper for event handlers
3477    Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
3478        if (this.currentUrlBlocked()) {
3479            return;
3480        }
3481
3482        var isCapturedForHeatMap = this.mp.is_recording_heatmap_data() && (
3483            (mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK)) ||
3484            (mpEventName === MP_EV_RAGE_CLICK && !this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK)) ||
3485            (mpEventName === MP_EV_DEAD_CLICK && !this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK))
3486        );
3487
3488        var props = getPropsForDOMEvent(ev, {
3489            allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
3490            allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
3491            blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
3492            blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
3493            blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
3494            captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
3495            captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT),
3496            capturedForHeatMap: isCapturedForHeatMap,
3497        });
3498        if (props) {
3499            _.extend(props, DEFAULT_PROPS);
3500            this.mp.track(mpEventName, props);
3501        }
3502    };
3503
3504    Autocapture.prototype.initPageListeners = function() {
3505        win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
3506        win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
3507
3508        if (!this.pageviewTrackingConfig() && !this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.get_config('record_heatmap_data')) {
3509            // These are all the configs that use these listeners
3510            return;
3511        }
3512
3513        this.listenerPopstate = function() {
3514            win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3515        };
3516        this.listenerHashchange = function() {
3517            win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3518        };
3519
3520        win.addEventListener(EV_POPSTATE, this.listenerPopstate);
3521        win.addEventListener(EV_HASHCHANGE, this.listenerHashchange);
3522        var nativePushState = win.history.pushState;
3523        if (typeof nativePushState === 'function') {
3524            win.history.pushState = function(state, unused, url) {
3525                nativePushState.call(win.history, state, unused, url);
3526                win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3527            };
3528        }
3529        var nativeReplaceState = win.history.replaceState;
3530        if (typeof nativeReplaceState === 'function') {
3531            win.history.replaceState = function(state, unused, url) {
3532                nativeReplaceState.call(win.history, state, unused, url);
3533                win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3534            };
3535        }
3536    };
3537
3538    Autocapture.prototype._getClickTrackingConfig = function(configKey) {
3539        var config = this.getConfig(configKey);
3540
3541        if (!config) {
3542            return null; // click tracking disabled
3543        }
3544
3545        if (config === true) {
3546            return {}; // use defaults
3547        }
3548
3549        if (typeof config === 'object') {
3550            return config; // use custom configuration
3551        }
3552
3553        return {}; // fallback to defaults for any other truthy value
3554    };
3555
3556    Autocapture.prototype._trackPageLeave = function(ev, currentUrl, currentScrollHeight) {
3557        if (this.hasTrackedScrollSession) {
3558            // User has navigated away already ending their impression.
3559            return;
3560        }
3561
3562        if (!this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.is_recording_heatmap_data()) {
3563            return;
3564        }
3565
3566        this.hasTrackedScrollSession = true;
3567        var viewportHeight = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3568        var scrollPercentage = Math.round(Math.max(this.maxScrollViewDepth - viewportHeight, 0) / (currentScrollHeight - viewportHeight) * 100);
3569        var foldLinePercentage = Math.round((viewportHeight / currentScrollHeight) * 100);
3570        if (currentScrollHeight <= viewportHeight) {
3571            // If the content fits within the viewport, consider it fully scrolled
3572            scrollPercentage = 100;
3573            foldLinePercentage = 100;
3574        }
3575
3576        var props = _.extend({
3577            '$max_scroll_view_depth': this.maxScrollViewDepth,
3578            '$max_scroll_percentage': scrollPercentage,
3579            '$fold_line_percentage': foldLinePercentage,
3580            '$scroll_height':  currentScrollHeight,
3581            '$event_type': ev.type,
3582            '$current_url': currentUrl || _.info.currentUrl(),
3583            '$viewportHeight': viewportHeight, // This is the fold line
3584            '$viewportWidth':  Math.max(document$1.documentElement.clientWidth, win.innerWidth || 0),
3585            '$captured_for_heatmap': this.mp.is_recording_heatmap_data()
3586        }, DEFAULT_PROPS);
3587
3588        // Send with beacon transport to ensure event is sent before unload
3589        this.mp.track(MP_EV_PAGE_LEAVE, props, {transport: 'sendBeacon'});
3590    };
3591
3592    Autocapture.prototype._initScrollDepthTracking = function() {
3593        win.removeEventListener(EV_SCROLL, this.listenerScrollDepth);
3594        win.removeEventListener(EV_SCROLLEND, this.listenerScrollDepth);
3595
3596        if (!this.mp.get_config('record_heatmap_data')) {
3597            return;
3598        }
3599
3600        logger$6.log('Initializing scroll depth tracking');
3601
3602        this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3603
3604        var updateScrollDepth = function() {
3605            if (this.currentUrlBlocked()) {
3606                return;
3607            }
3608            var scrollViewHeight = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0) + win.scrollY;
3609            if (scrollViewHeight > this.maxScrollViewDepth) {
3610                this.maxScrollViewDepth = scrollViewHeight;
3611            }
3612            this.previousScrollHeight = document$1.body.scrollHeight;
3613        }.bind(this);
3614
3615        var scrollEndPolyfill = getPolyfillScrollEndFunction(updateScrollDepth);
3616        this.listenerScrollDepth = scrollEndPolyfill.listener;
3617        win.addEventListener(scrollEndPolyfill.eventType, this.listenerScrollDepth);
3618    };
3619
3620    Autocapture.prototype.initClickTracking = function() {
3621        win.removeEventListener(EV_CLICK, this.listenerClick, true);
3622
3623        if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
3624            return;
3625        }
3626        logger$6.log('Initializing click tracking');
3627
3628        this.listenerClick = function(ev) {
3629            if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
3630                return;
3631            }
3632            this.trackDomEvent(ev, MP_EV_CLICK);
3633        }.bind(this);
3634        win.addEventListener(EV_CLICK, this.listenerClick, true);
3635    };
3636
3637    Autocapture.prototype.initDeadClickTracking = function() {
3638        var deadClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK);
3639
3640        if (!deadClickConfig && !this.mp.get_config('record_heatmap_data')) {
3641            this.stopDeadClickTracking();
3642            return;
3643        }
3644
3645        logger$6.log('Initializing dead click tracking');
3646        if (!this._deadClickTracker) {
3647            this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
3648                this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
3649            }.bind(this));
3650            this._deadClickTracker.startTracking();
3651        }
3652
3653        if (!this.listenerDeadClick) {
3654            this.listenerDeadClick = function(ev) {
3655                var currentDeadClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK);
3656                if (!currentDeadClickConfig && !this.mp.is_recording_heatmap_data()) {
3657                    return;
3658                }
3659                if (this.currentUrlBlocked()) {
3660                    return;
3661                }
3662                // Normalize config to ensure timeout_ms is always set
3663                var normalizedConfig = currentDeadClickConfig || {};
3664                if (!normalizedConfig['timeout_ms']) {
3665                    normalizedConfig['timeout_ms'] = DEFAULT_DEAD_CLICK_TIMEOUT_MS;
3666                }
3667                this._deadClickTracker.trackClick(ev, normalizedConfig);
3668            }.bind(this);
3669            win.addEventListener(EV_CLICK, this.listenerDeadClick, true);
3670        }
3671    };
3672
3673    Autocapture.prototype.initInputTracking = function() {
3674        win.removeEventListener(EV_CHANGE, this.listenerChange, true);
3675
3676        if (!this.getConfig(CONFIG_TRACK_INPUT)) {
3677            return;
3678        }
3679        logger$6.log('Initializing input tracking');
3680
3681        this.listenerChange = function(ev) {
3682            if (!this.getConfig(CONFIG_TRACK_INPUT)) {
3683                return;
3684            }
3685            this.trackDomEvent(ev, MP_EV_INPUT);
3686        }.bind(this);
3687        win.addEventListener(EV_CHANGE, this.listenerChange, true);
3688    };
3689
3690    Autocapture.prototype.initPageviewTracking = function() {
3691        win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
3692
3693        if (!this.pageviewTrackingConfig() && !this.mp.get_config('record_heatmap_data')) {
3694            return;
3695        }
3696        logger$6.log('Initializing pageview tracking');
3697
3698        var previousTrackedUrl = '';
3699        var tracked = false;
3700        // Track initial pageview if pageview tracking enabled OR heatmap recording is active
3701        if ((this.pageviewTrackingConfig() || this.mp.is_recording_heatmap_data()) && !this.currentUrlBlocked()) {
3702            tracked = this.mp.track_pageview(DEFAULT_PROPS);
3703        }
3704        if (tracked) {
3705            previousTrackedUrl = _.info.currentUrl();
3706        }
3707
3708        this.listenerLocationchange = safewrap(function() {
3709            if (this.currentUrlBlocked()) {
3710                return;
3711            }
3712
3713            var currentUrl = _.info.currentUrl();
3714            var shouldTrack = false;
3715            var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
3716            var trackPageviewOption = this.pageviewTrackingConfig();
3717            if (!trackPageviewOption && this.mp.is_recording_heatmap_data()) {
3718                trackPageviewOption = PAGEVIEW_OPTION_FULL_URL;
3719            }
3720
3721            if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
3722                shouldTrack = currentUrl !== previousTrackedUrl;
3723            } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
3724                shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
3725            } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
3726                shouldTrack = didPathChange;
3727            }
3728
3729            if (shouldTrack) {
3730                var tracked = this.mp.track_pageview(DEFAULT_PROPS);
3731                if (tracked) {
3732                    previousTrackedUrl = currentUrl;
3733                }
3734                if (didPathChange) {
3735                    this.lastScrollCheckpoint = 0;
3736                    logger$6.log('Path change: re-initializing scroll depth checkpoints');
3737                }
3738            }
3739        }.bind(this));
3740        win.addEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
3741    };
3742
3743    Autocapture.prototype.initRageClickTracking = function() {
3744        win.removeEventListener(EV_CLICK, this.listenerRageClick, true);
3745
3746        var rageClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK);
3747        if (!rageClickConfig && !this.mp.get_config('record_heatmap_data')) {
3748            return;
3749        }
3750
3751        logger$6.log('Initializing rage click tracking');
3752        if (!this._rageClickTracker) {
3753            this._rageClickTracker = new RageClickTracker();
3754        }
3755
3756        this.listenerRageClick = function(ev) {
3757            var currentRageClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK);
3758            if (!currentRageClickConfig && !this.mp.is_recording_heatmap_data()) {
3759                return;
3760            }
3761
3762            if (this.currentUrlBlocked()) {
3763                return;
3764            }
3765
3766            if (this._rageClickTracker.isRageClick(ev, currentRageClickConfig)) {
3767                this.trackDomEvent(ev, MP_EV_RAGE_CLICK);
3768            }
3769        }.bind(this);
3770        win.addEventListener(EV_CLICK, this.listenerRageClick, true);
3771    };
3772
3773    Autocapture.prototype.initScrollTracking = function() {
3774        win.removeEventListener(EV_SCROLLEND, this.listenerScroll);
3775        win.removeEventListener(EV_SCROLL, this.listenerScroll);
3776
3777
3778        if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3779            return;
3780        }
3781        logger$6.log('Initializing scroll tracking');
3782        this.lastScrollCheckpoint = 0;
3783
3784        var scrollTrackFunction = function() {
3785            if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3786                return;
3787            }
3788            if (this.currentUrlBlocked()) {
3789                return;
3790            }
3791
3792            var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
3793            var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
3794                .slice()
3795                .sort(function(a, b) { return a - b; });
3796
3797            var scrollTop = win.scrollY;
3798            var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
3799            try {
3800                var scrollHeight = document$1.body.scrollHeight;
3801                var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
3802                props['$scroll_height'] = scrollHeight;
3803                props['$scroll_percentage'] = scrollPercentage;
3804                if (scrollPercentage > this.lastScrollCheckpoint) {
3805                    for (var i = 0; i < scrollCheckpoints.length; i++) {
3806                        var checkpoint = scrollCheckpoints[i];
3807                        if (
3808                            scrollPercentage >= checkpoint &&
3809                            this.lastScrollCheckpoint < checkpoint
3810                        ) {
3811                            props['$scroll_checkpoint'] = checkpoint;
3812                            this.lastScrollCheckpoint = checkpoint;
3813                            shouldTrack = true;
3814                        }
3815                    }
3816                }
3817            } catch (err) {
3818                logger$6.critical('Error while calculating scroll percentage', err);
3819            }
3820            if (shouldTrack) {
3821                this.mp.track(MP_EV_SCROLL, props);
3822            }
3823        }.bind(this);
3824
3825        var scrollEndPolyfill = getPolyfillScrollEndFunction(scrollTrackFunction);
3826        this.listenerScroll = scrollEndPolyfill.listener;
3827        win.addEventListener(scrollEndPolyfill.eventType, this.listenerScroll);
3828    };
3829
3830    Autocapture.prototype.initSubmitTracking = function() {
3831        win.removeEventListener(EV_SUBMIT, this.listenerSubmit, true);
3832
3833        if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
3834            return;
3835        }
3836        logger$6.log('Initializing submit tracking');
3837
3838        this.listenerSubmit = function(ev) {
3839            if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
3840                return;
3841            }
3842            this.trackDomEvent(ev, MP_EV_SUBMIT);
3843        }.bind(this);
3844        win.addEventListener(EV_SUBMIT, this.listenerSubmit, true);
3845    };
3846
3847    Autocapture.prototype.initPageLeaveTracking = function() {
3848        // Capture page_leave both when the user navigates away from the page (visibilitychange) as well
3849        // as when they navigate to a different page within the SPA (popstate/pushstate/hashchange).
3850        document$1.removeEventListener(EV_VISIBILITYCHANGE, this.listenerPageLeaveVisibilitychange);
3851        win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerPageLeaveLocationchange);
3852        win.removeEventListener(EV_LOAD, this.listenerPageLoad);
3853
3854        if (!this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.get_config('record_heatmap_data')) {
3855            return;
3856        }
3857
3858        logger$6.log('Initializing page visibility tracking.');
3859        this._initScrollDepthTracking();
3860        var previousTrackedUrl = _.info.currentUrl();
3861
3862        // Initialize previousScrollHeight on `load` which handles async loading
3863        // https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
3864        this.listenerPageLoad = function() {
3865            this.previousScrollHeight = document$1.body.scrollHeight;
3866        }.bind(this);
3867        win.addEventListener(EV_LOAD, this.listenerPageLoad);
3868
3869        // Track page navigation events similar to how initPageviewTracking does it
3870        this.listenerPageLeaveLocationchange = safewrap(function(ev) {
3871            if (this.currentUrlBlocked()) {
3872                return;
3873            }
3874
3875            var currentUrl = _.info.currentUrl();
3876            // Track all URL changes including query string or fragment changes as separate scroll sessions
3877            var shouldTrack = currentUrl !== previousTrackedUrl;
3878
3879            if (shouldTrack) {
3880                this._trackPageLeave(ev, previousTrackedUrl, this.previousScrollHeight);
3881                previousTrackedUrl = currentUrl;
3882                // Fragment navigation should call scroll(end) and trigger listener, don't add window.scrollY here.
3883                this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3884                this.previousScrollHeight = document$1.body.scrollHeight;
3885                this.hasTrackedScrollSession = false;
3886            }
3887        }.bind(this));
3888        win.addEventListener(EV_MP_LOCATION_CHANGE, this.listenerPageLeaveLocationchange);
3889
3890        this.listenerPageLeaveVisibilitychange = function(ev) {
3891            if (document$1.hidden) {
3892                this._trackPageLeave(ev, previousTrackedUrl, this.previousScrollHeight);
3893            }
3894        }.bind(this);
3895        document$1.addEventListener(EV_VISIBILITYCHANGE, this.listenerPageLeaveVisibilitychange);
3896    };
3897
3898    Autocapture.prototype.stopDeadClickTracking = function() {
3899        if (this.listenerDeadClick) {
3900            win.removeEventListener(EV_CLICK, this.listenerDeadClick, true);
3901            this.listenerDeadClick = null;
3902        }
3903
3904        if (this._deadClickTracker) {
3905            this._deadClickTracker.stopTracking();
3906            this._deadClickTracker = null;
3907        }
3908    };
3909
3910    // TODO integrate error_reporter from mixpanel instance
3911    safewrapClass(Autocapture);
3912
3913    /**
3914     * Get the promise-based targeting loader
3915     * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
3916     * @param {string} targetingSrc - URL to targeting bundle
3917     * @returns {Promise} Promise that resolves with targeting library
3918     */
3919    var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
3920        // Return existing promise if already initialized or loading
3921        if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
3922            return win[TARGETING_GLOBAL_NAME];
3923        }
3924
3925        // Create loading promise and set it as the global immediately
3926        // This makes minified build behavior consistent with dev/CJS builds
3927        win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
3928            loadExtraBundle(targetingSrc, resolve);
3929        }).then(function () {
3930            var p = win[TARGETING_GLOBAL_NAME];
3931            if (p && typeof p.then === 'function') {
3932                return p;
3933            }
3934            throw new Error('targeting failed to load');
3935        }).catch(function (err) {
3936            delete win[TARGETING_GLOBAL_NAME];
3937            throw err;
3938        });
3939
3940        return win[TARGETING_GLOBAL_NAME];
3941    };
3942
3943    /**
3944     * @type {import('./wrapper').StorageWrapper}
3945     */
3946    var IDBStorageWrapper = function (dbName, storeName, versionData) {
3947        this.dbName = dbName;
3948        this.storeName = storeName;
3949        this.version = versionData.version;
3950        this.storeNamesInDb = versionData.storeNames;
3951        /**
3952         * @type {Promise<IDBDatabase>|null}
3953         */
3954        this.dbPromise = null;
3955    };
3956
3957    IDBStorageWrapper.prototype._openDb = function () {
3958        var dbName = this.dbName;
3959        var version = this.version;
3960        var storeNamesInDb = this.storeNamesInDb;
3961        return new PromisePolyfill(function (resolve, reject) {
3962            var openRequest = win.indexedDB.open(dbName, version);
3963            openRequest['onerror'] = function () {
3964                reject(openRequest.error);
3965            };
3966
3967            openRequest['onsuccess'] = function () {
3968                resolve(openRequest.result);
3969            };
3970
3971            openRequest['onupgradeneeded'] = function (ev) {
3972                var db = ev.target.result;
3973
3974                storeNamesInDb.forEach(function (storeName) {
3975                    if (!db.objectStoreNames.contains(storeName)) {
3976                        db.createObjectStore(storeName);
3977                    }
3978                });
3979            };
3980        });
3981    };
3982
3983    IDBStorageWrapper.prototype.init = function () {
3984        if (!win.indexedDB) {
3985            return PromisePolyfill.reject('indexedDB is not supported in this browser');
3986        }
3987
3988        if (!this.dbPromise) {
3989            this.dbPromise = this._openDb();
3990        }
3991
3992        return this.dbPromise
3993            .then(function (dbOrError) {
3994                if (dbOrError instanceof win['IDBDatabase']) {
3995                    return PromisePolyfill.resolve();
3996                } else {
3997                    return PromisePolyfill.reject(dbOrError);
3998                }
3999            });
4000    };
4001
4002    IDBStorageWrapper.prototype.isInitialized = function () {
4003        return !!this.dbPromise;
4004    };
4005
4006    /**
4007     * @param {IDBTransactionMode} mode
4008     * @param {function(IDBObjectStore): void} storeCb
4009     */
4010    IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
4011        var storeName = this.storeName;
4012        var doTransaction = function (db) {
4013            return new PromisePolyfill(function (resolve, reject) {
4014                var transaction = db.transaction(storeName, mode);
4015                transaction.oncomplete = function () {
4016                    resolve(transaction);
4017                };
4018                transaction.onabort = transaction.onerror = function () {
4019                    reject(transaction.error);
4020                };
4021
4022                storeCb(transaction.objectStore(storeName));
4023            });
4024        };
4025
4026        return this.dbPromise
4027            .then(doTransaction)
4028            .catch(function (err) {
4029                if (err && err['name'] === 'InvalidStateError') {
4030                    // try reopening the DB if the connection is closed
4031                    this.dbPromise = this._openDb();
4032                    return this.dbPromise.then(doTransaction);
4033                } else {
4034                    return PromisePolyfill.reject(err);
4035                }
4036            }.bind(this));
4037    };
4038
4039    IDBStorageWrapper.prototype.setItem = function (key, value) {
4040        return this.makeTransaction('readwrite', function (objectStore) {
4041            objectStore.put(value, key);
4042        });
4043    };
4044
4045    IDBStorageWrapper.prototype.getItem = function (key) {
4046        var req;
4047        return this.makeTransaction('readonly', function (objectStore) {
4048            req = objectStore.get(key);
4049        }).then(function () {
4050            return req.result;
4051        });
4052    };
4053
4054    IDBStorageWrapper.prototype.removeItem = function (key) {
4055        return this.makeTransaction('readwrite', function (objectStore) {
4056            objectStore.delete(key);
4057        });
4058    };
4059
4060    IDBStorageWrapper.prototype.getAll = function () {
4061        var req;
4062        return this.makeTransaction('readonly', function (objectStore) {
4063            req = objectStore.getAll();
4064        }).then(function () {
4065            return req.result;
4066        });
4067    };
4068
4069    var logger$5 = console_with_prefix('flags');
4070
4071    var MIXPANEL_FLAGS_DB_NAME = 'mixpanelFlagsDb';
4072    var FLAGS_STORE_NAME = 'mixpanelFlags';
4073
4074    // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
4075    var FLAGS_VERSION_DATA = { version: 1, storeNames: [FLAGS_STORE_NAME] };
4076
4077    var PERSISTED_VARIANTS_KEY_PREFIX = 'persisted_variants_for_';
4078    var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
4079
4080    var VariantLookupPolicy = Object.freeze({
4081        NETWORK_ONLY: 'networkOnly',
4082        NETWORK_FIRST: 'networkFirst',
4083        PERSISTENCE_UNTIL_NETWORK_SUCCESS: 'persistenceUntilNetworkSuccess'
4084    });
4085
4086    var VALID_POLICIES = [
4087        VariantLookupPolicy.NETWORK_ONLY,
4088        VariantLookupPolicy.NETWORK_FIRST,
4089        VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS
4090    ];
4091
4092    /**
4093     * Module for handling the storage and retrieval of persisted feature flag variants.
4094     */
4095    var FeatureFlagPersistence = function(persistenceConfig, token, isGloballyDisabled) {
4096        this.idb = new IDBStorageWrapper(MIXPANEL_FLAGS_DB_NAME, FLAGS_STORE_NAME, FLAGS_VERSION_DATA);
4097        this.persistenceConfig = persistenceConfig;
4098        this.persistedVariantsKey = PERSISTED_VARIANTS_KEY_PREFIX + token;
4099        this.isGloballyDisabled = isGloballyDisabled || function() { return false; };
4100    };
4101
4102    FeatureFlagPersistence.prototype.getPolicy = function() {
4103        if (this.isGloballyDisabled() || !this._isConfigValid()) {
4104            return VariantLookupPolicy.NETWORK_ONLY;
4105        }
4106        return this.persistenceConfig['variantLookupPolicy'];
4107    };
4108
4109    FeatureFlagPersistence.prototype.getTtlMs = function() {
4110        if (!this._isConfigValid()) {
4111            return DEFAULT_TTL_MS;
4112        }
4113        var configuredTtl = this.persistenceConfig['persistenceTtlMs'];
4114        return (configuredTtl === undefined || configuredTtl === null) ? DEFAULT_TTL_MS : configuredTtl;
4115    };
4116
4117    FeatureFlagPersistence.prototype._isConfigValid = function() {
4118        var config = this.persistenceConfig;
4119        if (!config) {
4120            return false;
4121        }
4122
4123        if (VALID_POLICIES.indexOf(config['variantLookupPolicy']) === -1) {
4124            logger$5.error('Invalid variantLookupPolicy:', config['variantLookupPolicy']);
4125            return false;
4126        }
4127
4128        if (config['persistenceTtlMs'] !== undefined &&
4129            config['persistenceTtlMs'] !== null &&
4130            config['persistenceTtlMs'] <= 0) {
4131            logger$5.error('If provided, persistenceTtlMs must be a positive number. Provided value:', config['persistenceTtlMs']);
4132            return false;
4133        }
4134
4135        return true;
4136    };
4137
4138    FeatureFlagPersistence.prototype.loadFlagsFromStorage = function(context) {
4139        var clearAndReturnNull = _.bind(function() {
4140            return this.clear().then(function() { return null; }).catch(function() { return null; });
4141        }, this);
4142
4143        if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
4144            return clearAndReturnNull();
4145        }
4146
4147        var ttlMs = this.getTtlMs();
4148
4149        return this.idb.init().then(_.bind(function() {
4150            return this.idb.getItem(this.persistedVariantsKey);
4151        }, this)).then(_.bind(function(data) {
4152            if (!data) {
4153                logger$5.log('No persisted variants found in IndexedDB');
4154                return null;
4155            }
4156
4157            if (ttlMs && Date.now() - data['persistedAt'] >= ttlMs) {
4158                logger$5.log('Persisted variants are expiring');
4159                return null;
4160            }
4161
4162            if (!context || data['distinctId'] !== context['distinct_id']) {
4163                logger$5.log('Persisted variants found, but for a different distinct_id so clearing.');
4164                return clearAndReturnNull();
4165            }
4166
4167            var persistedFlags = new Map();
4168            _.each(data['flagVariants'], function(variantData, key) {
4169                persistedFlags.set(key, {
4170                    'key': variantData['variant_key'],
4171                    'value': variantData['variant_value'],
4172                    'experiment_id': variantData['experiment_id'],
4173                    'is_experiment_active': variantData['is_experiment_active'],
4174                    'is_qa_tester': variantData['is_qa_tester'],
4175                    'variant_source': 'persistence',
4176                    'persisted_at_in_ms': data['persistedAt'],
4177                    'ttl_in_ms': ttlMs
4178                });
4179            });
4180
4181            logger$5.log('Loaded', persistedFlags.size, 'variants from IndexedDB for distinct_id', data['distinctId']);
4182
4183            return {
4184                flags: persistedFlags,
4185                pendingFirstTimeEvents: data['pendingFirstTimeEvents'] || {},
4186                persistedAtMs: data['persistedAt'],
4187                ttlMs: ttlMs
4188            };
4189        }, this)).catch(_.bind(function(error) {
4190            logger$5.error('Failed to load persisted variants from IndexedDB, so clearing', error);
4191            return clearAndReturnNull();
4192        }, this));
4193    };
4194
4195    FeatureFlagPersistence.prototype.save = function(context, flagsMap, pendingFirstTimeEvents) {
4196        if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
4197            return Promise.resolve();
4198        }
4199
4200        var flagVariants = {};
4201        flagsMap.forEach(function(variant, key) {
4202            flagVariants[key] = {
4203                'variant_key': variant['key'],
4204                'variant_value': variant['value'],
4205                'experiment_id': variant['experiment_id'],
4206                'is_experiment_active': variant['is_experiment_active'],
4207                'is_qa_tester': variant['is_qa_tester']
4208            };
4209        });
4210
4211        var data = {
4212            'persistedAt': Date.now(),
4213            'distinctId': context && context['distinct_id'],
4214            'context': context,
4215            'flagVariants': flagVariants,
4216            'pendingFirstTimeEvents': pendingFirstTimeEvents || {}
4217        };
4218
4219        return this.idb.init().then(_.bind(function() {
4220            return this.idb.setItem(this.persistedVariantsKey, data);
4221        }, this)).then(function() {
4222            logger$5.log('Saved', flagsMap.size, 'variants to IndexedDB for distinct_id', data['distinctId']);
4223        }).catch(function(error) {
4224            logger$5.error('Failed to persist variants to IndexedDB:', error);
4225        });
4226    };
4227
4228    FeatureFlagPersistence.prototype.clear = function() {
4229        if (this.isGloballyDisabled()) {
4230            return Promise.resolve();
4231        }
4232        return this.idb.init().then(_.bind(function() {
4233            return this.idb.removeItem(this.persistedVariantsKey);
4234        }, this)).then(function() {
4235            logger$5.log('Cleared persisted variants from IndexedDB');
4236        }).catch(function(error) {
4237            logger$5.error('Failed to clear persisted variants from IndexedDB:', error);
4238        });
4239    };
4240
4241    var logger$4 = console_with_prefix('flags');
4242    var FLAGS_CONFIG_KEY = 'flags';
4243
4244    var CONFIG_CONTEXT = 'context';
4245    var CONFIG_PERSISTENCE = 'persistence';
4246    var CONFIG_DEFAULTS = {};
4247    CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
4248
4249    /**
4250     * Generate a unique key for a pending first-time event
4251     * @param {string} flagKey - The flag key
4252     * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
4253     * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
4254     */
4255    var getPendingEventKey = function(flagKey, firstTimeEventHash) {
4256        return flagKey + ':' + firstTimeEventHash;
4257    };
4258
4259    /**
4260     * Extract the flag key from a pending event key
4261     * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
4262     * @returns {string} The flag key portion
4263     */
4264    var getFlagKeyFromPendingEventKey = function(eventKey) {
4265        return eventKey.split(':')[0];
4266    };
4267
4268    // Fallback reasons surfaced via a NEW `fallback_reason` field on the returned
4269    // variant. `variant_source` stays as the coarse 'network' | 'persistence' |
4270    // 'fallback' values shipped in 2.80.0 — adding a sibling field rather than
4271    // extending the existing one keeps every consumer of the published type
4272    // working unchanged. Values match mixpanel-php so the OpenFeature wrapper
4273    // dispatch is consistent across SDKs.
4274    var FALLBACK_REASON_FLAG_NOT_FOUND = 'FLAG_NOT_FOUND';
4275    var FALLBACK_REASON_NOT_READY = 'NOT_READY';
4276    var FALLBACK_REASON_BACKEND_ERROR = 'BACKEND_ERROR';
4277
4278    var withFallbackSource = function(fallback, reason) {
4279        var extras = {'variant_source': 'fallback'};
4280        if (reason) {
4281            extras['fallback_reason'] = reason;
4282        }
4283        if (_.isObject(fallback)) {
4284            return _.extend({}, fallback, extras);
4285        }
4286        return _.extend({'value': fallback}, extras);
4287    };
4288
4289    /**
4290     * FeatureFlagManager: support for Mixpanel's feature flagging product
4291     * @constructor
4292     */
4293    var FeatureFlagManager = function(initOptions) {
4294        this.fetch = win['fetch'];
4295        this.getFullApiRoute = initOptions.getFullApiRoute;
4296        this.getMpConfig = initOptions.getConfigFunc;
4297        this.setMpConfig = initOptions.setConfigFunc;
4298        this.getMpProperty = initOptions.getPropertyFunc;
4299        this.track = initOptions.trackingFunc;
4300        this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
4301        this.targetingSrc = initOptions.targetingSrc || '';
4302    };
4303
4304    FeatureFlagManager.prototype.init = function() {
4305        if (!this.minApisSupported()) {
4306            logger$4.critical('Feature Flags unavailable: missing minimum required APIs');
4307            return;
4308        }
4309
4310        this.flags = null;
4311        this.trackedFeatures = new Set();
4312        this.pendingFirstTimeEvents = {};
4313        this.activatedFirstTimeEvents = {};
4314        this._loadedPersistedAtMs = null;
4315        this._loadedTtlMs = null;
4316
4317        this.persistence = new FeatureFlagPersistence(
4318            this.getConfig(CONFIG_PERSISTENCE),
4319            this.getMpConfig('token'),
4320            _.bind(function() { return this.getMpConfig('disable_persistence'); }, this)
4321        );
4322
4323        this.persistenceLoadedPromise = this.persistence.loadFlagsFromStorage(this._buildContext())
4324            .then(_.bind(function(loaded) {
4325                if (loaded) {
4326                    this.flags = loaded.flags;
4327                    this.pendingFirstTimeEvents = loaded.pendingFirstTimeEvents;
4328                    this._loadedPersistedAtMs = loaded.persistedAtMs;
4329                    this._loadedTtlMs = loaded.ttlMs;
4330                }
4331            }, this));
4332
4333        return this.persistenceLoadedPromise
4334            .then(_.bind(function() {
4335                return this.fetchFlags();
4336            }, this))
4337            .catch(function() {
4338                logger$4.error('Error initializing feature flags');
4339            });
4340    };
4341
4342    FeatureFlagManager.prototype._buildContext = function() {
4343        return _.extend(
4344            {'distinct_id': this.getMpProperty('distinct_id'), 'device_id': this.getMpProperty('$device_id')},
4345            this.getConfig(CONFIG_CONTEXT)
4346        );
4347    };
4348
4349    FeatureFlagManager.prototype.reset = function() {
4350        if (!this.persistence) {
4351            return Promise.resolve();
4352        }
4353
4354        this.flags = null;
4355        this.pendingFirstTimeEvents = {};
4356        this.activatedFirstTimeEvents = {};
4357        this.trackedFeatures = new Set();
4358        this.fetchPromise = null;
4359        this._fetchInProgressStartTime = null;
4360        this._loadedPersistedAtMs = null;
4361        this._loadedTtlMs = null;
4362
4363        return this.persistence.clear().then(_.bind(function() {
4364            return this.fetchFlags();
4365        }, this)).catch(function() {
4366            logger$4.error('Error during flags reset');
4367        });
4368    };
4369
4370    FeatureFlagManager.prototype.getFullConfig = function() {
4371        var ffConfig = this.getMpConfig(FLAGS_CONFIG_KEY);
4372        if (!ffConfig) {
4373            // flags are completely off
4374            return {};
4375        } else if (_.isObject(ffConfig)) {
4376            return _.extend({}, CONFIG_DEFAULTS, ffConfig);
4377        } else {
4378            // config is non-object truthy value, return default
4379            return CONFIG_DEFAULTS;
4380        }
4381    };
4382
4383    FeatureFlagManager.prototype.getConfig = function(key) {
4384        return this.getFullConfig()[key];
4385    };
4386
4387    FeatureFlagManager.prototype.isSystemEnabled = function() {
4388        return !!this.getMpConfig(FLAGS_CONFIG_KEY);
4389    };
4390
4391    FeatureFlagManager.prototype.updateContext = function(newContext, options) {
4392        if (!this.isSystemEnabled()) {
4393            logger$4.critical('Feature Flags not enabled, cannot update context');
4394            return Promise.resolve();
4395        }
4396
4397        var ffConfig = this.getMpConfig(FLAGS_CONFIG_KEY);
4398        if (!_.isObject(ffConfig)) {
4399            ffConfig = {};
4400        }
4401        var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
4402        ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
4403
4404        var configUpdate = {};
4405        configUpdate[FLAGS_CONFIG_KEY] = ffConfig;
4406        this.setMpConfig(configUpdate);
4407        return this.fetchFlags().catch(function() {
4408            logger$4.error('Error fetching flags during updateContext');
4409        });
4410    };
4411
4412    FeatureFlagManager.prototype.areFlagsReady = function() {
4413        if (!this.isSystemEnabled()) {
4414            logger$4.error('Feature Flags not enabled');
4415        }
4416        return !!this.flags;
4417    };
4418
4419    FeatureFlagManager.prototype.fetchFlags = function() {
4420        if (!this.isSystemEnabled()) {
4421            return Promise.resolve();
4422        }
4423
4424        var context = this._buildContext();
4425        var distinctId = context['distinct_id'];
4426        var traceparent = generateTraceparent();
4427        logger$4.log('Fetching flags for distinct ID: ' + distinctId);
4428
4429        var searchParams = new URLSearchParams();
4430        searchParams.set('context', JSON.stringify(context));
4431        searchParams.set('token', this.getMpConfig('token'));
4432        searchParams.set('mp_lib', 'web');
4433        searchParams.set('$lib_version', Config.LIB_VERSION);
4434        var url = this.getFullApiRoute() + '?' + searchParams.toString();
4435
4436        this._fetchInProgressStartTime = Date.now();
4437        this.fetchPromise = this.fetch.call(win, url, {
4438            'method': 'GET',
4439            'headers': {
4440                'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
4441                'traceparent': traceparent
4442            }
4443        }).then(function(response) {
4444            this.markFetchComplete();
4445            return response.json();
4446        }.bind(this)).then(function(responseBody) {
4447            var responseFlags = responseBody['flags'];
4448            if (!responseFlags) {
4449                throw new Error('No flags in API response');
4450            }
4451            var flags = new Map();
4452            var pendingFirstTimeEvents = {};
4453
4454            // Process flags from response
4455            _.each(responseFlags, function(data, key) {
4456                // Check if this flag has any activated first-time events this session
4457                var hasActivatedEvent = false;
4458                var prefix = key + ':';
4459                _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4460                    if (eventKey.startsWith(prefix)) {
4461                        hasActivatedEvent = true;
4462                    }
4463                });
4464
4465                if (hasActivatedEvent) {
4466                    // Preserve the activated variant, don't overwrite with server's current variant
4467                    var currentFlag = this.flags && this.flags.get(key);
4468                    if (currentFlag) {
4469                        flags.set(key, currentFlag);
4470                    }
4471                } else {
4472                    // Use server's current variant
4473                    flags.set(key, {
4474                        'key': data['variant_key'],
4475                        'value': data['variant_value'],
4476                        'experiment_id': data['experiment_id'],
4477                        'is_experiment_active': data['is_experiment_active'],
4478                        'is_qa_tester': data['is_qa_tester'],
4479                        'variant_source': 'network'
4480                    });
4481                }
4482            }, this);
4483
4484            // Process top-level pending_first_time_events array
4485            var topLevelDefinitions = responseBody['pending_first_time_events'];
4486            if (topLevelDefinitions && topLevelDefinitions.length > 0) {
4487                _.each(topLevelDefinitions, function(def) {
4488                    var flagKey = def['flag_key'];
4489                    var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
4490
4491                    // Skip if this specific event has already been activated this session
4492                    if (this.activatedFirstTimeEvents[eventKey]) {
4493                        return;
4494                    }
4495
4496                    // Store pending event definition using composite key
4497                    pendingFirstTimeEvents[eventKey] = {
4498                        'flag_key': flagKey,
4499                        'flag_id': def['flag_id'],
4500                        'project_id': def['project_id'],
4501                        'first_time_event_hash': def['first_time_event_hash'],
4502                        'event_name': def['event_name'],
4503                        'property_filters': def['property_filters'],
4504                        'pending_variant': def['pending_variant']
4505                    };
4506                }, this);
4507            }
4508
4509            // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
4510            if (this.activatedFirstTimeEvents) {
4511                _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4512                    var flagKey = getFlagKeyFromPendingEventKey(eventKey);
4513                    if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
4514                        // Keep the activated flag even though it's not in the new response
4515                        flags.set(flagKey, this.flags.get(flagKey));
4516                    }
4517                }, this);
4518            }
4519
4520            this.flags = flags;
4521            this.trackedFeatures = new Set();
4522            this.pendingFirstTimeEvents = pendingFirstTimeEvents;
4523            this._loadedPersistedAtMs = null;
4524            this._loadedTtlMs = null;
4525            this._traceparent = traceparent;
4526
4527            this._loadTargetingIfNeeded();
4528
4529            this.persistence.save(context, this.flags, this.pendingFirstTimeEvents);
4530        }.bind(this)).catch(function(error) {
4531            if (this._fetchInProgressStartTime) {
4532                this.markFetchComplete();
4533            }
4534            logger$4.error(error);
4535            throw error;
4536        }.bind(this));
4537
4538        return this.fetchPromise;
4539    };
4540
4541    FeatureFlagManager.prototype.loadFlags = function() {
4542        if (!this.isSystemEnabled()) {
4543            return Promise.resolve();
4544        }
4545        if (!this.trackedFeatures) {
4546            logger$4.error('loadFlags called before init');
4547            return Promise.resolve();
4548        }
4549        if (this._fetchInProgressStartTime) {
4550            return this.fetchPromise;
4551        }
4552        return this.fetchFlags();
4553    };
4554
4555    FeatureFlagManager.prototype.markFetchComplete = function() {
4556        if (!this._fetchInProgressStartTime) {
4557            logger$4.error('Fetch in progress started time not set, cannot mark fetch complete');
4558            return;
4559        }
4560        this._fetchStartTime = this._fetchInProgressStartTime;
4561        this._fetchCompleteTime = Date.now();
4562        this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
4563        this._fetchInProgressStartTime = null;
4564    };
4565
4566    /**
4567     * Proactively load targeting bundle if any pending events have property filters
4568     */
4569    FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
4570        var hasPropertyFilters = false;
4571        _.each(this.pendingFirstTimeEvents, function(evt) {
4572            if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
4573                hasPropertyFilters = true;
4574            }
4575        });
4576
4577        if (hasPropertyFilters) {
4578            this.getTargeting().then(function() {
4579                logger$4.log('targeting loaded for property filter evaluation');
4580            });
4581        }
4582    };
4583
4584    /**
4585     * Get the targeting library (initializes if not already loaded)
4586     * This method is primarily for testing - production code should rely on automatic loading
4587     * @returns {Promise} Promise that resolves with targeting library
4588     */
4589    FeatureFlagManager.prototype.getTargeting = function() {
4590        return getTargetingPromise(
4591            this.loadExtraBundle.bind(this),
4592            this.targetingSrc
4593        ).catch(function(error) {
4594            logger$4.error('Failed to load targeting: ' + error);
4595        }.bind(this));
4596    };
4597
4598    /**
4599     * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
4600     * @param {string} eventName - The name of the event being tracked
4601     * @param {Object} properties - Event properties to evaluate against property filters
4602     *
4603     * When a match is found (event name matches and property filters pass), this method:
4604     * - Switches the flag to the pending variant
4605     * - Marks the event as activated for this session
4606     * - Records the activation via the API
4607     *
4608     * @returns {Promise} Resolves once the pending events have been processed and their recording
4609     * requests have settled. The promise never rejects, so callers that ignore it cannot produce an
4610     * unhandled rejection.
4611     */
4612    FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
4613        if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
4614            return Promise.resolve();
4615        }
4616
4617        // Check if targeting promise exists (either bundled or async loaded)
4618        if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
4619            // The catch is part of the returned chain so the promise handed back always resolves.
4620            return win[TARGETING_GLOBAL_NAME].then(function(library) {
4621                return this._processFirstTimeEventCheck(eventName, properties, library);
4622            }.bind(this)).catch(function() {
4623                // If targeting failed to load, process with null
4624                // Events without property filters will still match
4625                return this._processFirstTimeEventCheck(eventName, properties, null);
4626            }.bind(this));
4627        } else {
4628            // No targeting available, process with null
4629            // Events without property filters will still match
4630            return this._processFirstTimeEventCheck(eventName, properties, null);
4631        }
4632    };
4633
4634    /**
4635     * Internal method to process first-time event checks with loaded targeting library
4636     * @param {string} eventName - The name of the event being tracked
4637     * @param {Object} properties - Event properties to evaluate against property filters
4638     * @param {Object} targeting - The loaded targeting library
4639     * @returns {Promise} Resolves once every recording request started here has settled.
4640     */
4641    FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
4642        var recordings = [];
4643        _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
4644            if (this.activatedFirstTimeEvents[eventKey]) {
4645                return;
4646            }
4647
4648            var flagKey = pendingEvent['flag_key'];
4649
4650            // Use targeting module to check if event matches
4651            var matchResult;
4652
4653            // If no targeting library and event has property filters, skip it
4654            if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
4655                logger$4.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
4656                return;
4657            }
4658
4659            // For simple events (no property filters), just check event name
4660            if (!targeting) {
4661                matchResult = {
4662                    matches: eventName === pendingEvent['event_name'],
4663                    error: null
4664                };
4665            } else {
4666                var criteria = {
4667                    'event_name': pendingEvent['event_name'],
4668                    'property_filters': pendingEvent['property_filters']
4669                };
4670                matchResult = targeting['eventMatchesCriteria'](
4671                    eventName,
4672                    properties,
4673                    criteria
4674                );
4675            }
4676
4677            if (matchResult.error) {
4678                logger$4.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
4679                return;
4680            }
4681
4682            if (!matchResult.matches) {
4683                return;
4684            }
4685
4686            logger$4.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
4687
4688            var newVariant = {
4689                'key': pendingEvent['pending_variant']['variant_key'],
4690                'value': pendingEvent['pending_variant']['variant_value'],
4691                'experiment_id': pendingEvent['pending_variant']['experiment_id'],
4692                'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
4693            };
4694
4695            this.flags.set(flagKey, newVariant);
4696            this.trackedFeatures.delete(flagKey);
4697            this.activatedFirstTimeEvents[eventKey] = true;
4698
4699            recordings.push(this.recordFirstTimeEvent(
4700                pendingEvent['flag_id'],
4701                pendingEvent['project_id'],
4702                pendingEvent['first_time_event_hash']
4703            ));
4704        }, this);
4705        return Promise.all(recordings);
4706    };
4707
4708    FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
4709        var base = this.getFullApiRoute().replace(/\/$/, '');
4710        return base + '/' + flagId + '/first-time-events';
4711    };
4712
4713    FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
4714        var distinctId = this.getMpProperty('distinct_id');
4715        var traceparent = generateTraceparent();
4716
4717        // Build URL with query string parameters
4718        var searchParams = new URLSearchParams();
4719        searchParams.set('mp_lib', 'web');
4720        searchParams.set('$lib_version', Config.LIB_VERSION);
4721        var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
4722
4723        var payload = {
4724            'distinct_id': distinctId,
4725            'project_id': projectId,
4726            'first_time_event_hash': firstTimeEventHash
4727        };
4728
4729        logger$4.log('Recording first-time event for flag: ' + flagId);
4730
4731        // The caller chains this so it can wait for the recording to settle. The catch below keeps the
4732        // returned promise resolved on failure, so a dropped request never rejects the chain.
4733        return this.fetch.call(win, url, {
4734            'method': 'POST',
4735            'headers': {
4736                'Content-Type': 'application/json',
4737                'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
4738                'traceparent': traceparent
4739            },
4740            'body': JSON.stringify(payload)
4741        }).catch(function(error) {
4742            // Silent failure - cohort sync will catch up
4743            logger$4.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
4744        });
4745    };
4746
4747    FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
4748        if (!this.persistenceLoadedPromise) {
4749            return new Promise(function(resolve) {
4750                logger$4.critical('Feature Flags not initialized');
4751                resolve(withFallbackSource(fallback, FALLBACK_REASON_NOT_READY));
4752            });
4753        }
4754
4755        var policy = this.persistence.getPolicy();
4756
4757        return this.persistenceLoadedPromise.then(_.bind(function() {
4758            // Serve from persistence until the network completes a successful fetch. If a non-expired cached value is available, return it without waiting on the in-flight fetch.
4759            if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
4760                if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
4761                    return this.getVariantSync(featureName, fallback);
4762                }
4763                if (!this.fetchPromise) {
4764                    return withFallbackSource(fallback, FALLBACK_REASON_NOT_READY);
4765                }
4766                return this.fetchPromise.then(_.bind(function() {
4767                    return this.getVariantSync(featureName, fallback);
4768                }, this)).catch(function(error) {
4769                    logger$4.error(error);
4770                    return withFallbackSource(fallback, FALLBACK_REASON_BACKEND_ERROR);
4771                });
4772            }
4773
4774            var serve = _.bind(function() { return this.getVariantSync(featureName, fallback); }, this);
4775            if (!this.fetchPromise) {
4776                return withFallbackSource(fallback, FALLBACK_REASON_NOT_READY);
4777            }
4778            return this.fetchPromise.then(serve).catch(_.bind(function(error) {
4779                logger$4.error(error);
4780                // If the fetch failure still left usable state (e.g. persistence hit under
4781                // a different policy config), serve from cache. Otherwise stamp
4782                // BACKEND_ERROR so callers can distinguish "backend is down" from
4783                // "flags never loaded". Mirrors the PUNS branch above.
4784                return this.areFlagsReady()
4785                    ? serve()
4786                    : withFallbackSource(fallback, FALLBACK_REASON_BACKEND_ERROR);
4787            }, this));
4788        }, this));
4789    };
4790
4791    FeatureFlagManager.prototype._loadedPersistenceIsStale = function() {
4792        if (!this._loadedPersistedAtMs || !this._loadedTtlMs) {
4793            return false;
4794        }
4795        return Date.now() - this._loadedPersistedAtMs >= this._loadedTtlMs;
4796    };
4797
4798    FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
4799        if (this._loadedPersistenceIsStale()) {
4800            logger$4.log('Loaded persisted variants are past TTL so returning fallback for "' + featureName + '"');
4801            return withFallbackSource(fallback, FALLBACK_REASON_NOT_READY);
4802        }
4803        if (!this.areFlagsReady()) {
4804            logger$4.log('Flags not loaded yet');
4805            return withFallbackSource(fallback, FALLBACK_REASON_NOT_READY);
4806        }
4807        var feature = this.flags.get(featureName);
4808        if (!feature) {
4809            logger$4.log('No flag found: "' + featureName + '"');
4810            return withFallbackSource(fallback, FALLBACK_REASON_FLAG_NOT_FOUND);
4811        }
4812        this.trackFeatureCheck(featureName, feature);
4813        return feature;
4814    };
4815
4816    FeatureFlagManager.prototype.getAllVariants = function() {
4817        if (!this.persistenceLoadedPromise) {
4818            logger$4.critical('Feature Flags not initialized');
4819            return Promise.resolve(new Map());
4820        }
4821
4822        var policy = this.persistence.getPolicy();
4823
4824        return this.persistenceLoadedPromise.then(_.bind(function() {
4825            // Serve from persistence until the network completes a successful fetch. If a non-expired cached value is available, return it without waiting on the in-flight fetch.
4826            if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
4827                if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
4828                    return this.getAllVariantsSync();
4829                }
4830                if (!this.fetchPromise) {
4831                    return new Map();
4832                }
4833                return this.fetchPromise.then(_.bind(function() {
4834                    return this.getAllVariantsSync();
4835                }, this)).catch(function(error) {
4836                    logger$4.error(error);
4837                    return new Map();
4838                });
4839            }
4840
4841            var serve = _.bind(this.getAllVariantsSync, this);
4842            if (!this.fetchPromise) {
4843                return new Map();
4844            }
4845            return this.fetchPromise.then(serve).catch(serve);
4846        }, this));
4847    };
4848
4849    FeatureFlagManager.prototype.getAllVariantsSync = function() {
4850        if (this._loadedPersistenceIsStale()) {
4851            logger$4.log('Loaded persisted variants are past TTL so returning empty Map');
4852            return new Map();
4853        }
4854        return this.flags || new Map();
4855    };
4856
4857    FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
4858        return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
4859            return feature['value'];
4860        }).catch(function(error) {
4861            logger$4.error(error);
4862            return fallbackValue;
4863        });
4864    };
4865
4866    // TODO remove deprecated method
4867    FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
4868        logger$4.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
4869        return this.getVariantValue(featureName, fallbackValue);
4870    };
4871
4872    FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
4873        return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
4874    };
4875
4876    FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
4877        return this.getVariantValue(featureName).then(function() {
4878            return this.isEnabledSync(featureName, fallbackValue);
4879        }.bind(this)).catch(function(error) {
4880            logger$4.error(error);
4881            return fallbackValue;
4882        });
4883    };
4884
4885    FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
4886        fallbackValue = fallbackValue || false;
4887        var val = this.getVariantValueSync(featureName, fallbackValue);
4888        if (val !== true && val !== false) {
4889            logger$4.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
4890            val = fallbackValue;
4891        }
4892        return val;
4893    };
4894
4895    function isPresent(v) {
4896        return v !== undefined && v !== null;
4897    }
4898
4899    FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
4900        if (this.trackedFeatures.has(featureName)) {
4901            return;
4902        }
4903        this.trackedFeatures.add(featureName);
4904
4905        var trackingProperties = {
4906            'Experiment name': featureName,
4907            'Variant name': feature['key'],
4908            '$experiment_type': 'feature_flag',
4909            'Variant fetch start time': isPresent(this._fetchStartTime) ? new Date(this._fetchStartTime).toISOString() : null,
4910            'Variant fetch complete time': isPresent(this._fetchCompleteTime) ? new Date(this._fetchCompleteTime).toISOString() : null,
4911            'Variant fetch latency (ms)': this._fetchLatency,
4912            'Variant fetch traceparent': this._traceparent,
4913        };
4914
4915        if (isPresent(feature['experiment_id'])) {
4916            trackingProperties['$experiment_id'] = feature['experiment_id'];
4917        }
4918        if (isPresent(feature['is_experiment_active'])) {
4919            trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
4920        }
4921        if (isPresent(feature['is_qa_tester'])) {
4922            trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
4923        }
4924        if (isPresent(feature['variant_source'])) {
4925            trackingProperties['$variant_source'] = feature['variant_source'];
4926        }
4927        if (isPresent(feature['persisted_at_in_ms'])) {
4928            trackingProperties['$persisted_at_in_ms'] = feature['persisted_at_in_ms'];
4929        }
4930        if (isPresent(feature['ttl_in_ms'])) {
4931            trackingProperties['$ttl_in_ms'] = feature['ttl_in_ms'];
4932        }
4933
4934        this.track('$experiment_started', trackingProperties);
4935    };
4936
4937    FeatureFlagManager.prototype.whenReady = function() {
4938        if (this.fetchPromise) {
4939            return this.fetchPromise;
4940        }
4941        return Promise.resolve();
4942    };
4943
4944    FeatureFlagManager.prototype.minApisSupported = function() {
4945        return !!this.fetch &&
4946          typeof Promise !== 'undefined' &&
4947          typeof Map !== 'undefined' &&
4948          typeof Set !== 'undefined';
4949    };
4950
4951    safewrapClass(FeatureFlagManager);
4952
4953    FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
4954    FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
4955    FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
4956    FeatureFlagManager.prototype['get_all_variants'] = FeatureFlagManager.prototype.getAllVariants;
4957    FeatureFlagManager.prototype['get_all_variants_sync'] = FeatureFlagManager.prototype.getAllVariantsSync;
4958    FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
4959    FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
4960    FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
4961    FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
4962    FeatureFlagManager.prototype['load_flags'] = FeatureFlagManager.prototype.loadFlags;
4963    FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
4964    FeatureFlagManager.prototype['when_ready'] = FeatureFlagManager.prototype.whenReady;
4965
4966    // Deprecated method
4967    FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
4968
4969    // Exports intended only for testing
4970    FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
4971
4972    var MIXPANEL_BROWSER_DB_NAME = 'mixpanelBrowserDb';
4973    var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
4974    var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
4975
4976    // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
4977    var RECORDER_VERSION_DATA = {
4978        version: 1,
4979        storeNames: [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME]
4980    };
4981
4982    /**
4983     * @param {import('./session-recording').SerializedRecording} serializedRecording
4984     * @returns {boolean}
4985     */
4986    var isRecordingExpired = function(serializedRecording) {
4987        var now = Date.now();
4988        return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
4989    };
4990
4991    var validateAllowedOrigins = function(origins, logger) {
4992        if (!_.isArray(origins)) {
4993            if (origins) {
4994                logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
4995            }
4996            return [];
4997        }
4998        var valid = [];
4999        for (var i = 0; i < origins.length; i++) {
5000            try {
5001                var origin = new URL(origins[i]).origin;
5002                if (origin === 'null') {
5003                    logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
5004                    continue;
5005                }
5006                valid.push(origin);
5007            } catch (e) {
5008                logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
5009            }
5010        }
5011        return valid;
5012    };
5013
5014    /* eslint camelcase: "off" */
5015
5016
5017    var logger$3 = console_with_prefix('recorder');
5018
5019    var IFRAME_HANDSHAKE_REQUEST  = 'mp_iframe_handshake_request';
5020    var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
5021
5022
5023    /**
5024     * RecorderManager: manages session recording initialization, lifecycle and state
5025     * @constructor
5026     */
5027    var RecorderManager = function(initOptions) {
5028        // TODO - Passing in mixpanel instance as it is still needed for recorder creation
5029        // but ideally we should be able to remove this dependency.
5030        this.mixpanelInstance = initOptions.mixpanelInstance;
5031
5032        this.getMpConfig = initOptions.getConfigFunc;
5033        this.getTabId = initOptions.getTabIdFunc;
5034        this.reportError = initOptions.reportErrorFunc;
5035        this.getDistinctId = initOptions.getDistinctIdFunc;
5036        this.loadExtraBundle = initOptions.loadExtraBundle;
5037        this.recorderSrc = initOptions.recorderSrc;
5038        this.targetingSrc = initOptions.targetingSrc;
5039        this.libBasePath = initOptions.libBasePath;
5040
5041        this._recorder = null;
5042        this._parentReplayId = null;
5043        this._parentFrameRetryInterval = null;
5044    };
5045
5046    RecorderManager.prototype.shouldLoadRecorder = function() {
5047        if (this.getMpConfig('disable_persistence')) {
5048            console.log('Load recorder check skipped due to disable_persistence config');
5049            return PromisePolyfill.resolve(false);
5050        }
5051
5052        var recording_registry_idb = new IDBStorageWrapper(MIXPANEL_BROWSER_DB_NAME, RECORDING_REGISTRY_STORE_NAME, RECORDER_VERSION_DATA);
5053        var tab_id = this.getTabId();
5054        return recording_registry_idb.init()
5055            .then(function () {
5056                return recording_registry_idb.getAll();
5057            })
5058            .then(function (recordings) {
5059                for (var i = 0; i < recordings.length; i++) {
5060                    // if there are expired recordings in the registry, we should load the recorder to flush them
5061                    // if there's a recording for this tab id, we should load the recorder to continue the recording
5062                    if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
5063                        return true;
5064                    }
5065                }
5066                return false;
5067            })
5068            .catch(_.bind(function (err) {
5069                this.reportError('Error checking recording registry', err);
5070                return false;
5071            }, this));
5072    };
5073
5074    RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
5075        if (!win['MutationObserver']) {
5076            console.critical('Browser does not support MutationObserver; skipping session recording');
5077            return PromisePolyfill.resolve();
5078        }
5079
5080        var loadRecorder = _.bind(function(startNewIfInactive) {
5081            return new PromisePolyfill(_.bind(function(resolve) {
5082                var handleLoadedRecorder = safewrap(_.bind(function() {
5083                    this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
5084                    this._recorder['resumeRecording'](startNewIfInactive);
5085                    resolve();
5086                }, this));
5087
5088                if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
5089                    var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
5090                    this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
5091                } else {
5092                    handleLoadedRecorder();
5093                }
5094            }, this));
5095        }, this);
5096
5097        // Cross-origin iframe handling
5098        var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger$3);
5099        var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
5100
5101        if (isCrossOriginRecordingEnabled) {
5102            // listen for handshake requests from their own child iframes (including nested)
5103            this._setupParentFrameListener(allowedOrigins);
5104
5105            if (win.parent !== win) {
5106                // also wait for parent's replay ID
5107                this._setupChildFrameListener(allowedOrigins, loadRecorder);
5108                this._sendParentFrameRequestWithRetry(allowedOrigins);
5109                return PromisePolyfill.resolve();
5110            }
5111        }
5112
5113        /**
5114         * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
5115         * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
5116         */
5117        var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
5118        var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
5119        if (force_start || is_sampled) {
5120            return loadRecorder(true);
5121        } else {
5122            return this.shouldLoadRecorder()
5123                .then(_.bind(function (shouldLoad) {
5124                    if (shouldLoad) {
5125                        return loadRecorder(false);
5126                    }
5127                    return PromisePolyfill.resolve();
5128                }, this));
5129        }
5130    };
5131
5132    RecorderManager.prototype.isRecording = function() {
5133        // Safety check: ensure isRecording method exists (older CDN builds may not have it)
5134        if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
5135            return false;
5136        }
5137        try {
5138            return this._recorder['isRecording']();
5139        } catch (e) {
5140            this.reportError('Error checking if recording is active', e);
5141            return false;
5142        }
5143    };
5144
5145    RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
5146        var isRecording = this.isRecording();
5147        var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
5148
5149        if (!isRecording && recordingTriggerEvents) {
5150            var trigger = recordingTriggerEvents[event_name];
5151            if (trigger && typeof trigger['percentage'] === 'number') {
5152                var newRate = trigger['percentage'];
5153                var propertyFilters = trigger['property_filters'];
5154                if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
5155                    var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
5156                    getTargetingPromise(this.loadExtraBundle, targetingSrc)
5157                        .then(function(targeting) {
5158                            try {
5159                                var result = targeting['eventMatchesCriteria'](
5160                                    event_name,
5161                                    properties,
5162                                    {
5163                                        'event_name': event_name,
5164                                        'property_filters': propertyFilters
5165                                    }
5166                                );
5167                                if (result['matches']) {
5168                                    this.checkAndStartSessionRecording(false, newRate);
5169                                }
5170                            } catch (err) {
5171                                console.critical('Could not parse recording event trigger properties logic:', err);
5172                            }
5173                        }.bind(this)).catch(function(err) {
5174                            console.critical('Failed to load targeting library:', err);
5175                        });
5176                } else {
5177                    this.checkAndStartSessionRecording(false, newRate);
5178                }
5179            }
5180        }
5181    };
5182
5183    RecorderManager.prototype.stopSessionRecording = function() {
5184        if (this._recorder) {
5185            return this._recorder['stopRecording']();
5186        }
5187        return PromisePolyfill.resolve();
5188    };
5189
5190    RecorderManager.prototype.pauseSessionRecording = function() {
5191        if (this._recorder) {
5192            return this._recorder['pauseRecording']();
5193        }
5194        return PromisePolyfill.resolve();
5195    };
5196
5197    RecorderManager.prototype.resumeSessionRecording = function() {
5198        if (this._recorder) {
5199            return this._recorder['resumeRecording']();
5200        }
5201        return PromisePolyfill.resolve();
5202    };
5203
5204    RecorderManager.prototype.isRecordingHeatmapData = function() {
5205        return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
5206    };
5207
5208    RecorderManager.prototype.getSessionRecordingProperties = function() {
5209        var props = {};
5210        var replay_id = this.getSessionReplayId();
5211        if (replay_id) {
5212            props['$mp_replay_id'] = replay_id;
5213        }
5214        return props;
5215    };
5216
5217    RecorderManager.prototype.getSessionReplayUrl = function() {
5218        var replay_url = null;
5219        var replay_id = this.getSessionReplayId();
5220        if (replay_id) {
5221            var query_params = _.HTTPBuildQuery({
5222                'replay_id': replay_id,
5223                'distinct_id': this.getDistinctId(),
5224                'token': this.getMpConfig('token')
5225            });
5226            replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
5227        }
5228        return replay_url;
5229    };
5230
5231    RecorderManager.prototype.getSessionReplayId = function() {
5232        // Child iframe uses parent's replay ID
5233        if (this._parentReplayId) {
5234            return this._parentReplayId;
5235        }
5236        var replay_id = null;
5237        if (this._recorder) {
5238            replay_id = this._recorder['replayId'];
5239        }
5240        return replay_id || null;
5241    };
5242
5243    // "private" public method to reach into the recorder in test cases
5244    RecorderManager.prototype.getRecorder = function() {
5245        return this._recorder;
5246    };
5247
5248    RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
5249        if (this._childFrameMessageHandler) {
5250            return;
5251        }
5252        var self = this;
5253        this._childFrameMessageHandler = function(event) {
5254            if (allowedOrigins.indexOf(event.origin) === -1) return;
5255            var data = event.data;
5256            if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
5257                self._parentReplayId = data['replayId'];
5258                if (data['distinctId']) {
5259                    self.mixpanelInstance['identify'](data['distinctId']);
5260                }
5261                self._parentFrameRetryActive = false;
5262                win.removeEventListener('message', self._childFrameMessageHandler);
5263                self._childFrameMessageHandler = null;
5264                loadRecorder(true);
5265            }
5266        };
5267        win.addEventListener('message', this._childFrameMessageHandler);
5268    };
5269
5270    RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
5271        var message = {};
5272        message['type'] = IFRAME_HANDSHAKE_REQUEST;
5273        message['token'] = this.getMpConfig('token');
5274        for (var i = 0; i < allowedOrigins.length; i++) {
5275            try {
5276                win.parent.postMessage(message, allowedOrigins[i]);
5277            } catch (e) {
5278                // origin mismatch - ignore
5279            }
5280        }
5281    };
5282
5283    RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
5284        var self = this;
5285        var maxRetries = 10;
5286        var retryCount = 0;
5287        var delay = 50;
5288        this._parentFrameRetryActive = true;
5289
5290        this._sendParentFrameRequest(allowedOrigins);
5291
5292        function scheduleRetry() {
5293            setTimeout(function() {
5294                if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
5295                    return;
5296                }
5297                self._sendParentFrameRequest(allowedOrigins);
5298                delay *= 2;
5299                scheduleRetry();
5300            }, delay);
5301        }
5302        scheduleRetry();
5303    };
5304
5305    RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
5306        if (this._parentFrameMessageHandler) {
5307            return;
5308        }
5309        var self = this;
5310        this._parentFrameMessageHandler = function(event) {
5311            if (allowedOrigins.indexOf(event.origin) === -1) return;
5312            var data = event.data;
5313            if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
5314                var replayId = self.getSessionReplayId();
5315                if (replayId) {
5316                    var response = {};
5317                    response['type'] = IFRAME_HANDSHAKE_RESPONSE;
5318                    response['token'] = self.getMpConfig('token');
5319                    response['replayId'] = replayId;
5320                    response['distinctId'] = self.getDistinctId();
5321                    event.source.postMessage(response, event.origin);
5322                }
5323            }
5324        };
5325        win.addEventListener('message', this._parentFrameMessageHandler);
5326    };
5327
5328    safewrapClass(RecorderManager);
5329
5330    /* eslint camelcase: "off" */
5331
5332
5333    /**
5334     * DomTracker Object
5335     * @constructor
5336     */
5337    var DomTracker = function() {};
5338
5339
5340    // interface
5341    DomTracker.prototype.create_properties = function() {};
5342    DomTracker.prototype.event_handler = function() {};
5343    DomTracker.prototype.after_track_handler = function() {};
5344
5345    DomTracker.prototype.init = function(mixpanel_instance) {
5346        this.mp = mixpanel_instance;
5347        return this;
5348    };
5349
5350    /**
5351     * @param {Object|string} query
5352     * @param {string} event_name
5353     * @param {Object=} properties
5354     * @param {function=} user_callback
5355     */
5356    DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
5357        var that = this;
5358        var elements = _.dom_query(query);
5359
5360        if (elements.length === 0) {
5361            console.error('The DOM query (' + query + ') returned 0 elements');
5362            return;
5363        }
5364
5365        _.each(elements, function(element) {
5366            _.register_event(element, this.override_event, function(e) {
5367                var options = {};
5368                var props = that.create_properties(properties, this);
5369                var timeout = that.mp.get_config('track_links_timeout');
5370
5371                that.event_handler(e, this, options);
5372
5373                // in case the mixpanel servers don't get back to us in time
5374                win.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
5375
5376                // fire the tracking event
5377                that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
5378            });
5379        }, this);
5380
5381        return true;
5382    };
5383
5384    /**
5385     * @param {function} user_callback
5386     * @param {Object} props
5387     * @param {boolean=} timeout_occured
5388     */
5389    DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
5390        timeout_occured = timeout_occured || false;
5391        var that = this;
5392
5393        return function() {
5394            // options is referenced from both callbacks, so we can have
5395            // a 'lock' of sorts to ensure only one fires
5396            if (options.callback_fired) { return; }
5397            options.callback_fired = true;
5398
5399            if (user_callback && user_callback(timeout_occured, props) === false) {
5400                // user can prevent the default functionality by
5401                // returning false from their callback
5402                return;
5403            }
5404
5405            that.after_track_handler(props, options, timeout_occured);
5406        };
5407    };
5408
5409    DomTracker.prototype.create_properties = function(properties, element) {
5410        var props;
5411
5412        if (typeof(properties) === 'function') {
5413            props = properties(element);
5414        } else {
5415            props = _.extend({}, properties);
5416        }
5417
5418        return props;
5419    };
5420
5421    /**
5422     * LinkTracker Object
5423     * @constructor
5424     * @extends DomTracker
5425     */
5426    var LinkTracker = function() {
5427        this.override_event = 'click';
5428    };
5429    _.inherit(LinkTracker, DomTracker);
5430
5431    LinkTracker.prototype.create_properties = function(properties, element) {
5432        var props = LinkTracker.superclass.create_properties.apply(this, arguments);
5433
5434        if (element.href) { props['url'] = element.href; }
5435
5436        return props;
5437    };
5438
5439    LinkTracker.prototype.event_handler = function(evt, element, options) {
5440        options.new_tab = (
5441            evt.which === 2 ||
5442            evt.metaKey ||
5443            evt.ctrlKey ||
5444            element.target === '_blank'
5445        );
5446        options.href = element.href;
5447
5448        if (!options.new_tab) {
5449            evt.preventDefault();
5450        }
5451    };
5452
5453    LinkTracker.prototype.after_track_handler = function(props, options) {
5454        if (options.new_tab || !options.href) { return; }
5455
5456        setTimeout(function() {
5457            win.location = options.href;
5458        }, 0);
5459    };
5460
5461    /**
5462     * FormTracker Object
5463     * @constructor
5464     * @extends DomTracker
5465     */
5466    var FormTracker = function() {
5467        this.override_event = 'submit';
5468    };
5469    _.inherit(FormTracker, DomTracker);
5470
5471    FormTracker.prototype.event_handler = function(evt, element, options) {
5472        options.element = element;
5473        evt.preventDefault();
5474    };
5475
5476    FormTracker.prototype.after_track_handler = function(props, options) {
5477        setTimeout(function() {
5478            options.element.submit();
5479        }, 0);
5480    };
5481
5482    var logger$2 = console_with_prefix('lock');
5483
5484    /**
5485     * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
5486     * window/tab at a time will be able to access shared resources.
5487     *
5488     * Based on the Alur and Taubenfeld fast lock
5489     * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html)
5490     * with an added timeout to ensure there will be eventual progress in the event
5491     * that a window is closed in the middle of the callback.
5492     *
5493     * Implementation based on the original version by David Wolever (https://github.com/wolever)
5494     * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432.
5495     *
5496     * @example
5497     * const myLock = new SharedLock('some-key');
5498     * myLock.withLock(function() {
5499     *   console.log('I hold the mutex!');
5500     * });
5501     *
5502     * @constructor
5503     */
5504    var SharedLock = function(key, options) {
5505        options = options || {};
5506
5507        this.storageKey = key;
5508        this.storage = options.storage || getLocalStorage();
5509        this.pollIntervalMS = options.pollIntervalMS || 100;
5510        this.timeoutMS = options.timeoutMS || 2000;
5511
5512        // dependency-inject promise implementation for testing purposes
5513        this.promiseImpl = options.promiseImpl || PromisePolyfill;
5514    };
5515
5516    // pass in a specific pid to test contention scenarios; otherwise
5517    // it is chosen randomly for each acquisition attempt
5518    SharedLock.prototype.withLock = function(lockedCB, pid) {
5519        var Promise = this.promiseImpl;
5520        return new Promise(_.bind(function (resolve, reject) {
5521            var i = pid || (new Date().getTime() + '|' + Math.random());
5522            var startTime = new Date().getTime();
5523            var key = this.storageKey;
5524            var pollIntervalMS = this.pollIntervalMS;
5525            var timeoutMS = this.timeoutMS;
5526            var storage = this.storage;
5527
5528            var keyX = key + ':X';
5529            var keyY = key + ':Y';
5530            var keyZ = key + ':Z';
5531
5532            var delay = function(cb) {
5533                if (new Date().getTime() - startTime > timeoutMS) {
5534                    logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
5535                    storage.removeItem(keyZ);
5536                    storage.removeItem(keyY);
5537                    loop();
5538                    return;
5539                }
5540                setTimeout(function() {
5541                    try {
5542                        cb();
5543                    } catch(err) {
5544                        reject(err);
5545                    }
5546                }, pollIntervalMS * (Math.random() + 0.1));
5547            };
5548
5549            var waitFor = function(predicate, cb) {
5550                if (predicate()) {
5551                    cb();
5552                } else {
5553                    delay(function() {
5554                        waitFor(predicate, cb);
5555                    });
5556                }
5557            };
5558
5559            var getSetY = function() {
5560                var valY = storage.getItem(keyY);
5561                if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
5562                    return false;
5563                } else {
5564                    storage.setItem(keyY, i);
5565                    if (storage.getItem(keyY) === i) {
5566                        return true;
5567                    } else {
5568                        if (!localStorageSupported(storage, true)) {
5569                            reject(new Error('localStorage support dropped while acquiring lock'));
5570                        }
5571                        return false;
5572                    }
5573                }
5574            };
5575
5576            var loop = function() {
5577                storage.setItem(keyX, i);
5578
5579                waitFor(getSetY, function() {
5580                    if (storage.getItem(keyX) === i) {
5581                        criticalSection();
5582                        return;
5583                    }
5584
5585                    delay(function() {
5586                        if (storage.getItem(keyY) !== i) {
5587                            loop();
5588                            return;
5589                        }
5590                        waitFor(function() {
5591                            return !storage.getItem(keyZ);
5592                        }, criticalSection);
5593                    });
5594                });
5595            };
5596
5597            var criticalSection = function() {
5598                storage.setItem(keyZ, '1');
5599                var removeLock = function () {
5600                    storage.removeItem(keyZ);
5601                    if (storage.getItem(keyY) === i) {
5602                        storage.removeItem(keyY);
5603                    }
5604                    if (storage.getItem(keyX) === i) {
5605                        storage.removeItem(keyX);
5606                    }
5607                };
5608
5609                lockedCB()
5610                    .then(function (ret) {
5611                        removeLock();
5612                        resolve(ret);
5613                    })
5614                    .catch(function (err) {
5615                        removeLock();
5616                        reject(err);
5617                    });
5618            };
5619
5620            try {
5621                if (localStorageSupported(storage, true)) {
5622                    loop();
5623                } else {
5624                    throw new Error('localStorage support check failed');
5625                }
5626            } catch(err) {
5627                reject(err);
5628            }
5629        }, this));
5630    };
5631
5632    /**
5633     * @type {import('./wrapper').StorageWrapper}
5634     */
5635    var LocalStorageWrapper = function (storageOverride) {
5636        this.storage = storageOverride || getLocalStorage();
5637    };
5638
5639    LocalStorageWrapper.prototype.init = function () {
5640        if (!this.storage) {
5641            return PromisePolyfill.reject(new Error('localStorage is not available'));
5642        }
5643        return PromisePolyfill.resolve();
5644    };
5645
5646    LocalStorageWrapper.prototype.isInitialized = function () {
5647        return true;
5648    };
5649
5650    LocalStorageWrapper.prototype.setItem = function (key, value) {
5651        return new PromisePolyfill(_.bind(function (resolve, reject) {
5652            try {
5653                this.storage.setItem(key, JSONStringify(value));
5654            } catch (e) {
5655                reject(e);
5656            }
5657            resolve();
5658        }, this));
5659    };
5660
5661    LocalStorageWrapper.prototype.getItem = function (key) {
5662        return new PromisePolyfill(_.bind(function (resolve, reject) {
5663            var item;
5664            try {
5665                item = JSONParse(this.storage.getItem(key));
5666            } catch (e) {
5667                reject(e);
5668            }
5669            resolve(item);
5670        }, this));
5671    };
5672
5673    LocalStorageWrapper.prototype.removeItem = function (key) {
5674        return new PromisePolyfill(_.bind(function (resolve, reject) {
5675            try {
5676                this.storage.removeItem(key);
5677            } catch (e) {
5678                reject(e);
5679            }
5680            resolve();
5681        }, this));
5682    };
5683
5684    var logger$1 = console_with_prefix('batch');
5685
5686    /**
5687     * RequestQueue: queue for batching API requests with localStorage backup for retries.
5688     * Maintains an in-memory queue which represents the source of truth for the current
5689     * page, but also writes all items out to a copy in the browser's localStorage, which
5690     * can be read on subsequent pageloads and retried. For batchability, all the request
5691     * items in the queue should be of the same type (events, people updates, group updates)
5692     * so they can be sent in a single request to the same API endpoint.
5693     *
5694     * LocalStorage keying and locking: In order for reloads and subsequent pageloads of
5695     * the same site to access the same persisted data, they must share the same localStorage
5696     * key (for instance based on project token and queue type). Therefore access to the
5697     * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent
5698     * simultaneously open windows/tabs from overwriting each other's data (which would lead
5699     * to data loss in some situations).
5700     * @constructor
5701     */
5702    var RequestQueue = function (storageKey, options) {
5703        options = options || {};
5704        this.storageKey = storageKey;
5705        this.usePersistence = options.usePersistence;
5706        if (this.usePersistence) {
5707            this.queueStorage = options.queueStorage || new LocalStorageWrapper();
5708            this.lock = new SharedLock(storageKey, {
5709                storage: options.sharedLockStorage,
5710                timeoutMS: options.sharedLockTimeoutMS,
5711            });
5712        }
5713        this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
5714
5715        this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
5716
5717        this.memQueue = [];
5718        this.initialized = false;
5719
5720        if (options.enqueueThrottleMs) {
5721            this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
5722        } else {
5723            this.enqueuePersisted = _.bind(function (queueEntry) {
5724                return this._enqueuePersisted([queueEntry]);
5725            }, this);
5726        }
5727    };
5728
5729    RequestQueue.prototype.ensureInit = function () {
5730        if (this.initialized || !this.usePersistence) {
5731            return PromisePolyfill.resolve();
5732        }
5733
5734        return this.queueStorage
5735            .init()
5736            .then(_.bind(function () {
5737                this.initialized = true;
5738            }, this))
5739            .catch(_.bind(function (err) {
5740                this.reportError('Error initializing queue persistence. Disabling persistence', err);
5741                this.initialized = true;
5742                this.usePersistence = false;
5743            }, this));
5744    };
5745
5746    /**
5747     * Add one item to queues (memory and localStorage). The queued entry includes
5748     * the given item along with an auto-generated ID and a "flush-after" timestamp.
5749     * It is expected that the item will be sent over the network and dequeued
5750     * before the flush-after time; if this doesn't happen it is considered orphaned
5751     * (e.g., the original tab where it was enqueued got closed before it could be
5752     * sent) and the item can be sent by any tab that finds it in localStorage.
5753     *
5754     * The final callback param is called with a param indicating success or
5755     * failure of the enqueue operation; it is asynchronous because the localStorage
5756     * lock is asynchronous.
5757     */
5758    RequestQueue.prototype.enqueue = function (item, flushInterval) {
5759        var queueEntry = {
5760            'id': cheap_guid(),
5761            'flushAfter': new Date().getTime() + flushInterval * 2,
5762            'payload': item
5763        };
5764
5765        if (!this.usePersistence) {
5766            this.memQueue.push(queueEntry);
5767            return PromisePolyfill.resolve(true);
5768        } else {
5769            return this.enqueuePersisted(queueEntry);
5770        }
5771    };
5772
5773    RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
5774        var enqueueItem = _.bind(function () {
5775            return this.ensureInit()
5776                .then(_.bind(function () {
5777                    return this.readFromStorage();
5778                }, this))
5779                .then(_.bind(function (storedQueue) {
5780                    return this.saveToStorage(storedQueue.concat(queueEntries));
5781                }, this))
5782                .then(_.bind(function (succeeded) {
5783                    // only add to in-memory queue when storage succeeds
5784                    if (succeeded) {
5785                        this.memQueue = this.memQueue.concat(queueEntries);
5786                    }
5787
5788                    return succeeded;
5789                }, this))
5790                .catch(_.bind(function (err) {
5791                    this.reportError('Error enqueueing items', err, queueEntries);
5792                    return false;
5793                }, this));
5794        }, this);
5795
5796        return this.lock
5797            .withLock(enqueueItem, this.pid)
5798            .catch(_.bind(function (err) {
5799                this.reportError('Error acquiring storage lock', err);
5800                return false;
5801            }, this));
5802    };
5803
5804    /**
5805     * Read out the given number of queue entries. If this.memQueue
5806     * has fewer than batchSize items, then look for "orphaned" items
5807     * in the persisted queue (items where the 'flushAfter' time has
5808     * already passed).
5809     */
5810    RequestQueue.prototype.fillBatch = function (batchSize) {
5811        var batch = this.memQueue.slice(0, batchSize);
5812        if (this.usePersistence && batch.length < batchSize) {
5813            // don't need lock just to read events; localStorage is thread-safe
5814            // and the worst that could happen is a duplicate send of some
5815            // orphaned events, which will be deduplicated on the server side
5816            return this.ensureInit()
5817                .then(_.bind(function () {
5818                    return this.readFromStorage();
5819                }, this))
5820                .then(_.bind(function (storedQueue) {
5821                    if (storedQueue.length) {
5822                        // item IDs already in batch; don't duplicate out of storage
5823                        var idsInBatch = {}; // poor man's Set
5824                        _.each(batch, function (item) {
5825                            idsInBatch[item['id']] = true;
5826                        });
5827
5828                        for (var i = 0; i < storedQueue.length; i++) {
5829                            var item = storedQueue[i];
5830                            if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
5831                                item.orphaned = true;
5832                                batch.push(item);
5833                                if (batch.length >= batchSize) {
5834                                    break;
5835                                }
5836                            }
5837                        }
5838                    }
5839
5840                    return batch;
5841                }, this));
5842        } else {
5843            return PromisePolyfill.resolve(batch);
5844        }
5845    };
5846
5847    /**
5848     * Remove items with matching 'id' from array (immutably)
5849     * also remove any item without a valid id (e.g., malformed
5850     * storage entries).
5851     */
5852    var filterOutIDsAndInvalid = function (items, idSet) {
5853        var filteredItems = [];
5854        _.each(items, function (item) {
5855            if (item['id'] && !idSet[item['id']]) {
5856                filteredItems.push(item);
5857            }
5858        });
5859        return filteredItems;
5860    };
5861
5862    /**
5863     * Remove items with matching IDs from both in-memory queue
5864     * and persisted queue
5865     */
5866    RequestQueue.prototype.removeItemsByID = function (ids) {
5867        var idSet = {}; // poor man's Set
5868        _.each(ids, function (id) {
5869            idSet[id] = true;
5870        });
5871
5872        this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
5873        if (!this.usePersistence) {
5874            return PromisePolyfill.resolve(true);
5875        } else {
5876            var removeFromStorage = _.bind(function () {
5877                return this.ensureInit()
5878                    .then(_.bind(function () {
5879                        return this.readFromStorage();
5880                    }, this))
5881                    .then(_.bind(function (storedQueue) {
5882                        storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
5883                        return this.saveToStorage(storedQueue);
5884                    }, this))
5885                    .then(_.bind(function () {
5886                        return this.readFromStorage();
5887                    }, this))
5888                    .then(_.bind(function (storedQueue) {
5889                        // an extra check: did storage report success but somehow
5890                        // the items are still there?
5891                        for (var i = 0; i < storedQueue.length; i++) {
5892                            var item = storedQueue[i];
5893                            if (item['id'] && !!idSet[item['id']]) {
5894                                throw new Error('Item not removed from storage');
5895                            }
5896                        }
5897                        return true;
5898                    }, this))
5899                    .catch(_.bind(function (err) {
5900                        this.reportError('Error removing items', err, ids);
5901                        return false;
5902                    }, this));
5903            }, this);
5904
5905            return this.lock
5906                .withLock(removeFromStorage, this.pid)
5907                .catch(_.bind(function (err) {
5908                    this.reportError('Error acquiring storage lock', err);
5909                    if (!localStorageSupported(this.lock.storage, true)) {
5910                        // Looks like localStorage writes have stopped working sometime after
5911                        // initialization (probably full), and so nobody can acquire locks
5912                        // anymore. Consider it temporarily safe to remove items without the
5913                        // lock, since nobody's writing successfully anyway.
5914                        return removeFromStorage()
5915                            .then(_.bind(function (success) {
5916                                if (!success) {
5917                                    // OK, we couldn't even write out the smaller queue. Try clearing it
5918                                    // entirely.
5919                                    return this.queueStorage.removeItem(this.storageKey).then(function () {
5920                                        return success;
5921                                    });
5922                                }
5923                                return success;
5924                            }, this))
5925                            .catch(_.bind(function (err) {
5926                                this.reportError('Error clearing queue', err);
5927                                return false;
5928                            }, this));
5929                    } else {
5930                        return false;
5931                    }
5932                }, this));
5933        }
5934    };
5935
5936    // internal helper for RequestQueue.updatePayloads
5937    var updatePayloads = function (existingItems, itemsToUpdate) {
5938        var newItems = [];
5939        _.each(existingItems, function (item) {
5940            var id = item['id'];
5941            if (id in itemsToUpdate) {
5942                var newPayload = itemsToUpdate[id];
5943                if (newPayload !== null) {
5944                    item['payload'] = newPayload;
5945                    newItems.push(item);
5946                }
5947            } else {
5948                // no update
5949                newItems.push(item);
5950            }
5951        });
5952        return newItems;
5953    };
5954
5955    /**
5956     * Update payloads of given items in both in-memory queue and
5957     * persisted queue. Items set to null are removed from queues.
5958     */
5959    RequestQueue.prototype.updatePayloads = function (itemsToUpdate) {
5960        this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
5961        if (!this.usePersistence) {
5962            return PromisePolyfill.resolve(true);
5963        } else {
5964            return this.lock
5965                .withLock(_.bind(function lockAcquired() {
5966                    return this.ensureInit()
5967                        .then(_.bind(function () {
5968                            return this.readFromStorage();
5969                        }, this))
5970                        .then(_.bind(function (storedQueue) {
5971                            storedQueue = updatePayloads(storedQueue, itemsToUpdate);
5972                            return this.saveToStorage(storedQueue);
5973                        }, this))
5974                        .catch(_.bind(function (err) {
5975                            this.reportError('Error updating items', itemsToUpdate, err);
5976                            return false;
5977                        }, this));
5978                }, this), this.pid)
5979                .catch(_.bind(function (err) {
5980                    this.reportError('Error acquiring storage lock', err);
5981                    return false;
5982                }, this));
5983        }
5984    };
5985
5986    /**
5987     * Read and parse items array from localStorage entry, handling
5988     * malformed/missing data if necessary.
5989     */
5990    RequestQueue.prototype.readFromStorage = function () {
5991        return this.ensureInit()
5992            .then(_.bind(function () {
5993                return this.queueStorage.getItem(this.storageKey);
5994            }, this))
5995            .then(_.bind(function (storageEntry) {
5996                if (storageEntry) {
5997                    if (!_.isArray(storageEntry)) {
5998                        this.reportError('Invalid storage entry:', storageEntry);
5999                        storageEntry = null;
6000                    }
6001                }
6002                return storageEntry || [];
6003            }, this))
6004            .catch(_.bind(function (err) {
6005                this.reportError('Error retrieving queue', err);
6006                return [];
6007            }, this));
6008    };
6009
6010    /**
6011     * Serialize the given items array to localStorage.
6012     */
6013    RequestQueue.prototype.saveToStorage = function (queue) {
6014        return this.ensureInit()
6015            .then(_.bind(function () {
6016                return this.queueStorage.setItem(this.storageKey, queue);
6017            }, this))
6018            .then(function () {
6019                return true;
6020            })
6021            .catch(_.bind(function (err) {
6022                this.reportError('Error saving queue', err);
6023                return false;
6024            }, this));
6025    };
6026
6027    /**
6028     * Clear out queues (memory and localStorage).
6029     */
6030    RequestQueue.prototype.clear = function () {
6031        this.memQueue = [];
6032
6033        if (this.usePersistence) {
6034            return this.ensureInit()
6035                .then(_.bind(function () {
6036                    return this.queueStorage.removeItem(this.storageKey);
6037                }, this));
6038        } else {
6039            return PromisePolyfill.resolve();
6040        }
6041    };
6042
6043    // maximum interval between request retries after exponential backoff
6044    var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
6045
6046    var logger = console_with_prefix('batch');
6047
6048    /**
6049     * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
6050     * type (events, people, groups).
6051     * Uses RequestQueue to manage the backing store.
6052     * @constructor
6053     */
6054    var RequestBatcher = function(storageKey, options) {
6055        this.errorReporter = options.errorReporter;
6056        this.queue = new RequestQueue(storageKey, {
6057            errorReporter: _.bind(this.reportError, this),
6058            queueStorage: options.queueStorage,
6059            sharedLockStorage: options.sharedLockStorage,
6060            sharedLockTimeoutMS: options.sharedLockTimeoutMS,
6061            usePersistence: options.usePersistence,
6062            enqueueThrottleMs: options.enqueueThrottleMs
6063        });
6064
6065        this.libConfig = options.libConfig;
6066        this.sendRequest = options.sendRequestFunc;
6067        this.beforeSendHook = options.beforeSendHook;
6068        this.stopAllBatching = options.stopAllBatchingFunc;
6069
6070        // seed variable batch size + flush interval with configured values
6071        this.batchSize = this.libConfig['batch_size'];
6072        this.flushInterval = this.libConfig['batch_flush_interval_ms'];
6073
6074        this.stopped = !this.libConfig['batch_autostart'];
6075        this.consecutiveRemovalFailures = 0;
6076
6077        // extra client-side dedupe
6078        this.itemIdsSentSuccessfully = {};
6079
6080        // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes
6081        // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
6082        // in a request loop and get ratelimited by the server.
6083        this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
6084
6085        this._flushPromise = null;
6086    };
6087
6088    /**
6089     * Add one item to queue.
6090     */
6091    RequestBatcher.prototype.enqueue = function(item) {
6092        return this.queue.enqueue(item, this.flushInterval);
6093    };
6094
6095    /**
6096     * Start flushing batches at the configured time interval. Must call
6097     * this method upon SDK init in order to send anything over the network.
6098     */
6099    RequestBatcher.prototype.start = function() {
6100        this.stopped = false;
6101        this.consecutiveRemovalFailures = 0;
6102        return this.flush();
6103    };
6104
6105    /**
6106     * Stop flushing batches. Can be restarted by calling start().
6107     */
6108    RequestBatcher.prototype.stop = function() {
6109        this.stopped = true;
6110        if (this.timeoutID) {
6111            clearTimeout(this.timeoutID);
6112            this.timeoutID = null;
6113        }
6114    };
6115
6116    /**
6117     * Clear out queue.
6118     */
6119    RequestBatcher.prototype.clear = function() {
6120        return this.queue.clear();
6121    };
6122
6123    /**
6124     * Restore batch size configuration to whatever is set in the main SDK.
6125     */
6126    RequestBatcher.prototype.resetBatchSize = function() {
6127        this.batchSize = this.libConfig['batch_size'];
6128    };
6129
6130    /**
6131     * Restore flush interval time configuration to whatever is set in the main SDK.
6132     */
6133    RequestBatcher.prototype.resetFlush = function() {
6134        this.scheduleFlush(this.libConfig['batch_flush_interval_ms']);
6135    };
6136
6137    /**
6138     * Schedule the next flush in the given number of milliseconds.
6139     */
6140    RequestBatcher.prototype.scheduleFlush = function(flushMS) {
6141        this.flushInterval = flushMS;
6142        if (!this.stopped) { // don't schedule anymore if batching has been stopped
6143            this.timeoutID = setTimeout(_.bind(function() {
6144                if (!this.stopped) {
6145                    this._flushPromise = this.flush();
6146                }
6147            }, this), this.flushInterval);
6148        }
6149    };
6150
6151    /**
6152     * Send a request using the sendRequest callback, but promisified.
6153     * TODO: sendRequest should be promisified in the first place.
6154     */
6155    RequestBatcher.prototype.sendRequestPromise = function(data, options) {
6156        return new PromisePolyfill(_.bind(function(resolve) {
6157            this.sendRequest(data, options, resolve);
6158        }, this));
6159    };
6160
6161
6162    /**
6163     * Flush one batch to network. Depending on success/failure modes, it will either
6164     * remove the batch from the queue or leave it in for retry, and schedule the next
6165     * flush. In cases of most network or API failures, it will back off exponentially
6166     * when retrying.
6167     * @param {Object} [options]
6168     * @param {boolean} [options.sendBeacon] - whether to send batch with
6169     * navigator.sendBeacon (only useful for sending batches before page unloads, as
6170     * sendBeacon offers no callbacks or status indications)
6171     */
6172    RequestBatcher.prototype.flush = function(options) {
6173        if (this.requestInProgress) {
6174            logger.log('Flush: Request already in progress');
6175            return PromisePolyfill.resolve();
6176        }
6177
6178        this.requestInProgress = true;
6179
6180        options = options || {};
6181        var timeoutMS = this.libConfig['batch_request_timeout_ms'];
6182        var startTime = new Date().getTime();
6183        var currentBatchSize = this.batchSize;
6184
6185        return this.queue.fillBatch(currentBatchSize)
6186            .then(_.bind(function(batch) {
6187
6188                // if there's more items in the queue than the batch size, attempt
6189                // to flush again after the current batch is done.
6190                var attemptSecondaryFlush = batch.length === currentBatchSize;
6191                var dataForRequest = [];
6192                var transformedItems = {};
6193                _.each(batch, function(item) {
6194                    var payload = item['payload'];
6195                    if (this.beforeSendHook && !item.orphaned) {
6196                        payload = this.beforeSendHook(payload);
6197                    }
6198                    if (payload) {
6199                        // mp_sent_by_lib_version prop captures which lib version actually
6200                        // sends each event (regardless of which version originally queued
6201                        // it for sending)
6202                        if (payload['event'] && payload['properties']) {
6203                            payload['properties'] = _.extend(
6204                                {},
6205                                payload['properties'],
6206                                {'mp_sent_by_lib_version': Config.LIB_VERSION}
6207                            );
6208                        }
6209                        var addPayload = true;
6210                        var itemId = item['id'];
6211                        if (itemId) {
6212                            if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
6213                                this.reportError('[dupe] item ID sent too many times, not sending', {
6214                                    item: item,
6215                                    batchSize: batch.length,
6216                                    timesSent: this.itemIdsSentSuccessfully[itemId]
6217                                });
6218                                addPayload = false;
6219                            }
6220                        } else {
6221                            this.reportError('[dupe] found item with no ID', {item: item});
6222                        }
6223
6224                        if (addPayload) {
6225                            dataForRequest.push(payload);
6226                        }
6227                    }
6228                    transformedItems[item['id']] = payload;
6229                }, this);
6230
6231                if (dataForRequest.length < 1) {
6232                    this.requestInProgress = false;
6233                    this.resetFlush();
6234                    return PromisePolyfill.resolve(); // nothing to do
6235                }
6236
6237                var removeItemsFromQueue = _.bind(function () {
6238                    return this.queue
6239                        .removeItemsByID(
6240                            _.map(batch, function (item) {
6241                                return item['id'];
6242                            })
6243                        )
6244                        .then(_.bind(function (succeeded) {
6245                            // client-side dedupe
6246                            _.each(batch, _.bind(function(item) {
6247                                var itemId = item['id'];
6248                                if (itemId) {
6249                                    this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
6250                                    this.itemIdsSentSuccessfully[itemId]++;
6251                                    if (this.itemIdsSentSuccessfully[itemId] > 5) {
6252                                        this.reportError('[dupe] item ID sent too many times', {
6253                                            item: item,
6254                                            batchSize: batch.length,
6255                                            timesSent: this.itemIdsSentSuccessfully[itemId]
6256                                        });
6257                                    }
6258                                } else {
6259                                    this.reportError('[dupe] found item with no ID while removing', {item: item});
6260                                }
6261                            }, this));
6262
6263                            if (succeeded) {
6264                                this.consecutiveRemovalFailures = 0;
6265                                if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
6266                                    this.resetFlush(); // schedule next batch with a delay
6267                                    return PromisePolyfill.resolve();
6268                                } else {
6269                                    return this.flush(); // handle next batch if the queue isn't empty
6270                                }
6271                            } else {
6272                                if (++this.consecutiveRemovalFailures > 5) {
6273                                    this.reportError('Too many queue failures; disabling batching system.');
6274                                    this.stopAllBatching();
6275                                } else {
6276                                    this.resetFlush();
6277                                }
6278                                return PromisePolyfill.resolve();
6279                            }
6280                        }, this));
6281                }, this);
6282
6283                var batchSendCallback = _.bind(function(res) {
6284                    this.requestInProgress = false;
6285
6286                    try {
6287
6288                        // handle API response in a try-catch to make sure we can reset the
6289                        // flush operation if something goes wrong
6290
6291                        if (options.unloading) {
6292                            // update persisted data to include hook transformations
6293                            return this.queue.updatePayloads(transformedItems);
6294                        } else if (
6295                            _.isObject(res) &&
6296                                res.error === 'timeout' &&
6297                                new Date().getTime() - startTime >= timeoutMS
6298                        ) {
6299                            this.reportError('Network timeout; retrying');
6300                            return this.flush();
6301                        } else if (
6302                            _.isObject(res) &&
6303                                (
6304                                    res.httpStatusCode >= 500
6305                                    || res.httpStatusCode === 429
6306                                    || (res.httpStatusCode <= 0 && !isOnline())
6307                                    || res.error === 'timeout'
6308                                )
6309                        ) {
6310                            // network or API error, or 429 Too Many Requests, retry
6311                            var retryMS = this.flushInterval * 2;
6312                            if (res.retryAfter) {
6313                                retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
6314                            }
6315                            retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
6316                            this.reportError('Error; retry in ' + retryMS + ' ms');
6317                            this.scheduleFlush(retryMS);
6318                            return PromisePolyfill.resolve();
6319                        } else if (_.isObject(res) && res.httpStatusCode === 413) {
6320                            // 413 Payload Too Large
6321                            if (batch.length > 1) {
6322                                var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
6323                                this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
6324                                this.reportError('413 response; reducing batch size to ' + this.batchSize);
6325                                this.resetFlush();
6326                                return PromisePolyfill.resolve();
6327                            } else {
6328                                this.reportError('Single-event request too large; dropping', batch);
6329                                this.resetBatchSize();
6330                                return removeItemsFromQueue();
6331                            }
6332                        } else {
6333                            // successful network request+response; remove each item in batch from queue
6334                            // (even if it was e.g. a 400, in which case retrying won't help)
6335                            return removeItemsFromQueue();
6336                        }
6337                    } catch(err) {
6338                        this.reportError('Error handling API response', err);
6339                        this.resetFlush();
6340                    }
6341                }, this);
6342                var requestOptions = {
6343                    method: 'POST',
6344                    verbose: true,
6345                    ignore_json_errors: true, // eslint-disable-line camelcase
6346                    timeout_ms: timeoutMS // eslint-disable-line camelcase
6347                };
6348                if (options.unloading) {
6349                    requestOptions.transport = 'sendBeacon';
6350                }
6351                logger.log('MIXPANEL REQUEST:', dataForRequest);
6352                return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
6353            }, this))
6354            .catch(_.bind(function(err) {
6355                this.reportError('Error flushing request queue', err);
6356                this.resetFlush();
6357            }, this));
6358    };
6359
6360    /**
6361     * Log error to global logger and optional user-defined logger.
6362     */
6363    RequestBatcher.prototype.reportError = function(msg, err) {
6364        logger.error.apply(logger.error, arguments);
6365        if (this.errorReporter) {
6366            try {
6367                if (!(err instanceof Error)) {
6368                    err = new Error(msg);
6369                }
6370                this.errorReporter(msg, err);
6371            } catch(err) {
6372                logger.error(err);
6373            }
6374        }
6375    };
6376
6377    /**
6378     * GDPR utils
6379     *
6380     * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection
6381     * and privacy for all individuals within the European Union. It addresses the export of personal
6382     * data outside the EU. The GDPR aims primarily to give control back to citizens and residents
6383     * over their personal data and to simplify the regulatory environment for international business
6384     * by unifying the regulation within the EU.
6385     *
6386     * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK.
6387     * These functions are used internally by the SDK and are not intended to be publicly exposed.
6388     */
6389
6390
6391    /**
6392     * A function used to track a Mixpanel event (e.g. MixpanelLib.track)
6393     * @callback trackFunction
6394     * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
6395     * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself.
6396     * @param {Function} [callback] If provided, the callback function will be called after tracking the event.
6397     */
6398
6399    /** Public **/
6400
6401    var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_';
6402
6403    /**
6404     * Opt the user in to data tracking and cookies/localstorage for the given token
6405     * @param {string} token - Mixpanel project tracking token
6406     * @param {Object} [options]
6407     * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action
6408     * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action
6409     * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action
6410     * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage
6411     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6412     * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires
6413     * @param {string} [options.cookieDomain] - custom cookie domain
6414     * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled
6415     * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not
6416     * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not
6417     */
6418    function optIn(token, options) {
6419        _optInOut(true, token, options);
6420    }
6421
6422    /**
6423     * Opt the user out of data tracking and cookies/localstorage for the given token
6424     * @param {string} token - Mixpanel project tracking token
6425     * @param {Object} [options]
6426     * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage
6427     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6428     * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires
6429     * @param {string} [options.cookieDomain] - custom cookie domain
6430     * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled
6431     * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not
6432     * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not
6433     */
6434    function optOut(token, options) {
6435        _optInOut(false, token, options);
6436    }
6437
6438    /**
6439     * Check whether the user has opted in to data tracking and cookies/localstorage for the given token
6440     * @param {string} token - Mixpanel project tracking token
6441     * @param {Object} [options]
6442     * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage
6443     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6444     * @returns {boolean} whether the user has opted in to the given opt type
6445     */
6446    function hasOptedIn(token, options) {
6447        return _getStorageValue(token, options) === '1';
6448    }
6449
6450    /**
6451     * Check whether the user has opted out of data tracking and cookies/localstorage for the given token
6452     * @param {string} token - Mixpanel project tracking token
6453     * @param {Object} [options]
6454     * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage
6455     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6456     * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false
6457     * @returns {boolean} whether the user has opted out of the given opt type
6458     */
6459    function hasOptedOut(token, options) {
6460        if (_hasDoNotTrackFlagOn(options)) {
6461            console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"');
6462            return true;
6463        }
6464        var optedOut = _getStorageValue(token, options) === '0';
6465        if (optedOut) {
6466            console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.');
6467        }
6468        return optedOut;
6469    }
6470
6471    /**
6472     * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token
6473     * If the user has opted out, return early instead of executing the method.
6474     * If a callback argument was provided, execute it passing the 0 error code.
6475     * @param {function} method - wrapped method to be executed if the user has not opted out
6476     * @returns {*} the result of executing method OR undefined if the user has opted out
6477     */
6478    function addOptOutCheckMixpanelLib(method) {
6479        return _addOptOutCheck(method, function(name) {
6480            return this.get_config(name);
6481        });
6482    }
6483
6484    /**
6485     * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token
6486     * If the user has opted out, return early instead of executing the method.
6487     * If a callback argument was provided, execute it passing the 0 error code.
6488     * @param {function} method - wrapped method to be executed if the user has not opted out
6489     * @returns {*} the result of executing method OR undefined if the user has opted out
6490     */
6491    function addOptOutCheckMixpanelPeople(method) {
6492        return _addOptOutCheck(method, function(name) {
6493            return this._get_config(name);
6494        });
6495    }
6496
6497    /**
6498     * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token
6499     * If the user has opted out, return early instead of executing the method.
6500     * If a callback argument was provided, execute it passing the 0 error code.
6501     * @param {function} method - wrapped method to be executed if the user has not opted out
6502     * @returns {*} the result of executing method OR undefined if the user has opted out
6503     */
6504    function addOptOutCheckMixpanelGroup(method) {
6505        return _addOptOutCheck(method, function(name) {
6506            return this._get_config(name);
6507        });
6508    }
6509
6510    /**
6511     * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token
6512     * @param {string} token - Mixpanel project tracking token
6513     * @param {Object} [options]
6514     * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage
6515     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6516     * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires
6517     * @param {string} [options.cookieDomain] - custom cookie domain
6518     * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled
6519     * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not
6520     * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not
6521     */
6522    function clearOptInOut(token, options) {
6523        options = options || {};
6524        _getStorage(options).remove(
6525            _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain
6526        );
6527    }
6528
6529    /** Private **/
6530
6531    /**
6532     * Get storage util
6533     * @param {Object} [options]
6534     * @param {string} [options.persistenceType]
6535     * @returns {object} either _.cookie or _.localstorage
6536     */
6537    function _getStorage(options) {
6538        options = options || {};
6539        return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie;
6540    }
6541
6542    /**
6543     * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.)
6544     * @param {string} token - Mixpanel project tracking token
6545     * @param {Object} [options]
6546     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6547     * @returns {string} the name of the cookie for the given opt type
6548     */
6549    function _getStorageKey(token, options) {
6550        options = options || {};
6551        return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token;
6552    }
6553
6554    /**
6555     * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.)
6556     * @param {string} token - Mixpanel project tracking token
6557     * @param {Object} [options]
6558     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6559     * @returns {string} the value of the cookie for the given opt type
6560     */
6561    function _getStorageValue(token, options) {
6562        return _getStorage(options).get(_getStorageKey(token, options));
6563    }
6564
6565    /**
6566     * Check whether the user has set the DNT/doNotTrack setting to true in their browser
6567     * @param {Object} [options]
6568     * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests
6569     * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false
6570     * @returns {boolean} whether the DNT setting is true
6571     */
6572    function _hasDoNotTrackFlagOn(options) {
6573        if (options && options.ignoreDnt) {
6574            return false;
6575        }
6576        var win$1 = (options && options.window) || win;
6577        var nav = win$1['navigator'] || {};
6578        var hasDntOn = false;
6579
6580        _.each([
6581            nav['doNotTrack'], // standard
6582            nav['msDoNotTrack'],
6583            win$1['doNotTrack']
6584        ], function(dntValue) {
6585            if (_.includes([true, 1, '1', 'yes'], dntValue)) {
6586                hasDntOn = true;
6587            }
6588        });
6589
6590        return hasDntOn;
6591    }
6592
6593    /**
6594     * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type
6595     * @param {boolean} optValue - whether to opt the user in or out for the given opt type
6596     * @param {string} token - Mixpanel project tracking token
6597     * @param {Object} [options]
6598     * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action
6599     * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action
6600     * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action
6601     * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name
6602     * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires
6603     * @param {string} [options.cookieDomain] - custom cookie domain
6604     * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled
6605     * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not
6606     * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not
6607     */
6608    function _optInOut(optValue, token, options) {
6609        if (!_.isString(token) || !token.length) {
6610            console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token');
6611            return;
6612        }
6613
6614        options = options || {};
6615
6616        _getStorage(options).set(
6617            _getStorageKey(token, options),
6618            optValue ? 1 : 0,
6619            _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null,
6620            !!options.crossSubdomainCookie,
6621            !!options.secureCookie,
6622            !!options.crossSiteCookie,
6623            options.cookieDomain
6624        );
6625
6626        if (options.track && optValue) { // only track event if opting in (optValue=true)
6627            options.track(options.trackEventName || '$opt_in', options.trackProperties, {
6628                'send_immediately': true
6629            });
6630        }
6631    }
6632
6633    /**
6634     * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token
6635     * If the user has opted out, return early instead of executing the method.
6636     * If a callback argument was provided, execute it passing the 0 error code.
6637     * @param {function} method - wrapped method to be executed if the user has not opted out
6638     * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check
6639     * @returns {*} the result of executing method OR undefined if the user has opted out
6640     */
6641    function _addOptOutCheck(method, getConfigValue) {
6642        return function() {
6643            var optedOut = false;
6644
6645            try {
6646                var token = getConfigValue.call(this, 'token');
6647                var ignoreDnt = getConfigValue.call(this, 'ignore_dnt');
6648                var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type');
6649                var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix');
6650                var win = getConfigValue.call(this, 'window'); // used to override window during browser tests
6651
6652                if (token) { // if there was an issue getting the token, continue method execution as normal
6653                    optedOut = hasOptedOut(token, {
6654                        ignoreDnt: ignoreDnt,
6655                        persistenceType: persistenceType,
6656                        persistencePrefix: persistencePrefix,
6657                        window: win
6658                    });
6659                }
6660            } catch(err) {
6661                console.error('Unexpected error when checking tracking opt-out status: ' + err);
6662            }
6663
6664            if (!optedOut) {
6665                return method.apply(this, arguments);
6666            }
6667
6668            var callback = arguments[arguments.length - 1];
6669            if (typeof(callback) === 'function') {
6670                callback(0);
6671            }
6672
6673            return;
6674        };
6675    }
6676
6677    /* eslint camelcase: "off" */
6678
6679
6680    /** @const */ var SET_ACTION      = '$set';
6681    /** @const */ var SET_ONCE_ACTION = '$set_once';
6682    /** @const */ var UNSET_ACTION    = '$unset';
6683    /** @const */ var ADD_ACTION      = '$add';
6684    /** @const */ var APPEND_ACTION   = '$append';
6685    /** @const */ var UNION_ACTION    = '$union';
6686    /** @const */ var REMOVE_ACTION   = '$remove';
6687    /** @const */ var DELETE_ACTION   = '$delete';
6688
6689    // Common internal methods for mixpanel.people and mixpanel.group APIs.
6690    // These methods shouldn't involve network I/O.
6691    var apiActions = {
6692        set_action: function(prop, to) {
6693            var data = {};
6694            var $set = {};
6695            if (_.isObject(prop)) {
6696                _.each(prop, function(v, k) {
6697                    if (!this._is_reserved_property(k)) {
6698                        $set[k] = v;
6699                    }
6700                }, this);
6701            } else {
6702                $set[prop] = to;
6703            }
6704
6705            data[SET_ACTION] = $set;
6706            return data;
6707        },
6708
6709        unset_action: function(prop) {
6710            var data = {};
6711            var $unset = [];
6712            if (!_.isArray(prop)) {
6713                prop = [prop];
6714            }
6715
6716            _.each(prop, function(k) {
6717                if (!this._is_reserved_property(k)) {
6718                    $unset.push(k);
6719                }
6720            }, this);
6721
6722            data[UNSET_ACTION] = $unset;
6723            return data;
6724        },
6725
6726        set_once_action: function(prop, to) {
6727            var data = {};
6728            var $set_once = {};
6729            if (_.isObject(prop)) {
6730                _.each(prop, function(v, k) {
6731                    if (!this._is_reserved_property(k)) {
6732                        $set_once[k] = v;
6733                    }
6734                }, this);
6735            } else {
6736                $set_once[prop] = to;
6737            }
6738            data[SET_ONCE_ACTION] = $set_once;
6739            return data;
6740        },
6741
6742        union_action: function(list_name, values) {
6743            var data = {};
6744            var $union = {};
6745            if (_.isObject(list_name)) {
6746                _.each(list_name, function(v, k) {
6747                    if (!this._is_reserved_property(k)) {
6748                        $union[k] = _.isArray(v) ? v : [v];
6749                    }
6750                }, this);
6751            } else {
6752                $union[list_name] = _.isArray(values) ? values : [values];
6753            }
6754            data[UNION_ACTION] = $union;
6755            return data;
6756        },
6757
6758        append_action: function(list_name, value) {
6759            var data = {};
6760            var $append = {};
6761            if (_.isObject(list_name)) {
6762                _.each(list_name, function(v, k) {
6763                    if (!this._is_reserved_property(k)) {
6764                        $append[k] = v;
6765                    }
6766                }, this);
6767            } else {
6768                $append[list_name] = value;
6769            }
6770            data[APPEND_ACTION] = $append;
6771            return data;
6772        },
6773
6774        remove_action: function(list_name, value) {
6775            var data = {};
6776            var $remove = {};
6777            if (_.isObject(list_name)) {
6778                _.each(list_name, function(v, k) {
6779                    if (!this._is_reserved_property(k)) {
6780                        $remove[k] = v;
6781                    }
6782                }, this);
6783            } else {
6784                $remove[list_name] = value;
6785            }
6786            data[REMOVE_ACTION] = $remove;
6787            return data;
6788        },
6789
6790        delete_action: function() {
6791            var data = {};
6792            data[DELETE_ACTION] = '';
6793            return data;
6794        }
6795    };
6796
6797    /* eslint camelcase: "off" */
6798
6799    /**
6800     * Mixpanel Group Object
6801     * @constructor
6802     */
6803    var MixpanelGroup = function() {};
6804
6805    _.extend(MixpanelGroup.prototype, apiActions);
6806
6807    MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) {
6808        this._mixpanel = mixpanel_instance;
6809        this._group_key = group_key;
6810        this._group_id = group_id;
6811    };
6812
6813    /**
6814     * Set properties on a group.
6815     *
6816     * ### Usage:
6817     *
6818     *     mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard');
6819     *
6820     *     // or set multiple properties at once
6821     *     mixpanel.get_group('company', 'mixpanel').set({
6822     *          'Location': '405 Howard',
6823     *          'Founded' : 2009,
6824     *     });
6825     *     // properties can be strings, integers, dates, or lists
6826     *
6827     * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
6828     * @param {*} [to] A value to set on the given property name
6829     * @param {Function} [callback] If provided, the callback will be called after the tracking event
6830     */
6831    MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) {
6832        var data = this.set_action(prop, to);
6833        if (_.isObject(prop)) {
6834            callback = to;
6835        }
6836        return this._send_request(data, callback);
6837    });
6838
6839    /**
6840     * Set properties on a group, only if they do not yet exist.
6841     * This will not overwrite previous group property values, unlike
6842     * group.set().
6843     *
6844     * ### Usage:
6845     *
6846     *     mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard');
6847     *
6848     *     // or set multiple properties at once
6849     *     mixpanel.get_group('company', 'mixpanel').set_once({
6850     *          'Location': '405 Howard',
6851     *          'Founded' : 2009,
6852     *     });
6853     *     // properties can be strings, integers, lists or dates
6854     *
6855     * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
6856     * @param {*} [to] A value to set on the given property name
6857     * @param {Function} [callback] If provided, the callback will be called after the tracking event
6858     */
6859    MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) {
6860        var data = this.set_once_action(prop, to);
6861        if (_.isObject(prop)) {
6862            callback = to;
6863        }
6864        return this._send_request(data, callback);
6865    });
6866
6867    /**
6868     * Unset properties on a group permanently.
6869     *
6870     * ### Usage:
6871     *
6872     *     mixpanel.get_group('company', 'mixpanel').unset('Founded');
6873     *
6874     * @param {String} prop The name of the property.
6875     * @param {Function} [callback] If provided, the callback will be called after the tracking event
6876     */
6877    MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) {
6878        var data = this.unset_action(prop);
6879        return this._send_request(data, callback);
6880    });
6881
6882    /**
6883     * Merge a given list with a list-valued group property, excluding duplicate values.
6884     *
6885     * ### Usage:
6886     *
6887     *     // merge a value to a list, creating it if needed
6888     *     mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']);
6889     *
6890     * @param {String} list_name Name of the property.
6891     * @param {Array} values Values to merge with the given property
6892     * @param {Function} [callback] If provided, the callback will be called after the tracking event
6893     */
6894    MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) {
6895        if (_.isObject(list_name)) {
6896            callback = values;
6897        }
6898        var data = this.union_action(list_name, values);
6899        return this._send_request(data, callback);
6900    });
6901
6902    /**
6903     * Permanently delete a group.
6904     *
6905     * ### Usage:
6906     *
6907     *     mixpanel.get_group('company', 'mixpanel').delete();
6908     *
6909     * @param {Function} [callback] If provided, the callback will be called after the tracking event
6910     */
6911    MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
6912        // bracket notation above prevents a minification error related to reserved words
6913        var data = this.delete_action();
6914        return this._send_request(data, callback);
6915    });
6916
6917    /**
6918     * Remove a property from a group. The value will be ignored if doesn't exist.
6919     *
6920     * ### Usage:
6921     *
6922     *     mixpanel.get_group('company', 'mixpanel').remove('Location', 'London');
6923     *
6924     * @param {String} list_name Name of the property.
6925     * @param {Object} value Value to remove from the given group property
6926     * @param {Function} [callback] If provided, the callback will be called after the tracking event
6927     */
6928    MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) {
6929        var data = this.remove_action(list_name, value);
6930        return this._send_request(data, callback);
6931    });
6932
6933    MixpanelGroup.prototype._send_request = function(data, callback) {
6934        data['$group_key'] = this._group_key;
6935        data['$group_id'] = this._group_id;
6936        data['$token'] = this._get_config('token');
6937
6938        var date_encoded_data = _.encodeDates(data);
6939        return this._mixpanel._track_or_batch({
6940            type: 'groups',
6941            data: date_encoded_data,
6942            endpoint: this._mixpanel.get_api_host('groups') + '/' +  this._get_config('api_routes')['groups'],
6943            batcher: this._mixpanel.request_batchers.groups
6944        }, callback);
6945    };
6946
6947    MixpanelGroup.prototype._is_reserved_property = function(prop) {
6948        return prop === '$group_key' || prop === '$group_id';
6949    };
6950
6951    MixpanelGroup.prototype._get_config = function(conf) {
6952        return this._mixpanel.get_config(conf);
6953    };
6954
6955    MixpanelGroup.prototype.toString = function() {
6956        return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id;
6957    };
6958
6959    // MixpanelGroup Exports
6960    MixpanelGroup.prototype['remove']   = MixpanelGroup.prototype.remove;
6961    MixpanelGroup.prototype['set']      = MixpanelGroup.prototype.set;
6962    MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once;
6963    MixpanelGroup.prototype['union']    = MixpanelGroup.prototype.union;
6964    MixpanelGroup.prototype['unset']    = MixpanelGroup.prototype.unset;
6965    MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString;
6966
6967    /* eslint camelcase: "off" */
6968
6969    /**
6970     * Mixpanel People Object
6971     * @constructor
6972     */
6973    var MixpanelPeople = function() {};
6974
6975    _.extend(MixpanelPeople.prototype, apiActions);
6976
6977    MixpanelPeople.prototype._init = function(mixpanel_instance) {
6978        this._mixpanel = mixpanel_instance;
6979    };
6980
6981    /*
6982    * Set properties on a user record.
6983    *
6984    * ### Usage:
6985    *
6986    *     mixpanel.people.set('gender', 'm');
6987    *
6988    *     // or set multiple properties at once
6989    *     mixpanel.people.set({
6990    *         'Company': 'Acme',
6991    *         'Plan': 'Premium',
6992    *         'Upgrade date': new Date()
6993    *     });
6994    *     // properties can be strings, integers, dates, or lists
6995    *
6996    * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
6997    * @param {*} [to] A value to set on the given property name
6998    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
6999    */
7000    MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
7001        var data = this.set_action(prop, to);
7002        if (_.isObject(prop)) {
7003            callback = to;
7004        }
7005        // make sure that the referrer info has been updated and saved
7006        if (this._get_config('save_referrer')) {
7007            this._mixpanel['persistence'].update_referrer_info(document.referrer);
7008        }
7009
7010        // update $set object with default people properties
7011        data[SET_ACTION] = _.extend(
7012            {},
7013            _.info.people_properties(),
7014            data[SET_ACTION]
7015        );
7016        return this._send_request(data, callback);
7017    });
7018
7019    /*
7020    * Set properties on a user record, only if they do not yet exist.
7021    * This will not overwrite previous people property values, unlike
7022    * people.set().
7023    *
7024    * ### Usage:
7025    *
7026    *     mixpanel.people.set_once('First Login Date', new Date());
7027    *
7028    *     // or set multiple properties at once
7029    *     mixpanel.people.set_once({
7030    *         'First Login Date': new Date(),
7031    *         'Starting Plan': 'Premium'
7032    *     });
7033    *
7034    *     // properties can be strings, integers or dates
7035    *
7036    * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
7037    * @param {*} [to] A value to set on the given property name
7038    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7039    */
7040    MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
7041        var data = this.set_once_action(prop, to);
7042        if (_.isObject(prop)) {
7043            callback = to;
7044        }
7045        return this._send_request(data, callback);
7046    });
7047
7048    /*
7049    * Unset properties on a user record (permanently removes the properties and their values from a profile).
7050    *
7051    * ### Usage:
7052    *
7053    *     mixpanel.people.unset('gender');
7054    *
7055    *     // or unset multiple properties at once
7056    *     mixpanel.people.unset(['gender', 'Company']);
7057    *
7058    * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names.
7059    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7060    */
7061    MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) {
7062        var data = this.unset_action(prop);
7063        return this._send_request(data, callback);
7064    });
7065
7066    /*
7067    * Increment/decrement numeric people analytics properties.
7068    *
7069    * ### Usage:
7070    *
7071    *     mixpanel.people.increment('page_views', 1);
7072    *
7073    *     // or, for convenience, if you're just incrementing a counter by
7074    *     // 1, you can simply do
7075    *     mixpanel.people.increment('page_views');
7076    *
7077    *     // to decrement a counter, pass a negative number
7078    *     mixpanel.people.increment('credits_left', -1);
7079    *
7080    *     // like mixpanel.people.set(), you can increment multiple
7081    *     // properties at once:
7082    *     mixpanel.people.increment({
7083    *         counter1: 1,
7084    *         counter2: 6
7085    *     });
7086    *
7087    * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values.
7088    * @param {Number} [by] An amount to increment the given property
7089    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7090    */
7091    MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) {
7092        var data = {};
7093        var $add = {};
7094        if (_.isObject(prop)) {
7095            _.each(prop, function(v, k) {
7096                if (!this._is_reserved_property(k)) {
7097                    if (isNaN(parseFloat(v))) {
7098                        console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
7099                        return;
7100                    } else {
7101                        $add[k] = v;
7102                    }
7103                }
7104            }, this);
7105            callback = by;
7106        } else {
7107            // convenience: mixpanel.people.increment('property'); will
7108            // increment 'property' by 1
7109            if (_.isUndefined(by)) {
7110                by = 1;
7111            }
7112            $add[prop] = by;
7113        }
7114        data[ADD_ACTION] = $add;
7115
7116        return this._send_request(data, callback);
7117    });
7118
7119    /*
7120    * Append a value to a list-valued people analytics property.
7121    *
7122    * ### Usage:
7123    *
7124    *     // append a value to a list, creating it if needed
7125    *     mixpanel.people.append('pages_visited', 'homepage');
7126    *
7127    *     // like mixpanel.people.set(), you can append multiple
7128    *     // properties at once:
7129    *     mixpanel.people.append({
7130    *         list1: 'bob',
7131    *         list2: 123
7132    *     });
7133    *
7134    * @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
7135    * @param {*} [value] value An item to append to the list
7136    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7137    */
7138    MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
7139        if (_.isObject(list_name)) {
7140            callback = value;
7141        }
7142        var data = this.append_action(list_name, value);
7143        return this._send_request(data, callback);
7144    });
7145
7146    /*
7147    * Remove a value from a list-valued people analytics property.
7148    *
7149    * ### Usage:
7150    *
7151    *     mixpanel.people.remove('School', 'UCB');
7152    *
7153    * @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
7154    * @param {*} [value] value Item to remove from the list
7155    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7156    */
7157    MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
7158        if (_.isObject(list_name)) {
7159            callback = value;
7160        }
7161        var data = this.remove_action(list_name, value);
7162        return this._send_request(data, callback);
7163    });
7164
7165    /*
7166    * Merge a given list with a list-valued people analytics property,
7167    * excluding duplicate values.
7168    *
7169    * ### Usage:
7170    *
7171    *     // merge a value to a list, creating it if needed
7172    *     mixpanel.people.union('pages_visited', 'homepage');
7173    *
7174    *     // like mixpanel.people.set(), you can append multiple
7175    *     // properties at once:
7176    *     mixpanel.people.union({
7177    *         list1: 'bob',
7178    *         list2: 123
7179    *     });
7180    *
7181    *     // like mixpanel.people.append(), you can append multiple
7182    *     // values to the same list:
7183    *     mixpanel.people.union({
7184    *         list1: ['bob', 'billy']
7185    *     });
7186    *
7187    * @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values.
7188    * @param {*} [value] Value / values to merge with the given property
7189    * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7190    */
7191    MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) {
7192        if (_.isObject(list_name)) {
7193            callback = values;
7194        }
7195        var data = this.union_action(list_name, values);
7196        return this._send_request(data, callback);
7197    });
7198
7199    /*
7200     * Record that you have charged the current user a certain amount
7201     * of money. Charges recorded with track_charge() will appear in the
7202     * Mixpanel revenue report.
7203     *
7204     * ### Usage:
7205     *
7206     *     // charge a user $50
7207     *     mixpanel.people.track_charge(50);
7208     *
7209     *     // charge a user $30.50 on the 2nd of january
7210     *     mixpanel.people.track_charge(30.50, {
7211     *         '$time': new Date('jan 1 2012')
7212     *     });
7213     *
7214     * @param {Number} amount The amount of money charged to the current user
7215     * @param {Object} [properties] An associative array of properties associated with the charge
7216     * @param {Function} [callback] If provided, the callback will be called when the server responds
7217     * @deprecated
7218     */
7219    MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function() {
7220        console.error('mixpanel.people.track_charge() is deprecated and no longer has any effect.');
7221    });
7222
7223    /*
7224     * Permanently clear all revenue report transactions from the
7225     * current user's people analytics profile.
7226     *
7227     * ### Usage:
7228     *
7229     *     mixpanel.people.clear_charges();
7230     *
7231     * @param {Function} [callback] If provided, the callback will be called after tracking the event.
7232     * @deprecated
7233     */
7234    MixpanelPeople.prototype.clear_charges = function(callback) {
7235        return this.set('$transactions', [], callback);
7236    };
7237
7238    /*
7239    * Permanently deletes the current people analytics profile from
7240    * Mixpanel (using the current distinct_id).
7241    *
7242    * ### Usage:
7243    *
7244    *     // remove the all data you have stored about the current user
7245    *     mixpanel.people.delete_user();
7246    *
7247    */
7248    MixpanelPeople.prototype.delete_user = function() {
7249        if (!this._identify_called()) {
7250            console.error('mixpanel.people.delete_user() requires you to call identify() first');
7251            return;
7252        }
7253        var data = {'$delete': this._mixpanel.get_distinct_id()};
7254        return this._send_request(data);
7255    };
7256
7257    MixpanelPeople.prototype.toString = function() {
7258        return this._mixpanel.toString() + '.people';
7259    };
7260
7261    MixpanelPeople.prototype._send_request = function(data, callback) {
7262        data['$token'] = this._get_config('token');
7263        data['$distinct_id'] = this._mixpanel.get_distinct_id();
7264        var device_id = this._mixpanel.get_property('$device_id');
7265        var user_id = this._mixpanel.get_property('$user_id');
7266        var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id');
7267        if (device_id) {
7268            data['$device_id'] = device_id;
7269        }
7270        if (user_id) {
7271            data['$user_id'] = user_id;
7272        }
7273        if (had_persisted_distinct_id) {
7274            data['$had_persisted_distinct_id'] = had_persisted_distinct_id;
7275        }
7276
7277        var date_encoded_data = _.encodeDates(data);
7278
7279        if (!this._identify_called()) {
7280            this._enqueue(data);
7281            if (!_.isUndefined(callback)) {
7282                if (this._get_config('verbose')) {
7283                    callback({status: -1, error: null});
7284                } else {
7285                    callback(-1);
7286                }
7287            }
7288            return _.truncate(date_encoded_data, 255);
7289        }
7290
7291        return this._mixpanel._track_or_batch({
7292            type: 'people',
7293            data: date_encoded_data,
7294            endpoint: this._mixpanel.get_api_host('people') + '/' +  this._get_config('api_routes')['engage'],
7295            batcher: this._mixpanel.request_batchers.people
7296        }, callback);
7297    };
7298
7299    MixpanelPeople.prototype._get_config = function(conf_var) {
7300        return this._mixpanel.get_config(conf_var);
7301    };
7302
7303    MixpanelPeople.prototype._identify_called = function() {
7304        return this._mixpanel._flags.identify_called === true;
7305    };
7306
7307    // Queue up engage operations if identify hasn't been called yet.
7308    MixpanelPeople.prototype._enqueue = function(data) {
7309        if (SET_ACTION in data) {
7310            this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data);
7311        } else if (SET_ONCE_ACTION in data) {
7312            this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data);
7313        } else if (UNSET_ACTION in data) {
7314            this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data);
7315        } else if (ADD_ACTION in data) {
7316            this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data);
7317        } else if (APPEND_ACTION in data) {
7318            this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data);
7319        } else if (REMOVE_ACTION in data) {
7320            this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data);
7321        } else if (UNION_ACTION in data) {
7322            this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
7323        } else {
7324            console.error('Invalid call to _enqueue():', data);
7325        }
7326    };
7327
7328    MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) {
7329        var _this = this;
7330        var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action));
7331        var action_params = queued_data;
7332
7333        if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) {
7334            _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data);
7335            _this._mixpanel['persistence'].save();
7336            if (queue_to_params_fn) {
7337                action_params = queue_to_params_fn(queued_data);
7338            }
7339            action_method.call(_this, action_params, function(response, data) {
7340                // on bad response, we want to add it back to the queue
7341                if (response === 0) {
7342                    _this._mixpanel['persistence']._add_to_people_queue(action, queued_data);
7343                }
7344                if (!_.isUndefined(callback)) {
7345                    callback(response, data);
7346                }
7347            });
7348        }
7349    };
7350
7351    // Flush queued engage operations - order does not matter,
7352    // and there are network level race conditions anyway
7353    MixpanelPeople.prototype._flush = function(
7354        _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
7355    ) {
7356        var _this = this;
7357
7358        this._flush_one_queue(SET_ACTION, this.set, _set_callback);
7359        this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback);
7360        this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); });
7361        this._flush_one_queue(ADD_ACTION, this.increment, _add_callback);
7362        this._flush_one_queue(UNION_ACTION, this.union, _union_callback);
7363
7364        // we have to fire off each $append individually since there is
7365        // no concat method server side
7366        var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION);
7367        if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
7368            var $append_item;
7369            var append_callback = function(response, data) {
7370                if (response === 0) {
7371                    _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item);
7372                }
7373                if (!_.isUndefined(_append_callback)) {
7374                    _append_callback(response, data);
7375                }
7376            };
7377            for (var i = $append_queue.length - 1; i >= 0; i--) {
7378                $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION);
7379                $append_item = $append_queue.pop();
7380                _this._mixpanel['persistence'].save();
7381                if (!_.isEmptyObject($append_item)) {
7382                    _this.append($append_item, append_callback);
7383                }
7384            }
7385        }
7386
7387        // same for $remove
7388        var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION);
7389        if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) {
7390            var $remove_item;
7391            var remove_callback = function(response, data) {
7392                if (response === 0) {
7393                    _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item);
7394                }
7395                if (!_.isUndefined(_remove_callback)) {
7396                    _remove_callback(response, data);
7397                }
7398            };
7399            for (var j = $remove_queue.length - 1; j >= 0; j--) {
7400                $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION);
7401                $remove_item = $remove_queue.pop();
7402                _this._mixpanel['persistence'].save();
7403                if (!_.isEmptyObject($remove_item)) {
7404                    _this.remove($remove_item, remove_callback);
7405                }
7406            }
7407        }
7408    };
7409
7410    MixpanelPeople.prototype._is_reserved_property = function(prop) {
7411        return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id';
7412    };
7413
7414    // MixpanelPeople Exports
7415    MixpanelPeople.prototype['set']           = MixpanelPeople.prototype.set;
7416    MixpanelPeople.prototype['set_once']      = MixpanelPeople.prototype.set_once;
7417    MixpanelPeople.prototype['unset']         = MixpanelPeople.prototype.unset;
7418    MixpanelPeople.prototype['increment']     = MixpanelPeople.prototype.increment;
7419    MixpanelPeople.prototype['append']        = MixpanelPeople.prototype.append;
7420    MixpanelPeople.prototype['remove']        = MixpanelPeople.prototype.remove;
7421    MixpanelPeople.prototype['union']         = MixpanelPeople.prototype.union;
7422    MixpanelPeople.prototype['track_charge']  = MixpanelPeople.prototype.track_charge;
7423    MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
7424    MixpanelPeople.prototype['delete_user']   = MixpanelPeople.prototype.delete_user;
7425    MixpanelPeople.prototype['toString']      = MixpanelPeople.prototype.toString;
7426
7427    /* eslint camelcase: "off" */
7428
7429
7430    /*
7431     * Constants
7432     */
7433    /** @const */ var SET_QUEUE_KEY          = '__mps';
7434    /** @const */ var SET_ONCE_QUEUE_KEY     = '__mpso';
7435    /** @const */ var UNSET_QUEUE_KEY        = '__mpus';
7436    /** @const */ var ADD_QUEUE_KEY          = '__mpa';
7437    /** @const */ var APPEND_QUEUE_KEY       = '__mpap';
7438    /** @const */ var REMOVE_QUEUE_KEY       = '__mpr';
7439    /** @const */ var UNION_QUEUE_KEY        = '__mpu';
7440    // This key is deprecated, but we want to check for it to see whether aliasing is allowed.
7441    /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id';
7442    /** @const */ var ALIAS_ID_KEY           = '__alias';
7443    /** @const */ var EVENT_TIMERS_KEY       = '__timers';
7444    /** @const */ var RESERVED_PROPERTIES = [
7445        SET_QUEUE_KEY,
7446        SET_ONCE_QUEUE_KEY,
7447        UNSET_QUEUE_KEY,
7448        ADD_QUEUE_KEY,
7449        APPEND_QUEUE_KEY,
7450        REMOVE_QUEUE_KEY,
7451        UNION_QUEUE_KEY,
7452        PEOPLE_DISTINCT_ID_KEY,
7453        ALIAS_ID_KEY,
7454        EVENT_TIMERS_KEY
7455    ];
7456
7457    /**
7458     * Mixpanel Persistence Object
7459     * @constructor
7460     */
7461    var MixpanelPersistence = function(config) {
7462        this['props'] = {};
7463        this.campaign_params_saved = false;
7464
7465        if (config['persistence_name']) {
7466            this.name = 'mp_' + config['persistence_name'];
7467        } else {
7468            this.name = 'mp_' + config['token'] + '_mixpanel';
7469        }
7470
7471        var storage_type = config['persistence'];
7472        if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
7473            console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
7474            storage_type = config['persistence'] = 'cookie';
7475        }
7476
7477        if (storage_type === 'localStorage' && _.localStorage.is_supported()) {
7478            this.storage = _.localStorage;
7479        } else {
7480            this.storage = _.cookie;
7481        }
7482
7483        this.load();
7484        this.update_config(config);
7485        this.upgrade();
7486        this.save();
7487    };
7488
7489    MixpanelPersistence.prototype.properties = function() {
7490        var p = {};
7491
7492        this.load();
7493
7494        // Filter out reserved properties
7495        _.each(this['props'], function(v, k) {
7496            if (!_.include(RESERVED_PROPERTIES, k)) {
7497                p[k] = v;
7498            }
7499        });
7500        return p;
7501    };
7502
7503    MixpanelPersistence.prototype.load = function() {
7504        if (this.disabled) { return; }
7505
7506        var entry = this.storage.parse(this.name);
7507
7508        if (entry) {
7509            this['props'] = _.extend({}, entry);
7510        }
7511    };
7512
7513    MixpanelPersistence.prototype.upgrade = function() {
7514        var old_cookie,
7515            old_localstorage;
7516
7517        // if transferring from cookie to localStorage or vice-versa, copy existing
7518        // super properties over to new storage mode
7519        if (this.storage === _.localStorage) {
7520            old_cookie = _.cookie.parse(this.name);
7521
7522            _.cookie.remove(this.name);
7523            _.cookie.remove(this.name, true);
7524
7525            if (old_cookie) {
7526                this.register_once(old_cookie);
7527            }
7528        } else if (this.storage === _.cookie) {
7529            old_localstorage = _.localStorage.parse(this.name);
7530
7531            _.localStorage.remove(this.name);
7532
7533            if (old_localstorage) {
7534                this.register_once(old_localstorage);
7535            }
7536        }
7537    };
7538
7539    MixpanelPersistence.prototype.save = function() {
7540        if (this.disabled) { return; }
7541
7542        this.storage.set(
7543            this.name,
7544            JSONStringify(this['props']),
7545            this.expire_days,
7546            this.cross_subdomain,
7547            this.secure,
7548            this.cross_site,
7549            this.cookie_domain
7550        );
7551    };
7552
7553    MixpanelPersistence.prototype.load_prop = function(key) {
7554        this.load();
7555        return this['props'][key];
7556    };
7557
7558    MixpanelPersistence.prototype.remove = function() {
7559        // remove both domain and subdomain cookies
7560        this.storage.remove(this.name, false, this.cookie_domain);
7561        this.storage.remove(this.name, true, this.cookie_domain);
7562    };
7563
7564    // removes the storage entry and deletes all loaded data
7565    // forced name for tests
7566    MixpanelPersistence.prototype.clear = function() {
7567        this.remove();
7568        this['props'] = {};
7569    };
7570
7571    /**
7572    * @param {Object} props
7573    * @param {*=} default_value
7574    * @param {number=} days
7575    */
7576    MixpanelPersistence.prototype.register_once = function(props, default_value, days) {
7577        if (_.isObject(props)) {
7578            if (typeof(default_value) === 'undefined') { default_value = 'None'; }
7579            this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
7580
7581            this.load();
7582
7583            _.each(props, function(val, prop) {
7584                if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) {
7585                    this['props'][prop] = val;
7586                }
7587            }, this);
7588
7589            this.save();
7590
7591            return true;
7592        }
7593        return false;
7594    };
7595
7596    /**
7597    * @param {Object} props
7598    * @param {number=} days
7599    */
7600    MixpanelPersistence.prototype.register = function(props, days) {
7601        if (_.isObject(props)) {
7602            this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
7603
7604            this.load();
7605            _.extend(this['props'], props);
7606            this.save();
7607
7608            return true;
7609        }
7610        return false;
7611    };
7612
7613    MixpanelPersistence.prototype.unregister = function(prop) {
7614        this.load();
7615        if (prop in this['props']) {
7616            delete this['props'][prop];
7617            this.save();
7618        }
7619    };
7620
7621    MixpanelPersistence.prototype.update_search_keyword = function(referrer) {
7622        this.register(_.info.searchInfo(referrer));
7623    };
7624
7625    // EXPORTED METHOD, we test this directly.
7626    MixpanelPersistence.prototype.update_referrer_info = function(referrer) {
7627        // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
7628        this.register_once({
7629            '$initial_referrer': referrer || '$direct',
7630            '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct'
7631        }, '');
7632    };
7633
7634    MixpanelPersistence.prototype.get_referrer_info = function() {
7635        return _.strip_empty_properties({
7636            '$initial_referrer': this['props']['$initial_referrer'],
7637            '$initial_referring_domain': this['props']['$initial_referring_domain']
7638        });
7639    };
7640
7641    MixpanelPersistence.prototype.update_config = function(config) {
7642        this.default_expiry = this.expire_days = config['cookie_expiration'];
7643        this.set_disabled(config['disable_persistence']);
7644        this.set_cookie_domain(config['cookie_domain']);
7645        this.set_cross_site(config['cross_site_cookie']);
7646        this.set_cross_subdomain(config['cross_subdomain_cookie']);
7647        this.set_secure(config['secure_cookie']);
7648    };
7649
7650    MixpanelPersistence.prototype.set_disabled = function(disabled) {
7651        this.disabled = disabled;
7652        if (this.disabled) {
7653            this.remove();
7654        } else {
7655            this.save();
7656        }
7657    };
7658
7659    MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) {
7660        if (cookie_domain !== this.cookie_domain) {
7661            this.remove();
7662            this.cookie_domain = cookie_domain;
7663            this.save();
7664        }
7665    };
7666
7667    MixpanelPersistence.prototype.set_cross_site = function(cross_site) {
7668        if (cross_site !== this.cross_site) {
7669            this.cross_site = cross_site;
7670            this.remove();
7671            this.save();
7672        }
7673    };
7674
7675    MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) {
7676        if (cross_subdomain !== this.cross_subdomain) {
7677            this.cross_subdomain = cross_subdomain;
7678            this.remove();
7679            this.save();
7680        }
7681    };
7682
7683    MixpanelPersistence.prototype.get_cross_subdomain = function() {
7684        return this.cross_subdomain;
7685    };
7686
7687    MixpanelPersistence.prototype.set_secure = function(secure) {
7688        if (secure !== this.secure) {
7689            this.secure = secure ? true : false;
7690            this.remove();
7691            this.save();
7692        }
7693    };
7694
7695    MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
7696        var q_key = this._get_queue_key(queue),
7697            q_data = data[queue],
7698            set_q = this._get_or_create_queue(SET_ACTION),
7699            set_once_q = this._get_or_create_queue(SET_ONCE_ACTION),
7700            unset_q = this._get_or_create_queue(UNSET_ACTION),
7701            add_q = this._get_or_create_queue(ADD_ACTION),
7702            union_q = this._get_or_create_queue(UNION_ACTION),
7703            remove_q = this._get_or_create_queue(REMOVE_ACTION, []),
7704            append_q = this._get_or_create_queue(APPEND_ACTION, []);
7705
7706        if (q_key === SET_QUEUE_KEY) {
7707            // Update the set queue - we can override any existing values
7708            _.extend(set_q, q_data);
7709            // if there was a pending increment, override it
7710            // with the set.
7711            this._pop_from_people_queue(ADD_ACTION, q_data);
7712            // if there was a pending union, override it
7713            // with the set.
7714            this._pop_from_people_queue(UNION_ACTION, q_data);
7715            this._pop_from_people_queue(UNSET_ACTION, q_data);
7716        } else if (q_key === SET_ONCE_QUEUE_KEY) {
7717            // only queue the data if there is not already a set_once call for it.
7718            _.each(q_data, function(v, k) {
7719                if (!(k in set_once_q)) {
7720                    set_once_q[k] = v;
7721                }
7722            });
7723            this._pop_from_people_queue(UNSET_ACTION, q_data);
7724        } else if (q_key === UNSET_QUEUE_KEY) {
7725            _.each(q_data, function(prop) {
7726
7727                // undo previously-queued actions on this key
7728                _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) {
7729                    if (prop in enqueued_obj) {
7730                        delete enqueued_obj[prop];
7731                    }
7732                });
7733                _.each(append_q, function(append_obj) {
7734                    if (prop in append_obj) {
7735                        delete append_obj[prop];
7736                    }
7737                });
7738
7739                unset_q[prop] = true;
7740
7741            });
7742        } else if (q_key === ADD_QUEUE_KEY) {
7743            _.each(q_data, function(v, k) {
7744                // If it exists in the set queue, increment
7745                // the value
7746                if (k in set_q) {
7747                    set_q[k] += v;
7748                } else {
7749                    // If it doesn't exist, update the add
7750                    // queue
7751                    if (!(k in add_q)) {
7752                        add_q[k] = 0;
7753                    }
7754                    add_q[k] += v;
7755                }
7756            }, this);
7757            this._pop_from_people_queue(UNSET_ACTION, q_data);
7758        } else if (q_key === UNION_QUEUE_KEY) {
7759            _.each(q_data, function(v, k) {
7760                if (_.isArray(v)) {
7761                    if (!(k in union_q)) {
7762                        union_q[k] = [];
7763                    }
7764                    // Prevent duplicate values
7765                    _.each(v, function(item) {
7766                        if (!_.include(union_q[k], item)) {
7767                            union_q[k].push(item);
7768                        }
7769                    });
7770                }
7771            });
7772            this._pop_from_people_queue(UNSET_ACTION, q_data);
7773        } else if (q_key === REMOVE_QUEUE_KEY) {
7774            remove_q.push(q_data);
7775            this._pop_from_people_queue(APPEND_ACTION, q_data);
7776        } else if (q_key === APPEND_QUEUE_KEY) {
7777            append_q.push(q_data);
7778            this._pop_from_people_queue(UNSET_ACTION, q_data);
7779        }
7780
7781        console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
7782        console.log(data);
7783
7784        this.save();
7785    };
7786
7787    MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) {
7788        var q = this['props'][this._get_queue_key(queue)];
7789        if (!_.isUndefined(q)) {
7790            _.each(data, function(v, k) {
7791                if (queue === APPEND_ACTION || queue === REMOVE_ACTION) {
7792                    // list actions: only remove if both k+v match
7793                    // e.g. remove should not override append in a case like
7794                    // append({foo: 'bar'}); remove({foo: 'qux'})
7795                    _.each(q, function(queued_action) {
7796                        if (queued_action[k] === v) {
7797                            delete queued_action[k];
7798                        }
7799                    });
7800                } else {
7801                    delete q[k];
7802                }
7803            }, this);
7804        }
7805    };
7806
7807    MixpanelPersistence.prototype.load_queue = function(queue) {
7808        return this.load_prop(this._get_queue_key(queue));
7809    };
7810
7811    MixpanelPersistence.prototype._get_queue_key = function(queue) {
7812        if (queue === SET_ACTION) {
7813            return SET_QUEUE_KEY;
7814        } else if (queue === SET_ONCE_ACTION) {
7815            return SET_ONCE_QUEUE_KEY;
7816        } else if (queue === UNSET_ACTION) {
7817            return UNSET_QUEUE_KEY;
7818        } else if (queue === ADD_ACTION) {
7819            return ADD_QUEUE_KEY;
7820        } else if (queue === APPEND_ACTION) {
7821            return APPEND_QUEUE_KEY;
7822        } else if (queue === REMOVE_ACTION) {
7823            return REMOVE_QUEUE_KEY;
7824        } else if (queue === UNION_ACTION) {
7825            return UNION_QUEUE_KEY;
7826        } else {
7827            console.error('Invalid queue:', queue);
7828        }
7829    };
7830
7831    MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) {
7832        var key = this._get_queue_key(queue);
7833        default_val = _.isUndefined(default_val) ? {} : default_val;
7834        return this['props'][key] || (this['props'][key] = default_val);
7835    };
7836
7837    MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) {
7838        var timers = this.load_prop(EVENT_TIMERS_KEY) || {};
7839        timers[event_name] = timestamp;
7840        this['props'][EVENT_TIMERS_KEY] = timers;
7841        this.save();
7842    };
7843
7844    MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
7845        var timers = this.load_prop(EVENT_TIMERS_KEY) || {};
7846        var timestamp = timers[event_name];
7847        if (!_.isUndefined(timestamp)) {
7848            delete this['props'][EVENT_TIMERS_KEY][event_name];
7849            this.save();
7850        }
7851        return timestamp;
7852    };
7853
7854    /* eslint camelcase: "off" */
7855
7856    /*
7857     * Mixpanel JS Library
7858     *
7859     * Copyright 2012, Mixpanel, Inc. All Rights Reserved
7860     * http://mixpanel.com/
7861     *
7862     * Includes portions of Underscore.js
7863     * http://documentcloud.github.com/underscore/
7864     * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
7865     * Released under the MIT License.
7866     */
7867
7868    /*
7869    SIMPLE STYLE GUIDE:
7870
7871    this.x === public function
7872    this._x === internal - only use within this file
7873    this.__x === private - only use within the class
7874
7875    Globals should be all caps
7876    */
7877
7878    var init_type;       // MODULE or SNIPPET loader
7879    // allow bundlers to specify how extra code (recorder bundle) should be loaded
7880    // eslint-disable-next-line no-unused-vars
7881    var load_extra_bundle = function(src, _onload) {
7882        throw new Error(src + ' not available in this build.');
7883    };
7884
7885    var mixpanel_master; // main mixpanel instance / object
7886    var INIT_MODULE  = 0;
7887    var INIT_SNIPPET = 1;
7888
7889    /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
7890    /** @const */ var PAYLOAD_TYPE_BASE64   = 'base64';
7891    /** @const */ var PAYLOAD_TYPE_JSON     = 'json';
7892    /** @const */ var DEVICE_ID_PREFIX      = '$device:';
7893    /** @const */ var SETTING_STRICT        = 'strict';
7894    /** @const */ var SETTING_FALLBACK      = 'fallback';
7895    /** @const */ var SETTING_DISABLED      = 'disabled';
7896
7897    /*
7898     * Dynamic... constants? Is that an oxymoron?
7899     */
7900    // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
7901    // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials
7902    var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest());
7903
7904    // IE<10 does not support cross-origin XHR's but script tags
7905    // with defer won't block window.onload; ENQUEUE_REQUESTS
7906    // should only be true for Opera<12
7907    var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1);
7908
7909    // save reference to navigator.sendBeacon so it can be minified
7910    var sendBeacon = null;
7911    if (navigator['sendBeacon']) {
7912        sendBeacon = function() {
7913            // late reference to navigator.sendBeacon to allow patching/spying
7914            return navigator['sendBeacon'].apply(navigator, arguments);
7915        };
7916    }
7917
7918    var DEFAULT_API_ROUTES = {
7919        'track':  'track/',
7920        'engage': 'engage/',
7921        'groups': 'groups/',
7922        'record': 'record/',
7923        'flags':  'flags/',
7924        'settings': 'settings/'
7925    };
7926
7927    /*
7928     * Module-level globals
7929     */
7930    var DEFAULT_CONFIG = {
7931        'api_host':                          'https://api-js.mixpanel.com',
7932        'api_hosts':                         {},
7933        'api_routes':                        DEFAULT_API_ROUTES,
7934        'api_extra_query_params':            {},
7935        'api_method':                        'POST',
7936        'api_transport':                     'XHR',
7937        'api_payload_format':                PAYLOAD_TYPE_BASE64,
7938        'app_host':                          'https://mixpanel.com',
7939        'autocapture':                       false,
7940        'cdn':                               'https://cdn.mxpnl.com',
7941        'cross_site_cookie':                 false,
7942        'cross_subdomain_cookie':            true,
7943        'error_reporter':                    NOOP_FUNC,
7944        'flags':                             false,
7945        'persistence':                       'cookie',
7946        'persistence_name':                  '',
7947        'cookie_domain':                     '',
7948        'cookie_name':                       '',
7949        'loaded':                            NOOP_FUNC,
7950        'mp_loader':                         null,
7951        'track_marketing':                   true,
7952        'track_pageview':                    false,
7953        'skip_first_touch_marketing':        false,
7954        'store_google':                      true,
7955        'stop_utm_persistence':              false,
7956        'save_referrer':                     true,
7957        'test':                              false,
7958        'verbose':                           false,
7959        'img':                               false,
7960        'debug':                             false,
7961        'track_links_timeout':               300,
7962        'cookie_expiration':                 365,
7963        'upgrade':                           false,
7964        'disable_persistence':               false,
7965        'disable_cookie':                    false,
7966        'secure_cookie':                     false,
7967        'ip':                                true,
7968        'opt_out_tracking_by_default':       false,
7969        'opt_out_persistence_by_default':    false,
7970        'opt_out_tracking_persistence_type': 'localStorage',
7971        'opt_out_tracking_cookie_prefix':    null,
7972        'property_blacklist':                [],
7973        'xhr_headers':                       {}, // { header: value, header2: value }
7974        'ignore_dnt':                        false,
7975        'batch_requests':                    true,
7976        'batch_size':                        50,
7977        'batch_flush_interval_ms':           5000,
7978        'batch_request_timeout_ms':          90000,
7979        'batch_autostart':                   true,
7980        'hooks':                             {},
7981        'record_allowed_iframe_origins':     [],
7982        'record_block_class':                new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
7983        'record_block_selector':             'img, video, audio',
7984        'record_canvas':                     false,
7985        'record_collect_fonts':              false,
7986        'record_console':                    true,
7987        'record_heatmap_data':               false,
7988        'recording_event_triggers':          {},
7989        'record_idle_timeout_ms':            30 * 60 * 1000, // 30 minutes
7990        'record_mask_inputs':                true,
7991        'record_max_ms':                     MAX_RECORDING_MS,
7992        'record_min_ms':                     0,
7993        'record_network':                    false,
7994        'record_network_options':            {},
7995        'record_sessions_percent':           0,
7996        'recorder_src':                      null,
7997        'targeting_src':                     null,
7998        'lib_base_path':                     'https://cdn.mxpnl.com/libs/',
7999        'remote_settings_mode':              SETTING_DISABLED // 'strict', 'fallback', 'disabled'
8000    };
8001
8002    var DOM_LOADED = false;
8003
8004    /**
8005     * Mixpanel Library Object
8006     * @constructor
8007     */
8008    var MixpanelLib = function() {};
8009
8010
8011    /**
8012     * create_mplib(token:string, config:object, name:string)
8013     *
8014     * This function is used by the init method of MixpanelLib objects
8015     * as well as the main initializer at the end of the JSLib (that
8016     * initializes document.mixpanel as well as any additional instances
8017     * declared before this file has loaded).
8018     */
8019    var create_mplib = function(token, config, name) {
8020        var instance,
8021            target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name];
8022
8023        if (target && init_type === INIT_MODULE) {
8024            instance = target;
8025        } else {
8026            if (target && !_.isArray(target)) {
8027                console.error('You have already initialized ' + name);
8028                return;
8029            }
8030            instance = new MixpanelLib();
8031        }
8032
8033        instance._cached_groups = {}; // cache groups in a pool
8034
8035        instance._init(token, config, name);
8036
8037        instance['people'] = new MixpanelPeople();
8038        instance['people']._init(instance);
8039
8040        if (!instance.get_config('skip_first_touch_marketing')) {
8041            // We need null UTM params in the object because
8042            // UTM parameters act as a tuple. If any UTM param
8043            // is present, then we set all UTM params including
8044            // empty ones together
8045            var utm_params = _.info.campaignParams(null);
8046            var initial_utm_params = {};
8047            var has_utm = false;
8048            _.each(utm_params, function(utm_value, utm_key) {
8049                initial_utm_params['initial_' + utm_key] = utm_value;
8050                if (utm_value) {
8051                    has_utm = true;
8052                }
8053            });
8054            if (has_utm) {
8055                instance['people'].set_once(initial_utm_params);
8056            }
8057        }
8058
8059        // if any instance on the page has debug = true, we set the
8060        // global debug to be true
8061        Config.DEBUG = Config.DEBUG || instance.get_config('debug');
8062
8063        var source = init_type === INIT_MODULE ? 'module' : 'snippet';
8064        win.dispatchEvent(new win.CustomEvent('$mp_sdk_to_extension_event', {
8065            'detail': {
8066                'instance': instance,
8067                'source': source,
8068                'token': token,
8069                'name': name,
8070                'info': _.info
8071            }
8072        }));
8073
8074        // if target is not defined, we called init after the lib already
8075        // loaded, so there won't be an array of things to execute
8076        if (!_.isUndefined(target) && _.isArray(target)) {
8077            // Crunch through the people queue first - we queue this data up &
8078            // flush on identify, so it's better to do all these operations first
8079            instance._execute_array.call(instance['people'], target['people']);
8080            instance._execute_array(target);
8081        }
8082
8083        return instance;
8084    };
8085
8086    // Initialization methods
8087
8088    /**
8089     * This function initializes a new instance of the Mixpanel tracking object.
8090     * All new instances are added to the main mixpanel object as sub properties (such as
8091     * mixpanel.library_name) and also returned by this function. To define a
8092     * second instance on the page, you would call:
8093     *
8094     *     mixpanel.init('new token', { your: 'config' }, 'library_name');
8095     *
8096     * and use it like so:
8097     *
8098     *     mixpanel.library_name.track(...);
8099     *
8100     * @param {String} token   Your Mixpanel API token
8101     * @param {Object} [config]  A dictionary of config options to override. <a href="https://github.com/mixpanel/mixpanel-js/blob/v2.46.0/src/mixpanel-core.js#L88-L127">See a list of default config options</a>.
8102     * @param {String} [name]    The name for the new mixpanel instance that you want created
8103     */
8104    MixpanelLib.prototype.init = function (token, config, name) {
8105        if (_.isUndefined(name)) {
8106            this.report_error('You must name your new library: init(token, config, name)');
8107            return;
8108        }
8109        if (name === PRIMARY_INSTANCE_NAME) {
8110            this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
8111            return;
8112        }
8113
8114        var instance = create_mplib(token, config, name);
8115        mixpanel_master[name] = instance;
8116        instance._loaded();
8117
8118        return instance;
8119    };
8120
8121    // mixpanel._init(token:string, config:object, name:string)
8122    //
8123    // This function sets up the current instance of the mixpanel
8124    // library.  The difference between this method and the init(...)
8125    // method is this one initializes the actual instance, whereas the
8126    // init(...) method sets up a new library and calls _init on it.
8127    //
8128    MixpanelLib.prototype._init = function(token, config, name) {
8129        config = config || {};
8130
8131        this['__loaded'] = true;
8132        this['config'] = {};
8133
8134        var variable_features = {};
8135
8136        // default to JSON payload for standard mixpanel.com API hosts
8137        if (!('api_payload_format' in config)) {
8138            var api_host = config['api_host'] || DEFAULT_CONFIG['api_host'];
8139            if (api_host.match(/\.mixpanel\.com/)) {
8140                variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON;
8141            }
8142        }
8143
8144        this.hooks = {};
8145
8146        this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
8147            'name': name,
8148            'token': token,
8149            'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
8150        }));
8151
8152        this.recorderManager = new RecorderManager({
8153            mixpanelInstance: this,
8154            getConfigFunc: _.bind(this.get_config, this),
8155            setConfigFunc: _.bind(this.set_config, this),
8156            getTabIdFunc: _.bind(this.get_tab_id, this),
8157            reportErrorFunc: _.bind(this.report_error, this),
8158            getDistinctIdFunc: _.bind(this.get_distinct_id, this),
8159            recorderSrc: this.get_config('recorder_src'),
8160            targetingSrc: this.get_config('targeting_src'),
8161            libBasePath: this.get_config('lib_base_path'),
8162            loadExtraBundle: load_extra_bundle
8163        });
8164
8165        this['_jsc'] = NOOP_FUNC;
8166
8167        this.__dom_loaded_queue = [];
8168        this.__request_queue = [];
8169        this.__disabled_events = [];
8170        this._flags = {
8171            'disable_all_events': false,
8172            'identify_called': false
8173        };
8174        this._remote_settings_strict_disabled = false;
8175
8176        // set up request queueing/batching
8177        this.request_batchers = {};
8178        this._batch_requests = this.get_config('batch_requests');
8179        if (this._batch_requests) {
8180            if (!_.localStorage.is_supported(true) || !USE_XHR) {
8181                this._batch_requests = false;
8182                console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
8183                _.each(this.get_batcher_configs(), function(batcher_config) {
8184                    console.log('Clearing batch queue ' + batcher_config.queue_key);
8185                    _.localStorage.remove(batcher_config.queue_key);
8186                });
8187            } else {
8188                this.init_batchers();
8189                if (sendBeacon && win.addEventListener) {
8190                    // Before page closes or hides (user tabs away etc), attempt to flush any events
8191                    // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
8192                    // events will not be removed from the persistent store; if the site is loaded again,
8193                    // the events will be flushed again on startup and deduplicated on the Mixpanel server
8194                    // side.
8195                    // There is no reliable way to capture only page close events, so we lean on the
8196                    // visibilitychange and pagehide events as recommended at
8197                    // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
8198                    // These events fire when the user clicks away from the current page/tab, so will occur
8199                    // more frequently than page unload, but are the only mechanism currently for capturing
8200                    // this scenario somewhat reliably.
8201                    var flush_on_unload = _.bind(function() {
8202                        if (!this.request_batchers.events.stopped) {
8203                            this.request_batchers.events.flush({unloading: true});
8204                        }
8205                    }, this);
8206                    win.addEventListener('pagehide', function(ev) {
8207                        if (ev['persisted']) {
8208                            flush_on_unload();
8209                        }
8210                    });
8211                    win.addEventListener('visibilitychange', function() {
8212                        if (document$1['visibilityState'] === 'hidden') {
8213                            flush_on_unload();
8214                        }
8215                    });
8216                }
8217            }
8218        }
8219
8220        this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']);
8221        this.unpersisted_superprops = {};
8222        this._gdpr_init();
8223
8224        var uuid = _.UUID();
8225        if (!this.get_distinct_id()) {
8226            // There is no need to set the distinct id
8227            // or the device id if something was already stored
8228            // in the persitence
8229            this.register_once({
8230                'distinct_id': DEVICE_ID_PREFIX + uuid,
8231                '$device_id': uuid
8232            }, '');
8233        }
8234
8235        this.flags = new FeatureFlagManager({
8236            getFullApiRoute: _.bind(function() {
8237                return this.get_api_host('flags') + '/' + this.get_config('api_routes')['flags'];
8238            }, this),
8239            getConfigFunc: _.bind(this.get_config, this),
8240            setConfigFunc: _.bind(this.set_config, this),
8241            getPropertyFunc: _.bind(this.get_property, this),
8242            trackingFunc: _.bind(this.track, this),
8243            loadExtraBundle: load_extra_bundle,
8244            targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
8245        });
8246        this.flags.init();
8247        this['flags'] = this.flags;
8248
8249        this._init_tab_id();
8250
8251        // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
8252        var mode = this.get_config('remote_settings_mode');
8253        if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
8254            this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
8255                return this._check_and_start_session_recording();
8256            }, this));
8257        } else {
8258            this.__session_recording_init_promise = this._check_and_start_session_recording();
8259        }
8260
8261        this.autocapture = new Autocapture(this);
8262        this.autocapture.init();
8263    };
8264
8265    /**
8266     * Assigns a unique UUID to this tab / window by leveraging sessionStorage.
8267     * This is primarily used for session recording, where data must be isolated to the current tab.
8268     */
8269    MixpanelLib.prototype._init_tab_id = function() {
8270        if (this.get_config('disable_persistence')) {
8271            console.log('Tab ID initialization skipped due to disable_persistence config');
8272        } else if (_.sessionStorage.is_supported()) {
8273            try {
8274                var key_suffix = this.get_config('name') + '_' + this.get_config('token');
8275                var tab_id_key = 'mp_tab_id_' + key_suffix;
8276
8277                // A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
8278                // This enforces a unique ID in the cases like duplicated tab, window.open(...)
8279                var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
8280                if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
8281                    _.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
8282                }
8283
8284                _.sessionStorage.set(should_generate_new_tab_id_key, '1');
8285                this.tab_id = _.sessionStorage.get(tab_id_key);
8286
8287                // Remove the flag when the tab is unloaded to indicate the stored tab ID can be reused. This event is not reliable to detect all page unloads,
8288                // but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
8289                // If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
8290                win.addEventListener('beforeunload', function () {
8291                    _.sessionStorage.remove(should_generate_new_tab_id_key);
8292                });
8293            } catch(err) {
8294                this.report_error('Error initializing tab id', err);
8295            }
8296        } else {
8297            this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
8298        }
8299    };
8300
8301    MixpanelLib.prototype.get_tab_id = function () {
8302        return this.tab_id || null;
8303    };
8304
8305    MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
8306        return this.recorderManager.checkAndStartSessionRecording(force_start);
8307    });
8308
8309    MixpanelLib.prototype._start_recording_on_event = safewrap(function(event_name, properties) {
8310        // Wait for recording init to complete before evaluating event triggers.
8311        // This ensures recording_event_triggers config is fully loaded when remote settings are used.
8312        if (this.__session_recording_init_promise) {
8313            this.__session_recording_init_promise.then(_.bind(function() {
8314                // In strict mode, skip recording if remote settings failed
8315                if (this._remote_settings_strict_disabled) {
8316                    return;
8317                }
8318                return this.recorderManager.startRecordingOnEvent(event_name, properties);
8319            }, this));
8320        }
8321    });
8322
8323    MixpanelLib.prototype.start_session_recording = function () {
8324        return this._check_and_start_session_recording(true);
8325    };
8326
8327    MixpanelLib.prototype.stop_session_recording = function () {
8328        return this.recorderManager.stopSessionRecording();
8329    };
8330
8331    MixpanelLib.prototype.pause_session_recording = function () {
8332        return this.recorderManager.pauseSessionRecording();
8333    };
8334
8335    MixpanelLib.prototype.resume_session_recording = function () {
8336        return this.recorderManager.resumeSessionRecording();
8337    };
8338
8339    MixpanelLib.prototype.is_recording_heatmap_data = function () {
8340        return this.recorderManager.isRecordingHeatmapData();
8341    };
8342
8343    MixpanelLib.prototype.get_session_recording_properties = function () {
8344        return this.recorderManager.getSessionRecordingProperties();
8345    };
8346
8347    MixpanelLib.prototype.get_session_replay_url = function () {
8348        return this.recorderManager.getSessionReplayUrl();
8349    };
8350
8351    // "private" public method to reach into the recorder in test cases
8352    MixpanelLib.prototype.__get_recorder = function () {
8353        return this.recorderManager.getRecorder();
8354    };
8355
8356    // "private" public method to get session recording init promise in test cases
8357    MixpanelLib.prototype.__get_recording_init_promise = function () {
8358        return this.__session_recording_init_promise;
8359    };
8360
8361    // Private methods
8362
8363    MixpanelLib.prototype._loaded = function() {
8364        this.get_config('loaded')(this);
8365        this._set_default_superprops();
8366        this['people'].set_once(this['persistence'].get_referrer_info());
8367
8368        // `store_google` is now deprecated and previously stored UTM parameters are cleared
8369        // from persistence by default.
8370        if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) {
8371            var utm_params = _.info.campaignParams(null);
8372            _.each(utm_params, function(_utm_value, utm_key) {
8373                // We need to unregister persisted UTM parameters so old values
8374                // are not mixed with the new UTM parameters
8375                this.unregister(utm_key);
8376            }.bind(this));
8377        }
8378    };
8379
8380    // update persistence with info on referrer, UTM params, etc
8381    MixpanelLib.prototype._set_default_superprops = function() {
8382        this['persistence'].update_search_keyword(document$1.referrer);
8383        // Registering super properties for UTM persistence by 'store_google' is deprecated.
8384        if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) {
8385            this.register(_.info.campaignParams());
8386        }
8387        if (this.get_config('save_referrer')) {
8388            this['persistence'].update_referrer_info(document$1.referrer);
8389        }
8390    };
8391
8392    MixpanelLib.prototype._dom_loaded = function() {
8393        _.each(this.__dom_loaded_queue, function(item) {
8394            this._track_dom.apply(this, item);
8395        }, this);
8396
8397        if (!this.has_opted_out_tracking()) {
8398            _.each(this.__request_queue, function(item) {
8399                this._send_request.apply(this, item);
8400            }, this);
8401        }
8402
8403        delete this.__dom_loaded_queue;
8404        delete this.__request_queue;
8405    };
8406
8407    MixpanelLib.prototype._track_dom = function(DomClass, args) {
8408        if (this.get_config('img')) {
8409            this.report_error('You can\'t use DOM tracking functions with img = true.');
8410            return false;
8411        }
8412
8413        if (!DOM_LOADED) {
8414            this.__dom_loaded_queue.push([DomClass, args]);
8415            return false;
8416        }
8417
8418        var dt = new DomClass().init(this);
8419        return dt.track.apply(dt, args);
8420    };
8421
8422    /**
8423     * _prepare_callback() should be called by callers of _send_request for use
8424     * as the callback argument.
8425     *
8426     * If there is no callback, this returns null.
8427     * If we are going to make XHR/XDR requests, this returns a function.
8428     * If we are going to use script tags, this returns a string to use as the
8429     * callback GET param.
8430     */
8431    MixpanelLib.prototype._prepare_callback = function(callback, data) {
8432        if (_.isUndefined(callback)) {
8433            return null;
8434        }
8435
8436        if (USE_XHR) {
8437            var callback_function = function(response) {
8438                callback(response, data);
8439            };
8440            return callback_function;
8441        } else {
8442            // if the user gives us a callback, we store as a random
8443            // property on this instances jsc function and update our
8444            // callback string to reflect that.
8445            var jsc = this['_jsc'];
8446            var randomized_cb = '' + Math.floor(Math.random() * 100000000);
8447            var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']';
8448            jsc[randomized_cb] = function(response) {
8449                delete jsc[randomized_cb];
8450                callback(response, data);
8451            };
8452            return callback_string;
8453        }
8454    };
8455
8456    MixpanelLib.prototype._send_request = function(url, data, options, callback) {
8457        var succeeded = true;
8458
8459        if (ENQUEUE_REQUESTS) {
8460            this.__request_queue.push(arguments);
8461            return succeeded;
8462        }
8463
8464        var DEFAULT_OPTIONS = {
8465            method: this.get_config('api_method'),
8466            transport: this.get_config('api_transport'),
8467            verbose: this.get_config('verbose')
8468        };
8469        var body_data = null;
8470
8471        if (!callback && (_.isFunction(options) || typeof options === 'string')) {
8472            callback = options;
8473            options = null;
8474        }
8475        options = _.extend(DEFAULT_OPTIONS, options || {});
8476        if (!USE_XHR) {
8477            options.method = 'GET';
8478        }
8479        var use_post = options.method === 'POST';
8480        var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon';
8481
8482        // needed to correctly format responses
8483        var verbose_mode = options.verbose;
8484        if (data['verbose']) { verbose_mode = true; }
8485
8486        if (this.get_config('test')) { data['test'] = 1; }
8487        if (verbose_mode) { data['verbose'] = 1; }
8488        if (this.get_config('img')) { data['img'] = 1; }
8489        if (!USE_XHR) {
8490            if (callback) {
8491                data['callback'] = callback;
8492            } else if (verbose_mode || this.get_config('test')) {
8493                // Verbose output (from verbose mode, or an error in test mode) is a json blob,
8494                // which by itself is not valid javascript. Without a callback, this verbose output will
8495                // cause an error when returned via jsonp, so we force a no-op callback param.
8496                // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4
8497                data['callback'] = '(function(){})';
8498            }
8499        }
8500
8501        data['ip'] = this.get_config('ip')?1:0;
8502        data['_'] = new Date().getTime().toString();
8503
8504        if (use_post) {
8505            body_data = 'data=' + encodeURIComponent(data['data']);
8506            delete data['data'];
8507        }
8508
8509        _.extend(data, this.get_config('api_extra_query_params'));
8510
8511        url += '?' + _.HTTPBuildQuery(data);
8512
8513        var lib = this;
8514        if ('img' in data) {
8515            var img = document$1.createElement('img');
8516            img.src = url;
8517            document$1.body.appendChild(img);
8518        } else if (use_sendBeacon) {
8519            try {
8520                succeeded = sendBeacon(url, body_data);
8521            } catch (e) {
8522                lib.report_error(e);
8523                succeeded = false;
8524            }
8525            try {
8526                if (callback) {
8527                    callback(succeeded ? 1 : 0);
8528                }
8529            } catch (e) {
8530                lib.report_error(e);
8531            }
8532        } else if (USE_XHR) {
8533            try {
8534                var req = new XMLHttpRequest();
8535                req.open(options.method, url, true);
8536
8537                var headers = this.get_config('xhr_headers');
8538                if (use_post) {
8539                    headers['Content-Type'] = 'application/x-www-form-urlencoded';
8540                }
8541                _.each(headers, function(headerValue, headerName) {
8542                    req.setRequestHeader(headerName, headerValue);
8543                });
8544
8545                if (options.timeout_ms && typeof req.timeout !== 'undefined') {
8546                    req.timeout = options.timeout_ms;
8547                    var start_time = new Date().getTime();
8548                }
8549
8550                // send the mp_optout cookie
8551                // withCredentials cannot be modified until after calling .open on Android and Mobile Safari
8552                req.withCredentials = true;
8553                req.onreadystatechange = function () {
8554                    if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4
8555                        if (req.status === 200) {
8556                            if (callback) {
8557                                if (verbose_mode) {
8558                                    var response;
8559                                    try {
8560                                        response = _.JSONDecode(req.responseText);
8561                                    } catch (e) {
8562                                        lib.report_error(e);
8563                                        if (options.ignore_json_errors) {
8564                                            response = req.responseText;
8565                                        } else {
8566                                            return;
8567                                        }
8568                                    }
8569                                    callback(response);
8570                                } else {
8571                                    callback(Number(req.responseText));
8572                                }
8573                            }
8574                        } else {
8575                            var error;
8576                            if (
8577                                req.timeout &&
8578                                !req.status &&
8579                                new Date().getTime() - start_time >= req.timeout
8580                            ) {
8581                                error = 'timeout';
8582                            } else {
8583                                error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
8584                            }
8585                            lib.report_error(error);
8586                            if (callback) {
8587                                if (verbose_mode) {
8588                                    var response_headers = req['responseHeaders'] || {};
8589                                    callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']});
8590                                } else {
8591                                    callback(0);
8592                                }
8593                            }
8594                        }
8595                    }
8596                };
8597                req.send(body_data);
8598            } catch (e) {
8599                lib.report_error(e);
8600                succeeded = false;
8601            }
8602        } else {
8603            var script = document$1.createElement('script');
8604            script.type = 'text/javascript';
8605            script.async = true;
8606            script.defer = true;
8607            script.src = url;
8608            var s = document$1.getElementsByTagName('script')[0];
8609            s.parentNode.insertBefore(script, s);
8610        }
8611
8612        return succeeded;
8613    };
8614
8615    MixpanelLib.prototype._fetch_remote_settings = function(mode) {
8616        var self = this;
8617        var disableRecordingIfStrict = function() {
8618            if (mode === 'strict') {
8619                self.set_config({'record_sessions_percent': 0});
8620                self._remote_settings_strict_disabled = true;
8621            }
8622        };
8623
8624        if (!win['AbortController']) {
8625            console.critical('Remote settings unavailable: missing minimum required APIs');
8626            disableRecordingIfStrict();
8627            return Promise.resolve();
8628        }
8629
8630        var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
8631        var request_params = {
8632            '$lib_version': Config.LIB_VERSION,
8633            'mp_lib': 'web',
8634            'sdk_config': '1',
8635        };
8636        var query_string = _.HTTPBuildQuery(request_params);
8637        var full_url = settings_endpoint + '?' + query_string;
8638
8639        var abortController = new AbortController();
8640        var timeout_id = setTimeout(function() {
8641            abortController.abort();
8642        }, 500);
8643        var fetchOptions = {
8644            'method': 'GET',
8645            'headers': {
8646                'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
8647            },
8648            'signal': abortController.signal
8649        };
8650
8651        return win['fetch'](full_url, fetchOptions).then(function(response) {
8652            clearTimeout(timeout_id);
8653            if (!response['ok']) {
8654                console.critical('Network response was not ok');
8655                disableRecordingIfStrict();
8656                return;
8657            }
8658            return response.json();
8659        }).then(function(result) {
8660            if (result && result['sdk_config'] && result['sdk_config']['config']) {
8661                var remote_config = result['sdk_config']['config'];
8662
8663                // Verify that remote config contains only valid keys from DEFAULT_CONFIG
8664                var valid_config = {};
8665                _.each(remote_config, function(value, key) {
8666                    if (DEFAULT_CONFIG.hasOwnProperty(key)) {
8667                        valid_config[key] = value;
8668                    }
8669                });
8670
8671                if (_.isEmptyObject(valid_config)) {
8672                    console.critical('No valid config keys found in remote settings.');
8673                    disableRecordingIfStrict();
8674                } else {
8675                    self.set_config(valid_config);
8676                }
8677            } else {
8678                disableRecordingIfStrict();
8679            }
8680        }).catch(function(err) {
8681            clearTimeout(timeout_id);
8682            console.critical('Failed to fetch remote settings', err);
8683            disableRecordingIfStrict();
8684        });
8685    };
8686
8687    /**
8688     * _execute_array() deals with processing any mixpanel function
8689     * calls that were called before the Mixpanel library were loaded
8690     * (and are thus stored in an array so they can be called later)
8691     *
8692     * Note: we fire off all the mixpanel function calls && user defined
8693     * functions BEFORE we fire off mixpanel tracking calls. This is so
8694     * identify/register/set_config calls can properly modify early
8695     * tracking calls.
8696     *
8697     * @param {Array} array
8698     */
8699    MixpanelLib.prototype._execute_array = function(array) {
8700        var fn_name, alias_calls = [], other_calls = [], tracking_calls = [];
8701        _.each(array, function(item) {
8702            if (item) {
8703                fn_name = item[0];
8704                if (_.isArray(fn_name)) {
8705                    tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set()
8706                } else if (typeof(item) === 'function') {
8707                    item.call(this);
8708                } else if (_.isArray(item) && fn_name === 'alias') {
8709                    alias_calls.push(item);
8710                } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') {
8711                    tracking_calls.push(item);
8712                } else {
8713                    other_calls.push(item);
8714                }
8715            }
8716        }, this);
8717
8718        var execute = function(calls, context) {
8719            _.each(calls, function(item) {
8720                if (_.isArray(item[0])) {
8721                    // chained call
8722                    var caller = context;
8723                    _.each(item, function(call) {
8724                        caller = caller[call[0]].apply(caller, call.slice(1));
8725                    });
8726                } else {
8727                    this[item[0]].apply(this, item.slice(1));
8728                }
8729            }, context);
8730        };
8731
8732        execute(alias_calls, this);
8733        execute(other_calls, this);
8734        execute(tracking_calls, this);
8735    };
8736
8737    // request queueing utils
8738
8739    MixpanelLib.prototype.are_batchers_initialized = function() {
8740        return !!this.request_batchers.events;
8741    };
8742
8743    MixpanelLib.prototype.get_batcher_configs = function() {
8744        var queue_prefix = '__mpq_' + this.get_config('token');
8745        this._batcher_configs = this._batcher_configs || {
8746            events: {type: 'events', api_name: 'track', queue_key: queue_prefix + '_ev'},
8747            people: {type: 'people', api_name: 'engage', queue_key: queue_prefix + '_pp'},
8748            groups: {type: 'groups', api_name: 'groups', queue_key: queue_prefix + '_gr'}
8749        };
8750        return this._batcher_configs;
8751    };
8752
8753    MixpanelLib.prototype.init_batchers = function() {
8754        if (!this.are_batchers_initialized()) {
8755            var batcher_for = _.bind(function(attrs) {
8756                return new RequestBatcher(
8757                    attrs.queue_key,
8758                    {
8759                        libConfig: this['config'],
8760                        errorReporter: this.get_config('error_reporter'),
8761                        sendRequestFunc: _.bind(function(data, options, cb) {
8762                            var api_routes = this.get_config('api_routes');
8763                            this._send_request(
8764                                this.get_api_host(attrs.api_name) + '/' + api_routes[attrs.api_name],
8765                                this._encode_data_for_request(data),
8766                                options,
8767                                this._prepare_callback(cb, data)
8768                            );
8769                        }, this),
8770                        beforeSendHook: _.bind(function(item) {
8771                            var ret = this._run_hook('before_send_' + attrs.type, item);
8772                            if (ret) {
8773                                return ret[0];
8774                            } else {
8775                                return null;
8776                            }
8777                        }, this),
8778                        stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
8779                        usePersistence: true,
8780                    }
8781                );
8782            }, this);
8783            var batcher_configs = this.get_batcher_configs();
8784            this.request_batchers = {
8785                events: batcher_for(batcher_configs.events),
8786                people: batcher_for(batcher_configs.people),
8787                groups: batcher_for(batcher_configs.groups)
8788            };
8789        }
8790        if (this.get_config('batch_autostart')) {
8791            this.start_batch_senders();
8792        }
8793    };
8794
8795    MixpanelLib.prototype.start_batch_senders = function() {
8796        this._batchers_were_started = true;
8797        if (this.are_batchers_initialized()) {
8798            this._batch_requests = true;
8799            _.each(this.request_batchers, function(batcher) {
8800                batcher.start();
8801            });
8802        }
8803    };
8804
8805    MixpanelLib.prototype.stop_batch_senders = function() {
8806        this._batch_requests = false;
8807        _.each(this.request_batchers, function(batcher) {
8808            batcher.stop();
8809            batcher.clear();
8810        });
8811    };
8812
8813    /**
8814     * push() keeps the standard async-array-push
8815     * behavior around after the lib is loaded.
8816     * This is only useful for external integrations that
8817     * do not wish to rely on our convenience methods
8818     * (created in the snippet).
8819     *
8820     * ### Usage:
8821     *     mixpanel.push(['register', { a: 'b' }]);
8822     *
8823     * @param {Array} item A [function_name, args...] array to be executed
8824     */
8825    MixpanelLib.prototype.push = function(item) {
8826        this._execute_array([item]);
8827    };
8828
8829    /**
8830     * Enables events on the Mixpanel object. If passed no arguments,
8831     * this function enable tracking of all events. If passed an
8832     * array of event names, those events will be enabled, but other
8833     * existing disabled events will continue to be not tracked.
8834     *
8835     * @param {Array} [events] An array of event names to enable
8836     */
8837    MixpanelLib.prototype.enable = function(events) {
8838        var keys, new_disabled_events, i, j;
8839
8840        if (typeof(events) === 'undefined') {
8841            this._flags.disable_all_events = false;
8842        } else {
8843            keys = {};
8844            new_disabled_events = [];
8845            for (i = 0; i < events.length; i++) {
8846                keys[events[i]] = true;
8847            }
8848            for (j = 0; j < this.__disabled_events.length; j++) {
8849                if (!keys[this.__disabled_events[j]]) {
8850                    new_disabled_events.push(this.__disabled_events[j]);
8851                }
8852            }
8853            this.__disabled_events = new_disabled_events;
8854        }
8855    };
8856
8857    /**
8858     * Disable events on the Mixpanel object. If passed no arguments,
8859     * this function disables tracking of any event. If passed an
8860     * array of event names, those events will be disabled, but other
8861     * events will continue to be tracked.
8862     *
8863     * Note: this function does not stop other mixpanel functions from
8864     * firing, such as register() or people.set().
8865     *
8866     * @param {Array} [events] An array of event names to disable
8867     */
8868    MixpanelLib.prototype.disable = function(events) {
8869        if (typeof(events) === 'undefined') {
8870            this._flags.disable_all_events = true;
8871        } else {
8872            this.__disabled_events = this.__disabled_events.concat(events);
8873        }
8874    };
8875
8876    MixpanelLib.prototype._encode_data_for_request = function(data) {
8877        var encoded_data = JSONStringify(data);
8878        if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) {
8879            encoded_data = _.base64Encode(encoded_data);
8880        }
8881        return {'data': encoded_data};
8882    };
8883
8884    // internal method for handling track vs batch-enqueue logic
8885    MixpanelLib.prototype._track_or_batch = function(options, callback) {
8886        var truncated_data = _.truncate(options.data, 255);
8887        var endpoint = options.endpoint;
8888        var batcher = options.batcher;
8889        var should_send_immediately = options.should_send_immediately;
8890        var send_request_options = options.send_request_options || {};
8891        callback = callback || NOOP_FUNC;
8892
8893        var request_enqueued_or_initiated = true;
8894        var send_request_immediately = _.bind(function() {
8895            if (!send_request_options.skip_hooks) {
8896                truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
8897                if (truncated_data) {
8898                    truncated_data = truncated_data[0];
8899                }
8900            }
8901            if (truncated_data) {
8902                console.log('MIXPANEL REQUEST:');
8903                console.log(truncated_data);
8904                return this._send_request(
8905                    endpoint,
8906                    this._encode_data_for_request(truncated_data),
8907                    send_request_options,
8908                    this._prepare_callback(callback, truncated_data)
8909                );
8910            } else {
8911                return null;
8912            }
8913        }, this);
8914
8915        if (this._batch_requests && !should_send_immediately) {
8916            batcher.enqueue(truncated_data).then(function(succeeded) {
8917                if (succeeded) {
8918                    callback(1, truncated_data);
8919                } else {
8920                    send_request_immediately();
8921                }
8922            });
8923        } else {
8924            request_enqueued_or_initiated = send_request_immediately();
8925        }
8926
8927        return request_enqueued_or_initiated && truncated_data;
8928    };
8929
8930    /**
8931     * Track an event. This is the most important and
8932     * frequently used Mixpanel function.
8933     *
8934     * ### Usage:
8935     *
8936     *     // track an event named 'Registered'
8937     *     mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21});
8938     *
8939     *     // track an event using navigator.sendBeacon
8940     *     mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'});
8941     *
8942     * To track link clicks or form submissions, see track_links() or track_forms().
8943     *
8944     * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
8945     * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself.
8946     * @param {Object} [options] Optional configuration for this track request.
8947     * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon').
8948     * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately.
8949     * @param {Function} [callback] If provided, the callback function will be called after tracking the event.
8950     * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object
8951     * with the tracking payload sent to the API server is returned; otherwise false.
8952     */
8953    MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) {
8954        var ret;
8955        if (!(options && options.skip_hooks)) {
8956            ret = this._run_hook('before_track', event_name, properties);
8957            if (ret === null) {
8958                return;
8959            } else {
8960                event_name = ret[0];
8961                properties = ret[1];
8962            }
8963        }
8964
8965        if (!callback && typeof options === 'function') {
8966            callback = options;
8967            options = null;
8968        }
8969        options = options || {};
8970        var transport = options['transport']; // external API, don't minify 'transport' prop
8971        if (transport) {
8972            options.transport = transport; // 'transport' prop name can be minified internally
8973        }
8974        var should_send_immediately = options['send_immediately'];
8975        if (typeof callback !== 'function') {
8976            callback = NOOP_FUNC;
8977        }
8978
8979        if (_.isUndefined(event_name)) {
8980            this.report_error('No event name provided to mixpanel.track');
8981            return;
8982        }
8983
8984        if (this._event_is_disabled(event_name)) {
8985            callback(0);
8986            return;
8987        }
8988
8989        // set defaults
8990        properties = _.extend({}, properties);
8991        properties['token'] = this.get_config('token');
8992
8993        // set $duration if time_event was previously called for this event
8994        var start_timestamp = this['persistence'].remove_event_timer(event_name);
8995        if (!_.isUndefined(start_timestamp)) {
8996            var duration_in_ms = new Date().getTime() - start_timestamp;
8997            properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3));
8998        }
8999
9000        this._set_default_superprops();
9001
9002        var marketing_properties = this.get_config('track_marketing')
9003            ? _.info.marketingParams()
9004            : {};
9005
9006        // note: extend writes to the first object, so lets make sure we
9007        // don't write to the persistence properties object and info
9008        // properties object by passing in a new object
9009
9010        // update properties with pageview info and super-properties
9011        properties = _.extend(
9012            {},
9013            _.info.properties({'mp_loader': this.get_config('mp_loader')}),
9014            marketing_properties,
9015            this['persistence'].properties(),
9016            this.unpersisted_superprops,
9017            this.get_session_recording_properties(),
9018            properties
9019        );
9020
9021        var property_blacklist = this.get_config('property_blacklist');
9022        if (_.isArray(property_blacklist)) {
9023            _.each(property_blacklist, function(blacklisted_prop) {
9024                delete properties[blacklisted_prop];
9025            });
9026        } else {
9027            this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
9028        }
9029
9030        this._start_recording_on_event(event_name, properties);
9031
9032        var data = {
9033            'event': event_name,
9034            'properties': properties
9035        };
9036        ret = this._track_or_batch({
9037            type: 'events',
9038            data: data,
9039            endpoint: this.get_api_host('events') + '/' + this.get_config('api_routes')['track'],
9040            batcher: this.request_batchers.events,
9041            should_send_immediately: should_send_immediately,
9042            send_request_options: options
9043        }, callback);
9044
9045        // Check for first-time event matches
9046        if (this.flags && this.flags.checkFirstTimeEvents) {
9047            this.flags.checkFirstTimeEvents(event_name, properties);
9048        }
9049
9050        return ret;
9051    });
9052
9053    /**
9054     * Register the current user into one/many groups.
9055     *
9056     * ### Usage:
9057     *
9058     *      mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs
9059     *      mixpanel.set_group('company', 'mixpanel')
9060     *      mixpanel.set_group('company', 128746312)
9061     *
9062     * @param {String} group_key Group key
9063     * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID
9064     * @param {Function} [callback] If provided, the callback will be called after tracking the event.
9065     *
9066     */
9067    MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) {
9068        if (!_.isArray(group_ids)) {
9069            group_ids = [group_ids];
9070        }
9071        var prop = {};
9072        prop[group_key] = group_ids;
9073        this.register(prop);
9074        return this['people'].set(group_key, group_ids, callback);
9075    });
9076
9077    /**
9078     * Add a new group for this user.
9079     *
9080     * ### Usage:
9081     *
9082     *      mixpanel.add_group('company', 'mixpanel')
9083     *
9084     * @param {String} group_key Group key
9085     * @param {*} group_id A valid Mixpanel property type
9086     * @param {Function} [callback] If provided, the callback will be called after tracking the event.
9087     */
9088    MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) {
9089        var old_values = this.get_property(group_key);
9090        var prop = {};
9091        if (old_values === undefined) {
9092            prop[group_key] = [group_id];
9093            this.register(prop);
9094        } else {
9095            if (old_values.indexOf(group_id) === -1) {
9096                old_values.push(group_id);
9097                prop[group_key] = old_values;
9098                this.register(prop);
9099            }
9100        }
9101        return this['people'].union(group_key, group_id, callback);
9102    });
9103
9104    /**
9105     * Remove a group from this user.
9106     *
9107     * ### Usage:
9108     *
9109     *      mixpanel.remove_group('company', 'mixpanel')
9110     *
9111     * @param {String} group_key Group key
9112     * @param {*} group_id A valid Mixpanel property type
9113     * @param {Function} [callback] If provided, the callback will be called after tracking the event.
9114     */
9115    MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) {
9116        var old_value = this.get_property(group_key);
9117        // if the value doesn't exist, the persistent store is unchanged
9118        if (old_value !== undefined) {
9119            var idx = old_value.indexOf(group_id);
9120            if (idx > -1) {
9121                old_value.splice(idx, 1);
9122                this.register({group_key: old_value});
9123            }
9124            if (old_value.length === 0) {
9125                this.unregister(group_key);
9126            }
9127        }
9128        return this['people'].remove(group_key, group_id, callback);
9129    });
9130
9131    /**
9132     * Track an event with specific groups.
9133     *
9134     * ### Usage:
9135     *
9136     *      mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']})
9137     *
9138     * @param {String} event_name The name of the event (see `mixpanel.track()`)
9139     * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`)
9140     * @param {Object=} groups An object mapping group name keys to one or more values
9141     * @param {Function} [callback] If provided, the callback will be called after tracking the event.
9142     */
9143    MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) {
9144        var tracking_props = _.extend({}, properties || {});
9145        _.each(groups, function(v, k) {
9146            if (v !== null && v !== undefined) {
9147                tracking_props[k] = v;
9148            }
9149        });
9150        return this.track(event_name, tracking_props, callback);
9151    });
9152
9153    MixpanelLib.prototype._create_map_key = function (group_key, group_id) {
9154        return group_key + '_' + JSON.stringify(group_id);
9155    };
9156
9157    MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) {
9158        delete this._cached_groups[this._create_map_key(group_key, group_id)];
9159    };
9160
9161    /**
9162     * Look up reference to a Mixpanel group
9163     *
9164     * ### Usage:
9165     *
9166     *       mixpanel.get_group(group_key, group_id)
9167     *
9168     * @param {String} group_key Group key
9169     * @param {Object} group_id A valid Mixpanel property type
9170     * @returns {Object} A MixpanelGroup identifier
9171     */
9172    MixpanelLib.prototype.get_group = function (group_key, group_id) {
9173        var map_key = this._create_map_key(group_key, group_id);
9174        var group = this._cached_groups[map_key];
9175        if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) {
9176            group = new MixpanelGroup();
9177            group._init(this, group_key, group_id);
9178            this._cached_groups[map_key] = group;
9179        }
9180        return group;
9181    };
9182
9183    /**
9184     * Track a default Mixpanel page view event, which includes extra default event properties to
9185     * improve page view data.
9186     *
9187     * ### Usage:
9188     *
9189     *     // track a default $mp_web_page_view event
9190     *     mixpanel.track_pageview();
9191     *
9192     *     // track a page view event with additional event properties
9193     *     mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'});
9194     *
9195     *     // example approach to track page views on different page types as event properties
9196     *     mixpanel.track_pageview({'page': 'pricing'});
9197     *     mixpanel.track_pageview({'page': 'homepage'});
9198     *
9199     *     // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for
9200     *     // individual pages on the same site or product. Use cases for custom event_name may be page
9201     *     // views on different products or internal applications that are considered completely separate
9202     *     mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'});
9203     *
9204     * ### Notes:
9205     *
9206     * The `config.track_pageview` option for <a href="#mixpanelinit">mixpanel.init()</a>
9207     * may be turned on for tracking page loads automatically.
9208     *
9209     *     // track only page loads
9210     *     mixpanel.init(PROJECT_TOKEN, {track_pageview: true});
9211     *
9212     *     // track when the URL changes in any manner
9213     *     mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'});
9214     *
9215     *     // track when the URL changes, ignoring any changes in the hash part
9216     *     mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'});
9217     *
9218     *     // track when the path changes, ignoring any query parameter or hash changes
9219     *     mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'});
9220     *
9221     * @param {Object} [properties] An optional set of additional properties to send with the page view event
9222     * @param {Object} [options] Page view tracking options
9223     * @param {String} [options.event_name] - Alternate name for the tracking event
9224     * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object
9225     * with the tracking payload sent to the API server is returned; otherwise false.
9226     */
9227    MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) {
9228        if (typeof properties !== 'object') {
9229            properties = {};
9230        }
9231        options = options || {};
9232        var event_name = options['event_name'] || '$mp_web_page_view';
9233
9234        var default_page_properties = _.extend(
9235            _.info.mpPageViewProperties(),
9236            _.info.campaignParams(),
9237            _.info.clickParams()
9238        );
9239
9240        var event_properties = _.extend(
9241            {},
9242            default_page_properties,
9243            properties
9244        );
9245
9246        if (this.is_recording_heatmap_data()) {
9247            event_properties['$captured_for_heatmap'] = true;
9248        }
9249
9250        return this.track(event_name, event_properties);
9251    });
9252
9253    /**
9254     * Track clicks on a set of document elements. Selector must be a
9255     * valid query. Elements must exist on the page at the time track_links is called.
9256     *
9257     * ### Usage:
9258     *
9259     *     // track click for link id #nav
9260     *     mixpanel.track_links('#nav', 'Clicked Nav Link');
9261     *
9262     * ### Notes:
9263     *
9264     * This function will wait up to 300 ms for the Mixpanel
9265     * servers to respond. If they have not responded by that time
9266     * it will head to the link without ensuring that your event
9267     * has been tracked.  To configure this timeout please see the
9268     * set_config() documentation below.
9269     *
9270     * If you pass a function in as the properties argument, the
9271     * function will receive the DOMElement that triggered the
9272     * event as an argument.  You are expected to return an object
9273     * from the function; any properties defined on this object
9274     * will be sent to mixpanel as event properties.
9275     *
9276     * @type {Function}
9277     * @param {Object|String} query A valid DOM query, element or jQuery-esque list
9278     * @param {String} event_name The name of the event to track
9279     * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement
9280     */
9281    MixpanelLib.prototype.track_links = function() {
9282        return this._track_dom.call(this, LinkTracker, arguments);
9283    };
9284
9285    /**
9286     * Track form submissions. Selector must be a valid query.
9287     *
9288     * ### Usage:
9289     *
9290     *     // track submission for form id 'register'
9291     *     mixpanel.track_forms('#register', 'Created Account');
9292     *
9293     * ### Notes:
9294     *
9295     * This function will wait up to 300 ms for the mixpanel
9296     * servers to respond, if they have not responded by that time
9297     * it will head to the link without ensuring that your event
9298     * has been tracked.  To configure this timeout please see the
9299     * set_config() documentation below.
9300     *
9301     * If you pass a function in as the properties argument, the
9302     * function will receive the DOMElement that triggered the
9303     * event as an argument.  You are expected to return an object
9304     * from the function; any properties defined on this object
9305     * will be sent to mixpanel as event properties.
9306     *
9307     * @type {Function}
9308     * @param {Object|String} query A valid DOM query, element or jQuery-esque list
9309     * @param {String} event_name The name of the event to track
9310     * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement
9311     */
9312    MixpanelLib.prototype.track_forms = function() {
9313        return this._track_dom.call(this, FormTracker, arguments);
9314    };
9315
9316    /**
9317     * Time an event by including the time between this call and a
9318     * later 'track' call for the same event in the properties sent
9319     * with the event.
9320     *
9321     * ### Usage:
9322     *
9323     *     // time an event named 'Registered'
9324     *     mixpanel.time_event('Registered');
9325     *     mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21});
9326     *
9327     * When called for a particular event name, the next track call for that event
9328     * name will include the elapsed time between the 'time_event' and 'track'
9329     * calls. This value is stored as seconds in the '$duration' property.
9330     *
9331     * @param {String} event_name The name of the event.
9332     */
9333    MixpanelLib.prototype.time_event = function(event_name) {
9334        if (_.isUndefined(event_name)) {
9335            this.report_error('No event name provided to mixpanel.time_event');
9336            return;
9337        }
9338
9339        if (this._event_is_disabled(event_name)) {
9340            return;
9341        }
9342
9343        this['persistence'].set_event_timer(event_name,  new Date().getTime());
9344    };
9345
9346    var REGISTER_DEFAULTS = {
9347        'persistent': true
9348    };
9349    /**
9350     * Helper to parse options param for register methods, maintaining
9351     * legacy support for plain "days" param instead of options object
9352     * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods
9353     * @returns {Object} options object
9354     */
9355    var options_for_register = function(days_or_options) {
9356        var options;
9357        if (_.isObject(days_or_options)) {
9358            options = days_or_options;
9359        } else if (!_.isUndefined(days_or_options)) {
9360            options = {'days': days_or_options};
9361        } else {
9362            options = {};
9363        }
9364        return _.extend({}, REGISTER_DEFAULTS, options);
9365    };
9366
9367    /**
9368     * Register a set of super properties, which are included with all
9369     * events. This will overwrite previous super property values.
9370     *
9371     * ### Usage:
9372     *
9373     *     // register 'Gender' as a super property
9374     *     mixpanel.register({'Gender': 'Female'});
9375     *
9376     *     // register several super properties when a user signs up
9377     *     mixpanel.register({
9378     *         'Email': '[email protected]',
9379     *         'Account Type': 'Free'
9380     *     });
9381     *
9382     *     // register only for the current pageload
9383     *     mixpanel.register({'Name': 'Pat'}, {persistent: false});
9384     *
9385     * @param {Object} properties An associative array of properties to store about the user
9386     * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props)
9387     * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props)
9388     * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
9389     */
9390    MixpanelLib.prototype.register = function(props, days_or_options) {
9391        var ret = this._run_hook('before_register', props, days_or_options);
9392        if (ret === null) {
9393            return;
9394        } else {
9395            props = ret[0];
9396            days_or_options = ret[1];
9397        }
9398
9399        var options = options_for_register(days_or_options);
9400        if (options['persistent']) {
9401            this['persistence'].register(props, options['days']);
9402        } else {
9403            _.extend(this.unpersisted_superprops, props);
9404        }
9405    };
9406
9407    /**
9408     * Register a set of super properties only once. This will not
9409     * overwrite previous super property values, unlike register().
9410     *
9411     * ### Usage:
9412     *
9413     *     // register a super property for the first time only
9414     *     mixpanel.register_once({
9415     *         'First Login Date': new Date().toISOString()
9416     *     });
9417     *
9418     *     // register once, only for the current pageload
9419     *     mixpanel.register_once({
9420     *         'First interaction time': new Date().toISOString()
9421     *     }, 'None', {persistent: false});
9422     *
9423     * ### Notes:
9424     *
9425     * If default_value is specified, current super properties
9426     * with that value will be overwritten.
9427     *
9428     * @param {Object} properties An associative array of properties to store about the user
9429     * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
9430     * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props)
9431     * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props)
9432     * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
9433     */
9434    MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
9435        var ret = this._run_hook('before_register_once', props, default_value, days_or_options);
9436        if (ret === null) {
9437            return;
9438        } else {
9439            props = ret[0];
9440            default_value = ret[1];
9441            days_or_options = ret[2];
9442        }
9443
9444        var options = options_for_register(days_or_options);
9445        if (options['persistent']) {
9446            this['persistence'].register_once(props, default_value, options['days']);
9447        } else {
9448            if (typeof(default_value) === 'undefined') {
9449                default_value = 'None';
9450            }
9451            _.each(props, function(val, prop) {
9452                if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) {
9453                    this.unpersisted_superprops[prop] = val;
9454                }
9455            }, this);
9456        }
9457    };
9458
9459    /**
9460     * Delete a super property stored with the current user.
9461     *
9462     * @param {String} property The name of the super property to remove
9463     * @param {Object} [options]
9464     * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
9465     */
9466    MixpanelLib.prototype.unregister = function(property, options) {
9467        var ret = this._run_hook('before_unregister', property, options);
9468        if (ret === null) {
9469            return;
9470        } else {
9471            property = ret[0];
9472            options = ret[1];
9473        }
9474
9475        options = options_for_register(options);
9476        if (options['persistent']) {
9477            this['persistence'].unregister(property);
9478        } else {
9479            delete this.unpersisted_superprops[property];
9480        }
9481    };
9482
9483    MixpanelLib.prototype._register_single = function(prop, value) {
9484        var props = {};
9485        props[prop] = value;
9486        this.register(props);
9487    };
9488
9489    /**
9490     * Identify a user with a unique ID to track user activity across
9491     * devices, tie a user to their events, and create a user profile.
9492     * If you never call this method, unique visitors are tracked using
9493     * a UUID generated the first time they visit the site.
9494     *
9495     * Call identify when you know the identity of the current user,
9496     * typically after login or signup. We recommend against using
9497     * identify for anonymous visitors to your site.
9498     *
9499     * ### Notes:
9500     * If your project has
9501     * <a href="https://help.mixpanel.com/hc/en-us/articles/360039133851">ID Merge</a>
9502     * enabled, the identify method will connect pre- and
9503     * post-authentication events when appropriate.
9504     *
9505     * If your project does not have ID Merge enabled, identify will
9506     * change the user's local distinct_id to the unique ID you pass.
9507     * Events tracked prior to authentication will not be connected
9508     * to the same user identity. If ID Merge is disabled, alias can
9509     * be used to connect pre- and post-registration events.
9510     *
9511     * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used.
9512     */
9513    MixpanelLib.prototype.identify = function(
9514        new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
9515    ) {
9516        // Optional Parameters
9517        //  _set_callback:function  A callback to be run if and when the People set queue is flushed
9518        //  _add_callback:function  A callback to be run if and when the People add queue is flushed
9519        //  _append_callback:function  A callback to be run if and when the People append queue is flushed
9520        //  _set_once_callback:function  A callback to be run if and when the People set_once queue is flushed
9521        //  _union_callback:function  A callback to be run if and when the People union queue is flushed
9522        //  _unset_callback:function  A callback to be run if and when the People unset queue is flushed
9523        var ret = this._run_hook('before_identify', new_distinct_id);
9524
9525        if (ret === null) {
9526            return -1;
9527        } else {
9528            new_distinct_id = ret[0];
9529        }
9530
9531        var previous_distinct_id = this.get_distinct_id();
9532        if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
9533            // we allow the following condition if previous distinct_id is same as new_distinct_id
9534            // so that you can force flush people updates for anonymous profiles.
9535            if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) {
9536                this.report_error('distinct_id cannot have $device: prefix');
9537                return -1;
9538            }
9539            this.register({'$user_id': new_distinct_id});
9540        }
9541
9542        if (!this.get_property('$device_id')) {
9543            // The persisted distinct id might not actually be a device id at all
9544            // it might be a distinct id of the user from before
9545            var device_id = previous_distinct_id;
9546            this.register_once({
9547                '$had_persisted_distinct_id': true,
9548                '$device_id': device_id
9549            }, '');
9550        }
9551
9552        // identify only changes the distinct id if it doesn't match either the existing or the alias;
9553        // if it's new, blow away the alias as well.
9554        if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) {
9555            this.unregister(ALIAS_ID_KEY);
9556            this.register({'distinct_id': new_distinct_id});
9557        }
9558        this._flags.identify_called = true;
9559        // Flush any queued up people requests
9560        this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback);
9561
9562        // send an $identify event any time the distinct_id is changing - logic on the server
9563        // will determine whether or not to do anything with it.
9564        if (new_distinct_id !== previous_distinct_id) {
9565            this.track('$identify', {
9566                'distinct_id': new_distinct_id,
9567                '$anon_distinct_id': previous_distinct_id
9568            }, {skip_hooks: true});
9569        }
9570
9571        // check feature flags again if distinct id has changed
9572        if (new_distinct_id !== previous_distinct_id) {
9573            this.flags.fetchFlags().catch(function() {
9574                console.error('[flags] Error fetching flags during identify');
9575            });
9576        }
9577    };
9578
9579    /**
9580     * Clears super properties and generates a new random distinct_id for this instance.
9581     * Useful for clearing data when a user logs out.
9582     */
9583    MixpanelLib.prototype.reset = function() {
9584        this.stop_session_recording();
9585        this['persistence'].clear();
9586        this._flags.identify_called = false;
9587        var uuid = _.UUID();
9588        this.register_once({
9589            'distinct_id': DEVICE_ID_PREFIX + uuid,
9590            '$device_id': uuid
9591        }, '');
9592        this._check_and_start_session_recording();
9593        this.flags.reset();
9594    };
9595
9596    /**
9597     * Returns the current distinct id of the user. This is either the id automatically
9598     * generated by the library or the id that has been passed by a call to identify().
9599     *
9600     * ### Notes:
9601     *
9602     * get_distinct_id() can only be called after the Mixpanel library has finished loading.
9603     * init() has a loaded function available to handle this automatically. For example:
9604     *
9605     *     // set distinct_id after the mixpanel library has loaded
9606     *     mixpanel.init('YOUR PROJECT TOKEN', {
9607     *         loaded: function(mixpanel) {
9608     *             distinct_id = mixpanel.get_distinct_id();
9609     *         }
9610     *     });
9611     */
9612    MixpanelLib.prototype.get_distinct_id = function() {
9613        return this.get_property('distinct_id');
9614    };
9615
9616    /**
9617     * The alias method creates an alias which Mixpanel will use to
9618     * remap one id to another. Multiple aliases can point to the
9619     * same identifier.
9620     *
9621     * The following is a valid use of alias:
9622     *
9623     *     mixpanel.alias('new_id', 'existing_id');
9624     *     // You can add multiple id aliases to the existing ID
9625     *     mixpanel.alias('newer_id', 'existing_id');
9626     *
9627     * Aliases can also be chained - the following is a valid example:
9628     *
9629     *     mixpanel.alias('new_id', 'existing_id');
9630     *     // chain newer_id - new_id - existing_id
9631     *     mixpanel.alias('newer_id', 'new_id');
9632     *
9633     * Aliases cannot point to multiple identifiers - the following
9634     * example will not work:
9635     *
9636     *     mixpanel.alias('new_id', 'existing_id');
9637     *     // this is invalid as 'new_id' already points to 'existing_id'
9638     *     mixpanel.alias('new_id', 'newer_id');
9639     *
9640     * ### Notes:
9641     *
9642     * If your project does not have
9643     * <a href="https://help.mixpanel.com/hc/en-us/articles/360039133851">ID Merge</a>
9644     * enabled, the best practice is to call alias once when a unique
9645     * ID is first created for a user (e.g., when a user first registers
9646     * for an account). Do not use alias multiple times for a single
9647     * user without ID Merge enabled.
9648     *
9649     * @param {String} alias A unique identifier that you want to use for this user in the future.
9650     * @param {String} [original] The current identifier being used for this user.
9651     */
9652    MixpanelLib.prototype.alias = function(alias, original) {
9653        // If the $people_distinct_id key exists in persistence, there has been a previous
9654        // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
9655        // this ID, as it will duplicate users.
9656        if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
9657            this.report_error('Attempting to create alias for existing People user - aborting.');
9658            return -2;
9659        }
9660
9661        var _this = this;
9662        if (_.isUndefined(original)) {
9663            original = this.get_distinct_id();
9664        }
9665        if (alias !== original) {
9666            this._register_single(ALIAS_ID_KEY, alias);
9667            return this.track('$create_alias', {
9668                'alias': alias,
9669                'distinct_id': original
9670            }, {
9671                skip_hooks: true
9672            }, function() {
9673                // Flush the people queue
9674                _this.identify(alias);
9675            });
9676        } else {
9677            this.report_error('alias matches current distinct_id - skipping api call.');
9678            this.identify(alias);
9679            return -1;
9680        }
9681    };
9682
9683    /**
9684     * Provide a string to recognize the user by. The string passed to
9685     * this method will appear in the Mixpanel Streams product rather
9686     * than an automatically generated name. Name tags do not have to
9687     * be unique.
9688     *
9689     * This value will only be included in Streams data.
9690     *
9691     * @param {String} name_tag A human readable name for the user
9692     * @deprecated
9693     */
9694    MixpanelLib.prototype.name_tag = function(name_tag) {
9695        this._register_single('mp_name_tag', name_tag);
9696    };
9697
9698    /**
9699     * Update the configuration of a mixpanel library instance.
9700     *
9701     * The default config is:
9702     *
9703     *     {
9704     *       // host for requests (customizable for e.g. a local proxy)
9705     *       api_host: 'https://api-js.mixpanel.com',
9706     *
9707     *       // endpoints for different types of requests
9708     *       api_routes: {
9709     *         track: 'track/',
9710     *         engage: 'engage/',
9711     *         groups: 'groups/',
9712     *       }
9713     *
9714     *       // HTTP method for tracking requests
9715     *       api_method: 'POST'
9716     *
9717     *       // transport for sending requests ('XHR' or 'sendBeacon')
9718     *       // NB: sendBeacon should only be used for scenarios such as
9719     *       // page unload where a "best-effort" attempt to send is
9720     *       // acceptable; the sendBeacon API does not support callbacks
9721     *       // or any way to know the result of the request. Mixpanel
9722     *       // tracking via sendBeacon will not support any event-
9723     *       // batching or retry mechanisms.
9724     *       api_transport: 'XHR'
9725     *
9726     *       // request-batching/queueing/retry
9727     *       batch_requests: true,
9728     *
9729     *       // maximum number of events/updates to send in a single
9730     *       // network request
9731     *       batch_size: 50,
9732     *
9733     *       // milliseconds to wait between sending batch requests
9734     *       batch_flush_interval_ms: 5000,
9735     *
9736     *       // milliseconds to wait for network responses to batch requests
9737     *       // before they are considered timed-out and retried
9738     *       batch_request_timeout_ms: 90000,
9739     *
9740     *       // override value for cookie domain, only useful for ensuring
9741     *       // correct cross-subdomain cookies on unusual domains like
9742     *       // subdomain.mainsite.avocat.fr; NB this cannot be used to
9743     *       // set cookies on a different domain than the current origin
9744     *       cookie_domain: ''
9745     *
9746     *       // super properties cookie expiration (in days)
9747     *       cookie_expiration: 365
9748     *
9749     *       // if true, cookie will be set with SameSite=None; Secure
9750     *       // this is only useful in special situations, like embedded
9751     *       // 3rd-party iframes that set up a Mixpanel instance
9752     *       cross_site_cookie: false
9753     *
9754     *       // super properties span subdomains
9755     *       cross_subdomain_cookie: true
9756     *
9757     *       // debug mode
9758     *       debug: false
9759     *
9760     *       // if this is true, the mixpanel cookie or localStorage entry
9761     *       // will be deleted, and no user persistence will take place
9762     *       disable_persistence: false
9763     *
9764     *       // if this is true, Mixpanel will automatically determine
9765     *       // City, Region and Country data using the IP address of
9766     *       //the client
9767     *       ip: true
9768     *
9769     *       // opt users out of tracking by this Mixpanel instance by default
9770     *       opt_out_tracking_by_default: false
9771     *
9772     *       // opt users out of browser data storage by this Mixpanel instance by default
9773     *       opt_out_persistence_by_default: false
9774     *
9775     *       // persistence mechanism used by opt-in/opt-out methods - cookie
9776     *       // or localStorage - falls back to cookie if localStorage is unavailable
9777     *       opt_out_tracking_persistence_type: 'localStorage'
9778     *
9779     *       // customize the name of cookie/localStorage set by opt-in/opt-out methods
9780     *       opt_out_tracking_cookie_prefix: null
9781     *
9782     *       // type of persistent store for super properties (cookie/
9783     *       // localStorage) if set to 'localStorage', any existing
9784     *       // mixpanel cookie value with the same persistence_name
9785     *       // will be transferred to localStorage and deleted
9786     *       persistence: 'cookie'
9787     *
9788     *       // name for super properties persistent store
9789     *       persistence_name: ''
9790     *
9791     *       // names of properties/superproperties which should never
9792     *       // be sent with track() calls
9793     *       property_blacklist: []
9794     *
9795     *       // if this is true, mixpanel cookies will be marked as
9796     *       // secure, meaning they will only be transmitted over https
9797     *       secure_cookie: false
9798     *
9799     *       // disables enriching user profiles with first touch marketing data
9800     *       skip_first_touch_marketing: false
9801     *
9802     *       // the amount of time track_links will
9803     *       // wait for Mixpanel's servers to respond
9804     *       track_links_timeout: 300
9805     *
9806     *       // adds any UTM parameters and click IDs present on the page to any events fired
9807     *       track_marketing: true
9808     *
9809     *       // enables automatic page view tracking using default page view events through
9810     *       // the track_pageview() method
9811     *       track_pageview: false
9812     *
9813     *       // if you set upgrade to be true, the library will check for
9814     *       // a cookie from our old js library and import super
9815     *       // properties from it, then the old cookie is deleted
9816     *       // The upgrade config option only works in the initialization,
9817     *       // so make sure you set it when you create the library.
9818     *       upgrade: false
9819     *
9820     *       // extra HTTP request headers to set for each API request, in
9821     *       // the format {'Header-Name': value}
9822     *       xhr_headers: {}
9823     *
9824     *       // whether to ignore or respect the web browser's Do Not Track setting
9825     *       ignore_dnt: false
9826     *     }
9827     *
9828     *
9829     * @param {Object} config A dictionary of new configuration values to update
9830     */
9831    MixpanelLib.prototype.set_config = function(config) {
9832        if (_.isObject(config)) {
9833            _.extend(this['config'], config);
9834
9835            var new_batch_size = config['batch_size'];
9836            if (new_batch_size) {
9837                _.each(this.request_batchers, function(batcher) {
9838                    batcher.resetBatchSize();
9839                });
9840            }
9841
9842            if (!this.get_config('persistence_name')) {
9843                this['config']['persistence_name'] = this['config']['cookie_name'];
9844            }
9845            if (!this.get_config('disable_persistence')) {
9846                this['config']['disable_persistence'] = this['config']['disable_cookie'];
9847            }
9848
9849            if (this['persistence']) {
9850                this['persistence'].update_config(this['config']);
9851            }
9852            Config.DEBUG = Config.DEBUG || this.get_config('debug');
9853
9854            if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
9855                this.autocapture.init();
9856            }
9857
9858            if (_.isObject(config['hooks'])) {
9859                this.hooks = {};
9860                _.each(config['hooks'], function(hook_value, hook_name) {
9861                    if (_.isFunction(hook_value)) {
9862                        this.hooks[hook_name] = [hook_value];
9863                    } else if (_.isArray(hook_value)) {
9864                        this.hooks[hook_name] = [];
9865                        for (var i = 0; i < hook_value.length; i++) {
9866                            if (!_.isFunction(hook_value[i])) {
9867                                console.critical('Invalid hook added. Hook is not a function');
9868                            }
9869                            this.hooks[hook_name].push(hook_value[i]);
9870                        }
9871                    } else {
9872                        console.critical('Invalid hooks added. Ensure that the hook values passed into config.hooks are functions or arrays of functions.');
9873                    }
9874                }, this);
9875            }
9876        }
9877    };
9878
9879    /**
9880     * returns the current config object for the library.
9881     */
9882    MixpanelLib.prototype.get_config = function(prop_name) {
9883        return this['config'][prop_name];
9884    };
9885
9886    /**
9887     * Fetch a hook function from config, with safe default, and run it
9888     * against the given arguments
9889     * @param {string} hook_name which hook to retrieve
9890     * @returns {any|null} return value of user-provided hook, or null if nothing was returned
9891     */
9892    MixpanelLib.prototype._run_hook = function(hook_name) {
9893        var hook_data = slice.call(arguments, 1);
9894        _.each(this.hooks[hook_name], function(hook) {
9895            if (hook_data === null) {
9896                return null;
9897            }
9898
9899            var ret = hook.apply(this, hook_data);
9900
9901            if (typeof ret === 'undefined') {
9902                this.report_error(hook_name + ' hook did not return a valid value');
9903                hook_data = null;
9904            } else {
9905                if (!_.isArray(ret)) {
9906                    ret = [ret];
9907                }
9908                hook_data.splice.apply(hook_data, [0, ret.length].concat(ret));
9909            }
9910        }, this);
9911
9912        return hook_data;
9913    };
9914
9915    /**
9916     * Returns the value of the super property named property_name. If no such
9917     * property is set, get_property() will return the undefined value.
9918     *
9919     * ### Notes:
9920     *
9921     * get_property() can only be called after the Mixpanel library has finished loading.
9922     * init() has a loaded function available to handle this automatically. For example:
9923     *
9924     *     // grab value for 'user_id' after the mixpanel library has loaded
9925     *     mixpanel.init('YOUR PROJECT TOKEN', {
9926     *         loaded: function(mixpanel) {
9927     *             user_id = mixpanel.get_property('user_id');
9928     *         }
9929     *     });
9930     *
9931     * @param {String} property_name The name of the super property you want to retrieve
9932     */
9933    MixpanelLib.prototype.get_property = function(property_name) {
9934        return this['persistence'].load_prop([property_name]);
9935    };
9936
9937    /**
9938     * Get the API host for a specific endpoint type, falling back to the default api_host if not specified
9939     *
9940     * @param {String} endpoint_type The type of endpoint (e.g., "events", "people", "groups")
9941     * @returns {String} The API host to use for this endpoint
9942     */
9943    MixpanelLib.prototype.get_api_host = function(endpoint_type) {
9944        return this.get_config('api_hosts')[endpoint_type] || this.get_config('api_host');
9945    };
9946
9947    MixpanelLib.prototype.toString = function() {
9948        var name = this.get_config('name');
9949        if (name !== PRIMARY_INSTANCE_NAME) {
9950            name = PRIMARY_INSTANCE_NAME + '.' + name;
9951        }
9952        return name;
9953    };
9954
9955    MixpanelLib.prototype._event_is_disabled = function(event_name) {
9956        return _.isBlockedUA(userAgent) ||
9957            this._flags.disable_all_events ||
9958            _.include(this.__disabled_events, event_name);
9959    };
9960
9961    // perform some housekeeping around GDPR opt-in/out state
9962    MixpanelLib.prototype._gdpr_init = function() {
9963        var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage';
9964
9965        // try to convert opt-in/out cookies to localStorage if possible
9966        if (is_localStorage_requested && _.localStorage.is_supported()) {
9967            if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) {
9968                this.opt_in_tracking({'enable_persistence': false});
9969            }
9970            if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) {
9971                this.opt_out_tracking({'clear_persistence': false});
9972            }
9973            this.clear_opt_in_out_tracking({
9974                'persistence_type': 'cookie',
9975                'enable_persistence': false
9976            });
9977        }
9978
9979        // check whether the user has already opted out - if so, clear & disable persistence
9980        if (this.has_opted_out_tracking()) {
9981            this._gdpr_update_persistence({'clear_persistence': true});
9982
9983        // check whether we should opt out by default
9984        // note: we don't clear persistence here by default since opt-out default state is often
9985        //       used as an initial state while GDPR information is being collected
9986        } else if (!this.has_opted_in_tracking() && (
9987            this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout')
9988        )) {
9989            _.cookie.remove('mp_optout');
9990            this.opt_out_tracking({
9991                'clear_persistence': this.get_config('opt_out_persistence_by_default')
9992            });
9993        }
9994    };
9995
9996    /**
9997     * Enable or disable persistence based on options
9998     * only enable/disable if persistence is not already in this state
9999     * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it
10000     * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence
10001     */
10002    MixpanelLib.prototype._gdpr_update_persistence = function(options) {
10003        var disabled;
10004        if (options && options['clear_persistence']) {
10005            disabled = true;
10006        } else if (options && options['enable_persistence']) {
10007            disabled = false;
10008        } else {
10009            return;
10010        }
10011
10012        if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) {
10013            this['persistence'].set_disabled(disabled);
10014        }
10015
10016        if (disabled) {
10017            this.stop_batch_senders();
10018            this.stop_session_recording();
10019        } else {
10020            // only start batchers after opt-in if they have previously been started
10021            // in order to avoid unintentionally starting up batching for the first time
10022            if (this._batchers_were_started) {
10023                this.start_batch_senders();
10024            }
10025        }
10026    };
10027
10028    // call a base gdpr function after constructing the appropriate token and options args
10029    MixpanelLib.prototype._gdpr_call_func = function(func, options) {
10030        options = _.extend({
10031            'track': _.bind(this.track, this),
10032            'persistence_type': this.get_config('opt_out_tracking_persistence_type'),
10033            'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'),
10034            'cookie_expiration': this.get_config('cookie_expiration'),
10035            'cross_site_cookie': this.get_config('cross_site_cookie'),
10036            'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'),
10037            'cookie_domain': this.get_config('cookie_domain'),
10038            'secure_cookie': this.get_config('secure_cookie'),
10039            'ignore_dnt': this.get_config('ignore_dnt')
10040        }, options);
10041
10042        // check if localStorage can be used for recording opt out status, fall back to cookie if not
10043        if (!_.localStorage.is_supported()) {
10044            options['persistence_type'] = 'cookie';
10045        }
10046
10047        return func(this.get_config('token'), {
10048            track: options['track'],
10049            trackEventName: options['track_event_name'],
10050            trackProperties: options['track_properties'],
10051            persistenceType: options['persistence_type'],
10052            persistencePrefix: options['cookie_prefix'],
10053            cookieDomain: options['cookie_domain'],
10054            cookieExpiration: options['cookie_expiration'],
10055            crossSiteCookie: options['cross_site_cookie'],
10056            crossSubdomainCookie: options['cross_subdomain_cookie'],
10057            secureCookie: options['secure_cookie'],
10058            ignoreDnt: options['ignore_dnt']
10059        });
10060    };
10061
10062    /**
10063     * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance
10064     *
10065     * ### Usage:
10066     *
10067     *     // opt user in
10068     *     mixpanel.opt_in_tracking();
10069     *
10070     *     // opt user in with specific event name, properties, cookie configuration
10071     *     mixpanel.opt_in_tracking({
10072     *         track_event_name: 'User opted in',
10073     *         track_event_properties: {
10074     *             'Email': '[email protected]'
10075     *         },
10076     *         cookie_expiration: 30,
10077     *         secure_cookie: true
10078     *     });
10079     *
10080     * @param {Object} [options] A dictionary of config options to override
10081     * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method)
10082     * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action
10083     * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action
10084     * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
10085     * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
10086     * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
10087     * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config)
10088     * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config)
10089     * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config)
10090     * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config)
10091     * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config)
10092     */
10093    MixpanelLib.prototype.opt_in_tracking = function(options) {
10094        options = _.extend({
10095            'enable_persistence': true
10096        }, options);
10097
10098        this._gdpr_call_func(optIn, options);
10099        this._gdpr_update_persistence(options);
10100    };
10101
10102    /**
10103     * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance
10104     *
10105     * ### Usage:
10106     *
10107     *     // opt user out
10108     *     mixpanel.opt_out_tracking();
10109     *
10110     *     // opt user out with different cookie configuration from Mixpanel instance
10111     *     mixpanel.opt_out_tracking({
10112     *         cookie_expiration: 30,
10113     *         secure_cookie: true
10114     *     });
10115     *
10116     * @param {Object} [options] A dictionary of config options to override
10117     * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out
10118     * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence
10119     * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
10120     * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
10121     * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config)
10122     * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config)
10123     * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config)
10124     * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config)
10125     * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config)
10126     */
10127    MixpanelLib.prototype.opt_out_tracking = function(options) {
10128        options = _.extend({
10129            'clear_persistence': true,
10130            'delete_user': true
10131        }, options);
10132
10133        // delete user and clear charges since these methods may be disabled by opt-out
10134        if (options['delete_user'] && this['people'] && this['people']._identify_called()) {
10135            this['people'].delete_user();
10136            this['people'].clear_charges();
10137        }
10138
10139        this._gdpr_call_func(optOut, options);
10140        this._gdpr_update_persistence(options);
10141    };
10142
10143    /**
10144     * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance
10145     *
10146     * ### Usage:
10147     *
10148     *     var has_opted_in = mixpanel.has_opted_in_tracking();
10149     *     // use has_opted_in value
10150     *
10151     * @param {Object} [options] A dictionary of config options to override
10152     * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
10153     * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
10154     * @returns {boolean} current opt-in status
10155     */
10156    MixpanelLib.prototype.has_opted_in_tracking = function(options) {
10157        return this._gdpr_call_func(hasOptedIn, options);
10158    };
10159
10160    /**
10161     * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance
10162     *
10163     * ### Usage:
10164     *
10165     *     var has_opted_out = mixpanel.has_opted_out_tracking();
10166     *     // use has_opted_out value
10167     *
10168     * @param {Object} [options] A dictionary of config options to override
10169     * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
10170     * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
10171     * @returns {boolean} current opt-out status
10172     */
10173    MixpanelLib.prototype.has_opted_out_tracking = function(options) {
10174        return this._gdpr_call_func(hasOptedOut, options);
10175    };
10176
10177    /**
10178     * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance
10179     *
10180     * ### Usage:
10181     *
10182     *     // clear user's opt-in/out status
10183     *     mixpanel.clear_opt_in_out_tracking();
10184     *
10185     *     // clear user's opt-in/out status with specific cookie configuration - should match
10186     *     // configuration used when opt_in_tracking/opt_out_tracking methods were called.
10187     *     mixpanel.clear_opt_in_out_tracking({
10188     *         cookie_expiration: 30,
10189     *         secure_cookie: true
10190     *     });
10191     *
10192     * @param {Object} [options] A dictionary of config options to override
10193     * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
10194     * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
10195     * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
10196     * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config)
10197     * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config)
10198     * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config)
10199     * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config)
10200     * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config)
10201     */
10202    MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) {
10203        options = _.extend({
10204            'enable_persistence': true
10205        }, options);
10206
10207        this._gdpr_call_func(clearOptInOut, options);
10208        this._gdpr_update_persistence(options);
10209    };
10210
10211    MixpanelLib.prototype.report_error = function(msg, err) {
10212        console.error.apply(console.error, arguments);
10213        try {
10214            if (!err && !(msg instanceof Error)) {
10215                msg = new Error(msg);
10216            }
10217            this.get_config('error_reporter')(msg, err);
10218        } catch(err) {
10219            console.error(err);
10220        }
10221    };
10222
10223    MixpanelLib.prototype.add_hook = function(hook_name, hook_fn) {
10224        if (!this.hooks[hook_name]) {
10225            this.hooks[hook_name] = [];
10226        }
10227        this.hooks[hook_name].push(hook_fn);
10228    };
10229
10230    MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
10231        var fn_index;
10232        if (this.hooks[hook_name]) {
10233            fn_index = this.hooks[hook_name].indexOf(hook_fn);
10234            if (fn_index !== -1) {
10235                this.hooks[hook_name].splice(fn_index, 1);
10236            } else {
10237                console.log('remove_hook failed. Matching hook was not found');
10238            }
10239        }
10240    };
10241
10242    // EXPORTS (for closure compiler)
10243
10244    // MixpanelLib Exports
10245    MixpanelLib.prototype['init']                               = MixpanelLib.prototype.init;
10246    MixpanelLib.prototype['reset']                              = MixpanelLib.prototype.reset;
10247    MixpanelLib.prototype['enable']                             = MixpanelLib.prototype.enable;
10248    MixpanelLib.prototype['disable']                            = MixpanelLib.prototype.disable;
10249    MixpanelLib.prototype['time_event']                         = MixpanelLib.prototype.time_event;
10250    MixpanelLib.prototype['track']                              = MixpanelLib.prototype.track;
10251    MixpanelLib.prototype['track_links']                        = MixpanelLib.prototype.track_links;
10252    MixpanelLib.prototype['track_forms']                        = MixpanelLib.prototype.track_forms;
10253    MixpanelLib.prototype['track_pageview']                     = MixpanelLib.prototype.track_pageview;
10254    MixpanelLib.prototype['register']                           = MixpanelLib.prototype.register;
10255    MixpanelLib.prototype['register_once']                      = MixpanelLib.prototype.register_once;
10256    MixpanelLib.prototype['unregister']                         = MixpanelLib.prototype.unregister;
10257    MixpanelLib.prototype['identify']                           = MixpanelLib.prototype.identify;
10258    MixpanelLib.prototype['alias']                              = MixpanelLib.prototype.alias;
10259    MixpanelLib.prototype['name_tag']                           = MixpanelLib.prototype.name_tag;
10260    MixpanelLib.prototype['set_config']                         = MixpanelLib.prototype.set_config;
10261    MixpanelLib.prototype['get_config']                         = MixpanelLib.prototype.get_config;
10262    MixpanelLib.prototype['get_api_host']                       = MixpanelLib.prototype.get_api_host;
10263    MixpanelLib.prototype['get_property']                       = MixpanelLib.prototype.get_property;
10264    MixpanelLib.prototype['get_distinct_id']                    = MixpanelLib.prototype.get_distinct_id;
10265    MixpanelLib.prototype['toString']                           = MixpanelLib.prototype.toString;
10266    MixpanelLib.prototype['opt_out_tracking']                   = MixpanelLib.prototype.opt_out_tracking;
10267    MixpanelLib.prototype['opt_in_tracking']                    = MixpanelLib.prototype.opt_in_tracking;
10268    MixpanelLib.prototype['has_opted_out_tracking']             = MixpanelLib.prototype.has_opted_out_tracking;
10269    MixpanelLib.prototype['has_opted_in_tracking']              = MixpanelLib.prototype.has_opted_in_tracking;
10270    MixpanelLib.prototype['clear_opt_in_out_tracking']          = MixpanelLib.prototype.clear_opt_in_out_tracking;
10271    MixpanelLib.prototype['get_group']                          = MixpanelLib.prototype.get_group;
10272    MixpanelLib.prototype['set_group']                          = MixpanelLib.prototype.set_group;
10273    MixpanelLib.prototype['add_group']                          = MixpanelLib.prototype.add_group;
10274    MixpanelLib.prototype['remove_group']                       = MixpanelLib.prototype.remove_group;
10275    MixpanelLib.prototype['add_hook']                           = MixpanelLib.prototype.add_hook;
10276    MixpanelLib.prototype['remove_hook']                        = MixpanelLib.prototype.remove_hook;
10277    MixpanelLib.prototype['track_with_groups']                  = MixpanelLib.prototype.track_with_groups;
10278    MixpanelLib.prototype['start_batch_senders']                = MixpanelLib.prototype.start_batch_senders;
10279    MixpanelLib.prototype['stop_batch_senders']                 = MixpanelLib.prototype.stop_batch_senders;
10280    MixpanelLib.prototype['start_session_recording']            = MixpanelLib.prototype.start_session_recording;
10281    MixpanelLib.prototype['stop_session_recording']             = MixpanelLib.prototype.stop_session_recording;
10282    MixpanelLib.prototype['pause_session_recording']            = MixpanelLib.prototype.pause_session_recording;
10283    MixpanelLib.prototype['resume_session_recording']           = MixpanelLib.prototype.resume_session_recording;
10284    MixpanelLib.prototype['get_session_recording_properties']   = MixpanelLib.prototype.get_session_recording_properties;
10285    MixpanelLib.prototype['get_session_replay_url']             = MixpanelLib.prototype.get_session_replay_url;
10286    MixpanelLib.prototype['get_tab_id']                         = MixpanelLib.prototype.get_tab_id;
10287    MixpanelLib.prototype['DEFAULT_API_ROUTES']                 = DEFAULT_API_ROUTES;
10288
10289    // Exports intended only for testing
10290    MixpanelLib.prototype['__get_recorder']                     = MixpanelLib.prototype.__get_recorder;
10291    MixpanelLib.prototype['__get_recording_init_promise']       = MixpanelLib.prototype.__get_recording_init_promise;
10292
10293    // MixpanelPersistence Exports
10294    MixpanelPersistence.prototype['properties']            = MixpanelPersistence.prototype.properties;
10295    MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;
10296    MixpanelPersistence.prototype['update_referrer_info']  = MixpanelPersistence.prototype.update_referrer_info;
10297    MixpanelPersistence.prototype['get_cross_subdomain']   = MixpanelPersistence.prototype.get_cross_subdomain;
10298    MixpanelPersistence.prototype['clear']                 = MixpanelPersistence.prototype.clear;
10299
10300
10301    var instances = {};
10302    var extend_mp = function() {
10303        // add all the sub mixpanel instances
10304        _.each(instances, function(instance, name) {
10305            if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; }
10306        });
10307
10308        // add private functions as _
10309        mixpanel_master['_'] = _;
10310    };
10311
10312    var override_mp_init_func = function() {
10313        // we override the snippets init function to handle the case where a
10314        // user initializes the mixpanel library after the script loads & runs
10315        mixpanel_master['init'] = function(token, config, name) {
10316            if (name) {
10317                // initialize a sub library
10318                if (!mixpanel_master[name]) {
10319                    mixpanel_master[name] = instances[name] = create_mplib(token, config, name);
10320                    mixpanel_master[name]._loaded();
10321                }
10322                return mixpanel_master[name];
10323            } else {
10324                var instance = mixpanel_master;
10325
10326                if (instances[PRIMARY_INSTANCE_NAME]) {
10327                    // main mixpanel lib already initialized
10328                    instance = instances[PRIMARY_INSTANCE_NAME];
10329                } else if (token) {
10330                    // intialize the main mixpanel lib
10331                    instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME);
10332                    instance._loaded();
10333                    instances[PRIMARY_INSTANCE_NAME] = instance;
10334                }
10335
10336                mixpanel_master = instance;
10337                if (init_type === INIT_SNIPPET) {
10338                    win[PRIMARY_INSTANCE_NAME] = mixpanel_master;
10339                }
10340                extend_mp();
10341            }
10342        };
10343    };
10344
10345    var add_dom_loaded_handler = function() {
10346        // Cross browser DOM Loaded support
10347        function dom_loaded_handler() {
10348            // function flag since we only want to execute this once
10349            if (dom_loaded_handler.done) { return; }
10350            dom_loaded_handler.done = true;
10351
10352            DOM_LOADED = true;
10353            ENQUEUE_REQUESTS = false;
10354
10355            _.each(instances, function(inst) {
10356                inst._dom_loaded();
10357            });
10358        }
10359
10360        function do_scroll_check() {
10361            try {
10362                document$1.documentElement.doScroll('left');
10363            } catch(e) {
10364                setTimeout(do_scroll_check, 1);
10365                return;
10366            }
10367
10368            dom_loaded_handler();
10369        }
10370
10371        if (document$1.addEventListener) {
10372            if (document$1.readyState === 'complete') {
10373                // safari 4 can fire the DOMContentLoaded event before loading all
10374                // external JS (including this file). you will see some copypasta
10375                // on the internet that checks for 'complete' and 'loaded', but
10376                // 'loaded' is an IE thing
10377                dom_loaded_handler();
10378            } else {
10379                document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false);
10380            }
10381        } else if (document$1.attachEvent) {
10382            // IE
10383            document$1.attachEvent('onreadystatechange', dom_loaded_handler);
10384
10385            // check to make sure we arn't in a frame
10386            var toplevel = false;
10387            try {
10388                toplevel = win.frameElement === null;
10389            } catch(e) {
10390                // noop
10391            }
10392
10393            if (document$1.documentElement.doScroll && toplevel) {
10394                do_scroll_check();
10395            }
10396        }
10397
10398        // fallback handler, always will work
10399        _.register_event(win, 'load', dom_loaded_handler, true);
10400    };
10401
10402    function init_from_snippet(bundle_loader) {
10403        load_extra_bundle = bundle_loader;
10404        init_type = INIT_SNIPPET;
10405        mixpanel_master = win[PRIMARY_INSTANCE_NAME];
10406
10407        // Initialization
10408        if (_.isUndefined(mixpanel_master)) {
10409            // mixpanel wasn't initialized properly, report error and quit
10410            console.critical('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.');
10411            return;
10412        }
10413        if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) {
10414            // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
10415            console.critical('The Mixpanel library has already been downloaded at least once. Ensure that the Mixpanel code snippet only appears once on the page (and is not double-loaded by a tag manager) in order to avoid errors.');
10416            return;
10417        }
10418        var snippet_version = mixpanel_master['__SV'] || 0;
10419        if (snippet_version < 1.1) {
10420            // mixpanel wasn't initialized properly, report error and quit
10421            console.critical('Version mismatch; please ensure you\'re using the latest version of the Mixpanel code snippet.');
10422            return;
10423        }
10424
10425        // Load instances of the Mixpanel Library
10426        _.each(mixpanel_master['_i'], function(item) {
10427            if (item && _.isArray(item)) {
10428                instances[item[item.length-1]] = create_mplib.apply(this, item);
10429            }
10430        });
10431
10432        override_mp_init_func();
10433        mixpanel_master['init']();
10434
10435        // Fire loaded events after updating the window's mixpanel object
10436        _.each(instances, function(instance) {
10437            instance._loaded();
10438        });
10439
10440        add_dom_loaded_handler();
10441    }
10442
10443    // For loading separate bundles asynchronously via script tag
10444    // so that we don't load them until they are needed at runtime.
10445    function loadAsync (src, onload) {
10446        var scriptEl = document.createElement('script');
10447        scriptEl.type = 'text/javascript';
10448        scriptEl.async = true;
10449        scriptEl.onload = onload;
10450        scriptEl.src = src;
10451        document.head.appendChild(scriptEl);
10452    }
10453
10454    /* eslint camelcase: "off" */
10455
10456    init_from_snippet(loadAsync);
10457
10458})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.