@splitsoftware/splitio-commons 1.2.1-rc.4 → 1.2.1-rc.7

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 (81) hide show
  1. package/cjs/listeners/browser.js +14 -10
  2. package/cjs/logger/constants.js +6 -3
  3. package/cjs/logger/messages/debug.js +3 -3
  4. package/cjs/logger/messages/error.js +3 -2
  5. package/cjs/logger/messages/info.js +4 -2
  6. package/cjs/sdkClient/client.js +10 -4
  7. package/cjs/sdkFactory/index.js +5 -4
  8. package/cjs/sdkFactory/userConsentProps.js +37 -0
  9. package/cjs/storages/KeyBuilder.js +1 -5
  10. package/cjs/storages/KeyBuilderCS.js +11 -1
  11. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +23 -3
  12. package/cjs/sync/streaming/SSEClient/index.js +2 -1
  13. package/cjs/sync/submitters/eventsSyncTask.js +14 -3
  14. package/cjs/sync/submitters/impressionsSyncTask.js +6 -2
  15. package/cjs/sync/syncManagerOnline.js +11 -7
  16. package/cjs/utils/consent.js +10 -0
  17. package/cjs/utils/constants/index.js +5 -1
  18. package/cjs/utils/lang/index.js +8 -1
  19. package/cjs/utils/settingsValidation/consent.js +16 -0
  20. package/cjs/utils/settingsValidation/impressionsMode.js +6 -6
  21. package/cjs/utils/settingsValidation/index.js +4 -1
  22. package/esm/listeners/browser.js +14 -10
  23. package/esm/logger/constants.js +4 -1
  24. package/esm/logger/messages/debug.js +3 -3
  25. package/esm/logger/messages/error.js +3 -2
  26. package/esm/logger/messages/info.js +4 -2
  27. package/esm/sdkClient/client.js +11 -5
  28. package/esm/sdkFactory/index.js +5 -4
  29. package/esm/sdkFactory/userConsentProps.js +33 -0
  30. package/esm/storages/KeyBuilder.js +2 -6
  31. package/esm/storages/KeyBuilderCS.js +11 -1
  32. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +23 -3
  33. package/esm/sync/streaming/SSEClient/index.js +2 -1
  34. package/esm/sync/submitters/eventsSyncTask.js +14 -3
  35. package/esm/sync/submitters/impressionsSyncTask.js +6 -2
  36. package/esm/sync/syncManagerOnline.js +11 -7
  37. package/esm/utils/consent.js +6 -0
  38. package/esm/utils/constants/index.js +4 -0
  39. package/esm/utils/lang/index.js +6 -0
  40. package/esm/utils/settingsValidation/consent.js +12 -0
  41. package/esm/utils/settingsValidation/impressionsMode.js +7 -7
  42. package/esm/utils/settingsValidation/index.js +4 -1
  43. package/package.json +1 -1
  44. package/src/listeners/browser.ts +13 -9
  45. package/src/logger/constants.ts +4 -1
  46. package/src/logger/messages/debug.ts +3 -3
  47. package/src/logger/messages/error.ts +3 -2
  48. package/src/logger/messages/info.ts +4 -2
  49. package/src/sdkClient/client.ts +7 -5
  50. package/src/sdkFactory/index.ts +5 -4
  51. package/src/sdkFactory/types.ts +2 -0
  52. package/src/sdkFactory/userConsentProps.ts +40 -0
  53. package/src/storages/KeyBuilder.ts +2 -6
  54. package/src/storages/KeyBuilderCS.ts +13 -1
  55. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +23 -3
  56. package/src/sync/streaming/SSEClient/index.ts +2 -1
  57. package/src/sync/submitters/eventsSyncTask.ts +14 -3
  58. package/src/sync/submitters/impressionsSyncTask.ts +6 -2
  59. package/src/sync/syncManagerOnline.ts +13 -7
  60. package/src/sync/types.ts +4 -1
  61. package/src/types.ts +6 -0
  62. package/src/utils/consent.ts +8 -0
  63. package/src/utils/constants/index.ts +5 -0
  64. package/src/utils/lang/index.ts +8 -1
  65. package/src/utils/settingsValidation/consent.ts +16 -0
  66. package/src/utils/settingsValidation/impressionsMode.ts +8 -8
  67. package/src/utils/settingsValidation/index.ts +5 -1
  68. package/src/utils/settingsValidation/types.ts +2 -0
  69. package/types/logger/constants.d.ts +4 -1
  70. package/types/sdkFactory/types.d.ts +1 -0
  71. package/types/sdkFactory/userConsentProps.d.ts +6 -0
  72. package/types/storages/KeyBuilderCS.d.ts +2 -0
  73. package/types/sync/types.d.ts +3 -0
  74. package/types/types.d.ts +6 -0
  75. package/types/utils/consent.d.ts +2 -0
  76. package/types/utils/constants/index.d.ts +3 -0
  77. package/types/utils/lang/index.d.ts +4 -0
  78. package/types/utils/settingsValidation/consent.d.ts +6 -0
  79. package/types/utils/settingsValidation/impressionsMode.d.ts +1 -1
  80. package/types/utils/settingsValidation/types.d.ts +2 -0
  81. package/types/utils/settingsValidation/userConsent.d.ts +5 -0
@@ -67,6 +67,8 @@ export var SYNC_CONTINUE_POLLING = 118;
67
67
  export var SYNC_STOP_POLLING = 119;
68
68
  export var EVENTS_TRACKER_SUCCESS = 120;
69
69
  export var IMPRESSIONS_TRACKER_SUCCESS = 121;
70
+ export var USER_CONSENT_UPDATED = 122;
71
+ export var USER_CONSENT_NOT_UPDATED = 123;
70
72
  export var ENGINE_VALUE_INVALID = 200;
71
73
  export var ENGINE_VALUE_NO_ATTRIBUTES = 201;
72
74
  export var CLIENT_NO_LISTENER = 202;
@@ -112,10 +114,11 @@ export var ERROR_INVALID_KEY_OBJECT = 317;
112
114
  export var ERROR_INVALID = 318;
113
115
  export var ERROR_EMPTY = 319;
114
116
  export var ERROR_EMPTY_ARRAY = 320;
115
- export var ERROR_INVALID_IMPRESSIONS_MODE = 321;
117
+ export var ERROR_INVALID_CONFIG_PARAM = 321;
116
118
  export var ERROR_HTTP = 322;
117
119
  export var ERROR_LOCALHOST_MODULE_REQUIRED = 323;
118
120
  export var ERROR_STORAGE_INVALID = 324;
121
+ export var ERROR_NOT_BOOLEAN = 325;
119
122
  // Log prefixes (a.k.a. tags or categories)
120
123
  export var LOG_PREFIX_SETTINGS = 'settings';
121
124
  export var LOG_PREFIX_INSTANTIATION = 'Factory instantiation';
@@ -30,9 +30,9 @@ export var codesDebug = codesInfo.concat([
30
30
  // SDK
31
31
  [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'],
32
32
  [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'],
33
- [c.RETRIEVE_CLIENT_DEFAULT, ' Retrieving default SDK client.'],
34
- [c.RETRIEVE_CLIENT_EXISTING, ' Retrieving existing SDK client.'],
35
- [c.RETRIEVE_MANAGER, ' Retrieving manager instance.'],
33
+ [c.RETRIEVE_CLIENT_DEFAULT, 'Retrieving default SDK client.'],
34
+ [c.RETRIEVE_CLIENT_EXISTING, 'Retrieving existing SDK client.'],
35
+ [c.RETRIEVE_MANAGER, 'Retrieving manager instance.'],
36
36
  // synchronizer
37
37
  [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Splits data: \n%s'],
38
38
  [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up split update using since = %s'],
@@ -4,7 +4,7 @@ export var codesError = [
4
4
  [c.ERROR_ENGINE_COMBINER_IFELSEIF, c.LOG_PREFIX_ENGINE_COMBINER + 'Invalid Split, no valid rules found'],
5
5
  // SDK
6
6
  [c.ERROR_LOGLEVEL_INVALID, 'logger: Invalid Log Level - No changes to the logs will be applied.'],
7
- [c.ERROR_CLIENT_CANNOT_GET_READY, ' The SDK will not get ready. Reason: %s'],
7
+ [c.ERROR_CLIENT_CANNOT_GET_READY, 'The SDK will not get ready. Reason: %s'],
8
8
  [c.ERROR_IMPRESSIONS_TRACKER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Could not store impressions bulk with %s impression(s). Error: %s'],
9
9
  [c.ERROR_IMPRESSIONS_LISTENER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Impression listener logImpression method threw: %s.'],
10
10
  [c.ERROR_EVENTS_TRACKER, c.LOG_PREFIX_EVENTS_TRACKER + 'Failed to queue %s'],
@@ -27,8 +27,9 @@ export var codesError = [
27
27
  [c.ERROR_INVALID, '%s: you passed an invalid %s. It must be a non-empty string.'],
28
28
  [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'],
29
29
  [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
30
+ [c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'],
30
31
  // initialization / settings validation
31
- [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'],
32
+ [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'],
32
33
  [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'],
33
34
  [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS + ': The provided storage is invalid.%s Fallbacking into default MEMORY storage'],
34
35
  ];
@@ -8,10 +8,12 @@ export var codesInfo = codesWarn.concat([
8
8
  // SDK
9
9
  [c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Split: %s. Key: %s. Evaluation: %s. Label: %s'],
10
10
  [c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Queueing corresponding impression.'],
11
- [c.NEW_SHARED_CLIENT, ' New shared client instance created.'],
12
- [c.NEW_FACTORY, ' New Split SDK instance created.'],
11
+ [c.NEW_SHARED_CLIENT, 'New shared client instance created.'],
12
+ [c.NEW_FACTORY, 'New Split SDK instance created.'],
13
13
  [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'],
14
14
  [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'],
15
+ [c.USER_CONSENT_UPDATED, 'setUserConsent: consent status changed from %s to %s.'],
16
+ [c.USER_CONSENT_NOT_UPDATED, 'setUserConsent: call had no effect because it was the current consent status (%s).'],
15
17
  // synchronizer
16
18
  [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'],
17
19
  [c.POLLING_START, c.LOG_PREFIX_SYNC_POLLING + 'Starting polling'],
@@ -4,20 +4,22 @@ import { getMatching, getBucketing } from '../utils/key';
4
4
  import { validateSplitExistance } from '../utils/inputValidation/splitExistance';
5
5
  import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance';
6
6
  import { SDK_NOT_READY } from '../utils/labels';
7
- import { CONTROL } from '../utils/constants';
7
+ import { CONSENT_DECLINED, CONTROL } from '../utils/constants';
8
8
  import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants';
9
9
  /**
10
10
  * Creator of base client with getTreatments and track methods.
11
11
  */
12
12
  // @TODO missing time tracking to collect telemetry
13
13
  export function clientFactory(params) {
14
- var readinessManager = params.sdkReadinessManager.readinessManager, storage = params.storage, _a = params.settings, log = _a.log, mode = _a.mode, impressionsTracker = params.impressionsTracker, eventTracker = params.eventTracker;
14
+ var readinessManager = params.sdkReadinessManager.readinessManager, storage = params.storage, settings = params.settings, impressionsTracker = params.impressionsTracker, eventTracker = params.eventTracker;
15
+ var log = settings.log, mode = settings.mode;
15
16
  function getTreatment(key, splitName, attributes, withConfig) {
16
17
  if (withConfig === void 0) { withConfig = false; }
17
18
  var wrapUp = function (evaluationResult) {
18
19
  var queue = [];
19
20
  var treatment = processEvaluation(evaluationResult, splitName, key, attributes, withConfig, "getTreatment" + (withConfig ? 'withConfig' : ''), queue);
20
- impressionsTracker.track(queue, attributes);
21
+ if (settings.userConsent !== CONSENT_DECLINED)
22
+ impressionsTracker.track(queue, attributes);
21
23
  return treatment;
22
24
  };
23
25
  var evaluation = evaluateFeature(log, key, splitName, attributes, storage);
@@ -34,7 +36,8 @@ export function clientFactory(params) {
34
36
  Object.keys(evaluationResults).forEach(function (splitName) {
35
37
  treatments[splitName] = processEvaluation(evaluationResults[splitName], splitName, key, attributes, withConfig, "getTreatments" + (withConfig ? 'withConfig' : ''), queue);
36
38
  });
37
- impressionsTracker.track(queue, attributes);
39
+ if (settings.userConsent !== CONSENT_DECLINED)
40
+ impressionsTracker.track(queue, attributes);
38
41
  return treatments;
39
42
  };
40
43
  var evaluations = evaluateFeatures(log, key, splitNames, attributes, storage);
@@ -88,7 +91,10 @@ export function clientFactory(params) {
88
91
  };
89
92
  // This may be async but we only warn, we don't actually care if it is valid or not in terms of queueing the event.
90
93
  validateTrafficTypeExistance(log, readinessManager, storage.splits, mode, trafficTypeName, 'track');
91
- return eventTracker.track(eventData, size);
94
+ if (settings.userConsent !== CONSENT_DECLINED)
95
+ return eventTracker.track(eventData, size);
96
+ else
97
+ return false;
92
98
  }
93
99
  return {
94
100
  getTreatment: getTreatment,
@@ -8,11 +8,12 @@ import { createLoggerAPI } from '../logger/sdkLogger';
8
8
  import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants';
9
9
  import { metadataBuilder } from '../storages/metadataBuilder';
10
10
  import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
11
+ import { objectAssign } from '../utils/lang/objectAssign';
11
12
  /**
12
13
  * Modular SDK factory
13
14
  */
14
15
  export function sdkFactory(params) {
15
- var settings = params.settings, platform = params.platform, storageFactory = params.storageFactory, splitApiFactory = params.splitApiFactory, syncManagerFactory = params.syncManagerFactory, SignalListener = params.SignalListener, impressionsObserverFactory = params.impressionsObserverFactory, impressionListener = params.impressionListener, integrationsManagerFactory = params.integrationsManagerFactory, sdkManagerFactory = params.sdkManagerFactory, sdkClientMethodFactory = params.sdkClientMethodFactory;
16
+ var settings = params.settings, platform = params.platform, storageFactory = params.storageFactory, splitApiFactory = params.splitApiFactory, extraProps = params.extraProps, syncManagerFactory = params.syncManagerFactory, SignalListener = params.SignalListener, impressionsObserverFactory = params.impressionsObserverFactory, impressionListener = params.impressionListener, integrationsManagerFactory = params.integrationsManagerFactory, sdkManagerFactory = params.sdkManagerFactory, sdkClientMethodFactory = params.sdkClientMethodFactory;
16
17
  var log = settings.log;
17
18
  // @TODO handle non-recoverable errors: not start sync, mark the SDK as destroyed, etc.
18
19
  // We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle.
@@ -65,11 +66,11 @@ export function sdkFactory(params) {
65
66
  syncManager && syncManager.start();
66
67
  signalListener && signalListener.start();
67
68
  log.info(NEW_FACTORY);
68
- return {
69
+ // @ts-ignore
70
+ return objectAssign({
69
71
  // Split evaluation and event tracking engine
70
72
  client: clientMethod,
71
73
  // Manager API to explore available information
72
- // @ts-ignore
73
74
  manager: function () {
74
75
  log.debug(RETRIEVE_MANAGER);
75
76
  return managerInstance;
@@ -77,5 +78,5 @@ export function sdkFactory(params) {
77
78
  // Logger wrapper API
78
79
  Logger: createLoggerAPI(settings.log),
79
80
  settings: settings,
80
- };
81
+ }, extraProps && extraProps(settings, syncManager));
81
82
  }
@@ -0,0 +1,33 @@
1
+ import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED } from '../logger/constants';
2
+ import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants';
3
+ import { isBoolean } from '../utils/lang';
4
+ // Extend client-side factory instances with user consent getter/setter
5
+ export function userConsentProps(settings, syncManager) {
6
+ var log = settings.log;
7
+ return {
8
+ setUserConsent: function (consent) {
9
+ var _a, _b;
10
+ // validate input param
11
+ if (!isBoolean(consent)) {
12
+ log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']);
13
+ return false;
14
+ }
15
+ var newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED;
16
+ if (settings.userConsent !== newConsentStatus) { // @ts-ignore, modify readonly prop
17
+ settings.userConsent = newConsentStatus;
18
+ if (consent)
19
+ (_a = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitter) === null || _a === void 0 ? void 0 : _a.start(); // resumes submitters if transitioning to GRANTED
20
+ else
21
+ (_b = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitter) === null || _b === void 0 ? void 0 : _b.stop(); // pauses submitters if transitioning to DECLINED
22
+ log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]);
23
+ }
24
+ else {
25
+ log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]);
26
+ }
27
+ return true;
28
+ },
29
+ getUserConsent: function () {
30
+ return settings.userConsent;
31
+ }
32
+ };
33
+ }
@@ -1,12 +1,8 @@
1
- import { endsWith, startsWith } from '../utils/lang';
1
+ import { startsWith } from '../utils/lang';
2
2
  var everythingAtTheEnd = /[^.]+$/;
3
3
  var DEFAULT_PREFIX = 'SPLITIO';
4
4
  export function validatePrefix(prefix) {
5
- return prefix && typeof prefix === 'string' ?
6
- endsWith(prefix, '.' + DEFAULT_PREFIX) ?
7
- prefix : // suffix already appended
8
- prefix + '.' + DEFAULT_PREFIX : // append suffix
9
- DEFAULT_PREFIX; // use default prefix if none is provided
5
+ return prefix ? prefix + '.SPLITIO' : 'SPLITIO';
10
6
  }
11
7
  var KeyBuilder = /** @class */ (function () {
12
8
  function KeyBuilder(prefix) {
@@ -13,9 +13,19 @@ var KeyBuilderCS = /** @class */ (function (_super) {
13
13
  * @override
14
14
  */
15
15
  KeyBuilderCS.prototype.buildSegmentNameKey = function (segmentName) {
16
- return this.matchingKey + "." + this.prefix + ".segment." + segmentName;
16
+ return this.prefix + "." + this.matchingKey + ".segment." + segmentName;
17
17
  };
18
18
  KeyBuilderCS.prototype.extractSegmentName = function (builtSegmentKeyName) {
19
+ var prefix = this.prefix + "." + this.matchingKey + ".segment.";
20
+ if (startsWith(builtSegmentKeyName, prefix))
21
+ return builtSegmentKeyName.substr(prefix.length);
22
+ };
23
+ // @BREAKING: The key used to start with the matching key instead of the prefix, this was changed on version 10.17.3
24
+ KeyBuilderCS.prototype.buildOldSegmentNameKey = function (segmentName) {
25
+ return this.matchingKey + "." + this.prefix + ".segment." + segmentName;
26
+ };
27
+ // @BREAKING: The key used to start with the matching key instead of the prefix, this was changed on version 10.17.3
28
+ KeyBuilderCS.prototype.extractOldSegmentKey = function (builtSegmentKeyName) {
19
29
  var prefix = this.matchingKey + "." + this.prefix + ".segment.";
20
30
  if (startsWith(builtSegmentKeyName, prefix))
21
31
  return builtSegmentKeyName.substr(prefix.length);
@@ -57,9 +57,29 @@ var MySegmentsCacheInLocal = /** @class */ (function (_super) {
57
57
  var index;
58
58
  // Scan current values from localStorage
59
59
  var storedSegmentNames = Object.keys(localStorage).reduce(function (accum, key) {
60
- var name = _this.keys.extractSegmentName(key);
61
- if (name)
62
- accum.push(name);
60
+ var segmentName = _this.keys.extractSegmentName(key);
61
+ if (segmentName) {
62
+ accum.push(segmentName);
63
+ }
64
+ else {
65
+ // @BREAKING: This is only to clean up "old" keys. Remove this whole else code block.
66
+ segmentName = _this.keys.extractOldSegmentKey(key);
67
+ if (segmentName) { // this was an old segment key, let's clean up.
68
+ var newSegmentKey = _this.keys.buildSegmentNameKey(segmentName);
69
+ try {
70
+ // If the new format key is not there, create it.
71
+ if (!localStorage.getItem(newSegmentKey) && names.indexOf(segmentName) > -1) {
72
+ localStorage.setItem(newSegmentKey, DEFINED);
73
+ // we are migrating a segment, let's track it.
74
+ accum.push(segmentName);
75
+ }
76
+ localStorage.removeItem(key); // we migrated the current key, let's delete it.
77
+ }
78
+ catch (e) {
79
+ _this.log.error(e);
80
+ }
81
+ }
82
+ }
63
83
  return accum;
64
84
  }, []);
65
85
  // Extreme fast => everything is empty
@@ -1,3 +1,4 @@
1
+ import { isString } from '../../../utils/lang';
1
2
  var VERSION = '1.1';
2
3
  var CONTROL_CHANNEL_REGEX = /^control_/;
3
4
  /**
@@ -8,7 +9,7 @@ var CONTROL_CHANNEL_REGEX = /^control_/;
8
9
  */
9
10
  function buildSSEHeaders(settings) {
10
11
  var headers = {
11
- SplitSDKClientKey: settings.core.authorizationKey.slice(-4),
12
+ SplitSDKClientKey: isString(settings.core.authorizationKey) ? settings.core.authorizationKey.slice(-4) : '',
12
13
  SplitSDKVersion: settings.version,
13
14
  };
14
15
  // ip and hostname are false if IPAddressesEnabled is false
@@ -7,23 +7,34 @@ var DATA_NAME = 'events';
7
7
  export function eventsSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, eventsFirstPushWindow, latencyTracker) {
8
8
  // don't retry events.
9
9
  var syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME, latencyTracker);
10
- // Set a timer for the first push of events,
10
+ // Set a timer for the first push window of events.
11
+ // Not implemented in the base submitter or sync task, since this feature is only used by the events submitter.
11
12
  if (eventsFirstPushWindow > 0) {
13
+ var running_1 = false;
12
14
  var stopEventPublisherTimeout_1;
13
15
  var originalStart_1 = syncTask.start;
14
16
  syncTask.start = function () {
17
+ running_1 = true;
15
18
  stopEventPublisherTimeout_1 = setTimeout(originalStart_1, eventsFirstPushWindow);
16
19
  };
17
20
  var originalStop_1 = syncTask.stop;
18
21
  syncTask.stop = function () {
22
+ running_1 = false;
19
23
  clearTimeout(stopEventPublisherTimeout_1);
20
24
  originalStop_1();
21
25
  };
26
+ syncTask.isRunning = function () {
27
+ return running_1;
28
+ };
22
29
  }
23
30
  // register events submitter to be executed when events cache is full
24
31
  eventsCache.setOnFullQueueCb(function () {
25
- log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]);
26
- syncTask.execute();
32
+ if (syncTask.isRunning()) {
33
+ log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]);
34
+ syncTask.execute();
35
+ }
36
+ // If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data.
37
+ // Data will be sent when submitter is resumed.
27
38
  });
28
39
  return syncTask;
29
40
  }
@@ -37,8 +37,12 @@ export function impressionsSyncTaskFactory(log, postTestImpressionsBulk, impress
37
37
  var syncTask = submitterSyncTaskFactory(log, postTestImpressionsBulk, impressionsCache, impressionsRefreshRate, DATA_NAME, latencyTracker, fromImpressionsCollector.bind(undefined, sendLabels), 1);
38
38
  // register impressions submitter to be executed when impressions cache is full
39
39
  impressionsCache.setOnFullQueueCb(function () {
40
- log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]);
41
- syncTask.execute();
40
+ if (syncTask.isRunning()) {
41
+ log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]);
42
+ syncTask.execute();
43
+ }
44
+ // If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data.
45
+ // Data will be sent when submitter is resumed.
42
46
  });
43
47
  return syncTask;
44
48
  }
@@ -1,6 +1,7 @@
1
1
  import { submitterManagerFactory } from './submitters/submitterManager';
2
2
  import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants';
3
3
  import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants';
4
+ import { isConsentGranted } from '../utils/consent';
4
5
  /**
5
6
  * Online SyncManager factory.
6
7
  * Can be used for server-side API, and client-side API with or without multiple clients.
@@ -14,7 +15,7 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
14
15
  * SyncManager factory for modular SDK
15
16
  */
16
17
  return function (params) {
17
- var _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled;
18
+ var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled;
18
19
  /** Polling Manager */
19
20
  var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
20
21
  /** Push Manager */
@@ -49,11 +50,16 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
49
50
  var running = false; // flag that indicates whether the syncManager has been started (true) or stopped (false)
50
51
  var startFirstTime = true; // flag to distinguish calling the `start` method for the first time, to support pausing and resuming the synchronization
51
52
  return {
53
+ // Exposed for fine-grained control of synchronization.
54
+ // E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline).
55
+ pollingManager: pollingManager,
52
56
  pushManager: pushManager,
57
+ submitter: submitter,
53
58
  /**
54
59
  * Method used to start the syncManager for the first time, or resume it after being stopped.
55
60
  */
56
61
  start: function () {
62
+ running = true;
57
63
  // start syncing splits and segments
58
64
  if (pollingManager) {
59
65
  if (pushManager) {
@@ -69,29 +75,27 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
69
75
  }
70
76
  }
71
77
  // start periodic data recording (events, impressions, telemetry).
72
- if (submitter)
78
+ if (isConsentGranted(settings))
73
79
  submitter.start();
74
- running = true;
75
80
  },
76
81
  /**
77
82
  * Method used to stop/pause the syncManager.
78
83
  */
79
84
  stop: function () {
85
+ running = false;
80
86
  // stop syncing
81
87
  if (pushManager)
82
88
  pushManager.stop();
83
89
  if (pollingManager && pollingManager.isRunning())
84
90
  pollingManager.stop();
85
91
  // stop periodic data recording (events, impressions, telemetry).
86
- if (submitter)
87
- submitter.stop();
88
- running = false;
92
+ submitter.stop();
89
93
  },
90
94
  isRunning: function () {
91
95
  return running;
92
96
  },
93
97
  flush: function () {
94
- if (submitter)
98
+ if (isConsentGranted(settings))
95
99
  return submitter.execute();
96
100
  else
97
101
  return Promise.resolve();
@@ -0,0 +1,6 @@
1
+ import { CONSENT_GRANTED } from './constants';
2
+ export function isConsentGranted(settings) {
3
+ var userConsent = settings.userConsent;
4
+ // undefined userConsent is handled as granted (default)
5
+ return !userConsent || userConsent === CONSENT_GRANTED;
6
+ }
@@ -24,3 +24,7 @@ export var STORAGE_MEMORY = 'MEMORY';
24
24
  export var STORAGE_LOCALSTORAGE = 'LOCALSTORAGE';
25
25
  export var STORAGE_REDIS = 'REDIS';
26
26
  export var STORAGE_PLUGGABLE = 'PLUGGABLE';
27
+ // User consent
28
+ export var CONSENT_GRANTED = 'GRANTED'; // The user has granted consent for tracking events and impressions
29
+ export var CONSENT_DECLINED = 'DECLINED'; // The user has declined consent for tracking events and impressions
30
+ export var CONSENT_UNKNOWN = 'UNKNOWN'; // The user has neither granted nor declined consent for tracking events and impressions
@@ -152,6 +152,12 @@ export function isObject(obj) {
152
152
  export function isString(val) {
153
153
  return typeof val === 'string' || val instanceof String;
154
154
  }
155
+ /**
156
+ * String sanitizer. Returns the provided value converted to uppercase if it is a string.
157
+ */
158
+ export function stringToUpperCase(val) {
159
+ return isString(val) ? val.toUpperCase() : val;
160
+ }
155
161
  /**
156
162
  * Deep copy version of Object.assign using recursion.
157
163
  * There are some assumptions here. It's for internal use and we don't need verbose errors
@@ -0,0 +1,12 @@
1
+ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
2
+ import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants';
3
+ import { stringToUpperCase } from '../lang';
4
+ var userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN];
5
+ export function validateConsent(_a) {
6
+ var userConsent = _a.userConsent, log = _a.log;
7
+ userConsent = stringToUpperCase(userConsent);
8
+ if (userConsentValues.indexOf(userConsent) > -1)
9
+ return userConsent;
10
+ log.error(ERROR_INVALID_CONFIG_PARAM, ['userConsent', userConsentValues, CONSENT_GRANTED]);
11
+ return CONSENT_GRANTED;
12
+ }
@@ -1,10 +1,10 @@
1
- import { ERROR_INVALID_IMPRESSIONS_MODE } from '../../logger/constants';
1
+ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
2
2
  import { DEBUG, OPTIMIZED } from '../constants';
3
+ import { stringToUpperCase } from '../lang';
3
4
  export function validImpressionsMode(log, impressionsMode) {
4
- impressionsMode = impressionsMode.toUpperCase();
5
- if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) === -1) {
6
- log.error(ERROR_INVALID_IMPRESSIONS_MODE, [[DEBUG, OPTIMIZED], OPTIMIZED]);
7
- impressionsMode = OPTIMIZED;
8
- }
9
- return impressionsMode;
5
+ impressionsMode = stringToUpperCase(impressionsMode);
6
+ if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) > -1)
7
+ return impressionsMode;
8
+ log.error(ERROR_INVALID_CONFIG_PARAM, ['impressionsMode', [DEBUG, OPTIMIZED], OPTIMIZED]);
9
+ return OPTIMIZED;
10
10
  }
@@ -80,7 +80,7 @@ function fromSecondsToMillis(n) {
80
80
  * @param validationParams defaults and fields validators used to validate and creates a settings object from a given config
81
81
  */
82
82
  export function settingsValidation(config, validationParams) {
83
- var defaults = validationParams.defaults, runtime = validationParams.runtime, storage = validationParams.storage, integrations = validationParams.integrations, logger = validationParams.logger, localhost = validationParams.localhost;
83
+ var defaults = validationParams.defaults, runtime = validationParams.runtime, storage = validationParams.storage, integrations = validationParams.integrations, logger = validationParams.logger, localhost = validationParams.localhost, consent = validationParams.consent;
84
84
  // creates a settings object merging base, defaults and config objects.
85
85
  var withDefaults = merge({}, base, defaults, config);
86
86
  // ensure a valid logger.
@@ -133,5 +133,8 @@ export function settingsValidation(config, validationParams) {
133
133
  withDefaults.sync.__splitFiltersValidation = splitFiltersValidation;
134
134
  // ensure a valid impressionsMode
135
135
  withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);
136
+ // ensure a valid user consent value
137
+ // @ts-ignore, modify readonly prop
138
+ withDefaults.userConsent = consent(withDefaults);
136
139
  return withDefaults;
137
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.2.1-rc.4",
3
+ "version": "1.2.1-rc.7",
4
4
  "description": "Split Javascript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -11,6 +11,7 @@ import { OPTIMIZED, DEBUG } from '../utils/constants';
11
11
  import { objectAssign } from '../utils/lang/objectAssign';
12
12
  import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants';
13
13
  import { ISyncManager } from '../sync/types';
14
+ import { isConsentGranted } from '../utils/consent';
14
15
 
15
16
  // 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring.
16
17
  const UNLOAD_DOM_EVENT = 'unload';
@@ -65,15 +66,18 @@ export class BrowserSignalListener implements ISignalListener {
65
66
  flushData() {
66
67
  if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush
67
68
 
68
- const eventsUrl = this.settings.urls.events;
69
- const extraMetadata = {
70
- // sim stands for Sync/Split Impressions Mode
71
- sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG
72
- };
69
+ // Flush data if there is user consent
70
+ if (isConsentGranted(this.settings)) {
71
+ const eventsUrl = this.settings.urls.events;
72
+ const extraMetadata = {
73
+ // sim stands for Sync/Split Impressions Mode
74
+ sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG
75
+ };
73
76
 
74
- this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
75
- this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
76
- if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
77
+ this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
78
+ this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
79
+ if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
80
+ }
77
81
 
78
82
  // Close streaming connection
79
83
  if (this.syncManager.pushManager) this.syncManager.pushManager.stop();
@@ -84,7 +88,7 @@ export class BrowserSignalListener implements ISignalListener {
84
88
  if (!cache.isEmpty()) {
85
89
  const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.state()) : cache.state();
86
90
  if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
87
- postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exceptions
91
+ postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exception
88
92
  }
89
93
  cache.clear();
90
94
  }
@@ -68,6 +68,8 @@ export const SYNC_CONTINUE_POLLING = 118;
68
68
  export const SYNC_STOP_POLLING = 119;
69
69
  export const EVENTS_TRACKER_SUCCESS = 120;
70
70
  export const IMPRESSIONS_TRACKER_SUCCESS = 121;
71
+ export const USER_CONSENT_UPDATED = 122;
72
+ export const USER_CONSENT_NOT_UPDATED = 123;
71
73
 
72
74
  export const ENGINE_VALUE_INVALID = 200;
73
75
  export const ENGINE_VALUE_NO_ATTRIBUTES = 201;
@@ -115,10 +117,11 @@ export const ERROR_INVALID_KEY_OBJECT = 317;
115
117
  export const ERROR_INVALID = 318;
116
118
  export const ERROR_EMPTY = 319;
117
119
  export const ERROR_EMPTY_ARRAY = 320;
118
- export const ERROR_INVALID_IMPRESSIONS_MODE = 321;
120
+ export const ERROR_INVALID_CONFIG_PARAM = 321;
119
121
  export const ERROR_HTTP = 322;
120
122
  export const ERROR_LOCALHOST_MODULE_REQUIRED = 323;
121
123
  export const ERROR_STORAGE_INVALID = 324;
124
+ export const ERROR_NOT_BOOLEAN = 325;
122
125
 
123
126
  // Log prefixes (a.k.a. tags or categories)
124
127
  export const LOG_PREFIX_SETTINGS = 'settings';
@@ -31,9 +31,9 @@ export const codesDebug: [number, string][] = codesInfo.concat([
31
31
  // SDK
32
32
  [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'],
33
33
  [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'],
34
- [c.RETRIEVE_CLIENT_DEFAULT, ' Retrieving default SDK client.'],
35
- [c.RETRIEVE_CLIENT_EXISTING, ' Retrieving existing SDK client.'],
36
- [c.RETRIEVE_MANAGER, ' Retrieving manager instance.'],
34
+ [c.RETRIEVE_CLIENT_DEFAULT, 'Retrieving default SDK client.'],
35
+ [c.RETRIEVE_CLIENT_EXISTING, 'Retrieving existing SDK client.'],
36
+ [c.RETRIEVE_MANAGER, 'Retrieving manager instance.'],
37
37
  // synchronizer
38
38
  [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Splits data: \n%s'],
39
39
  [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up split update using since = %s'],
@@ -5,7 +5,7 @@ export const codesError: [number, string][] = [
5
5
  [c.ERROR_ENGINE_COMBINER_IFELSEIF, c.LOG_PREFIX_ENGINE_COMBINER + 'Invalid Split, no valid rules found'],
6
6
  // SDK
7
7
  [c.ERROR_LOGLEVEL_INVALID, 'logger: Invalid Log Level - No changes to the logs will be applied.'],
8
- [c.ERROR_CLIENT_CANNOT_GET_READY, ' The SDK will not get ready. Reason: %s'],
8
+ [c.ERROR_CLIENT_CANNOT_GET_READY, 'The SDK will not get ready. Reason: %s'],
9
9
  [c.ERROR_IMPRESSIONS_TRACKER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Could not store impressions bulk with %s impression(s). Error: %s'],
10
10
  [c.ERROR_IMPRESSIONS_LISTENER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Impression listener logImpression method threw: %s.'],
11
11
  [c.ERROR_EVENTS_TRACKER, c.LOG_PREFIX_EVENTS_TRACKER + 'Failed to queue %s'],
@@ -28,8 +28,9 @@ export const codesError: [number, string][] = [
28
28
  [c.ERROR_INVALID, '%s: you passed an invalid %s. It must be a non-empty string.'],
29
29
  [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'],
30
30
  [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
31
+ [c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'],
31
32
  // initialization / settings validation
32
- [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'],
33
+ [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'],
33
34
  [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'],
34
35
  [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid.%s Fallbacking into default MEMORY storage'],
35
36
  ];
@@ -10,10 +10,12 @@ export const codesInfo: [number, string][] = codesWarn.concat([
10
10
  // SDK
11
11
  [c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Split: %s. Key: %s. Evaluation: %s. Label: %s'],
12
12
  [c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression.'],
13
- [c.NEW_SHARED_CLIENT, ' New shared client instance created.'],
14
- [c.NEW_FACTORY, ' New Split SDK instance created.'],
13
+ [c.NEW_SHARED_CLIENT, 'New shared client instance created.'],
14
+ [c.NEW_FACTORY, 'New Split SDK instance created.'],
15
15
  [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'],
16
16
  [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'],
17
+ [c.USER_CONSENT_UPDATED, 'setUserConsent: consent status changed from %s to %s.'],
18
+ [c.USER_CONSENT_NOT_UPDATED, 'setUserConsent: call had no effect because it was the current consent status (%s).'],
17
19
 
18
20
  // synchronizer
19
21
  [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'],