text: /* eslint camelcase: "off" */
  import Config from './config';
  import { _, console, userAgent, window, document } from './utils';
  import { evaluateSelector } from './property-filters';
  import { autotrack } from './autotrack';
  import { MixpanelGroup } from './mixpanel-group';
  import {
      optIn,
      optOut,
      hasOptedIn,
      hasOptedOut,
      clearOptInOut,
      addOptOutCheckMixpanelLib,
      addOptOutCheckMixpanelPeople
  } from './gdpr-utils';
  
  import {
      SET_ACTION,
      SET_ONCE_ACTION,
      UNSET_ACTION,
      ADD_ACTION,
      APPEND_ACTION,
      REMOVE_ACTION,
      UNION_ACTION,
      apiActions
  } from './api-actions';
  
  /*
   * Mixpanel JS Library
   *
   * Copyright 2012, Mixpanel, Inc. All Rights Reserved
   * http://mixpanel.com/
   *
   * Includes portions of Underscore.js
   * http://documentcloud.github.com/underscore/
   * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
   * Released under the MIT License.
   */
  
  // ==ClosureCompiler==
  // @compilation_level ADVANCED_OPTIMIZATIONS
  // @output_file_name mixpanel-2.8.min.js
  // ==/ClosureCompiler==
  
  /*
  SIMPLE STYLE GUIDE:
  
  this.x === public function
  this._x === internal - only use within this file
  this.__x === private - only use within the class
  
  Globals should be all caps
  */
  
  var init_type;       // MODULE or SNIPPET loader
  var mixpanel_master; // main mixpanel instance / object
  var INIT_MODULE  = 0;
  var INIT_SNIPPET = 1;
  
  /*
   * Constants
   */
  /** @const */   var PRIMARY_INSTANCE_NAME     = 'mixpanel';
  /** @const */   var SET_QUEUE_KEY             = '__mps';
  /** @const */   var SET_ONCE_QUEUE_KEY        = '__mpso';
  /** @const */   var UNSET_QUEUE_KEY           = '__mpus';
  /** @const */   var ADD_QUEUE_KEY             = '__mpa';
  /** @const */   var APPEND_QUEUE_KEY          = '__mpap';
  /** @const */   var REMOVE_QUEUE_KEY          = '__mpr';
  /** @const */   var UNION_QUEUE_KEY           = '__mpu';
  // This key is deprecated, but we want to check for it to see whether aliasing is allowed.
  /** @const */   var PEOPLE_DISTINCT_ID_KEY    = '$people_distinct_id';
  /** @const */   var ALIAS_ID_KEY              = '__alias';
  /** @const */   var CAMPAIGN_IDS_KEY          = '__cmpns';
  /** @const */   var EVENT_TIMERS_KEY          = '__timers';
  /** @const */   var RESERVED_PROPERTIES       = [
      SET_QUEUE_KEY,
      SET_ONCE_QUEUE_KEY,
      UNSET_QUEUE_KEY,
      ADD_QUEUE_KEY,
      APPEND_QUEUE_KEY,
      REMOVE_QUEUE_KEY,
      UNION_QUEUE_KEY,
      PEOPLE_DISTINCT_ID_KEY,
      ALIAS_ID_KEY,
      CAMPAIGN_IDS_KEY,
      EVENT_TIMERS_KEY
  ];
  
  /*
   * Dynamic... constants? Is that an oxymoron?
   */
      // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
      // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials
  var USE_XHR = (window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest());
  
      // IE<10 does not support cross-origin XHR's but script tags
      // with defer won't block window.onload; ENQUEUE_REQUESTS
      // should only be true for Opera<12
  var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1);
  
  /*
   * Module-level globals
   */
  var DEFAULT_CONFIG = {
      'api_host':                          'https://api.mixpanel.com',
      'app_host':                          'https://mixpanel.com',
      'autotrack':                         true,
      'cdn':                               'https://cdn.mxpnl.com',
      'cross_subdomain_cookie':            true,
      'persistence':                       'cookie',
      'persistence_name':                  '',
      'cookie_name':                       '',
      'loaded':                            function() {},
      'store_google':                      true,
      'save_referrer':                     true,
      'test':                              false,
      'verbose':                           false,
      'img':                               false,
      'track_pageview':                    true,
      'debug':                             false,
      'track_links_timeout':               300,
      'cookie_expiration':                 365,
      'upgrade':                           false,
      'disable_persistence':               false,
      'disable_cookie':                    false,
      'secure_cookie':                     false,
      'ip':                                true,
      'opt_out_tracking_by_default':       false,
      'opt_out_persistence_by_default':    false,
      'opt_out_tracking_persistence_type': 'localStorage',
      'opt_out_tracking_cookie_prefix':    null,
      'property_blacklist':                [],
      'xhr_headers':                       {}, // { header: value, header2: value }
      'inapp_protocol':                    '//',
      'inapp_link_new_window':             false
  };
  
  var DOM_LOADED = false;
  
  /**
   * DomTracker Object
   * @constructor
   */
  var DomTracker = function() {};
  
  
  // interface
  DomTracker.prototype.create_properties = function() {};
  DomTracker.prototype.event_handler = function() {};
  DomTracker.prototype.after_track_handler = function() {};
  
  DomTracker.prototype.init = function(mixpanel_instance) {
      this.mp = mixpanel_instance;
      return this;
  };
  
  /**
   * @param {Object|string} query
   * @param {string} event_name
   * @param {Object=} properties
   * @param {function=} user_callback
   */
  DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
      var that = this;
      var elements = _.dom_query(query);
  
      if (elements.length === 0) {
          console.error('The DOM query (' + query + ') returned 0 elements');
          return;
      }
  
      _.each(elements, function(element) {
          _.register_event(element, this.override_event, function(e) {
              var options = {};
              var props = that.create_properties(properties, this);
              var timeout = that.mp.get_config('track_links_timeout');
  
              that.event_handler(e, this, options);
  
              // in case the mixpanel servers don't get back to us in time
              window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
  
              // fire the tracking event
              that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
          });
      }, this);
  
      return true;
  };
  
  /**
   * @param {function} user_callback
   * @param {Object} props
   * @param {boolean=} timeout_occured
   */
  DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
      timeout_occured = timeout_occured || false;
      var that = this;
  
      return function() {
          // options is referenced from both callbacks, so we can have
          // a 'lock' of sorts to ensure only one fires
          if (options.callback_fired) { return; }
          options.callback_fired = true;
  
          if (user_callback && user_callback(timeout_occured, props) === false) {
              // user can prevent the default functionality by
              // returning false from their callback
              return;
          }
  
          that.after_track_handler(props, options, timeout_occured);
      };
  };
  
  DomTracker.prototype.create_properties = function(properties, element) {
      var props;
  
      if (typeof(properties) === 'function') {
          props = properties(element);
      } else {
          props = _.extend({}, properties);
      }
  
      return props;
  };
  
  /**
   * LinkTracker Object
   * @constructor
   * @extends DomTracker
   */
  var LinkTracker = function() {
      this.override_event = 'click';
  };
  _.inherit(LinkTracker, DomTracker);
  
  LinkTracker.prototype.create_properties = function(properties, element) {
      var props = LinkTracker.superclass.create_properties.apply(this, arguments);
  
      if (element.href) { props['url'] = element.href; }
  
      return props;
  };
  
  LinkTracker.prototype.event_handler = function(evt, element, options) {
      options.new_tab = (
          evt.which === 2 ||
          evt.metaKey ||
          evt.ctrlKey ||
          element.target === '_blank'
      );
      options.href = element.href;
  
      if (!options.new_tab) {
          evt.preventDefault();
      }
  };
  
  LinkTracker.prototype.after_track_handler = function(props, options) {
      if (options.new_tab) { return; }
  
      setTimeout(function() {
          window.location = options.href;
      }, 0);
  };
  
  /**
   * FormTracker Object
   * @constructor
   * @extends DomTracker
   */
  var FormTracker = function() {
      this.override_event = 'submit';
  };
  _.inherit(FormTracker, DomTracker);
  
  FormTracker.prototype.event_handler = function(evt, element, options) {
      options.element = element;
      evt.preventDefault();
  };
  
  FormTracker.prototype.after_track_handler = function(props, options) {
      setTimeout(function() {
          options.element.submit();
      }, 0);
  };
  
  /**
   * Mixpanel Persistence Object
   * @constructor
   */
  var MixpanelPersistence = function(config) {
      this['props'] = {};
      this.campaign_params_saved = false;
  
      if (config['persistence_name']) {
          this.name = 'mp_' + config['persistence_name'];
      } else {
          this.name = 'mp_' + config['token'] + '_mixpanel';
      }
  
      var storage_type = config['persistence'];
      if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
          console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
          storage_type = config['persistence'] = 'cookie';
      }
  
      if (storage_type === 'localStorage' && _.localStorage.is_supported()) {
          this.storage = _.localStorage;
      } else {
          this.storage = _.cookie;
      }
  
      this.load();
      this.update_config(config);
      this.upgrade(config);
      this.save();
  };
  
  MixpanelPersistence.prototype.properties = function() {
      var p = {};
      // Filter out reserved properties
      _.each(this['props'], function(v, k) {
          if (!_.include(RESERVED_PROPERTIES, k)) {
              p[k] = v;
          }
      });
      return p;
  };
  
  MixpanelPersistence.prototype.load = function() {
      if (this.disabled) { return; }
  
      var entry = this.storage.parse(this.name);
  
      if (entry) {
          this['props'] = _.extend({}, entry);
      }
  };
  
  MixpanelPersistence.prototype.upgrade = function(config) {
      var upgrade_from_old_lib = config['upgrade'],
          old_cookie_name,
          old_cookie;
  
      if (upgrade_from_old_lib) {
          old_cookie_name = 'mp_super_properties';
          // Case where they had a custom cookie name before.
          if (typeof(upgrade_from_old_lib) === 'string') {
              old_cookie_name = upgrade_from_old_lib;
          }
  
          old_cookie = this.storage.parse(old_cookie_name);
  
          // remove the cookie
          this.storage.remove(old_cookie_name);
          this.storage.remove(old_cookie_name, true);
  
          if (old_cookie) {
              this['props'] = _.extend(
                  this['props'],
                  old_cookie['all'],
                  old_cookie['events']
              );
          }
      }
  
      if (!config['cookie_name'] && config['name'] !== 'mixpanel') {
          // special case to handle people with cookies of the form
          // mp_TOKEN_INSTANCENAME from the first release of this library
          old_cookie_name = 'mp_' + config['token'] + '_' + config['name'];
          old_cookie = this.storage.parse(old_cookie_name);
  
          if (old_cookie) {
              this.storage.remove(old_cookie_name);
              this.storage.remove(old_cookie_name, true);
  
              // Save the prop values that were in the cookie from before -
              // this should only happen once as we delete the old one.
              this.register_once(old_cookie);
          }
      }
  
      if (this.storage === _.localStorage) {
          old_cookie = _.cookie.parse(this.name);
  
          _.cookie.remove(this.name);
          _.cookie.remove(this.name, true);
  
          if (old_cookie) {
              this.register_once(old_cookie);
          }
      }
  };
  
  MixpanelPersistence.prototype.save = function() {
      if (this.disabled) { return; }
      this._expire_notification_campaigns();
      this.storage.set(
          this.name,
          _.JSONEncode(this['props']),
          this.expire_days,
          this.cross_subdomain,
          this.secure
      );
  };
  
  MixpanelPersistence.prototype.remove = function() {
      // remove both domain and subdomain cookies
      this.storage.remove(this.name, false);
      this.storage.remove(this.name, true);
  };
  
  // removes the storage entry and deletes all loaded data
  // forced name for tests
  MixpanelPersistence.prototype.clear = function() {
      this.remove();
      this['props'] = {};
  };
  
  /**
   * @param {Object} props
   * @param {*=} default_value
   * @param {number=} days
   */
  MixpanelPersistence.prototype.register_once = function(props, default_value, days) {
      if (_.isObject(props)) {
          if (typeof(default_value) === 'undefined') { default_value = 'None'; }
          this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
  
          _.each(props, function(val, prop) {
              if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) {
                  this['props'][prop] = val;
              }
          }, this);
  
          this.save();
  
          return true;
      }
      return false;
  };
  
  /**
   * @param {Object} props
   * @param {number=} days
   */
  MixpanelPersistence.prototype.register = function(props, days) {
      if (_.isObject(props)) {
          this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
  
          _.extend(this['props'], props);
  
          this.save();
  
          return true;
      }
      return false;
  };
  
  MixpanelPersistence.prototype.unregister = function(prop) {
      if (prop in this['props']) {
          delete this['props'][prop];
          this.save();
      }
  };
  
  MixpanelPersistence.prototype._expire_notification_campaigns = _.safewrap(function() {
      var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY],
          EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN)
      if (!campaigns_shown) {
          return;
      }
      for (var campaign_id in campaigns_shown) {
          if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) {
              delete campaigns_shown[campaign_id];
          }
      }
      if (_.isEmptyObject(campaigns_shown)) {
          delete this['props'][CAMPAIGN_IDS_KEY];
      }
  });
  
  MixpanelPersistence.prototype.update_campaign_params = function() {
      if (!this.campaign_params_saved) {
          this.register_once(_.info.campaignParams());
          this.campaign_params_saved = true;
      }
  };
  
  MixpanelPersistence.prototype.update_search_keyword = function(referrer) {
      this.register(_.info.searchInfo(referrer));
  };
  
  // EXPORTED METHOD, we test this directly.
  MixpanelPersistence.prototype.update_referrer_info = function(referrer) {
      // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
      this.register_once({
          '$initial_referrer': referrer || '$direct',
          '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct'
      }, '');
  };
  
  MixpanelPersistence.prototype.get_referrer_info = function() {
      return _.strip_empty_properties({
          '$initial_referrer': this['props']['$initial_referrer'],
          '$initial_referring_domain': this['props']['$initial_referring_domain']
      });
  };
  
  // safely fills the passed in object with stored properties,
  // does not override any properties defined in both
  // returns the passed in object
  MixpanelPersistence.prototype.safe_merge = function(props) {
      _.each(this['props'], function(val, prop) {
          if (!(prop in props)) {
              props[prop] = val;
          }
      });
  
      return props;
  };
  
  MixpanelPersistence.prototype.update_config = function(config) {
      this.default_expiry = this.expire_days = config['cookie_expiration'];
      this.set_disabled(config['disable_persistence']);
      this.set_cross_subdomain(config['cross_subdomain_cookie']);
      this.set_secure(config['secure_cookie']);
  };
  
  MixpanelPersistence.prototype.set_disabled = function(disabled) {
      this.disabled = disabled;
      if (this.disabled) {
          this.remove();
      } else {
          this.save();
      }
  };
  
  MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) {
      if (cross_subdomain !== this.cross_subdomain) {
          this.cross_subdomain = cross_subdomain;
          this.remove();
          this.save();
      }
  };
  
  MixpanelPersistence.prototype.get_cross_subdomain = function() {
      return this.cross_subdomain;
  };
  
  MixpanelPersistence.prototype.set_secure = function(secure) {
      if (secure !== this.secure) {
          this.secure = secure ? true : false;
          this.remove();
          this.save();
      }
  };
  
  MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
      var q_key = this._get_queue_key(queue),
          q_data = data[queue],
          set_q = this._get_or_create_queue(SET_ACTION),
          set_once_q = this._get_or_create_queue(SET_ONCE_ACTION),
          unset_q = this._get_or_create_queue(UNSET_ACTION),
          add_q = this._get_or_create_queue(ADD_ACTION),
          union_q = this._get_or_create_queue(UNION_ACTION),
          remove_q = this._get_or_create_queue(REMOVE_ACTION, []),
          append_q = this._get_or_create_queue(APPEND_ACTION, []);
  
      if (q_key === SET_QUEUE_KEY) {
          // Update the set queue - we can override any existing values
          _.extend(set_q, q_data);
          // if there was a pending increment, override it
          // with the set.
          this._pop_from_people_queue(ADD_ACTION, q_data);
          // if there was a pending union, override it
          // with the set.
          this._pop_from_people_queue(UNION_ACTION, q_data);
          this._pop_from_people_queue(UNSET_ACTION, q_data);
      } else if (q_key === SET_ONCE_QUEUE_KEY) {
          // only queue the data if there is not already a set_once call for it.
          _.each(q_data, function(v, k) {
              if (!(k in set_once_q)) {
                  set_once_q[k] = v;
              }
          });
          this._pop_from_people_queue(UNSET_ACTION, q_data);
      } else if (q_key === UNSET_QUEUE_KEY) {
          _.each(q_data, function(prop) {
  
              // undo previously-queued actions on this key
              _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) {
                  if (prop in enqueued_obj) {
                      delete enqueued_obj[prop];
                  }
              });
              _.each(append_q, function(append_obj) {
                  if (prop in append_obj) {
                      delete append_obj[prop];
                  }
              });
  
              unset_q[prop] = true;
  
          });
      } else if (q_key === ADD_QUEUE_KEY) {
          _.each(q_data, function(v, k) {
              // If it exists in the set queue, increment
              // the value
              if (k in set_q) {
                  set_q[k] += v;
              } else {
                  // If it doesn't exist, update the add
                  // queue
                  if (!(k in add_q)) {
                      add_q[k] = 0;
                  }
                  add_q[k] += v;
              }
          }, this);
          this._pop_from_people_queue(UNSET_ACTION, q_data);
      } else if (q_key === UNION_QUEUE_KEY) {
          _.each(q_data, function(v, k) {
              if (_.isArray(v)) {
                  if (!(k in union_q)) {
                      union_q[k] = [];
                  }
                  // We may send duplicates, the server will dedup them.
                  union_q[k] = union_q[k].concat(v);
              }
          });
          this._pop_from_people_queue(UNSET_ACTION, q_data);
      } else if (q_key === REMOVE_QUEUE_KEY) {
          remove_q.push(q_data);
          this._pop_from_people_queue(APPEND_ACTION, q_data);
      } else if (q_key === APPEND_QUEUE_KEY) {
          append_q.push(q_data);
          this._pop_from_people_queue(UNSET_ACTION, q_data);
      }
  
      console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
      console.log(data);
  
      this.save();
  };
  
  MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) {
      var q = this._get_queue(queue);
      if (!_.isUndefined(q)) {
          _.each(data, function(v, k) {
              if (queue === APPEND_ACTION || queue === REMOVE_ACTION) {
                  // list actions: only remove if both k+v match
                  // e.g. remove should not override append in a case like
                  // append({foo: 'bar'}); remove({foo: 'qux'})
                  _.each(q, function(queued_action) {
                      if (queued_action[k] === v) {
                          delete queued_action[k];
                      }
                  });
              } else {
                  delete q[k];
              }
          }, this);
  
          this.save();
      }
  };
  
  MixpanelPersistence.prototype._get_queue_key = function(queue) {
      if (queue === SET_ACTION) {
          return SET_QUEUE_KEY;
      } else if (queue === SET_ONCE_ACTION) {
          return SET_ONCE_QUEUE_KEY;
      } else if (queue === UNSET_ACTION) {
          return UNSET_QUEUE_KEY;
      } else if (queue === ADD_ACTION) {
          return ADD_QUEUE_KEY;
      } else if (queue === APPEND_ACTION) {
          return APPEND_QUEUE_KEY;
      } else if (queue === REMOVE_ACTION) {
          return REMOVE_QUEUE_KEY;
      } else if (queue === UNION_ACTION) {
          return UNION_QUEUE_KEY;
      } else {
          console.error('Invalid queue:', queue);
      }
  };
  
  MixpanelPersistence.prototype._get_queue = function(queue) {
      return this['props'][this._get_queue_key(queue)];
  };
  MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) {
      var key = this._get_queue_key(queue);
      default_val = _.isUndefined(default_val) ? {} : default_val;
  
      return this['props'][key] || (this['props'][key] = default_val);
  };
  
  MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) {
      var timers = this['props'][EVENT_TIMERS_KEY] || {};
      timers[event_name] = timestamp;
      this['props'][EVENT_TIMERS_KEY] = timers;
      this.save();
  };
  
  MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
      var timers = this['props'][EVENT_TIMERS_KEY] || {};
      var timestamp = timers[event_name];
      if (!_.isUndefined(timestamp)) {
          delete this['props'][EVENT_TIMERS_KEY][event_name];
          this.save();
      }
      return timestamp;
  };
  
  /**
   * Mixpanel Library Object
   * @constructor
   */
  var MixpanelLib = function() {};
  
  /**
   * Mixpanel People Object
   * @constructor
   */
  var MixpanelPeople = function() {};
  
  var MPNotif;
  
  
  _.extend(MixpanelPeople.prototype, apiActions);
  
  /**
   * create_mplib(token:string, config:object, name:string)
   *
   * This function is used by the init method of MixpanelLib objects
   * as well as the main initializer at the end of the JSLib (that
   * initializes document.mixpanel as well as any additional instances
   * declared before this file has loaded).
   */
  var create_mplib = function(token, config, name) {
      var instance,
          target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name];
  
      if (target && init_type === INIT_MODULE) {
          instance = target;
      } else {
          if (target && !_.isArray(target)) {
              console.error('You have already initialized ' + name);
              return;
          }
          instance = new MixpanelLib();
      }
  
      instance._init(token, config, name);
  
      instance['people'] = new MixpanelPeople();
      instance['people']._init(instance);
  
      instance._cached_groups = {}; // cache groups in a pool
  
      // if any instance on the page has debug = true, we set the
      // global debug to be true
      Config.DEBUG = Config.DEBUG || instance.get_config('debug');
  
      instance['__autotrack_enabled'] = instance.get_config('autotrack');
      if (instance.get_config('autotrack')) {
          var num_buckets = 100;
          var num_enabled_buckets = 100;
          if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
              instance['__autotrack_enabled'] = false;
              console.log('Not in active bucket: disabling Automatic Event Collection.');
          } else if (!autotrack.isBrowserSupported()) {
              instance['__autotrack_enabled'] = false;
              console.log('Disabling Automatic Event Collection because this browser is not supported');
          } else {
              autotrack.init(instance);
          }
      }
  
      // if target is not defined, we called init after the lib already
      // loaded, so there won't be an array of things to execute
      if (!_.isUndefined(target) && _.isArray(target)) {
          // Crunch through the people queue first - we queue this data up &
          // flush on identify, so it's better to do all these operations first
          instance._execute_array.call(instance['people'], target['people']);
          instance._execute_array(target);
      }
  
      return instance;
  };
  
  // Initialization methods
  
  /**
   * This function initializes a new instance of the Mixpanel tracking object.
   * All new instances are added to the main mixpanel object as sub properties (such as
   * mixpanel.library_name) and also returned by this function. To define a
   * second instance on the page, you would call:
   *
   *     mixpanel.init('new token', { your: 'config' }, 'library_name');
   *
   * and use it like so:
   *
   *     mixpanel.library_name.track(...);
   *
   * @param {String} token   Your Mixpanel API token
   * @param {Object} [config]  A dictionary of config options to override. <a href="https://github.com/mixpanel/mixpanel-js/blob/8b2e1f7b/src/mixpanel-core.js#L87-L110">See a list of default config options</a>.
   * @param {String} [name]    The name for the new mixpanel instance that you want created
   */
  MixpanelLib.prototype.init = function (token, config, name) {
      if (_.isUndefined(name)) {
          console.error('You must name your new library: init(token, config, name)');
          return;
      }
      if (name === PRIMARY_INSTANCE_NAME) {
          console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
          return;
      }
  
      var instance = create_mplib(token, config, name);
      mixpanel_master[name] = instance;
      instance._loaded();
  
      return instance;
  };
  
  // mixpanel._init(token:string, config:object, name:string)
  //
  // This function sets up the current instance of the mixpanel
  // library.  The difference between this method and the init(...)
  // method is this one initializes the actual instance, whereas the
  // init(...) method sets up a new library and calls _init on it.
  //
  MixpanelLib.prototype._init = function(token, config, name) {
      this['__loaded'] = true;
      this['config'] = {};
      this['_triggered_notifs'] = [];
  
      this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
          'name': name,
          'token': token,
          'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
      }));
  
      this['_jsc'] = function() {};
  
      this.__dom_loaded_queue = [];
      this.__request_queue = [];
      this.__disabled_events = [];
      this._flags = {
          'disable_all_events': false,
          'identify_called': false
      };
  
      this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']);
      this._gdpr_init();
  
      var uuid = _.UUID();
      if (!this.get_distinct_id()) {
          // There is no need to set the distinct id
          // or the device id if something was already stored
          // in the persitence
          this.register_once({
              'distinct_id': uuid,
              '$device_id': uuid
          }, '');
      }
  };
  
  // Private methods
  
  MixpanelLib.prototype._loaded = function() {
      this.get_config('loaded')(this);
  
      // this happens after so a user can call identify/name_tag in
      // the loaded callback
      if (this.get_config('track_pageview')) {
          this.track_pageview();
      }
  };
  
  MixpanelLib.prototype._dom_loaded = function() {
      _.each(this.__dom_loaded_queue, function(item) {
          this._track_dom.apply(this, item);
      }, this);
  
      if (!this.has_opted_out_tracking()) {
          _.each(this.__request_queue, function(item) {
              this._send_request.apply(this, item);
          }, this);
      }
  
      delete this.__dom_loaded_queue;
      delete this.__request_queue;
  };
  
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
      if (this.get_config('img')) {
          console.error('You can\'t use DOM tracking functions with img = true.');
          return false;
      }
  
      if (!DOM_LOADED) {
          this.__dom_loaded_queue.push([DomClass, args]);
          return false;
      }
  
      var dt = new DomClass().init(this);
      return dt.track.apply(dt, args);
  };
  
  /**
   * _prepare_callback() should be called by callers of _send_request for use
   * as the callback argument.
   *
   * If there is no callback, this returns null.
   * If we are going to make XHR/XDR requests, this returns a function.
   * If we are going to use script tags, this returns a string to use as the
   * callback GET param.
   */
  MixpanelLib.prototype._prepare_callback = function(callback, data) {
      if (_.isUndefined(callback)) {
          return null;
      }
  
      if (USE_XHR) {
          var callback_function = function(response) {
              callback(response, data);
          };
          return callback_function;
      } else {
          // if the user gives us a callback, we store as a random
          // property on this instances jsc function and update our
          // callback string to reflect that.
          var jsc = this['_jsc'];
          var randomized_cb = '' + Math.floor(Math.random() * 100000000);
          var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']';
          jsc[randomized_cb] = function(response) {
              delete jsc[randomized_cb];
              callback(response, data);
          };
          return callback_string;
      }
  };
  
  MixpanelLib.prototype._send_request = function(url, data, callback) {
      if (ENQUEUE_REQUESTS) {
          this.__request_queue.push(arguments);
          return;
      }
  
      // needed to correctly format responses
      var verbose_mode = this.get_config('verbose');
      if (data['verbose']) { verbose_mode = true; }
  
      if (this.get_config('test')) { data['test'] = 1; }
      if (verbose_mode) { data['verbose'] = 1; }
      if (this.get_config('img')) { data['img'] = 1; }
      if (!USE_XHR) {
          if (callback) {
              data['callback'] = callback;
          } else if (verbose_mode || this.get_config('test')) {
              // Verbose output (from verbose mode, or an error in test mode) is a json blob,
              // which by itself is not valid javascript. Without a callback, this verbose output will
              // cause an error when returned via jsonp, so we force a no-op callback param.
              // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4
              data['callback'] = '(function(){})';
          }
      }
  
      data['ip'] = this.get_config('ip')?1:0;
      data['_'] = new Date().getTime().toString();
      url += '?' + _.HTTPBuildQuery(data);
  
      if ('img' in data) {
          var img = document.createElement('img');
          img.src = url;
          document.body.appendChild(img);
      } else if (USE_XHR) {
          try {
              var req = new XMLHttpRequest();
              req.open('GET', url, true);
  
              var headers = this.get_config('xhr_headers');
              _.each(headers, function(headerValue, headerName) {
                  req.setRequestHeader(headerName, headerValue);
              });
  
              // send the mp_optout cookie
              // withCredentials cannot be modified until after calling .open on Android and Mobile Safari
              req.withCredentials = true;
              req.onreadystatechange = function () {
                  if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4
                      if (req.status === 200) {
                          if (callback) {
                              if (verbose_mode) {
                                  var response;
                                  try {
                                      response = _.JSONDecode(req.responseText);
                                  } catch (e) {
                                      console.error(e);
                                      return;
                                  }
                                  callback(response);
                              } else {
                                  callback(Number(req.responseText));
                              }
                          }
                      } else {
                          var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
                          console.error(error);
                          if (callback) {
                              if (verbose_mode) {
                                  callback({status: 0, error: error});
                              } else {
                                  callback(0);
                              }
                          }
                      }
                  }
              };
              req.send(null);
          } catch (e) {
              console.error(e);
          }
      } else {
          var script = document.createElement('script');
          script.type = 'text/javascript';
          script.async = true;
          script.defer = true;
          script.src = url;
          var s = document.getElementsByTagName('script')[0];
          s.parentNode.insertBefore(script, s);
      }
  };
  
  /**
   * _execute_array() deals with processing any mixpanel function
   * calls that were called before the Mixpanel library were loaded
   * (and are thus stored in an array so they can be called later)
   *
   * Note: we fire off all the mixpanel function calls && user defined
   * functions BEFORE we fire off mixpanel tracking calls. This is so
   * identify/register/set_config calls can properly modify early
   * tracking calls.
   *
   * @param {Array} array
   */
  MixpanelLib.prototype._execute_array = function(array) {
      var fn_name, alias_calls = [], other_calls = [], tracking_calls = [];
      _.each(array, function(item) {
          if (item) {
              fn_name = item[0];
              if (_.isArray(fn_name)) {
                  tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set()
              } else if (typeof(item) === 'function') {
                  item.call(this);
              } else if (_.isArray(item) && fn_name === 'alias') {
                  alias_calls.push(item);
              } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') {
                  tracking_calls.push(item);
              } else {
                  other_calls.push(item);
              }
          }
      }, this);
  
      var execute = function(calls, context) {
          _.each(calls, function(item) {
              if (_.isArray(item[0])) {
                  // chained call
                  var caller = context;
                  _.each(item, function(call) {
                      caller = caller[call[0]].apply(caller, call.slice(1));
                  });
              } else {
                  this[item[0]].apply(this, item.slice(1));
              }
          }, context);
      };
  
      execute(alias_calls, this);
      execute(other_calls, this);
      execute(tracking_calls, this);
  };
  
  /**
   * push() keeps the standard async-array-push
   * behavior around after the lib is loaded.
   * This is only useful for external integrations that
   * do not wish to rely on our convenience methods
   * (created in the snippet).
   *
   * ### Usage:
   *     mixpanel.push(['register', { a: 'b' }]);
   *
   * @param {Array} item A [function_name, args...] array to be executed
   */
  MixpanelLib.prototype.push = function(item) {
      this._execute_array([item]);
  };
  
  /**
   * Disable events on the Mixpanel object. If passed no arguments,
   * this function disables tracking of any event. If passed an
   * array of event names, those events will be disabled, but other
   * events will continue to be tracked.
   *
   * Note: this function does not stop other mixpanel functions from
   * firing, such as register() or people.set().
   *
   * @param {Array} [events] An array of event names to disable
   */
  MixpanelLib.prototype.disable = function(events) {
      if (typeof(events) === 'undefined') {
          this._flags.disable_all_events = true;
      } else {
          this.__disabled_events = this.__disabled_events.concat(events);
      }
  };
  
  /**
   * Track an event. This is the most important and
   * frequently used Mixpanel function.
   *
   * ### Usage:
   *
   *     // track an event named 'Registered'
   *     mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21});
   *
   * To track link clicks or form submissions, see track_links() or track_forms().
   *
   * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
   * @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.
   * @param {Function} [callback] If provided, the callback function will be called after tracking the event.
   */
  MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, callback) {
      if (typeof(callback) !== 'function') {
          callback = function() {};
      }
  
      if (_.isUndefined(event_name)) {
          console.error('No event name provided to mixpanel.track');
          return;
      }
  
      if (this._event_is_disabled(event_name)) {
          callback(0);
          return;
      }
  
      // set defaults
      properties = properties || {};
      properties['token'] = this.get_config('token');
  
      // set $duration if time_event was previously called for this event
      var start_timestamp = this['persistence'].remove_event_timer(event_name);
      if (!_.isUndefined(start_timestamp)) {
          var duration_in_ms = new Date().getTime() - start_timestamp;
          properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3));
      }
  
      // update persistence
      this['persistence'].update_search_keyword(document.referrer);
  
      if (this.get_config('store_google')) { this['persistence'].update_campaign_params(); }
      if (this.get_config('save_referrer')) { this['persistence'].update_referrer_info(document.referrer); }
  
      // note: extend writes to the first object, so lets make sure we
      // don't write to the persistence properties object and info
      // properties object by passing in a new object
  
      // update properties with pageview info and super-properties
      properties = _.extend(
          {},
          _.info.properties(),
          this['persistence'].properties(),
          properties
      );
  
      var property_blacklist = this.get_config('property_blacklist');
      if (_.isArray(property_blacklist)) {
          _.each(property_blacklist, function(blacklisted_prop) {
              delete properties[blacklisted_prop];
          });
      } else {
          console.error('Invalid value for property_blacklist config: ' + property_blacklist);
      }
  
      var data = {
          'event': event_name,
          'properties': properties
      };
      var truncated_data = _.truncate(data, 255);
      var json_data      = _.JSONEncode(truncated_data);
      var encoded_data   = _.base64Encode(json_data);
  
      console.log('MIXPANEL REQUEST:');
      console.log(truncated_data);
  
      this._send_request(
          this.get_config('api_host') + '/track/',
          { 'data': encoded_data },
          this._prepare_callback(callback, truncated_data)
      );
  
      this._check_and_handle_triggered_notifications(data);
  
      return truncated_data;
  });
  
  /**
   * Register the current user into one/many groups.
   *
   * ### Usage:
   *
   *      mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs
   *      mixpanel.set_group('company', 'mixpanel')
   *      mixpanel.set_group('company', 128746312)
   *
   * @param {String} group_key Group key
   * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   *
   */
  MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) {
      if (!_.isArray(group_ids)) {
          group_ids = [group_ids];
      }
      var prop = {};
      prop[group_key] = group_ids;
      this.register(prop);
      return this['people'].set(group_key, group_ids, callback);
  });
  
  /**
   * Add a new group for this user.
   *
   * ### Usage:
   *
   *      mixpanel.add_group('company', 'mixpanel')
   *
   * @param {String} group_key Group key
   * @param {*} group_id A valid Mixpanel property type
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) {
      var old_values = this.get_property(group_key);
      if (old_values === undefined) {
          var prop = {};
          prop[group_key] = [group_id];
          this.register(prop);
      } else {
          if (old_values.indexOf(group_id) === -1) {
              old_values.push(group_id);
              this.register(prop);
          }
      }
      return this['people'].union(group_key, group_id, callback);
  });
  
  /**
   * Remove a group from this user.
   *
   * ### Usage:
   *
   *      mixpanel.remove_group('company', 'mixpanel')
   *
   * @param {String} group_key Group key
   * @param {*} group_id A valid Mixpanel property type
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) {
      var old_value = this.get_property(group_key);
      // if the value doesn't exist, the persistent store is unchanged
      if (old_value !== undefined) {
          var idx = old_value.indexOf(group_id);
          if (idx > -1) {
              old_value.splice(idx, 1);
              this.register({group_key: old_value});
          }
          if (old_value.length === 0) {
              this.unregister(group_key);
          }
      }
      return this['people'].remove(group_key, group_id, callback);
  });
  
  /**
   * Track an event with specific groups.
   *
   * ### Usage:
   *
   *      mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']})
   *
   * @param {String} event_name The name of the event (see `mixpanel.track()`)
   * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`)
   * @param {Object=} groups An object mapping group name keys to one or more values
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) {
      var tracking_props = _.extend({}, properties || {});
      _.each(groups, function(v, k) {
          if (v !== null && v !== undefined) {
              tracking_props[k] = v;
          }
      });
      return this.track(event_name, tracking_props, callback);
  });
  
  MixpanelLib.prototype._create_map_key = function (group_key, group_id) {
      return group_key + '_' + JSON.stringify(group_id);
  };
  
  MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) {
      delete this._cached_groups[this._create_map_key(group_key, group_id)];
  };
  
  /**
   * Look up reference to a Mixpanel group
   *
   * ### Usage:
   *
   *       mixpanel.get_group(group_key, group_id)
   *
   * @param {String} group_key Group key
   * @param {Object} group_id A valid Mixpanel property type
   * @returns {Object} A MixpanelGroup identifier
   */
  MixpanelLib.prototype.get_group = function (group_key, group_id) {
      var map_key = this._create_map_key(group_key, group_id);
      var group = this._cached_groups[map_key];
      if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) {
          group = new MixpanelGroup();
          group._init(this, group_key, group_id);
          this._cached_groups[map_key] = group;
      }
      return group;
  };
  
  /**
   * Track a page view event, which is currently ignored by the server.
   * This function is called by default on page load unless the
   * track_pageview configuration variable is false.
   *
   * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
   * @api private
   */
  MixpanelLib.prototype.track_pageview = function(page) {
      if (_.isUndefined(page)) {
          page = document.location.href;
      }
      this.track('mp_page_view', _.info.pageviewInfo(page));
  };
  
  /**
   * Track clicks on a set of document elements. Selector must be a
   * valid query. Elements must exist on the page at the time track_links is called.
   *
   * ### Usage:
   *
   *     // track click for link id #nav
   *     mixpanel.track_links('#nav', 'Clicked Nav Link');
   *
   * ### Notes:
   *
   * This function will wait up to 300 ms for the Mixpanel
   * servers to respond. If they have not responded by that time
   * it will head to the link without ensuring that your event
   * has been tracked.  To configure this timeout please see the
   * set_config() documentation below.
   *
   * If you pass a function in as the properties argument, the
   * function will receive the DOMElement that triggered the
   * event as an argument.  You are expected to return an object
   * from the function; any properties defined on this object
   * will be sent to mixpanel as event properties.
   *
   * @type {Function}
   * @param {Object|String} query A valid DOM query, element or jQuery-esque list
   * @param {String} event_name The name of the event to track
   * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement
   */
  MixpanelLib.prototype.track_links = function() {
      return this._track_dom.call(this, LinkTracker, arguments);
  };
  
  /**
   * Track form submissions. Selector must be a valid query.
   *
   * ### Usage:
   *
   *     // track submission for form id 'register'
   *     mixpanel.track_forms('#register', 'Created Account');
   *
   * ### Notes:
   *
   * This function will wait up to 300 ms for the mixpanel
   * servers to respond, if they have not responded by that time
   * it will head to the link without ensuring that your event
   * has been tracked.  To configure this timeout please see the
   * set_config() documentation below.
   *
   * If you pass a function in as the properties argument, the
   * function will receive the DOMElement that triggered the
   * event as an argument.  You are expected to return an object
   * from the function; any properties defined on this object
   * will be sent to mixpanel as event properties.
   *
   * @type {Function}
   * @param {Object|String} query A valid DOM query, element or jQuery-esque list
   * @param {String} event_name The name of the event to track
   * @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
   */
  MixpanelLib.prototype.track_forms = function() {
      return this._track_dom.call(this, FormTracker, arguments);
  };
  
  /**
   * Time an event by including the time between this call and a
   * later 'track' call for the same event in the properties sent
   * with the event.
   *
   * ### Usage:
   *
   *     // time an event named 'Registered'
   *     mixpanel.time_event('Registered');
   *     mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21});
   *
   * When called for a particular event name, the next track call for that event
   * name will include the elapsed time between the 'time_event' and 'track'
   * calls. This value is stored as seconds in the '$duration' property.
   *
   * @param {String} event_name The name of the event.
   */
  MixpanelLib.prototype.time_event = function(event_name) {
      if (_.isUndefined(event_name)) {
          console.error('No event name provided to mixpanel.time_event');
          return;
      }
  
      if (this._event_is_disabled(event_name)) {
          return;
      }
  
      this['persistence'].set_event_timer(event_name,  new Date().getTime());
  };
  
  /**
   * Register a set of super properties, which are included with all
   * events. This will overwrite previous super property values.
   *
   * ### Usage:
   *
   *     // register 'Gender' as a super property
   *     mixpanel.register({'Gender': 'Female'});
   *
   *     // register several super properties when a user signs up
   *     mixpanel.register({
   *         'Email': 'jdoe@example.com',
   *         'Account Type': 'Free'
   *     });
   *
   * @param {Object} properties An associative array of properties to store about the user
   * @param {Number} [days] How many days since the user's last visit to store the super properties
   */
  MixpanelLib.prototype.register = function(props, days) {
      this['persistence'].register(props, days);
  };
  
  /**
   * Register a set of super properties only once. This will not
   * overwrite previous super property values, unlike register().
   *
   * ### Usage:
   *
   *     // register a super property for the first time only
   *     mixpanel.register_once({
   *         'First Login Date': new Date().toISOString()
   *     });
   *
   * ### Notes:
   *
   * If default_value is specified, current super properties
   * with that value will be overwritten.
   *
   * @param {Object} properties An associative array of properties to store about the user
   * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
   * @param {Number} [days] How many days since the users last visit to store the super properties
   */
  MixpanelLib.prototype.register_once = function(props, default_value, days) {
      this['persistence'].register_once(props, default_value, days);
  };
  
  /**
   * Delete a super property stored with the current user.
   *
   * @param {String} property The name of the super property to remove
   */
  MixpanelLib.prototype.unregister = function(property) {
      this['persistence'].unregister(property);
  };
  
  MixpanelLib.prototype._register_single = function(prop, value) {
      var props = {};
      props[prop] = value;
      this.register(props);
  };
  
  /**
   * Identify a user with a unique ID instead of a Mixpanel
   * randomly generated distinct_id. If the method is never called,
   * then unique visitors will be identified by a UUID generated
   * the first time they visit the site.
   *
   * ### Notes:
   *
   * You can call this function to overwrite a previously set
   * unique ID for the current user. Mixpanel cannot translate
   * between IDs at this time, so when you change a user's ID
   * they will appear to be a new user.
   *
   * When used alone, mixpanel.identify will change the user's
   * distinct_id to the unique ID provided. When used in tandem
   * with mixpanel.alias, it will allow you to identify based on
   * unique ID and map that back to the original, anonymous
   * distinct_id given to the user upon her first arrival to your
   * site (thus connecting anonymous pre-signup activity to
   * post-signup activity). Though the two work together, do not
   * call identify() at the same time as alias(). Calling the two
   * at the same time can cause a race condition, so it is best
   * practice to call identify on the original, anonymous ID
   * right after you've aliased it.
   * <a href="https://mixpanel.com/help/questions/articles/how-should-i-handle-my-user-identity-with-the-mixpanel-javascript-library">Learn more about how mixpanel.identify and mixpanel.alias can be used</a>.
   *
   * @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.
   */
  MixpanelLib.prototype.identify = function(
      unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
  ) {
      // Optional Parameters
      //  _set_callback:function  A callback to be run if and when the People set queue is flushed
      //  _add_callback:function  A callback to be run if and when the People add queue is flushed
      //  _append_callback:function  A callback to be run if and when the People append queue is flushed
      //  _set_once_callback:function  A callback to be run if and when the People set_once queue is flushed
      //  _union_callback:function  A callback to be run if and when the People union queue is flushed
      //  _unset_callback:function  A callback to be run if and when the People unset queue is flushed
  
      // identify only changes the distinct id if it doesn't match either the existing or the alias;
      // if it's new, blow away the alias as well.
      var distinct_id = this.get_distinct_id();
      this.register({'$user_id': unique_id});
  
      if (!this.get_property('$device_id')) {
          // The persisted distinct id might not actually be a device id at all
          // it might be a distinct id of the user from before
          var device_id = distinct_id;
          this.register_once({
              '$had_persisted_distinct_id': true,
              '$device_id': device_id
          }, '');
      }
  
      if (unique_id !== distinct_id && unique_id !== this.get_property(ALIAS_ID_KEY)) {
          this.unregister(ALIAS_ID_KEY);
          this.register({'distinct_id': unique_id});
      }
      this._check_and_handle_notifications(this.get_distinct_id());
      this._flags.identify_called = true;
      // Flush any queued up people requests
      this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback);
  };
  
  /**
   * Clears super properties and generates a new random distinct_id for this instance.
   * Useful for clearing data when a user logs out.
   */
  MixpanelLib.prototype.reset = function() {
      this['persistence'].clear();
      this._flags.identify_called = false;
      var uuid = _.UUID();
      this.register_once({
          'distinct_id': uuid,
          '$device_id': uuid
      }, '');
  };
  
  /**
   * Returns the current distinct id of the user. This is either the id automatically
   * generated by the library or the id that has been passed by a call to identify().
   *
   * ### Notes:
   *
   * get_distinct_id() can only be called after the Mixpanel library has finished loading.
   * init() has a loaded function available to handle this automatically. For example:
   *
   *     // set distinct_id after the mixpanel library has loaded
   *     mixpanel.init('YOUR PROJECT TOKEN', {
   *         loaded: function(mixpanel) {
   *             distinct_id = mixpanel.get_distinct_id();
   *         }
   *     });
   */
  MixpanelLib.prototype.get_distinct_id = function() {
      return this.get_property('distinct_id');
  };
  
  /**
   * Create an alias, which Mixpanel will use to link two distinct_ids going forward (not retroactively).
   * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the
   * following is a valid scenario:
   *
   *     mixpanel.alias('new_id', 'existing_id');
   *     ...
   *     mixpanel.alias('newer_id', 'new_id');
   *
   * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID.
   *
   * ### Notes:
   *
   * The best practice is to call alias() when a unique ID is first created for a user
   * (e.g., when a user first registers for an account and provides an email address).
   * alias() should never be called more than once for a given user, except to
   * chain a newer ID to a previously new ID, as described above.
   *
   * @param {String} alias A unique identifier that you want to use for this user in the future.
   * @param {String} [original] The current identifier being used for this user.
   */
  MixpanelLib.prototype.alias = function(alias, original) {
      // If the $people_distinct_id key exists in persistence, there has been a previous
      // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
      // this ID, as it will duplicate users.
      if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
          console.critical('Attempting to create alias for existing People user - aborting.');
          return -2;
      }
  
      var _this = this;
      if (_.isUndefined(original)) {
          original = this.get_distinct_id();
      }
      if (alias !== original) {
          this._register_single(ALIAS_ID_KEY, alias);
          return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() {
              // Flush the people queue
              _this.identify(alias);
          });
      } else {
          console.error('alias matches current distinct_id - skipping api call.');
          this.identify(alias);
          return -1;
      }
  };
  
  /**
   * Provide a string to recognize the user by. The string passed to
   * this method will appear in the Mixpanel Streams product rather
   * than an automatically generated name. Name tags do not have to
   * be unique.
   *
   * This value will only be included in Streams data.
   *
   * @param {String} name_tag A human readable name for the user
   * @api private
   */
  MixpanelLib.prototype.name_tag = function(name_tag) {
      this._register_single('mp_name_tag', name_tag);
  };
  
  /**
   * Update the configuration of a mixpanel library instance.
   *
   * The default config is:
   *
   *     {
   *       // super properties cookie expiration (in days)
   *       cookie_expiration: 365
   *
   *       // super properties span subdomains
   *       cross_subdomain_cookie: true
   *
   *       // debug mode
   *       debug: false
   *
   *       // if this is true, the mixpanel cookie or localStorage entry
   *       // will be deleted, and no user persistence will take place
   *       disable_persistence: false
   *
   *       // if this is true, Mixpanel will automatically determine
   *       // City, Region and Country data using the IP address of
   *       //the client
   *       ip: true
   *
   *       // opt users out of tracking by this Mixpanel instance by default
   *       opt_out_tracking_by_default: false
   *
   *       // opt users out of browser data storage by this Mixpanel instance by default
   *       opt_out_persistence_by_default: false
   *
   *       // persistence mechanism used by opt-in/opt-out methods - cookie
   *       // or localStorage - falls back to cookie if localStorage is unavailable
   *       opt_out_tracking_persistence_type: 'localStorage'
   *
   *       // customize the name of cookie/localStorage set by opt-in/opt-out methods
   *       opt_out_tracking_cookie_prefix: null
   *
   *       // type of persistent store for super properties (cookie/
   *       // localStorage) if set to 'localStorage', any existing
   *       // mixpanel cookie value with the same persistence_name
   *       // will be transferred to localStorage and deleted
   *       persistence: 'cookie'
   *
   *       // name for super properties persistent store
   *       persistence_name: ''
   *
   *       // names of properties/superproperties which should never
   *       // be sent with track() calls
   *       property_blacklist: []
   *
   *       // if this is true, mixpanel cookies will be marked as
   *       // secure, meaning they will only be transmitted over https
   *       secure_cookie: false
   *
   *       // the amount of time track_links will
   *       // wait for Mixpanel's servers to respond
   *       track_links_timeout: 300
   *
   *       // should we track a page view on page load
   *       track_pageview: true
   *
   *       // if you set upgrade to be true, the library will check for
   *       // a cookie from our old js library and import super
   *       // properties from it, then the old cookie is deleted
   *       // The upgrade config option only works in the initialization,
   *       // so make sure you set it when you create the library.
   *       upgrade: false
   *
   *       // extra HTTP request headers to set for each API request, in
   *       // the format {'Header-Name': value}
   *       xhr_headers: {}
   *
   *       // protocol for fetching in-app message resources, e.g.
   *       // 'https://' or 'http://'; defaults to '//' (which defers to the
   *       // current page's protocol)
   *       inapp_protocol: '//'
   *
   *       // whether to open in-app message link in new tab/window
   *       inapp_link_new_window: false
   *     }
   *
   *
   * @param {Object} config A dictionary of new configuration values to update
   */
  MixpanelLib.prototype.set_config = function(config) {
      if (_.isObject(config)) {
          _.extend(this['config'], config);
  
          if (!this.get_config('persistence_name')) {
              this['config']['persistence_name'] = this['config']['cookie_name'];
          }
          if (!this.get_config('disable_persistence')) {
              this['config']['disable_persistence'] = this['config']['disable_cookie'];
          }
  
          if (this['persistence']) {
              this['persistence'].update_config(this['config']);
          }
          Config.DEBUG = Config.DEBUG || this.get_config('debug');
      }
  };
  
  /**
   * returns the current config object for the library.
   */
  MixpanelLib.prototype.get_config = function(prop_name) {
      return this['config'][prop_name];
  };
  
  /**
   * Returns the value of the super property named property_name. If no such
   * property is set, get_property() will return the undefined value.
   *
   * ### Notes:
   *
   * get_property() can only be called after the Mixpanel library has finished loading.
   * init() has a loaded function available to handle this automatically. For example:
   *
   *     // grab value for 'user_id' after the mixpanel library has loaded
   *     mixpanel.init('YOUR PROJECT TOKEN', {
   *         loaded: function(mixpanel) {
   *             user_id = mixpanel.get_property('user_id');
   *         }
   *     });
   *
   * @param {String} property_name The name of the super property you want to retrieve
   */
  MixpanelLib.prototype.get_property = function(property_name) {
      return this['persistence']['props'][property_name];
  };
  
  MixpanelLib.prototype.toString = function() {
      var name = this.get_config('name');
      if (name !== PRIMARY_INSTANCE_NAME) {
          name = PRIMARY_INSTANCE_NAME + '.' + name;
      }
      return name;
  };
  
  MixpanelLib.prototype._event_is_disabled = function(event_name) {
      return _.isBlockedUA(userAgent) ||
          this._flags.disable_all_events ||
          _.include(this.__disabled_events, event_name);
  };
  
  MixpanelLib.prototype._check_and_handle_triggered_notifications = addOptOutCheckMixpanelLib(function(event_data) {
      var arr = this['_triggered_notifs'];
      for (var i = 0; i < arr.length; i++) {
          var notif = new MPNotif(arr[i], this);
          if (notif._matches_event_data(event_data)) {
              this._show_notification(arr[i]);
              return;
          }
      }
  });
  
  MixpanelLib.prototype._check_and_handle_notifications = addOptOutCheckMixpanelLib(function(distinct_id) {
      if (
          !distinct_id ||
          this._flags.identify_called ||
          this.get_config('disable_notifications')
      ) {
          return;
      }
  
      console.log('MIXPANEL NOTIFICATION CHECK');
  
      var data = {
          'verbose':     true,
          'version':     '3',
          'lib':         'web',
          'token':       this.get_config('token'),
          'distinct_id': distinct_id
      };
      this._send_request(
          this.get_config('api_host') + '/decide/',
          data,
          this._prepare_callback(_.bind(function(result) {
              if (result['notifications'] && result['notifications'].length > 0) {
                  this['_triggered_notifs'] = [];
                  var notifications = [];
                  _.each(result['notifications'], function(notif) {
                      (notif['display_triggers'] && notif['display_triggers'].length > 0 ? this['_triggered_notifs'] : notifications).push(notif);
                  }, this);
                  if (notifications.length > 0) {
                      this._show_notification.call(this, notifications[0]);
                  }
              }
          }, this))
      );
  });
  
  MixpanelLib.prototype._show_notification = function(notif_data) {
      var notification = new MPNotif(notif_data, this);
      notification.show();
  };
  
  // perform some housekeeping around GDPR opt-in/out state
  MixpanelLib.prototype._gdpr_init = function() {
      var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage';
  
      // try to convert opt-in/out cookies to localStorage if possible
      if (is_localStorage_requested && _.localStorage.is_supported()) {
          if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) {
              this.opt_in_tracking({'enable_persistence': false});
          }
          if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) {
              this.opt_out_tracking({'clear_persistence': false});
          }
          this.clear_opt_in_out_tracking({
              'persistence_type': 'cookie',
              'enable_persistence': false
          });
      }
  
      // check whether the user has already opted out - if so, clear & disable persistence
      if (this.has_opted_out_tracking()) {
          this._gdpr_update_persistence({'clear_persistence': true});
  
      // check whether we should opt out by default
      // note: we don't clear persistence here by default since opt-out default state is often
      //       used as an initial state while GDPR information is being collected
      } else if (!this.has_opted_in_tracking() && (
          this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout')
      )) {
          _.cookie.remove('mp_optout');
          this.opt_out_tracking({
              'clear_persistence': this.get_config('opt_out_persistence_by_default')
          });
      }
  };
  
  /**
   * Enable or disable persistence based on options
   * only enable/disable if persistence is not already in this state
   * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it
   * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence
   */
  MixpanelLib.prototype._gdpr_update_persistence = function(options) {
      var disabled;
      if (options && options['clear_persistence']) {
          disabled = true;
      } else if (options && options['enable_persistence']) {
          disabled = false;
      } else {
          return;
      }
  
      if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) {
          this['persistence'].set_disabled(disabled);
      }
  };
  
  // call a base gdpr function after constructing the appropriate token and options args
  MixpanelLib.prototype._gdpr_call_func = function(func, options) {
      options = _.extend({
          'track': _.bind(this.track, this),
          'persistence_type': this.get_config('opt_out_tracking_persistence_type'),
          'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'),
          'cookie_expiration': this.get_config('cookie_expiration'),
          'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'),
          'secure_cookie': this.get_config('secure_cookie')
      }, options);
  
      // check if localStorage can be used for recording opt out status, fall back to cookie if not
      if (!_.localStorage.is_supported()) {
          options['persistence_type'] = 'cookie';
      }
  
      return func(this.get_config('token'), {
          track: options['track'],
          trackEventName: options['track_event_name'],
          trackProperties: options['track_properties'],
          persistenceType: options['persistence_type'],
          persistencePrefix: options['cookie_prefix'],
          cookieExpiration: options['cookie_expiration'],
          crossSubdomainCookie: options['cross_subdomain_cookie'],
          secureCookie: options['secure_cookie']
      });
  };
  
  /**
   * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance
   *
   * ### Usage
   *
   *     // opt user in
   *     mixpanel.opt_in_tracking();
   *
   *     // opt user in with specific event name, properties, cookie configuration
   *     mixpanel.opt_in_tracking({
   *         track_event_name: 'User opted in',
   *         track_event_properties: {
   *             'Email': 'jdoe@example.com'
   *         },
   *         cookie_expiration: 30,
   *         secure_cookie: true
   *     });
   *
   * @param {Object} [options] A dictionary of config options to override
   * @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)
   * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action
   * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action
   * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
   * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
   * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
   * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config)
   * @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)
   * @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)
   */
  MixpanelLib.prototype.opt_in_tracking = function(options) {
      options = _.extend({
          'enable_persistence': true
      }, options);
  
      this._gdpr_call_func(optIn, options);
      this._gdpr_update_persistence(options);
  };
  
  /**
   * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance
   *
   * ### Usage
   *
   *     // opt user out
   *     mixpanel.opt_out_tracking();
   *
   *     // opt user out with different cookie configuration from Mixpanel instance
   *     mixpanel.opt_out_tracking({
   *         cookie_expiration: 30,
   *         secure_cookie: true
   *     });
   *
   * @param {Object} [options] A dictionary of config options to override
   * @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
   * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence
   * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
   * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
   * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config)
   * @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)
   * @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)
   */
  MixpanelLib.prototype.opt_out_tracking = function(options) {
      options = _.extend({
          'clear_persistence': true,
          'delete_user': true
      }, options);
  
      // delete use and clear charges since these methods may be disabled by opt-out
      if (options['delete_user'] && this['people'] && this['people']._identify_called()) {
          this['people'].delete_user();
          this['people'].clear_charges();
      }
  
      this._gdpr_call_func(optOut, options);
      this._gdpr_update_persistence(options);
  };
  
  /**
   * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance
   *
   * ### Usage
   *
   *     var has_opted_in = mixpanel.has_opted_in_tracking();
   *     // use has_opted_in value
   *
   * @param {Object} [options] A dictionary of config options to override
   * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
   * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
   * @returns {boolean} current opt-in status
   */
  MixpanelLib.prototype.has_opted_in_tracking = function(options) {
      return this._gdpr_call_func(hasOptedIn, options);
  };
  
  /**
   * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance
   *
   * ### Usage
   *
   *     var has_opted_out = mixpanel.has_opted_out_tracking();
   *     // use has_opted_out value
   *
   * @param {Object} [options] A dictionary of config options to override
   * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
   * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
   * @returns {boolean} current opt-out status
   */
  MixpanelLib.prototype.has_opted_out_tracking = function(options) {
      return this._gdpr_call_func(hasOptedOut, options);
  };
  
  /**
   * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance
   *
   * ### Usage
   *
   *     // clear user's opt-in/out status
   *     mixpanel.clear_opt_in_out_tracking();
   *
   *     // clear user's opt-in/out status with specific cookie configuration - should match
   *     // configuration used when opt_in_tracking/opt_out_tracking methods were called.
   *     mixpanel.clear_opt_in_out_tracking({
   *         cookie_expiration: 30,
   *         secure_cookie: true
   *     });
   *
   * @param {Object} [options] A dictionary of config options to override
   * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
   * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
   * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name
   * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config)
   * @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)
   * @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)
   */
  MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) {
      options = _.extend({
          'enable_persistence': true
      }, options);
  
      this._gdpr_call_func(clearOptInOut, options);
      this._gdpr_update_persistence(options);
  };
  
  
  MixpanelPeople.prototype._init = function(mixpanel_instance) {
      this._mixpanel = mixpanel_instance;
  };
  
  /*
   * Set properties on a user record.
   *
   * ### Usage:
   *
   *     mixpanel.people.set('gender', 'm');
   *
   *     // or set multiple properties at once
   *     mixpanel.people.set({
   *         'Company': 'Acme',
   *         'Plan': 'Premium',
   *         'Upgrade date': new Date()
   *     });
   *     // properties can be strings, integers, dates, or lists
   *
   * @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.
   * @param {*} [to] A value to set on the given property name
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
      var data = this.set_action(prop, to);
      if (_.isObject(prop)) {
          callback = to;
      }
      // make sure that the referrer info has been updated and saved
      if (this._get_config('save_referrer')) {
          this._mixpanel['persistence'].update_referrer_info(document.referrer);
      }
  
      // update $set object with default people properties
      data[SET_ACTION] = _.extend(
          {},
          _.info.people_properties(),
          this._mixpanel['persistence'].get_referrer_info(),
          data[SET_ACTION]
      );
      return this._send_request(data, callback);
  });
  
  /*
   * Set properties on a user record, only if they do not yet exist.
   * This will not overwrite previous people property values, unlike
   * people.set().
   *
   * ### Usage:
   *
   *     mixpanel.people.set_once('First Login Date', new Date());
   *
   *     // or set multiple properties at once
   *     mixpanel.people.set_once({
   *         'First Login Date': new Date(),
   *         'Starting Plan': 'Premium'
   *     });
   *
   *     // properties can be strings, integers or dates
   *
   * @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.
   * @param {*} [to] A value to set on the given property name
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
      var data = this.set_once_action(prop, to);
      if (_.isObject(prop)) {
          callback = to;
      }
      return this._send_request(data, callback);
  });
  
  /*
   * Unset properties on a user record (permanently removes the properties and their values from a profile).
   *
   * ### Usage:
   *
   *     mixpanel.people.unset('gender');
   *
   *     // or unset multiple properties at once
   *     mixpanel.people.unset(['gender', 'Company']);
   *
   * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names.
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) {
      var data = this.unset_action(prop);
      return this._send_request(data, callback);
  });
  
  /*
   * Increment/decrement numeric people analytics properties.
   *
   * ### Usage:
   *
   *     mixpanel.people.increment('page_views', 1);
   *
   *     // or, for convenience, if you're just incrementing a counter by
   *     // 1, you can simply do
   *     mixpanel.people.increment('page_views');
   *
   *     // to decrement a counter, pass a negative number
   *     mixpanel.people.increment('credits_left', -1);
   *
   *     // like mixpanel.people.set(), you can increment multiple
   *     // properties at once:
   *     mixpanel.people.increment({
   *         counter1: 1,
   *         counter2: 6
   *     });
   *
   * @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.
   * @param {Number} [by] An amount to increment the given property
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) {
      var data = {};
      var $add = {};
      if (_.isObject(prop)) {
          _.each(prop, function(v, k) {
              if (!this._is_reserved_property(k)) {
                  if (isNaN(parseFloat(v))) {
                      console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
                      return;
                  } else {
                      $add[k] = v;
                  }
              }
          }, this);
          callback = by;
      } else {
          // convenience: mixpanel.people.increment('property'); will
          // increment 'property' by 1
          if (_.isUndefined(by)) {
              by = 1;
          }
          $add[prop] = by;
      }
      data[ADD_ACTION] = $add;
  
      return this._send_request(data, callback);
  });
  
  /*
   * Append a value to a list-valued people analytics property.
   *
   * ### Usage:
   *
   *     // append a value to a list, creating it if needed
   *     mixpanel.people.append('pages_visited', 'homepage');
   *
   *     // like mixpanel.people.set(), you can append multiple
   *     // properties at once:
   *     mixpanel.people.append({
   *         list1: 'bob',
   *         list2: 123
   *     });
   *
   * @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.
   * @param {*} [value] value An item to append to the list
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
      if (_.isObject(list_name)) {
          callback = value;
      }
      var data = this.append_action(list_name, value);
      return this._send_request(data, callback);
  });
  
  /*
   * Remove a value from a list-valued people analytics property.
   *
   * ### Usage:
   *
   *     mixpanel.people.remove('School', 'UCB');
   *
   * @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.
   * @param {*} [value] value Item to remove from the list
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
      if (_.isObject(list_name)) {
          callback = value;
      }
      var data = this.remove_action(list_name, value);
      return this._send_request(data, callback);
  });
  
  /*
   * Merge a given list with a list-valued people analytics property,
   * excluding duplicate values.
   *
   * ### Usage:
   *
   *     // merge a value to a list, creating it if needed
   *     mixpanel.people.union('pages_visited', 'homepage');
   *
   *     // like mixpanel.people.set(), you can append multiple
   *     // properties at once:
   *     mixpanel.people.union({
   *         list1: 'bob',
   *         list2: 123
   *     });
   *
   *     // like mixpanel.people.append(), you can append multiple
   *     // values to the same list:
   *     mixpanel.people.union({
   *         list1: ['bob', 'billy']
   *     });
   *
   * @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.
   * @param {*} [value] Value / values to merge with the given property
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) {
      if (_.isObject(list_name)) {
          callback = values;
      }
      var data = this.union_action(list_name, values);
      return this._send_request(data, callback);
  });
  
  /*
   * Record that you have charged the current user a certain amount
   * of money. Charges recorded with track_charge() will appear in the
   * Mixpanel revenue report.
   *
   * ### Usage:
   *
   *     // charge a user $50
   *     mixpanel.people.track_charge(50);
   *
   *     // charge a user $30.50 on the 2nd of january
   *     mixpanel.people.track_charge(30.50, {
   *         '$time': new Date('jan 1 2012')
   *     });
   *
   * @param {Number} amount The amount of money charged to the current user
   * @param {Object} [properties] An associative array of properties associated with the charge
   * @param {Function} [callback] If provided, the callback will be called when the server responds
   */
  MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) {
      if (!_.isNumber(amount)) {
          amount = parseFloat(amount);
          if (isNaN(amount)) {
              console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
              return;
          }
      }
  
      return this.append('$transactions', _.extend({
          '$amount': amount
      }, properties), callback);
  });
  
  /*
   * Permanently clear all revenue report transactions from the
   * current user's people analytics profile.
   *
   * ### Usage:
   *
   *     mixpanel.people.clear_charges();
   *
   * @param {Function} [callback] If provided, the callback will be called after tracking the event.
   */
  MixpanelPeople.prototype.clear_charges = function(callback) {
      return this.set('$transactions', [], callback);
  };
  
  /*
   * Permanently deletes the current people analytics profile from
   * Mixpanel (using the current distinct_id).
   *
   * ### Usage:
   *
   *     // remove the all data you have stored about the current user
   *     mixpanel.people.delete_user();
   *
   */
  MixpanelPeople.prototype.delete_user = function() {
      if (!this._identify_called()) {
          console.error('mixpanel.people.delete_user() requires you to call identify() first');
          return;
      }
      var data = {'$delete': this._mixpanel.get_distinct_id()};
      return this._send_request(data);
  };
  
  MixpanelPeople.prototype.toString = function() {
      return this._mixpanel.toString() + '.people';
  };
  
  MixpanelPeople.prototype._send_request = function(data, callback) {
      data['$token'] = this._get_config('token');
      data['$distinct_id'] = this._mixpanel.get_distinct_id();
      var device_id = this._mixpanel.get_property('$device_id');
      var user_id = this._mixpanel.get_property('$user_id');
      var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id');
      if (device_id) {
          data['$device_id'] = device_id;
      }
      if (user_id) {
          data['$user_id'] = user_id;
      }
      if (had_persisted_distinct_id) {
          data['$had_persisted_distinct_id'] = had_persisted_distinct_id;
      }
  
      var date_encoded_data = _.encodeDates(data);
      var truncated_data    = _.truncate(date_encoded_data, 255);
      var json_data         = _.JSONEncode(date_encoded_data);
      var encoded_data      = _.base64Encode(json_data);
  
      if (!this._identify_called()) {
          this._enqueue(data);
          if (!_.isUndefined(callback)) {
              if (this._get_config('verbose')) {
                  callback({status: -1, error: null});
              } else {
                  callback(-1);
              }
          }
          return truncated_data;
      }
  
      console.log('MIXPANEL PEOPLE REQUEST:');
      console.log(truncated_data);
  
      this._mixpanel._send_request(
          this._get_config('api_host') + '/engage/',
          {'data': encoded_data},
          this._mixpanel._prepare_callback(callback, truncated_data)
      );
  
      return truncated_data;
  };
  
  MixpanelPeople.prototype._get_config = function(conf_var) {
      return this._mixpanel.get_config(conf_var);
  };
  
  MixpanelPeople.prototype._identify_called = function() {
      return this._mixpanel._flags.identify_called === true;
  };
  
  // Queue up engage operations if identify hasn't been called yet.
  MixpanelPeople.prototype._enqueue = function(data) {
      if (SET_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data);
      } else if (SET_ONCE_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data);
      } else if (UNSET_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data);
      } else if (ADD_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data);
      } else if (APPEND_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data);
      } else if (REMOVE_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data);
      } else if (UNION_ACTION in data) {
          this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
      } else {
          console.error('Invalid call to _enqueue():', data);
      }
  };
  
  MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) {
      var _this = this;
      var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action));
      var action_params = queued_data;
  
      if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) {
          _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data);
          if (queue_to_params_fn) {
              action_params = queue_to_params_fn(queued_data);
          }
          action_method.call(_this, action_params, function(response, data) {
              // on bad response, we want to add it back to the queue
              if (response === 0) {
                  _this._mixpanel['persistence']._add_to_people_queue(action, queued_data);
              }
              if (!_.isUndefined(callback)) {
                  callback(response, data);
              }
          });
      }
  };
  
  // Flush queued engage operations - order does not matter,
  // and there are network level race conditions anyway
  MixpanelPeople.prototype._flush = function(
      _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
  ) {
      var _this = this;
      var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION);
      var $remove_queue = this._mixpanel['persistence']._get_queue(REMOVE_ACTION);
  
      this._flush_one_queue(SET_ACTION, this.set, _set_callback);
      this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback);
      this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); });
      this._flush_one_queue(ADD_ACTION, this.increment, _add_callback);
      this._flush_one_queue(UNION_ACTION, this.union, _union_callback);
  
      // we have to fire off each $append individually since there is
      // no concat method server side
      if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
          var $append_item;
          var append_callback = function(response, data) {
              if (response === 0) {
                  _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item);
              }
              if (!_.isUndefined(_append_callback)) {
                  _append_callback(response, data);
              }
          };
          for (var i = $append_queue.length - 1; i >= 0; i--) {
              $append_item = $append_queue.pop();
              if (!_.isEmptyObject($append_item)) {
                  _this.append($append_item, append_callback);
              }
          }
          // Save the shortened append queue
          _this._mixpanel['persistence'].save();
      }
  
      // same for $remove
      if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) {
          var $remove_item;
          var remove_callback = function(response, data) {
              if (response === 0) {
                  _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item);
              }
              if (!_.isUndefined(_remove_callback)) {
                  _remove_callback(response, data);
              }
          };
          for (var j = $remove_queue.length - 1; j >= 0; j--) {
              $remove_item = $remove_queue.pop();
              if (!_.isEmptyObject($remove_item)) {
                  _this.remove($remove_item, remove_callback);
              }
          }
          _this._mixpanel['persistence'].save();
      }
  };
  
  MixpanelPeople.prototype._is_reserved_property = function(prop) {
      return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id';
  };
  
  
  // Internal class for notification display
  MixpanelLib._Notification = function(notif_data, mixpanel_instance) {
      _.bind_instance_methods(this);
  
      this.mixpanel          = mixpanel_instance;
      this.persistence       = this.mixpanel['persistence'];
      this.resource_protocol = this.mixpanel.get_config('inapp_protocol');
      this.cdn_host          = this.mixpanel.get_config('cdn');
  
      this.campaign_id = _.escapeHTML(notif_data['id']);
      this.message_id  = _.escapeHTML(notif_data['message_id']);
  
      this.body            = (_.escapeHTML(notif_data['body']) || '').replace(/\n/g, '<br/>');
      this.cta             = _.escapeHTML(notif_data['cta']) || 'Close';
      this.notif_type      = _.escapeHTML(notif_data['type']) || 'takeover';
      this.style           = _.escapeHTML(notif_data['style']) || 'light';
      this.title           = _.escapeHTML(notif_data['title']) || '';
      this.video_width     = MPNotif.VIDEO_WIDTH;
      this.video_height    = MPNotif.VIDEO_HEIGHT;
  
      this.display_triggers = notif_data['display_triggers'] || [];
  
      // These fields are url-sanitized in the backend already.
      this.dest_url        = notif_data['cta_url'] || null;
      this.image_url       = notif_data['image_url'] || null;
      this.thumb_image_url = notif_data['thumb_image_url'] || null;
      this.video_url       = notif_data['video_url'] || null;
  
      if (this.thumb_image_url && this.thumb_image_url.indexOf('//') === 0) {
          this.thumb_image_url = this.thumb_image_url.replace('//', this.resource_protocol);
      }
  
      this.clickthrough = true;
      if (!this.dest_url) {
          this.dest_url = '#dismiss';
          this.clickthrough = false;
      }
  
      this.mini = this.notif_type === 'mini';
      if (!this.mini) {
          this.notif_type = 'takeover';
      }
      this.notif_width = !this.mini ? MPNotif.NOTIF_WIDTH : MPNotif.NOTIF_WIDTH_MINI;
  
      this._set_client_config();
      this.imgs_to_preload = this._init_image_html();
      this._init_video();
  };
  
  MPNotif = MixpanelLib._Notification;
  
  MPNotif.ANIM_TIME         = 200;
  MPNotif.MARKUP_PREFIX     = 'mixpanel-notification';
  MPNotif.BG_OPACITY        = 0.6;
  MPNotif.NOTIF_TOP         = 25;
  MPNotif.NOTIF_START_TOP   = 200;
  MPNotif.NOTIF_WIDTH       = 388;
  MPNotif.NOTIF_WIDTH_MINI  = 420;
  MPNotif.NOTIF_HEIGHT_MINI = 85;
  MPNotif.THUMB_BORDER_SIZE = 5;
  MPNotif.THUMB_IMG_SIZE    = 60;
  MPNotif.THUMB_OFFSET      = Math.round(MPNotif.THUMB_IMG_SIZE / 2);
  MPNotif.VIDEO_WIDTH       = 595;
  MPNotif.VIDEO_HEIGHT      = 334;
  
  MPNotif.prototype.show = function() {
      var self = this;
      this._set_client_config();
  
      // don't display until HTML body exists
      if (!this.body_el) {
          setTimeout(function() { self.show(); }, 300);
          return;
      }
  
      this._init_styles();
      this._init_notification_el();
  
      // wait for any images to load before showing notification
      this._preload_images(this._attach_and_animate);
  };
  
  MPNotif.prototype.dismiss = _.safewrap(function() {
      if (!this.marked_as_shown) {
          // unexpected condition: user interacted with notif even though we didn't consider it
          // visible (see _mark_as_shown()); send tracking signals to mark delivery
          this._mark_delivery({'invisible': true});
      }
  
      var exiting_el = this.showing_video ? this._get_el('video') : this._get_notification_display_el();
      if (this.use_transitions) {
          this._remove_class('bg', 'visible');
          this._add_class(exiting_el, 'exiting');
          setTimeout(this._remove_notification_el, MPNotif.ANIM_TIME);
      } else {
          var notif_attr, notif_start, notif_goal;
          if (this.mini) {
              notif_attr  = 'right';
              notif_start = 20;
              notif_goal  = -100;
          } else {
              notif_attr  = 'top';
              notif_start = MPNotif.NOTIF_TOP;
              notif_goal  = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP;
          }
          this._animate_els([
              {
                  el:    this._get_el('bg'),
                  attr:  'opacity',
                  start: MPNotif.BG_OPACITY,
                  goal:  0.0
              },
              {
                  el:    exiting_el,
                  attr:  'opacity',
                  start: 1.0,
                  goal:  0.0
              },
              {
                  el:    exiting_el,
                  attr:  notif_attr,
                  start: notif_start,
                  goal:  notif_goal
              }
          ], MPNotif.ANIM_TIME, this._remove_notification_el);
      }
  });
  
  MPNotif.prototype._add_class = _.safewrap(function(el, class_name) {
      class_name = MPNotif.MARKUP_PREFIX + '-' + class_name;
      if (typeof el === 'string') {
          el = this._get_el(el);
      }
      if (!el.className) {
          el.className = class_name;
      } else if (!~(' ' + el.className + ' ').indexOf(' ' + class_name + ' ')) {
          el.className += ' ' + class_name;
      }
  });
  MPNotif.prototype._remove_class = _.safewrap(function(el, class_name) {
      class_name = MPNotif.MARKUP_PREFIX + '-' + class_name;
      if (typeof el === 'string') {
          el = this._get_el(el);
      }
      if (el.className) {
          el.className = (' ' + el.className + ' ')
                  .replace(' ' + class_name + ' ', '')
                  .replace(/^[\s\xA0]+/, '')
                  .replace(/[\s\xA0]+$/, '');
      }
  });
  
  MPNotif.prototype._animate_els = _.safewrap(function(anims, mss, done_cb, start_time) {
      var self = this,
          in_progress = false,
          ai, anim,
          cur_time = 1 * new Date(), time_diff;
  
      start_time = start_time || cur_time;
      time_diff = cur_time - start_time;
  
      for (ai = 0; ai < anims.length; ai++) {
          anim = anims[ai];
          if (typeof anim.val === 'undefined') {
              anim.val = anim.start;
          }
          if (anim.val !== anim.goal) {
              in_progress = true;
              var anim_diff = anim.goal - anim.start,
                  anim_dir = anim.goal >= anim.start ? 1 : -1;
              anim.val = anim.start + anim_diff * time_diff / mss;
              if (anim.attr !== 'opacity') {
                  anim.val = Math.round(anim.val);
              }
              if ((anim_dir > 0 && anim.val >= anim.goal) || (anim_dir < 0 && anim.val <= anim.goal)) {
                  anim.val = anim.goal;
              }
          }
      }
      if (!in_progress) {
          if (done_cb) {
              done_cb();
          }
          return;
      }
  
      for (ai = 0; ai < anims.length; ai++) {
          anim = anims[ai];
          if (anim.el) {
              var suffix = anim.attr === 'opacity' ? '' : 'px';
              anim.el.style[anim.attr] = String(anim.val) + suffix;
          }
      }
      setTimeout(function() { self._animate_els(anims, mss, done_cb, start_time); }, 10);
  });
  
  MPNotif.prototype._attach_and_animate = _.safewrap(function() {
      var self = this;
  
      // no possibility to double-display
      if (this.shown || this._get_shown_campaigns()[this.campaign_id]) {
          return;
      }
      this.shown = true;
  
      this.body_el.appendChild(this.notification_el);
      setTimeout(function() {
          var notif_el = self._get_notification_display_el();
          if (self.use_transitions) {
              if (!self.mini) {
                  self._add_class('bg', 'visible');
              }
              self._add_class(notif_el, 'visible');
              self._mark_as_shown();
          } else {
              var notif_attr, notif_start, notif_goal;
              if (self.mini) {
                  notif_attr  = 'right';
                  notif_start = -100;
                  notif_goal  = 20;
              } else {
                  notif_attr  = 'top';
                  notif_start = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP;
                  notif_goal  = MPNotif.NOTIF_TOP;
              }
              self._animate_els([
                  {
                      el:    self._get_el('bg'),
                      attr:  'opacity',
                      start: 0.0,
                      goal:  MPNotif.BG_OPACITY
                  },
                  {
                      el:    notif_el,
                      attr:  'opacity',
                      start: 0.0,
                      goal:  1.0
                  },
                  {
                      el:    notif_el,
                      attr:  notif_attr,
                      start: notif_start,
                      goal:  notif_goal
                  }
              ], MPNotif.ANIM_TIME, self._mark_as_shown);
          }
      }, 100);
      _.register_event(self._get_el('cancel'), 'click', function(e) {
          e.preventDefault();
          self.dismiss();
      });
      var click_el = self._get_el('button') ||
                         self._get_el('mini-content');
      _.register_event(click_el, 'click', function(e) {
          e.preventDefault();
          if (self.show_video) {
              self._track_event('$campaign_open', {'$resource_type': 'video'});
              self._switch_to_video();
          } else {
              self.dismiss();
              if (self.clickthrough) {
                  var tracking_cb = null;
                  if (self.mixpanel.get_config('inapp_link_new_window')) {
                      window.open(self.dest_url);
                  } else {
                      tracking_cb = function() {
                          window.location.href = self.dest_url;
                      };
                  }
                  self._track_event('$campaign_open', {'$resource_type': 'link'}, tracking_cb);
              }
          }
      });
  });
  
  MPNotif.prototype._get_el = function(id) {
      return document.getElementById(MPNotif.MARKUP_PREFIX + '-' + id);
  };
  
  MPNotif.prototype._get_notification_display_el = function() {
      return this._get_el(this.notif_type);
  };
  
  MPNotif.prototype._get_shown_campaigns = function() {
      return this.persistence['props'][CAMPAIGN_IDS_KEY] || (this.persistence['props'][CAMPAIGN_IDS_KEY] = {});
  };
  
  MPNotif.prototype._matches_event_data = _.safewrap(function(event_data) {
      var event_name = event_data['event'] || '';
      for (var i = 0; i < this.display_triggers.length; i++) {
          var display_trigger = this.display_triggers[i];
          var match_event = display_trigger['event'] || '';
          if (match_event === '$any_event' || event_name === display_trigger['event']) {
              if (display_trigger['selector'] && !_.isEmptyObject(display_trigger['selector'])) {
                  if (evaluateSelector(display_trigger['selector'], event_data['properties'])) {
                      return true;
                  }
              } else {
                  return true;
              }
          }
      }
      return false;
  });
  
  
  MPNotif.prototype._browser_lte = function(browser, version) {
      return this.browser_versions[browser] && this.browser_versions[browser] <= version;
  };
  
  MPNotif.prototype._init_image_html = function() {
      var imgs_to_preload = [];
  
      if (!this.mini) {
          if (this.image_url) {
              imgs_to_preload.push(this.image_url);
              this.img_html = '<img id="img" src="' + this.image_url + '"/>';
          } else {
              this.img_html = '';
          }
          if (this.thumb_image_url) {
              imgs_to_preload.push(this.thumb_image_url);
              this.thumb_img_html =
                      '<div id="thumbborder-wrapper"><div id="thumbborder"></div></div>' +
                      '<img id="thumbnail"' +
                          ' src="' + this.thumb_image_url + '"' +
                          ' width="' + MPNotif.THUMB_IMG_SIZE + '"' +
                          ' height="' + MPNotif.THUMB_IMG_SIZE + '"' +
                      '/>' +
                      '<div id="thumbspacer"></div>';
          } else {
              this.thumb_img_html = '';
          }
      } else {
          this.thumb_image_url = this.thumb_image_url || (this.cdn_host + '/site_media/images/icons/notifications/mini-news-dark.png');
          imgs_to_preload.push(this.thumb_image_url);
      }
  
      return imgs_to_preload;
  };
  
  MPNotif.prototype._init_notification_el = function() {
      var notification_html = '';
      var video_src         = '';
      var video_html        = '';
      var cancel_html       = '<div id="cancel">' +
                                      '<div id="cancel-icon"></div>' +
                                  '</div>';
  
      this.notification_el = document.createElement('div');
      this.notification_el.id = MPNotif.MARKUP_PREFIX + '-wrapper';
      if (!this.mini) {
          // TAKEOVER notification
          var close_html  = (this.clickthrough || this.show_video) ? '' : '<div id="button-close"></div>',
              play_html   = this.show_video ? '<div id="button-play"></div>' : '';
          if (this._browser_lte('ie', 7)) {
              close_html = '';
              play_html = '';
          }
          notification_html =
                  '<div id="takeover">' +
                      this.thumb_img_html +
                      '<div id="mainbox">' +
                          cancel_html +
                          '<div id="content">' +
                              this.img_html +
                              '<div id="title">' + this.title + '</div>' +
                              '<div id="body">' + this.body + '</div>' +
                              '<div id="tagline">' +
                                  '<a href="http://mixpanel.com?from=inapp" target="_blank">POWERED BY MIXPANEL</a>' +
                              '</div>' +
                          '</div>' +
                          '<div id="button">' +
                              close_html +
                              '<a id="button-link" href="' + this.dest_url + '">' + this.cta + '</a>' +
                              play_html +
                          '</div>' +
                      '</div>' +
                  '</div>';
      } else {
          // MINI notification
          notification_html =
                  '<div id="mini">' +
                      '<div id="mainbox">' +
                          cancel_html +
                          '<div id="mini-content">' +
                              '<div id="mini-icon">' +
                                  '<div id="mini-icon-img"></div>' +
                              '</div>' +
                              '<div id="body">' +
                                  '<div id="body-text"><div>' + this.body + '</div></div>' +
                              '</div>' +
                          '</div>' +
                      '</div>' +
                      '<div id="mini-border"></div>' +
                  '</div>';
      }
      if (this.youtube_video) {
          video_src = this.resource_protocol + 'www.youtube.com/embed/' + this.youtube_video +
                  '?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080';
          if (this.yt_custom) {
              video_src += '&enablejsapi=1&html5=1&controls=0';
              video_html =
                      '<div id="video-controls">' +
                          '<div id="video-progress" class="video-progress-el">' +
                              '<div id="video-progress-total" class="video-progress-el"></div>' +
                              '<div id="video-elapsed" class="video-progress-el"></div>' +
                          '</div>' +
                          '<div id="video-time" class="video-progress-el"></div>' +
                      '</div>';
          }
      } else if (this.vimeo_video) {
          video_src = this.resource_protocol + 'player.vimeo.com/video/' + this.vimeo_video + '?autoplay=1&title=0&byline=0&portrait=0';
      }
      if (this.show_video) {
          this.video_iframe =
                  '<iframe id="' + MPNotif.MARKUP_PREFIX + '-video-frame" ' +
                      'width="' + this.video_width + '" height="' + this.video_height + '" ' +
                      ' src="' + video_src + '"' +
                      ' frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen="1" scrolling="no"' +
                  '></iframe>';
          video_html =
                  '<div id="video-' + (this.flip_animate ? '' : 'no') + 'flip">' +
                      '<div id="video">' +
                          '<div id="video-holder"></div>' +
                          video_html +
                      '</div>' +
                  '</div>';
      }
      var main_html = video_html + notification_html;
      if (this.flip_animate) {
          main_html =
                  (this.mini ? notification_html : '') +
                  '<div id="flipcontainer"><div id="flipper">' +
                      (this.mini ? video_html : main_html) +
                  '</div></div>';
      }
  
      this.notification_el.innerHTML =
              ('<div id="overlay" class="' + this.notif_type + '">' +
                  '<div id="campaignid-' + this.campaign_id + '">' +
                      '<div id="bgwrapper">' +
                          '<div id="bg"></div>' +
                          main_html +
                      '</div>' +
                  '</div>' +
              '</div>')
              .replace(/class=\"/g, 'class="' + MPNotif.MARKUP_PREFIX + '-')
              .replace(/id=\"/g, 'id="' + MPNotif.MARKUP_PREFIX + '-');
  };
  
  MPNotif.prototype._init_styles = function() {
      if (this.style === 'dark') {
          this.style_vals = {
              bg:             '#1d1f25',
              bg_actions:     '#282b32',
              bg_hover:       '#3a4147',
              bg_light:       '#4a5157',
              border_gray:    '#32353c',
              cancel_opacity: '0.4',
              mini_hover:     '#2a3137',
              text_title:     '#fff',
              text_main:      '#9498a3',
              text_tagline:   '#464851',
              text_hover:     '#ddd'
          };
      } else {
          this.style_vals = {
              bg:             '#fff',
              bg_actions:     '#e7eaee',
              bg_hover:       '#eceff3',
              bg_light:       '#f5f5f5',
              border_gray:    '#e4ecf2',
              cancel_opacity: '1.0',
              mini_hover:     '#fafafa',
              text_title:     '#5c6578',
              text_main:      '#8b949b',
              text_tagline:   '#ced9e6',
              text_hover:     '#7c8598'
          };
      }
      var shadow = '0px 0px 35px 0px rgba(45, 49, 56, 0.7)',
          video_shadow = shadow,
          mini_shadow = shadow,
          thumb_total_size = MPNotif.THUMB_IMG_SIZE + MPNotif.THUMB_BORDER_SIZE * 2,
          anim_seconds = (MPNotif.ANIM_TIME / 1000) + 's';
      if (this.mini) {
          shadow = 'none';
      }
  
      // don't display on small viewports
      var notif_media_queries = {},
          min_width = MPNotif.NOTIF_WIDTH_MINI + 20;
      notif_media_queries['@media only screen and (max-width: ' + (min_width - 1) + 'px)'] = {
          '#overlay': {
              'display': 'none'
          }
      };
      var notif_styles = {
          '.flipped': {
              'transform': 'rotateY(180deg)'
          },
          '#overlay': {
              'position': 'fixed',
              'top': '0',
              'left': '0',
              'width': '100%',
              'height': '100%',
              'overflow': 'auto',
              'text-align': 'center',
              'z-index': '10000',
              'font-family': '"Helvetica", "Arial", sans-serif',
              '-webkit-font-smoothing': 'antialiased',
              '-moz-osx-font-smoothing': 'grayscale'
          },
          '#overlay.mini': {
              'height': '0',
              'overflow': 'visible'
          },
          '#overlay a': {
              'width': 'initial',
              'padding': '0',
              'text-decoration': 'none',
              'text-transform': 'none',
              'color': 'inherit'
          },
          '#bgwrapper': {
              'position': 'relative',
              'width': '100%',
              'height': '100%'
          },
          '#bg': {
              'position': 'fixed',
              'top': '0',
              'left': '0',
              'width': '100%',
              'height': '100%',
              'min-width': this.doc_width * 4 + 'px',
              'min-height': this.doc_height * 4 + 'px',
              'background-color': 'black',
              'opacity': '0.0',
              '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)', // IE8
              'filter': 'alpha(opacity=60)', // IE5-7
              'transition': 'opacity ' + anim_seconds
          },
          '#bg.visible': {
              'opacity': MPNotif.BG_OPACITY
          },
          '.mini #bg': {
              'width': '0',
              'height': '0',
              'min-width': '0'
          },
          '#flipcontainer': {
              'perspective': '1000px',
              'position': 'absolute',
              'width': '100%'
          },
          '#flipper': {
              'position': 'relative',
              'transform-style': 'preserve-3d',
              'transition': '0.3s'
          },
          '#takeover': {
              'position': 'absolute',
              'left': '50%',
              'width': MPNotif.NOTIF_WIDTH + 'px',
              'margin-left': Math.round(-MPNotif.NOTIF_WIDTH / 2) + 'px',
              'backface-visibility': 'hidden',
              'transform': 'rotateY(0deg)',
              'opacity': '0.0',
              'top': MPNotif.NOTIF_START_TOP + 'px',
              'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds
          },
          '#takeover.visible': {
              'opacity': '1.0',
              'top': MPNotif.NOTIF_TOP + 'px'
          },
          '#takeover.exiting': {
              'opacity': '0.0',
              'top': MPNotif.NOTIF_START_TOP + 'px'
          },
          '#thumbspacer': {
              'height': MPNotif.THUMB_OFFSET + 'px'
          },
          '#thumbborder-wrapper': {
              'position': 'absolute',
              'top': (-MPNotif.THUMB_BORDER_SIZE) + 'px',
              'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET - MPNotif.THUMB_BORDER_SIZE) + 'px',
              'width': thumb_total_size + 'px',
              'height': (thumb_total_size / 2) + 'px',
              'overflow': 'hidden'
          },
          '#thumbborder': {
              'position': 'absolute',
              'width': thumb_total_size + 'px',
              'height': thumb_total_size + 'px',
              'border-radius': thumb_total_size + 'px',
              'background-color': this.style_vals.bg_actions,
              'opacity': '0.5'
          },
          '#thumbnail': {
              'position': 'absolute',
              'top': '0px',
              'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET) + 'px',
              'width': MPNotif.THUMB_IMG_SIZE + 'px',
              'height': MPNotif.THUMB_IMG_SIZE + 'px',
              'overflow': 'hidden',
              'z-index': '100',
              'border-radius': MPNotif.THUMB_IMG_SIZE + 'px'
          },
          '#mini': {
              'position': 'absolute',
              'right': '20px',
              'top': MPNotif.NOTIF_TOP + 'px',
              'width': this.notif_width + 'px',
              'height': MPNotif.NOTIF_HEIGHT_MINI * 2 + 'px',
              'margin-top': 20 - MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'backface-visibility': 'hidden',
              'opacity': '0.0',
              'transform': 'rotateX(90deg)',
              'transition': 'opacity 0.3s, transform 0.3s, right 0.3s'
          },
          '#mini.visible': {
              'opacity': '1.0',
              'transform': 'rotateX(0deg)'
          },
          '#mini.exiting': {
              'opacity': '0.0',
              'right': '-150px'
          },
          '#mainbox': {
              'border-radius': '4px',
              'box-shadow': shadow,
              'text-align': 'center',
              'background-color': this.style_vals.bg,
              'font-size': '14px',
              'color': this.style_vals.text_main
          },
          '#mini #mainbox': {
              'height': MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'border-radius': '3px',
              'transition': 'background-color ' + anim_seconds
          },
          '#mini-border': {
              'height': (MPNotif.NOTIF_HEIGHT_MINI + 6) + 'px',
              'width': (MPNotif.NOTIF_WIDTH_MINI + 6) + 'px',
              'position': 'absolute',
              'top': '-3px',
              'left': '-3px',
              'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'border-radius': '6px',
              'opacity': '0.25',
              'background-color': '#fff',
              'z-index': '-1',
              'box-shadow': mini_shadow
          },
          '#mini-icon': {
              'position': 'relative',
              'display': 'inline-block',
              'width': '75px',
              'height': MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'border-radius': '3px 0 0 3px',
              'background-color': this.style_vals.bg_actions,
              'background': 'linear-gradient(135deg, ' + this.style_vals.bg_light + ' 0%, ' + this.style_vals.bg_actions + ' 100%)',
              'transition': 'background-color ' + anim_seconds
          },
          '#mini:hover #mini-icon': {
              'background-color': this.style_vals.mini_hover
          },
          '#mini:hover #mainbox': {
              'background-color': this.style_vals.mini_hover
          },
          '#mini-icon-img': {
              'position': 'absolute',
              'background-image': 'url(' + this.thumb_image_url + ')',
              'width': '48px',
              'height': '48px',
              'top': '20px',
              'left': '12px'
          },
          '#content': {
              'padding': '30px 20px 0px 20px'
          },
          '#mini-content': {
              'text-align': 'left',
              'height': MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'cursor': 'pointer'
          },
          '#img': {
              'width': '328px',
              'margin-top': '30px',
              'border-radius': '5px'
          },
          '#title': {
              'max-height': '600px',
              'overflow': 'hidden',
              'word-wrap': 'break-word',
              'padding': '25px 0px 20px 0px',
              'font-size': '19px',
              'font-weight': 'bold',
              'color': this.style_vals.text_title
          },
          '#body': {
              'max-height': '600px',
              'margin-bottom': '25px',
              'overflow': 'hidden',
              'word-wrap': 'break-word',
              'line-height': '21px',
              'font-size': '15px',
              'font-weight': 'normal',
              'text-align': 'left'
          },
          '#mini #body': {
              'display': 'inline-block',
              'max-width': '250px',
              'margin': '0 0 0 30px',
              'height': MPNotif.NOTIF_HEIGHT_MINI + 'px',
              'font-size': '16px',
              'letter-spacing': '0.8px',
              'color': this.style_vals.text_title
          },
          '#mini #body-text': {
              'display': 'table',
              'height': MPNotif.NOTIF_HEIGHT_MINI + 'px'
          },
          '#mini #body-text div': {
              'display': 'table-cell',
              'vertical-align': 'middle'
          },
          '#tagline': {
              'margin-bottom': '15px',
              'font-size': '10px',
              'font-weight': '600',
              'letter-spacing': '0.8px',
              'color': '#ccd7e0',
              'text-align': 'left'
          },
          '#tagline a': {
              'color': this.style_vals.text_tagline,
              'transition': 'color ' + anim_seconds
          },
          '#tagline a:hover': {
              'color': this.style_vals.text_hover
          },
          '#cancel': {
              'position': 'absolute',
              'right': '0',
              'width': '8px',
              'height': '8px',
              'padding': '10px',
              'border-radius': '20px',
              'margin': '12px 12px 0 0',
              'box-sizing': 'content-box',
              'cursor': 'pointer',
              'transition': 'background-color ' + anim_seconds
          },
          '#mini #cancel': {
              'margin': '7px 7px 0 0'
          },
          '#cancel-icon': {
              'width': '8px',
              'height': '8px',
              'overflow': 'hidden',
              'background-image': 'url(' + this.cdn_host + '/site_media/images/icons/notifications/cancel-x.png)',
              'opacity': this.style_vals.cancel_opacity
          },
          '#cancel:hover': {
              'background-color': this.style_vals.bg_hover
          },
          '#button': {
              'display': 'block',
              'height': '60px',
              'line-height': '60px',
              'text-align': 'center',
              'background-color': this.style_vals.bg_actions,
              'border-radius': '0 0 4px 4px',
              'overflow': 'hidden',
              'cursor': 'pointer',
              'transition': 'background-color ' + anim_seconds
          },
          '#button-close': {
              'display': 'inline-block',
              'width': '9px',
              'height': '60px',
              'margin-right': '8px',
              'vertical-align': 'top',
              'background-image': 'url(' + this.cdn_host + '/site_media/images/icons/notifications/close-x-' + this.style + '.png)',
              'background-repeat': 'no-repeat',
              'background-position': '0px 25px'
          },
          '#button-play': {
              'display': 'inline-block',
              'width': '30px',
              'height': '60px',
              'margin-left': '15px',
              'background-image': 'url(' + this.cdn_host + '/site_media/images/icons/notifications/play-' + this.style + '-small.png)',
              'background-repeat': 'no-repeat',
              'background-position': '0px 15px'
          },
          'a#button-link': {
              'display': 'inline-block',
              'vertical-align': 'top',
              'text-align': 'center',
              'font-size': '17px',
              'font-weight': 'bold',
              'overflow': 'hidden',
              'word-wrap': 'break-word',
              'color': this.style_vals.text_title,
              'transition': 'color ' + anim_seconds
          },
          '#button:hover': {
              'background-color': this.style_vals.bg_hover,
              'color': this.style_vals.text_hover
          },
          '#button:hover a': {
              'color': this.style_vals.text_hover
          },
  
          '#video-noflip': {
              'position': 'relative',
              'top': (-this.video_height * 2) + 'px'
          },
          '#video-flip': {
              'backface-visibility': 'hidden',
              'transform': 'rotateY(180deg)'
          },
          '#video': {
              'position': 'absolute',
              'width': (this.video_width - 1) + 'px',
              'height': this.video_height + 'px',
              'top': MPNotif.NOTIF_TOP + 'px',
              'margin-top': '100px',
              'left': '50%',
              'margin-left': Math.round(-this.video_width / 2) + 'px',
              'overflow': 'hidden',
              'border-radius': '5px',
              'box-shadow': video_shadow,
              'transform': 'translateZ(1px)', // webkit rendering bug http://stackoverflow.com/questions/18167981/clickable-link-area-unexpectedly-smaller-after-css-transform
              'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds
          },
          '#video.exiting': {
              'opacity': '0.0',
              'top': this.video_height + 'px'
          },
          '#video-holder': {
              'position': 'absolute',
              'width': (this.video_width - 1) + 'px',
              'height': this.video_height + 'px',
              'overflow': 'hidden',
              'border-radius': '5px'
          },
          '#video-frame': {
              'margin-left': '-1px',
              'width': this.video_width + 'px'
          },
          '#video-controls': {
              'opacity': '0',
              'transition': 'opacity 0.5s'
          },
          '#video:hover #video-controls': {
              'opacity': '1.0'
          },
          '#video .video-progress-el': {
              'position': 'absolute',
              'bottom': '0',
              'height': '25px',
              'border-radius': '0 0 0 5px'
          },
          '#video-progress': {
              'width': '90%'
          },
          '#video-progress-total': {
              'width': '100%',
              'background-color': this.style_vals.bg,
              'opacity': '0.7'
          },
          '#video-elapsed': {
              'width': '0',
              'background-color': '#6cb6f5',
              'opacity': '0.9'
          },
          '#video #video-time': {
              'width': '10%',
              'right': '0',
              'font-size': '11px',
              'line-height': '25px',
              'color': this.style_vals.text_main,
              'background-color': '#666',
              'border-radius': '0 0 5px 0'
          }
      };
  
      // IE hacks
      if (this._browser_lte('ie', 8)) {
          _.extend(notif_styles, {
              '* html #overlay': {
                  'position': 'absolute'
              },
              '* html #bg': {
                  'position': 'absolute'
              },
              'html, body': {
                  'height': '100%'
              }
          });
      }
      if (this._browser_lte('ie', 7)) {
          _.extend(notif_styles, {
              '#mini #body': {
                  'display': 'inline',
                  'zoom': '1',
                  'border': '1px solid ' + this.style_vals.bg_hover
              },
              '#mini #body-text': {
                  'padding': '20px'
              },
              '#mini #mini-icon': {
                  'display': 'none'
              }
          });
      }
  
      // add vendor-prefixed style rules
      var VENDOR_STYLES   = ['backface-visibility', 'border-radius', 'box-shadow', 'opacity',
                                 'perspective', 'transform', 'transform-style', 'transition'],
          VENDOR_PREFIXES = ['khtml', 'moz', 'ms', 'o', 'webkit'];
      for (var selector in notif_styles) {
          for (var si = 0; si < VENDOR_STYLES.length; si++) {
              var prop = VENDOR_STYLES[si];
              if (prop in notif_styles[selector]) {
                  var val = notif_styles[selector][prop];
                  for (var pi = 0; pi < VENDOR_PREFIXES.length; pi++) {
                      notif_styles[selector]['-' + VENDOR_PREFIXES[pi] + '-' + prop] = val;
                  }
              }
          }
      }
  
      var inject_styles = function(styles, media_queries) {
          var create_style_text = function(style_defs) {
              var st = '';
              for (var selector in style_defs) {
                  var mp_selector = selector
                          .replace(/#/g, '#' + MPNotif.MARKUP_PREFIX + '-')
                          .replace(/\./g, '.' + MPNotif.MARKUP_PREFIX + '-');
                  st += '\n' + mp_selector + ' {';
                  var props = style_defs[selector];
                  for (var k in props) {
                      st += k + ':' + props[k] + ';';
                  }
                  st += '}';
              }
              return st;
          };
          var create_media_query_text = function(mq_defs) {
              var mqt = '';
              for (var mq in mq_defs) {
                  mqt += '\n' + mq + ' {' + create_style_text(mq_defs[mq]) + '\n}';
              }
              return mqt;
          };
  
          var style_text = create_style_text(styles) + create_media_query_text(media_queries),
              head_el = document.head || document.getElementsByTagName('head')[0] || document.documentElement,
              style_el = document.createElement('style');
          head_el.appendChild(style_el);
          style_el.setAttribute('type', 'text/css');
          if (style_el.styleSheet) { // IE
              style_el.styleSheet.cssText = style_text;
          } else {
              style_el.textContent = style_text;
          }
      };
      inject_styles(notif_styles, notif_media_queries);
  };
  
  MPNotif.prototype._init_video = _.safewrap(function() {
      if (!this.video_url) {
          return;
      }
      var self = this;
  
      // Youtube iframe API compatibility
      self.yt_custom = 'postMessage' in window;
  
      self.dest_url = self.video_url;
      var youtube_match = self.video_url.match(
                  // http://stackoverflow.com/questions/2936467/parse-youtube-video-id-using-preg-match
                  /(?:youtube(?:-nocookie)?\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i
              ),
          vimeo_match = self.video_url.match(
                  /vimeo\.com\/.*?(\d+)/i
              );
      if (youtube_match) {
          self.show_video = true;
          self.youtube_video = youtube_match[1];
  
          if (self.yt_custom) {
              window['onYouTubeIframeAPIReady'] = function() {
                  if (self._get_el('video-frame')) {
                      self._yt_video_ready();
                  }
              };
  
              // load Youtube iframe API; see https://developers.google.com/youtube/iframe_api_reference
              var tag = document.createElement('script');
              tag.src = self.resource_protocol + 'www.youtube.com/iframe_api';
              var firstScriptTag = document.getElementsByTagName('script')[0];
              firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
          }
      } else if (vimeo_match) {
          self.show_video = true;
          self.vimeo_video = vimeo_match[1];
      }
  
      // IE <= 7, FF <= 3: fall through to video link rather than embedded player
      if (self._browser_lte('ie', 7) || self._browser_lte('firefox', 3)) {
          self.show_video = false;
          self.clickthrough = true;
      }
  });
  
  MPNotif.prototype._mark_as_shown = _.safewrap(function() {
      // click on background to dismiss
      var self = this;
      _.register_event(self._get_el('bg'), 'click', function() {
          self.dismiss();
      });
  
      var get_style = function(el, style_name) {
          var styles = {};
          if (document.defaultView && document.defaultView.getComputedStyle) {
              styles = document.defaultView.getComputedStyle(el, null); // FF3 requires both args
          } else if (el.currentStyle) { // IE
              styles = el.currentStyle;
          }
          return styles[style_name];
      };
  
      if (this.campaign_id) {
          var notif_el = this._get_el('overlay');
          if (notif_el && get_style(notif_el, 'visibility') !== 'hidden' && get_style(notif_el, 'display') !== 'none') {
              this._mark_delivery();
          }
      }
  });
  
  MPNotif.prototype._mark_delivery = _.safewrap(function(extra_props) {
      if (!this.marked_as_shown) {
          this.marked_as_shown = true;
  
          if (this.campaign_id) {
              // mark notification shown (local cache)
              this._get_shown_campaigns()[this.campaign_id] = 1 * new Date();
              this.persistence.save();
          }
  
          // track delivery
          this._track_event('$campaign_delivery', extra_props);
  
          // mark notification shown (mixpanel property)
          this.mixpanel['people']['append']({
              '$campaigns': this.campaign_id,
              '$notifications': {
                  'campaign_id': this.campaign_id,
                  'message_id':  this.message_id,
                  'type':        'web',
                  'time':        new Date()
              }
          });
      }
  });
  
  MPNotif.prototype._preload_images = function(all_loaded_cb) {
      var self = this;
      if (this.imgs_to_preload.length === 0) {
          all_loaded_cb();
          return;
      }
  
      var preloaded_imgs = 0;
      var img_objs = [];
      var onload = function() {
          preloaded_imgs++;
          if (preloaded_imgs === self.imgs_to_preload.length && all_loaded_cb) {
              all_loaded_cb();
              all_loaded_cb = null;
          }
      };
      for (var i = 0; i < this.imgs_to_preload.length; i++) {
          var img = new Image();
          img.onload = onload;
          img.src = this.imgs_to_preload[i];
          if (img.complete) {
              onload();
          }
          img_objs.push(img);
      }
  
      // IE6/7 doesn't fire onload reliably
      if (this._browser_lte('ie', 7)) {
          setTimeout(function() {
              var imgs_loaded = true;
              for (i = 0; i < img_objs.length; i++) {
                  if (!img_objs[i].complete) {
                      imgs_loaded = false;
                  }
              }
              if (imgs_loaded && all_loaded_cb) {
                  all_loaded_cb();
                  all_loaded_cb = null;
              }
          }, 500);
      }
  };
  
  MPNotif.prototype._remove_notification_el = _.safewrap(function() {
      window.clearInterval(this._video_progress_checker);
      this.notification_el.style.visibility = 'hidden';
      this.body_el.removeChild(this.notification_el);
  });
  
  MPNotif.prototype._set_client_config = function() {
      var get_browser_version = function(browser_ex) {
          var match = navigator.userAgent.match(browser_ex);
          return match && match[1];
      };
      this.browser_versions = {};
      this.browser_versions['chrome']  = get_browser_version(/Chrome\/(\d+)/);
      this.browser_versions['firefox'] = get_browser_version(/Firefox\/(\d+)/);
      this.browser_versions['ie']      = get_browser_version(/MSIE (\d+).+/);
      if (!this.browser_versions['ie'] && !(window.ActiveXObject) && 'ActiveXObject' in window) {
          this.browser_versions['ie'] = 11;
      }
  
      this.body_el = document.body || document.getElementsByTagName('body')[0];
      if (this.body_el) {
          this.doc_width = Math.max(
                  this.body_el.scrollWidth, document.documentElement.scrollWidth,
                  this.body_el.offsetWidth, document.documentElement.offsetWidth,
                  this.body_el.clientWidth, document.documentElement.clientWidth
              );
          this.doc_height = Math.max(
                  this.body_el.scrollHeight, document.documentElement.scrollHeight,
                  this.body_el.offsetHeight, document.documentElement.offsetHeight,
                  this.body_el.clientHeight, document.documentElement.clientHeight
              );
      }
  
      // detect CSS compatibility
      var ie_ver = this.browser_versions['ie'];
      var sample_styles = document.createElement('div').style,
          is_css_compatible = function(rule) {
              if (rule in sample_styles) {
                  return true;
              }
              if (!ie_ver) {
                  rule = rule[0].toUpperCase() + rule.slice(1);
                  var props = ['O' + rule, 'Webkit' + rule, 'Moz' + rule];
                  for (var i = 0; i < props.length; i++) {
                      if (props[i] in sample_styles) {
                          return true;
                      }
                  }
              }
              return false;
          };
      this.use_transitions = this.body_el &&
          is_css_compatible('transition') &&
          is_css_compatible('transform');
      this.flip_animate = (this.browser_versions['chrome'] >= 33 || this.browser_versions['firefox'] >= 15) &&
          this.body_el &&
          is_css_compatible('backfaceVisibility') &&
          is_css_compatible('perspective') &&
          is_css_compatible('transform');
  };
  
  MPNotif.prototype._switch_to_video = _.safewrap(function() {
      var self = this,
          anims = [
              {
                  el:    self._get_notification_display_el(),
                  attr:  'opacity',
                  start: 1.0,
                  goal:  0.0
              },
              {
                  el:    self._get_notification_display_el(),
                  attr:  'top',
                  start: MPNotif.NOTIF_TOP,
                  goal:  -500
              },
              {
                  el:    self._get_el('video-noflip'),
                  attr:  'opacity',
                  start: 0.0,
                  goal:  1.0
              },
              {
                  el:    self._get_el('video-noflip'),
                  attr:  'top',
                  start: -self.video_height * 2,
                  goal:  0
              }
          ];
  
      if (self.mini) {
          var bg = self._get_el('bg'),
              overlay = self._get_el('overlay');
          bg.style.width = '100%';
          bg.style.height = '100%';
          overlay.style.width = '100%';
  
          self._add_class(self._get_notification_display_el(), 'exiting');
          self._add_class(bg, 'visible');
  
          anims.push({
              el:    self._get_el('bg'),
              attr:  'opacity',
              start: 0.0,
              goal:  MPNotif.BG_OPACITY
          });
      }
  
      var video_el = self._get_el('video-holder');
      video_el.innerHTML = self.video_iframe;
  
      var video_ready = function() {
          if (window['YT'] && window['YT']['loaded']) {
              self._yt_video_ready();
          }
          self.showing_video = true;
          self._get_notification_display_el().style.visibility = 'hidden';
      };
      if (self.flip_animate) {
          self._add_class('flipper', 'flipped');
          setTimeout(video_ready, MPNotif.ANIM_TIME);
      } else {
          self._animate_els(anims, MPNotif.ANIM_TIME, video_ready);
      }
  });
  
  MPNotif.prototype._track_event = function(event_name, properties, cb) {
      if (this.campaign_id) {
          properties = properties || {};
          properties = _.extend(properties, {
              'campaign_id':     this.campaign_id,
              'message_id':      this.message_id,
              'message_type':    'web_inapp',
              'message_subtype': this.notif_type
          });
          this.mixpanel['track'](event_name, properties, cb);
      } else if (cb) {
          cb.call();
      }
  };
  
  MPNotif.prototype._yt_video_ready = _.safewrap(function() {
      var self = this;
      if (self.video_inited) {
          return;
      }
      self.video_inited = true;
  
      var progress_bar  = self._get_el('video-elapsed'),
          progress_time = self._get_el('video-time'),
          progress_el   = self._get_el('video-progress');
  
      new window['YT']['Player'](MPNotif.MARKUP_PREFIX + '-video-frame', {
          'events': {
              'onReady': function(event) {
                  var ytplayer = event['target'],
                      video_duration = ytplayer['getDuration'](),
                      pad = function(i) {
                          return ('00' + i).slice(-2);
                      },
                      update_video_time = function(current_time) {
                          var secs = Math.round(video_duration - current_time),
                              mins = Math.floor(secs / 60),
                              hours = Math.floor(mins / 60);
                          secs -= mins * 60;
                          mins -= hours * 60;
                          progress_time.innerHTML = '-' + (hours ? hours + ':' : '') + pad(mins) + ':' + pad(secs);
                      };
                  update_video_time(0);
                  self._video_progress_checker = window.setInterval(function() {
                      var current_time = ytplayer['getCurrentTime']();
                      progress_bar.style.width = (current_time / video_duration * 100) + '%';
                      update_video_time(current_time);
                  }, 250);
                  _.register_event(progress_el, 'click', function(e) {
                      var clickx = Math.max(0, e.pageX - progress_el.getBoundingClientRect().left);
                      ytplayer['seekTo'](video_duration * clickx / progress_el.clientWidth, true);
                  });
              }
          }
      });
  });
  
  // EXPORTS (for closure compiler)
  
  // MixpanelLib Exports
  MixpanelLib.prototype['init']                            = MixpanelLib.prototype.init;
  MixpanelLib.prototype['reset']                           = MixpanelLib.prototype.reset;
  MixpanelLib.prototype['disable']                         = MixpanelLib.prototype.disable;
  MixpanelLib.prototype['time_event']                      = MixpanelLib.prototype.time_event;
  MixpanelLib.prototype['track']                           = MixpanelLib.prototype.track;
  MixpanelLib.prototype['track_links']                     = MixpanelLib.prototype.track_links;
  MixpanelLib.prototype['track_forms']                     = MixpanelLib.prototype.track_forms;
  MixpanelLib.prototype['track_pageview']                  = MixpanelLib.prototype.track_pageview;
  MixpanelLib.prototype['register']                        = MixpanelLib.prototype.register;
  MixpanelLib.prototype['register_once']                   = MixpanelLib.prototype.register_once;
  MixpanelLib.prototype['unregister']                      = MixpanelLib.prototype.unregister;
  MixpanelLib.prototype['identify']                        = MixpanelLib.prototype.identify;
  MixpanelLib.prototype['alias']                           = MixpanelLib.prototype.alias;
  MixpanelLib.prototype['name_tag']                        = MixpanelLib.prototype.name_tag;
  MixpanelLib.prototype['set_config']                      = MixpanelLib.prototype.set_config;
  MixpanelLib.prototype['get_config']                      = MixpanelLib.prototype.get_config;
  MixpanelLib.prototype['get_property']                    = MixpanelLib.prototype.get_property;
  MixpanelLib.prototype['get_distinct_id']                 = MixpanelLib.prototype.get_distinct_id;
  MixpanelLib.prototype['toString']                        = MixpanelLib.prototype.toString;
  MixpanelLib.prototype['_check_and_handle_notifications'] = MixpanelLib.prototype._check_and_handle_notifications;
  MixpanelLib.prototype['_show_notification']              = MixpanelLib.prototype._show_notification;
  MixpanelLib.prototype['opt_out_tracking']                = MixpanelLib.prototype.opt_out_tracking;
  MixpanelLib.prototype['opt_in_tracking']                 = MixpanelLib.prototype.opt_in_tracking;
  MixpanelLib.prototype['has_opted_out_tracking']          = MixpanelLib.prototype.has_opted_out_tracking;
  MixpanelLib.prototype['has_opted_in_tracking']           = MixpanelLib.prototype.has_opted_in_tracking;
  MixpanelLib.prototype['clear_opt_in_out_tracking']       = MixpanelLib.prototype.clear_opt_in_out_tracking;
  MixpanelLib.prototype['get_group']                       = MixpanelLib.prototype.get_group;
  MixpanelLib.prototype['set_group']                       = MixpanelLib.prototype.set_group;
  MixpanelLib.prototype['add_group']                       = MixpanelLib.prototype.add_group;
  MixpanelLib.prototype['remove_group']                    = MixpanelLib.prototype.remove_group;
  MixpanelLib.prototype['track_with_groups']               = MixpanelLib.prototype.track_with_groups;
  
  // MixpanelPersistence Exports
  MixpanelPersistence.prototype['properties']            = MixpanelPersistence.prototype.properties;
  MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;
  MixpanelPersistence.prototype['update_referrer_info']  = MixpanelPersistence.prototype.update_referrer_info;
  MixpanelPersistence.prototype['get_cross_subdomain']   = MixpanelPersistence.prototype.get_cross_subdomain;
  MixpanelPersistence.prototype['clear']                 = MixpanelPersistence.prototype.clear;
  
  // MixpanelPeople Exports
  MixpanelPeople.prototype['set']           = MixpanelPeople.prototype.set;
  MixpanelPeople.prototype['set_once']      = MixpanelPeople.prototype.set_once;
  MixpanelPeople.prototype['unset']         = MixpanelPeople.prototype.unset;
  MixpanelPeople.prototype['increment']     = MixpanelPeople.prototype.increment;
  MixpanelPeople.prototype['append']        = MixpanelPeople.prototype.append;
  MixpanelPeople.prototype['remove']        = MixpanelPeople.prototype.remove;
  MixpanelPeople.prototype['union']         = MixpanelPeople.prototype.union;
  MixpanelPeople.prototype['track_charge']  = MixpanelPeople.prototype.track_charge;
  MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
  MixpanelPeople.prototype['delete_user']   = MixpanelPeople.prototype.delete_user;
  MixpanelPeople.prototype['toString']      = MixpanelPeople.prototype.toString;
  
  _.safewrap_class(MixpanelLib, ['identify', '_check_and_handle_notifications', '_show_notification']);
  
  
  var instances = {};
  var extend_mp = function() {
      // add all the sub mixpanel instances
      _.each(instances, function(instance, name) {
          if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; }
      });
  
      // add private functions as _
      mixpanel_master['_'] = _;
  };
  
  var override_mp_init_func = function() {
      // we override the snippets init function to handle the case where a
      // user initializes the mixpanel library after the script loads & runs
      mixpanel_master['init'] = function(token, config, name) {
          if (name) {
              // initialize a sub library
              if (!mixpanel_master[name]) {
                  mixpanel_master[name] = instances[name] = create_mplib(token, config, name);
                  mixpanel_master[name]._loaded();
              }
              return mixpanel_master[name];
          } else {
              var instance = mixpanel_master;
  
              if (instances[PRIMARY_INSTANCE_NAME]) {
                  // main mixpanel lib already initialized
                  instance = instances[PRIMARY_INSTANCE_NAME];
              } else if (token) {
                  // intialize the main mixpanel lib
                  instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME);
                  instance._loaded();
                  instances[PRIMARY_INSTANCE_NAME] = instance;
              }
  
              mixpanel_master = instance;
              if (init_type === INIT_SNIPPET) {
                  window[PRIMARY_INSTANCE_NAME] = mixpanel_master;
              }
              extend_mp();
          }
      };
  };
  
  var add_dom_loaded_handler = function() {
      // Cross browser DOM Loaded support
      function dom_loaded_handler() {
          // function flag since we only want to execute this once
          if (dom_loaded_handler.done) { return; }
          dom_loaded_handler.done = true;
  
          DOM_LOADED = true;
          ENQUEUE_REQUESTS = false;
  
          _.each(instances, function(inst) {
              inst._dom_loaded();
          });
      }
  
      function do_scroll_check() {
          try {
              document.documentElement.doScroll('left');
          } catch(e) {
              setTimeout(do_scroll_check, 1);
              return;
          }
  
          dom_loaded_handler();
      }
  
      if (document.addEventListener) {
          if (document.readyState === 'complete') {
              // safari 4 can fire the DOMContentLoaded event before loading all
              // external JS (including this file). you will see some copypasta
              // on the internet that checks for 'complete' and 'loaded', but
              // 'loaded' is an IE thing
              dom_loaded_handler();
          } else {
              document.addEventListener('DOMContentLoaded', dom_loaded_handler, false);
          }
      } else if (document.attachEvent) {
          // IE
          document.attachEvent('onreadystatechange', dom_loaded_handler);
  
          // check to make sure we arn't in a frame
          var toplevel = false;
          try {
              toplevel = window.frameElement === null;
          } catch(e) {
              // noop
          }
  
          if (document.documentElement.doScroll && toplevel) {
              do_scroll_check();
          }
      }
  
      // fallback handler, always will work
      _.register_event(window, 'load', dom_loaded_handler, true);
  };
  
  export function init_from_snippet() {
      init_type = INIT_SNIPPET;
      mixpanel_master = window[PRIMARY_INSTANCE_NAME];
  
      // Initialization
      if (_.isUndefined(mixpanel_master)) {
          // mixpanel wasn't initialized properly, report error and quit
          console.critical('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.');
          return;
      }
      if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) {
          // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
          console.error('Mixpanel library has already been downloaded at least once.');
          return;
      }
      var snippet_version = mixpanel_master['__SV'] || 0;
      if (snippet_version < 1.1) {
          // mixpanel wasn't initialized properly, report error and quit
          console.critical('Version mismatch; please ensure you\'re using the latest version of the Mixpanel code snippet.');
          return;
      }
  
      // Load instances of the Mixpanel Library
      _.each(mixpanel_master['_i'], function(item) {
          if (item && _.isArray(item)) {
              instances[item[item.length-1]] = create_mplib.apply(this, item);
          }
      });
  
      override_mp_init_func();
      mixpanel_master['init']();
  
      // Fire loaded events after updating the window's mixpanel object
      _.each(instances, function(instance) {
          instance._loaded();
      });
  
      add_dom_loaded_handler();
  }
  
  export function init_as_module() {
      init_type = INIT_MODULE;
      mixpanel_master = new MixpanelLib();
  
      override_mp_init_func();
      mixpanel_master['init']();
      add_dom_loaded_handler();
  
      return mixpanel_master;
  }
  