mixpanel-browser 2.41.0 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
 
7
7
  var Config = {
8
8
  DEBUG: false,
9
- LIB_VERSION: '2.41.0'
9
+ LIB_VERSION: '2.45.0'
10
10
  };
11
11
 
12
12
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -40,11 +40,11 @@
40
40
  var toString = ObjProto.toString;
41
41
  var hasOwnProperty = ObjProto.hasOwnProperty;
42
42
  var windowConsole = window$1.console;
43
- var navigator$1 = window$1.navigator;
43
+ var navigator = window$1.navigator;
44
44
  var document$1 = window$1.document;
45
45
  var windowOpera = window$1.opera;
46
46
  var screen = window$1.screen;
47
- var userAgent = navigator$1.userAgent;
47
+ var userAgent = navigator.userAgent;
48
48
  var nativeBind = FuncProto.bind;
49
49
  var nativeForEach = ArrayProto.forEach;
50
50
  var nativeIndexOf = ArrayProto.indexOf;
@@ -156,14 +156,6 @@
156
156
  return bound;
157
157
  };
158
158
 
159
- _.bind_instance_methods = function(obj) {
160
- for (var func in obj) {
161
- if (typeof(obj[func]) === 'function') {
162
- obj[func] = _.bind(obj[func], obj);
163
- }
164
- }
165
- };
166
-
167
159
  /**
168
160
  * @param {*=} obj
169
161
  * @param {function(...*)=} iterator
@@ -192,19 +184,6 @@
192
184
  }
193
185
  };
194
186
 
195
- _.escapeHTML = function(s) {
196
- var escaped = s;
197
- if (escaped && _.isString(escaped)) {
198
- escaped = escaped
199
- .replace(/&/g, '&')
200
- .replace(/</g, '&lt;')
201
- .replace(/>/g, '&gt;')
202
- .replace(/"/g, '&quot;')
203
- .replace(/'/g, '&#039;');
204
- }
205
- return escaped;
206
- };
207
-
208
187
  _.extend = function(obj) {
209
188
  _.each(slice.call(arguments, 1), function(source) {
210
189
  for (var prop in source) {
@@ -380,33 +359,6 @@
380
359
  pad(d.getUTCSeconds());
381
360
  };
382
361
 
383
- _.safewrap = function(f) {
384
- return function() {
385
- try {
386
- return f.apply(this, arguments);
387
- } catch (e) {
388
- console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
389
- if (Config.DEBUG){
390
- console.critical(e);
391
- }
392
- }
393
- };
394
- };
395
-
396
- _.safewrap_class = function(klass, functions) {
397
- for (var i = 0; i < functions.length; i++) {
398
- klass.prototype[functions[i]] = _.safewrap(klass.prototype[functions[i]]);
399
- }
400
- };
401
-
402
- _.safewrap_instance_methods = function(obj) {
403
- for (var func in obj) {
404
- if (typeof(obj[func]) === 'function') {
405
- obj[func] = _.safewrap(obj[func]);
406
- }
407
- }
408
- };
409
-
410
362
  _.strip_empty_properties = function(p) {
411
363
  var ret = {};
412
364
  _.each(p, function(v, k) {
@@ -948,10 +900,12 @@
948
900
  // This is to block various web spiders from executing our JS and
949
901
  // sending false tracking data
950
902
  var BLOCKED_UA_STRS = [
903
+ 'ahrefsbot',
951
904
  'baiduspider',
952
905
  'bingbot',
953
906
  'bingpreview',
954
907
  'facebookexternal',
908
+ 'petalbot',
955
909
  'pinterest',
956
910
  'screaming frog',
957
911
  'yahoo! slurp',
@@ -1648,13 +1602,13 @@
1648
1602
  properties: function() {
1649
1603
  return _.extend(_.strip_empty_properties({
1650
1604
  '$os': _.info.os(),
1651
- '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera),
1605
+ '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera),
1652
1606
  '$referrer': document$1.referrer,
1653
1607
  '$referring_domain': _.info.referringDomain(document$1.referrer),
1654
1608
  '$device': _.info.device(userAgent)
1655
1609
  }), {
1656
1610
  '$current_url': window$1.location.href,
1657
- '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera),
1611
+ '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera),
1658
1612
  '$screen_height': screen.height,
1659
1613
  '$screen_width': screen.width,
1660
1614
  'mp_lib': 'web',
@@ -1667,9 +1621,9 @@
1667
1621
  people_properties: function() {
1668
1622
  return _.extend(_.strip_empty_properties({
1669
1623
  '$os': _.info.os(),
1670
- '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera)
1624
+ '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera)
1671
1625
  }), {
1672
- '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera)
1626
+ '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera)
1673
1627
  });
1674
1628
  },
1675
1629
 
@@ -1677,7 +1631,7 @@
1677
1631
  return _.strip_empty_properties({
1678
1632
  'mp_page': page,
1679
1633
  'mp_referrer': document$1.referrer,
1680
- 'mp_browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera),
1634
+ 'mp_browser': _.info.browser(userAgent, navigator.vendor, windowOpera),
1681
1635
  'mp_platform': _.info.os()
1682
1636
  });
1683
1637
  }
@@ -1688,28 +1642,6 @@
1688
1642
  return maxlen ? guid.substring(0, maxlen) : guid;
1689
1643
  };
1690
1644
 
1691
- /**
1692
- * Check deterministically whether to include or exclude from a feature rollout/test based on the
1693
- * given string and the desired percentage to include.
1694
- * @param {String} str - string to run the check against (for instance a project's token)
1695
- * @param {String} feature - name of feature (for inclusion in hash, to ensure different results
1696
- * for different features)
1697
- * @param {Number} percent_allowed - percentage chance that a given string will be included
1698
- * @returns {Boolean} whether the given string should be included
1699
- */
1700
- var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
1701
- str = str + feature;
1702
-
1703
- // Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
1704
- var hash = 5381;
1705
- for (var i = 0; i < str.length; i++) {
1706
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
1707
- hash = hash & hash;
1708
- }
1709
- var dart = (hash >>> 0) % 100;
1710
- return dart < percent_allowed;
1711
- });
1712
-
1713
1645
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1714
1646
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1715
1647
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -2081,6 +2013,7 @@
2081
2013
  options = options || {};
2082
2014
  this.storageKey = storageKey;
2083
2015
  this.storage = options.storage || window.localStorage;
2016
+ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
2084
2017
  this.lock = new SharedLock(storageKey, {storage: this.storage});
2085
2018
 
2086
2019
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
@@ -2118,18 +2051,18 @@
2118
2051
  this.memQueue.push(queueEntry);
2119
2052
  }
2120
2053
  } catch(err) {
2121
- logger$1.error('Error enqueueing item', item);
2054
+ this.reportError('Error enqueueing item', item);
2122
2055
  succeeded = false;
2123
2056
  }
2124
2057
  if (cb) {
2125
2058
  cb(succeeded);
2126
2059
  }
2127
- }, this), function lockFailure(err) {
2128
- logger$1.error('Error acquiring storage lock', err);
2060
+ }, this), _.bind(function lockFailure(err) {
2061
+ this.reportError('Error acquiring storage lock', err);
2129
2062
  if (cb) {
2130
2063
  cb(false);
2131
2064
  }
2132
- }, this.pid);
2065
+ }, this), this.pid);
2133
2066
  };
2134
2067
 
2135
2068
  /**
@@ -2189,25 +2122,61 @@
2189
2122
  _.each(ids, function(id) { idSet[id] = true; });
2190
2123
 
2191
2124
  this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
2192
- this.lock.withLock(_.bind(function lockAcquired() {
2125
+
2126
+ var removeFromStorage = _.bind(function() {
2193
2127
  var succeeded;
2194
2128
  try {
2195
2129
  var storedQueue = this.readFromStorage();
2196
2130
  storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
2197
2131
  succeeded = this.saveToStorage(storedQueue);
2132
+
2133
+ // an extra check: did storage report success but somehow
2134
+ // the items are still there?
2135
+ if (succeeded) {
2136
+ storedQueue = this.readFromStorage();
2137
+ for (var i = 0; i < storedQueue.length; i++) {
2138
+ var item = storedQueue[i];
2139
+ if (item['id'] && !!idSet[item['id']]) {
2140
+ this.reportError('Item not removed from storage');
2141
+ return false;
2142
+ }
2143
+ }
2144
+ }
2198
2145
  } catch(err) {
2199
- logger$1.error('Error removing items', ids);
2146
+ this.reportError('Error removing items', ids);
2200
2147
  succeeded = false;
2201
2148
  }
2149
+ return succeeded;
2150
+ }, this);
2151
+
2152
+ this.lock.withLock(function lockAcquired() {
2153
+ var succeeded = removeFromStorage();
2202
2154
  if (cb) {
2203
2155
  cb(succeeded);
2204
2156
  }
2205
- }, this), function lockFailure(err) {
2206
- logger$1.error('Error acquiring storage lock', err);
2157
+ }, _.bind(function lockFailure(err) {
2158
+ var succeeded = false;
2159
+ this.reportError('Error acquiring storage lock', err);
2160
+ if (!localStorageSupported(this.storage, true)) {
2161
+ // Looks like localStorage writes have stopped working sometime after
2162
+ // initialization (probably full), and so nobody can acquire locks
2163
+ // anymore. Consider it temporarily safe to remove items without the
2164
+ // lock, since nobody's writing successfully anyway.
2165
+ succeeded = removeFromStorage();
2166
+ if (!succeeded) {
2167
+ // OK, we couldn't even write out the smaller queue. Try clearing it
2168
+ // entirely.
2169
+ try {
2170
+ this.storage.removeItem(this.storageKey);
2171
+ } catch(err) {
2172
+ this.reportError('Error clearing queue', err);
2173
+ }
2174
+ }
2175
+ }
2207
2176
  if (cb) {
2208
- cb(false);
2177
+ cb(succeeded);
2209
2178
  }
2210
- }, this.pid);
2179
+ }, this), this.pid);
2211
2180
  };
2212
2181
 
2213
2182
  // internal helper for RequestQueue.updatePayloads
@@ -2242,18 +2211,18 @@
2242
2211
  storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2243
2212
  succeeded = this.saveToStorage(storedQueue);
2244
2213
  } catch(err) {
2245
- logger$1.error('Error updating items', itemsToUpdate);
2214
+ this.reportError('Error updating items', itemsToUpdate);
2246
2215
  succeeded = false;
2247
2216
  }
2248
2217
  if (cb) {
2249
2218
  cb(succeeded);
2250
2219
  }
2251
- }, this), function lockFailure(err) {
2252
- logger$1.error('Error acquiring storage lock', err);
2220
+ }, this), _.bind(function lockFailure(err) {
2221
+ this.reportError('Error acquiring storage lock', err);
2253
2222
  if (cb) {
2254
2223
  cb(false);
2255
2224
  }
2256
- }, this.pid);
2225
+ }, this), this.pid);
2257
2226
  };
2258
2227
 
2259
2228
  /**
@@ -2267,12 +2236,12 @@
2267
2236
  if (storageEntry) {
2268
2237
  storageEntry = JSONParse(storageEntry);
2269
2238
  if (!_.isArray(storageEntry)) {
2270
- logger$1.error('Invalid storage entry:', storageEntry);
2239
+ this.reportError('Invalid storage entry:', storageEntry);
2271
2240
  storageEntry = null;
2272
2241
  }
2273
2242
  }
2274
2243
  } catch (err) {
2275
- logger$1.error('Error retrieving queue', err);
2244
+ this.reportError('Error retrieving queue', err);
2276
2245
  storageEntry = null;
2277
2246
  }
2278
2247
  return storageEntry || [];
@@ -2286,7 +2255,7 @@
2286
2255
  this.storage.setItem(this.storageKey, JSONStringify(queue));
2287
2256
  return true;
2288
2257
  } catch (err) {
2289
- logger$1.error('Error saving queue', err);
2258
+ this.reportError('Error saving queue', err);
2290
2259
  return false;
2291
2260
  }
2292
2261
  };
@@ -2313,17 +2282,23 @@
2313
2282
  * @constructor
2314
2283
  */
2315
2284
  var RequestBatcher = function(storageKey, options) {
2316
- this.queue = new RequestQueue(storageKey, {storage: options.storage});
2285
+ this.errorReporter = options.errorReporter;
2286
+ this.queue = new RequestQueue(storageKey, {
2287
+ errorReporter: _.bind(this.reportError, this),
2288
+ storage: options.storage
2289
+ });
2317
2290
 
2318
2291
  this.libConfig = options.libConfig;
2319
2292
  this.sendRequest = options.sendRequestFunc;
2320
2293
  this.beforeSendHook = options.beforeSendHook;
2294
+ this.stopAllBatching = options.stopAllBatchingFunc;
2321
2295
 
2322
2296
  // seed variable batch size + flush interval with configured values
2323
2297
  this.batchSize = this.libConfig['batch_size'];
2324
2298
  this.flushInterval = this.libConfig['batch_flush_interval_ms'];
2325
2299
 
2326
2300
  this.stopped = !this.libConfig['batch_autostart'];
2301
+ this.consecutiveRemovalFailures = 0;
2327
2302
  };
2328
2303
 
2329
2304
  /**
@@ -2339,6 +2314,7 @@
2339
2314
  */
2340
2315
  RequestBatcher.prototype.start = function() {
2341
2316
  this.stopped = false;
2317
+ this.consecutiveRemovalFailures = 0;
2342
2318
  this.flush();
2343
2319
  };
2344
2320
 
@@ -2443,14 +2419,14 @@
2443
2419
  res.error === 'timeout' &&
2444
2420
  new Date().getTime() - startTime >= timeoutMS
2445
2421
  ) {
2446
- logger.error('Network timeout; retrying');
2422
+ this.reportError('Network timeout; retrying');
2447
2423
  this.flush();
2448
2424
  } else if (
2449
2425
  _.isObject(res) &&
2450
2426
  res.xhr_req &&
2451
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
2427
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
2452
2428
  ) {
2453
- // network or API error, retry
2429
+ // network or API error, or 429 Too Many Requests, retry
2454
2430
  var retryMS = this.flushInterval * 2;
2455
2431
  var headers = res.xhr_req['responseHeaders'];
2456
2432
  if (headers) {
@@ -2460,17 +2436,17 @@
2460
2436
  }
2461
2437
  }
2462
2438
  retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
2463
- logger.error('Error; retry in ' + retryMS + ' ms');
2439
+ this.reportError('Error; retry in ' + retryMS + ' ms');
2464
2440
  this.scheduleFlush(retryMS);
2465
2441
  } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) {
2466
2442
  // 413 Payload Too Large
2467
2443
  if (batch.length > 1) {
2468
2444
  var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
2469
2445
  this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
2470
- logger.error('413 response; reducing batch size to ' + this.batchSize);
2446
+ this.reportError('413 response; reducing batch size to ' + this.batchSize);
2471
2447
  this.resetFlush();
2472
2448
  } else {
2473
- logger.error('Single-event request too large; dropping', batch);
2449
+ this.reportError('Single-event request too large; dropping', batch);
2474
2450
  this.resetBatchSize();
2475
2451
  removeItemsFromQueue = true;
2476
2452
  }
@@ -2483,12 +2459,25 @@
2483
2459
  if (removeItemsFromQueue) {
2484
2460
  this.queue.removeItemsByID(
2485
2461
  _.map(batch, function(item) { return item['id']; }),
2486
- _.bind(this.flush, this) // handle next batch if the queue isn't empty
2462
+ _.bind(function(succeeded) {
2463
+ if (succeeded) {
2464
+ this.consecutiveRemovalFailures = 0;
2465
+ this.flush(); // handle next batch if the queue isn't empty
2466
+ } else {
2467
+ this.reportError('Failed to remove items from queue');
2468
+ if (++this.consecutiveRemovalFailures > 5) {
2469
+ this.reportError('Too many queue failures; disabling batching system.');
2470
+ this.stopAllBatching();
2471
+ } else {
2472
+ this.resetFlush();
2473
+ }
2474
+ }
2475
+ }, this)
2487
2476
  );
2488
2477
  }
2489
2478
 
2490
2479
  } catch(err) {
2491
- logger.error('Error handling API response', err);
2480
+ this.reportError('Error handling API response', err);
2492
2481
  this.resetFlush();
2493
2482
  }
2494
2483
  }, this);
@@ -2505,11 +2494,28 @@
2505
2494
  this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
2506
2495
 
2507
2496
  } catch(err) {
2508
- logger.error('Error flushing request queue', err);
2497
+ this.reportError('Error flushing request queue', err);
2509
2498
  this.resetFlush();
2510
2499
  }
2511
2500
  };
2512
2501
 
2502
+ /**
2503
+ * Log error to global logger and optional user-defined logger.
2504
+ */
2505
+ RequestBatcher.prototype.reportError = function(msg, err) {
2506
+ logger.error.apply(logger.error, arguments);
2507
+ if (this.errorReporter) {
2508
+ try {
2509
+ if (!(err instanceof Error)) {
2510
+ err = new Error(msg);
2511
+ }
2512
+ this.errorReporter(msg, err);
2513
+ } catch(err) {
2514
+ logger.error(err);
2515
+ }
2516
+ }
2517
+ };
2518
+
2513
2519
  /**
2514
2520
  * A function used to track a Mixpanel event (e.g. MixpanelLib.track)
2515
2521
  * @callback trackFunction
@@ -3020,9 +3026,13 @@
3020
3026
  * Permanently delete a group.
3021
3027
  *
3022
3028
  * ### Usage:
3029
+ *
3023
3030
  * mixpanel.get_group('company', 'mixpanel').delete();
3031
+ *
3032
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
3024
3033
  */
3025
3034
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
3035
+ // bracket notation above prevents a minification error related to reserved words
3026
3036
  var data = this.delete_action();
3027
3037
  return this._send_request(data, callback);
3028
3038
  });
@@ -3077,2755 +3087,929 @@
3077
3087
  MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset;
3078
3088
  MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString;
3079
3089
 
3080
- /*
3081
- * Constants
3082
- */
3083
- /** @const */ var SET_QUEUE_KEY = '__mps';
3084
- /** @const */ var SET_ONCE_QUEUE_KEY = '__mpso';
3085
- /** @const */ var UNSET_QUEUE_KEY = '__mpus';
3086
- /** @const */ var ADD_QUEUE_KEY = '__mpa';
3087
- /** @const */ var APPEND_QUEUE_KEY = '__mpap';
3088
- /** @const */ var REMOVE_QUEUE_KEY = '__mpr';
3089
- /** @const */ var UNION_QUEUE_KEY = '__mpu';
3090
- // This key is deprecated, but we want to check for it to see whether aliasing is allowed.
3091
- /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id';
3092
- /** @const */ var ALIAS_ID_KEY = '__alias';
3093
- /** @const */ var CAMPAIGN_IDS_KEY = '__cmpns';
3094
- /** @const */ var EVENT_TIMERS_KEY = '__timers';
3095
- /** @const */ var RESERVED_PROPERTIES = [
3096
- SET_QUEUE_KEY,
3097
- SET_ONCE_QUEUE_KEY,
3098
- UNSET_QUEUE_KEY,
3099
- ADD_QUEUE_KEY,
3100
- APPEND_QUEUE_KEY,
3101
- REMOVE_QUEUE_KEY,
3102
- UNION_QUEUE_KEY,
3103
- PEOPLE_DISTINCT_ID_KEY,
3104
- ALIAS_ID_KEY,
3105
- CAMPAIGN_IDS_KEY,
3106
- EVENT_TIMERS_KEY
3107
- ];
3108
-
3109
3090
  /**
3110
- * Mixpanel Persistence Object
3091
+ * Mixpanel People Object
3111
3092
  * @constructor
3112
3093
  */
3113
- var MixpanelPersistence = function(config) {
3114
- this['props'] = {};
3115
- this.campaign_params_saved = false;
3116
-
3117
- if (config['persistence_name']) {
3118
- this.name = 'mp_' + config['persistence_name'];
3119
- } else {
3120
- this.name = 'mp_' + config['token'] + '_mixpanel';
3121
- }
3122
-
3123
- var storage_type = config['persistence'];
3124
- if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
3125
- console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3126
- storage_type = config['persistence'] = 'cookie';
3127
- }
3128
-
3129
- if (storage_type === 'localStorage' && _.localStorage.is_supported()) {
3130
- this.storage = _.localStorage;
3131
- } else {
3132
- this.storage = _.cookie;
3133
- }
3094
+ var MixpanelPeople = function() {};
3134
3095
 
3135
- this.load();
3136
- this.update_config(config);
3137
- this.upgrade(config);
3138
- this.save();
3139
- };
3096
+ _.extend(MixpanelPeople.prototype, apiActions);
3140
3097
 
3141
- MixpanelPersistence.prototype.properties = function() {
3142
- var p = {};
3143
- // Filter out reserved properties
3144
- _.each(this['props'], function(v, k) {
3145
- if (!_.include(RESERVED_PROPERTIES, k)) {
3146
- p[k] = v;
3147
- }
3148
- });
3149
- return p;
3098
+ MixpanelPeople.prototype._init = function(mixpanel_instance) {
3099
+ this._mixpanel = mixpanel_instance;
3150
3100
  };
3151
3101
 
3152
- MixpanelPersistence.prototype.load = function() {
3153
- if (this.disabled) { return; }
3154
-
3155
- var entry = this.storage.parse(this.name);
3156
-
3157
- if (entry) {
3158
- this['props'] = _.extend({}, entry);
3102
+ /*
3103
+ * Set properties on a user record.
3104
+ *
3105
+ * ### Usage:
3106
+ *
3107
+ * mixpanel.people.set('gender', 'm');
3108
+ *
3109
+ * // or set multiple properties at once
3110
+ * mixpanel.people.set({
3111
+ * 'Company': 'Acme',
3112
+ * 'Plan': 'Premium',
3113
+ * 'Upgrade date': new Date()
3114
+ * });
3115
+ * // properties can be strings, integers, dates, or lists
3116
+ *
3117
+ * @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.
3118
+ * @param {*} [to] A value to set on the given property name
3119
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3120
+ */
3121
+ MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
3122
+ var data = this.set_action(prop, to);
3123
+ if (_.isObject(prop)) {
3124
+ callback = to;
3159
3125
  }
3160
- };
3161
-
3162
- MixpanelPersistence.prototype.upgrade = function(config) {
3163
- var upgrade_from_old_lib = config['upgrade'],
3164
- old_cookie_name,
3165
- old_cookie;
3166
-
3167
- if (upgrade_from_old_lib) {
3168
- old_cookie_name = 'mp_super_properties';
3169
- // Case where they had a custom cookie name before.
3170
- if (typeof(upgrade_from_old_lib) === 'string') {
3171
- old_cookie_name = upgrade_from_old_lib;
3172
- }
3173
-
3174
- old_cookie = this.storage.parse(old_cookie_name);
3175
-
3176
- // remove the cookie
3177
- this.storage.remove(old_cookie_name);
3178
- this.storage.remove(old_cookie_name, true);
3179
-
3180
- if (old_cookie) {
3181
- this['props'] = _.extend(
3182
- this['props'],
3183
- old_cookie['all'],
3184
- old_cookie['events']
3185
- );
3186
- }
3126
+ // make sure that the referrer info has been updated and saved
3127
+ if (this._get_config('save_referrer')) {
3128
+ this._mixpanel['persistence'].update_referrer_info(document.referrer);
3187
3129
  }
3188
3130
 
3189
- if (!config['cookie_name'] && config['name'] !== 'mixpanel') {
3190
- // special case to handle people with cookies of the form
3191
- // mp_TOKEN_INSTANCENAME from the first release of this library
3192
- old_cookie_name = 'mp_' + config['token'] + '_' + config['name'];
3193
- old_cookie = this.storage.parse(old_cookie_name);
3194
-
3195
- if (old_cookie) {
3196
- this.storage.remove(old_cookie_name);
3197
- this.storage.remove(old_cookie_name, true);
3131
+ // update $set object with default people properties
3132
+ data[SET_ACTION] = _.extend(
3133
+ {},
3134
+ _.info.people_properties(),
3135
+ this._mixpanel['persistence'].get_referrer_info(),
3136
+ data[SET_ACTION]
3137
+ );
3138
+ return this._send_request(data, callback);
3139
+ });
3198
3140
 
3199
- // Save the prop values that were in the cookie from before -
3200
- // this should only happen once as we delete the old one.
3201
- this.register_once(old_cookie);
3202
- }
3141
+ /*
3142
+ * Set properties on a user record, only if they do not yet exist.
3143
+ * This will not overwrite previous people property values, unlike
3144
+ * people.set().
3145
+ *
3146
+ * ### Usage:
3147
+ *
3148
+ * mixpanel.people.set_once('First Login Date', new Date());
3149
+ *
3150
+ * // or set multiple properties at once
3151
+ * mixpanel.people.set_once({
3152
+ * 'First Login Date': new Date(),
3153
+ * 'Starting Plan': 'Premium'
3154
+ * });
3155
+ *
3156
+ * // properties can be strings, integers or dates
3157
+ *
3158
+ * @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.
3159
+ * @param {*} [to] A value to set on the given property name
3160
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3161
+ */
3162
+ MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
3163
+ var data = this.set_once_action(prop, to);
3164
+ if (_.isObject(prop)) {
3165
+ callback = to;
3203
3166
  }
3167
+ return this._send_request(data, callback);
3168
+ });
3204
3169
 
3205
- if (this.storage === _.localStorage) {
3206
- old_cookie = _.cookie.parse(this.name);
3170
+ /*
3171
+ * Unset properties on a user record (permanently removes the properties and their values from a profile).
3172
+ *
3173
+ * ### Usage:
3174
+ *
3175
+ * mixpanel.people.unset('gender');
3176
+ *
3177
+ * // or unset multiple properties at once
3178
+ * mixpanel.people.unset(['gender', 'Company']);
3179
+ *
3180
+ * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names.
3181
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3182
+ */
3183
+ MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) {
3184
+ var data = this.unset_action(prop);
3185
+ return this._send_request(data, callback);
3186
+ });
3207
3187
 
3208
- _.cookie.remove(this.name);
3209
- _.cookie.remove(this.name, true);
3210
-
3211
- if (old_cookie) {
3212
- this.register_once(old_cookie);
3213
- }
3214
- }
3215
- };
3216
-
3217
- MixpanelPersistence.prototype.save = function() {
3218
- if (this.disabled) { return; }
3219
- this._expire_notification_campaigns();
3220
- this.storage.set(
3221
- this.name,
3222
- _.JSONEncode(this['props']),
3223
- this.expire_days,
3224
- this.cross_subdomain,
3225
- this.secure,
3226
- this.cross_site,
3227
- this.cookie_domain
3228
- );
3229
- };
3230
-
3231
- MixpanelPersistence.prototype.remove = function() {
3232
- // remove both domain and subdomain cookies
3233
- this.storage.remove(this.name, false, this.cookie_domain);
3234
- this.storage.remove(this.name, true, this.cookie_domain);
3235
- };
3236
-
3237
- // removes the storage entry and deletes all loaded data
3238
- // forced name for tests
3239
- MixpanelPersistence.prototype.clear = function() {
3240
- this.remove();
3241
- this['props'] = {};
3242
- };
3243
-
3244
- /**
3245
- * @param {Object} props
3246
- * @param {*=} default_value
3247
- * @param {number=} days
3188
+ /*
3189
+ * Increment/decrement numeric people analytics properties.
3190
+ *
3191
+ * ### Usage:
3192
+ *
3193
+ * mixpanel.people.increment('page_views', 1);
3194
+ *
3195
+ * // or, for convenience, if you're just incrementing a counter by
3196
+ * // 1, you can simply do
3197
+ * mixpanel.people.increment('page_views');
3198
+ *
3199
+ * // to decrement a counter, pass a negative number
3200
+ * mixpanel.people.increment('credits_left', -1);
3201
+ *
3202
+ * // like mixpanel.people.set(), you can increment multiple
3203
+ * // properties at once:
3204
+ * mixpanel.people.increment({
3205
+ * counter1: 1,
3206
+ * counter2: 6
3207
+ * });
3208
+ *
3209
+ * @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.
3210
+ * @param {Number} [by] An amount to increment the given property
3211
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3248
3212
  */
3249
- MixpanelPersistence.prototype.register_once = function(props, default_value, days) {
3250
- if (_.isObject(props)) {
3251
- if (typeof(default_value) === 'undefined') { default_value = 'None'; }
3252
- this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
3253
-
3254
- _.each(props, function(val, prop) {
3255
- if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) {
3256
- this['props'][prop] = val;
3213
+ MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) {
3214
+ var data = {};
3215
+ var $add = {};
3216
+ if (_.isObject(prop)) {
3217
+ _.each(prop, function(v, k) {
3218
+ if (!this._is_reserved_property(k)) {
3219
+ if (isNaN(parseFloat(v))) {
3220
+ console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
3221
+ return;
3222
+ } else {
3223
+ $add[k] = v;
3224
+ }
3257
3225
  }
3258
3226
  }, this);
3227
+ callback = by;
3228
+ } else {
3229
+ // convenience: mixpanel.people.increment('property'); will
3230
+ // increment 'property' by 1
3231
+ if (_.isUndefined(by)) {
3232
+ by = 1;
3233
+ }
3234
+ $add[prop] = by;
3235
+ }
3236
+ data[ADD_ACTION] = $add;
3259
3237
 
3260
- this.save();
3238
+ return this._send_request(data, callback);
3239
+ });
3261
3240
 
3262
- return true;
3241
+ /*
3242
+ * Append a value to a list-valued people analytics property.
3243
+ *
3244
+ * ### Usage:
3245
+ *
3246
+ * // append a value to a list, creating it if needed
3247
+ * mixpanel.people.append('pages_visited', 'homepage');
3248
+ *
3249
+ * // like mixpanel.people.set(), you can append multiple
3250
+ * // properties at once:
3251
+ * mixpanel.people.append({
3252
+ * list1: 'bob',
3253
+ * list2: 123
3254
+ * });
3255
+ *
3256
+ * @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.
3257
+ * @param {*} [value] value An item to append to the list
3258
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3259
+ */
3260
+ MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
3261
+ if (_.isObject(list_name)) {
3262
+ callback = value;
3263
3263
  }
3264
- return false;
3265
- };
3264
+ var data = this.append_action(list_name, value);
3265
+ return this._send_request(data, callback);
3266
+ });
3266
3267
 
3267
- /**
3268
- * @param {Object} props
3269
- * @param {number=} days
3268
+ /*
3269
+ * Remove a value from a list-valued people analytics property.
3270
+ *
3271
+ * ### Usage:
3272
+ *
3273
+ * mixpanel.people.remove('School', 'UCB');
3274
+ *
3275
+ * @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.
3276
+ * @param {*} [value] value Item to remove from the list
3277
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3270
3278
  */
3271
- MixpanelPersistence.prototype.register = function(props, days) {
3272
- if (_.isObject(props)) {
3273
- this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
3274
-
3275
- _.extend(this['props'], props);
3276
-
3277
- this.save();
3278
-
3279
- return true;
3279
+ MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
3280
+ if (_.isObject(list_name)) {
3281
+ callback = value;
3280
3282
  }
3281
- return false;
3282
- };
3283
+ var data = this.remove_action(list_name, value);
3284
+ return this._send_request(data, callback);
3285
+ });
3283
3286
 
3284
- MixpanelPersistence.prototype.unregister = function(prop) {
3285
- if (prop in this['props']) {
3286
- delete this['props'][prop];
3287
- this.save();
3287
+ /*
3288
+ * Merge a given list with a list-valued people analytics property,
3289
+ * excluding duplicate values.
3290
+ *
3291
+ * ### Usage:
3292
+ *
3293
+ * // merge a value to a list, creating it if needed
3294
+ * mixpanel.people.union('pages_visited', 'homepage');
3295
+ *
3296
+ * // like mixpanel.people.set(), you can append multiple
3297
+ * // properties at once:
3298
+ * mixpanel.people.union({
3299
+ * list1: 'bob',
3300
+ * list2: 123
3301
+ * });
3302
+ *
3303
+ * // like mixpanel.people.append(), you can append multiple
3304
+ * // values to the same list:
3305
+ * mixpanel.people.union({
3306
+ * list1: ['bob', 'billy']
3307
+ * });
3308
+ *
3309
+ * @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.
3310
+ * @param {*} [value] Value / values to merge with the given property
3311
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3312
+ */
3313
+ MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) {
3314
+ if (_.isObject(list_name)) {
3315
+ callback = values;
3288
3316
  }
3289
- };
3317
+ var data = this.union_action(list_name, values);
3318
+ return this._send_request(data, callback);
3319
+ });
3290
3320
 
3291
- MixpanelPersistence.prototype._expire_notification_campaigns = _.safewrap(function() {
3292
- var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY],
3293
- EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN)
3294
- if (!campaigns_shown) {
3295
- return;
3296
- }
3297
- for (var campaign_id in campaigns_shown) {
3298
- if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) {
3299
- delete campaigns_shown[campaign_id];
3321
+ /*
3322
+ * Record that you have charged the current user a certain amount
3323
+ * of money. Charges recorded with track_charge() will appear in the
3324
+ * Mixpanel revenue report.
3325
+ *
3326
+ * ### Usage:
3327
+ *
3328
+ * // charge a user $50
3329
+ * mixpanel.people.track_charge(50);
3330
+ *
3331
+ * // charge a user $30.50 on the 2nd of january
3332
+ * mixpanel.people.track_charge(30.50, {
3333
+ * '$time': new Date('jan 1 2012')
3334
+ * });
3335
+ *
3336
+ * @param {Number} amount The amount of money charged to the current user
3337
+ * @param {Object} [properties] An associative array of properties associated with the charge
3338
+ * @param {Function} [callback] If provided, the callback will be called when the server responds
3339
+ */
3340
+ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) {
3341
+ if (!_.isNumber(amount)) {
3342
+ amount = parseFloat(amount);
3343
+ if (isNaN(amount)) {
3344
+ console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
3345
+ return;
3300
3346
  }
3301
3347
  }
3302
- if (_.isEmptyObject(campaigns_shown)) {
3303
- delete this['props'][CAMPAIGN_IDS_KEY];
3304
- }
3348
+
3349
+ return this.append('$transactions', _.extend({
3350
+ '$amount': amount
3351
+ }, properties), callback);
3305
3352
  });
3306
3353
 
3307
- MixpanelPersistence.prototype.update_campaign_params = function() {
3308
- if (!this.campaign_params_saved) {
3309
- this.register_once(_.info.campaignParams());
3310
- this.campaign_params_saved = true;
3354
+ /*
3355
+ * Permanently clear all revenue report transactions from the
3356
+ * current user's people analytics profile.
3357
+ *
3358
+ * ### Usage:
3359
+ *
3360
+ * mixpanel.people.clear_charges();
3361
+ *
3362
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3363
+ */
3364
+ MixpanelPeople.prototype.clear_charges = function(callback) {
3365
+ return this.set('$transactions', [], callback);
3366
+ };
3367
+
3368
+ /*
3369
+ * Permanently deletes the current people analytics profile from
3370
+ * Mixpanel (using the current distinct_id).
3371
+ *
3372
+ * ### Usage:
3373
+ *
3374
+ * // remove the all data you have stored about the current user
3375
+ * mixpanel.people.delete_user();
3376
+ *
3377
+ */
3378
+ MixpanelPeople.prototype.delete_user = function() {
3379
+ if (!this._identify_called()) {
3380
+ console.error('mixpanel.people.delete_user() requires you to call identify() first');
3381
+ return;
3311
3382
  }
3383
+ var data = {'$delete': this._mixpanel.get_distinct_id()};
3384
+ return this._send_request(data);
3312
3385
  };
3313
3386
 
3314
- MixpanelPersistence.prototype.update_search_keyword = function(referrer) {
3315
- this.register(_.info.searchInfo(referrer));
3387
+ MixpanelPeople.prototype.toString = function() {
3388
+ return this._mixpanel.toString() + '.people';
3316
3389
  };
3317
3390
 
3318
- // EXPORTED METHOD, we test this directly.
3319
- MixpanelPersistence.prototype.update_referrer_info = function(referrer) {
3320
- // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
3321
- this.register_once({
3322
- '$initial_referrer': referrer || '$direct',
3323
- '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct'
3324
- }, '');
3325
- };
3391
+ MixpanelPeople.prototype._send_request = function(data, callback) {
3392
+ data['$token'] = this._get_config('token');
3393
+ data['$distinct_id'] = this._mixpanel.get_distinct_id();
3394
+ var device_id = this._mixpanel.get_property('$device_id');
3395
+ var user_id = this._mixpanel.get_property('$user_id');
3396
+ var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id');
3397
+ if (device_id) {
3398
+ data['$device_id'] = device_id;
3399
+ }
3400
+ if (user_id) {
3401
+ data['$user_id'] = user_id;
3402
+ }
3403
+ if (had_persisted_distinct_id) {
3404
+ data['$had_persisted_distinct_id'] = had_persisted_distinct_id;
3405
+ }
3326
3406
 
3327
- MixpanelPersistence.prototype.get_referrer_info = function() {
3328
- return _.strip_empty_properties({
3329
- '$initial_referrer': this['props']['$initial_referrer'],
3330
- '$initial_referring_domain': this['props']['$initial_referring_domain']
3331
- });
3332
- };
3407
+ var date_encoded_data = _.encodeDates(data);
3333
3408
 
3334
- // safely fills the passed in object with stored properties,
3335
- // does not override any properties defined in both
3336
- // returns the passed in object
3337
- MixpanelPersistence.prototype.safe_merge = function(props) {
3338
- _.each(this['props'], function(val, prop) {
3339
- if (!(prop in props)) {
3340
- props[prop] = val;
3409
+ if (!this._identify_called()) {
3410
+ this._enqueue(data);
3411
+ if (!_.isUndefined(callback)) {
3412
+ if (this._get_config('verbose')) {
3413
+ callback({status: -1, error: null});
3414
+ } else {
3415
+ callback(-1);
3416
+ }
3341
3417
  }
3342
- });
3343
-
3344
- return props;
3345
- };
3346
-
3347
- MixpanelPersistence.prototype.update_config = function(config) {
3348
- this.default_expiry = this.expire_days = config['cookie_expiration'];
3349
- this.set_disabled(config['disable_persistence']);
3350
- this.set_cookie_domain(config['cookie_domain']);
3351
- this.set_cross_site(config['cross_site_cookie']);
3352
- this.set_cross_subdomain(config['cross_subdomain_cookie']);
3353
- this.set_secure(config['secure_cookie']);
3354
- };
3355
-
3356
- MixpanelPersistence.prototype.set_disabled = function(disabled) {
3357
- this.disabled = disabled;
3358
- if (this.disabled) {
3359
- this.remove();
3360
- } else {
3361
- this.save();
3362
- }
3363
- };
3364
-
3365
- MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) {
3366
- if (cookie_domain !== this.cookie_domain) {
3367
- this.remove();
3368
- this.cookie_domain = cookie_domain;
3369
- this.save();
3418
+ return _.truncate(date_encoded_data, 255);
3370
3419
  }
3371
- };
3372
3420
 
3373
- MixpanelPersistence.prototype.set_cross_site = function(cross_site) {
3374
- if (cross_site !== this.cross_site) {
3375
- this.cross_site = cross_site;
3376
- this.remove();
3377
- this.save();
3378
- }
3421
+ return this._mixpanel._track_or_batch({
3422
+ type: 'people',
3423
+ data: date_encoded_data,
3424
+ endpoint: this._get_config('api_host') + '/engage/',
3425
+ batcher: this._mixpanel.request_batchers.people
3426
+ }, callback);
3379
3427
  };
3380
3428
 
3381
- MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) {
3382
- if (cross_subdomain !== this.cross_subdomain) {
3383
- this.cross_subdomain = cross_subdomain;
3384
- this.remove();
3385
- this.save();
3386
- }
3429
+ MixpanelPeople.prototype._get_config = function(conf_var) {
3430
+ return this._mixpanel.get_config(conf_var);
3387
3431
  };
3388
3432
 
3389
- MixpanelPersistence.prototype.get_cross_subdomain = function() {
3390
- return this.cross_subdomain;
3433
+ MixpanelPeople.prototype._identify_called = function() {
3434
+ return this._mixpanel._flags.identify_called === true;
3391
3435
  };
3392
3436
 
3393
- MixpanelPersistence.prototype.set_secure = function(secure) {
3394
- if (secure !== this.secure) {
3395
- this.secure = secure ? true : false;
3396
- this.remove();
3397
- this.save();
3437
+ // Queue up engage operations if identify hasn't been called yet.
3438
+ MixpanelPeople.prototype._enqueue = function(data) {
3439
+ if (SET_ACTION in data) {
3440
+ this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data);
3441
+ } else if (SET_ONCE_ACTION in data) {
3442
+ this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data);
3443
+ } else if (UNSET_ACTION in data) {
3444
+ this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data);
3445
+ } else if (ADD_ACTION in data) {
3446
+ this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data);
3447
+ } else if (APPEND_ACTION in data) {
3448
+ this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data);
3449
+ } else if (REMOVE_ACTION in data) {
3450
+ this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data);
3451
+ } else if (UNION_ACTION in data) {
3452
+ this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
3453
+ } else {
3454
+ console.error('Invalid call to _enqueue():', data);
3398
3455
  }
3399
3456
  };
3400
3457
 
3401
- MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
3402
- var q_key = this._get_queue_key(queue),
3403
- q_data = data[queue],
3404
- set_q = this._get_or_create_queue(SET_ACTION),
3405
- set_once_q = this._get_or_create_queue(SET_ONCE_ACTION),
3406
- unset_q = this._get_or_create_queue(UNSET_ACTION),
3407
- add_q = this._get_or_create_queue(ADD_ACTION),
3408
- union_q = this._get_or_create_queue(UNION_ACTION),
3409
- remove_q = this._get_or_create_queue(REMOVE_ACTION, []),
3410
- append_q = this._get_or_create_queue(APPEND_ACTION, []);
3458
+ MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) {
3459
+ var _this = this;
3460
+ var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action));
3461
+ var action_params = queued_data;
3411
3462
 
3412
- if (q_key === SET_QUEUE_KEY) {
3413
- // Update the set queue - we can override any existing values
3414
- _.extend(set_q, q_data);
3415
- // if there was a pending increment, override it
3416
- // with the set.
3417
- this._pop_from_people_queue(ADD_ACTION, q_data);
3418
- // if there was a pending union, override it
3419
- // with the set.
3420
- this._pop_from_people_queue(UNION_ACTION, q_data);
3421
- this._pop_from_people_queue(UNSET_ACTION, q_data);
3422
- } else if (q_key === SET_ONCE_QUEUE_KEY) {
3423
- // only queue the data if there is not already a set_once call for it.
3424
- _.each(q_data, function(v, k) {
3425
- if (!(k in set_once_q)) {
3426
- set_once_q[k] = v;
3463
+ if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) {
3464
+ _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data);
3465
+ if (queue_to_params_fn) {
3466
+ action_params = queue_to_params_fn(queued_data);
3467
+ }
3468
+ action_method.call(_this, action_params, function(response, data) {
3469
+ // on bad response, we want to add it back to the queue
3470
+ if (response === 0) {
3471
+ _this._mixpanel['persistence']._add_to_people_queue(action, queued_data);
3472
+ }
3473
+ if (!_.isUndefined(callback)) {
3474
+ callback(response, data);
3427
3475
  }
3428
3476
  });
3429
- this._pop_from_people_queue(UNSET_ACTION, q_data);
3430
- } else if (q_key === UNSET_QUEUE_KEY) {
3431
- _.each(q_data, function(prop) {
3477
+ }
3478
+ };
3432
3479
 
3433
- // undo previously-queued actions on this key
3434
- _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) {
3435
- if (prop in enqueued_obj) {
3436
- delete enqueued_obj[prop];
3437
- }
3438
- });
3439
- _.each(append_q, function(append_obj) {
3440
- if (prop in append_obj) {
3441
- delete append_obj[prop];
3442
- }
3443
- });
3480
+ // Flush queued engage operations - order does not matter,
3481
+ // and there are network level race conditions anyway
3482
+ MixpanelPeople.prototype._flush = function(
3483
+ _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
3484
+ ) {
3485
+ var _this = this;
3486
+ var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION);
3487
+ var $remove_queue = this._mixpanel['persistence']._get_queue(REMOVE_ACTION);
3444
3488
 
3445
- unset_q[prop] = true;
3489
+ this._flush_one_queue(SET_ACTION, this.set, _set_callback);
3490
+ this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback);
3491
+ this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); });
3492
+ this._flush_one_queue(ADD_ACTION, this.increment, _add_callback);
3493
+ this._flush_one_queue(UNION_ACTION, this.union, _union_callback);
3446
3494
 
3447
- });
3448
- } else if (q_key === ADD_QUEUE_KEY) {
3449
- _.each(q_data, function(v, k) {
3450
- // If it exists in the set queue, increment
3451
- // the value
3452
- if (k in set_q) {
3453
- set_q[k] += v;
3454
- } else {
3455
- // If it doesn't exist, update the add
3456
- // queue
3457
- if (!(k in add_q)) {
3458
- add_q[k] = 0;
3459
- }
3460
- add_q[k] += v;
3495
+ // we have to fire off each $append individually since there is
3496
+ // no concat method server side
3497
+ if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
3498
+ var $append_item;
3499
+ var append_callback = function(response, data) {
3500
+ if (response === 0) {
3501
+ _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item);
3461
3502
  }
3462
- }, this);
3463
- this._pop_from_people_queue(UNSET_ACTION, q_data);
3464
- } else if (q_key === UNION_QUEUE_KEY) {
3465
- _.each(q_data, function(v, k) {
3466
- if (_.isArray(v)) {
3467
- if (!(k in union_q)) {
3468
- union_q[k] = [];
3469
- }
3470
- // We may send duplicates, the server will dedup them.
3471
- union_q[k] = union_q[k].concat(v);
3503
+ if (!_.isUndefined(_append_callback)) {
3504
+ _append_callback(response, data);
3472
3505
  }
3473
- });
3474
- this._pop_from_people_queue(UNSET_ACTION, q_data);
3475
- } else if (q_key === REMOVE_QUEUE_KEY) {
3476
- remove_q.push(q_data);
3477
- this._pop_from_people_queue(APPEND_ACTION, q_data);
3478
- } else if (q_key === APPEND_QUEUE_KEY) {
3479
- append_q.push(q_data);
3480
- this._pop_from_people_queue(UNSET_ACTION, q_data);
3506
+ };
3507
+ for (var i = $append_queue.length - 1; i >= 0; i--) {
3508
+ $append_item = $append_queue.pop();
3509
+ if (!_.isEmptyObject($append_item)) {
3510
+ _this.append($append_item, append_callback);
3511
+ }
3512
+ }
3513
+ // Save the shortened append queue
3514
+ _this._mixpanel['persistence'].save();
3481
3515
  }
3482
3516
 
3483
- console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3484
- console.log(data);
3485
-
3486
- this.save();
3487
- };
3488
-
3489
- MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) {
3490
- var q = this._get_queue(queue);
3491
- if (!_.isUndefined(q)) {
3492
- _.each(data, function(v, k) {
3493
- if (queue === APPEND_ACTION || queue === REMOVE_ACTION) {
3494
- // list actions: only remove if both k+v match
3495
- // e.g. remove should not override append in a case like
3496
- // append({foo: 'bar'}); remove({foo: 'qux'})
3497
- _.each(q, function(queued_action) {
3498
- if (queued_action[k] === v) {
3499
- delete queued_action[k];
3500
- }
3501
- });
3502
- } else {
3503
- delete q[k];
3517
+ // same for $remove
3518
+ if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) {
3519
+ var $remove_item;
3520
+ var remove_callback = function(response, data) {
3521
+ if (response === 0) {
3522
+ _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item);
3504
3523
  }
3505
- }, this);
3506
-
3507
- this.save();
3508
- }
3509
- };
3510
-
3511
- MixpanelPersistence.prototype._get_queue_key = function(queue) {
3512
- if (queue === SET_ACTION) {
3513
- return SET_QUEUE_KEY;
3514
- } else if (queue === SET_ONCE_ACTION) {
3515
- return SET_ONCE_QUEUE_KEY;
3516
- } else if (queue === UNSET_ACTION) {
3517
- return UNSET_QUEUE_KEY;
3518
- } else if (queue === ADD_ACTION) {
3519
- return ADD_QUEUE_KEY;
3520
- } else if (queue === APPEND_ACTION) {
3521
- return APPEND_QUEUE_KEY;
3522
- } else if (queue === REMOVE_ACTION) {
3523
- return REMOVE_QUEUE_KEY;
3524
- } else if (queue === UNION_ACTION) {
3525
- return UNION_QUEUE_KEY;
3526
- } else {
3527
- console.error('Invalid queue:', queue);
3524
+ if (!_.isUndefined(_remove_callback)) {
3525
+ _remove_callback(response, data);
3526
+ }
3527
+ };
3528
+ for (var j = $remove_queue.length - 1; j >= 0; j--) {
3529
+ $remove_item = $remove_queue.pop();
3530
+ if (!_.isEmptyObject($remove_item)) {
3531
+ _this.remove($remove_item, remove_callback);
3532
+ }
3533
+ }
3534
+ _this._mixpanel['persistence'].save();
3528
3535
  }
3529
3536
  };
3530
3537
 
3531
- MixpanelPersistence.prototype._get_queue = function(queue) {
3532
- return this['props'][this._get_queue_key(queue)];
3533
- };
3534
- MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) {
3535
- var key = this._get_queue_key(queue);
3536
- default_val = _.isUndefined(default_val) ? {} : default_val;
3537
-
3538
- return this['props'][key] || (this['props'][key] = default_val);
3539
- };
3540
-
3541
- MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) {
3542
- var timers = this['props'][EVENT_TIMERS_KEY] || {};
3543
- timers[event_name] = timestamp;
3544
- this['props'][EVENT_TIMERS_KEY] = timers;
3545
- this.save();
3538
+ MixpanelPeople.prototype._is_reserved_property = function(prop) {
3539
+ return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id';
3546
3540
  };
3547
3541
 
3548
- MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
3549
- var timers = this['props'][EVENT_TIMERS_KEY] || {};
3550
- var timestamp = timers[event_name];
3551
- if (!_.isUndefined(timestamp)) {
3552
- delete this['props'][EVENT_TIMERS_KEY][event_name];
3553
- this.save();
3554
- }
3555
- return timestamp;
3556
- };
3542
+ // MixpanelPeople Exports
3543
+ MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set;
3544
+ MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once;
3545
+ MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset;
3546
+ MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment;
3547
+ MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append;
3548
+ MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove;
3549
+ MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union;
3550
+ MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge;
3551
+ MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
3552
+ MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user;
3553
+ MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString;
3557
3554
 
3558
3555
  /*
3559
- * This file is a js implementation for a subset in eval_node.c
3556
+ * Constants
3560
3557
  */
3558
+ /** @const */ var SET_QUEUE_KEY = '__mps';
3559
+ /** @const */ var SET_ONCE_QUEUE_KEY = '__mpso';
3560
+ /** @const */ var UNSET_QUEUE_KEY = '__mpus';
3561
+ /** @const */ var ADD_QUEUE_KEY = '__mpa';
3562
+ /** @const */ var APPEND_QUEUE_KEY = '__mpap';
3563
+ /** @const */ var REMOVE_QUEUE_KEY = '__mpr';
3564
+ /** @const */ var UNION_QUEUE_KEY = '__mpu';
3565
+ // This key is deprecated, but we want to check for it to see whether aliasing is allowed.
3566
+ /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id';
3567
+ /** @const */ var ALIAS_ID_KEY = '__alias';
3568
+ /** @const */ var EVENT_TIMERS_KEY = '__timers';
3569
+ /** @const */ var RESERVED_PROPERTIES = [
3570
+ SET_QUEUE_KEY,
3571
+ SET_ONCE_QUEUE_KEY,
3572
+ UNSET_QUEUE_KEY,
3573
+ ADD_QUEUE_KEY,
3574
+ APPEND_QUEUE_KEY,
3575
+ REMOVE_QUEUE_KEY,
3576
+ UNION_QUEUE_KEY,
3577
+ PEOPLE_DISTINCT_ID_KEY,
3578
+ ALIAS_ID_KEY,
3579
+ EVENT_TIMERS_KEY
3580
+ ];
3561
3581
 
3562
- /*
3563
- * Constants
3582
+ /**
3583
+ * Mixpanel Persistence Object
3584
+ * @constructor
3564
3585
  */
3565
- // Metadata keys
3566
- /** @const */ var OPERATOR_KEY = 'operator';
3567
- /** @const */ var PROPERTY_KEY = 'property';
3568
- /** @const */ var WINDOW_KEY = 'window';
3569
- /** @const */ var UNIT_KEY = 'unit';
3570
- /** @const */ var VALUE_KEY = 'value';
3571
- /** @const */ var HOUR_KEY = 'hour';
3572
- /** @const */ var DAY_KEY = 'day';
3573
- /** @const */ var WEEK_KEY = 'week';
3574
- /** @const */ var MONTH_KEY = 'month';
3575
-
3576
- // Operands
3577
- /** @const */ var EVENT_PROPERTY = 'event';
3578
- /** @const */ var LITERAL_PROPERTY = 'literal';
3579
-
3580
- // Binary Operators
3581
- /** @const */ var AND_OPERATOR = 'and';
3582
- /** @const */ var OR_OPERATOR = 'or';
3583
- /** @const */ var IN_OPERATOR = 'in';
3584
- /** @const */ var NOT_IN_OPERATOR = 'not in';
3585
- /** @const */ var PLUS_OPERATOR = '+';
3586
- /** @const */ var MINUS_OPERATOR = '-';
3587
- /** @const */ var MUL_OPERATOR = '*';
3588
- /** @const */ var DIV_OPERATOR = '/';
3589
- /** @const */ var MOD_OPERATOR = '%';
3590
- /** @const */ var EQUALS_OPERATOR = '==';
3591
- /** @const */ var NOT_EQUALS_OPERATOR = '!=';
3592
- /** @const */ var GREATER_OPERATOR = '>';
3593
- /** @const */ var LESS_OPERATOR = '<';
3594
- /** @const */ var GREATER_EQUAL_OPERATOR = '>=';
3595
- /** @const */ var LESS_EQUAL_OPERATOR = '<=';
3596
-
3597
- // Typecast Operators
3598
- /** @const */ var BOOLEAN_OPERATOR = 'boolean';
3599
- /** @const */ var DATETIME_OPERATOR = 'datetime';
3600
- /** @const */ var LIST_OPERATOR = 'list';
3601
- /** @const */ var NUMBER_OPERATOR = 'number';
3602
- /** @const */ var STRING_OPERATOR = 'string';
3603
-
3604
- // Unary Operators
3605
- /** @const */ var NOT_OPERATOR = 'not';
3606
- /** @const */ var DEFINED_OPERATOR = 'defined';
3607
- /** @const */ var NOT_DEFINED_OPERATOR = 'not defined';
3608
-
3609
- // Special literals
3610
- /** @const */ var NOW_LITERAL = 'now';
3611
-
3612
- // Type cast functions
3613
- function toNumber(value) {
3614
- if (value === null) {
3615
- return null;
3616
- }
3586
+ var MixpanelPersistence = function(config) {
3587
+ this['props'] = {};
3588
+ this.campaign_params_saved = false;
3617
3589
 
3618
- switch (typeof(value)) {
3619
- case 'object':
3620
- if (_.isDate(value) && value.getTime() >= 0) {
3621
- return value.getTime();
3622
- }
3623
- return null;
3624
- case 'boolean':
3625
- return Number(value);
3626
- case 'number':
3627
- return value;
3628
- case 'string':
3629
- value = Number(value);
3630
- if (!isNaN(value)) {
3631
- return value;
3632
- }
3633
- return 0;
3590
+ if (config['persistence_name']) {
3591
+ this.name = 'mp_' + config['persistence_name'];
3592
+ } else {
3593
+ this.name = 'mp_' + config['token'] + '_mixpanel';
3634
3594
  }
3635
- return null;
3636
- }
3637
3595
 
3638
- function evaluateNumber(op, properties) {
3639
- if (!op['operator'] || op['operator'] !== NUMBER_OPERATOR || !op['children'] || op['children'].length !== 1) {
3640
- throw ('Invalid cast operator: number ' + op);
3596
+ var storage_type = config['persistence'];
3597
+ if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
3598
+ console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3599
+ storage_type = config['persistence'] = 'cookie';
3641
3600
  }
3642
3601
 
3643
- return toNumber(evaluateSelector(op['children'][0], properties));
3644
- }
3645
-
3646
- function toBoolean(value) {
3647
- if (value === null) {
3648
- return false;
3602
+ if (storage_type === 'localStorage' && _.localStorage.is_supported()) {
3603
+ this.storage = _.localStorage;
3604
+ } else {
3605
+ this.storage = _.cookie;
3649
3606
  }
3650
3607
 
3651
- switch (typeof value) {
3652
- case 'boolean':
3653
- return value;
3654
- case 'number':
3655
- return value !== 0.0;
3656
- case 'string':
3657
- return value.length > 0;
3658
- case 'object':
3659
- if (_.isArray(value) && value.length > 0) {
3660
- return true;
3661
- }
3662
- if (_.isDate(value) && value.getTime() > 0) {
3663
- return true;
3664
- }
3665
- if (_.isObject(value) && !_.isEmptyObject(value)) {
3666
- return true;
3667
- }
3668
- return false;
3669
- }
3670
- return false;
3671
- }
3608
+ this.load();
3609
+ this.update_config(config);
3610
+ this.upgrade(config);
3611
+ this.save();
3612
+ };
3672
3613
 
3673
- function evaluateBoolean(op, properties) {
3674
- if (!op['operator'] || op['operator'] !== BOOLEAN_OPERATOR || !op['children'] || op['children'].length !== 1) {
3675
- throw ('Invalid cast operator: boolean ' + op);
3676
- }
3614
+ MixpanelPersistence.prototype.properties = function() {
3615
+ var p = {};
3616
+ // Filter out reserved properties
3617
+ _.each(this['props'], function(v, k) {
3618
+ if (!_.include(RESERVED_PROPERTIES, k)) {
3619
+ p[k] = v;
3620
+ }
3621
+ });
3622
+ return p;
3623
+ };
3677
3624
 
3678
- return toBoolean(evaluateSelector(op['children'][0], properties));
3679
- }
3625
+ MixpanelPersistence.prototype.load = function() {
3626
+ if (this.disabled) { return; }
3680
3627
 
3681
- function evaluateDateTime(op, properties) {
3682
- if (!op['operator'] || op['operator'] !== DATETIME_OPERATOR || !op['children'] || op['children'].length !== 1) {
3683
- throw ('Invalid cast operator: datetime ' + op);
3684
- }
3628
+ var entry = this.storage.parse(this.name);
3685
3629
 
3686
- var v = evaluateSelector(op['children'][0], properties);
3687
- if (v === null) {
3688
- return null;
3630
+ if (entry) {
3631
+ this['props'] = _.extend({}, entry);
3689
3632
  }
3633
+ };
3690
3634
 
3691
- switch (typeof(v)) {
3692
- case 'number':
3693
- case 'string':
3694
- var d = new Date(v);
3695
- if (isNaN(d.getTime())) {
3696
- return null;
3697
- }
3698
- return d;
3699
- case 'object':
3700
- if (_.isDate(v)) {
3701
- return v;
3702
- }
3703
- }
3635
+ MixpanelPersistence.prototype.upgrade = function(config) {
3636
+ var upgrade_from_old_lib = config['upgrade'],
3637
+ old_cookie_name,
3638
+ old_cookie;
3704
3639
 
3705
- return null;
3706
- }
3640
+ if (upgrade_from_old_lib) {
3641
+ old_cookie_name = 'mp_super_properties';
3642
+ // Case where they had a custom cookie name before.
3643
+ if (typeof(upgrade_from_old_lib) === 'string') {
3644
+ old_cookie_name = upgrade_from_old_lib;
3645
+ }
3707
3646
 
3708
- function evaluateList(op, properties) {
3709
- if (!op['operator'] || op['operator'] !== LIST_OPERATOR || !op['children'] || op['children'].length !== 1) {
3710
- throw ('Invalid cast operator: list ' + op);
3711
- }
3647
+ old_cookie = this.storage.parse(old_cookie_name);
3712
3648
 
3713
- var v = evaluateSelector(op['children'][0], properties);
3714
- if (v === null) {
3715
- return null;
3716
- }
3649
+ // remove the cookie
3650
+ this.storage.remove(old_cookie_name);
3651
+ this.storage.remove(old_cookie_name, true);
3717
3652
 
3718
- if (_.isArray(v)) {
3719
- return v;
3653
+ if (old_cookie) {
3654
+ this['props'] = _.extend(
3655
+ this['props'],
3656
+ old_cookie['all'],
3657
+ old_cookie['events']
3658
+ );
3659
+ }
3720
3660
  }
3721
3661
 
3722
- return null;
3723
- }
3724
-
3725
- function evaluateString(op, properties) {
3726
- if (!op['operator'] || op['operator'] !== STRING_OPERATOR || !op['children'] || op['children'].length !== 1) {
3727
- throw ('Invalid cast operator: string ' + op);
3728
- }
3662
+ if (!config['cookie_name'] && config['name'] !== 'mixpanel') {
3663
+ // special case to handle people with cookies of the form
3664
+ // mp_TOKEN_INSTANCENAME from the first release of this library
3665
+ old_cookie_name = 'mp_' + config['token'] + '_' + config['name'];
3666
+ old_cookie = this.storage.parse(old_cookie_name);
3729
3667
 
3730
- var v = evaluateSelector(op['children'][0], properties);
3731
- switch (typeof(v)) {
3732
- case 'object':
3733
- if (_.isDate(v)) {
3734
- return v.toJSON();
3735
- }
3736
- return JSON.stringify(v);
3737
- }
3738
- return String(v);
3739
- }
3668
+ if (old_cookie) {
3669
+ this.storage.remove(old_cookie_name);
3670
+ this.storage.remove(old_cookie_name, true);
3740
3671
 
3741
- // Operators
3742
- function evaluateAnd(op, properties) {
3743
- if (!op['operator'] || op['operator'] !== AND_OPERATOR || !op['children'] || op['children'].length !== 2) {
3744
- throw ('Invalid operator: AND ' + op);
3672
+ // Save the prop values that were in the cookie from before -
3673
+ // this should only happen once as we delete the old one.
3674
+ this.register_once(old_cookie);
3675
+ }
3745
3676
  }
3746
3677
 
3747
- return toBoolean(evaluateSelector(op['children'][0], properties)) && toBoolean(evaluateSelector(op['children'][1], properties));
3748
- }
3749
-
3750
- function evaluateOr(op, properties) {
3751
- if (!op['operator'] || op['operator'] !== OR_OPERATOR || !op['children'] || op['children'].length !== 2) {
3752
- throw ('Invalid operator: OR ' + op);
3753
- }
3678
+ if (this.storage === _.localStorage) {
3679
+ old_cookie = _.cookie.parse(this.name);
3754
3680
 
3755
- return toBoolean(evaluateSelector(op['children'][0], properties)) || toBoolean(evaluateSelector(op['children'][1], properties));
3756
- }
3681
+ _.cookie.remove(this.name);
3682
+ _.cookie.remove(this.name, true);
3757
3683
 
3758
- function evaluateIn(op, properties) {
3759
- if (!op['operator'] || [IN_OPERATOR, NOT_IN_OPERATOR].indexOf(op['operator']) === -1 || !op['children'] || op['children'].length !== 2) {
3760
- throw ('Invalid operator: IN/NOT IN ' + op);
3684
+ if (old_cookie) {
3685
+ this.register_once(old_cookie);
3686
+ }
3761
3687
  }
3762
- var leftValue = evaluateSelector(op['children'][0], properties);
3763
- var rightValue = evaluateSelector(op['children'][1], properties);
3688
+ };
3764
3689
 
3765
- if (!_.isArray(rightValue) && !_.isString(rightValue)) {
3766
- throw ('Invalid operand for operator IN: invalid type' + rightValue);
3767
- }
3690
+ MixpanelPersistence.prototype.save = function() {
3691
+ if (this.disabled) { return; }
3692
+ this.storage.set(
3693
+ this.name,
3694
+ _.JSONEncode(this['props']),
3695
+ this.expire_days,
3696
+ this.cross_subdomain,
3697
+ this.secure,
3698
+ this.cross_site,
3699
+ this.cookie_domain
3700
+ );
3701
+ };
3768
3702
 
3769
- var v = rightValue.indexOf(leftValue) > -1;
3770
- if (op['operator'] === NOT_IN_OPERATOR) {
3771
- return !v;
3772
- }
3773
- return v;
3774
- }
3703
+ MixpanelPersistence.prototype.remove = function() {
3704
+ // remove both domain and subdomain cookies
3705
+ this.storage.remove(this.name, false, this.cookie_domain);
3706
+ this.storage.remove(this.name, true, this.cookie_domain);
3707
+ };
3775
3708
 
3776
- function evaluatePlus(op, properties) {
3777
- if (!op['operator'] || op['operator'] !== PLUS_OPERATOR || !op['children'] || op['children'].length < 2) {
3778
- throw ('Invalid operator: PLUS ' + op);
3779
- }
3780
- var l = evaluateSelector(op['children'][0], properties);
3781
- var r = evaluateSelector(op['children'][1], properties);
3782
-
3783
- if (typeof l === 'number' && typeof r === 'number') {
3784
- return l + r;
3785
- }
3786
- if (typeof l === 'string' && typeof r === 'string') {
3787
- return l + r;
3788
- }
3789
- return null;
3790
- }
3791
-
3792
- function evaluateArithmetic(op, properties) {
3793
- if (!op['operator'] || [MINUS_OPERATOR, MUL_OPERATOR, DIV_OPERATOR, MOD_OPERATOR].indexOf(op['operator']) === -1 ||
3794
- !op['children'] || op['children'].length < 2) {
3795
- throw ('Invalid arithmetic operator ' + op);
3796
- }
3797
-
3798
- var l = evaluateSelector(op['children'][0], properties);
3799
- var r = evaluateSelector(op['children'][1], properties);
3800
-
3801
- if (typeof l === 'number' && typeof r === 'number') {
3802
- switch (op['operator']) {
3803
- case MINUS_OPERATOR:
3804
- return l - r;
3805
- case MUL_OPERATOR:
3806
- return l * r;
3807
- case DIV_OPERATOR:
3808
- if (r !== 0) {
3809
- return l / r;
3810
- }
3811
- return null;
3812
- case MOD_OPERATOR:
3813
- if (r === 0) {
3814
- return null;
3815
- }
3816
- if (l === 0) {
3817
- return 0;
3818
- }
3819
- if ((l < 0 && r > 0) || (l > 0 && r < 0)) {
3820
- /* Mimic python modulo - result takes sign of the divisor
3821
- * if one operand is negative. */
3822
- return -(Math.floor(l / r) * r - l);
3823
- }
3824
- return l % r;
3825
- default:
3826
- throw('Unknown operator: ' + op['operator']);
3827
- }
3828
- }
3829
-
3830
- return null;
3831
- }
3832
-
3833
- function _isArrayEqual(l, r) {
3834
- if (l === r) return true;
3835
- if (l === null || r === null) return false;
3836
- if (l.length !== r.length) return false;
3837
-
3838
- for (var i = 0; i < l.length; i++) {
3839
- if (l[i] !== r[i]) {
3840
- return false;
3841
- }
3842
- }
3843
-
3844
- return true;
3845
- }
3846
-
3847
- function _isEqual(l, r) {
3848
- if ( l === null && l === r ) {
3849
- return true;
3850
- }
3851
- if (typeof l === typeof r) {
3852
- switch (typeof l) {
3853
- case 'number':
3854
- case 'string':
3855
- case 'boolean':
3856
- return l === r;
3857
- case 'object':
3858
- if (_.isArray(l) && _.isArray(r)) {
3859
- return _isArrayEqual(l, r);
3860
- }
3861
- if (_.isDate(l) && _.isDate(r)) {
3862
- return l.getTime() === r.getTime();
3863
- }
3864
- if (_.isObject(l) && _.isObject(r)) {
3865
- return JSON.stringify(l) === JSON.stringify(r);
3866
- }
3867
- }
3868
- }
3869
- return false;
3870
- }
3871
-
3872
- function evaluateEquality(op, properties) {
3873
- if (!op['operator'] || [EQUALS_OPERATOR, NOT_EQUALS_OPERATOR].indexOf(op['operator']) === -1 || !op['children'] || op['children'].length !== 2) {
3874
- throw ('Invalid equality operator ' + op);
3875
- }
3876
-
3877
- var v = _isEqual(evaluateSelector(op['children'][0], properties), evaluateSelector(op['children'][1], properties));
3878
-
3879
- switch (op['operator']) {
3880
- case EQUALS_OPERATOR:
3881
- return v;
3882
- case NOT_EQUALS_OPERATOR:
3883
- return !v;
3884
- }
3885
- }
3886
-
3887
- function evaluateComparison(op, properties) {
3888
- if (!op['operator'] ||
3889
- [GREATER_OPERATOR, GREATER_EQUAL_OPERATOR, LESS_OPERATOR, LESS_EQUAL_OPERATOR].indexOf(op['operator']) === -1 ||
3890
- !op['children'] || op['children'].length !== 2) {
3891
- throw ('Invalid comparison operator ' + op);
3892
- }
3893
- var l = evaluateSelector(op['children'][0], properties);
3894
- var r = evaluateSelector(op['children'][1], properties);
3895
-
3896
- if (typeof(l) === typeof(r)) {
3897
- if (typeof(r) === 'number' || _.isDate(r)) {
3898
- l = toNumber(l);
3899
- r = toNumber(r);
3900
- switch (op['operator']) {
3901
- case GREATER_OPERATOR:
3902
- return l > r;
3903
- case GREATER_EQUAL_OPERATOR:
3904
- return l >= r;
3905
- case LESS_OPERATOR:
3906
- return l < r;
3907
- case LESS_EQUAL_OPERATOR:
3908
- return l <= r;
3909
- }
3910
- } else if (typeof(r) === 'string') {
3911
- var compare = l.localeCompare(r);
3912
- switch (op['operator']) {
3913
- case GREATER_OPERATOR:
3914
- return compare > 0;
3915
- case GREATER_EQUAL_OPERATOR:
3916
- return compare >= 0;
3917
- case LESS_OPERATOR:
3918
- return compare < 0;
3919
- case LESS_EQUAL_OPERATOR:
3920
- return compare <= 0;
3921
- }
3922
- }
3923
- }
3924
-
3925
- return null;
3926
- }
3927
-
3928
- function evaluateDefined(op, properties) {
3929
- if (!op['operator'] || [DEFINED_OPERATOR, NOT_DEFINED_OPERATOR].indexOf(op['operator']) === -1 ||
3930
- !op['children'] || op['children'].length !== 1) {
3931
- throw ('Invalid defined/not defined operator: ' + op);
3932
- }
3933
-
3934
- var b = evaluateSelector(op['children'][0], properties) !== null;
3935
- if (op['operator'] === NOT_DEFINED_OPERATOR) {
3936
- return !b;
3937
- }
3938
-
3939
- return b;
3940
- }
3941
-
3942
- function evaluateNot(op, properties) {
3943
- if (!op['operator'] || op['operator'] !== NOT_OPERATOR || !op['children'] || op['children'].length !== 1) {
3944
- throw ('Invalid not operator: ' + op);
3945
- }
3946
-
3947
- var v = evaluateSelector(op['children'][0], properties);
3948
- if (v === null) {
3949
- return true;
3950
- }
3951
-
3952
- if (typeof(v) === 'boolean') {
3953
- return !v;
3954
- }
3955
-
3956
- return null;
3957
- }
3958
-
3959
- function evaluateOperator(op, properties) {
3960
- if (!op['operator']) {
3961
- throw ('Invalid operator: operator key missing ' + op);
3962
- }
3963
-
3964
- switch (op['operator']) {
3965
- case AND_OPERATOR:
3966
- return evaluateAnd(op, properties);
3967
- case OR_OPERATOR:
3968
- return evaluateOr(op, properties);
3969
- case IN_OPERATOR:
3970
- case NOT_IN_OPERATOR:
3971
- return evaluateIn(op, properties);
3972
- case PLUS_OPERATOR:
3973
- return evaluatePlus(op, properties);
3974
- case MINUS_OPERATOR:
3975
- case MUL_OPERATOR:
3976
- case DIV_OPERATOR:
3977
- case MOD_OPERATOR:
3978
- return evaluateArithmetic(op, properties);
3979
- case EQUALS_OPERATOR:
3980
- case NOT_EQUALS_OPERATOR:
3981
- return evaluateEquality(op, properties);
3982
- case GREATER_OPERATOR:
3983
- case LESS_OPERATOR:
3984
- case GREATER_EQUAL_OPERATOR:
3985
- case LESS_EQUAL_OPERATOR:
3986
- return evaluateComparison(op, properties);
3987
- case BOOLEAN_OPERATOR:
3988
- return evaluateBoolean(op, properties);
3989
- case DATETIME_OPERATOR:
3990
- return evaluateDateTime(op, properties);
3991
- case LIST_OPERATOR:
3992
- return evaluateList(op, properties);
3993
- case NUMBER_OPERATOR:
3994
- return evaluateNumber(op, properties);
3995
- case STRING_OPERATOR:
3996
- return evaluateString(op, properties);
3997
- case DEFINED_OPERATOR:
3998
- case NOT_DEFINED_OPERATOR:
3999
- return evaluateDefined(op, properties);
4000
- case NOT_OPERATOR:
4001
- return evaluateNot(op, properties);
4002
- }
4003
- }
4004
-
4005
- function evaluateWindow(value) {
4006
- var win = value[WINDOW_KEY];
4007
- if (!win || !win[UNIT_KEY] || !win[VALUE_KEY]) {
4008
- throw('Invalid window: missing required keys ' + JSON.stringify(value));
4009
- }
4010
- var out = new Date();
4011
- switch (win[UNIT_KEY]) {
4012
- case HOUR_KEY:
4013
- out.setTime(out.getTime() + (win[VALUE_KEY]*-1*60*60*1000));
4014
- break;
4015
- case DAY_KEY:
4016
- out.setTime(out.getTime() + (win[VALUE_KEY]*-1*24*60*60*1000));
4017
- break;
4018
- case WEEK_KEY:
4019
- out.setTime(out.getTime() + (win[VALUE_KEY]*-1*7*24*60*60*1000));
4020
- break;
4021
- case MONTH_KEY:
4022
- out.setTime(out.getTime() + (win[VALUE_KEY]*-1*30*24*60*60*1000));
4023
- break;
4024
- default:
4025
- throw('Invalid unit: ' + win[UNIT_KEY]);
4026
- }
4027
-
4028
- return out;
4029
- }
4030
-
4031
- function evaluateOperand(op, properties) {
4032
- if (!op['property'] || !op['value']) {
4033
- throw('Invalid operand: missing required keys ' + op);
4034
- }
4035
- switch (op['property']) {
4036
- case EVENT_PROPERTY:
4037
- if (properties[op['value']] !== undefined) {
4038
- return properties[op['value']];
4039
- }
4040
- return null;
4041
- case LITERAL_PROPERTY:
4042
- if (op['value'] === NOW_LITERAL) {
4043
- return new Date();
4044
- }
4045
- if (typeof(op['value']) === 'object') {
4046
- return evaluateWindow(op['value']);
4047
- }
4048
- return op['value'];
4049
- default:
4050
- throw('Invalid operand: Invalid property type ' + op['property']);
4051
- }
4052
- }
4053
-
4054
- function evaluateSelector(filters, properties) {
4055
- if (filters[PROPERTY_KEY]) {
4056
- return evaluateOperand(filters, properties);
4057
- }
4058
- if (filters[OPERATOR_KEY]) {
4059
- return evaluateOperator(filters, properties);
4060
- }
4061
- }
4062
-
4063
- // Internal class for notification display
4064
-
4065
- var MixpanelNotification = function(notif_data, mixpanel_instance) {
4066
- _.bind_instance_methods(this);
4067
-
4068
- this.mixpanel = mixpanel_instance;
4069
- this.persistence = this.mixpanel['persistence'];
4070
- this.resource_protocol = this.mixpanel.get_config('inapp_protocol');
4071
- this.cdn_host = this.mixpanel.get_config('cdn');
4072
-
4073
- this.campaign_id = _.escapeHTML(notif_data['id']);
4074
- this.message_id = _.escapeHTML(notif_data['message_id']);
4075
-
4076
- this.body = (_.escapeHTML(notif_data['body']) || '').replace(/\n/g, '<br/>');
4077
- this.cta = _.escapeHTML(notif_data['cta']) || 'Close';
4078
- this.notif_type = _.escapeHTML(notif_data['type']) || 'takeover';
4079
- this.style = _.escapeHTML(notif_data['style']) || 'light';
4080
- this.title = _.escapeHTML(notif_data['title']) || '';
4081
- this.video_width = MixpanelNotification.VIDEO_WIDTH;
4082
- this.video_height = MixpanelNotification.VIDEO_HEIGHT;
4083
-
4084
- this.display_triggers = notif_data['display_triggers'] || [];
4085
-
4086
- // These fields are url-sanitized in the backend already.
4087
- this.dest_url = notif_data['cta_url'] || null;
4088
- this.image_url = notif_data['image_url'] || null;
4089
- this.thumb_image_url = notif_data['thumb_image_url'] || null;
4090
- this.video_url = notif_data['video_url'] || null;
4091
-
4092
- if (this.thumb_image_url && this.thumb_image_url.indexOf('//') === 0) {
4093
- this.thumb_image_url = this.thumb_image_url.replace('//', this.resource_protocol);
4094
- }
4095
-
4096
- this.clickthrough = true;
4097
- if (!this.dest_url) {
4098
- this.dest_url = '#dismiss';
4099
- this.clickthrough = false;
4100
- }
4101
-
4102
- this.mini = this.notif_type === 'mini';
4103
- if (!this.mini) {
4104
- this.notif_type = 'takeover';
4105
- }
4106
- this.notif_width = !this.mini ? MixpanelNotification.NOTIF_WIDTH : MixpanelNotification.NOTIF_WIDTH_MINI;
4107
-
4108
- this._set_client_config();
4109
- this.imgs_to_preload = this._init_image_html();
4110
- this._init_video();
4111
- };
4112
-
4113
- MixpanelNotification.ANIM_TIME = 200;
4114
- MixpanelNotification.MARKUP_PREFIX = 'mixpanel-notification';
4115
- MixpanelNotification.BG_OPACITY = 0.6;
4116
- MixpanelNotification.NOTIF_TOP = 25;
4117
- MixpanelNotification.NOTIF_START_TOP = 200;
4118
- MixpanelNotification.NOTIF_WIDTH = 388;
4119
- MixpanelNotification.NOTIF_WIDTH_MINI = 420;
4120
- MixpanelNotification.NOTIF_HEIGHT_MINI = 85;
4121
- MixpanelNotification.THUMB_BORDER_SIZE = 5;
4122
- MixpanelNotification.THUMB_IMG_SIZE = 60;
4123
- MixpanelNotification.THUMB_OFFSET = Math.round(MixpanelNotification.THUMB_IMG_SIZE / 2);
4124
- MixpanelNotification.VIDEO_WIDTH = 595;
4125
- MixpanelNotification.VIDEO_HEIGHT = 334;
4126
-
4127
- MixpanelNotification.prototype.show = function() {
4128
- var self = this;
4129
- this._set_client_config();
4130
-
4131
- // don't display until HTML body exists
4132
- if (!this.body_el) {
4133
- setTimeout(function() { self.show(); }, 300);
4134
- return;
4135
- }
4136
-
4137
- this._init_styles();
4138
- this._init_notification_el();
4139
-
4140
- // wait for any images to load before showing notification
4141
- this._preload_images(this._attach_and_animate);
4142
- };
4143
-
4144
- MixpanelNotification.prototype.dismiss = _.safewrap(function() {
4145
- if (!this.marked_as_shown) {
4146
- // unexpected condition: user interacted with notif even though we didn't consider it
4147
- // visible (see _mark_as_shown()); send tracking signals to mark delivery
4148
- this._mark_delivery({'invisible': true});
4149
- }
4150
-
4151
- var exiting_el = this.showing_video ? this._get_el('video') : this._get_notification_display_el();
4152
- if (this.use_transitions) {
4153
- this._remove_class('bg', 'visible');
4154
- this._add_class(exiting_el, 'exiting');
4155
- setTimeout(this._remove_notification_el, MixpanelNotification.ANIM_TIME);
4156
- } else {
4157
- var notif_attr, notif_start, notif_goal;
4158
- if (this.mini) {
4159
- notif_attr = 'right';
4160
- notif_start = 20;
4161
- notif_goal = -100;
4162
- } else {
4163
- notif_attr = 'top';
4164
- notif_start = MixpanelNotification.NOTIF_TOP;
4165
- notif_goal = MixpanelNotification.NOTIF_START_TOP + MixpanelNotification.NOTIF_TOP;
4166
- }
4167
- this._animate_els([
4168
- {
4169
- el: this._get_el('bg'),
4170
- attr: 'opacity',
4171
- start: MixpanelNotification.BG_OPACITY,
4172
- goal: 0.0
4173
- },
4174
- {
4175
- el: exiting_el,
4176
- attr: 'opacity',
4177
- start: 1.0,
4178
- goal: 0.0
4179
- },
4180
- {
4181
- el: exiting_el,
4182
- attr: notif_attr,
4183
- start: notif_start,
4184
- goal: notif_goal
4185
- }
4186
- ], MixpanelNotification.ANIM_TIME, this._remove_notification_el);
4187
- }
4188
- });
4189
-
4190
- MixpanelNotification.prototype._add_class = _.safewrap(function(el, class_name) {
4191
- class_name = MixpanelNotification.MARKUP_PREFIX + '-' + class_name;
4192
- if (typeof el === 'string') {
4193
- el = this._get_el(el);
4194
- }
4195
- if (!el.className) {
4196
- el.className = class_name;
4197
- } else if (!~(' ' + el.className + ' ').indexOf(' ' + class_name + ' ')) {
4198
- el.className += ' ' + class_name;
4199
- }
4200
- });
4201
- MixpanelNotification.prototype._remove_class = _.safewrap(function(el, class_name) {
4202
- class_name = MixpanelNotification.MARKUP_PREFIX + '-' + class_name;
4203
- if (typeof el === 'string') {
4204
- el = this._get_el(el);
4205
- }
4206
- if (el.className) {
4207
- el.className = (' ' + el.className + ' ')
4208
- .replace(' ' + class_name + ' ', '')
4209
- .replace(/^[\s\xA0]+/, '')
4210
- .replace(/[\s\xA0]+$/, '');
4211
- }
4212
- });
4213
-
4214
- MixpanelNotification.prototype._animate_els = _.safewrap(function(anims, mss, done_cb, start_time) {
4215
- var self = this,
4216
- in_progress = false,
4217
- ai, anim,
4218
- cur_time = 1 * new Date(), time_diff;
4219
-
4220
- start_time = start_time || cur_time;
4221
- time_diff = cur_time - start_time;
4222
-
4223
- for (ai = 0; ai < anims.length; ai++) {
4224
- anim = anims[ai];
4225
- if (typeof anim.val === 'undefined') {
4226
- anim.val = anim.start;
4227
- }
4228
- if (anim.val !== anim.goal) {
4229
- in_progress = true;
4230
- var anim_diff = anim.goal - anim.start,
4231
- anim_dir = anim.goal >= anim.start ? 1 : -1;
4232
- anim.val = anim.start + anim_diff * time_diff / mss;
4233
- if (anim.attr !== 'opacity') {
4234
- anim.val = Math.round(anim.val);
4235
- }
4236
- if ((anim_dir > 0 && anim.val >= anim.goal) || (anim_dir < 0 && anim.val <= anim.goal)) {
4237
- anim.val = anim.goal;
4238
- }
4239
- }
4240
- }
4241
- if (!in_progress) {
4242
- if (done_cb) {
4243
- done_cb();
4244
- }
4245
- return;
4246
- }
4247
-
4248
- for (ai = 0; ai < anims.length; ai++) {
4249
- anim = anims[ai];
4250
- if (anim.el) {
4251
- var suffix = anim.attr === 'opacity' ? '' : 'px';
4252
- anim.el.style[anim.attr] = String(anim.val) + suffix;
4253
- }
4254
- }
4255
- setTimeout(function() { self._animate_els(anims, mss, done_cb, start_time); }, 10);
4256
- });
4257
-
4258
- MixpanelNotification.prototype._attach_and_animate = _.safewrap(function() {
4259
- var self = this;
4260
-
4261
- // no possibility to double-display
4262
- if (this.shown || this._get_shown_campaigns()[this.campaign_id]) {
4263
- return;
4264
- }
4265
- this.shown = true;
4266
-
4267
- this.body_el.appendChild(this.notification_el);
4268
- setTimeout(function() {
4269
- var notif_el = self._get_notification_display_el();
4270
- if (self.use_transitions) {
4271
- if (!self.mini) {
4272
- self._add_class('bg', 'visible');
4273
- }
4274
- self._add_class(notif_el, 'visible');
4275
- self._mark_as_shown();
4276
- } else {
4277
- var notif_attr, notif_start, notif_goal;
4278
- if (self.mini) {
4279
- notif_attr = 'right';
4280
- notif_start = -100;
4281
- notif_goal = 20;
4282
- } else {
4283
- notif_attr = 'top';
4284
- notif_start = MixpanelNotification.NOTIF_START_TOP + MixpanelNotification.NOTIF_TOP;
4285
- notif_goal = MixpanelNotification.NOTIF_TOP;
4286
- }
4287
- self._animate_els([
4288
- {
4289
- el: self._get_el('bg'),
4290
- attr: 'opacity',
4291
- start: 0.0,
4292
- goal: MixpanelNotification.BG_OPACITY
4293
- },
4294
- {
4295
- el: notif_el,
4296
- attr: 'opacity',
4297
- start: 0.0,
4298
- goal: 1.0
4299
- },
4300
- {
4301
- el: notif_el,
4302
- attr: notif_attr,
4303
- start: notif_start,
4304
- goal: notif_goal
4305
- }
4306
- ], MixpanelNotification.ANIM_TIME, self._mark_as_shown);
4307
- }
4308
- }, 100);
4309
- _.register_event(self._get_el('cancel'), 'click', function(e) {
4310
- e.preventDefault();
4311
- self.dismiss();
4312
- });
4313
- var click_el = self._get_el('button') ||
4314
- self._get_el('mini-content');
4315
- _.register_event(click_el, 'click', function(e) {
4316
- e.preventDefault();
4317
- if (self.show_video) {
4318
- self._track_event('$campaign_open', {'$resource_type': 'video'});
4319
- self._switch_to_video();
4320
- } else {
4321
- self.dismiss();
4322
- if (self.clickthrough) {
4323
- var tracking_cb = null;
4324
- if (self.mixpanel.get_config('inapp_link_new_window')) {
4325
- window.open(self.dest_url);
4326
- } else {
4327
- tracking_cb = function() {
4328
- window.location.href = self.dest_url;
4329
- };
4330
- }
4331
- self._track_event('$campaign_open', {'$resource_type': 'link'}, tracking_cb);
4332
- }
4333
- }
4334
- });
4335
- });
4336
-
4337
- MixpanelNotification.prototype._get_el = function(id) {
4338
- return document.getElementById(MixpanelNotification.MARKUP_PREFIX + '-' + id);
4339
- };
4340
-
4341
- MixpanelNotification.prototype._get_notification_display_el = function() {
4342
- return this._get_el(this.notif_type);
4343
- };
4344
-
4345
- MixpanelNotification.prototype._get_shown_campaigns = function() {
4346
- return this.persistence['props'][CAMPAIGN_IDS_KEY] || (this.persistence['props'][CAMPAIGN_IDS_KEY] = {});
4347
- };
4348
-
4349
- MixpanelNotification.prototype._matches_event_data = _.safewrap(function(event_data) {
4350
- var event_name = event_data['event'] || '';
4351
- for (var i = 0; i < this.display_triggers.length; i++) {
4352
- var display_trigger = this.display_triggers[i];
4353
- var match_event = display_trigger['event'] || '';
4354
- if (match_event === '$any_event' || event_name === display_trigger['event']) {
4355
- if (display_trigger['selector'] && !_.isEmptyObject(display_trigger['selector'])) {
4356
- if (evaluateSelector(display_trigger['selector'], event_data['properties'])) {
4357
- return true;
4358
- }
4359
- } else {
4360
- return true;
4361
- }
4362
- }
4363
- }
4364
- return false;
4365
- });
4366
-
4367
-
4368
- MixpanelNotification.prototype._browser_lte = function(browser, version) {
4369
- return this.browser_versions[browser] && this.browser_versions[browser] <= version;
4370
- };
4371
-
4372
- MixpanelNotification.prototype._init_image_html = function() {
4373
- var imgs_to_preload = [];
4374
-
4375
- if (!this.mini) {
4376
- if (this.image_url) {
4377
- imgs_to_preload.push(this.image_url);
4378
- this.img_html = '<img id="img" src="' + this.image_url + '"/>';
4379
- } else {
4380
- this.img_html = '';
4381
- }
4382
- if (this.thumb_image_url) {
4383
- imgs_to_preload.push(this.thumb_image_url);
4384
- this.thumb_img_html =
4385
- '<div id="thumbborder-wrapper"><div id="thumbborder"></div></div>' +
4386
- '<img id="thumbnail"' +
4387
- ' src="' + this.thumb_image_url + '"' +
4388
- ' width="' + MixpanelNotification.THUMB_IMG_SIZE + '"' +
4389
- ' height="' + MixpanelNotification.THUMB_IMG_SIZE + '"' +
4390
- '/>' +
4391
- '<div id="thumbspacer"></div>';
4392
- } else {
4393
- this.thumb_img_html = '';
4394
- }
4395
- } else {
4396
- this.thumb_image_url = this.thumb_image_url || (this.cdn_host + '/site_media/images/icons/notifications/mini-news-dark.png');
4397
- imgs_to_preload.push(this.thumb_image_url);
4398
- }
4399
-
4400
- return imgs_to_preload;
4401
- };
4402
-
4403
- MixpanelNotification.prototype._init_notification_el = function() {
4404
- var notification_html = '';
4405
- var video_src = '';
4406
- var video_html = '';
4407
- var cancel_html = '<div id="cancel">' +
4408
- '<div id="cancel-icon"></div>' +
4409
- '</div>';
4410
-
4411
- this.notification_el = document.createElement('div');
4412
- this.notification_el.id = MixpanelNotification.MARKUP_PREFIX + '-wrapper';
4413
- if (!this.mini) {
4414
- // TAKEOVER notification
4415
- var close_html = (this.clickthrough || this.show_video) ? '' : '<div id="button-close"></div>',
4416
- play_html = this.show_video ? '<div id="button-play"></div>' : '';
4417
- if (this._browser_lte('ie', 7)) {
4418
- close_html = '';
4419
- play_html = '';
4420
- }
4421
- notification_html =
4422
- '<div id="takeover">' +
4423
- this.thumb_img_html +
4424
- '<div id="mainbox">' +
4425
- cancel_html +
4426
- '<div id="content">' +
4427
- this.img_html +
4428
- '<div id="title">' + this.title + '</div>' +
4429
- '<div id="body">' + this.body + '</div>' +
4430
- '<div id="tagline">' +
4431
- '<a href="http://mixpanel.com?from=inapp" target="_blank">POWERED BY MIXPANEL</a>' +
4432
- '</div>' +
4433
- '</div>' +
4434
- '<div id="button">' +
4435
- close_html +
4436
- '<a id="button-link" href="' + this.dest_url + '">' + this.cta + '</a>' +
4437
- play_html +
4438
- '</div>' +
4439
- '</div>' +
4440
- '</div>';
4441
- } else {
4442
- // MINI notification
4443
- notification_html =
4444
- '<div id="mini">' +
4445
- '<div id="mainbox">' +
4446
- cancel_html +
4447
- '<div id="mini-content">' +
4448
- '<div id="mini-icon">' +
4449
- '<div id="mini-icon-img"></div>' +
4450
- '</div>' +
4451
- '<div id="body">' +
4452
- '<div id="body-text"><div>' + this.body + '</div></div>' +
4453
- '</div>' +
4454
- '</div>' +
4455
- '</div>' +
4456
- '<div id="mini-border"></div>' +
4457
- '</div>';
4458
- }
4459
- if (this.youtube_video) {
4460
- video_src = this.resource_protocol + 'www.youtube.com/embed/' + this.youtube_video +
4461
- '?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080';
4462
- if (this.yt_custom) {
4463
- video_src += '&enablejsapi=1&html5=1&controls=0';
4464
- video_html =
4465
- '<div id="video-controls">' +
4466
- '<div id="video-progress" class="video-progress-el">' +
4467
- '<div id="video-progress-total" class="video-progress-el"></div>' +
4468
- '<div id="video-elapsed" class="video-progress-el"></div>' +
4469
- '</div>' +
4470
- '<div id="video-time" class="video-progress-el"></div>' +
4471
- '</div>';
4472
- }
4473
- } else if (this.vimeo_video) {
4474
- video_src = this.resource_protocol + 'player.vimeo.com/video/' + this.vimeo_video + '?autoplay=1&title=0&byline=0&portrait=0';
4475
- }
4476
- if (this.show_video) {
4477
- this.video_iframe =
4478
- '<iframe id="' + MixpanelNotification.MARKUP_PREFIX + '-video-frame" ' +
4479
- 'width="' + this.video_width + '" height="' + this.video_height + '" ' +
4480
- ' src="' + video_src + '"' +
4481
- ' frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen="1" scrolling="no"' +
4482
- '></iframe>';
4483
- video_html =
4484
- '<div id="video-' + (this.flip_animate ? '' : 'no') + 'flip">' +
4485
- '<div id="video">' +
4486
- '<div id="video-holder"></div>' +
4487
- video_html +
4488
- '</div>' +
4489
- '</div>';
4490
- }
4491
- var main_html = video_html + notification_html;
4492
- if (this.flip_animate) {
4493
- main_html =
4494
- (this.mini ? notification_html : '') +
4495
- '<div id="flipcontainer"><div id="flipper">' +
4496
- (this.mini ? video_html : main_html) +
4497
- '</div></div>';
4498
- }
4499
-
4500
- this.notification_el.innerHTML =
4501
- ('<div id="overlay" class="' + this.notif_type + '">' +
4502
- '<div id="campaignid-' + this.campaign_id + '">' +
4503
- '<div id="bgwrapper">' +
4504
- '<div id="bg"></div>' +
4505
- main_html +
4506
- '</div>' +
4507
- '</div>' +
4508
- '</div>')
4509
- .replace(/class="/g, 'class="' + MixpanelNotification.MARKUP_PREFIX + '-')
4510
- .replace(/id="/g, 'id="' + MixpanelNotification.MARKUP_PREFIX + '-');
4511
- };
4512
-
4513
- MixpanelNotification.prototype._init_styles = function() {
4514
- if (this.style === 'dark') {
4515
- this.style_vals = {
4516
- bg: '#1d1f25',
4517
- bg_actions: '#282b32',
4518
- bg_hover: '#3a4147',
4519
- bg_light: '#4a5157',
4520
- border_gray: '#32353c',
4521
- cancel_opacity: '0.4',
4522
- mini_hover: '#2a3137',
4523
- text_title: '#fff',
4524
- text_main: '#9498a3',
4525
- text_tagline: '#464851',
4526
- text_hover: '#ddd'
4527
- };
4528
- } else {
4529
- this.style_vals = {
4530
- bg: '#fff',
4531
- bg_actions: '#e7eaee',
4532
- bg_hover: '#eceff3',
4533
- bg_light: '#f5f5f5',
4534
- border_gray: '#e4ecf2',
4535
- cancel_opacity: '1.0',
4536
- mini_hover: '#fafafa',
4537
- text_title: '#5c6578',
4538
- text_main: '#8b949b',
4539
- text_tagline: '#ced9e6',
4540
- text_hover: '#7c8598'
4541
- };
4542
- }
4543
- var shadow = '0px 0px 35px 0px rgba(45, 49, 56, 0.7)',
4544
- video_shadow = shadow,
4545
- mini_shadow = shadow,
4546
- thumb_total_size = MixpanelNotification.THUMB_IMG_SIZE + MixpanelNotification.THUMB_BORDER_SIZE * 2,
4547
- anim_seconds = (MixpanelNotification.ANIM_TIME / 1000) + 's';
4548
- if (this.mini) {
4549
- shadow = 'none';
4550
- }
4551
-
4552
- // don't display on small viewports
4553
- var notif_media_queries = {},
4554
- min_width = MixpanelNotification.NOTIF_WIDTH_MINI + 20;
4555
- notif_media_queries['@media only screen and (max-width: ' + (min_width - 1) + 'px)'] = {
4556
- '#overlay': {
4557
- 'display': 'none'
4558
- }
4559
- };
4560
- var notif_styles = {
4561
- '.flipped': {
4562
- 'transform': 'rotateY(180deg)'
4563
- },
4564
- '#overlay': {
4565
- 'position': 'fixed',
4566
- 'top': '0',
4567
- 'left': '0',
4568
- 'width': '100%',
4569
- 'height': '100%',
4570
- 'overflow': 'auto',
4571
- 'text-align': 'center',
4572
- 'z-index': '10000',
4573
- 'font-family': '"Helvetica", "Arial", sans-serif',
4574
- '-webkit-font-smoothing': 'antialiased',
4575
- '-moz-osx-font-smoothing': 'grayscale'
4576
- },
4577
- '#overlay.mini': {
4578
- 'height': '0',
4579
- 'overflow': 'visible'
4580
- },
4581
- '#overlay a': {
4582
- 'width': 'initial',
4583
- 'padding': '0',
4584
- 'text-decoration': 'none',
4585
- 'text-transform': 'none',
4586
- 'color': 'inherit'
4587
- },
4588
- '#bgwrapper': {
4589
- 'position': 'relative',
4590
- 'width': '100%',
4591
- 'height': '100%'
4592
- },
4593
- '#bg': {
4594
- 'position': 'fixed',
4595
- 'top': '0',
4596
- 'left': '0',
4597
- 'width': '100%',
4598
- 'height': '100%',
4599
- 'min-width': this.doc_width * 4 + 'px',
4600
- 'min-height': this.doc_height * 4 + 'px',
4601
- 'background-color': 'black',
4602
- 'opacity': '0.0',
4603
- '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)', // IE8
4604
- 'filter': 'alpha(opacity=60)', // IE5-7
4605
- 'transition': 'opacity ' + anim_seconds
4606
- },
4607
- '#bg.visible': {
4608
- 'opacity': MixpanelNotification.BG_OPACITY
4609
- },
4610
- '.mini #bg': {
4611
- 'width': '0',
4612
- 'height': '0',
4613
- 'min-width': '0'
4614
- },
4615
- '#flipcontainer': {
4616
- 'perspective': '1000px',
4617
- 'position': 'absolute',
4618
- 'width': '100%'
4619
- },
4620
- '#flipper': {
4621
- 'position': 'relative',
4622
- 'transform-style': 'preserve-3d',
4623
- 'transition': '0.3s'
4624
- },
4625
- '#takeover': {
4626
- 'position': 'absolute',
4627
- 'left': '50%',
4628
- 'width': MixpanelNotification.NOTIF_WIDTH + 'px',
4629
- 'margin-left': Math.round(-MixpanelNotification.NOTIF_WIDTH / 2) + 'px',
4630
- 'backface-visibility': 'hidden',
4631
- 'transform': 'rotateY(0deg)',
4632
- 'opacity': '0.0',
4633
- 'top': MixpanelNotification.NOTIF_START_TOP + 'px',
4634
- 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds
4635
- },
4636
- '#takeover.visible': {
4637
- 'opacity': '1.0',
4638
- 'top': MixpanelNotification.NOTIF_TOP + 'px'
4639
- },
4640
- '#takeover.exiting': {
4641
- 'opacity': '0.0',
4642
- 'top': MixpanelNotification.NOTIF_START_TOP + 'px'
4643
- },
4644
- '#thumbspacer': {
4645
- 'height': MixpanelNotification.THUMB_OFFSET + 'px'
4646
- },
4647
- '#thumbborder-wrapper': {
4648
- 'position': 'absolute',
4649
- 'top': (-MixpanelNotification.THUMB_BORDER_SIZE) + 'px',
4650
- 'left': (MixpanelNotification.NOTIF_WIDTH / 2 - MixpanelNotification.THUMB_OFFSET - MixpanelNotification.THUMB_BORDER_SIZE) + 'px',
4651
- 'width': thumb_total_size + 'px',
4652
- 'height': (thumb_total_size / 2) + 'px',
4653
- 'overflow': 'hidden'
4654
- },
4655
- '#thumbborder': {
4656
- 'position': 'absolute',
4657
- 'width': thumb_total_size + 'px',
4658
- 'height': thumb_total_size + 'px',
4659
- 'border-radius': thumb_total_size + 'px',
4660
- 'background-color': this.style_vals.bg_actions,
4661
- 'opacity': '0.5'
4662
- },
4663
- '#thumbnail': {
4664
- 'position': 'absolute',
4665
- 'top': '0px',
4666
- 'left': (MixpanelNotification.NOTIF_WIDTH / 2 - MixpanelNotification.THUMB_OFFSET) + 'px',
4667
- 'width': MixpanelNotification.THUMB_IMG_SIZE + 'px',
4668
- 'height': MixpanelNotification.THUMB_IMG_SIZE + 'px',
4669
- 'overflow': 'hidden',
4670
- 'z-index': '100',
4671
- 'border-radius': MixpanelNotification.THUMB_IMG_SIZE + 'px'
4672
- },
4673
- '#mini': {
4674
- 'position': 'absolute',
4675
- 'right': '20px',
4676
- 'top': MixpanelNotification.NOTIF_TOP + 'px',
4677
- 'width': this.notif_width + 'px',
4678
- 'height': MixpanelNotification.NOTIF_HEIGHT_MINI * 2 + 'px',
4679
- 'margin-top': 20 - MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4680
- 'backface-visibility': 'hidden',
4681
- 'opacity': '0.0',
4682
- 'transform': 'rotateX(90deg)',
4683
- 'transition': 'opacity 0.3s, transform 0.3s, right 0.3s'
4684
- },
4685
- '#mini.visible': {
4686
- 'opacity': '1.0',
4687
- 'transform': 'rotateX(0deg)'
4688
- },
4689
- '#mini.exiting': {
4690
- 'opacity': '0.0',
4691
- 'right': '-150px'
4692
- },
4693
- '#mainbox': {
4694
- 'border-radius': '4px',
4695
- 'box-shadow': shadow,
4696
- 'text-align': 'center',
4697
- 'background-color': this.style_vals.bg,
4698
- 'font-size': '14px',
4699
- 'color': this.style_vals.text_main
4700
- },
4701
- '#mini #mainbox': {
4702
- 'height': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4703
- 'margin-top': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4704
- 'border-radius': '3px',
4705
- 'transition': 'background-color ' + anim_seconds
4706
- },
4707
- '#mini-border': {
4708
- 'height': (MixpanelNotification.NOTIF_HEIGHT_MINI + 6) + 'px',
4709
- 'width': (MixpanelNotification.NOTIF_WIDTH_MINI + 6) + 'px',
4710
- 'position': 'absolute',
4711
- 'top': '-3px',
4712
- 'left': '-3px',
4713
- 'margin-top': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4714
- 'border-radius': '6px',
4715
- 'opacity': '0.25',
4716
- 'background-color': '#fff',
4717
- 'z-index': '-1',
4718
- 'box-shadow': mini_shadow
4719
- },
4720
- '#mini-icon': {
4721
- 'position': 'relative',
4722
- 'display': 'inline-block',
4723
- 'width': '75px',
4724
- 'height': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4725
- 'border-radius': '3px 0 0 3px',
4726
- 'background-color': this.style_vals.bg_actions,
4727
- 'background': 'linear-gradient(135deg, ' + this.style_vals.bg_light + ' 0%, ' + this.style_vals.bg_actions + ' 100%)',
4728
- 'transition': 'background-color ' + anim_seconds
4729
- },
4730
- '#mini:hover #mini-icon': {
4731
- 'background-color': this.style_vals.mini_hover
4732
- },
4733
- '#mini:hover #mainbox': {
4734
- 'background-color': this.style_vals.mini_hover
4735
- },
4736
- '#mini-icon-img': {
4737
- 'position': 'absolute',
4738
- 'background-image': 'url(' + this.thumb_image_url + ')',
4739
- 'width': '48px',
4740
- 'height': '48px',
4741
- 'top': '20px',
4742
- 'left': '12px'
4743
- },
4744
- '#content': {
4745
- 'padding': '30px 20px 0px 20px'
4746
- },
4747
- '#mini-content': {
4748
- 'text-align': 'left',
4749
- 'height': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4750
- 'cursor': 'pointer'
4751
- },
4752
- '#img': {
4753
- 'width': '328px',
4754
- 'margin-top': '30px',
4755
- 'border-radius': '5px'
4756
- },
4757
- '#title': {
4758
- 'max-height': '600px',
4759
- 'overflow': 'hidden',
4760
- 'word-wrap': 'break-word',
4761
- 'padding': '25px 0px 20px 0px',
4762
- 'font-size': '19px',
4763
- 'font-weight': 'bold',
4764
- 'color': this.style_vals.text_title
4765
- },
4766
- '#body': {
4767
- 'max-height': '600px',
4768
- 'margin-bottom': '25px',
4769
- 'overflow': 'hidden',
4770
- 'word-wrap': 'break-word',
4771
- 'line-height': '21px',
4772
- 'font-size': '15px',
4773
- 'font-weight': 'normal',
4774
- 'text-align': 'left'
4775
- },
4776
- '#mini #body': {
4777
- 'display': 'inline-block',
4778
- 'max-width': '250px',
4779
- 'margin': '0 0 0 30px',
4780
- 'height': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px',
4781
- 'font-size': '16px',
4782
- 'letter-spacing': '0.8px',
4783
- 'color': this.style_vals.text_title
4784
- },
4785
- '#mini #body-text': {
4786
- 'display': 'table',
4787
- 'height': MixpanelNotification.NOTIF_HEIGHT_MINI + 'px'
4788
- },
4789
- '#mini #body-text div': {
4790
- 'display': 'table-cell',
4791
- 'vertical-align': 'middle'
4792
- },
4793
- '#tagline': {
4794
- 'margin-bottom': '15px',
4795
- 'font-size': '10px',
4796
- 'font-weight': '600',
4797
- 'letter-spacing': '0.8px',
4798
- 'color': '#ccd7e0',
4799
- 'text-align': 'left'
4800
- },
4801
- '#tagline a': {
4802
- 'color': this.style_vals.text_tagline,
4803
- 'transition': 'color ' + anim_seconds
4804
- },
4805
- '#tagline a:hover': {
4806
- 'color': this.style_vals.text_hover
4807
- },
4808
- '#cancel': {
4809
- 'position': 'absolute',
4810
- 'right': '0',
4811
- 'width': '8px',
4812
- 'height': '8px',
4813
- 'padding': '10px',
4814
- 'border-radius': '20px',
4815
- 'margin': '12px 12px 0 0',
4816
- 'box-sizing': 'content-box',
4817
- 'cursor': 'pointer',
4818
- 'transition': 'background-color ' + anim_seconds
4819
- },
4820
- '#mini #cancel': {
4821
- 'margin': '7px 7px 0 0'
4822
- },
4823
- '#cancel-icon': {
4824
- 'width': '8px',
4825
- 'height': '8px',
4826
- 'overflow': 'hidden',
4827
- 'background-image': 'url(' + this.cdn_host + '/site_media/images/icons/notifications/cancel-x.png)',
4828
- 'opacity': this.style_vals.cancel_opacity
4829
- },
4830
- '#cancel:hover': {
4831
- 'background-color': this.style_vals.bg_hover
4832
- },
4833
- '#button': {
4834
- 'display': 'block',
4835
- 'height': '60px',
4836
- 'line-height': '60px',
4837
- 'text-align': 'center',
4838
- 'background-color': this.style_vals.bg_actions,
4839
- 'border-radius': '0 0 4px 4px',
4840
- 'overflow': 'hidden',
4841
- 'cursor': 'pointer',
4842
- 'transition': 'background-color ' + anim_seconds
4843
- },
4844
- '#button-close': {
4845
- 'display': 'inline-block',
4846
- 'width': '9px',
4847
- 'height': '60px',
4848
- 'margin-right': '8px',
4849
- 'vertical-align': 'top',
4850
- 'background-image': 'url(' + this.cdn_host + '/site_media/images/icons/notifications/close-x-' + this.style + '.png)',
4851
- 'background-repeat': 'no-repeat',
4852
- 'background-position': '0px 25px'
4853
- },
4854
- '#button-play': {
4855
- 'display': 'inline-block',
4856
- 'width': '30px',
4857
- 'height': '60px',
4858
- 'margin-left': '15px',
4859
- 'background-image': 'url(' + this.cdn_host + '/site_media/images/icons/notifications/play-' + this.style + '-small.png)',
4860
- 'background-repeat': 'no-repeat',
4861
- 'background-position': '0px 15px'
4862
- },
4863
- 'a#button-link': {
4864
- 'display': 'inline-block',
4865
- 'vertical-align': 'top',
4866
- 'text-align': 'center',
4867
- 'font-size': '17px',
4868
- 'font-weight': 'bold',
4869
- 'overflow': 'hidden',
4870
- 'word-wrap': 'break-word',
4871
- 'color': this.style_vals.text_title,
4872
- 'transition': 'color ' + anim_seconds
4873
- },
4874
- '#button:hover': {
4875
- 'background-color': this.style_vals.bg_hover,
4876
- 'color': this.style_vals.text_hover
4877
- },
4878
- '#button:hover a': {
4879
- 'color': this.style_vals.text_hover
4880
- },
4881
-
4882
- '#video-noflip': {
4883
- 'position': 'relative',
4884
- 'top': (-this.video_height * 2) + 'px'
4885
- },
4886
- '#video-flip': {
4887
- 'backface-visibility': 'hidden',
4888
- 'transform': 'rotateY(180deg)'
4889
- },
4890
- '#video': {
4891
- 'position': 'absolute',
4892
- 'width': (this.video_width - 1) + 'px',
4893
- 'height': this.video_height + 'px',
4894
- 'top': MixpanelNotification.NOTIF_TOP + 'px',
4895
- 'margin-top': '100px',
4896
- 'left': '50%',
4897
- 'margin-left': Math.round(-this.video_width / 2) + 'px',
4898
- 'overflow': 'hidden',
4899
- 'border-radius': '5px',
4900
- 'box-shadow': video_shadow,
4901
- 'transform': 'translateZ(1px)', // webkit rendering bug http://stackoverflow.com/questions/18167981/clickable-link-area-unexpectedly-smaller-after-css-transform
4902
- 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds
4903
- },
4904
- '#video.exiting': {
4905
- 'opacity': '0.0',
4906
- 'top': this.video_height + 'px'
4907
- },
4908
- '#video-holder': {
4909
- 'position': 'absolute',
4910
- 'width': (this.video_width - 1) + 'px',
4911
- 'height': this.video_height + 'px',
4912
- 'overflow': 'hidden',
4913
- 'border-radius': '5px'
4914
- },
4915
- '#video-frame': {
4916
- 'margin-left': '-1px',
4917
- 'width': this.video_width + 'px'
4918
- },
4919
- '#video-controls': {
4920
- 'opacity': '0',
4921
- 'transition': 'opacity 0.5s'
4922
- },
4923
- '#video:hover #video-controls': {
4924
- 'opacity': '1.0'
4925
- },
4926
- '#video .video-progress-el': {
4927
- 'position': 'absolute',
4928
- 'bottom': '0',
4929
- 'height': '25px',
4930
- 'border-radius': '0 0 0 5px'
4931
- },
4932
- '#video-progress': {
4933
- 'width': '90%'
4934
- },
4935
- '#video-progress-total': {
4936
- 'width': '100%',
4937
- 'background-color': this.style_vals.bg,
4938
- 'opacity': '0.7'
4939
- },
4940
- '#video-elapsed': {
4941
- 'width': '0',
4942
- 'background-color': '#6cb6f5',
4943
- 'opacity': '0.9'
4944
- },
4945
- '#video #video-time': {
4946
- 'width': '10%',
4947
- 'right': '0',
4948
- 'font-size': '11px',
4949
- 'line-height': '25px',
4950
- 'color': this.style_vals.text_main,
4951
- 'background-color': '#666',
4952
- 'border-radius': '0 0 5px 0'
4953
- }
4954
- };
4955
-
4956
- // IE hacks
4957
- if (this._browser_lte('ie', 8)) {
4958
- _.extend(notif_styles, {
4959
- '* html #overlay': {
4960
- 'position': 'absolute'
4961
- },
4962
- '* html #bg': {
4963
- 'position': 'absolute'
4964
- },
4965
- 'html, body': {
4966
- 'height': '100%'
4967
- }
4968
- });
4969
- }
4970
- if (this._browser_lte('ie', 7)) {
4971
- _.extend(notif_styles, {
4972
- '#mini #body': {
4973
- 'display': 'inline',
4974
- 'zoom': '1',
4975
- 'border': '1px solid ' + this.style_vals.bg_hover
4976
- },
4977
- '#mini #body-text': {
4978
- 'padding': '20px'
4979
- },
4980
- '#mini #mini-icon': {
4981
- 'display': 'none'
4982
- }
4983
- });
4984
- }
4985
-
4986
- // add vendor-prefixed style rules
4987
- var VENDOR_STYLES = [
4988
- 'backface-visibility', 'border-radius', 'box-shadow', 'opacity',
4989
- 'perspective', 'transform', 'transform-style', 'transition'
4990
- ],
4991
- VENDOR_PREFIXES = ['khtml', 'moz', 'ms', 'o', 'webkit'];
4992
- for (var selector in notif_styles) {
4993
- for (var si = 0; si < VENDOR_STYLES.length; si++) {
4994
- var prop = VENDOR_STYLES[si];
4995
- if (prop in notif_styles[selector]) {
4996
- var val = notif_styles[selector][prop];
4997
- for (var pi = 0; pi < VENDOR_PREFIXES.length; pi++) {
4998
- notif_styles[selector]['-' + VENDOR_PREFIXES[pi] + '-' + prop] = val;
4999
- }
5000
- }
5001
- }
5002
- }
5003
-
5004
- var inject_styles = function(styles, media_queries) {
5005
- var create_style_text = function(style_defs) {
5006
- var st = '';
5007
- for (var selector in style_defs) {
5008
- var mp_selector = selector
5009
- .replace(/#/g, '#' + MixpanelNotification.MARKUP_PREFIX + '-')
5010
- .replace(/\./g, '.' + MixpanelNotification.MARKUP_PREFIX + '-');
5011
- st += '\n' + mp_selector + ' {';
5012
- var props = style_defs[selector];
5013
- for (var k in props) {
5014
- st += k + ':' + props[k] + ';';
5015
- }
5016
- st += '}';
5017
- }
5018
- return st;
5019
- };
5020
- var create_media_query_text = function(mq_defs) {
5021
- var mqt = '';
5022
- for (var mq in mq_defs) {
5023
- mqt += '\n' + mq + ' {' + create_style_text(mq_defs[mq]) + '\n}';
5024
- }
5025
- return mqt;
5026
- };
5027
-
5028
- var style_text = create_style_text(styles) + create_media_query_text(media_queries),
5029
- head_el = document.head || document.getElementsByTagName('head')[0] || document.documentElement,
5030
- style_el = document.createElement('style');
5031
- head_el.appendChild(style_el);
5032
- style_el.setAttribute('type', 'text/css');
5033
- if (style_el.styleSheet) { // IE
5034
- style_el.styleSheet.cssText = style_text;
5035
- } else {
5036
- style_el.textContent = style_text;
5037
- }
5038
- };
5039
- inject_styles(notif_styles, notif_media_queries);
5040
- };
5041
-
5042
- MixpanelNotification.prototype._init_video = _.safewrap(function() {
5043
- if (!this.video_url) {
5044
- return;
5045
- }
5046
- var self = this;
5047
-
5048
- // Youtube iframe API compatibility
5049
- self.yt_custom = 'postMessage' in window;
5050
-
5051
- self.dest_url = self.video_url;
5052
- var youtube_match = self.video_url.match(
5053
- // http://stackoverflow.com/questions/2936467/parse-youtube-video-id-using-preg-match
5054
- /(?:youtube(?:-nocookie)?\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/ ]{11})/i
5055
- ),
5056
- vimeo_match = self.video_url.match(
5057
- /vimeo\.com\/.*?(\d+)/i
5058
- );
5059
- if (youtube_match) {
5060
- self.show_video = true;
5061
- self.youtube_video = youtube_match[1];
5062
-
5063
- if (self.yt_custom) {
5064
- window['onYouTubeIframeAPIReady'] = function() {
5065
- if (self._get_el('video-frame')) {
5066
- self._yt_video_ready();
5067
- }
5068
- };
5069
-
5070
- // load Youtube iframe API; see https://developers.google.com/youtube/iframe_api_reference
5071
- var tag = document.createElement('script');
5072
- tag.src = self.resource_protocol + 'www.youtube.com/iframe_api';
5073
- var firstScriptTag = document.getElementsByTagName('script')[0];
5074
- firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
5075
- }
5076
- } else if (vimeo_match) {
5077
- self.show_video = true;
5078
- self.vimeo_video = vimeo_match[1];
5079
- }
5080
-
5081
- // IE <= 7, FF <= 3: fall through to video link rather than embedded player
5082
- if (self._browser_lte('ie', 7) || self._browser_lte('firefox', 3)) {
5083
- self.show_video = false;
5084
- self.clickthrough = true;
5085
- }
5086
- });
5087
-
5088
- MixpanelNotification.prototype._mark_as_shown = _.safewrap(function() {
5089
- // click on background to dismiss
5090
- var self = this;
5091
- _.register_event(self._get_el('bg'), 'click', function() {
5092
- self.dismiss();
5093
- });
5094
-
5095
- var get_style = function(el, style_name) {
5096
- var styles = {};
5097
- if (document.defaultView && document.defaultView.getComputedStyle) {
5098
- styles = document.defaultView.getComputedStyle(el, null); // FF3 requires both args
5099
- } else if (el.currentStyle) { // IE
5100
- styles = el.currentStyle;
5101
- }
5102
- return styles[style_name];
5103
- };
5104
-
5105
- if (this.campaign_id) {
5106
- var notif_el = this._get_el('overlay');
5107
- if (notif_el && get_style(notif_el, 'visibility') !== 'hidden' && get_style(notif_el, 'display') !== 'none') {
5108
- this._mark_delivery();
5109
- }
5110
- }
5111
- });
5112
-
5113
- MixpanelNotification.prototype._mark_delivery = _.safewrap(function(extra_props) {
5114
- if (!this.marked_as_shown) {
5115
- this.marked_as_shown = true;
5116
-
5117
- if (this.campaign_id) {
5118
- // mark notification shown (local cache)
5119
- this._get_shown_campaigns()[this.campaign_id] = 1 * new Date();
5120
- this.persistence.save();
5121
- }
5122
-
5123
- // track delivery
5124
- this._track_event('$campaign_delivery', extra_props);
5125
-
5126
- // mark notification shown (mixpanel property)
5127
- this.mixpanel['people']['append']({
5128
- '$campaigns': this.campaign_id,
5129
- '$notifications': {
5130
- 'campaign_id': this.campaign_id,
5131
- 'message_id': this.message_id,
5132
- 'type': 'web',
5133
- 'time': new Date()
5134
- }
5135
- });
5136
- }
5137
- });
5138
-
5139
- MixpanelNotification.prototype._preload_images = function(all_loaded_cb) {
5140
- var self = this;
5141
- if (this.imgs_to_preload.length === 0) {
5142
- all_loaded_cb();
5143
- return;
5144
- }
5145
-
5146
- var preloaded_imgs = 0;
5147
- var img_objs = [];
5148
- var onload = function() {
5149
- preloaded_imgs++;
5150
- if (preloaded_imgs === self.imgs_to_preload.length && all_loaded_cb) {
5151
- all_loaded_cb();
5152
- all_loaded_cb = null;
5153
- }
5154
- };
5155
- for (var i = 0; i < this.imgs_to_preload.length; i++) {
5156
- var img = new Image();
5157
- img.onload = onload;
5158
- img.src = this.imgs_to_preload[i];
5159
- if (img.complete) {
5160
- onload();
5161
- }
5162
- img_objs.push(img);
5163
- }
5164
-
5165
- // IE6/7 doesn't fire onload reliably
5166
- if (this._browser_lte('ie', 7)) {
5167
- setTimeout(function() {
5168
- var imgs_loaded = true;
5169
- for (i = 0; i < img_objs.length; i++) {
5170
- if (!img_objs[i].complete) {
5171
- imgs_loaded = false;
5172
- }
5173
- }
5174
- if (imgs_loaded && all_loaded_cb) {
5175
- all_loaded_cb();
5176
- all_loaded_cb = null;
5177
- }
5178
- }, 500);
5179
- }
5180
- };
5181
-
5182
- MixpanelNotification.prototype._remove_notification_el = _.safewrap(function() {
5183
- window.clearInterval(this._video_progress_checker);
5184
- this.notification_el.style.visibility = 'hidden';
5185
- this.body_el.removeChild(this.notification_el);
5186
- });
5187
-
5188
- MixpanelNotification.prototype._set_client_config = function() {
5189
- var get_browser_version = function(browser_ex) {
5190
- var match = navigator.userAgent.match(browser_ex);
5191
- return match && match[1];
5192
- };
5193
- this.browser_versions = {};
5194
- this.browser_versions['chrome'] = get_browser_version(/Chrome\/(\d+)/);
5195
- this.browser_versions['firefox'] = get_browser_version(/Firefox\/(\d+)/);
5196
- this.browser_versions['ie'] = get_browser_version(/MSIE (\d+).+/);
5197
- if (!this.browser_versions['ie'] && !(window.ActiveXObject) && 'ActiveXObject' in window) {
5198
- this.browser_versions['ie'] = 11;
5199
- }
5200
-
5201
- this.body_el = document.body || document.getElementsByTagName('body')[0];
5202
- if (this.body_el) {
5203
- this.doc_width = Math.max(
5204
- this.body_el.scrollWidth, document.documentElement.scrollWidth,
5205
- this.body_el.offsetWidth, document.documentElement.offsetWidth,
5206
- this.body_el.clientWidth, document.documentElement.clientWidth
5207
- );
5208
- this.doc_height = Math.max(
5209
- this.body_el.scrollHeight, document.documentElement.scrollHeight,
5210
- this.body_el.offsetHeight, document.documentElement.offsetHeight,
5211
- this.body_el.clientHeight, document.documentElement.clientHeight
5212
- );
5213
- }
5214
-
5215
- // detect CSS compatibility
5216
- var ie_ver = this.browser_versions['ie'];
5217
- var sample_styles = document.createElement('div').style,
5218
- is_css_compatible = function(rule) {
5219
- if (rule in sample_styles) {
5220
- return true;
5221
- }
5222
- if (!ie_ver) {
5223
- rule = rule[0].toUpperCase() + rule.slice(1);
5224
- var props = ['O' + rule, 'Webkit' + rule, 'Moz' + rule];
5225
- for (var i = 0; i < props.length; i++) {
5226
- if (props[i] in sample_styles) {
5227
- return true;
5228
- }
5229
- }
5230
- }
5231
- return false;
5232
- };
5233
- this.use_transitions = this.body_el &&
5234
- is_css_compatible('transition') &&
5235
- is_css_compatible('transform');
5236
- this.flip_animate = (this.browser_versions['chrome'] >= 33 || this.browser_versions['firefox'] >= 15) &&
5237
- this.body_el &&
5238
- is_css_compatible('backfaceVisibility') &&
5239
- is_css_compatible('perspective') &&
5240
- is_css_compatible('transform');
5241
- };
5242
-
5243
- MixpanelNotification.prototype._switch_to_video = _.safewrap(function() {
5244
- var self = this,
5245
- anims = [
5246
- {
5247
- el: self._get_notification_display_el(),
5248
- attr: 'opacity',
5249
- start: 1.0,
5250
- goal: 0.0
5251
- },
5252
- {
5253
- el: self._get_notification_display_el(),
5254
- attr: 'top',
5255
- start: MixpanelNotification.NOTIF_TOP,
5256
- goal: -500
5257
- },
5258
- {
5259
- el: self._get_el('video-noflip'),
5260
- attr: 'opacity',
5261
- start: 0.0,
5262
- goal: 1.0
5263
- },
5264
- {
5265
- el: self._get_el('video-noflip'),
5266
- attr: 'top',
5267
- start: -self.video_height * 2,
5268
- goal: 0
5269
- }
5270
- ];
5271
-
5272
- if (self.mini) {
5273
- var bg = self._get_el('bg'),
5274
- overlay = self._get_el('overlay');
5275
- bg.style.width = '100%';
5276
- bg.style.height = '100%';
5277
- overlay.style.width = '100%';
5278
-
5279
- self._add_class(self._get_notification_display_el(), 'exiting');
5280
- self._add_class(bg, 'visible');
5281
-
5282
- anims.push({
5283
- el: self._get_el('bg'),
5284
- attr: 'opacity',
5285
- start: 0.0,
5286
- goal: MixpanelNotification.BG_OPACITY
5287
- });
5288
- }
5289
-
5290
- var video_el = self._get_el('video-holder');
5291
- video_el.innerHTML = self.video_iframe;
5292
-
5293
- var video_ready = function() {
5294
- if (window['YT'] && window['YT']['loaded']) {
5295
- self._yt_video_ready();
5296
- }
5297
- self.showing_video = true;
5298
- self._get_notification_display_el().style.visibility = 'hidden';
5299
- };
5300
- if (self.flip_animate) {
5301
- self._add_class('flipper', 'flipped');
5302
- setTimeout(video_ready, MixpanelNotification.ANIM_TIME);
5303
- } else {
5304
- self._animate_els(anims, MixpanelNotification.ANIM_TIME, video_ready);
5305
- }
5306
- });
5307
-
5308
- MixpanelNotification.prototype._track_event = function(event_name, properties, cb) {
5309
- if (this.campaign_id) {
5310
- properties = properties || {};
5311
- properties = _.extend(properties, {
5312
- 'campaign_id': this.campaign_id,
5313
- 'message_id': this.message_id,
5314
- 'message_type': 'web_inapp',
5315
- 'message_subtype': this.notif_type
5316
- });
5317
- this.mixpanel['track'](event_name, properties, cb);
5318
- } else if (cb) {
5319
- cb.call();
5320
- }
3709
+ // removes the storage entry and deletes all loaded data
3710
+ // forced name for tests
3711
+ MixpanelPersistence.prototype.clear = function() {
3712
+ this.remove();
3713
+ this['props'] = {};
5321
3714
  };
5322
3715
 
5323
- MixpanelNotification.prototype._yt_video_ready = _.safewrap(function() {
5324
- var self = this;
5325
- if (self.video_inited) {
5326
- return;
5327
- }
5328
- self.video_inited = true;
5329
-
5330
- var progress_bar = self._get_el('video-elapsed'),
5331
- progress_time = self._get_el('video-time'),
5332
- progress_el = self._get_el('video-progress');
5333
-
5334
- new window['YT']['Player'](MixpanelNotification.MARKUP_PREFIX + '-video-frame', {
5335
- 'events': {
5336
- 'onReady': function(event) {
5337
- var ytplayer = event['target'],
5338
- video_duration = ytplayer['getDuration'](),
5339
- pad = function(i) {
5340
- return ('00' + i).slice(-2);
5341
- },
5342
- update_video_time = function(current_time) {
5343
- var secs = Math.round(video_duration - current_time),
5344
- mins = Math.floor(secs / 60),
5345
- hours = Math.floor(mins / 60);
5346
- secs -= mins * 60;
5347
- mins -= hours * 60;
5348
- progress_time.innerHTML = '-' + (hours ? hours + ':' : '') + pad(mins) + ':' + pad(secs);
5349
- };
5350
- update_video_time(0);
5351
- self._video_progress_checker = window.setInterval(function() {
5352
- var current_time = ytplayer['getCurrentTime']();
5353
- progress_bar.style.width = (current_time / video_duration * 100) + '%';
5354
- update_video_time(current_time);
5355
- }, 250);
5356
- _.register_event(progress_el, 'click', function(e) {
5357
- var clickx = Math.max(0, e.pageX - progress_el.getBoundingClientRect().left);
5358
- ytplayer['seekTo'](video_duration * clickx / progress_el.clientWidth, true);
5359
- });
5360
- }
5361
- }
5362
- });
5363
- });
5364
-
5365
3716
  /**
5366
- * Mixpanel People Object
5367
- * @constructor
5368
- */
5369
- var MixpanelPeople = function() {};
5370
-
5371
- _.extend(MixpanelPeople.prototype, apiActions);
5372
-
5373
- MixpanelPeople.prototype._init = function(mixpanel_instance) {
5374
- this._mixpanel = mixpanel_instance;
5375
- };
5376
-
5377
- /*
5378
- * Set properties on a user record.
5379
- *
5380
- * ### Usage:
5381
- *
5382
- * mixpanel.people.set('gender', 'm');
5383
- *
5384
- * // or set multiple properties at once
5385
- * mixpanel.people.set({
5386
- * 'Company': 'Acme',
5387
- * 'Plan': 'Premium',
5388
- * 'Upgrade date': new Date()
5389
- * });
5390
- * // properties can be strings, integers, dates, or lists
5391
- *
5392
- * @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.
5393
- * @param {*} [to] A value to set on the given property name
5394
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5395
- */
5396
- MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
5397
- var data = this.set_action(prop, to);
5398
- if (_.isObject(prop)) {
5399
- callback = to;
5400
- }
5401
- // make sure that the referrer info has been updated and saved
5402
- if (this._get_config('save_referrer')) {
5403
- this._mixpanel['persistence'].update_referrer_info(document.referrer);
5404
- }
5405
-
5406
- // update $set object with default people properties
5407
- data[SET_ACTION] = _.extend(
5408
- {},
5409
- _.info.people_properties(),
5410
- this._mixpanel['persistence'].get_referrer_info(),
5411
- data[SET_ACTION]
5412
- );
5413
- return this._send_request(data, callback);
5414
- });
5415
-
5416
- /*
5417
- * Set properties on a user record, only if they do not yet exist.
5418
- * This will not overwrite previous people property values, unlike
5419
- * people.set().
5420
- *
5421
- * ### Usage:
5422
- *
5423
- * mixpanel.people.set_once('First Login Date', new Date());
5424
- *
5425
- * // or set multiple properties at once
5426
- * mixpanel.people.set_once({
5427
- * 'First Login Date': new Date(),
5428
- * 'Starting Plan': 'Premium'
5429
- * });
5430
- *
5431
- * // properties can be strings, integers or dates
5432
- *
5433
- * @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.
5434
- * @param {*} [to] A value to set on the given property name
5435
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5436
- */
5437
- MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) {
5438
- var data = this.set_once_action(prop, to);
5439
- if (_.isObject(prop)) {
5440
- callback = to;
5441
- }
5442
- return this._send_request(data, callback);
5443
- });
5444
-
5445
- /*
5446
- * Unset properties on a user record (permanently removes the properties and their values from a profile).
5447
- *
5448
- * ### Usage:
5449
- *
5450
- * mixpanel.people.unset('gender');
5451
- *
5452
- * // or unset multiple properties at once
5453
- * mixpanel.people.unset(['gender', 'Company']);
5454
- *
5455
- * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names.
5456
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5457
- */
5458
- MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) {
5459
- var data = this.unset_action(prop);
5460
- return this._send_request(data, callback);
5461
- });
5462
-
5463
- /*
5464
- * Increment/decrement numeric people analytics properties.
5465
- *
5466
- * ### Usage:
5467
- *
5468
- * mixpanel.people.increment('page_views', 1);
5469
- *
5470
- * // or, for convenience, if you're just incrementing a counter by
5471
- * // 1, you can simply do
5472
- * mixpanel.people.increment('page_views');
5473
- *
5474
- * // to decrement a counter, pass a negative number
5475
- * mixpanel.people.increment('credits_left', -1);
5476
- *
5477
- * // like mixpanel.people.set(), you can increment multiple
5478
- * // properties at once:
5479
- * mixpanel.people.increment({
5480
- * counter1: 1,
5481
- * counter2: 6
5482
- * });
5483
- *
5484
- * @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.
5485
- * @param {Number} [by] An amount to increment the given property
5486
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5487
- */
5488
- MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) {
5489
- var data = {};
5490
- var $add = {};
5491
- if (_.isObject(prop)) {
5492
- _.each(prop, function(v, k) {
5493
- if (!this._is_reserved_property(k)) {
5494
- if (isNaN(parseFloat(v))) {
5495
- console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5496
- return;
5497
- } else {
5498
- $add[k] = v;
5499
- }
5500
- }
5501
- }, this);
5502
- callback = by;
5503
- } else {
5504
- // convenience: mixpanel.people.increment('property'); will
5505
- // increment 'property' by 1
5506
- if (_.isUndefined(by)) {
5507
- by = 1;
5508
- }
5509
- $add[prop] = by;
5510
- }
5511
- data[ADD_ACTION] = $add;
5512
-
5513
- return this._send_request(data, callback);
5514
- });
5515
-
5516
- /*
5517
- * Append a value to a list-valued people analytics property.
5518
- *
5519
- * ### Usage:
5520
- *
5521
- * // append a value to a list, creating it if needed
5522
- * mixpanel.people.append('pages_visited', 'homepage');
5523
- *
5524
- * // like mixpanel.people.set(), you can append multiple
5525
- * // properties at once:
5526
- * mixpanel.people.append({
5527
- * list1: 'bob',
5528
- * list2: 123
5529
- * });
5530
- *
5531
- * @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.
5532
- * @param {*} [value] value An item to append to the list
5533
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5534
- */
5535
- MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
5536
- if (_.isObject(list_name)) {
5537
- callback = value;
5538
- }
5539
- var data = this.append_action(list_name, value);
5540
- return this._send_request(data, callback);
5541
- });
5542
-
5543
- /*
5544
- * Remove a value from a list-valued people analytics property.
5545
- *
5546
- * ### Usage:
5547
- *
5548
- * mixpanel.people.remove('School', 'UCB');
5549
- *
5550
- * @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.
5551
- * @param {*} [value] value Item to remove from the list
5552
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5553
- */
5554
- MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) {
5555
- if (_.isObject(list_name)) {
5556
- callback = value;
5557
- }
5558
- var data = this.remove_action(list_name, value);
5559
- return this._send_request(data, callback);
5560
- });
5561
-
5562
- /*
5563
- * Merge a given list with a list-valued people analytics property,
5564
- * excluding duplicate values.
5565
- *
5566
- * ### Usage:
5567
- *
5568
- * // merge a value to a list, creating it if needed
5569
- * mixpanel.people.union('pages_visited', 'homepage');
5570
- *
5571
- * // like mixpanel.people.set(), you can append multiple
5572
- * // properties at once:
5573
- * mixpanel.people.union({
5574
- * list1: 'bob',
5575
- * list2: 123
5576
- * });
5577
- *
5578
- * // like mixpanel.people.append(), you can append multiple
5579
- * // values to the same list:
5580
- * mixpanel.people.union({
5581
- * list1: ['bob', 'billy']
5582
- * });
5583
- *
5584
- * @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.
5585
- * @param {*} [value] Value / values to merge with the given property
5586
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
3717
+ * @param {Object} props
3718
+ * @param {*=} default_value
3719
+ * @param {number=} days
5587
3720
  */
5588
- MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) {
5589
- if (_.isObject(list_name)) {
5590
- callback = values;
3721
+ MixpanelPersistence.prototype.register_once = function(props, default_value, days) {
3722
+ if (_.isObject(props)) {
3723
+ if (typeof(default_value) === 'undefined') { default_value = 'None'; }
3724
+ this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
3725
+
3726
+ _.each(props, function(val, prop) {
3727
+ if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) {
3728
+ this['props'][prop] = val;
3729
+ }
3730
+ }, this);
3731
+
3732
+ this.save();
3733
+
3734
+ return true;
5591
3735
  }
5592
- var data = this.union_action(list_name, values);
5593
- return this._send_request(data, callback);
5594
- });
3736
+ return false;
3737
+ };
5595
3738
 
5596
- /*
5597
- * Record that you have charged the current user a certain amount
5598
- * of money. Charges recorded with track_charge() will appear in the
5599
- * Mixpanel revenue report.
5600
- *
5601
- * ### Usage:
5602
- *
5603
- * // charge a user $50
5604
- * mixpanel.people.track_charge(50);
5605
- *
5606
- * // charge a user $30.50 on the 2nd of january
5607
- * mixpanel.people.track_charge(30.50, {
5608
- * '$time': new Date('jan 1 2012')
5609
- * });
5610
- *
5611
- * @param {Number} amount The amount of money charged to the current user
5612
- * @param {Object} [properties] An associative array of properties associated with the charge
5613
- * @param {Function} [callback] If provided, the callback will be called when the server responds
3739
+ /**
3740
+ * @param {Object} props
3741
+ * @param {number=} days
5614
3742
  */
5615
- MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) {
5616
- if (!_.isNumber(amount)) {
5617
- amount = parseFloat(amount);
5618
- if (isNaN(amount)) {
5619
- console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
5620
- return;
5621
- }
3743
+ MixpanelPersistence.prototype.register = function(props, days) {
3744
+ if (_.isObject(props)) {
3745
+ this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
3746
+
3747
+ _.extend(this['props'], props);
3748
+
3749
+ this.save();
3750
+
3751
+ return true;
3752
+ }
3753
+ return false;
3754
+ };
3755
+
3756
+ MixpanelPersistence.prototype.unregister = function(prop) {
3757
+ if (prop in this['props']) {
3758
+ delete this['props'][prop];
3759
+ this.save();
5622
3760
  }
3761
+ };
5623
3762
 
5624
- return this.append('$transactions', _.extend({
5625
- '$amount': amount
5626
- }, properties), callback);
5627
- });
3763
+ MixpanelPersistence.prototype.update_campaign_params = function() {
3764
+ if (!this.campaign_params_saved) {
3765
+ this.register_once(_.info.campaignParams());
3766
+ this.campaign_params_saved = true;
3767
+ }
3768
+ };
5628
3769
 
5629
- /*
5630
- * Permanently clear all revenue report transactions from the
5631
- * current user's people analytics profile.
5632
- *
5633
- * ### Usage:
5634
- *
5635
- * mixpanel.people.clear_charges();
5636
- *
5637
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
5638
- */
5639
- MixpanelPeople.prototype.clear_charges = function(callback) {
5640
- return this.set('$transactions', [], callback);
3770
+ MixpanelPersistence.prototype.update_search_keyword = function(referrer) {
3771
+ this.register(_.info.searchInfo(referrer));
5641
3772
  };
5642
3773
 
5643
- /*
5644
- * Permanently deletes the current people analytics profile from
5645
- * Mixpanel (using the current distinct_id).
5646
- *
5647
- * ### Usage:
5648
- *
5649
- * // remove the all data you have stored about the current user
5650
- * mixpanel.people.delete_user();
5651
- *
5652
- */
5653
- MixpanelPeople.prototype.delete_user = function() {
5654
- if (!this._identify_called()) {
5655
- console.error('mixpanel.people.delete_user() requires you to call identify() first');
5656
- return;
3774
+ // EXPORTED METHOD, we test this directly.
3775
+ MixpanelPersistence.prototype.update_referrer_info = function(referrer) {
3776
+ // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
3777
+ this.register_once({
3778
+ '$initial_referrer': referrer || '$direct',
3779
+ '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct'
3780
+ }, '');
3781
+ };
3782
+
3783
+ MixpanelPersistence.prototype.get_referrer_info = function() {
3784
+ return _.strip_empty_properties({
3785
+ '$initial_referrer': this['props']['$initial_referrer'],
3786
+ '$initial_referring_domain': this['props']['$initial_referring_domain']
3787
+ });
3788
+ };
3789
+
3790
+ // safely fills the passed in object with stored properties,
3791
+ // does not override any properties defined in both
3792
+ // returns the passed in object
3793
+ MixpanelPersistence.prototype.safe_merge = function(props) {
3794
+ _.each(this['props'], function(val, prop) {
3795
+ if (!(prop in props)) {
3796
+ props[prop] = val;
3797
+ }
3798
+ });
3799
+
3800
+ return props;
3801
+ };
3802
+
3803
+ MixpanelPersistence.prototype.update_config = function(config) {
3804
+ this.default_expiry = this.expire_days = config['cookie_expiration'];
3805
+ this.set_disabled(config['disable_persistence']);
3806
+ this.set_cookie_domain(config['cookie_domain']);
3807
+ this.set_cross_site(config['cross_site_cookie']);
3808
+ this.set_cross_subdomain(config['cross_subdomain_cookie']);
3809
+ this.set_secure(config['secure_cookie']);
3810
+ };
3811
+
3812
+ MixpanelPersistence.prototype.set_disabled = function(disabled) {
3813
+ this.disabled = disabled;
3814
+ if (this.disabled) {
3815
+ this.remove();
3816
+ } else {
3817
+ this.save();
5657
3818
  }
5658
- var data = {'$delete': this._mixpanel.get_distinct_id()};
5659
- return this._send_request(data);
5660
3819
  };
5661
3820
 
5662
- MixpanelPeople.prototype.toString = function() {
5663
- return this._mixpanel.toString() + '.people';
3821
+ MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) {
3822
+ if (cookie_domain !== this.cookie_domain) {
3823
+ this.remove();
3824
+ this.cookie_domain = cookie_domain;
3825
+ this.save();
3826
+ }
5664
3827
  };
5665
3828
 
5666
- MixpanelPeople.prototype._send_request = function(data, callback) {
5667
- data['$token'] = this._get_config('token');
5668
- data['$distinct_id'] = this._mixpanel.get_distinct_id();
5669
- var device_id = this._mixpanel.get_property('$device_id');
5670
- var user_id = this._mixpanel.get_property('$user_id');
5671
- var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id');
5672
- if (device_id) {
5673
- data['$device_id'] = device_id;
3829
+ MixpanelPersistence.prototype.set_cross_site = function(cross_site) {
3830
+ if (cross_site !== this.cross_site) {
3831
+ this.cross_site = cross_site;
3832
+ this.remove();
3833
+ this.save();
5674
3834
  }
5675
- if (user_id) {
5676
- data['$user_id'] = user_id;
3835
+ };
3836
+
3837
+ MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) {
3838
+ if (cross_subdomain !== this.cross_subdomain) {
3839
+ this.cross_subdomain = cross_subdomain;
3840
+ this.remove();
3841
+ this.save();
5677
3842
  }
5678
- if (had_persisted_distinct_id) {
5679
- data['$had_persisted_distinct_id'] = had_persisted_distinct_id;
3843
+ };
3844
+
3845
+ MixpanelPersistence.prototype.get_cross_subdomain = function() {
3846
+ return this.cross_subdomain;
3847
+ };
3848
+
3849
+ MixpanelPersistence.prototype.set_secure = function(secure) {
3850
+ if (secure !== this.secure) {
3851
+ this.secure = secure ? true : false;
3852
+ this.remove();
3853
+ this.save();
5680
3854
  }
3855
+ };
5681
3856
 
5682
- var date_encoded_data = _.encodeDates(data);
3857
+ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
3858
+ var q_key = this._get_queue_key(queue),
3859
+ q_data = data[queue],
3860
+ set_q = this._get_or_create_queue(SET_ACTION),
3861
+ set_once_q = this._get_or_create_queue(SET_ONCE_ACTION),
3862
+ unset_q = this._get_or_create_queue(UNSET_ACTION),
3863
+ add_q = this._get_or_create_queue(ADD_ACTION),
3864
+ union_q = this._get_or_create_queue(UNION_ACTION),
3865
+ remove_q = this._get_or_create_queue(REMOVE_ACTION, []),
3866
+ append_q = this._get_or_create_queue(APPEND_ACTION, []);
5683
3867
 
5684
- if (!this._identify_called()) {
5685
- this._enqueue(data);
5686
- if (!_.isUndefined(callback)) {
5687
- if (this._get_config('verbose')) {
5688
- callback({status: -1, error: null});
3868
+ if (q_key === SET_QUEUE_KEY) {
3869
+ // Update the set queue - we can override any existing values
3870
+ _.extend(set_q, q_data);
3871
+ // if there was a pending increment, override it
3872
+ // with the set.
3873
+ this._pop_from_people_queue(ADD_ACTION, q_data);
3874
+ // if there was a pending union, override it
3875
+ // with the set.
3876
+ this._pop_from_people_queue(UNION_ACTION, q_data);
3877
+ this._pop_from_people_queue(UNSET_ACTION, q_data);
3878
+ } else if (q_key === SET_ONCE_QUEUE_KEY) {
3879
+ // only queue the data if there is not already a set_once call for it.
3880
+ _.each(q_data, function(v, k) {
3881
+ if (!(k in set_once_q)) {
3882
+ set_once_q[k] = v;
3883
+ }
3884
+ });
3885
+ this._pop_from_people_queue(UNSET_ACTION, q_data);
3886
+ } else if (q_key === UNSET_QUEUE_KEY) {
3887
+ _.each(q_data, function(prop) {
3888
+
3889
+ // undo previously-queued actions on this key
3890
+ _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) {
3891
+ if (prop in enqueued_obj) {
3892
+ delete enqueued_obj[prop];
3893
+ }
3894
+ });
3895
+ _.each(append_q, function(append_obj) {
3896
+ if (prop in append_obj) {
3897
+ delete append_obj[prop];
3898
+ }
3899
+ });
3900
+
3901
+ unset_q[prop] = true;
3902
+
3903
+ });
3904
+ } else if (q_key === ADD_QUEUE_KEY) {
3905
+ _.each(q_data, function(v, k) {
3906
+ // If it exists in the set queue, increment
3907
+ // the value
3908
+ if (k in set_q) {
3909
+ set_q[k] += v;
5689
3910
  } else {
5690
- callback(-1);
3911
+ // If it doesn't exist, update the add
3912
+ // queue
3913
+ if (!(k in add_q)) {
3914
+ add_q[k] = 0;
3915
+ }
3916
+ add_q[k] += v;
5691
3917
  }
5692
- }
5693
- return _.truncate(date_encoded_data, 255);
3918
+ }, this);
3919
+ this._pop_from_people_queue(UNSET_ACTION, q_data);
3920
+ } else if (q_key === UNION_QUEUE_KEY) {
3921
+ _.each(q_data, function(v, k) {
3922
+ if (_.isArray(v)) {
3923
+ if (!(k in union_q)) {
3924
+ union_q[k] = [];
3925
+ }
3926
+ // We may send duplicates, the server will dedup them.
3927
+ union_q[k] = union_q[k].concat(v);
3928
+ }
3929
+ });
3930
+ this._pop_from_people_queue(UNSET_ACTION, q_data);
3931
+ } else if (q_key === REMOVE_QUEUE_KEY) {
3932
+ remove_q.push(q_data);
3933
+ this._pop_from_people_queue(APPEND_ACTION, q_data);
3934
+ } else if (q_key === APPEND_QUEUE_KEY) {
3935
+ append_q.push(q_data);
3936
+ this._pop_from_people_queue(UNSET_ACTION, q_data);
5694
3937
  }
5695
3938
 
5696
- return this._mixpanel._track_or_batch({
5697
- type: 'people',
5698
- data: date_encoded_data,
5699
- endpoint: this._get_config('api_host') + '/engage/',
5700
- batcher: this._mixpanel.request_batchers.people
5701
- }, callback);
5702
- };
3939
+ console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3940
+ console.log(data);
5703
3941
 
5704
- MixpanelPeople.prototype._get_config = function(conf_var) {
5705
- return this._mixpanel.get_config(conf_var);
3942
+ this.save();
5706
3943
  };
5707
3944
 
5708
- MixpanelPeople.prototype._identify_called = function() {
5709
- return this._mixpanel._flags.identify_called === true;
5710
- };
3945
+ MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) {
3946
+ var q = this._get_queue(queue);
3947
+ if (!_.isUndefined(q)) {
3948
+ _.each(data, function(v, k) {
3949
+ if (queue === APPEND_ACTION || queue === REMOVE_ACTION) {
3950
+ // list actions: only remove if both k+v match
3951
+ // e.g. remove should not override append in a case like
3952
+ // append({foo: 'bar'}); remove({foo: 'qux'})
3953
+ _.each(q, function(queued_action) {
3954
+ if (queued_action[k] === v) {
3955
+ delete queued_action[k];
3956
+ }
3957
+ });
3958
+ } else {
3959
+ delete q[k];
3960
+ }
3961
+ }, this);
5711
3962
 
5712
- // Queue up engage operations if identify hasn't been called yet.
5713
- MixpanelPeople.prototype._enqueue = function(data) {
5714
- if (SET_ACTION in data) {
5715
- this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data);
5716
- } else if (SET_ONCE_ACTION in data) {
5717
- this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data);
5718
- } else if (UNSET_ACTION in data) {
5719
- this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data);
5720
- } else if (ADD_ACTION in data) {
5721
- this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data);
5722
- } else if (APPEND_ACTION in data) {
5723
- this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data);
5724
- } else if (REMOVE_ACTION in data) {
5725
- this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data);
5726
- } else if (UNION_ACTION in data) {
5727
- this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
5728
- } else {
5729
- console.error('Invalid call to _enqueue():', data);
3963
+ this.save();
5730
3964
  }
5731
3965
  };
5732
3966
 
5733
- MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) {
5734
- var _this = this;
5735
- var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action));
5736
- var action_params = queued_data;
5737
-
5738
- if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) {
5739
- _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data);
5740
- if (queue_to_params_fn) {
5741
- action_params = queue_to_params_fn(queued_data);
5742
- }
5743
- action_method.call(_this, action_params, function(response, data) {
5744
- // on bad response, we want to add it back to the queue
5745
- if (response === 0) {
5746
- _this._mixpanel['persistence']._add_to_people_queue(action, queued_data);
5747
- }
5748
- if (!_.isUndefined(callback)) {
5749
- callback(response, data);
5750
- }
5751
- });
3967
+ MixpanelPersistence.prototype._get_queue_key = function(queue) {
3968
+ if (queue === SET_ACTION) {
3969
+ return SET_QUEUE_KEY;
3970
+ } else if (queue === SET_ONCE_ACTION) {
3971
+ return SET_ONCE_QUEUE_KEY;
3972
+ } else if (queue === UNSET_ACTION) {
3973
+ return UNSET_QUEUE_KEY;
3974
+ } else if (queue === ADD_ACTION) {
3975
+ return ADD_QUEUE_KEY;
3976
+ } else if (queue === APPEND_ACTION) {
3977
+ return APPEND_QUEUE_KEY;
3978
+ } else if (queue === REMOVE_ACTION) {
3979
+ return REMOVE_QUEUE_KEY;
3980
+ } else if (queue === UNION_ACTION) {
3981
+ return UNION_QUEUE_KEY;
3982
+ } else {
3983
+ console.error('Invalid queue:', queue);
5752
3984
  }
5753
3985
  };
5754
3986
 
5755
- // Flush queued engage operations - order does not matter,
5756
- // and there are network level race conditions anyway
5757
- MixpanelPeople.prototype._flush = function(
5758
- _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback
5759
- ) {
5760
- var _this = this;
5761
- var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION);
5762
- var $remove_queue = this._mixpanel['persistence']._get_queue(REMOVE_ACTION);
5763
-
5764
- this._flush_one_queue(SET_ACTION, this.set, _set_callback);
5765
- this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback);
5766
- this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); });
5767
- this._flush_one_queue(ADD_ACTION, this.increment, _add_callback);
5768
- this._flush_one_queue(UNION_ACTION, this.union, _union_callback);
5769
-
5770
- // we have to fire off each $append individually since there is
5771
- // no concat method server side
5772
- if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
5773
- var $append_item;
5774
- var append_callback = function(response, data) {
5775
- if (response === 0) {
5776
- _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item);
5777
- }
5778
- if (!_.isUndefined(_append_callback)) {
5779
- _append_callback(response, data);
5780
- }
5781
- };
5782
- for (var i = $append_queue.length - 1; i >= 0; i--) {
5783
- $append_item = $append_queue.pop();
5784
- if (!_.isEmptyObject($append_item)) {
5785
- _this.append($append_item, append_callback);
5786
- }
5787
- }
5788
- // Save the shortened append queue
5789
- _this._mixpanel['persistence'].save();
5790
- }
3987
+ MixpanelPersistence.prototype._get_queue = function(queue) {
3988
+ return this['props'][this._get_queue_key(queue)];
3989
+ };
3990
+ MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) {
3991
+ var key = this._get_queue_key(queue);
3992
+ default_val = _.isUndefined(default_val) ? {} : default_val;
5791
3993
 
5792
- // same for $remove
5793
- if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) {
5794
- var $remove_item;
5795
- var remove_callback = function(response, data) {
5796
- if (response === 0) {
5797
- _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item);
5798
- }
5799
- if (!_.isUndefined(_remove_callback)) {
5800
- _remove_callback(response, data);
5801
- }
5802
- };
5803
- for (var j = $remove_queue.length - 1; j >= 0; j--) {
5804
- $remove_item = $remove_queue.pop();
5805
- if (!_.isEmptyObject($remove_item)) {
5806
- _this.remove($remove_item, remove_callback);
5807
- }
5808
- }
5809
- _this._mixpanel['persistence'].save();
5810
- }
3994
+ return this['props'][key] || (this['props'][key] = default_val);
5811
3995
  };
5812
3996
 
5813
- MixpanelPeople.prototype._is_reserved_property = function(prop) {
5814
- return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id';
3997
+ MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) {
3998
+ var timers = this['props'][EVENT_TIMERS_KEY] || {};
3999
+ timers[event_name] = timestamp;
4000
+ this['props'][EVENT_TIMERS_KEY] = timers;
4001
+ this.save();
5815
4002
  };
5816
4003
 
5817
- // MixpanelPeople Exports
5818
- MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set;
5819
- MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once;
5820
- MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset;
5821
- MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment;
5822
- MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append;
5823
- MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove;
5824
- MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union;
5825
- MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge;
5826
- MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
5827
- MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user;
5828
- MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString;
4004
+ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
4005
+ var timers = this['props'][EVENT_TIMERS_KEY] || {};
4006
+ var timestamp = timers[event_name];
4007
+ if (!_.isUndefined(timestamp)) {
4008
+ delete this['props'][EVENT_TIMERS_KEY][event_name];
4009
+ this.save();
4010
+ }
4011
+ return timestamp;
4012
+ };
5829
4013
 
5830
4014
  /*
5831
4015
  * Mixpanel JS Library
@@ -5863,6 +4047,8 @@
5863
4047
  var NOOP_FUNC = function() {};
5864
4048
 
5865
4049
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
4050
+ /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
4051
+ /** @const */ var PAYLOAD_TYPE_JSON = 'json';
5866
4052
 
5867
4053
 
5868
4054
  /*
@@ -5879,10 +4065,10 @@
5879
4065
 
5880
4066
  // save reference to navigator.sendBeacon so it can be minified
5881
4067
  var sendBeacon = null;
5882
- if (navigator$1['sendBeacon']) {
4068
+ if (navigator['sendBeacon']) {
5883
4069
  sendBeacon = function() {
5884
4070
  // late reference to navigator.sendBeacon to allow patching/spying
5885
- return navigator$1['sendBeacon'].apply(navigator$1, arguments);
4071
+ return navigator['sendBeacon'].apply(navigator, arguments);
5886
4072
  };
5887
4073
  }
5888
4074
 
@@ -5893,10 +4079,12 @@
5893
4079
  'api_host': 'https://api-js.mixpanel.com',
5894
4080
  'api_method': 'POST',
5895
4081
  'api_transport': 'XHR',
4082
+ 'api_payload_format': PAYLOAD_TYPE_BASE64,
5896
4083
  'app_host': 'https://mixpanel.com',
5897
4084
  'cdn': 'https://cdn.mxpnl.com',
5898
4085
  'cross_site_cookie': false,
5899
4086
  'cross_subdomain_cookie': true,
4087
+ 'error_reporter': NOOP_FUNC,
5900
4088
  'persistence': 'cookie',
5901
4089
  'persistence_name': '',
5902
4090
  'cookie_domain': '',
@@ -5921,10 +4109,8 @@
5921
4109
  'opt_out_tracking_cookie_prefix': null,
5922
4110
  'property_blacklist': [],
5923
4111
  'xhr_headers': {}, // { header: value, header2: value }
5924
- 'inapp_protocol': '//',
5925
- 'inapp_link_new_window': false,
5926
4112
  'ignore_dnt': false,
5927
- 'batch_requests': false, // for now
4113
+ 'batch_requests': true,
5928
4114
  'batch_size': 50,
5929
4115
  'batch_flush_interval_ms': 5000,
5930
4116
  'batch_request_timeout_ms': 90000,
@@ -5964,8 +4150,6 @@
5964
4150
  }
5965
4151
 
5966
4152
  instance._cached_groups = {}; // cache groups in a pool
5967
- instance._user_decide_check_complete = false;
5968
- instance._events_tracked_before_user_decide_check_complete = [];
5969
4153
 
5970
4154
  instance._init(token, config, name);
5971
4155
 
@@ -5988,12 +4172,6 @@
5988
4172
  return instance;
5989
4173
  };
5990
4174
 
5991
- var encode_data_for_request = function(data) {
5992
- var json_data = _.JSONEncode(data);
5993
- var encoded_data = _.base64Encode(json_data);
5994
- return {'data': encoded_data};
5995
- };
5996
-
5997
4175
  // Initialization methods
5998
4176
 
5999
4177
  /**
@@ -6014,11 +4192,11 @@
6014
4192
  */
6015
4193
  MixpanelLib.prototype.init = function (token, config, name) {
6016
4194
  if (_.isUndefined(name)) {
6017
- console.error('You must name your new library: init(token, config, name)');
4195
+ this.report_error('You must name your new library: init(token, config, name)');
6018
4196
  return;
6019
4197
  }
6020
4198
  if (name === PRIMARY_INSTANCE_NAME) {
6021
- console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
4199
+ this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6022
4200
  return;
6023
4201
  }
6024
4202
 
@@ -6041,16 +4219,15 @@
6041
4219
 
6042
4220
  this['__loaded'] = true;
6043
4221
  this['config'] = {};
6044
- this['_triggered_notifs'] = [];
6045
4222
 
6046
- // rollout: enable batch_requests by default for 60% of projects
6047
- // (only if they have not specified a value in their init config
6048
- // and they aren't using a custom API host)
6049
4223
  var variable_features = {};
6050
- var api_host = config['api_host'];
6051
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
6052
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 60)) {
6053
- variable_features['batch_requests'] = true;
4224
+
4225
+ // default to JSON payload for standard mixpanel.com API hosts
4226
+ if (!('api_payload_format' in config)) {
4227
+ var api_host = config['api_host'] || DEFAULT_CONFIG['api_host'];
4228
+ if (api_host.match(/\.mixpanel\.com$/)) {
4229
+ variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON;
4230
+ }
6054
4231
  }
6055
4232
 
6056
4233
  this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
@@ -6079,15 +4256,32 @@
6079
4256
  } else {
6080
4257
  this.init_batchers();
6081
4258
  if (sendBeacon && window$1.addEventListener) {
6082
- window$1.addEventListener('unload', _.bind(function() {
6083
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
6084
- // Since sendBeacon doesn't report success/failure, events will not be removed from
6085
- // the persistent store; if the site is loaded again, the events will be flushed again
6086
- // on startup and deduplicated on the Mixpanel server side.
4259
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
4260
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
4261
+ // events will not be removed from the persistent store; if the site is loaded again,
4262
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
4263
+ // side.
4264
+ // There is no reliable way to capture only page close events, so we lean on the
4265
+ // visibilitychange and pagehide events as recommended at
4266
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
4267
+ // These events fire when the user clicks away from the current page/tab, so will occur
4268
+ // more frequently than page unload, but are the only mechanism currently for capturing
4269
+ // this scenario somewhat reliably.
4270
+ var flush_on_unload = _.bind(function() {
6087
4271
  if (!this.request_batchers.events.stopped) {
6088
4272
  this.request_batchers.events.flush({unloading: true});
6089
4273
  }
6090
- }, this));
4274
+ }, this);
4275
+ window$1.addEventListener('pagehide', function(ev) {
4276
+ if (ev['persisted']) {
4277
+ flush_on_unload();
4278
+ }
4279
+ });
4280
+ window$1.addEventListener('visibilitychange', function() {
4281
+ if (document$1['visibilityState'] === 'hidden') {
4282
+ flush_on_unload();
4283
+ }
4284
+ });
6091
4285
  }
6092
4286
  }
6093
4287
  }
@@ -6143,7 +4337,7 @@
6143
4337
 
6144
4338
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
6145
4339
  if (this.get_config('img')) {
6146
- console.error('You can\'t use DOM tracking functions with img = true.');
4340
+ this.report_error('You can\'t use DOM tracking functions with img = true.');
6147
4341
  return false;
6148
4342
  }
6149
4343
 
@@ -6245,6 +4439,7 @@
6245
4439
 
6246
4440
  url += '?' + _.HTTPBuildQuery(data);
6247
4441
 
4442
+ var lib = this;
6248
4443
  if ('img' in data) {
6249
4444
  var img = document$1.createElement('img');
6250
4445
  img.src = url;
@@ -6253,7 +4448,7 @@
6253
4448
  try {
6254
4449
  succeeded = sendBeacon(url, body_data);
6255
4450
  } catch (e) {
6256
- console.error(e);
4451
+ lib.report_error(e);
6257
4452
  succeeded = false;
6258
4453
  }
6259
4454
  try {
@@ -6261,7 +4456,7 @@
6261
4456
  callback(succeeded ? 1 : 0);
6262
4457
  }
6263
4458
  } catch (e) {
6264
- console.error(e);
4459
+ lib.report_error(e);
6265
4460
  }
6266
4461
  } else if (USE_XHR) {
6267
4462
  try {
@@ -6293,7 +4488,7 @@
6293
4488
  try {
6294
4489
  response = _.JSONDecode(req.responseText);
6295
4490
  } catch (e) {
6296
- console.error(e);
4491
+ lib.report_error(e);
6297
4492
  if (options.ignore_json_errors) {
6298
4493
  response = req.responseText;
6299
4494
  } else {
@@ -6316,7 +4511,7 @@
6316
4511
  } else {
6317
4512
  error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
6318
4513
  }
6319
- console.error(error);
4514
+ lib.report_error(error);
6320
4515
  if (callback) {
6321
4516
  if (verbose_mode) {
6322
4517
  callback({status: 0, error: error, xhr_req: req});
@@ -6329,7 +4524,7 @@
6329
4524
  };
6330
4525
  req.send(body_data);
6331
4526
  } catch (e) {
6332
- console.error(e);
4527
+ lib.report_error(e);
6333
4528
  succeeded = false;
6334
4529
  }
6335
4530
  } else {
@@ -6412,14 +4607,16 @@
6412
4607
  sendRequestFunc: _.bind(function(data, options, cb) {
6413
4608
  this._send_request(
6414
4609
  this.get_config('api_host') + attrs.endpoint,
6415
- encode_data_for_request(data),
4610
+ this._encode_data_for_request(data),
6416
4611
  options,
6417
4612
  this._prepare_callback(cb, data)
6418
4613
  );
6419
4614
  }, this),
6420
4615
  beforeSendHook: _.bind(function(item) {
6421
4616
  return this._run_hook('before_send_' + attrs.type, item);
6422
- }, this)
4617
+ }, this),
4618
+ errorReporter: this.get_config('error_reporter'),
4619
+ stopAllBatchingFunc: _.bind(this.stop_batch_senders, this)
6423
4620
  }
6424
4621
  );
6425
4622
  }, this);
@@ -6486,6 +4683,14 @@
6486
4683
  }
6487
4684
  };
6488
4685
 
4686
+ MixpanelLib.prototype._encode_data_for_request = function(data) {
4687
+ var encoded_data = _.JSONEncode(data);
4688
+ if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) {
4689
+ encoded_data = _.base64Encode(encoded_data);
4690
+ }
4691
+ return {'data': encoded_data};
4692
+ };
4693
+
6489
4694
  // internal method for handling track vs batch-enqueue logic
6490
4695
  MixpanelLib.prototype._track_or_batch = function(options, callback) {
6491
4696
  var truncated_data = _.truncate(options.data, 255);
@@ -6505,7 +4710,7 @@
6505
4710
  console.log(truncated_data);
6506
4711
  return this._send_request(
6507
4712
  endpoint,
6508
- encode_data_for_request(truncated_data),
4713
+ this._encode_data_for_request(truncated_data),
6509
4714
  send_request_options,
6510
4715
  this._prepare_callback(callback, truncated_data)
6511
4716
  );
@@ -6568,7 +4773,7 @@
6568
4773
  }
6569
4774
 
6570
4775
  if (_.isUndefined(event_name)) {
6571
- console.error('No event name provided to mixpanel.track');
4776
+ this.report_error('No event name provided to mixpanel.track');
6572
4777
  return;
6573
4778
  }
6574
4779
 
@@ -6609,7 +4814,7 @@
6609
4814
  delete properties[blacklisted_prop];
6610
4815
  });
6611
4816
  } else {
6612
- console.error('Invalid value for property_blacklist config: ' + property_blacklist);
4817
+ this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
6613
4818
  }
6614
4819
 
6615
4820
  var data = {
@@ -6625,8 +4830,6 @@
6625
4830
  send_request_options: options
6626
4831
  }, callback);
6627
4832
 
6628
- this._check_and_handle_triggered_notifications(data);
6629
-
6630
4833
  return ret;
6631
4834
  });
6632
4835
 
@@ -6854,7 +5057,7 @@
6854
5057
  */
6855
5058
  MixpanelLib.prototype.time_event = function(event_name) {
6856
5059
  if (_.isUndefined(event_name)) {
6857
- console.error('No event name provided to mixpanel.time_event');
5060
+ this.report_error('No event name provided to mixpanel.time_event');
6858
5061
  return;
6859
5062
  }
6860
5063
 
@@ -7037,7 +5240,6 @@
7037
5240
  this.unregister(ALIAS_ID_KEY);
7038
5241
  this.register({'distinct_id': new_distinct_id});
7039
5242
  }
7040
- this._check_and_handle_notifications(this.get_distinct_id());
7041
5243
  this._flags.identify_called = true;
7042
5244
  // Flush any queued up people requests
7043
5245
  this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback);
@@ -7127,7 +5329,7 @@
7127
5329
  // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
7128
5330
  // this ID, as it will duplicate users.
7129
5331
  if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
7130
- console.critical('Attempting to create alias for existing People user - aborting.');
5332
+ this.report_error('Attempting to create alias for existing People user - aborting.');
7131
5333
  return -2;
7132
5334
  }
7133
5335
 
@@ -7147,7 +5349,7 @@
7147
5349
  _this.identify(alias);
7148
5350
  });
7149
5351
  } else {
7150
- console.error('alias matches current distinct_id - skipping api call.');
5352
+ this.report_error('alias matches current distinct_id - skipping api call.');
7151
5353
  this.identify(alias);
7152
5354
  return -1;
7153
5355
  }
@@ -7274,14 +5476,6 @@
7274
5476
  * // the format {'Header-Name': value}
7275
5477
  * xhr_headers: {}
7276
5478
  *
7277
- * // protocol for fetching in-app message resources, e.g.
7278
- * // 'https://' or 'http://'; defaults to '//' (which defers to the
7279
- * // current page's protocol)
7280
- * inapp_protocol: '//'
7281
- *
7282
- * // whether to open in-app message link in new tab/window
7283
- * inapp_link_new_window: false
7284
- *
7285
5479
  * // whether to ignore or respect the web browser's Do Not Track setting
7286
5480
  * ignore_dnt: false
7287
5481
  * }
@@ -7330,7 +5524,7 @@
7330
5524
  MixpanelLib.prototype._run_hook = function(hook_name) {
7331
5525
  var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
7332
5526
  if (typeof ret === 'undefined') {
7333
- console.error(hook_name + ' hook did not return a value');
5527
+ this.report_error(hook_name + ' hook did not return a value');
7334
5528
  ret = null;
7335
5529
  }
7336
5530
  return ret;
@@ -7372,75 +5566,6 @@
7372
5566
  _.include(this.__disabled_events, event_name);
7373
5567
  };
7374
5568
 
7375
- MixpanelLib.prototype._check_and_handle_triggered_notifications = addOptOutCheckMixpanelLib(function(event_data) {
7376
- if (!this._user_decide_check_complete) {
7377
- this._events_tracked_before_user_decide_check_complete.push(event_data);
7378
- } else {
7379
- var arr = this['_triggered_notifs'];
7380
- for (var i = 0; i < arr.length; i++) {
7381
- var notif = new MixpanelNotification(arr[i], this);
7382
- if (notif._matches_event_data(event_data)) {
7383
- this._show_notification(arr[i]);
7384
- return;
7385
- }
7386
- }
7387
- }
7388
- });
7389
-
7390
- MixpanelLib.prototype._check_and_handle_notifications = addOptOutCheckMixpanelLib(function(distinct_id) {
7391
- if (
7392
- !distinct_id ||
7393
- this._flags.identify_called ||
7394
- this.get_config('disable_notifications')
7395
- ) {
7396
- return;
7397
- }
7398
-
7399
- console.log('MIXPANEL NOTIFICATION CHECK');
7400
-
7401
- var data = {
7402
- 'verbose': true,
7403
- 'version': '3',
7404
- 'lib': 'web',
7405
- 'token': this.get_config('token'),
7406
- 'distinct_id': distinct_id
7407
- };
7408
- this._send_request(
7409
- this.get_config('api_host') + '/decide/',
7410
- data,
7411
- {method: 'GET', transport: 'XHR'},
7412
- this._prepare_callback(_.bind(function(result) {
7413
- if (result['notifications'] && result['notifications'].length > 0) {
7414
- this['_triggered_notifs'] = [];
7415
- var notifications = [];
7416
- _.each(result['notifications'], function(notif) {
7417
- (notif['display_triggers'] && notif['display_triggers'].length > 0 ? this['_triggered_notifs'] : notifications).push(notif);
7418
- }, this);
7419
- if (notifications.length > 0) {
7420
- this._show_notification.call(this, notifications[0]);
7421
- }
7422
- }
7423
- this._handle_user_decide_check_complete();
7424
- }, this))
7425
- );
7426
- });
7427
-
7428
- MixpanelLib.prototype._handle_user_decide_check_complete = function() {
7429
- this._user_decide_check_complete = true;
7430
-
7431
- // check notifications against events that were tracked before decide call completed
7432
- var events = this._events_tracked_before_user_decide_check_complete;
7433
- while (events.length > 0) {
7434
- var data = events.shift(); // replay in the same order they came in
7435
- this._check_and_handle_triggered_notifications(data);
7436
- }
7437
- };
7438
-
7439
- MixpanelLib.prototype._show_notification = function(notif_data) {
7440
- var notification = new MixpanelNotification(notif_data, this);
7441
- notification.show();
7442
- };
7443
-
7444
5569
  // perform some housekeeping around GDPR opt-in/out state
7445
5570
  MixpanelLib.prototype._gdpr_init = function() {
7446
5571
  var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage';
@@ -7686,6 +5811,18 @@
7686
5811
  this._gdpr_update_persistence(options);
7687
5812
  };
7688
5813
 
5814
+ MixpanelLib.prototype.report_error = function(msg, err) {
5815
+ console.error.apply(console.error, arguments);
5816
+ try {
5817
+ if (!err && !(msg instanceof Error)) {
5818
+ msg = new Error(msg);
5819
+ }
5820
+ this.get_config('error_reporter')(msg, err);
5821
+ } catch(err) {
5822
+ console.error(err);
5823
+ }
5824
+ };
5825
+
7689
5826
  // EXPORTS (for closure compiler)
7690
5827
 
7691
5828
  // MixpanelLib Exports
@@ -7708,9 +5845,6 @@
7708
5845
  MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property;
7709
5846
  MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id;
7710
5847
  MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString;
7711
- MixpanelLib.prototype['_check_and_handle_notifications'] = MixpanelLib.prototype._check_and_handle_notifications;
7712
- MixpanelLib.prototype['_handle_user_decide_check_complete'] = MixpanelLib.prototype._handle_user_decide_check_complete;
7713
- MixpanelLib.prototype['_show_notification'] = MixpanelLib.prototype._show_notification;
7714
5848
  MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking;
7715
5849
  MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking;
7716
5850
  MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking;
@@ -7731,8 +5865,6 @@
7731
5865
  MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain;
7732
5866
  MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear;
7733
5867
 
7734
- _.safewrap_class(MixpanelLib, ['identify', '_check_and_handle_notifications', '_show_notification']);
7735
-
7736
5868
 
7737
5869
  var instances = {};
7738
5870
  var extend_mp = function() {