mixpanel-browser 2.76.0 → 2.78.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.
Files changed (52) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.github/dependabot.yml +8 -0
  3. package/.github/workflows/integration-tests.yml +2 -2
  4. package/.github/workflows/unit-tests.yml +2 -2
  5. package/CHANGELOG.md +8 -0
  6. package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js +2 -0
  7. package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js.map +1 -0
  8. package/dist/async-modules/{mixpanel-recorder-bIS4LMGd.js → mixpanel-recorder-zMBXIyeG.js} +84 -10
  9. package/dist/async-modules/{mixpanel-targeting-VOeN7RWY.min.js → mixpanel-targeting-BSHal4N9.min.js} +2 -2
  10. package/dist/async-modules/{mixpanel-targeting-VOeN7RWY.min.js.map → mixpanel-targeting-BSHal4N9.min.js.map} +1 -1
  11. package/dist/async-modules/{mixpanel-targeting-BcAPS-Mz.js → mixpanel-targeting-UHf4eBfC.js} +1 -1
  12. package/dist/mixpanel-core.cjs.d.ts +3 -1
  13. package/dist/mixpanel-core.cjs.js +292 -130
  14. package/dist/mixpanel-recorder.js +84 -10
  15. package/dist/mixpanel-recorder.min.js +1 -1
  16. package/dist/mixpanel-recorder.min.js.map +1 -1
  17. package/dist/mixpanel-targeting.js +1 -1
  18. package/dist/mixpanel-targeting.min.js +1 -1
  19. package/dist/mixpanel-targeting.min.js.map +1 -1
  20. package/dist/mixpanel-with-async-modules.cjs.d.ts +3 -1
  21. package/dist/mixpanel-with-async-modules.cjs.js +294 -132
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +3 -1
  23. package/dist/mixpanel-with-async-recorder.cjs.js +294 -132
  24. package/dist/mixpanel-with-recorder.d.ts +3 -1
  25. package/dist/mixpanel-with-recorder.js +381 -168
  26. package/dist/mixpanel-with-recorder.min.d.ts +3 -1
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +3 -1
  29. package/dist/mixpanel.amd.js +381 -168
  30. package/dist/mixpanel.cjs.d.ts +3 -1
  31. package/dist/mixpanel.cjs.js +381 -168
  32. package/dist/mixpanel.globals.js +294 -132
  33. package/dist/mixpanel.min.js +191 -186
  34. package/dist/mixpanel.module.d.ts +3 -1
  35. package/dist/mixpanel.module.js +381 -168
  36. package/dist/mixpanel.umd.d.ts +3 -1
  37. package/dist/mixpanel.umd.js +381 -168
  38. package/dist/rrweb-bundled.js +61 -9
  39. package/dist/rrweb-compiled.js +56 -9
  40. package/package.json +6 -5
  41. package/src/config.js +1 -1
  42. package/src/flags/CLAUDE.md +24 -0
  43. package/src/flags/index.js +109 -80
  44. package/src/index.d.ts +3 -1
  45. package/src/mixpanel-core.js +4 -2
  46. package/src/recorder/session-recording.js +5 -1
  47. package/src/recorder/utils.js +27 -1
  48. package/src/recorder-manager.js +110 -2
  49. package/testServer.js +16 -1
  50. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +0 -2
  51. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +0 -1
  52. /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
@@ -9304,14 +9304,7 @@ class MutationBuffer {
9304
9304
  };
9305
9305
  while (this.mapRemoves.length) {
9306
9306
  const removedNode = this.mapRemoves.shift();
9307
- if (removedNode.nodeName === "IFRAME") {
9308
- try {
9309
- this.iframeManager.removeIframe(removedNode);
9310
- } catch (e2) {
9311
- }
9312
- } else {
9313
- this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
9314
- }
9307
+ this.cleanupRemovedNode(removedNode);
9315
9308
  this.mirror.removeNodeFromMap(removedNode);
9316
9309
  }
9317
9310
  for (const n2 of this.movedSet) {
@@ -9620,6 +9613,22 @@ class MutationBuffer {
9620
9613
  }
9621
9614
  }
9622
9615
  });
9616
+ __publicField$1(this, "cleanupRemovedNode", (node2) => {
9617
+ if (node2.nodeName === "IFRAME") {
9618
+ try {
9619
+ this.iframeManager.removeIframe(node2);
9620
+ } catch (e2) {
9621
+ }
9622
+ } else {
9623
+ try {
9624
+ this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
9625
+ } catch (e2) {
9626
+ }
9627
+ }
9628
+ node2.childNodes.forEach((child) => {
9629
+ this.cleanupRemovedNode(child);
9630
+ });
9631
+ });
9623
9632
  }
9624
9633
  init(options) {
9625
9634
  [
@@ -11844,6 +11853,35 @@ class ProcessedNodeManager {
11844
11853
  destroy() {
11845
11854
  }
11846
11855
  }
11856
+ function toOrigin(url) {
11857
+ try {
11858
+ const origin = new URL(url).origin;
11859
+ return origin !== "null" ? origin : null;
11860
+ } catch {
11861
+ return null;
11862
+ }
11863
+ }
11864
+ function buildAllowedOriginSet(origins) {
11865
+ if (!Array.isArray(origins) || origins.length === 0) {
11866
+ throw new Error(
11867
+ "[rrweb] allowedIframeOrigins must be a non-empty array of origin strings."
11868
+ );
11869
+ }
11870
+ const set = /* @__PURE__ */ new Set();
11871
+ for (let i2 = 0; i2 < origins.length; i2++) {
11872
+ const entry = origins[i2];
11873
+ if (typeof entry !== "string") {
11874
+ throw new Error(
11875
+ `[rrweb] allowedIframeOrigins[${i2}] must be a string, got ${typeof entry}.`
11876
+ );
11877
+ }
11878
+ const origin = toOrigin(entry);
11879
+ if (origin) {
11880
+ set.add(origin);
11881
+ }
11882
+ }
11883
+ return Object.freeze(set);
11884
+ }
11847
11885
  let wrappedEmit;
11848
11886
  let takeFullSnapshot$1;
11849
11887
  let canvasManager;
@@ -11884,6 +11922,7 @@ function record(options = {}) {
11884
11922
  recordDOM = true,
11885
11923
  recordCanvas = false,
11886
11924
  recordCrossOriginIframes = false,
11925
+ allowedIframeOrigins,
11887
11926
  recordAfter = options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load",
11888
11927
  userTriggeredOnInput = false,
11889
11928
  collectFonts = false,
@@ -11894,6 +11933,13 @@ function record(options = {}) {
11894
11933
  errorHandler: errorHandler2
11895
11934
  } = options;
11896
11935
  registerErrorHandler(errorHandler2);
11936
+ let validatedOrigins;
11937
+ if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
11938
+ validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
11939
+ if (validatedOrigins.size === 0) {
11940
+ validatedOrigins = void 0;
11941
+ }
11942
+ }
11897
11943
  const inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
11898
11944
  let passEmitsToParent = false;
11899
11945
  if (!inEmittingFrame) {
@@ -11981,7 +12027,13 @@ function record(options = {}) {
11981
12027
  origin: window.location.origin,
11982
12028
  isCheckout
11983
12029
  };
11984
- window.parent.postMessage(message, "*");
12030
+ if (validatedOrigins) {
12031
+ for (const targetOrigin of validatedOrigins) {
12032
+ window.parent.postMessage(message, targetOrigin);
12033
+ }
12034
+ } else {
12035
+ window.parent.postMessage(message, "*");
12036
+ }
11985
12037
  }
11986
12038
  if (e2.type === EventType.FullSnapshot) {
11987
12039
  lastFullSnapshotEvent = e2;
@@ -10695,13 +10695,7 @@ var MutationBuffer = /*#__PURE__*/ function() {
10695
10695
  };
10696
10696
  while(_this.mapRemoves.length){
10697
10697
  var removedNode = _this.mapRemoves.shift();
10698
- if (removedNode.nodeName === "IFRAME") {
10699
- try {
10700
- _this.iframeManager.removeIframe(removedNode);
10701
- } catch (e2) {}
10702
- } else {
10703
- _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10704
- }
10698
+ _this.cleanupRemovedNode(removedNode);
10705
10699
  _this.mirror.removeNodeFromMap(removedNode);
10706
10700
  }
10707
10701
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
@@ -11021,6 +11015,20 @@ var MutationBuffer = /*#__PURE__*/ function() {
11021
11015
  }
11022
11016
  }
11023
11017
  });
11018
+ __publicField$1(this, "cleanupRemovedNode", function(node2) {
11019
+ if (node2.nodeName === "IFRAME") {
11020
+ try {
11021
+ _this.iframeManager.removeIframe(node2);
11022
+ } catch (e2) {}
11023
+ } else {
11024
+ try {
11025
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
11026
+ } catch (e2) {}
11027
+ }
11028
+ node2.childNodes.forEach(function(child) {
11029
+ _this.cleanupRemovedNode(child);
11030
+ });
11031
+ });
11024
11032
  }
11025
11033
  var _proto = MutationBuffer.prototype;
11026
11034
  _proto.init = function init(options) {
@@ -13248,6 +13256,31 @@ var ProcessedNodeManager = /*#__PURE__*/ function() {
13248
13256
  _proto.destroy = function destroy() {};
13249
13257
  return ProcessedNodeManager;
13250
13258
  }();
13259
+ function toOrigin(url) {
13260
+ try {
13261
+ var origin = new URL(url).origin;
13262
+ return origin !== "null" ? origin : null;
13263
+ } catch (e) {
13264
+ return null;
13265
+ }
13266
+ }
13267
+ function buildAllowedOriginSet(origins) {
13268
+ if (!Array.isArray(origins) || origins.length === 0) {
13269
+ throw new Error("[rrweb] allowedIframeOrigins must be a non-empty array of origin strings.");
13270
+ }
13271
+ var set = /* @__PURE__ */ new Set();
13272
+ for(var i2 = 0; i2 < origins.length; i2++){
13273
+ var entry = origins[i2];
13274
+ if (typeof entry !== "string") {
13275
+ throw new Error("[rrweb] allowedIframeOrigins[" + i2 + "] must be a string, got " + (typeof entry === "undefined" ? "undefined" : _type_of(entry)) + ".");
13276
+ }
13277
+ var origin = toOrigin(entry);
13278
+ if (origin) {
13279
+ set.add(origin);
13280
+ }
13281
+ }
13282
+ return Object.freeze(set);
13283
+ }
13251
13284
  var wrappedEmit;
13252
13285
  var takeFullSnapshot$1;
13253
13286
  var canvasManager;
@@ -13269,10 +13302,17 @@ try {
13269
13302
  var mirror = createMirror$2();
13270
13303
  function record(options) {
13271
13304
  if (options === void 0) options = {};
13272
- var emit = options.emit, checkoutEveryNms = options.checkoutEveryNms, checkoutEveryNth = options.checkoutEveryNth, _options_blockClass = options.blockClass, blockClass = _options_blockClass === void 0 ? "rr-block" : _options_blockClass, _options_blockSelector = options.blockSelector, blockSelector = _options_blockSelector === void 0 ? null : _options_blockSelector, _options_ignoreClass = options.ignoreClass, ignoreClass = _options_ignoreClass === void 0 ? "rr-ignore" : _options_ignoreClass, _options_ignoreSelector = options.ignoreSelector, ignoreSelector = _options_ignoreSelector === void 0 ? null : _options_ignoreSelector, _options_maskTextClass = options.maskTextClass, maskTextClass = _options_maskTextClass === void 0 ? "rr-mask" : _options_maskTextClass, _options_maskTextSelector = options.maskTextSelector, maskTextSelector = _options_maskTextSelector === void 0 ? null : _options_maskTextSelector, _options_inlineStylesheet = options.inlineStylesheet, inlineStylesheet = _options_inlineStylesheet === void 0 ? true : _options_inlineStylesheet, maskAllInputs = options.maskAllInputs, _maskInputOptions = options.maskInputOptions, _slimDOMOptions = options.slimDOMOptions, maskInputFn = options.maskInputFn, maskTextFn = options.maskTextFn, hooks = options.hooks, packFn = options.packFn, _options_sampling = options.sampling, sampling = _options_sampling === void 0 ? {} : _options_sampling, _options_dataURLOptions = options.dataURLOptions, dataURLOptions = _options_dataURLOptions === void 0 ? {} : _options_dataURLOptions, mousemoveWait = options.mousemoveWait, _options_recordDOM = options.recordDOM, recordDOM = _options_recordDOM === void 0 ? true : _options_recordDOM, _options_recordCanvas = options.recordCanvas, recordCanvas = _options_recordCanvas === void 0 ? false : _options_recordCanvas, _options_recordCrossOriginIframes = options.recordCrossOriginIframes, recordCrossOriginIframes = _options_recordCrossOriginIframes === void 0 ? false : _options_recordCrossOriginIframes, _options_recordAfter = options.recordAfter, recordAfter = _options_recordAfter === void 0 ? options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load" : _options_recordAfter, _options_userTriggeredOnInput = options.userTriggeredOnInput, userTriggeredOnInput = _options_userTriggeredOnInput === void 0 ? false : _options_userTriggeredOnInput, _options_collectFonts = options.collectFonts, collectFonts = _options_collectFonts === void 0 ? false : _options_collectFonts, _options_inlineImages = options.inlineImages, inlineImages = _options_inlineImages === void 0 ? false : _options_inlineImages, plugins = options.plugins, _options_keepIframeSrcFn = options.keepIframeSrcFn, keepIframeSrcFn = _options_keepIframeSrcFn === void 0 ? function() {
13305
+ var emit = options.emit, checkoutEveryNms = options.checkoutEveryNms, checkoutEveryNth = options.checkoutEveryNth, _options_blockClass = options.blockClass, blockClass = _options_blockClass === void 0 ? "rr-block" : _options_blockClass, _options_blockSelector = options.blockSelector, blockSelector = _options_blockSelector === void 0 ? null : _options_blockSelector, _options_ignoreClass = options.ignoreClass, ignoreClass = _options_ignoreClass === void 0 ? "rr-ignore" : _options_ignoreClass, _options_ignoreSelector = options.ignoreSelector, ignoreSelector = _options_ignoreSelector === void 0 ? null : _options_ignoreSelector, _options_maskTextClass = options.maskTextClass, maskTextClass = _options_maskTextClass === void 0 ? "rr-mask" : _options_maskTextClass, _options_maskTextSelector = options.maskTextSelector, maskTextSelector = _options_maskTextSelector === void 0 ? null : _options_maskTextSelector, _options_inlineStylesheet = options.inlineStylesheet, inlineStylesheet = _options_inlineStylesheet === void 0 ? true : _options_inlineStylesheet, maskAllInputs = options.maskAllInputs, _maskInputOptions = options.maskInputOptions, _slimDOMOptions = options.slimDOMOptions, maskInputFn = options.maskInputFn, maskTextFn = options.maskTextFn, hooks = options.hooks, packFn = options.packFn, _options_sampling = options.sampling, sampling = _options_sampling === void 0 ? {} : _options_sampling, _options_dataURLOptions = options.dataURLOptions, dataURLOptions = _options_dataURLOptions === void 0 ? {} : _options_dataURLOptions, mousemoveWait = options.mousemoveWait, _options_recordDOM = options.recordDOM, recordDOM = _options_recordDOM === void 0 ? true : _options_recordDOM, _options_recordCanvas = options.recordCanvas, recordCanvas = _options_recordCanvas === void 0 ? false : _options_recordCanvas, _options_recordCrossOriginIframes = options.recordCrossOriginIframes, recordCrossOriginIframes = _options_recordCrossOriginIframes === void 0 ? false : _options_recordCrossOriginIframes, allowedIframeOrigins = options.allowedIframeOrigins, _options_recordAfter = options.recordAfter, recordAfter = _options_recordAfter === void 0 ? options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load" : _options_recordAfter, _options_userTriggeredOnInput = options.userTriggeredOnInput, userTriggeredOnInput = _options_userTriggeredOnInput === void 0 ? false : _options_userTriggeredOnInput, _options_collectFonts = options.collectFonts, collectFonts = _options_collectFonts === void 0 ? false : _options_collectFonts, _options_inlineImages = options.inlineImages, inlineImages = _options_inlineImages === void 0 ? false : _options_inlineImages, plugins = options.plugins, _options_keepIframeSrcFn = options.keepIframeSrcFn, keepIframeSrcFn = _options_keepIframeSrcFn === void 0 ? function() {
13273
13306
  return false;
13274
13307
  } : _options_keepIframeSrcFn, _options_ignoreCSSAttributes = options.ignoreCSSAttributes, ignoreCSSAttributes = _options_ignoreCSSAttributes === void 0 ? /* @__PURE__ */ new Set([]) : _options_ignoreCSSAttributes, errorHandler2 = options.errorHandler;
13275
13308
  registerErrorHandler(errorHandler2);
13309
+ var validatedOrigins;
13310
+ if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
13311
+ validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
13312
+ if (validatedOrigins.size === 0) {
13313
+ validatedOrigins = void 0;
13314
+ }
13315
+ }
13276
13316
  var inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
13277
13317
  var passEmitsToParent = false;
13278
13318
  if (!inEmittingFrame) {
@@ -13364,7 +13404,14 @@ function record(options) {
13364
13404
  origin: window.location.origin,
13365
13405
  isCheckout: isCheckout
13366
13406
  };
13367
- window.parent.postMessage(message, "*");
13407
+ if (validatedOrigins) {
13408
+ for(var _iterator = _create_for_of_iterator_helper_loose(validatedOrigins), _step; !(_step = _iterator()).done;){
13409
+ var targetOrigin = _step.value;
13410
+ window.parent.postMessage(message, targetOrigin);
13411
+ }
13412
+ } else {
13413
+ window.parent.postMessage(message, "*");
13414
+ }
13368
13415
  }
13369
13416
  if (e2.type === EventType.FullSnapshot) {
13370
13417
  lastFullSnapshotEvent = e2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.76.0",
3
+ "version": "2.78.0",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
6
  "module": "dist/mixpanel.module.js",
@@ -91,9 +91,10 @@
91
91
  "webpack": "1.12.2"
92
92
  },
93
93
  "dependencies": {
94
- "@mixpanel/rrweb": "2.0.0-alpha.18.3",
95
- "@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.3",
96
- "@mixpanel/rrweb-utils": "2.0.0-alpha.18.3",
97
- "json-logic-js": "2.0.5"
94
+ "@mixpanel/rrweb": "2.0.0-alpha.18.4",
95
+ "@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.4",
96
+ "@mixpanel/rrweb-utils": "2.0.0-alpha.18.4",
97
+ "json-logic-js": "2.0.5",
98
+ "@types/json-logic-js": "2.0.5"
98
99
  }
99
100
  }
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.76.0'
3
+ LIB_VERSION: '2.78.0'
4
4
  };
5
5
 
6
6
  // Window global names for async modules
@@ -0,0 +1,24 @@
1
+ # Flags Module
2
+
3
+ ## Testing
4
+ - Test runner is **mocha** (not jest): `BABEL_ENV=test npx mocha --require babel-core/register tests/unit/flags.js`
5
+ - Unit tests live in `tests/unit/flags.js`
6
+
7
+ ## Code style
8
+ - ES5 prototypal classes — no arrow functions, no ES6 classes
9
+ - Use `.bind(this)` for `this` context in promise `.then()` / `.catch()` callbacks
10
+ - Flat promise chains preferred over nested `.then()` inside `.then()`
11
+
12
+ ## Public API pattern
13
+ - Snake-case aliases are registered at the bottom of `index.js` (e.g., `prototype['load_flags'] = prototype.loadFlags`)
14
+ - New public methods need both the camelCase implementation and a snake_case alias
15
+
16
+ ## Error handling convention
17
+ - `fetchFlags()` always rejects on error (single `.catch` that logs and re-throws)
18
+ - Fire-and-forget callers (`init`, `updateContext`, `mixpanel-core.js` identify call) swallow errors at the call site with `.catch(function() {})`
19
+ - User-facing methods like `loadFlags` propagate rejections so the caller can handle them
20
+
21
+ ## Key files
22
+ - `src/flags/index.js` — `FeatureFlagManager` class (fetch, load, variants, first-time events)
23
+ - `src/mixpanel-core.js` — calls `fetchFlags()` on distinct_id change (~line 1764)
24
+ - `tests/unit/flags.js` — all flag unit tests
@@ -53,7 +53,9 @@ FeatureFlagManager.prototype.init = function() {
53
53
  }
54
54
 
55
55
  this.flags = null;
56
- this.fetchFlags();
56
+ this.fetchFlags().catch(function() {
57
+ logger.error('Error fetching flags during init');
58
+ });
57
59
 
58
60
  this.trackedFeatures = new Set();
59
61
  this.pendingFirstTimeEvents = {};
@@ -94,8 +96,12 @@ FeatureFlagManager.prototype.updateContext = function(newContext, options) {
94
96
  var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
95
97
  ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
96
98
 
97
- this.setMpConfig(FLAGS_CONFIG_KEY, ffConfig);
98
- return this.fetchFlags();
99
+ var configUpdate = {};
100
+ configUpdate[FLAGS_CONFIG_KEY] = ffConfig;
101
+ this.setMpConfig(configUpdate);
102
+ return this.fetchFlags().catch(function() {
103
+ logger.error('Error fetching flags during updateContext');
104
+ });
99
105
  };
100
106
 
101
107
  FeatureFlagManager.prototype.areFlagsReady = function() {
@@ -132,96 +138,110 @@ FeatureFlagManager.prototype.fetchFlags = function() {
132
138
  }
133
139
  }).then(function(response) {
134
140
  this.markFetchComplete();
135
- return response.json().then(function(responseBody) {
136
- var responseFlags = responseBody['flags'];
137
- if (!responseFlags) {
138
- throw new Error('No flags in API response');
139
- }
140
- var flags = new Map();
141
- var pendingFirstTimeEvents = {};
142
-
143
- // Process flags from response
144
- _.each(responseFlags, function(data, key) {
145
- // Check if this flag has any activated first-time events this session
146
- var hasActivatedEvent = false;
147
- var prefix = key + ':';
148
- _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
149
- if (eventKey.startsWith(prefix)) {
150
- hasActivatedEvent = true;
151
- }
152
- });
153
-
154
- if (hasActivatedEvent) {
155
- // Preserve the activated variant, don't overwrite with server's current variant
156
- var currentFlag = this.flags && this.flags.get(key);
157
- if (currentFlag) {
158
- flags.set(key, currentFlag);
159
- }
160
- } else {
161
- // Use server's current variant
162
- flags.set(key, {
163
- 'key': data['variant_key'],
164
- 'value': data['variant_value'],
165
- 'experiment_id': data['experiment_id'],
166
- 'is_experiment_active': data['is_experiment_active'],
167
- 'is_qa_tester': data['is_qa_tester']
168
- });
141
+ return response.json();
142
+ }.bind(this)).then(function(responseBody) {
143
+ var responseFlags = responseBody['flags'];
144
+ if (!responseFlags) {
145
+ throw new Error('No flags in API response');
146
+ }
147
+ var flags = new Map();
148
+ var pendingFirstTimeEvents = {};
149
+
150
+ // Process flags from response
151
+ _.each(responseFlags, function(data, key) {
152
+ // Check if this flag has any activated first-time events this session
153
+ var hasActivatedEvent = false;
154
+ var prefix = key + ':';
155
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
156
+ if (eventKey.startsWith(prefix)) {
157
+ hasActivatedEvent = true;
169
158
  }
170
- }, this);
159
+ });
171
160
 
172
- // Process top-level pending_first_time_events array
173
- var topLevelDefinitions = responseBody['pending_first_time_events'];
174
- if (topLevelDefinitions && topLevelDefinitions.length > 0) {
175
- _.each(topLevelDefinitions, function(def) {
176
- var flagKey = def['flag_key'];
177
- var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
178
-
179
- // Skip if this specific event has already been activated this session
180
- if (this.activatedFirstTimeEvents[eventKey]) {
181
- return;
182
- }
183
-
184
- // Store pending event definition using composite key
185
- pendingFirstTimeEvents[eventKey] = {
186
- 'flag_key': flagKey,
187
- 'flag_id': def['flag_id'],
188
- 'project_id': def['project_id'],
189
- 'first_time_event_hash': def['first_time_event_hash'],
190
- 'event_name': def['event_name'],
191
- 'property_filters': def['property_filters'],
192
- 'pending_variant': def['pending_variant']
193
- };
194
- }, this);
161
+ if (hasActivatedEvent) {
162
+ // Preserve the activated variant, don't overwrite with server's current variant
163
+ var currentFlag = this.flags && this.flags.get(key);
164
+ if (currentFlag) {
165
+ flags.set(key, currentFlag);
166
+ }
167
+ } else {
168
+ // Use server's current variant
169
+ flags.set(key, {
170
+ 'key': data['variant_key'],
171
+ 'value': data['variant_value'],
172
+ 'experiment_id': data['experiment_id'],
173
+ 'is_experiment_active': data['is_experiment_active'],
174
+ 'is_qa_tester': data['is_qa_tester']
175
+ });
195
176
  }
177
+ }, this);
178
+
179
+ // Process top-level pending_first_time_events array
180
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
181
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
182
+ _.each(topLevelDefinitions, function(def) {
183
+ var flagKey = def['flag_key'];
184
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
185
+
186
+ // Skip if this specific event has already been activated this session
187
+ if (this.activatedFirstTimeEvents[eventKey]) {
188
+ return;
189
+ }
196
190
 
197
- // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
198
- if (this.activatedFirstTimeEvents) {
199
- _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
200
- var flagKey = getFlagKeyFromPendingEventKey(eventKey);
201
- if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
202
- // Keep the activated flag even though it's not in the new response
203
- flags.set(flagKey, this.flags.get(flagKey));
204
- }
205
- }, this);
206
- }
191
+ // Store pending event definition using composite key
192
+ pendingFirstTimeEvents[eventKey] = {
193
+ 'flag_key': flagKey,
194
+ 'flag_id': def['flag_id'],
195
+ 'project_id': def['project_id'],
196
+ 'first_time_event_hash': def['first_time_event_hash'],
197
+ 'event_name': def['event_name'],
198
+ 'property_filters': def['property_filters'],
199
+ 'pending_variant': def['pending_variant']
200
+ };
201
+ }, this);
202
+ }
207
203
 
208
- this.flags = flags;
209
- this.pendingFirstTimeEvents = pendingFirstTimeEvents;
210
- this._traceparent = traceparent;
204
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
205
+ if (this.activatedFirstTimeEvents) {
206
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
207
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
208
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
209
+ // Keep the activated flag even though it's not in the new response
210
+ flags.set(flagKey, this.flags.get(flagKey));
211
+ }
212
+ }, this);
213
+ }
211
214
 
212
- this._loadTargetingIfNeeded();
213
- }.bind(this)).catch(function(error) {
214
- this.markFetchComplete();
215
- logger.error(error);
216
- }.bind(this));
215
+ this.flags = flags;
216
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
217
+ this._traceparent = traceparent;
218
+
219
+ this._loadTargetingIfNeeded();
217
220
  }.bind(this)).catch(function(error) {
218
- this.markFetchComplete();
221
+ if (this._fetchInProgressStartTime) {
222
+ this.markFetchComplete();
223
+ }
219
224
  logger.error(error);
225
+ throw error;
220
226
  }.bind(this));
221
227
 
222
228
  return this.fetchPromise;
223
229
  };
224
230
 
231
+ FeatureFlagManager.prototype.loadFlags = function() {
232
+ if (!this.isSystemEnabled()) {
233
+ return Promise.resolve();
234
+ }
235
+ if (!this.trackedFeatures) {
236
+ logger.error('loadFlags called before init');
237
+ return Promise.resolve();
238
+ }
239
+ if (this._fetchInProgressStartTime) {
240
+ return this.fetchPromise;
241
+ }
242
+ return this.fetchFlags();
243
+ };
244
+
225
245
  FeatureFlagManager.prototype.markFetchComplete = function() {
226
246
  if (!this._fetchInProgressStartTime) {
227
247
  logger.error('Fetch in progress started time not set, cannot mark fetch complete');
@@ -501,6 +521,13 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
501
521
  this.track('$experiment_started', trackingProperties);
502
522
  };
503
523
 
524
+ FeatureFlagManager.prototype.whenReady = function() {
525
+ if (this.fetchPromise) {
526
+ return this.fetchPromise;
527
+ }
528
+ return Promise.resolve();
529
+ };
530
+
504
531
  FeatureFlagManager.prototype.minApisSupported = function() {
505
532
  return !!this.fetch &&
506
533
  typeof Promise !== 'undefined' &&
@@ -517,7 +544,9 @@ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype
517
544
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
518
545
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
519
546
  FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
547
+ FeatureFlagManager.prototype['load_flags'] = FeatureFlagManager.prototype.loadFlags;
520
548
  FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
549
+ FeatureFlagManager.prototype['when_ready'] = FeatureFlagManager.prototype.whenReady;
521
550
 
522
551
  // Deprecated method
523
552
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
package/src/index.d.ts CHANGED
@@ -252,11 +252,12 @@ export interface Config {
252
252
  record_mask_all_inputs: boolean;
253
253
  record_min_ms: number;
254
254
  record_max_ms: number;
255
- record_sessions_percent: number;
255
+ record_allowed_iframe_origins: string[];
256
256
  record_canvas: boolean;
257
257
  recording_event_triggers: RecordingEventTriggers;
258
258
  record_heatmap_data: boolean;
259
259
  remote_settings_mode: RemoteSettingType;
260
+ record_sessions_percent: number;
260
261
  hooks: {
261
262
  before_identify?: (new_distinct_id: string) => string | null;
262
263
  before_register?: (
@@ -356,6 +357,7 @@ export interface FlagsUpdateContextOptions {
356
357
 
357
358
  export interface FlagsManager {
358
359
  are_flags_ready(): boolean;
360
+ load_flags(): Promise<void>;
359
361
  get_variant(
360
362
  featureName: string,
361
363
  fallback: FlagsVariant
@@ -64,7 +64,6 @@ var INIT_SNIPPET = 1;
64
64
  /** @const */ var SETTING_FALLBACK = 'fallback';
65
65
  /** @const */ var SETTING_DISABLED = 'disabled';
66
66
 
67
-
68
67
  /*
69
68
  * Dynamic... constants? Is that an oxymoron?
70
69
  */
@@ -149,6 +148,7 @@ var DEFAULT_CONFIG = {
149
148
  'batch_request_timeout_ms': 90000,
150
149
  'batch_autostart': true,
151
150
  'hooks': {},
151
+ 'record_allowed_iframe_origins': [],
152
152
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
153
153
  'record_block_selector': 'img, video, audio',
154
154
  'record_canvas': false,
@@ -1724,7 +1724,9 @@ MixpanelLib.prototype.identify = function(
1724
1724
 
1725
1725
  // check feature flags again if distinct id has changed
1726
1726
  if (new_distinct_id !== previous_distinct_id) {
1727
- this.flags.fetchFlags();
1727
+ this.flags.fetchFlags().catch(function() {
1728
+ console.error('[flags] Error fetching flags during identify');
1729
+ });
1728
1730
  }
1729
1731
  };
1730
1732
 
@@ -10,7 +10,7 @@ import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
10
10
  import { RequestBatcher } from '../request-batcher';
11
11
 
12
12
  import { Config } from '../config';
13
- import { RECORD_ENQUEUE_THROTTLE_MS } from './utils';
13
+ import { RECORD_ENQUEUE_THROTTLE_MS, validateAllowedOrigins } from './utils';
14
14
  import { shouldMaskInput, shouldMaskText, getPrivacyConfig } from './masking';
15
15
  import { getRecordNetworkPlugin } from './rrweb-network-plugin';
16
16
 
@@ -265,6 +265,8 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
265
265
  );
266
266
  }
267
267
 
268
+ var validatedOrigins = validateAllowedOrigins(this.getConfig('record_allowed_iframe_origins'), logger);
269
+
268
270
  try {
269
271
  this._stopRecording = this._rrwebRecord({
270
272
  'emit': function (ev) {
@@ -299,6 +301,8 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
299
301
  'maskTextSelector': '*',
300
302
  'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
301
303
  'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
304
+ 'recordCrossOriginIframes': validatedOrigins.length > 0,
305
+ 'allowedIframeOrigins': validatedOrigins,
302
306
  'recordCanvas': this.getConfig('record_canvas'),
303
307
  'sampling': {
304
308
  'canvas': 15
@@ -1,3 +1,5 @@
1
+ import { _ } from '../utils';
2
+
1
3
  /**
2
4
  * @param {import('./session-recording').SerializedRecording} serializedRecording
3
5
  * @returns {boolean}
@@ -10,7 +12,31 @@ var isRecordingExpired = function(serializedRecording) {
10
12
 
11
13
  var RECORD_ENQUEUE_THROTTLE_MS = 250;
12
14
 
15
+ var validateAllowedOrigins = function(origins, logger) {
16
+ if (!_.isArray(origins)) {
17
+ if (origins) {
18
+ logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
19
+ }
20
+ return [];
21
+ }
22
+ var valid = [];
23
+ for (var i = 0; i < origins.length; i++) {
24
+ try {
25
+ var origin = new URL(origins[i]).origin;
26
+ if (origin === 'null') {
27
+ logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
28
+ continue;
29
+ }
30
+ valid.push(origin);
31
+ } catch (e) {
32
+ logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
33
+ }
34
+ }
35
+ return valid;
36
+ };
37
+
13
38
  export {
14
39
  isRecordingExpired,
15
- RECORD_ENQUEUE_THROTTLE_MS
40
+ RECORD_ENQUEUE_THROTTLE_MS,
41
+ validateAllowedOrigins
16
42
  };