mixpanel-browser 2.75.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.
Files changed (57) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/workflows/integration-tests.yml +2 -2
  3. package/.github/workflows/unit-tests.yml +2 -2
  4. package/CHANGELOG.md +10 -0
  5. package/build.sh +10 -8
  6. package/dist/async-modules/mixpanel-recorder-bIS4LMGd.js +23595 -0
  7. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +2 -0
  8. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +1 -0
  9. package/dist/async-modules/mixpanel-targeting-BcAPS-Mz.js +2520 -0
  10. package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js +2 -0
  11. package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js.map +1 -0
  12. package/dist/mixpanel-core.cjs.d.ts +68 -0
  13. package/dist/mixpanel-core.cjs.js +550 -383
  14. package/dist/mixpanel-recorder.js +708 -32
  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 +6 -62
  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 +68 -0
  21. package/dist/mixpanel-with-async-modules.cjs.js +550 -383
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
  23. package/dist/mixpanel-with-async-recorder.cjs.js +550 -383
  24. package/dist/mixpanel-with-recorder.d.ts +68 -0
  25. package/dist/mixpanel-with-recorder.js +1036 -197
  26. package/dist/mixpanel-with-recorder.min.d.ts +68 -0
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +68 -0
  29. package/dist/mixpanel.amd.js +1038 -251
  30. package/dist/mixpanel.cjs.d.ts +68 -0
  31. package/dist/mixpanel.cjs.js +1038 -251
  32. package/dist/mixpanel.globals.js +550 -383
  33. package/dist/mixpanel.min.js +184 -181
  34. package/dist/mixpanel.module.d.ts +68 -0
  35. package/dist/mixpanel.module.js +1038 -251
  36. package/dist/mixpanel.umd.d.ts +68 -0
  37. package/dist/mixpanel.umd.js +1038 -251
  38. package/logo.svg +5 -0
  39. package/package.json +2 -1
  40. package/rollup.config.mjs +163 -46
  41. package/src/autocapture/index.js +10 -27
  42. package/src/config.js +9 -3
  43. package/src/flags/index.js +1 -2
  44. package/src/index.d.ts +68 -0
  45. package/src/mixpanel-core.js +76 -111
  46. package/src/recorder/index.js +1 -1
  47. package/src/recorder/recorder.js +5 -1
  48. package/src/recorder/rrweb-network-plugin.js +649 -0
  49. package/src/recorder/session-recording.js +31 -11
  50. package/src/recorder-manager.js +216 -0
  51. package/src/request-batcher.js +1 -1
  52. package/src/targeting/event-matcher.js +2 -57
  53. package/src/targeting/index.js +1 -1
  54. package/src/targeting/loader.js +1 -1
  55. package/src/utils.js +13 -1
  56. package/testServer.js +55 -0
  57. package/src/globals.js +0 -14
@@ -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': this.getConfig('record_console') ? [
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.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
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 };
@@ -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
@@ -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/globals.js DELETED
@@ -1,14 +0,0 @@
1
- /**
2
- * Shared global window property names used across modules
3
- */
4
-
5
- // Targeting library global (used by flags and targeting modules)
6
- var TARGETING_GLOBAL_NAME = '__mp_targeting';
7
-
8
- // Recorder library global (used by recorder and mixpanel-core)
9
- var RECORDER_GLOBAL_NAME = '__mp_recorder';
10
-
11
- export {
12
- TARGETING_GLOBAL_NAME,
13
- RECORDER_GLOBAL_NAME
14
- };