mixpanel-browser 2.53.0 → 2.54.1

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.
@@ -1432,7 +1432,7 @@
1432
1432
  return doc.contains(n) || shadowHostInDom(n);
1433
1433
  }
1434
1434
 
1435
- var EventType = /* @__PURE__ */ ((EventType2) => {
1435
+ var EventType$1 = /* @__PURE__ */ ((EventType2) => {
1436
1436
  EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
1437
1437
  EventType2[EventType2["Load"] = 1] = "Load";
1438
1438
  EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
@@ -1441,8 +1441,8 @@
1441
1441
  EventType2[EventType2["Custom"] = 5] = "Custom";
1442
1442
  EventType2[EventType2["Plugin"] = 6] = "Plugin";
1443
1443
  return EventType2;
1444
- })(EventType || {});
1445
- var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
1444
+ })(EventType$1 || {});
1445
+ var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => {
1446
1446
  IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
1447
1447
  IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
1448
1448
  IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
@@ -1461,7 +1461,7 @@
1461
1461
  IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
1462
1462
  IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
1463
1463
  return IncrementalSource2;
1464
- })(IncrementalSource || {});
1464
+ })(IncrementalSource$1 || {});
1465
1465
  var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => {
1466
1466
  MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp";
1467
1467
  MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown";
@@ -2180,10 +2180,10 @@
2180
2180
  timeOffset: nowTimestamp() - timeBaseline,
2181
2181
  });
2182
2182
  wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent
2183
- ? IncrementalSource.Drag
2183
+ ? IncrementalSource$1.Drag
2184
2184
  : evt instanceof MouseEvent
2185
- ? IncrementalSource.MouseMove
2186
- : IncrementalSource.TouchMove);
2185
+ ? IncrementalSource$1.MouseMove
2186
+ : IncrementalSource$1.TouchMove);
2187
2187
  }), threshold, {
2188
2188
  trailing: false,
2189
2189
  }));
@@ -3096,7 +3096,7 @@
3096
3096
  transformCrossOriginEvent(iframeEl, e) {
3097
3097
  var _a;
3098
3098
  switch (e.type) {
3099
- case EventType.FullSnapshot: {
3099
+ case EventType$1.FullSnapshot: {
3100
3100
  this.crossOriginIframeMirror.reset(iframeEl);
3101
3101
  this.crossOriginIframeStyleMirror.reset(iframeEl);
3102
3102
  this.replaceIdOnNode(e.data.node, iframeEl);
@@ -3105,9 +3105,9 @@
3105
3105
  this.patchRootIdOnNode(e.data.node, rootId);
3106
3106
  return {
3107
3107
  timestamp: e.timestamp,
3108
- type: EventType.IncrementalSnapshot,
3108
+ type: EventType$1.IncrementalSnapshot,
3109
3109
  data: {
3110
- source: IncrementalSource.Mutation,
3110
+ source: IncrementalSource$1.Mutation,
3111
3111
  adds: [
3112
3112
  {
3113
3113
  parentId: this.mirror.getId(iframeEl),
@@ -3122,21 +3122,21 @@
3122
3122
  },
3123
3123
  };
3124
3124
  }
3125
- case EventType.Meta:
3126
- case EventType.Load:
3127
- case EventType.DomContentLoaded: {
3125
+ case EventType$1.Meta:
3126
+ case EventType$1.Load:
3127
+ case EventType$1.DomContentLoaded: {
3128
3128
  return false;
3129
3129
  }
3130
- case EventType.Plugin: {
3130
+ case EventType$1.Plugin: {
3131
3131
  return e;
3132
3132
  }
3133
- case EventType.Custom: {
3133
+ case EventType$1.Custom: {
3134
3134
  this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']);
3135
3135
  return e;
3136
3136
  }
3137
- case EventType.IncrementalSnapshot: {
3137
+ case EventType$1.IncrementalSnapshot: {
3138
3138
  switch (e.data.source) {
3139
- case IncrementalSource.Mutation: {
3139
+ case IncrementalSource$1.Mutation: {
3140
3140
  e.data.adds.forEach((n) => {
3141
3141
  this.replaceIds(n, iframeEl, [
3142
3142
  'parentId',
@@ -3158,41 +3158,41 @@
3158
3158
  });
3159
3159
  return e;
3160
3160
  }
3161
- case IncrementalSource.Drag:
3162
- case IncrementalSource.TouchMove:
3163
- case IncrementalSource.MouseMove: {
3161
+ case IncrementalSource$1.Drag:
3162
+ case IncrementalSource$1.TouchMove:
3163
+ case IncrementalSource$1.MouseMove: {
3164
3164
  e.data.positions.forEach((p) => {
3165
3165
  this.replaceIds(p, iframeEl, ['id']);
3166
3166
  });
3167
3167
  return e;
3168
3168
  }
3169
- case IncrementalSource.ViewportResize: {
3169
+ case IncrementalSource$1.ViewportResize: {
3170
3170
  return false;
3171
3171
  }
3172
- case IncrementalSource.MediaInteraction:
3173
- case IncrementalSource.MouseInteraction:
3174
- case IncrementalSource.Scroll:
3175
- case IncrementalSource.CanvasMutation:
3176
- case IncrementalSource.Input: {
3172
+ case IncrementalSource$1.MediaInteraction:
3173
+ case IncrementalSource$1.MouseInteraction:
3174
+ case IncrementalSource$1.Scroll:
3175
+ case IncrementalSource$1.CanvasMutation:
3176
+ case IncrementalSource$1.Input: {
3177
3177
  this.replaceIds(e.data, iframeEl, ['id']);
3178
3178
  return e;
3179
3179
  }
3180
- case IncrementalSource.StyleSheetRule:
3181
- case IncrementalSource.StyleDeclaration: {
3180
+ case IncrementalSource$1.StyleSheetRule:
3181
+ case IncrementalSource$1.StyleDeclaration: {
3182
3182
  this.replaceIds(e.data, iframeEl, ['id']);
3183
3183
  this.replaceStyleIds(e.data, iframeEl, ['styleId']);
3184
3184
  return e;
3185
3185
  }
3186
- case IncrementalSource.Font: {
3186
+ case IncrementalSource$1.Font: {
3187
3187
  return e;
3188
3188
  }
3189
- case IncrementalSource.Selection: {
3189
+ case IncrementalSource$1.Selection: {
3190
3190
  e.data.ranges.forEach((range) => {
3191
3191
  this.replaceIds(range, iframeEl, ['start', 'end']);
3192
3192
  });
3193
3193
  return e;
3194
3194
  }
3195
- case IncrementalSource.AdoptedStyleSheet: {
3195
+ case IncrementalSource$1.AdoptedStyleSheet: {
3196
3196
  this.replaceIds(e.data, iframeEl, ['id']);
3197
3197
  this.replaceStyleIds(e.data, iframeEl, ['styleIds']);
3198
3198
  (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => {
@@ -4143,9 +4143,9 @@
4143
4143
  wrappedEmit = (e, isCheckout) => {
4144
4144
  var _a;
4145
4145
  if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) &&
4146
- e.type !== EventType.FullSnapshot &&
4147
- !(e.type === EventType.IncrementalSnapshot &&
4148
- e.data.source === IncrementalSource.Mutation)) {
4146
+ e.type !== EventType$1.FullSnapshot &&
4147
+ !(e.type === EventType$1.IncrementalSnapshot &&
4148
+ e.data.source === IncrementalSource$1.Mutation)) {
4149
4149
  mutationBuffers.forEach((buf) => buf.unfreeze());
4150
4150
  }
4151
4151
  if (inEmittingFrame) {
@@ -4160,12 +4160,12 @@
4160
4160
  };
4161
4161
  window.parent.postMessage(message, '*');
4162
4162
  }
4163
- if (e.type === EventType.FullSnapshot) {
4163
+ if (e.type === EventType$1.FullSnapshot) {
4164
4164
  lastFullSnapshotEvent = e;
4165
4165
  incrementalSnapshotCount = 0;
4166
4166
  }
4167
- else if (e.type === EventType.IncrementalSnapshot) {
4168
- if (e.data.source === IncrementalSource.Mutation &&
4167
+ else if (e.type === EventType$1.IncrementalSnapshot) {
4168
+ if (e.data.source === IncrementalSource$1.Mutation &&
4169
4169
  e.data.isAttachIframe) {
4170
4170
  return;
4171
4171
  }
@@ -4180,21 +4180,21 @@
4180
4180
  };
4181
4181
  const wrappedMutationEmit = (m) => {
4182
4182
  wrappedEmit(wrapEvent({
4183
- type: EventType.IncrementalSnapshot,
4184
- data: Object.assign({ source: IncrementalSource.Mutation }, m),
4183
+ type: EventType$1.IncrementalSnapshot,
4184
+ data: Object.assign({ source: IncrementalSource$1.Mutation }, m),
4185
4185
  }));
4186
4186
  };
4187
4187
  const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({
4188
- type: EventType.IncrementalSnapshot,
4189
- data: Object.assign({ source: IncrementalSource.Scroll }, p),
4188
+ type: EventType$1.IncrementalSnapshot,
4189
+ data: Object.assign({ source: IncrementalSource$1.Scroll }, p),
4190
4190
  }));
4191
4191
  const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({
4192
- type: EventType.IncrementalSnapshot,
4193
- data: Object.assign({ source: IncrementalSource.CanvasMutation }, p),
4192
+ type: EventType$1.IncrementalSnapshot,
4193
+ data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p),
4194
4194
  }));
4195
4195
  const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({
4196
- type: EventType.IncrementalSnapshot,
4197
- data: Object.assign({ source: IncrementalSource.AdoptedStyleSheet }, a),
4196
+ type: EventType$1.IncrementalSnapshot,
4197
+ data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a),
4198
4198
  }));
4199
4199
  const stylesheetManager = new StylesheetManager({
4200
4200
  mutationCb: wrappedMutationEmit,
@@ -4256,7 +4256,7 @@
4256
4256
  return;
4257
4257
  }
4258
4258
  wrappedEmit(wrapEvent({
4259
- type: EventType.Meta,
4259
+ type: EventType$1.Meta,
4260
4260
  data: {
4261
4261
  href: window.location.href,
4262
4262
  width: getWindowWidth(),
@@ -4303,7 +4303,7 @@
4303
4303
  return console.warn('Failed to snapshot the document');
4304
4304
  }
4305
4305
  wrappedEmit(wrapEvent({
4306
- type: EventType.FullSnapshot,
4306
+ type: EventType$1.FullSnapshot,
4307
4307
  data: {
4308
4308
  node,
4309
4309
  initialOffset: getWindowScroll(window),
@@ -4320,52 +4320,52 @@
4320
4320
  return callbackWrapper(initObservers)({
4321
4321
  mutationCb: wrappedMutationEmit,
4322
4322
  mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({
4323
- type: EventType.IncrementalSnapshot,
4323
+ type: EventType$1.IncrementalSnapshot,
4324
4324
  data: {
4325
4325
  source,
4326
4326
  positions,
4327
4327
  },
4328
4328
  })),
4329
4329
  mouseInteractionCb: (d) => wrappedEmit(wrapEvent({
4330
- type: EventType.IncrementalSnapshot,
4331
- data: Object.assign({ source: IncrementalSource.MouseInteraction }, d),
4330
+ type: EventType$1.IncrementalSnapshot,
4331
+ data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d),
4332
4332
  })),
4333
4333
  scrollCb: wrappedScrollEmit,
4334
4334
  viewportResizeCb: (d) => wrappedEmit(wrapEvent({
4335
- type: EventType.IncrementalSnapshot,
4336
- data: Object.assign({ source: IncrementalSource.ViewportResize }, d),
4335
+ type: EventType$1.IncrementalSnapshot,
4336
+ data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d),
4337
4337
  })),
4338
4338
  inputCb: (v) => wrappedEmit(wrapEvent({
4339
- type: EventType.IncrementalSnapshot,
4340
- data: Object.assign({ source: IncrementalSource.Input }, v),
4339
+ type: EventType$1.IncrementalSnapshot,
4340
+ data: Object.assign({ source: IncrementalSource$1.Input }, v),
4341
4341
  })),
4342
4342
  mediaInteractionCb: (p) => wrappedEmit(wrapEvent({
4343
- type: EventType.IncrementalSnapshot,
4344
- data: Object.assign({ source: IncrementalSource.MediaInteraction }, p),
4343
+ type: EventType$1.IncrementalSnapshot,
4344
+ data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p),
4345
4345
  })),
4346
4346
  styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({
4347
- type: EventType.IncrementalSnapshot,
4348
- data: Object.assign({ source: IncrementalSource.StyleSheetRule }, r),
4347
+ type: EventType$1.IncrementalSnapshot,
4348
+ data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r),
4349
4349
  })),
4350
4350
  styleDeclarationCb: (r) => wrappedEmit(wrapEvent({
4351
- type: EventType.IncrementalSnapshot,
4352
- data: Object.assign({ source: IncrementalSource.StyleDeclaration }, r),
4351
+ type: EventType$1.IncrementalSnapshot,
4352
+ data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r),
4353
4353
  })),
4354
4354
  canvasMutationCb: wrappedCanvasMutationEmit,
4355
4355
  fontCb: (p) => wrappedEmit(wrapEvent({
4356
- type: EventType.IncrementalSnapshot,
4357
- data: Object.assign({ source: IncrementalSource.Font }, p),
4356
+ type: EventType$1.IncrementalSnapshot,
4357
+ data: Object.assign({ source: IncrementalSource$1.Font }, p),
4358
4358
  })),
4359
4359
  selectionCb: (p) => {
4360
4360
  wrappedEmit(wrapEvent({
4361
- type: EventType.IncrementalSnapshot,
4362
- data: Object.assign({ source: IncrementalSource.Selection }, p),
4361
+ type: EventType$1.IncrementalSnapshot,
4362
+ data: Object.assign({ source: IncrementalSource$1.Selection }, p),
4363
4363
  }));
4364
4364
  },
4365
4365
  customElementCb: (c) => {
4366
4366
  wrappedEmit(wrapEvent({
4367
- type: EventType.IncrementalSnapshot,
4368
- data: Object.assign({ source: IncrementalSource.CustomElement }, c),
4367
+ type: EventType$1.IncrementalSnapshot,
4368
+ data: Object.assign({ source: IncrementalSource$1.CustomElement }, c),
4369
4369
  }));
4370
4370
  },
4371
4371
  blockClass,
@@ -4399,7 +4399,7 @@
4399
4399
  observer: p.observer,
4400
4400
  options: p.options,
4401
4401
  callback: (payload) => wrappedEmit(wrapEvent({
4402
- type: EventType.Plugin,
4402
+ type: EventType$1.Plugin,
4403
4403
  data: {
4404
4404
  plugin: p.name,
4405
4405
  payload,
@@ -4428,7 +4428,7 @@
4428
4428
  else {
4429
4429
  handlers.push(on('DOMContentLoaded', () => {
4430
4430
  wrappedEmit(wrapEvent({
4431
- type: EventType.DomContentLoaded,
4431
+ type: EventType$1.DomContentLoaded,
4432
4432
  data: {},
4433
4433
  }));
4434
4434
  if (recordAfter === 'DOMContentLoaded')
@@ -4436,7 +4436,7 @@
4436
4436
  }));
4437
4437
  handlers.push(on('load', () => {
4438
4438
  wrappedEmit(wrapEvent({
4439
- type: EventType.Load,
4439
+ type: EventType$1.Load,
4440
4440
  data: {},
4441
4441
  }));
4442
4442
  if (recordAfter === 'load')
@@ -4459,7 +4459,7 @@
4459
4459
  throw new Error('please add custom event after start recording');
4460
4460
  }
4461
4461
  wrappedEmit(wrapEvent({
4462
- type: EventType.Custom,
4462
+ type: EventType$1.Custom,
4463
4463
  data: {
4464
4464
  tag,
4465
4465
  payload,
@@ -4477,9 +4477,40 @@
4477
4477
  };
4478
4478
  record.mirror = mirror;
4479
4479
 
4480
+ var EventType = /* @__PURE__ */ ((EventType2) => {
4481
+ EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
4482
+ EventType2[EventType2["Load"] = 1] = "Load";
4483
+ EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
4484
+ EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
4485
+ EventType2[EventType2["Meta"] = 4] = "Meta";
4486
+ EventType2[EventType2["Custom"] = 5] = "Custom";
4487
+ EventType2[EventType2["Plugin"] = 6] = "Plugin";
4488
+ return EventType2;
4489
+ })(EventType || {});
4490
+ var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4491
+ IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
4492
+ IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
4493
+ IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
4494
+ IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
4495
+ IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
4496
+ IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
4497
+ IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
4498
+ IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
4499
+ IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
4500
+ IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
4501
+ IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
4502
+ IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
4503
+ IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
4504
+ IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
4505
+ IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
4506
+ IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
4507
+ IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
4508
+ return IncrementalSource2;
4509
+ })(IncrementalSource || {});
4510
+
4480
4511
  var Config = {
4481
4512
  DEBUG: false,
4482
- LIB_VERSION: '2.53.0'
4513
+ LIB_VERSION: '2.54.1'
4483
4514
  };
4484
4515
 
4485
4516
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -6344,8 +6375,783 @@
6344
6375
  };
6345
6376
  }
6346
6377
 
6378
+ var logger$3 = console_with_prefix('lock');
6379
+
6380
+ /**
6381
+ * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
6382
+ * window/tab at a time will be able to access shared resources.
6383
+ *
6384
+ * Based on the Alur and Taubenfeld fast lock
6385
+ * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html)
6386
+ * with an added timeout to ensure there will be eventual progress in the event
6387
+ * that a window is closed in the middle of the callback.
6388
+ *
6389
+ * Implementation based on the original version by David Wolever (https://github.com/wolever)
6390
+ * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432.
6391
+ *
6392
+ * @example
6393
+ * const myLock = new SharedLock('some-key');
6394
+ * myLock.withLock(function() {
6395
+ * console.log('I hold the mutex!');
6396
+ * });
6397
+ *
6398
+ * @constructor
6399
+ */
6400
+ var SharedLock = function(key, options) {
6401
+ options = options || {};
6402
+
6403
+ this.storageKey = key;
6404
+ this.storage = options.storage || window.localStorage;
6405
+ this.pollIntervalMS = options.pollIntervalMS || 100;
6406
+ this.timeoutMS = options.timeoutMS || 2000;
6407
+ };
6408
+
6409
+ // pass in a specific pid to test contention scenarios; otherwise
6410
+ // it is chosen randomly for each acquisition attempt
6411
+ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) {
6412
+ if (!pid && typeof errorCB !== 'function') {
6413
+ pid = errorCB;
6414
+ errorCB = null;
6415
+ }
6416
+
6417
+ var i = pid || (new Date().getTime() + '|' + Math.random());
6418
+ var startTime = new Date().getTime();
6419
+
6420
+ var key = this.storageKey;
6421
+ var pollIntervalMS = this.pollIntervalMS;
6422
+ var timeoutMS = this.timeoutMS;
6423
+ var storage = this.storage;
6424
+
6425
+ var keyX = key + ':X';
6426
+ var keyY = key + ':Y';
6427
+ var keyZ = key + ':Z';
6428
+
6429
+ var reportError = function(err) {
6430
+ errorCB && errorCB(err);
6431
+ };
6432
+
6433
+ var delay = function(cb) {
6434
+ if (new Date().getTime() - startTime > timeoutMS) {
6435
+ logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
6436
+ storage.removeItem(keyZ);
6437
+ storage.removeItem(keyY);
6438
+ loop();
6439
+ return;
6440
+ }
6441
+ setTimeout(function() {
6442
+ try {
6443
+ cb();
6444
+ } catch(err) {
6445
+ reportError(err);
6446
+ }
6447
+ }, pollIntervalMS * (Math.random() + 0.1));
6448
+ };
6449
+
6450
+ var waitFor = function(predicate, cb) {
6451
+ if (predicate()) {
6452
+ cb();
6453
+ } else {
6454
+ delay(function() {
6455
+ waitFor(predicate, cb);
6456
+ });
6457
+ }
6458
+ };
6459
+
6460
+ var getSetY = function() {
6461
+ var valY = storage.getItem(keyY);
6462
+ if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
6463
+ return false;
6464
+ } else {
6465
+ storage.setItem(keyY, i);
6466
+ if (storage.getItem(keyY) === i) {
6467
+ return true;
6468
+ } else {
6469
+ if (!localStorageSupported(storage, true)) {
6470
+ throw new Error('localStorage support dropped while acquiring lock');
6471
+ }
6472
+ return false;
6473
+ }
6474
+ }
6475
+ };
6476
+
6477
+ var loop = function() {
6478
+ storage.setItem(keyX, i);
6479
+
6480
+ waitFor(getSetY, function() {
6481
+ if (storage.getItem(keyX) === i) {
6482
+ criticalSection();
6483
+ return;
6484
+ }
6485
+
6486
+ delay(function() {
6487
+ if (storage.getItem(keyY) !== i) {
6488
+ loop();
6489
+ return;
6490
+ }
6491
+ waitFor(function() {
6492
+ return !storage.getItem(keyZ);
6493
+ }, criticalSection);
6494
+ });
6495
+ });
6496
+ };
6497
+
6498
+ var criticalSection = function() {
6499
+ storage.setItem(keyZ, '1');
6500
+ try {
6501
+ lockedCB();
6502
+ } finally {
6503
+ storage.removeItem(keyZ);
6504
+ if (storage.getItem(keyY) === i) {
6505
+ storage.removeItem(keyY);
6506
+ }
6507
+ if (storage.getItem(keyX) === i) {
6508
+ storage.removeItem(keyX);
6509
+ }
6510
+ }
6511
+ };
6512
+
6513
+ try {
6514
+ if (localStorageSupported(storage, true)) {
6515
+ loop();
6516
+ } else {
6517
+ throw new Error('localStorage support check failed');
6518
+ }
6519
+ } catch(err) {
6520
+ reportError(err);
6521
+ }
6522
+ };
6523
+
6524
+ var logger$2 = console_with_prefix('batch');
6525
+
6526
+ /**
6527
+ * RequestQueue: queue for batching API requests with localStorage backup for retries.
6528
+ * Maintains an in-memory queue which represents the source of truth for the current
6529
+ * page, but also writes all items out to a copy in the browser's localStorage, which
6530
+ * can be read on subsequent pageloads and retried. For batchability, all the request
6531
+ * items in the queue should be of the same type (events, people updates, group updates)
6532
+ * so they can be sent in a single request to the same API endpoint.
6533
+ *
6534
+ * LocalStorage keying and locking: In order for reloads and subsequent pageloads of
6535
+ * the same site to access the same persisted data, they must share the same localStorage
6536
+ * key (for instance based on project token and queue type). Therefore access to the
6537
+ * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent
6538
+ * simultaneously open windows/tabs from overwriting each other's data (which would lead
6539
+ * to data loss in some situations).
6540
+ * @constructor
6541
+ */
6542
+ var RequestQueue = function(storageKey, options) {
6543
+ options = options || {};
6544
+ this.storageKey = storageKey;
6545
+ this.storage = options.storage || window.localStorage;
6546
+ this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2);
6547
+ this.lock = new SharedLock(storageKey, {storage: this.storage});
6548
+
6549
+ this.usePersistence = options.usePersistence;
6550
+ this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
6551
+
6552
+ this.memQueue = [];
6553
+ };
6554
+
6555
+ /**
6556
+ * Add one item to queues (memory and localStorage). The queued entry includes
6557
+ * the given item along with an auto-generated ID and a "flush-after" timestamp.
6558
+ * It is expected that the item will be sent over the network and dequeued
6559
+ * before the flush-after time; if this doesn't happen it is considered orphaned
6560
+ * (e.g., the original tab where it was enqueued got closed before it could be
6561
+ * sent) and the item can be sent by any tab that finds it in localStorage.
6562
+ *
6563
+ * The final callback param is called with a param indicating success or
6564
+ * failure of the enqueue operation; it is asynchronous because the localStorage
6565
+ * lock is asynchronous.
6566
+ */
6567
+ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
6568
+ var queueEntry = {
6569
+ 'id': cheap_guid(),
6570
+ 'flushAfter': new Date().getTime() + flushInterval * 2,
6571
+ 'payload': item
6572
+ };
6573
+
6574
+ if (!this.usePersistence) {
6575
+ this.memQueue.push(queueEntry);
6576
+ if (cb) {
6577
+ cb(true);
6578
+ }
6579
+ } else {
6580
+ this.lock.withLock(_.bind(function lockAcquired() {
6581
+ var succeeded;
6582
+ try {
6583
+ var storedQueue = this.readFromStorage();
6584
+ storedQueue.push(queueEntry);
6585
+ succeeded = this.saveToStorage(storedQueue);
6586
+ if (succeeded) {
6587
+ // only add to in-memory queue when storage succeeds
6588
+ this.memQueue.push(queueEntry);
6589
+ }
6590
+ } catch(err) {
6591
+ this.reportError('Error enqueueing item', item);
6592
+ succeeded = false;
6593
+ }
6594
+ if (cb) {
6595
+ cb(succeeded);
6596
+ }
6597
+ }, this), _.bind(function lockFailure(err) {
6598
+ this.reportError('Error acquiring storage lock', err);
6599
+ if (cb) {
6600
+ cb(false);
6601
+ }
6602
+ }, this), this.pid);
6603
+ }
6604
+ };
6605
+
6606
+ /**
6607
+ * Read out the given number of queue entries. If this.memQueue
6608
+ * has fewer than batchSize items, then look for "orphaned" items
6609
+ * in the persisted queue (items where the 'flushAfter' time has
6610
+ * already passed).
6611
+ */
6612
+ RequestQueue.prototype.fillBatch = function(batchSize) {
6613
+ var batch = this.memQueue.slice(0, batchSize);
6614
+ if (this.usePersistence && batch.length < batchSize) {
6615
+ // don't need lock just to read events; localStorage is thread-safe
6616
+ // and the worst that could happen is a duplicate send of some
6617
+ // orphaned events, which will be deduplicated on the server side
6618
+ var storedQueue = this.readFromStorage();
6619
+ if (storedQueue.length) {
6620
+ // item IDs already in batch; don't duplicate out of storage
6621
+ var idsInBatch = {}; // poor man's Set
6622
+ _.each(batch, function(item) { idsInBatch[item['id']] = true; });
6623
+
6624
+ for (var i = 0; i < storedQueue.length; i++) {
6625
+ var item = storedQueue[i];
6626
+ if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
6627
+ item.orphaned = true;
6628
+ batch.push(item);
6629
+ if (batch.length >= batchSize) {
6630
+ break;
6631
+ }
6632
+ }
6633
+ }
6634
+ }
6635
+ }
6636
+ return batch;
6637
+ };
6638
+
6639
+ /**
6640
+ * Remove items with matching 'id' from array (immutably)
6641
+ * also remove any item without a valid id (e.g., malformed
6642
+ * storage entries).
6643
+ */
6644
+ var filterOutIDsAndInvalid = function(items, idSet) {
6645
+ var filteredItems = [];
6646
+ _.each(items, function(item) {
6647
+ if (item['id'] && !idSet[item['id']]) {
6648
+ filteredItems.push(item);
6649
+ }
6650
+ });
6651
+ return filteredItems;
6652
+ };
6653
+
6654
+ /**
6655
+ * Remove items with matching IDs from both in-memory queue
6656
+ * and persisted queue
6657
+ */
6658
+ RequestQueue.prototype.removeItemsByID = function(ids, cb) {
6659
+ var idSet = {}; // poor man's Set
6660
+ _.each(ids, function(id) { idSet[id] = true; });
6661
+
6662
+ this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
6663
+ if (!this.usePersistence) {
6664
+ if (cb) {
6665
+ cb(true);
6666
+ }
6667
+ } else {
6668
+ var removeFromStorage = _.bind(function() {
6669
+ var succeeded;
6670
+ try {
6671
+ var storedQueue = this.readFromStorage();
6672
+ storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
6673
+ succeeded = this.saveToStorage(storedQueue);
6674
+
6675
+ // an extra check: did storage report success but somehow
6676
+ // the items are still there?
6677
+ if (succeeded) {
6678
+ storedQueue = this.readFromStorage();
6679
+ for (var i = 0; i < storedQueue.length; i++) {
6680
+ var item = storedQueue[i];
6681
+ if (item['id'] && !!idSet[item['id']]) {
6682
+ this.reportError('Item not removed from storage');
6683
+ return false;
6684
+ }
6685
+ }
6686
+ }
6687
+ } catch(err) {
6688
+ this.reportError('Error removing items', ids);
6689
+ succeeded = false;
6690
+ }
6691
+ return succeeded;
6692
+ }, this);
6693
+
6694
+ this.lock.withLock(function lockAcquired() {
6695
+ var succeeded = removeFromStorage();
6696
+ if (cb) {
6697
+ cb(succeeded);
6698
+ }
6699
+ }, _.bind(function lockFailure(err) {
6700
+ var succeeded = false;
6701
+ this.reportError('Error acquiring storage lock', err);
6702
+ if (!localStorageSupported(this.storage, true)) {
6703
+ // Looks like localStorage writes have stopped working sometime after
6704
+ // initialization (probably full), and so nobody can acquire locks
6705
+ // anymore. Consider it temporarily safe to remove items without the
6706
+ // lock, since nobody's writing successfully anyway.
6707
+ succeeded = removeFromStorage();
6708
+ if (!succeeded) {
6709
+ // OK, we couldn't even write out the smaller queue. Try clearing it
6710
+ // entirely.
6711
+ try {
6712
+ this.storage.removeItem(this.storageKey);
6713
+ } catch(err) {
6714
+ this.reportError('Error clearing queue', err);
6715
+ }
6716
+ }
6717
+ }
6718
+ if (cb) {
6719
+ cb(succeeded);
6720
+ }
6721
+ }, this), this.pid);
6722
+ }
6723
+
6724
+ };
6725
+
6726
+ // internal helper for RequestQueue.updatePayloads
6727
+ var updatePayloads = function(existingItems, itemsToUpdate) {
6728
+ var newItems = [];
6729
+ _.each(existingItems, function(item) {
6730
+ var id = item['id'];
6731
+ if (id in itemsToUpdate) {
6732
+ var newPayload = itemsToUpdate[id];
6733
+ if (newPayload !== null) {
6734
+ item['payload'] = newPayload;
6735
+ newItems.push(item);
6736
+ }
6737
+ } else {
6738
+ // no update
6739
+ newItems.push(item);
6740
+ }
6741
+ });
6742
+ return newItems;
6743
+ };
6744
+
6745
+ /**
6746
+ * Update payloads of given items in both in-memory queue and
6747
+ * persisted queue. Items set to null are removed from queues.
6748
+ */
6749
+ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
6750
+ this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
6751
+ if (!this.usePersistence) {
6752
+ if (cb) {
6753
+ cb(true);
6754
+ }
6755
+ } else {
6756
+ this.lock.withLock(_.bind(function lockAcquired() {
6757
+ var succeeded;
6758
+ try {
6759
+ var storedQueue = this.readFromStorage();
6760
+ storedQueue = updatePayloads(storedQueue, itemsToUpdate);
6761
+ succeeded = this.saveToStorage(storedQueue);
6762
+ } catch(err) {
6763
+ this.reportError('Error updating items', itemsToUpdate);
6764
+ succeeded = false;
6765
+ }
6766
+ if (cb) {
6767
+ cb(succeeded);
6768
+ }
6769
+ }, this), _.bind(function lockFailure(err) {
6770
+ this.reportError('Error acquiring storage lock', err);
6771
+ if (cb) {
6772
+ cb(false);
6773
+ }
6774
+ }, this), this.pid);
6775
+ }
6776
+
6777
+ };
6778
+
6779
+ /**
6780
+ * Read and parse items array from localStorage entry, handling
6781
+ * malformed/missing data if necessary.
6782
+ */
6783
+ RequestQueue.prototype.readFromStorage = function() {
6784
+ var storageEntry;
6785
+ try {
6786
+ storageEntry = this.storage.getItem(this.storageKey);
6787
+ if (storageEntry) {
6788
+ storageEntry = JSONParse(storageEntry);
6789
+ if (!_.isArray(storageEntry)) {
6790
+ this.reportError('Invalid storage entry:', storageEntry);
6791
+ storageEntry = null;
6792
+ }
6793
+ }
6794
+ } catch (err) {
6795
+ this.reportError('Error retrieving queue', err);
6796
+ storageEntry = null;
6797
+ }
6798
+ return storageEntry || [];
6799
+ };
6800
+
6801
+ /**
6802
+ * Serialize the given items array to localStorage.
6803
+ */
6804
+ RequestQueue.prototype.saveToStorage = function(queue) {
6805
+ try {
6806
+ this.storage.setItem(this.storageKey, JSONStringify(queue));
6807
+ return true;
6808
+ } catch (err) {
6809
+ this.reportError('Error saving queue', err);
6810
+ return false;
6811
+ }
6812
+ };
6813
+
6814
+ /**
6815
+ * Clear out queues (memory and localStorage).
6816
+ */
6817
+ RequestQueue.prototype.clear = function() {
6818
+ this.memQueue = [];
6819
+
6820
+ if (this.usePersistence) {
6821
+ this.storage.removeItem(this.storageKey);
6822
+ }
6823
+ };
6824
+
6825
+ // maximum interval between request retries after exponential backoff
6826
+ var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
6827
+
6828
+ var logger$1 = console_with_prefix('batch');
6829
+
6830
+ /**
6831
+ * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
6832
+ * type (events, people, groups).
6833
+ * Uses RequestQueue to manage the backing store.
6834
+ * @constructor
6835
+ */
6836
+ var RequestBatcher = function(storageKey, options) {
6837
+ this.errorReporter = options.errorReporter;
6838
+ this.queue = new RequestQueue(storageKey, {
6839
+ errorReporter: _.bind(this.reportError, this),
6840
+ storage: options.storage,
6841
+ usePersistence: options.usePersistence
6842
+ });
6843
+
6844
+ this.libConfig = options.libConfig;
6845
+ this.sendRequest = options.sendRequestFunc;
6846
+ this.beforeSendHook = options.beforeSendHook;
6847
+ this.stopAllBatching = options.stopAllBatchingFunc;
6848
+
6849
+ // seed variable batch size + flush interval with configured values
6850
+ this.batchSize = this.libConfig['batch_size'];
6851
+ this.flushInterval = this.libConfig['batch_flush_interval_ms'];
6852
+
6853
+ this.stopped = !this.libConfig['batch_autostart'];
6854
+ this.consecutiveRemovalFailures = 0;
6855
+
6856
+ // extra client-side dedupe
6857
+ this.itemIdsSentSuccessfully = {};
6858
+
6859
+ // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes
6860
+ // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
6861
+ // in a request loop and get ratelimited by the server.
6862
+ this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
6863
+ };
6864
+
6865
+ /**
6866
+ * Add one item to queue.
6867
+ */
6868
+ RequestBatcher.prototype.enqueue = function(item, cb) {
6869
+ this.queue.enqueue(item, this.flushInterval, cb);
6870
+ };
6871
+
6872
+ /**
6873
+ * Start flushing batches at the configured time interval. Must call
6874
+ * this method upon SDK init in order to send anything over the network.
6875
+ */
6876
+ RequestBatcher.prototype.start = function() {
6877
+ this.stopped = false;
6878
+ this.consecutiveRemovalFailures = 0;
6879
+ this.flush();
6880
+ };
6881
+
6882
+ /**
6883
+ * Stop flushing batches. Can be restarted by calling start().
6884
+ */
6885
+ RequestBatcher.prototype.stop = function() {
6886
+ this.stopped = true;
6887
+ if (this.timeoutID) {
6888
+ clearTimeout(this.timeoutID);
6889
+ this.timeoutID = null;
6890
+ }
6891
+ };
6892
+
6893
+ /**
6894
+ * Clear out queue.
6895
+ */
6896
+ RequestBatcher.prototype.clear = function() {
6897
+ this.queue.clear();
6898
+ };
6899
+
6900
+ /**
6901
+ * Restore batch size configuration to whatever is set in the main SDK.
6902
+ */
6903
+ RequestBatcher.prototype.resetBatchSize = function() {
6904
+ this.batchSize = this.libConfig['batch_size'];
6905
+ };
6906
+
6907
+ /**
6908
+ * Restore flush interval time configuration to whatever is set in the main SDK.
6909
+ */
6910
+ RequestBatcher.prototype.resetFlush = function() {
6911
+ this.scheduleFlush(this.libConfig['batch_flush_interval_ms']);
6912
+ };
6913
+
6914
+ /**
6915
+ * Schedule the next flush in the given number of milliseconds.
6916
+ */
6917
+ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
6918
+ this.flushInterval = flushMS;
6919
+ if (!this.stopped) { // don't schedule anymore if batching has been stopped
6920
+ this.timeoutID = setTimeout(_.bind(function() {
6921
+ if (!this.stopped) {
6922
+ this.flush();
6923
+ }
6924
+ }, this), this.flushInterval);
6925
+ }
6926
+ };
6927
+
6928
+ /**
6929
+ * Flush one batch to network. Depending on success/failure modes, it will either
6930
+ * remove the batch from the queue or leave it in for retry, and schedule the next
6931
+ * flush. In cases of most network or API failures, it will back off exponentially
6932
+ * when retrying.
6933
+ * @param {Object} [options]
6934
+ * @param {boolean} [options.sendBeacon] - whether to send batch with
6935
+ * navigator.sendBeacon (only useful for sending batches before page unloads, as
6936
+ * sendBeacon offers no callbacks or status indications)
6937
+ */
6938
+ RequestBatcher.prototype.flush = function(options) {
6939
+ try {
6940
+
6941
+ if (this.requestInProgress) {
6942
+ logger$1.log('Flush: Request already in progress');
6943
+ return;
6944
+ }
6945
+
6946
+ options = options || {};
6947
+ var timeoutMS = this.libConfig['batch_request_timeout_ms'];
6948
+ var startTime = new Date().getTime();
6949
+ var currentBatchSize = this.batchSize;
6950
+ var batch = this.queue.fillBatch(currentBatchSize);
6951
+ // if there's more items in the queue than the batch size, attempt
6952
+ // to flush again after the current batch is done.
6953
+ var attemptSecondaryFlush = batch.length === currentBatchSize;
6954
+ var dataForRequest = [];
6955
+ var transformedItems = {};
6956
+ _.each(batch, function(item) {
6957
+ var payload = item['payload'];
6958
+ if (this.beforeSendHook && !item.orphaned) {
6959
+ payload = this.beforeSendHook(payload);
6960
+ }
6961
+ if (payload) {
6962
+ // mp_sent_by_lib_version prop captures which lib version actually
6963
+ // sends each event (regardless of which version originally queued
6964
+ // it for sending)
6965
+ if (payload['event'] && payload['properties']) {
6966
+ payload['properties'] = _.extend(
6967
+ {},
6968
+ payload['properties'],
6969
+ {'mp_sent_by_lib_version': Config.LIB_VERSION}
6970
+ );
6971
+ }
6972
+ var addPayload = true;
6973
+ var itemId = item['id'];
6974
+ if (itemId) {
6975
+ if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
6976
+ this.reportError('[dupe] item ID sent too many times, not sending', {
6977
+ item: item,
6978
+ batchSize: batch.length,
6979
+ timesSent: this.itemIdsSentSuccessfully[itemId]
6980
+ });
6981
+ addPayload = false;
6982
+ }
6983
+ } else {
6984
+ this.reportError('[dupe] found item with no ID', {item: item});
6985
+ }
6986
+
6987
+ if (addPayload) {
6988
+ dataForRequest.push(payload);
6989
+ }
6990
+ }
6991
+ transformedItems[item['id']] = payload;
6992
+ }, this);
6993
+ if (dataForRequest.length < 1) {
6994
+ this.resetFlush();
6995
+ return; // nothing to do
6996
+ }
6997
+
6998
+ this.requestInProgress = true;
6999
+
7000
+ var batchSendCallback = _.bind(function(res) {
7001
+ this.requestInProgress = false;
7002
+
7003
+ try {
7004
+
7005
+ // handle API response in a try-catch to make sure we can reset the
7006
+ // flush operation if something goes wrong
7007
+
7008
+ var removeItemsFromQueue = false;
7009
+ if (options.unloading) {
7010
+ // update persisted data to include hook transformations
7011
+ this.queue.updatePayloads(transformedItems);
7012
+ } else if (
7013
+ _.isObject(res) &&
7014
+ res.error === 'timeout' &&
7015
+ new Date().getTime() - startTime >= timeoutMS
7016
+ ) {
7017
+ this.reportError('Network timeout; retrying');
7018
+ this.flush();
7019
+ } else if (
7020
+ _.isObject(res) &&
7021
+ (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
7022
+ ) {
7023
+ // network or API error, or 429 Too Many Requests, retry
7024
+ var retryMS = this.flushInterval * 2;
7025
+ if (res.retryAfter) {
7026
+ retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
7027
+ }
7028
+ retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
7029
+ this.reportError('Error; retry in ' + retryMS + ' ms');
7030
+ this.scheduleFlush(retryMS);
7031
+ } else if (_.isObject(res) && res.httpStatusCode === 413) {
7032
+ // 413 Payload Too Large
7033
+ if (batch.length > 1) {
7034
+ var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
7035
+ this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
7036
+ this.reportError('413 response; reducing batch size to ' + this.batchSize);
7037
+ this.resetFlush();
7038
+ } else {
7039
+ this.reportError('Single-event request too large; dropping', batch);
7040
+ this.resetBatchSize();
7041
+ removeItemsFromQueue = true;
7042
+ }
7043
+ } else {
7044
+ // successful network request+response; remove each item in batch from queue
7045
+ // (even if it was e.g. a 400, in which case retrying won't help)
7046
+ removeItemsFromQueue = true;
7047
+ }
7048
+
7049
+ if (removeItemsFromQueue) {
7050
+ this.queue.removeItemsByID(
7051
+ _.map(batch, function(item) { return item['id']; }),
7052
+ _.bind(function(succeeded) {
7053
+ if (succeeded) {
7054
+ this.consecutiveRemovalFailures = 0;
7055
+ if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
7056
+ this.resetFlush(); // schedule next batch with a delay
7057
+ } else {
7058
+ this.flush(); // handle next batch if the queue isn't empty
7059
+ }
7060
+ } else {
7061
+ this.reportError('Failed to remove items from queue');
7062
+ if (++this.consecutiveRemovalFailures > 5) {
7063
+ this.reportError('Too many queue failures; disabling batching system.');
7064
+ this.stopAllBatching();
7065
+ } else {
7066
+ this.resetFlush();
7067
+ }
7068
+ }
7069
+ }, this)
7070
+ );
7071
+
7072
+ // client-side dedupe
7073
+ _.each(batch, _.bind(function(item) {
7074
+ var itemId = item['id'];
7075
+ if (itemId) {
7076
+ this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
7077
+ this.itemIdsSentSuccessfully[itemId]++;
7078
+ if (this.itemIdsSentSuccessfully[itemId] > 5) {
7079
+ this.reportError('[dupe] item ID sent too many times', {
7080
+ item: item,
7081
+ batchSize: batch.length,
7082
+ timesSent: this.itemIdsSentSuccessfully[itemId]
7083
+ });
7084
+ }
7085
+ } else {
7086
+ this.reportError('[dupe] found item with no ID while removing', {item: item});
7087
+ }
7088
+ }, this));
7089
+ }
7090
+
7091
+ } catch(err) {
7092
+ this.reportError('Error handling API response', err);
7093
+ this.resetFlush();
7094
+ }
7095
+ }, this);
7096
+ var requestOptions = {
7097
+ method: 'POST',
7098
+ verbose: true,
7099
+ ignore_json_errors: true, // eslint-disable-line camelcase
7100
+ timeout_ms: timeoutMS // eslint-disable-line camelcase
7101
+ };
7102
+ if (options.unloading) {
7103
+ requestOptions.transport = 'sendBeacon';
7104
+ }
7105
+ logger$1.log('MIXPANEL REQUEST:', dataForRequest);
7106
+ this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
7107
+ } catch(err) {
7108
+ this.reportError('Error flushing request queue', err);
7109
+ this.resetFlush();
7110
+ }
7111
+ };
7112
+
7113
+ /**
7114
+ * Log error to global logger and optional user-defined logger.
7115
+ */
7116
+ RequestBatcher.prototype.reportError = function(msg, err) {
7117
+ logger$1.error.apply(logger$1.error, arguments);
7118
+ if (this.errorReporter) {
7119
+ try {
7120
+ if (!(err instanceof Error)) {
7121
+ err = new Error(msg);
7122
+ }
7123
+ this.errorReporter(msg, err);
7124
+ } catch(err) {
7125
+ logger$1.error(err);
7126
+ }
7127
+ }
7128
+ };
7129
+
6347
7130
  var logger = console_with_prefix('recorder');
6348
- var CompressionStream = window['CompressionStream'];
7131
+ var CompressionStream = win['CompressionStream'];
7132
+
7133
+ var RECORDER_BATCHER_LIB_CONFIG = {
7134
+ 'batch_size': 1000,
7135
+ 'batch_flush_interval_ms': 10 * 1000,
7136
+ 'batch_request_timeout_ms': 90 * 1000,
7137
+ 'batch_autostart': true
7138
+ };
7139
+
7140
+ var ACTIVE_SOURCES = new Set([
7141
+ IncrementalSource.MouseMove,
7142
+ IncrementalSource.MouseInteraction,
7143
+ IncrementalSource.Scroll,
7144
+ IncrementalSource.ViewportResize,
7145
+ IncrementalSource.Input,
7146
+ IncrementalSource.TouchMove,
7147
+ IncrementalSource.MediaInteraction,
7148
+ IncrementalSource.Drag,
7149
+ IncrementalSource.Selection,
7150
+ ]);
7151
+
7152
+ function isUserEvent(ev) {
7153
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
7154
+ }
6349
7155
 
6350
7156
  var MixpanelRecorder = function(mixpanelInstance) {
6351
7157
  this._mixpanel = mixpanelInstance;
@@ -6357,14 +7163,24 @@
6357
7163
  this.seqNo = 0;
6358
7164
  this.replayId = null;
6359
7165
  this.replayStartTime = null;
6360
- this.batchStartTime = null;
6361
- this.replayLengthMs = 0;
6362
7166
  this.sendBatchId = null;
6363
7167
 
6364
7168
  this.idleTimeoutId = null;
6365
7169
  this.maxTimeoutId = null;
6366
7170
 
6367
7171
  this.recordMaxMs = MAX_RECORDING_MS;
7172
+ this._initBatcher();
7173
+ };
7174
+
7175
+
7176
+ MixpanelRecorder.prototype._initBatcher = function () {
7177
+ this.batcher = new RequestBatcher('__mprec', {
7178
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
7179
+ sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
7180
+ errorReporter: _.bind(this.reportError, this),
7181
+ flushOnlyOnInterval: true,
7182
+ usePersistence: false
7183
+ });
6368
7184
  };
6369
7185
 
6370
7186
  // eslint-disable-next-line camelcase
@@ -6372,7 +7188,7 @@
6372
7188
  return this._mixpanel.get_config(configVar);
6373
7189
  };
6374
7190
 
6375
- MixpanelRecorder.prototype.startRecording = function () {
7191
+ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
6376
7192
  if (this._stopRecording !== null) {
6377
7193
  logger.log('Recording already in progress, skipping startRecording.');
6378
7194
  return;
@@ -6386,12 +7202,18 @@
6386
7202
 
6387
7203
  this.recEvents = [];
6388
7204
  this.seqNo = 0;
6389
- this.startDate = new Date();
6390
- this.replayStartTime = this.startDate.getTime();
6391
- this.batchStartTime = this.replayStartTime;
7205
+ this.replayStartTime = null;
6392
7206
 
6393
7207
  this.replayId = _.UUID();
6394
- this.replayLengthMs = 0;
7208
+
7209
+ if (shouldStopBatcher) {
7210
+ // this is the case when we're starting recording after a reset
7211
+ // and don't want to send anything over the network until there's
7212
+ // actual user activity
7213
+ this.batcher.stop();
7214
+ } else {
7215
+ this.batcher.start();
7216
+ }
6395
7217
 
6396
7218
  var resetIdleTimeout = _.bind(function () {
6397
7219
  clearTimeout(this.idleTimeoutId);
@@ -6403,26 +7225,32 @@
6403
7225
 
6404
7226
  this._stopRecording = record({
6405
7227
  'emit': _.bind(function (ev) {
6406
- this.recEvents.push(ev);
6407
- this.replayLengthMs = new Date().getTime() - this.replayStartTime;
6408
- resetIdleTimeout();
7228
+ this.batcher.enqueue(ev);
7229
+ if (isUserEvent(ev)) {
7230
+ if (this.batcher.stopped) {
7231
+ // start flushing again after user activity
7232
+ this.batcher.start();
7233
+ }
7234
+ resetIdleTimeout();
7235
+ }
6409
7236
  }, this),
6410
- 'maskAllInputs': true,
6411
- 'maskTextSelector': this.get_config('record_mask_text_selector'),
7237
+ 'blockClass': this.get_config('record_block_class'),
6412
7238
  'blockSelector': this.get_config('record_block_selector'),
7239
+ 'collectFonts': this.get_config('record_collect_fonts'),
7240
+ 'inlineImages': this.get_config('record_inline_images'),
7241
+ 'maskAllInputs': true,
6413
7242
  'maskTextClass': this.get_config('record_mask_text_class'),
6414
- 'blockClass': this.get_config('record_block_class'),
7243
+ 'maskTextSelector': this.get_config('record_mask_text_selector')
6415
7244
  });
6416
7245
 
6417
7246
  resetIdleTimeout();
6418
7247
 
6419
- this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000);
6420
7248
  this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
6421
7249
  };
6422
7250
 
6423
7251
  MixpanelRecorder.prototype.resetRecording = function () {
6424
7252
  this.stopRecording();
6425
- this.startRecording();
7253
+ this.startRecording(true);
6426
7254
  };
6427
7255
 
6428
7256
  MixpanelRecorder.prototype.stopRecording = function () {
@@ -6431,10 +7259,16 @@
6431
7259
  this._stopRecording = null;
6432
7260
  }
6433
7261
 
6434
- this._flushEvents(); // flush any remaining events
7262
+ if (this.batcher.stopped) {
7263
+ // never got user activity to flush after reset, so just clear the batcher
7264
+ this.batcher.clear();
7265
+ } else {
7266
+ // flush any remaining events from running batcher
7267
+ this.batcher.flush();
7268
+ this.batcher.stop();
7269
+ }
6435
7270
  this.replayId = null;
6436
7271
 
6437
- clearInterval(this.sendBatchId);
6438
7272
  clearTimeout(this.idleTimeoutId);
6439
7273
  clearTimeout(this.maxTimeoutId);
6440
7274
  };
@@ -6443,8 +7277,8 @@
6443
7277
  * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
6444
7278
  * we stop recording and dump any queued events if the user has opted out.
6445
7279
  */
6446
- MixpanelRecorder.prototype.flushEventsWithOptOut = function () {
6447
- this._flushEvents(_.bind(this._onOptOut, this));
7280
+ MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
7281
+ this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
6448
7282
  };
6449
7283
 
6450
7284
  MixpanelRecorder.prototype._onOptOut = function (code) {
@@ -6455,33 +7289,60 @@
6455
7289
  }
6456
7290
  };
6457
7291
 
6458
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) {
6459
- window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
7292
+ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
7293
+ var onSuccess = _.bind(function (response, responseBody) {
7294
+ // Increment sequence counter only if the request was successful to guarantee ordering.
7295
+ // RequestBatcher will always flush the next batch after the previous one succeeds.
7296
+ if (response.status === 200) {
7297
+ this.seqNo++;
7298
+ }
7299
+
7300
+ callback({
7301
+ status: 0,
7302
+ httpStatusCode: response.status,
7303
+ responseBody: responseBody,
7304
+ retryAfter: response.headers.get('Retry-After')
7305
+ });
7306
+ }, this);
7307
+
7308
+ win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
6460
7309
  'method': 'POST',
6461
7310
  'headers': {
6462
7311
  'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
6463
7312
  'Content-Type': 'application/octet-stream'
6464
7313
  },
6465
- 'body': reqBody
7314
+ 'body': reqBody,
7315
+ }).then(function (response) {
7316
+ response.json().then(function (responseBody) {
7317
+ onSuccess(response, responseBody);
7318
+ }).catch(function (error) {
7319
+ callback({error: error});
7320
+ });
7321
+ }).catch(function (error) {
7322
+ callback({error: error});
6466
7323
  });
6467
7324
  };
6468
7325
 
6469
- /**
6470
- * @api private
6471
- * Private method, flushes the current batch of events to the server.
6472
- */
6473
- MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
6474
- var numEvents = this.recEvents.length;
7326
+ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
7327
+ const numEvents = data.length;
7328
+
6475
7329
  if (numEvents > 0) {
7330
+ // each rrweb event has a timestamp - leverage those to get time properties
7331
+ var batchStartTime = data[0].timestamp;
7332
+ if (this.seqNo === 0) {
7333
+ this.replayStartTime = batchStartTime;
7334
+ }
7335
+ var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
7336
+
6476
7337
  var reqParams = {
6477
7338
  'distinct_id': String(this._mixpanel.get_distinct_id()),
6478
- 'seq': this.seqNo++,
6479
- 'batch_start_time': this.batchStartTime / 1000,
7339
+ 'seq': this.seqNo,
7340
+ 'batch_start_time': batchStartTime / 1000,
6480
7341
  'replay_id': this.replayId,
6481
- 'replay_length_ms': this.replayLengthMs,
7342
+ 'replay_length_ms': replayLengthMs,
6482
7343
  'replay_start_time': this.replayStartTime / 1000
6483
7344
  };
6484
- var eventsJson = _.JSONEncode(this.recEvents);
7345
+ var eventsJson = _.JSONEncode(data);
6485
7346
 
6486
7347
  // send ID management props if they exist
6487
7348
  var deviceId = this._mixpanel.get_property('$device_id');
@@ -6493,8 +7354,6 @@
6493
7354
  reqParams['$user_id'] = userId;
6494
7355
  }
6495
7356
 
6496
- this.recEvents = this.recEvents.slice(numEvents);
6497
- this.batchStartTime = new Date().getTime();
6498
7357
  if (CompressionStream) {
6499
7358
  var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
6500
7359
  var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
@@ -6502,15 +7361,29 @@
6502
7361
  .blob()
6503
7362
  .then(_.bind(function(compressedBlob) {
6504
7363
  reqParams['format'] = 'gzip';
6505
- this._sendRequest(reqParams, compressedBlob);
7364
+ this._sendRequest(reqParams, compressedBlob, callback);
6506
7365
  }, this));
6507
7366
  } else {
6508
7367
  reqParams['format'] = 'body';
6509
- this._sendRequest(reqParams, eventsJson);
7368
+ this._sendRequest(reqParams, eventsJson, callback);
6510
7369
  }
6511
7370
  }
6512
7371
  });
6513
7372
 
6514
- window['__mp_recorder'] = MixpanelRecorder;
7373
+
7374
+ MixpanelRecorder.prototype.reportError = function(msg, err) {
7375
+ logger.error.apply(logger.error, arguments);
7376
+ try {
7377
+ if (!err && !(msg instanceof Error)) {
7378
+ msg = new Error(msg);
7379
+ }
7380
+ this.get_config('error_reporter')(msg, err);
7381
+ } catch(err) {
7382
+ logger.error(err);
7383
+ }
7384
+ };
7385
+
7386
+
7387
+ win['__mp_recorder'] = MixpanelRecorder;
6515
7388
 
6516
7389
  })();