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.