mixpanel-browser 2.74.0 → 2.76.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.
- package/.claude/settings.local.json +3 -1
- package/.github/workflows/integration-tests.yml +2 -2
- package/.github/workflows/unit-tests.yml +3 -3
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/build.sh +10 -8
- package/dist/async-modules/mixpanel-recorder-bIS4LMGd.js +23595 -0
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +2 -0
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +1 -0
- package/dist/async-modules/mixpanel-targeting-BcAPS-Mz.js +2520 -0
- package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js +2 -0
- package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js.map +1 -0
- package/dist/mixpanel-core.cjs.d.ts +68 -0
- package/dist/mixpanel-core.cjs.js +802 -337
- package/dist/mixpanel-recorder.js +828 -40
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +2520 -0
- package/dist/mixpanel-targeting.min.js +2 -0
- package/dist/mixpanel-targeting.min.js.map +1 -0
- package/dist/mixpanel-with-async-modules.cjs.d.ts +590 -0
- package/dist/mixpanel-with-async-modules.cjs.js +9867 -0
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +802 -337
- package/dist/mixpanel-with-recorder.d.ts +68 -0
- package/dist/mixpanel-with-recorder.js +1591 -343
- package/dist/mixpanel-with-recorder.min.d.ts +68 -0
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +68 -0
- package/dist/mixpanel.amd.js +2124 -345
- package/dist/mixpanel.cjs.d.ts +68 -0
- package/dist/mixpanel.cjs.js +2124 -345
- package/dist/mixpanel.globals.js +802 -337
- package/dist/mixpanel.min.js +185 -175
- package/dist/mixpanel.module.d.ts +68 -0
- package/dist/mixpanel.module.js +2124 -345
- package/dist/mixpanel.umd.d.ts +68 -0
- package/dist/mixpanel.umd.js +2124 -345
- package/dist/rrweb-bundled.js +119 -5
- package/dist/rrweb-compiled.js +116 -5
- package/logo.svg +5 -0
- package/package.json +5 -3
- package/rollup.config.mjs +189 -40
- package/src/autocapture/index.js +10 -27
- package/src/config.js +9 -3
- package/src/flags/index.js +269 -9
- package/src/index.d.ts +68 -0
- package/src/loaders/loader-module.js +1 -0
- package/src/mixpanel-core.js +83 -109
- package/src/recorder/index.js +2 -1
- package/src/recorder/recorder.js +5 -1
- package/src/recorder/rrweb-network-plugin.js +649 -0
- package/src/recorder/session-recording.js +31 -11
- package/src/recorder-manager.js +216 -0
- package/src/request-batcher.js +1 -1
- package/src/targeting/event-matcher.js +42 -0
- package/src/targeting/index.js +11 -0
- package/src/targeting/loader.js +36 -0
- package/src/utils.js +14 -9
- package/testServer.js +55 -0
- /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +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';
|
|
12
|
+
import { Config } from '../config';
|
|
13
13
|
import { RECORD_ENQUEUE_THROTTLE_MS } 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,29 @@ 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
|
+
|
|
244
268
|
try {
|
|
245
269
|
this._stopRecording = this._rrwebRecord({
|
|
246
270
|
'emit': function (ev) {
|
|
@@ -279,15 +303,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
279
303
|
'sampling': {
|
|
280
304
|
'canvas': 15
|
|
281
305
|
},
|
|
282
|
-
'plugins':
|
|
283
|
-
getRecordConsolePlugin({
|
|
284
|
-
stringifyOptions: {
|
|
285
|
-
stringLengthLimit: 1000,
|
|
286
|
-
numOfKeysLimit: 50,
|
|
287
|
-
depthOfLimit: 2
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
] : []
|
|
306
|
+
'plugins': plugins,
|
|
291
307
|
});
|
|
292
308
|
} catch (err) {
|
|
293
309
|
this.reportError('Unexpected error when starting rrweb recording.', err);
|
|
@@ -402,6 +418,10 @@ SessionRecording.deserialize = function (serializedRecording, options) {
|
|
|
402
418
|
return recording;
|
|
403
419
|
};
|
|
404
420
|
|
|
421
|
+
SessionRecording.prototype._getApiRoute = function () {
|
|
422
|
+
return this.getConfig('api_routes')['record'];
|
|
423
|
+
};
|
|
424
|
+
|
|
405
425
|
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
406
426
|
var onSuccess = function (response, responseBody) {
|
|
407
427
|
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
@@ -421,7 +441,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
421
441
|
});
|
|
422
442
|
}.bind(this);
|
|
423
443
|
var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
|
|
424
|
-
window['fetch'](apiHost + '/' + this.
|
|
444
|
+
window['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
|
|
425
445
|
'method': 'POST',
|
|
426
446
|
'headers': {
|
|
427
447
|
'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* eslint camelcase: "off" */
|
|
2
|
+
import {RECORDER_FILENAME, TARGETING_FILENAME, RECORDER_GLOBAL_NAME} from './config';
|
|
3
|
+
import { _, console, safewrap, safewrapClass } from './utils';
|
|
4
|
+
import { window } from './window';
|
|
5
|
+
import { Promise } from './promise-polyfill';
|
|
6
|
+
import { IDBStorageWrapper, RECORDING_REGISTRY_STORE_NAME } from './storage/indexed-db';
|
|
7
|
+
import { isRecordingExpired } from './recorder/utils';
|
|
8
|
+
import { getTargetingPromise } from './targeting/loader';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* RecorderManager: manages session recording initialization, lifecycle and state
|
|
13
|
+
* @constructor
|
|
14
|
+
*/
|
|
15
|
+
var RecorderManager = function(initOptions) {
|
|
16
|
+
// TODO - Passing in mixpanel instance as it is still needed for recorder creation
|
|
17
|
+
// but ideally we should be able to remove this dependency.
|
|
18
|
+
this.mixpanelInstance = initOptions.mixpanelInstance;
|
|
19
|
+
|
|
20
|
+
this.getMpConfig = initOptions.getConfigFunc;
|
|
21
|
+
this.getTabId = initOptions.getTabIdFunc;
|
|
22
|
+
this.reportError = initOptions.reportErrorFunc;
|
|
23
|
+
this.getDistinctId = initOptions.getDistinctIdFunc;
|
|
24
|
+
this.loadExtraBundle = initOptions.loadExtraBundle;
|
|
25
|
+
this.recorderSrc = initOptions.recorderSrc;
|
|
26
|
+
this.targetingSrc = initOptions.targetingSrc;
|
|
27
|
+
this.libBasePath = initOptions.libBasePath;
|
|
28
|
+
|
|
29
|
+
this._recorder = null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
RecorderManager.prototype.shouldLoadRecorder = function() {
|
|
33
|
+
if (this.getMpConfig('disable_persistence')) {
|
|
34
|
+
console.log('Load recorder check skipped due to disable_persistence config');
|
|
35
|
+
return Promise.resolve(false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
39
|
+
var tab_id = this.getTabId();
|
|
40
|
+
return recording_registry_idb.init()
|
|
41
|
+
.then(function () {
|
|
42
|
+
return recording_registry_idb.getAll();
|
|
43
|
+
})
|
|
44
|
+
.then(function (recordings) {
|
|
45
|
+
for (var i = 0; i < recordings.length; i++) {
|
|
46
|
+
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
47
|
+
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
48
|
+
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
})
|
|
54
|
+
.catch(_.bind(function (err) {
|
|
55
|
+
this.reportError('Error checking recording registry', err);
|
|
56
|
+
return false;
|
|
57
|
+
}, this));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
|
|
61
|
+
if (!window['MutationObserver']) {
|
|
62
|
+
console.critical('Browser does not support MutationObserver; skipping session recording');
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
67
|
+
return new Promise(_.bind(function(resolve) {
|
|
68
|
+
var handleLoadedRecorder = safewrap(_.bind(function() {
|
|
69
|
+
this._recorder = this._recorder || new window[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
|
|
70
|
+
this._recorder['resumeRecording'](startNewIfInactive);
|
|
71
|
+
resolve();
|
|
72
|
+
}, this));
|
|
73
|
+
|
|
74
|
+
if (_.isUndefined(window[RECORDER_GLOBAL_NAME])) {
|
|
75
|
+
var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
|
|
76
|
+
this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
|
|
77
|
+
} else {
|
|
78
|
+
handleLoadedRecorder();
|
|
79
|
+
}
|
|
80
|
+
}, this));
|
|
81
|
+
}, this);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
85
|
+
* 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.
|
|
86
|
+
*/
|
|
87
|
+
var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
|
|
88
|
+
var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
|
|
89
|
+
if (force_start || is_sampled) {
|
|
90
|
+
return loadRecorder(true);
|
|
91
|
+
} else {
|
|
92
|
+
return this.shouldLoadRecorder()
|
|
93
|
+
.then(_.bind(function (shouldLoad) {
|
|
94
|
+
if (shouldLoad) {
|
|
95
|
+
return loadRecorder(false);
|
|
96
|
+
}
|
|
97
|
+
return Promise.resolve();
|
|
98
|
+
}, this));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
RecorderManager.prototype.isRecording = function() {
|
|
103
|
+
// Safety check: ensure isRecording method exists (older CDN builds may not have it)
|
|
104
|
+
if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return this._recorder['isRecording']();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
this.reportError('Error checking if recording is active', e);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
|
|
116
|
+
var isRecording = this.isRecording();
|
|
117
|
+
var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
|
|
118
|
+
|
|
119
|
+
if (!isRecording && recordingTriggerEvents) {
|
|
120
|
+
var trigger = recordingTriggerEvents[event_name];
|
|
121
|
+
if (trigger && typeof trigger['percentage'] === 'number') {
|
|
122
|
+
var newRate = trigger['percentage'];
|
|
123
|
+
var propertyFilters = trigger['property_filters'];
|
|
124
|
+
if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
|
|
125
|
+
var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
|
|
126
|
+
getTargetingPromise(this.loadExtraBundle, targetingSrc)
|
|
127
|
+
.then(function(targeting) {
|
|
128
|
+
try {
|
|
129
|
+
var result = targeting['eventMatchesCriteria'](
|
|
130
|
+
event_name,
|
|
131
|
+
properties,
|
|
132
|
+
{
|
|
133
|
+
'event_name': event_name,
|
|
134
|
+
'property_filters': propertyFilters
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
if (result['matches']) {
|
|
138
|
+
this.checkAndStartSessionRecording(false, newRate);
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.critical('Could not parse recording event trigger properties logic:', err);
|
|
142
|
+
}
|
|
143
|
+
}.bind(this)).catch(function(err) {
|
|
144
|
+
console.critical('Failed to load targeting library:', err);
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
this.checkAndStartSessionRecording(false, newRate);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
RecorderManager.prototype.stopSessionRecording = function() {
|
|
154
|
+
if (this._recorder) {
|
|
155
|
+
return this._recorder['stopRecording']();
|
|
156
|
+
}
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
RecorderManager.prototype.pauseSessionRecording = function() {
|
|
161
|
+
if (this._recorder) {
|
|
162
|
+
return this._recorder['pauseRecording']();
|
|
163
|
+
}
|
|
164
|
+
return Promise.resolve();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
RecorderManager.prototype.resumeSessionRecording = function() {
|
|
168
|
+
if (this._recorder) {
|
|
169
|
+
return this._recorder['resumeRecording']();
|
|
170
|
+
}
|
|
171
|
+
return Promise.resolve();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
RecorderManager.prototype.isRecordingHeatmapData = function() {
|
|
175
|
+
return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
RecorderManager.prototype.getSessionRecordingProperties = function() {
|
|
179
|
+
var props = {};
|
|
180
|
+
var replay_id = this.getSessionReplayId();
|
|
181
|
+
if (replay_id) {
|
|
182
|
+
props['$mp_replay_id'] = replay_id;
|
|
183
|
+
}
|
|
184
|
+
return props;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
RecorderManager.prototype.getSessionReplayUrl = function() {
|
|
188
|
+
var replay_url = null;
|
|
189
|
+
var replay_id = this.getSessionReplayId();
|
|
190
|
+
if (replay_id) {
|
|
191
|
+
var query_params = _.HTTPBuildQuery({
|
|
192
|
+
'replay_id': replay_id,
|
|
193
|
+
'distinct_id': this.getDistinctId(),
|
|
194
|
+
'token': this.getMpConfig('token')
|
|
195
|
+
});
|
|
196
|
+
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
197
|
+
}
|
|
198
|
+
return replay_url;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
RecorderManager.prototype.getSessionReplayId = function() {
|
|
202
|
+
var replay_id = null;
|
|
203
|
+
if (this._recorder) {
|
|
204
|
+
replay_id = this._recorder['replayId'];
|
|
205
|
+
}
|
|
206
|
+
return replay_id || null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// "private" public method to reach into the recorder in test cases
|
|
210
|
+
RecorderManager.prototype.getRecorder = function() {
|
|
211
|
+
return this._recorder;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
safewrapClass(RecorderManager);
|
|
215
|
+
|
|
216
|
+
export { RecorderManager };
|
package/src/request-batcher.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { _ } from '../utils';
|
|
2
|
+
import jsonLogic from 'json-logic-js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if an event matches the given criteria
|
|
6
|
+
* @param {string} eventName - The name of the event being checked
|
|
7
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
8
|
+
* @param {Object} criteria - Criteria to match against, with:
|
|
9
|
+
* - event_name: string - Required event name (case-sensitive match)
|
|
10
|
+
* - property_filters: Object - Optional JsonLogic filters for properties
|
|
11
|
+
* @returns {Object} Result object with:
|
|
12
|
+
* - matches: boolean - Whether the event matches the criteria
|
|
13
|
+
* - error: string|undefined - Error message if evaluation failed
|
|
14
|
+
*/
|
|
15
|
+
var eventMatchesCriteria = function(eventName, properties, criteria) {
|
|
16
|
+
// Check exact event name match (case-sensitive)
|
|
17
|
+
if (eventName !== criteria.event_name) {
|
|
18
|
+
return { matches: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Evaluate property filters using JsonLogic
|
|
22
|
+
var propertyFilters = criteria.property_filters;
|
|
23
|
+
var filtersMatch = true; // default to true if no filters
|
|
24
|
+
|
|
25
|
+
if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
|
|
26
|
+
try {
|
|
27
|
+
// Use properties as-is for case-sensitive matching
|
|
28
|
+
filtersMatch = jsonLogic.apply(propertyFilters, properties || {});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
matches: false,
|
|
32
|
+
error: error.toString()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { matches: filtersMatch };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
eventMatchesCriteria
|
|
42
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { window } from '../window';
|
|
2
|
+
import { TARGETING_GLOBAL_NAME } from '../config';
|
|
3
|
+
import { eventMatchesCriteria } from './event-matcher';
|
|
4
|
+
|
|
5
|
+
// Create targeting library object
|
|
6
|
+
var targetingLibrary = {};
|
|
7
|
+
targetingLibrary['eventMatchesCriteria'] = eventMatchesCriteria;
|
|
8
|
+
|
|
9
|
+
// Set global Promise (use bracket notation to prevent minification)
|
|
10
|
+
// This is the ONE AND ONLY global - matches recorder pattern
|
|
11
|
+
window[TARGETING_GLOBAL_NAME] = Promise.resolve(targetingLibrary);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { window } from '../window';
|
|
2
|
+
import { TARGETING_GLOBAL_NAME } from '../config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the promise-based targeting loader
|
|
6
|
+
* @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
|
|
7
|
+
* @param {string} targetingSrc - URL to targeting bundle
|
|
8
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
9
|
+
*/
|
|
10
|
+
var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
|
|
11
|
+
// Return existing promise if already initialized or loading
|
|
12
|
+
if (window[TARGETING_GLOBAL_NAME] && typeof window[TARGETING_GLOBAL_NAME].then === 'function') {
|
|
13
|
+
return window[TARGETING_GLOBAL_NAME];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Create loading promise and set it as the global immediately
|
|
17
|
+
// This makes minified build behavior consistent with dev/CJS builds
|
|
18
|
+
window[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
|
|
19
|
+
loadExtraBundle(targetingSrc, resolve);
|
|
20
|
+
}).then(function () {
|
|
21
|
+
var p = window[TARGETING_GLOBAL_NAME];
|
|
22
|
+
if (p && typeof p.then === 'function') {
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
throw new Error('targeting failed to load');
|
|
26
|
+
}).catch(function (err) {
|
|
27
|
+
delete window[TARGETING_GLOBAL_NAME];
|
|
28
|
+
throw err;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return window[TARGETING_GLOBAL_NAME];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
getTargetingPromise
|
|
36
|
+
};
|
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
|
|
|
@@ -204,15 +204,8 @@ _.isArray = nativeIsArray || function(obj) {
|
|
|
204
204
|
return toString.call(obj) === '[object Array]';
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
-
// from a comment on http://dbj.org/dbj/?p=286
|
|
208
|
-
// fails on only one very rare and deliberate custom object:
|
|
209
|
-
// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
|
|
210
207
|
_.isFunction = function(f) {
|
|
211
|
-
|
|
212
|
-
return /^\s*\bfunction\b/.test(f);
|
|
213
|
-
} catch (x) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
208
|
+
return typeof f === 'function';
|
|
216
209
|
};
|
|
217
210
|
|
|
218
211
|
_.isArguments = function(obj) {
|
|
@@ -1739,6 +1732,17 @@ var isOnline = function() {
|
|
|
1739
1732
|
|
|
1740
1733
|
var NOOP_FUNC = function () {};
|
|
1741
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
|
+
|
|
1742
1746
|
var JSONStringify = null, JSONParse = null;
|
|
1743
1747
|
if (typeof JSON !== 'undefined') {
|
|
1744
1748
|
JSONStringify = JSON.stringify;
|
|
@@ -1804,6 +1808,7 @@ export {
|
|
|
1804
1808
|
safewrap,
|
|
1805
1809
|
safewrapClass,
|
|
1806
1810
|
slice,
|
|
1811
|
+
urlMatchesRegexList,
|
|
1807
1812
|
userAgent,
|
|
1808
1813
|
windowOpera,
|
|
1809
1814
|
};
|
package/testServer.js
CHANGED
|
@@ -31,10 +31,65 @@ const TEST_SUITES = {
|
|
|
31
31
|
|
|
32
32
|
app.use(cookieParser());
|
|
33
33
|
app.use(logger('dev'));
|
|
34
|
+
app.use(express.json());
|
|
35
|
+
app.use(express.urlencoded({ extended: true }));
|
|
34
36
|
|
|
35
37
|
app.set('views', __dirname + '/tests');
|
|
36
38
|
app.set('view engine', 'pug');
|
|
37
39
|
|
|
40
|
+
// ========================================
|
|
41
|
+
// Test API endpoints for network plugin tests
|
|
42
|
+
// ========================================
|
|
43
|
+
|
|
44
|
+
// Main test endpoint - handles all HTTP methods
|
|
45
|
+
app.all('/api/test', function(req, res) {
|
|
46
|
+
res.json({
|
|
47
|
+
success: true,
|
|
48
|
+
method: req.method,
|
|
49
|
+
headers: req.headers,
|
|
50
|
+
query: req.query,
|
|
51
|
+
body: req.body,
|
|
52
|
+
url: req.originalUrl,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Form submission endpoint
|
|
58
|
+
app.post('/api/test/form', function(req, res) {
|
|
59
|
+
res.json({
|
|
60
|
+
success: true,
|
|
61
|
+
method: 'POST',
|
|
62
|
+
contentType: req.get('Content-Type'),
|
|
63
|
+
formData: req.body,
|
|
64
|
+
timestamp: Date.now()
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Endpoint with custom response headers
|
|
69
|
+
app.get('/api/test/headers', function(req, res) {
|
|
70
|
+
res.set({
|
|
71
|
+
'X-Custom-Header': 'custom-value',
|
|
72
|
+
'X-Request-Id': 'test-request-123'
|
|
73
|
+
});
|
|
74
|
+
res.json({
|
|
75
|
+
success: true,
|
|
76
|
+
message: 'Response includes custom headers'
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Error response endpoint
|
|
81
|
+
app.get('/api/test/error/:status', function(req, res) {
|
|
82
|
+
const status = parseInt(req.params.status, 10) || 500;
|
|
83
|
+
res.status(status).json({ success: false, status: status });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Session recording endpoint (mimics Mixpanel's /record API)
|
|
87
|
+
app.post(/^\/record\/.*/, express.raw({ type: '*/*', limit: '10mb' }), function(req, res) {
|
|
88
|
+
res.json({ code: 200, status: 'OK' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ========================================
|
|
92
|
+
|
|
38
93
|
app.use('/tests', express.static(__dirname + "/tests"));
|
|
39
94
|
app.get('/tests/cookie_included/:cookieName', function(req, res) {
|
|
40
95
|
if (req.cookies && req.cookies[req.params.cookieName]) {
|
/package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js}
RENAMED
|
File without changes
|