mixpanel-browser 2.75.0 → 2.77.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 (62) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/dependabot.yml +8 -0
  3. package/.github/workflows/integration-tests.yml +4 -4
  4. package/.github/workflows/unit-tests.yml +4 -4
  5. package/CHANGELOG.md +14 -0
  6. package/build.sh +10 -8
  7. package/dist/async-modules/mixpanel-recorder-DLKbUIEE.js +23669 -0
  8. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js +2 -0
  9. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +1 -0
  10. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +2 -0
  11. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +1 -0
  12. package/dist/async-modules/mixpanel-targeting-CmVvUyFM.js +2520 -0
  13. package/dist/mixpanel-core.cjs.d.ts +70 -1
  14. package/dist/mixpanel-core.cjs.js +724 -426
  15. package/dist/mixpanel-recorder.js +791 -41
  16. package/dist/mixpanel-recorder.min.js +1 -1
  17. package/dist/mixpanel-recorder.min.js.map +1 -1
  18. package/dist/mixpanel-targeting.js +6 -62
  19. package/dist/mixpanel-targeting.min.js +1 -1
  20. package/dist/mixpanel-targeting.min.js.map +1 -1
  21. package/dist/mixpanel-with-async-modules.cjs.d.ts +70 -1
  22. package/dist/mixpanel-with-async-modules.cjs.js +724 -426
  23. package/dist/mixpanel-with-async-recorder.cjs.d.ts +70 -1
  24. package/dist/mixpanel-with-async-recorder.cjs.js +724 -426
  25. package/dist/mixpanel-with-recorder.d.ts +70 -1
  26. package/dist/mixpanel-with-recorder.js +1471 -450
  27. package/dist/mixpanel-with-recorder.min.d.ts +70 -1
  28. package/dist/mixpanel-with-recorder.min.js +1 -1
  29. package/dist/mixpanel.amd.d.ts +70 -1
  30. package/dist/mixpanel.amd.js +1473 -504
  31. package/dist/mixpanel.cjs.d.ts +70 -1
  32. package/dist/mixpanel.cjs.js +1473 -504
  33. package/dist/mixpanel.globals.js +724 -426
  34. package/dist/mixpanel.min.js +189 -182
  35. package/dist/mixpanel.module.d.ts +70 -1
  36. package/dist/mixpanel.module.js +1473 -504
  37. package/dist/mixpanel.umd.d.ts +70 -1
  38. package/dist/mixpanel.umd.js +1473 -504
  39. package/dist/rrweb-bundled.js +61 -9
  40. package/dist/rrweb-compiled.js +56 -9
  41. package/logo.svg +5 -0
  42. package/package.json +6 -4
  43. package/rollup.config.mjs +163 -46
  44. package/src/autocapture/index.js +10 -27
  45. package/src/config.js +9 -3
  46. package/src/flags/index.js +1 -2
  47. package/src/index.d.ts +70 -1
  48. package/src/mixpanel-core.js +77 -112
  49. package/src/recorder/index.js +1 -1
  50. package/src/recorder/recorder.js +5 -1
  51. package/src/recorder/rrweb-network-plugin.js +649 -0
  52. package/src/recorder/session-recording.js +36 -12
  53. package/src/recorder/utils.js +27 -1
  54. package/src/recorder-manager.js +324 -0
  55. package/src/request-batcher.js +1 -1
  56. package/src/targeting/event-matcher.js +2 -57
  57. package/src/targeting/index.js +1 -1
  58. package/src/targeting/loader.js +1 -1
  59. package/src/utils.js +13 -1
  60. package/testServer.js +69 -1
  61. package/src/globals.js +0 -14
  62. /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
@@ -9,9 +9,10 @@ import { IDBStorageWrapper, RECORDING_EVENTS_STORE_NAME } from '../storage/index
9
9
  import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
10
10
  import { RequestBatcher } from '../request-batcher';
11
11
 
12
- import Config from '../config';
13
- import { RECORD_ENQUEUE_THROTTLE_MS } from './utils';
12
+ import { Config } from '../config';
13
+ import { RECORD_ENQUEUE_THROTTLE_MS, validateAllowedOrigins } from './utils';
14
14
  import { shouldMaskInput, shouldMaskText, getPrivacyConfig } from './masking';
15
+ import { getRecordNetworkPlugin } from './rrweb-network-plugin';
15
16
 
16
17
  var logger = console_with_prefix('recorder');
17
18
  var CompressionStream = window['CompressionStream'];
@@ -241,6 +242,31 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
241
242
 
242
243
  var privacyConfig = getPrivacyConfig(this._mixpanel);
243
244
 
245
+ var plugins = [];
246
+ if (this.getConfig('record_network')) {
247
+ var options = this.getConfig('record_network_options') || {};
248
+ // don't track requests to Mixpanel /record API
249
+ var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
250
+ ignoreRequestUrls.push(this._getApiRoute());
251
+ options.ignoreRequestUrls = ignoreRequestUrls;
252
+
253
+ plugins.push(getRecordNetworkPlugin(options));
254
+ }
255
+
256
+ if (this.getConfig('record_console')) {
257
+ plugins.push(
258
+ getRecordConsolePlugin({
259
+ stringifyOptions: {
260
+ stringLengthLimit: 1000,
261
+ numOfKeysLimit: 50,
262
+ depthOfLimit: 2
263
+ }
264
+ })
265
+ );
266
+ }
267
+
268
+ var validatedOrigins = validateAllowedOrigins(this.getConfig('record_allowed_iframe_origins'), logger);
269
+
244
270
  try {
245
271
  this._stopRecording = this._rrwebRecord({
246
272
  'emit': function (ev) {
@@ -275,19 +301,13 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
275
301
  'maskTextSelector': '*',
276
302
  'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
277
303
  'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
304
+ 'recordCrossOriginIframes': validatedOrigins.length > 0,
305
+ 'allowedIframeOrigins': validatedOrigins,
278
306
  'recordCanvas': this.getConfig('record_canvas'),
279
307
  'sampling': {
280
308
  'canvas': 15
281
309
  },
282
- 'plugins': this.getConfig('record_console') ? [
283
- getRecordConsolePlugin({
284
- stringifyOptions: {
285
- stringLengthLimit: 1000,
286
- numOfKeysLimit: 50,
287
- depthOfLimit: 2
288
- }
289
- })
290
- ] : []
310
+ 'plugins': plugins,
291
311
  });
292
312
  } catch (err) {
293
313
  this.reportError('Unexpected error when starting rrweb recording.', err);
@@ -402,6 +422,10 @@ SessionRecording.deserialize = function (serializedRecording, options) {
402
422
  return recording;
403
423
  };
404
424
 
425
+ SessionRecording.prototype._getApiRoute = function () {
426
+ return this.getConfig('api_routes')['record'];
427
+ };
428
+
405
429
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
406
430
  var onSuccess = function (response, responseBody) {
407
431
  // Update batch specific props only if the request was successful to guarantee ordering.
@@ -421,7 +445,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
421
445
  });
422
446
  }.bind(this);
423
447
  var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
424
- window['fetch'](apiHost + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
448
+ window['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
425
449
  'method': 'POST',
426
450
  'headers': {
427
451
  'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
@@ -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
  };
@@ -0,0 +1,324 @@
1
+ /* eslint camelcase: "off" */
2
+
3
+ import {RECORDER_FILENAME, TARGETING_FILENAME, RECORDER_GLOBAL_NAME} from './config';
4
+ import { _, console, console_with_prefix, safewrap, safewrapClass } from './utils';
5
+ import { window } from './window';
6
+ import { Promise } from './promise-polyfill';
7
+ import { IDBStorageWrapper, RECORDING_REGISTRY_STORE_NAME } from './storage/indexed-db';
8
+ import { isRecordingExpired, validateAllowedOrigins } from './recorder/utils';
9
+ import { getTargetingPromise } from './targeting/loader';
10
+
11
+ var logger = console_with_prefix('recorder');
12
+
13
+ var IFRAME_HANDSHAKE_REQUEST = 'mp_iframe_handshake_request';
14
+ var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
15
+
16
+
17
+ /**
18
+ * RecorderManager: manages session recording initialization, lifecycle and state
19
+ * @constructor
20
+ */
21
+ var RecorderManager = function(initOptions) {
22
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
23
+ // but ideally we should be able to remove this dependency.
24
+ this.mixpanelInstance = initOptions.mixpanelInstance;
25
+
26
+ this.getMpConfig = initOptions.getConfigFunc;
27
+ this.getTabId = initOptions.getTabIdFunc;
28
+ this.reportError = initOptions.reportErrorFunc;
29
+ this.getDistinctId = initOptions.getDistinctIdFunc;
30
+ this.loadExtraBundle = initOptions.loadExtraBundle;
31
+ this.recorderSrc = initOptions.recorderSrc;
32
+ this.targetingSrc = initOptions.targetingSrc;
33
+ this.libBasePath = initOptions.libBasePath;
34
+
35
+ this._recorder = null;
36
+ this._parentReplayId = null;
37
+ this._parentFrameRetryInterval = null;
38
+ };
39
+
40
+ RecorderManager.prototype.shouldLoadRecorder = function() {
41
+ if (this.getMpConfig('disable_persistence')) {
42
+ console.log('Load recorder check skipped due to disable_persistence config');
43
+ return Promise.resolve(false);
44
+ }
45
+
46
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
47
+ var tab_id = this.getTabId();
48
+ return recording_registry_idb.init()
49
+ .then(function () {
50
+ return recording_registry_idb.getAll();
51
+ })
52
+ .then(function (recordings) {
53
+ for (var i = 0; i < recordings.length; i++) {
54
+ // if there are expired recordings in the registry, we should load the recorder to flush them
55
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
56
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ })
62
+ .catch(_.bind(function (err) {
63
+ this.reportError('Error checking recording registry', err);
64
+ return false;
65
+ }, this));
66
+ };
67
+
68
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
69
+ if (!window['MutationObserver']) {
70
+ console.critical('Browser does not support MutationObserver; skipping session recording');
71
+ return Promise.resolve();
72
+ }
73
+
74
+ var loadRecorder = _.bind(function(startNewIfInactive) {
75
+ return new Promise(_.bind(function(resolve) {
76
+ var handleLoadedRecorder = safewrap(_.bind(function() {
77
+ this._recorder = this._recorder || new window[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
78
+ this._recorder['resumeRecording'](startNewIfInactive);
79
+ resolve();
80
+ }, this));
81
+
82
+ if (_.isUndefined(window[RECORDER_GLOBAL_NAME])) {
83
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
84
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
85
+ } else {
86
+ handleLoadedRecorder();
87
+ }
88
+ }, this));
89
+ }, this);
90
+
91
+ // Cross-origin iframe handling
92
+ var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger);
93
+ var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
94
+
95
+ if (isCrossOriginRecordingEnabled) {
96
+ // listen for handshake requests from their own child iframes (including nested)
97
+ this._setupParentFrameListener(allowedOrigins);
98
+
99
+ if (window.parent !== window) {
100
+ // also wait for parent's replay ID
101
+ this._setupChildFrameListener(allowedOrigins, loadRecorder);
102
+ this._sendParentFrameRequestWithRetry(allowedOrigins);
103
+ return Promise.resolve();
104
+ }
105
+ }
106
+
107
+ /**
108
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
109
+ * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
110
+ */
111
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
112
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
113
+ if (force_start || is_sampled) {
114
+ return loadRecorder(true);
115
+ } else {
116
+ return this.shouldLoadRecorder()
117
+ .then(_.bind(function (shouldLoad) {
118
+ if (shouldLoad) {
119
+ return loadRecorder(false);
120
+ }
121
+ return Promise.resolve();
122
+ }, this));
123
+ }
124
+ };
125
+
126
+ RecorderManager.prototype.isRecording = function() {
127
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
128
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
129
+ return false;
130
+ }
131
+ try {
132
+ return this._recorder['isRecording']();
133
+ } catch (e) {
134
+ this.reportError('Error checking if recording is active', e);
135
+ return false;
136
+ }
137
+ };
138
+
139
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
140
+ var isRecording = this.isRecording();
141
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
142
+
143
+ if (!isRecording && recordingTriggerEvents) {
144
+ var trigger = recordingTriggerEvents[event_name];
145
+ if (trigger && typeof trigger['percentage'] === 'number') {
146
+ var newRate = trigger['percentage'];
147
+ var propertyFilters = trigger['property_filters'];
148
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
149
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
150
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
151
+ .then(function(targeting) {
152
+ try {
153
+ var result = targeting['eventMatchesCriteria'](
154
+ event_name,
155
+ properties,
156
+ {
157
+ 'event_name': event_name,
158
+ 'property_filters': propertyFilters
159
+ }
160
+ );
161
+ if (result['matches']) {
162
+ this.checkAndStartSessionRecording(false, newRate);
163
+ }
164
+ } catch (err) {
165
+ console.critical('Could not parse recording event trigger properties logic:', err);
166
+ }
167
+ }.bind(this)).catch(function(err) {
168
+ console.critical('Failed to load targeting library:', err);
169
+ });
170
+ } else {
171
+ this.checkAndStartSessionRecording(false, newRate);
172
+ }
173
+ }
174
+ }
175
+ };
176
+
177
+ RecorderManager.prototype.stopSessionRecording = function() {
178
+ if (this._recorder) {
179
+ return this._recorder['stopRecording']();
180
+ }
181
+ return Promise.resolve();
182
+ };
183
+
184
+ RecorderManager.prototype.pauseSessionRecording = function() {
185
+ if (this._recorder) {
186
+ return this._recorder['pauseRecording']();
187
+ }
188
+ return Promise.resolve();
189
+ };
190
+
191
+ RecorderManager.prototype.resumeSessionRecording = function() {
192
+ if (this._recorder) {
193
+ return this._recorder['resumeRecording']();
194
+ }
195
+ return Promise.resolve();
196
+ };
197
+
198
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
199
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
200
+ };
201
+
202
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
203
+ var props = {};
204
+ var replay_id = this.getSessionReplayId();
205
+ if (replay_id) {
206
+ props['$mp_replay_id'] = replay_id;
207
+ }
208
+ return props;
209
+ };
210
+
211
+ RecorderManager.prototype.getSessionReplayUrl = function() {
212
+ var replay_url = null;
213
+ var replay_id = this.getSessionReplayId();
214
+ if (replay_id) {
215
+ var query_params = _.HTTPBuildQuery({
216
+ 'replay_id': replay_id,
217
+ 'distinct_id': this.getDistinctId(),
218
+ 'token': this.getMpConfig('token')
219
+ });
220
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
221
+ }
222
+ return replay_url;
223
+ };
224
+
225
+ RecorderManager.prototype.getSessionReplayId = function() {
226
+ // Child iframe uses parent's replay ID
227
+ if (this._parentReplayId) {
228
+ return this._parentReplayId;
229
+ }
230
+ var replay_id = null;
231
+ if (this._recorder) {
232
+ replay_id = this._recorder['replayId'];
233
+ }
234
+ return replay_id || null;
235
+ };
236
+
237
+ // "private" public method to reach into the recorder in test cases
238
+ RecorderManager.prototype.getRecorder = function() {
239
+ return this._recorder;
240
+ };
241
+
242
+ RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
243
+ if (this._childFrameMessageHandler) {
244
+ return;
245
+ }
246
+ var self = this;
247
+ this._childFrameMessageHandler = function(event) {
248
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
249
+ var data = event.data;
250
+ if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
251
+ self._parentReplayId = data['replayId'];
252
+ if (data['distinctId']) {
253
+ self.mixpanelInstance['identify'](data['distinctId']);
254
+ }
255
+ self._parentFrameRetryActive = false;
256
+ window.removeEventListener('message', self._childFrameMessageHandler);
257
+ self._childFrameMessageHandler = null;
258
+ loadRecorder(true);
259
+ }
260
+ };
261
+ window.addEventListener('message', this._childFrameMessageHandler);
262
+ };
263
+
264
+ RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
265
+ var message = {};
266
+ message['type'] = IFRAME_HANDSHAKE_REQUEST;
267
+ message['token'] = this.getMpConfig('token');
268
+ for (var i = 0; i < allowedOrigins.length; i++) {
269
+ try {
270
+ window.parent.postMessage(message, allowedOrigins[i]);
271
+ } catch (e) {
272
+ // origin mismatch - ignore
273
+ }
274
+ }
275
+ };
276
+
277
+ RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
278
+ var self = this;
279
+ var maxRetries = 10;
280
+ var retryCount = 0;
281
+ var delay = 50;
282
+ this._parentFrameRetryActive = true;
283
+
284
+ this._sendParentFrameRequest(allowedOrigins);
285
+
286
+ function scheduleRetry() {
287
+ setTimeout(function() {
288
+ if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
289
+ return;
290
+ }
291
+ self._sendParentFrameRequest(allowedOrigins);
292
+ delay *= 2;
293
+ scheduleRetry();
294
+ }, delay);
295
+ }
296
+ scheduleRetry();
297
+ };
298
+
299
+ RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
300
+ if (this._parentFrameMessageHandler) {
301
+ return;
302
+ }
303
+ var self = this;
304
+ this._parentFrameMessageHandler = function(event) {
305
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
306
+ var data = event.data;
307
+ if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
308
+ var replayId = self.getSessionReplayId();
309
+ if (replayId) {
310
+ var response = {};
311
+ response['type'] = IFRAME_HANDSHAKE_RESPONSE;
312
+ response['token'] = self.getMpConfig('token');
313
+ response['replayId'] = replayId;
314
+ response['distinctId'] = self.getDistinctId();
315
+ event.source.postMessage(response, event.origin);
316
+ }
317
+ }
318
+ };
319
+ window.addEventListener('message', this._parentFrameMessageHandler);
320
+ };
321
+
322
+ safewrapClass(RecorderManager);
323
+
324
+ export { RecorderManager };
@@ -1,4 +1,4 @@
1
- import Config from './config';
1
+ import { Config } from './config';
2
2
  import { Promise } from './promise-polyfill';
3
3
  import { RequestQueue } from './request-queue';
4
4
  import { console_with_prefix, isOnline, _ } from './utils'; // eslint-disable-line camelcase
@@ -1,53 +1,6 @@
1
1
  import { _ } from '../utils';
2
2
  import jsonLogic from 'json-logic-js';
3
3
 
4
- /**
5
- * Shared helper to recursively lowercase strings in nested structures
6
- * @param {*} obj - Value to process
7
- * @param {boolean} lowercaseKeys - Whether to lowercase object keys
8
- * @returns {*} Processed value with lowercased strings
9
- */
10
- var lowercaseJson = function(obj, lowercaseKeys) {
11
- if (obj === null || obj === undefined) {
12
- return obj;
13
- } else if (typeof obj === 'string') {
14
- return obj.toLowerCase();
15
- } else if (Array.isArray(obj)) {
16
- return obj.map(function(item) {
17
- return lowercaseJson(item, lowercaseKeys);
18
- });
19
- } else if (obj === Object(obj)) {
20
- var result = {};
21
- for (var key in obj) {
22
- if (obj.hasOwnProperty(key)) {
23
- var newKey = lowercaseKeys && typeof key === 'string' ? key.toLowerCase() : key;
24
- result[newKey] = lowercaseJson(obj[key], lowercaseKeys);
25
- }
26
- }
27
- return result;
28
- } else {
29
- return obj;
30
- }
31
- };
32
-
33
- /**
34
- * Lowercase all string keys and values in a nested structure
35
- * @param {*} val - Value to process
36
- * @returns {*} Processed value with lowercased strings
37
- */
38
- var lowercaseKeysAndValues = function(val) {
39
- return lowercaseJson(val, true);
40
- };
41
-
42
- /**
43
- * Lowercase only leaf node string values in a nested structure (keys unchanged)
44
- * @param {*} val - Value to process
45
- * @returns {*} Processed value with lowercased leaf strings
46
- */
47
- var lowercaseOnlyLeafNodes = function(val) {
48
- return lowercaseJson(val, false);
49
- };
50
-
51
4
  /**
52
5
  * Check if an event matches the given criteria
53
6
  * @param {string} eventName - The name of the event being checked
@@ -71,13 +24,8 @@ var eventMatchesCriteria = function(eventName, properties, criteria) {
71
24
 
72
25
  if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
73
26
  try {
74
- // Lowercase all keys and values in event properties for case-insensitive matching
75
- var lowercasedProperties = lowercaseKeysAndValues(properties || {});
76
-
77
- // Lowercase only leaf nodes in JsonLogic filters (keep operators intact)
78
- var lowercasedFilters = lowercaseOnlyLeafNodes(propertyFilters);
79
-
80
- filtersMatch = jsonLogic.apply(lowercasedFilters, lowercasedProperties);
27
+ // Use properties as-is for case-sensitive matching
28
+ filtersMatch = jsonLogic.apply(propertyFilters, properties || {});
81
29
  } catch (error) {
82
30
  return {
83
31
  matches: false,
@@ -90,8 +38,5 @@ var eventMatchesCriteria = function(eventName, properties, criteria) {
90
38
  };
91
39
 
92
40
  export {
93
- lowercaseJson,
94
- lowercaseKeysAndValues,
95
- lowercaseOnlyLeafNodes,
96
41
  eventMatchesCriteria
97
42
  };
@@ -1,5 +1,5 @@
1
1
  import { window } from '../window';
2
- import { TARGETING_GLOBAL_NAME } from '../globals';
2
+ import { TARGETING_GLOBAL_NAME } from '../config';
3
3
  import { eventMatchesCriteria } from './event-matcher';
4
4
 
5
5
  // Create targeting library object
@@ -1,5 +1,5 @@
1
1
  import { window } from '../window';
2
- import { TARGETING_GLOBAL_NAME } from '../globals';
2
+ import { TARGETING_GLOBAL_NAME } from '../config';
3
3
 
4
4
  /**
5
5
  * Get the promise-based targeting loader
package/src/utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint camelcase: "off", eqeqeq: "off" */
2
- import Config from './config';
2
+ import { Config } from './config';
3
3
  import { NpoPromise, Promise } from './promise-polyfill';
4
4
  import { window } from './window';
5
5
 
@@ -1732,6 +1732,17 @@ var isOnline = function() {
1732
1732
 
1733
1733
  var NOOP_FUNC = function () {};
1734
1734
 
1735
+ var urlMatchesRegexList = function (url, regexList) {
1736
+ var matches = false;
1737
+ for (var i = 0; i < regexList.length; i++) {
1738
+ if (url.match(regexList[i])) {
1739
+ matches = true;
1740
+ break;
1741
+ }
1742
+ }
1743
+ return matches;
1744
+ };
1745
+
1735
1746
  var JSONStringify = null, JSONParse = null;
1736
1747
  if (typeof JSON !== 'undefined') {
1737
1748
  JSONStringify = JSON.stringify;
@@ -1797,6 +1808,7 @@ export {
1797
1808
  safewrap,
1798
1809
  safewrapClass,
1799
1810
  slice,
1811
+ urlMatchesRegexList,
1800
1812
  userAgent,
1801
1813
  windowOpera,
1802
1814
  };
package/testServer.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const express = require('express');
4
4
  const cookieParser = require('cookie-parser');
5
5
  const logger = require('morgan');
6
+ const { PARENT_PORT, CHILD_PORT } = require('./tests/browser/test-ports');
6
7
 
7
8
  const app = express();
8
9
 
@@ -31,10 +32,65 @@ const TEST_SUITES = {
31
32
 
32
33
  app.use(cookieParser());
33
34
  app.use(logger('dev'));
35
+ app.use(express.json());
36
+ app.use(express.urlencoded({ extended: true }));
34
37
 
35
38
  app.set('views', __dirname + '/tests');
36
39
  app.set('view engine', 'pug');
37
40
 
41
+ // ========================================
42
+ // Test API endpoints for network plugin tests
43
+ // ========================================
44
+
45
+ // Main test endpoint - handles all HTTP methods
46
+ app.all('/api/test', function(req, res) {
47
+ res.json({
48
+ success: true,
49
+ method: req.method,
50
+ headers: req.headers,
51
+ query: req.query,
52
+ body: req.body,
53
+ url: req.originalUrl,
54
+ timestamp: Date.now()
55
+ });
56
+ });
57
+
58
+ // Form submission endpoint
59
+ app.post('/api/test/form', function(req, res) {
60
+ res.json({
61
+ success: true,
62
+ method: 'POST',
63
+ contentType: req.get('Content-Type'),
64
+ formData: req.body,
65
+ timestamp: Date.now()
66
+ });
67
+ });
68
+
69
+ // Endpoint with custom response headers
70
+ app.get('/api/test/headers', function(req, res) {
71
+ res.set({
72
+ 'X-Custom-Header': 'custom-value',
73
+ 'X-Request-Id': 'test-request-123'
74
+ });
75
+ res.json({
76
+ success: true,
77
+ message: 'Response includes custom headers'
78
+ });
79
+ });
80
+
81
+ // Error response endpoint
82
+ app.get('/api/test/error/:status', function(req, res) {
83
+ const status = parseInt(req.params.status, 10) || 500;
84
+ res.status(status).json({ success: false, status: status });
85
+ });
86
+
87
+ // Session recording endpoint (mimics Mixpanel's /record API)
88
+ app.post(/^\/record\/.*/, express.raw({ type: '*/*', limit: '10mb' }), function(req, res) {
89
+ res.json({ code: 200, status: 'OK' });
90
+ });
91
+
92
+ // ========================================
93
+
38
94
  app.use('/tests', express.static(__dirname + "/tests"));
39
95
  app.get('/tests/cookie_included/:cookieName', function(req, res) {
40
96
  if (req.cookies && req.cookies[req.params.cookieName]) {
@@ -64,8 +120,20 @@ for (const [suiteId, suite] of Object.entries(TEST_SUITES)) {
64
120
  testUrl: suite.testUrl
65
121
  });
66
122
  });
123
+
124
+ // Cross-origin child iframe page for session recording tests
125
+ app.get('/tests/new/' + suiteId + '-cross-origin-page', function(req, res) {
126
+ res.render('cross-origin-page.pug', {
127
+ testUrl: './static/build/test/browser/cross-origin-page.js'
128
+ });
129
+ });
67
130
  }
68
131
 
69
- const server = app.listen(3001, function () {
132
+ const server = app.listen(PARENT_PORT, function () {
70
133
  console.log(`Mixpanel test app listening on port ${server.address().port}`);
71
134
  });
135
+
136
+ // Second port for cross-origin iframe tests
137
+ const server2 = app.listen(CHILD_PORT, function () {
138
+ console.log(`Mixpanel cross-origin test server listening on port ${server2.address().port}`);
139
+ });