@splitsoftware/splitio-commons 1.4.2-rc.0 → 1.4.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGES.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  1.4.2 (June XX, 2022)
2
- - Added `autoRequire` configuration for GoogleAnalyticsToSplit integration (See https://help.split.io/hc/en-us/articles/360040838752#google-tag-manager).
2
+ - Added `sync.enabled` property to SDK configuration to allow synchronize splits and segments only once.
3
3
  - Updated telemetry to submit data even if user consent is not granted.
4
+ - Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while performing its periodic post of impressions.
4
5
 
5
6
  1.4.1 (June 13, 2022)
6
7
  - Bugfixing - Updated submitters logic, to avoid dropping impressions and events that are being tracked while POST request is pending.
@@ -10,24 +10,23 @@ var logNameMapper = 'ga-to-split:mapper';
10
10
  /**
11
11
  * Provides a plugin to use with analytics.js, accounting for the possibility
12
12
  * that the global command queue has been renamed or not yet defined.
13
- * @param window Reference to global object.
14
- * @param pluginName The plugin name identifier.
15
- * @param pluginConstructor The plugin constructor function.
16
- * @param log Logger instance.
17
- * @param autoRequire If true, log error when auto-require script is not detected
13
+ * @param {string} pluginName The plugin name identifier.
14
+ * @param {Function} pluginConstructor The plugin constructor function.
18
15
  */
19
- function providePlugin(window, pluginName, pluginConstructor, log, autoRequire) {
16
+ function providePlugin(pluginName, pluginConstructor) {
20
17
  // get reference to global command queue. Init it if not defined yet.
18
+ // @ts-expect-error
21
19
  var gaAlias = window.GoogleAnalyticsObject || 'ga';
22
20
  window[gaAlias] = window[gaAlias] || function () {
23
- (window[gaAlias].q = window[gaAlias].q || []).push(arguments);
21
+ var args = [];
22
+ for (var _i = 0; _i < arguments.length; _i++) {
23
+ args[_i] = arguments[_i];
24
+ }
25
+ (window[gaAlias].q = window[gaAlias].q || []).push(args);
24
26
  };
25
27
  // provides the plugin for use with analytics.js.
28
+ // @ts-expect-error
26
29
  window[gaAlias]('provide', pluginName, pluginConstructor);
27
- if (autoRequire && (!window[gaAlias].q || window[gaAlias].q.push === [].push)) {
28
- // Expecting spy on ga.q push method but not found
29
- log.error('Auto-require script was expected but not provided.');
30
- }
31
30
  }
32
31
  // Default mapping: object used for building the default mapper from hits to Split events
33
32
  var defaultMapping = {
@@ -247,6 +246,6 @@ function GaToSplit(sdkOptions, params) {
247
246
  return SplitTracker;
248
247
  }());
249
248
  // Register the plugin, even if config is invalid, since, if not provided, it will block `ga` command queue.
250
- providePlugin(window, 'splitTracker', SplitTracker, log, sdkOptions.autoRequire);
249
+ providePlugin('splitTracker', SplitTracker);
251
250
  }
252
251
  exports.GaToSplit = GaToSplit;
@@ -19,11 +19,11 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
19
19
  * SyncManager factory for modular SDK
20
20
  */
21
21
  return function (params) {
22
- var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, telemetryTracker = params.telemetryTracker;
22
+ var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker;
23
23
  /** Polling Manager */
24
24
  var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
25
25
  /** Push Manager */
26
- var pushManager = streamingEnabled && pollingManager && pushManagerFactory ?
26
+ var pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
27
27
  pushManagerFactory(params, pollingManager) :
28
28
  undefined;
29
29
  /** Submitter Manager */
@@ -69,16 +69,25 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
69
69
  running = true;
70
70
  // start syncing splits and segments
71
71
  if (pollingManager) {
72
- if (pushManager) {
73
- // Doesn't call `syncAll` when the syncManager is resuming
72
+ // If synchronization is disabled pushManager and pollingManager should not start
73
+ if (syncEnabled) {
74
+ if (pushManager) {
75
+ // Doesn't call `syncAll` when the syncManager is resuming
76
+ if (startFirstTime) {
77
+ pollingManager.syncAll();
78
+ startFirstTime = false;
79
+ }
80
+ pushManager.start();
81
+ }
82
+ else {
83
+ pollingManager.start();
84
+ }
85
+ }
86
+ else {
74
87
  if (startFirstTime) {
75
88
  pollingManager.syncAll();
76
89
  startFirstTime = false;
77
90
  }
78
- pushManager.start();
79
- }
80
- else {
81
- pollingManager.start();
82
91
  }
83
92
  }
84
93
  // start periodic data recording (events, impressions, telemetry).
@@ -112,22 +121,28 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
112
121
  return {
113
122
  isRunning: mySegmentsSyncTask.isRunning,
114
123
  start: function () {
115
- if (pushManager) {
116
- if (pollingManager.isRunning()) {
117
- // if doing polling, we must start the periodic fetch of data
118
- if (storage.splits.usesSegments())
119
- mySegmentsSyncTask.start();
124
+ if (syncEnabled) {
125
+ if (pushManager) {
126
+ if (pollingManager.isRunning()) {
127
+ // if doing polling, we must start the periodic fetch of data
128
+ if (storage.splits.usesSegments())
129
+ mySegmentsSyncTask.start();
130
+ }
131
+ else {
132
+ // if not polling, we must execute the sync task for the initial fetch
133
+ // of segments since `syncAll` was already executed when starting the main client
134
+ mySegmentsSyncTask.execute();
135
+ }
136
+ pushManager.add(matchingKey, mySegmentsSyncTask);
120
137
  }
121
138
  else {
122
- // if not polling, we must execute the sync task for the initial fetch
123
- // of segments since `syncAll` was already executed when starting the main client
124
- mySegmentsSyncTask.execute();
139
+ if (storage.splits.usesSegments())
140
+ mySegmentsSyncTask.start();
125
141
  }
126
- pushManager.add(matchingKey, mySegmentsSyncTask);
127
142
  }
128
143
  else {
129
- if (storage.splits.usesSegments())
130
- mySegmentsSyncTask.start();
144
+ if (!readinessManager.isReady())
145
+ mySegmentsSyncTask.execute();
131
146
  }
132
147
  },
133
148
  stop: function () {
@@ -4,8 +4,8 @@ exports.syncTaskFactory = void 0;
4
4
  var constants_1 = require("../logger/constants");
5
5
  /**
6
6
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
7
- * The task can be executed once calling the "execute" method.
8
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
7
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
8
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
9
9
  *
10
10
  * @param log Logger instance.
11
11
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -15,8 +15,8 @@ var constants_1 = require("../logger/constants");
15
15
  */
16
16
  function syncTaskFactory(log, task, period, taskName) {
17
17
  if (taskName === void 0) { taskName = 'task'; }
18
- // Flag that indicates if the task is being executed
19
- var executing = false;
18
+ // Task promise while it is pending. Undefined once the promise is resolved
19
+ var pendingTask;
20
20
  // flag that indicates if the task periodic execution has been started/stopped.
21
21
  var running = false;
22
22
  // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently
@@ -30,13 +30,19 @@ function syncTaskFactory(log, task, period, taskName) {
30
30
  for (var _i = 0; _i < arguments.length; _i++) {
31
31
  args[_i] = arguments[_i];
32
32
  }
33
- executing = true;
33
+ // If task is executing, chain the new execution
34
+ if (pendingTask) {
35
+ return pendingTask.then(function () {
36
+ return execute.apply(void 0, args);
37
+ });
38
+ }
39
+ // Execute task
34
40
  log.debug(constants_1.SYNC_TASK_EXECUTE, [taskName]);
35
- return task.apply(void 0, args).then(function (result) {
36
- executing = false;
41
+ pendingTask = task.apply(void 0, args).then(function (result) {
42
+ pendingTask = undefined;
37
43
  return result;
38
44
  });
39
- // No need to handle promise rejection because it is a pre-condition that provided task never rejects.
45
+ return pendingTask;
40
46
  }
41
47
  function periodicExecute(currentRunningId) {
42
48
  return execute.apply(void 0, runningArgs).then(function (result) {
@@ -48,10 +54,9 @@ function syncTaskFactory(log, task, period, taskName) {
48
54
  });
49
55
  }
50
56
  return {
51
- // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming.
52
57
  execute: execute,
53
58
  isExecuting: function () {
54
- return executing;
59
+ return pendingTask !== undefined;
55
60
  },
56
61
  start: function () {
57
62
  var args = [];
@@ -73,7 +73,8 @@ exports.base = {
73
73
  splitFilters: undefined,
74
74
  // impressions collection mode
75
75
  impressionsMode: constants_1.OPTIMIZED,
76
- localhostMode: undefined
76
+ localhostMode: undefined,
77
+ enabled: true
77
78
  },
78
79
  // Logger
79
80
  log: undefined
@@ -167,6 +168,10 @@ function settingsValidation(config, validationParams) {
167
168
  // We are not checking if bases are positive numbers. Thus, we might be reauthenticating immediately (`setTimeout` with NaN or negative number)
168
169
  scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase);
169
170
  }
171
+ // validate sync enabled
172
+ if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop
173
+ withDefaults.sync.enabled = true;
174
+ }
170
175
  // validate the `splitFilters` settings and parse splits query
171
176
  var splitFiltersValidation = (0, splitFilters_1.validateSplitFilters)(log, withDefaults.sync.splitFilters, withDefaults.mode);
172
177
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
@@ -7,24 +7,23 @@ var logNameMapper = 'ga-to-split:mapper';
7
7
  /**
8
8
  * Provides a plugin to use with analytics.js, accounting for the possibility
9
9
  * that the global command queue has been renamed or not yet defined.
10
- * @param window Reference to global object.
11
- * @param pluginName The plugin name identifier.
12
- * @param pluginConstructor The plugin constructor function.
13
- * @param log Logger instance.
14
- * @param autoRequire If true, log error when auto-require script is not detected
10
+ * @param {string} pluginName The plugin name identifier.
11
+ * @param {Function} pluginConstructor The plugin constructor function.
15
12
  */
16
- function providePlugin(window, pluginName, pluginConstructor, log, autoRequire) {
13
+ function providePlugin(pluginName, pluginConstructor) {
17
14
  // get reference to global command queue. Init it if not defined yet.
15
+ // @ts-expect-error
18
16
  var gaAlias = window.GoogleAnalyticsObject || 'ga';
19
17
  window[gaAlias] = window[gaAlias] || function () {
20
- (window[gaAlias].q = window[gaAlias].q || []).push(arguments);
18
+ var args = [];
19
+ for (var _i = 0; _i < arguments.length; _i++) {
20
+ args[_i] = arguments[_i];
21
+ }
22
+ (window[gaAlias].q = window[gaAlias].q || []).push(args);
21
23
  };
22
24
  // provides the plugin for use with analytics.js.
25
+ // @ts-expect-error
23
26
  window[gaAlias]('provide', pluginName, pluginConstructor);
24
- if (autoRequire && (!window[gaAlias].q || window[gaAlias].q.push === [].push)) {
25
- // Expecting spy on ga.q push method but not found
26
- log.error('Auto-require script was expected but not provided.');
27
- }
28
27
  }
29
28
  // Default mapping: object used for building the default mapper from hits to Split events
30
29
  var defaultMapping = {
@@ -241,5 +240,5 @@ export function GaToSplit(sdkOptions, params) {
241
240
  return SplitTracker;
242
241
  }());
243
242
  // Register the plugin, even if config is invalid, since, if not provided, it will block `ga` command queue.
244
- providePlugin(window, 'splitTracker', SplitTracker, log, sdkOptions.autoRequire);
243
+ providePlugin('splitTracker', SplitTracker);
245
244
  }
@@ -16,11 +16,11 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
16
16
  * SyncManager factory for modular SDK
17
17
  */
18
18
  return function (params) {
19
- var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, telemetryTracker = params.telemetryTracker;
19
+ var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker;
20
20
  /** Polling Manager */
21
21
  var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
22
22
  /** Push Manager */
23
- var pushManager = streamingEnabled && pollingManager && pushManagerFactory ?
23
+ var pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
24
24
  pushManagerFactory(params, pollingManager) :
25
25
  undefined;
26
26
  /** Submitter Manager */
@@ -66,16 +66,25 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
66
66
  running = true;
67
67
  // start syncing splits and segments
68
68
  if (pollingManager) {
69
- if (pushManager) {
70
- // Doesn't call `syncAll` when the syncManager is resuming
69
+ // If synchronization is disabled pushManager and pollingManager should not start
70
+ if (syncEnabled) {
71
+ if (pushManager) {
72
+ // Doesn't call `syncAll` when the syncManager is resuming
73
+ if (startFirstTime) {
74
+ pollingManager.syncAll();
75
+ startFirstTime = false;
76
+ }
77
+ pushManager.start();
78
+ }
79
+ else {
80
+ pollingManager.start();
81
+ }
82
+ }
83
+ else {
71
84
  if (startFirstTime) {
72
85
  pollingManager.syncAll();
73
86
  startFirstTime = false;
74
87
  }
75
- pushManager.start();
76
- }
77
- else {
78
- pollingManager.start();
79
88
  }
80
89
  }
81
90
  // start periodic data recording (events, impressions, telemetry).
@@ -109,22 +118,28 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
109
118
  return {
110
119
  isRunning: mySegmentsSyncTask.isRunning,
111
120
  start: function () {
112
- if (pushManager) {
113
- if (pollingManager.isRunning()) {
114
- // if doing polling, we must start the periodic fetch of data
115
- if (storage.splits.usesSegments())
116
- mySegmentsSyncTask.start();
121
+ if (syncEnabled) {
122
+ if (pushManager) {
123
+ if (pollingManager.isRunning()) {
124
+ // if doing polling, we must start the periodic fetch of data
125
+ if (storage.splits.usesSegments())
126
+ mySegmentsSyncTask.start();
127
+ }
128
+ else {
129
+ // if not polling, we must execute the sync task for the initial fetch
130
+ // of segments since `syncAll` was already executed when starting the main client
131
+ mySegmentsSyncTask.execute();
132
+ }
133
+ pushManager.add(matchingKey, mySegmentsSyncTask);
117
134
  }
118
135
  else {
119
- // if not polling, we must execute the sync task for the initial fetch
120
- // of segments since `syncAll` was already executed when starting the main client
121
- mySegmentsSyncTask.execute();
136
+ if (storage.splits.usesSegments())
137
+ mySegmentsSyncTask.start();
122
138
  }
123
- pushManager.add(matchingKey, mySegmentsSyncTask);
124
139
  }
125
140
  else {
126
- if (storage.splits.usesSegments())
127
- mySegmentsSyncTask.start();
141
+ if (!readinessManager.isReady())
142
+ mySegmentsSyncTask.execute();
128
143
  }
129
144
  },
130
145
  stop: function () {
@@ -1,8 +1,8 @@
1
1
  import { SYNC_TASK_EXECUTE, SYNC_TASK_START, SYNC_TASK_STOP } from '../logger/constants';
2
2
  /**
3
3
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
4
- * The task can be executed once calling the "execute" method.
5
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
4
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
5
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
6
6
  *
7
7
  * @param log Logger instance.
8
8
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -12,8 +12,8 @@ import { SYNC_TASK_EXECUTE, SYNC_TASK_START, SYNC_TASK_STOP } from '../logger/co
12
12
  */
13
13
  export function syncTaskFactory(log, task, period, taskName) {
14
14
  if (taskName === void 0) { taskName = 'task'; }
15
- // Flag that indicates if the task is being executed
16
- var executing = false;
15
+ // Task promise while it is pending. Undefined once the promise is resolved
16
+ var pendingTask;
17
17
  // flag that indicates if the task periodic execution has been started/stopped.
18
18
  var running = false;
19
19
  // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently
@@ -27,13 +27,19 @@ export function syncTaskFactory(log, task, period, taskName) {
27
27
  for (var _i = 0; _i < arguments.length; _i++) {
28
28
  args[_i] = arguments[_i];
29
29
  }
30
- executing = true;
30
+ // If task is executing, chain the new execution
31
+ if (pendingTask) {
32
+ return pendingTask.then(function () {
33
+ return execute.apply(void 0, args);
34
+ });
35
+ }
36
+ // Execute task
31
37
  log.debug(SYNC_TASK_EXECUTE, [taskName]);
32
- return task.apply(void 0, args).then(function (result) {
33
- executing = false;
38
+ pendingTask = task.apply(void 0, args).then(function (result) {
39
+ pendingTask = undefined;
34
40
  return result;
35
41
  });
36
- // No need to handle promise rejection because it is a pre-condition that provided task never rejects.
42
+ return pendingTask;
37
43
  }
38
44
  function periodicExecute(currentRunningId) {
39
45
  return execute.apply(void 0, runningArgs).then(function (result) {
@@ -45,10 +51,9 @@ export function syncTaskFactory(log, task, period, taskName) {
45
51
  });
46
52
  }
47
53
  return {
48
- // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming.
49
54
  execute: execute,
50
55
  isExecuting: function () {
51
- return executing;
56
+ return pendingTask !== undefined;
52
57
  },
53
58
  start: function () {
54
59
  var args = [];
@@ -70,7 +70,8 @@ export var base = {
70
70
  splitFilters: undefined,
71
71
  // impressions collection mode
72
72
  impressionsMode: OPTIMIZED,
73
- localhostMode: undefined
73
+ localhostMode: undefined,
74
+ enabled: true
74
75
  },
75
76
  // Logger
76
77
  log: undefined
@@ -164,6 +165,10 @@ export function settingsValidation(config, validationParams) {
164
165
  // We are not checking if bases are positive numbers. Thus, we might be reauthenticating immediately (`setTimeout` with NaN or negative number)
165
166
  scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase);
166
167
  }
168
+ // validate sync enabled
169
+ if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop
170
+ withDefaults.sync.enabled = true;
171
+ }
167
172
  // validate the `splitFilters` settings and parse splits query
168
173
  var splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode);
169
174
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.4.2-rc.0",
3
+ "version": "1.4.2-rc.1",
4
4
  "description": "Split Javascript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -19,26 +19,20 @@ const logNameMapper = 'ga-to-split:mapper';
19
19
  /**
20
20
  * Provides a plugin to use with analytics.js, accounting for the possibility
21
21
  * that the global command queue has been renamed or not yet defined.
22
- * @param window Reference to global object.
23
- * @param pluginName The plugin name identifier.
24
- * @param pluginConstructor The plugin constructor function.
25
- * @param log Logger instance.
26
- * @param autoRequire If true, log error when auto-require script is not detected
22
+ * @param {string} pluginName The plugin name identifier.
23
+ * @param {Function} pluginConstructor The plugin constructor function.
27
24
  */
28
- function providePlugin(window: any, pluginName: string, pluginConstructor: Function, log: ILogger, autoRequire?: boolean) {
25
+ function providePlugin(pluginName: string, pluginConstructor: Function) {
29
26
  // get reference to global command queue. Init it if not defined yet.
27
+ // @ts-expect-error
30
28
  const gaAlias = window.GoogleAnalyticsObject || 'ga';
31
- window[gaAlias] = window[gaAlias] || function () {
32
- (window[gaAlias].q = window[gaAlias].q || []).push(arguments);
29
+ window[gaAlias] = window[gaAlias] || function (...args: any[]) { // @ts-expect-error
30
+ (window[gaAlias].q = window[gaAlias].q || []).push(args);
33
31
  };
34
32
 
35
33
  // provides the plugin for use with analytics.js.
34
+ // @ts-expect-error
36
35
  window[gaAlias]('provide', pluginName, pluginConstructor);
37
-
38
- if (autoRequire && (!window[gaAlias].q || window[gaAlias].q.push === [].push)) {
39
- // Expecting spy on ga.q push method but not found
40
- log.error('Auto-require script was expected but not provided.');
41
- }
42
36
  }
43
37
 
44
38
  // Default mapping: object used for building the default mapper from hits to Split events
@@ -290,5 +284,5 @@ export function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, params: IIn
290
284
  }
291
285
 
292
286
  // Register the plugin, even if config is invalid, since, if not provided, it will block `ga` command queue.
293
- providePlugin(window, 'splitTracker', SplitTracker, log, sdkOptions.autoRequire);
287
+ providePlugin('splitTracker', SplitTracker);
294
288
  }
@@ -53,24 +53,13 @@ export interface GoogleAnalyticsToSplitOptions {
53
53
  * If not provided, events are sent using the key and traffic type provided at SDK config
54
54
  */
55
55
  identities?: Identity[],
56
- /**
57
- * Optional flag to log an error if the `auto-require` script is not detected.
58
- * The auto-require script automatically requires the `splitTracker` plugin for created trackers,
59
- * and should be placed right after your Google Analytics, GTM or gtag.js script tag.
60
- *
61
- * @see {@link https://help.split.io/hc/en-us/articles/360040838752#google-tag-manager}
62
- *
63
- * @property {boolean} autoRequire
64
- * @default false
65
- */
66
- autoRequire?: boolean,
67
56
  }
68
57
 
69
58
  /**
70
59
  * Enable 'Google Analytics to Split' integration, to track Google Analytics hits as Split events.
71
60
  * Used by the browser variant of the isomorphic JS SDK.
72
61
  *
73
- * @see {@link https://help.split.io/hc/en-us/articles/360040838752#google-analytics-to-split}
62
+ * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#google-analytics-to-split}
74
63
  */
75
64
  export interface IGoogleAnalyticsToSplitConfig extends GoogleAnalyticsToSplitOptions {
76
65
  type: 'GOOGLE_ANALYTICS_TO_SPLIT'
@@ -140,7 +129,7 @@ export interface SplitToGoogleAnalyticsOptions {
140
129
  * Enable 'Split to Google Analytics' integration, to track Split impressions and events as Google Analytics hits.
141
130
  * Used by the browser variant of the isomorphic JS SDK.
142
131
  *
143
- * @see {@link https://help.split.io/hc/en-us/articles/360040838752#split-to-google-analytics}
132
+ * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#split-to-google-analytics}
144
133
  */
145
134
  export interface ISplitToGoogleAnalyticsConfig extends SplitToGoogleAnalyticsOptions {
146
135
  type: 'SPLIT_TO_GOOGLE_ANALYTICS'
@@ -5,7 +5,7 @@ import { ILogger } from '../logger/types';
5
5
  import { objectAssign } from '../utils/lang/objectAssign';
6
6
 
7
7
  /**
8
- * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call
8
+ * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call
9
9
  */
10
10
  export function clientAttributesDecoration<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(log: ILogger, client: TClient) {
11
11
 
@@ -52,10 +52,10 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
52
52
  getTreatments: getTreatments,
53
53
  getTreatmentsWithConfig: getTreatmentsWithConfig,
54
54
  track: track,
55
-
55
+
56
56
  /**
57
57
  * Add an attribute to client's in memory attributes storage
58
- *
58
+ *
59
59
  * @param {string} attributeName Attrinute name
60
60
  * @param {string, number, boolean, list} attributeValue Attribute value
61
61
  * @returns {boolean} true if the attribute was stored and false otherways
@@ -70,7 +70,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
70
70
 
71
71
  /**
72
72
  * Returns the attribute with the given key
73
- *
73
+ *
74
74
  * @param {string} attributeName Attribute name
75
75
  * @returns {Object} Attribute with the given key
76
76
  */
@@ -81,7 +81,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
81
81
 
82
82
  /**
83
83
  * Add to client's in memory attributes storage the attributes in 'attributes'
84
- *
84
+ *
85
85
  * @param {Object} attributes Object with attributes to store
86
86
  * @returns true if attributes were stored an false otherways
87
87
  */
@@ -92,7 +92,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
92
92
 
93
93
  /**
94
94
  * Return all the attributes stored in client's in memory attributes storage
95
- *
95
+ *
96
96
  * @returns {Object} returns all the stored attributes
97
97
  */
98
98
  getAttributes(): Record<string, Object> {
@@ -101,8 +101,8 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
101
101
 
102
102
  /**
103
103
  * Removes from client's in memory attributes storage the attribute with the given key
104
- *
105
- * @param {string} attributeName
104
+ *
105
+ * @param {string} attributeName
106
106
  * @returns {boolean} true if attribute was removed and false otherways
107
107
  */
108
108
  removeAttribute(attributeName: string) {
@@ -116,7 +116,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
116
116
  clearAttributes() {
117
117
  return attributeStorage.clear();
118
118
  }
119
-
119
+
120
120
  });
121
121
 
122
122
  }
@@ -28,13 +28,13 @@ export function syncManagerOnlineFactory(
28
28
  */
29
29
  return function (params: ISdkFactoryContextSync): ISyncManagerCS {
30
30
 
31
- const { settings, settings: { log, streamingEnabled }, telemetryTracker } = params;
31
+ const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker } = params;
32
32
 
33
33
  /** Polling Manager */
34
34
  const pollingManager = pollingManagerFactory && pollingManagerFactory(params);
35
35
 
36
36
  /** Push Manager */
37
- const pushManager = streamingEnabled && pollingManager && pushManagerFactory ?
37
+ const pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
38
38
  pushManagerFactory(params, pollingManager) :
39
39
  undefined;
40
40
 
@@ -89,15 +89,24 @@ export function syncManagerOnlineFactory(
89
89
 
90
90
  // start syncing splits and segments
91
91
  if (pollingManager) {
92
- if (pushManager) {
93
- // Doesn't call `syncAll` when the syncManager is resuming
92
+
93
+ // If synchronization is disabled pushManager and pollingManager should not start
94
+ if (syncEnabled) {
95
+ if (pushManager) {
96
+ // Doesn't call `syncAll` when the syncManager is resuming
97
+ if (startFirstTime) {
98
+ pollingManager.syncAll();
99
+ startFirstTime = false;
100
+ }
101
+ pushManager.start();
102
+ } else {
103
+ pollingManager.start();
104
+ }
105
+ } else {
94
106
  if (startFirstTime) {
95
107
  pollingManager.syncAll();
96
108
  startFirstTime = false;
97
109
  }
98
- pushManager.start();
99
- } else {
100
- pollingManager.start();
101
110
  }
102
111
  }
103
112
 
@@ -137,18 +146,22 @@ export function syncManagerOnlineFactory(
137
146
  return {
138
147
  isRunning: mySegmentsSyncTask.isRunning,
139
148
  start() {
140
- if (pushManager) {
141
- if (pollingManager!.isRunning()) {
142
- // if doing polling, we must start the periodic fetch of data
143
- if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
149
+ if (syncEnabled) {
150
+ if (pushManager) {
151
+ if (pollingManager!.isRunning()) {
152
+ // if doing polling, we must start the periodic fetch of data
153
+ if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
154
+ } else {
155
+ // if not polling, we must execute the sync task for the initial fetch
156
+ // of segments since `syncAll` was already executed when starting the main client
157
+ mySegmentsSyncTask.execute();
158
+ }
159
+ pushManager.add(matchingKey, mySegmentsSyncTask);
144
160
  } else {
145
- // if not polling, we must execute the sync task for the initial fetch
146
- // of segments since `syncAll` was already executed when starting the main client
147
- mySegmentsSyncTask.execute();
161
+ if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
148
162
  }
149
- pushManager.add(matchingKey, mySegmentsSyncTask);
150
163
  } else {
151
- if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
164
+ if (!readinessManager.isReady()) mySegmentsSyncTask.execute();
152
165
  }
153
166
  },
154
167
  stop() {
@@ -4,8 +4,8 @@ import { ISyncTask } from './types';
4
4
 
5
5
  /**
6
6
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
7
- * The task can be executed once calling the "execute" method.
8
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
7
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
8
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
9
9
  *
10
10
  * @param log Logger instance.
11
11
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -15,8 +15,8 @@ import { ISyncTask } from './types';
15
15
  */
16
16
  export function syncTaskFactory<Input extends any[], Output = any>(log: ILogger, task: (...args: Input) => Promise<Output>, period: number, taskName = 'task'): ISyncTask<Input, Output> {
17
17
 
18
- // Flag that indicates if the task is being executed
19
- let executing = false;
18
+ // Task promise while it is pending. Undefined once the promise is resolved
19
+ let pendingTask: Promise<Output> | undefined;
20
20
  // flag that indicates if the task periodic execution has been started/stopped.
21
21
  let running = false;
22
22
  // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently
@@ -26,14 +26,21 @@ export function syncTaskFactory<Input extends any[], Output = any>(log: ILogger,
26
26
  // Id of the periodic call timeout
27
27
  let timeoutID: any;
28
28
 
29
- function execute(...args: Input) {
30
- executing = true;
29
+ function execute(...args: Input): Promise<Output> {
30
+ // If task is executing, chain the new execution
31
+ if (pendingTask) {
32
+ return pendingTask.then(() => {
33
+ return execute(...args);
34
+ });
35
+ }
36
+
37
+ // Execute task
31
38
  log.debug(SYNC_TASK_EXECUTE, [taskName]);
32
- return task(...args).then(result => {
33
- executing = false;
39
+ pendingTask = task(...args).then(result => {
40
+ pendingTask = undefined;
34
41
  return result;
35
42
  });
36
- // No need to handle promise rejection because it is a pre-condition that provided task never rejects.
43
+ return pendingTask;
37
44
  }
38
45
 
39
46
  function periodicExecute(currentRunningId: number) {
@@ -46,11 +53,10 @@ export function syncTaskFactory<Input extends any[], Output = any>(log: ILogger,
46
53
  }
47
54
 
48
55
  return {
49
- // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming.
50
56
  execute,
51
57
 
52
58
  isExecuting() {
53
- return executing;
59
+ return pendingTask !== undefined;
54
60
  },
55
61
 
56
62
  start(...args: Input) {
package/src/types.ts CHANGED
@@ -117,7 +117,8 @@ export interface ISettings {
117
117
  splitFilters: SplitIO.SplitFilter[],
118
118
  impressionsMode: SplitIO.ImpressionsMode,
119
119
  __splitFiltersValidation: ISplitFiltersValidation,
120
- localhostMode?: SplitIO.LocalhostFactory
120
+ localhostMode?: SplitIO.LocalhostFactory,
121
+ enabled: boolean
121
122
  },
122
123
  readonly runtime: {
123
124
  ip: string | false
@@ -214,6 +215,11 @@ interface ISharedSettings {
214
215
  * @default 'OPTIMIZED'
215
216
  */
216
217
  impressionsMode?: SplitIO.ImpressionsMode,
218
+ /**
219
+ * Enables synchronization.
220
+ * @property {boolean} enabled
221
+ */
222
+ enabled: boolean
217
223
  }
218
224
  }
219
225
  /**
@@ -83,7 +83,8 @@ export const base = {
83
83
  splitFilters: undefined,
84
84
  // impressions collection mode
85
85
  impressionsMode: OPTIMIZED,
86
- localhostMode: undefined
86
+ localhostMode: undefined,
87
+ enabled: true
87
88
  },
88
89
 
89
90
  // Logger
@@ -191,6 +192,11 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
191
192
  scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase);
192
193
  }
193
194
 
195
+ // validate sync enabled
196
+ if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop
197
+ withDefaults.sync.enabled = true;
198
+ }
199
+
194
200
  // validate the `splitFilters` settings and parse splits query
195
201
  const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode);
196
202
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
@@ -52,23 +52,12 @@ export interface GoogleAnalyticsToSplitOptions {
52
52
  * If not provided, events are sent using the key and traffic type provided at SDK config
53
53
  */
54
54
  identities?: Identity[];
55
- /**
56
- * Optional flag to log an error if the `auto-require` script is not detected.
57
- * The auto-require script automatically requires the `splitTracker` plugin for created trackers,
58
- * and should be placed right after your Google Analytics, GTM or gtag.js script tag.
59
- *
60
- * @see {@link https://help.split.io/hc/en-us/articles/360040838752#google-tag-manager}
61
- *
62
- * @property {boolean} autoRequire
63
- * @default false
64
- */
65
- autoRequire?: boolean;
66
55
  }
67
56
  /**
68
57
  * Enable 'Google Analytics to Split' integration, to track Google Analytics hits as Split events.
69
58
  * Used by the browser variant of the isomorphic JS SDK.
70
59
  *
71
- * @see {@link https://help.split.io/hc/en-us/articles/360040838752#google-analytics-to-split}
60
+ * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#google-analytics-to-split}
72
61
  */
73
62
  export interface IGoogleAnalyticsToSplitConfig extends GoogleAnalyticsToSplitOptions {
74
63
  type: 'GOOGLE_ANALYTICS_TO_SPLIT';
@@ -136,7 +125,7 @@ export interface SplitToGoogleAnalyticsOptions {
136
125
  * Enable 'Split to Google Analytics' integration, to track Split impressions and events as Google Analytics hits.
137
126
  * Used by the browser variant of the isomorphic JS SDK.
138
127
  *
139
- * @see {@link https://help.split.io/hc/en-us/articles/360040838752#split-to-google-analytics}
128
+ * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#split-to-google-analytics}
140
129
  */
141
130
  export interface ISplitToGoogleAnalyticsConfig extends SplitToGoogleAnalyticsOptions {
142
131
  type: 'SPLIT_TO_GOOGLE_ANALYTICS';
@@ -2,8 +2,8 @@ import { ILogger } from '../logger/types';
2
2
  import { ISyncTask } from './types';
3
3
  /**
4
4
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
5
- * The task can be executed once calling the "execute" method.
6
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
5
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
6
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
7
7
  *
8
8
  * @param log Logger instance.
9
9
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
package/types/types.d.ts CHANGED
@@ -112,6 +112,7 @@ export interface ISettings {
112
112
  impressionsMode: SplitIO.ImpressionsMode;
113
113
  __splitFiltersValidation: ISplitFiltersValidation;
114
114
  localhostMode?: SplitIO.LocalhostFactory;
115
+ enabled: boolean;
115
116
  };
116
117
  readonly runtime: {
117
118
  ip: string | false;
@@ -208,6 +209,11 @@ interface ISharedSettings {
208
209
  * @default 'OPTIMIZED'
209
210
  */
210
211
  impressionsMode?: SplitIO.ImpressionsMode;
212
+ /**
213
+ * Enables synchronization.
214
+ * @property {boolean} enabled
215
+ */
216
+ enabled: boolean;
211
217
  };
212
218
  }
213
219
  /**
@@ -37,6 +37,7 @@ export declare const base: {
37
37
  splitFilters: undefined;
38
38
  impressionsMode: string;
39
39
  localhostMode: undefined;
40
+ enabled: boolean;
40
41
  };
41
42
  log: undefined;
42
43
  };
@@ -1,34 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.autoRequire = void 0;
4
- /* eslint-disable no-undef */
5
- /**
6
- * Auto-require script to use with GaToSplit integration
7
- */
8
- function autoRequire() {
9
- (function (i, r, s) {
10
- i[s] = i[s] || r;
11
- i[r] = i[r] || function () { i[r].q.push(arguments); };
12
- i[r].q = i[r].q || [];
13
- var ts = {}; // Tracker names
14
- var o = i[r].q.push;
15
- i[r].q.push = function (v) {
16
- var result = o.apply(this, arguments);
17
- if (v && v[0] === 'create') {
18
- var t = typeof v[2] === 'object' && typeof v[2].name === 'string' ?
19
- v[2].name : // `ga('create', 'UA-ID', { name: 'trackerName', ... })`
20
- typeof v[3] === 'object' && typeof v[3].name === 'string' ?
21
- v[3].name : // `ga('create', 'UA-ID', 'auto', { name: 'trackerName', ... })`
22
- typeof v[3] === 'string' ?
23
- v[3] : // `ga('create', 'UA-ID', 'auto', 'trackerName')`
24
- undefined; // No name tracker, e.g.: `ga('create', 'UA-ID', 'auto')`
25
- if (!ts[t]) {
26
- ts[t] = true;
27
- i[r](t ? t + '.require' : 'require', 'splitTracker'); // Auto-require
28
- }
29
- }
30
- return result;
31
- };
32
- })(window, 'ga', 'GoogleAnalyticsObject');
33
- }
34
- exports.autoRequire = autoRequire;
@@ -1,30 +0,0 @@
1
- /* eslint-disable no-undef */
2
- /**
3
- * Auto-require script to use with GaToSplit integration
4
- */
5
- export function autoRequire() {
6
- (function (i, r, s) {
7
- i[s] = i[s] || r;
8
- i[r] = i[r] || function () { i[r].q.push(arguments); };
9
- i[r].q = i[r].q || [];
10
- var ts = {}; // Tracker names
11
- var o = i[r].q.push;
12
- i[r].q.push = function (v) {
13
- var result = o.apply(this, arguments);
14
- if (v && v[0] === 'create') {
15
- var t = typeof v[2] === 'object' && typeof v[2].name === 'string' ?
16
- v[2].name : // `ga('create', 'UA-ID', { name: 'trackerName', ... })`
17
- typeof v[3] === 'object' && typeof v[3].name === 'string' ?
18
- v[3].name : // `ga('create', 'UA-ID', 'auto', { name: 'trackerName', ... })`
19
- typeof v[3] === 'string' ?
20
- v[3] : // `ga('create', 'UA-ID', 'auto', 'trackerName')`
21
- undefined; // No name tracker, e.g.: `ga('create', 'UA-ID', 'auto')`
22
- if (!ts[t]) {
23
- ts[t] = true;
24
- i[r](t ? t + '.require' : 'require', 'splitTracker'); // Auto-require
25
- }
26
- }
27
- return result;
28
- };
29
- })(window, 'ga', 'GoogleAnalyticsObject');
30
- }
@@ -1,35 +0,0 @@
1
- /* eslint-disable no-undef */
2
- /**
3
- * Auto-require script to use with GaToSplit integration
4
- */
5
- export function autoRequire() {
6
- (function (i: any, r: string, s: string) {
7
- i[s] = i[s] || r;
8
- i[r] = i[r] || function () { i[r].q.push(arguments); };
9
- i[r].q = i[r].q || [];
10
-
11
- var ts: any = {}; // Tracker names
12
- var o = i[r].q.push;
13
- i[r].q.push = function (v: any) {
14
- var result = o.apply(this, arguments);
15
-
16
- if (v && v[0] === 'create') {
17
- var t = typeof v[2] === 'object' && typeof v[2].name === 'string' ?
18
- v[2].name : // `ga('create', 'UA-ID', { name: 'trackerName', ... })`
19
- typeof v[3] === 'object' && typeof v[3].name === 'string' ?
20
- v[3].name : // `ga('create', 'UA-ID', 'auto', { name: 'trackerName', ... })`
21
- typeof v[3] === 'string' ?
22
- v[3] : // `ga('create', 'UA-ID', 'auto', 'trackerName')`
23
- undefined; // No name tracker, e.g.: `ga('create', 'UA-ID', 'auto')`
24
-
25
- if (!ts[t]) {
26
- ts[t] = true;
27
- i[r](t ? t + '.require' : 'require', 'splitTracker'); // Auto-require
28
- }
29
- }
30
-
31
- return result;
32
- };
33
-
34
- })(window, 'ga', 'GoogleAnalyticsObject');
35
- }