@splitsoftware/splitio-commons 1.3.2-rc.1 → 1.3.2-rc.4

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 (48) hide show
  1. package/cjs/listeners/browser.js +6 -1
  2. package/cjs/readiness/sdkReadinessManager.js +7 -7
  3. package/cjs/services/splitApi.js +2 -2
  4. package/cjs/storages/KeyBuilderSS.js +9 -5
  5. package/cjs/storages/inRedis/RedisAdapter.js +1 -1
  6. package/cjs/storages/inRedis/TelemetryCacheInRedis.js +7 -0
  7. package/cjs/storages/inRedis/index.js +4 -1
  8. package/cjs/storages/pluggable/index.js +2 -1
  9. package/cjs/sync/submitters/submitter.js +5 -4
  10. package/cjs/sync/submitters/telemetrySubmitter.js +22 -9
  11. package/cjs/utils/settingsValidation/index.js +16 -6
  12. package/cjs/utils/settingsValidation/url.js +1 -1
  13. package/esm/listeners/browser.js +6 -1
  14. package/esm/readiness/sdkReadinessManager.js +7 -7
  15. package/esm/services/splitApi.js +2 -2
  16. package/esm/storages/KeyBuilderSS.js +9 -5
  17. package/esm/storages/inRedis/RedisAdapter.js +1 -1
  18. package/esm/storages/inRedis/TelemetryCacheInRedis.js +7 -0
  19. package/esm/storages/inRedis/index.js +4 -1
  20. package/esm/storages/pluggable/index.js +2 -1
  21. package/esm/sync/submitters/submitter.js +5 -4
  22. package/esm/sync/submitters/telemetrySubmitter.js +21 -9
  23. package/esm/utils/settingsValidation/index.js +17 -7
  24. package/esm/utils/settingsValidation/url.js +1 -1
  25. package/package.json +1 -1
  26. package/src/listeners/browser.ts +6 -1
  27. package/src/readiness/sdkReadinessManager.ts +7 -5
  28. package/src/readiness/types.ts +7 -1
  29. package/src/services/splitApi.ts +2 -2
  30. package/src/storages/KeyBuilderSS.ts +12 -6
  31. package/src/storages/inRedis/RedisAdapter.ts +1 -1
  32. package/src/storages/inRedis/TelemetryCacheInRedis.ts +7 -0
  33. package/src/storages/inRedis/index.ts +5 -1
  34. package/src/storages/pluggable/index.ts +2 -1
  35. package/src/sync/submitters/submitter.ts +4 -4
  36. package/src/sync/submitters/telemetrySubmitter.ts +24 -10
  37. package/src/sync/submitters/types.ts +11 -6
  38. package/src/types.ts +1 -1
  39. package/src/utils/murmur3/utfx.ts +1 -2
  40. package/src/utils/settingsValidation/index.ts +18 -8
  41. package/src/utils/settingsValidation/url.ts +1 -1
  42. package/types/readiness/sdkReadinessManager.d.ts +1 -3
  43. package/types/readiness/types.d.ts +6 -1
  44. package/types/storages/KeyBuilderSS.d.ts +3 -2
  45. package/types/storages/inRedis/TelemetryCacheInRedis.d.ts +1 -0
  46. package/types/sync/submitters/telemetrySubmitter.d.ts +3 -2
  47. package/types/sync/submitters/types.d.ts +8 -5
  48. package/types/types.d.ts +1 -1
@@ -1,10 +1,11 @@
1
- var _a, _b;
1
+ var _a, _b, _c;
2
2
  import { submitterFactory, firstPushWindowDecorator } from './submitter';
3
- import { QUEUED, DEDUPED, DROPPED, CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, DEBUG_ENUM, OPTIMIZED_ENUM } from '../../utils/constants';
3
+ import { QUEUED, DEDUPED, DROPPED, CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, DEBUG_ENUM, OPTIMIZED_ENUM, CONSENT_GRANTED, CONSENT_DECLINED, CONSENT_UNKNOWN } from '../../utils/constants';
4
4
  import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants';
5
5
  import { base } from '../../utils/settingsValidation';
6
6
  import { usedKeysMap } from '../../utils/inputValidation/apiKey';
7
7
  import { timer } from '../../utils/timeTracker/timer';
8
+ import { objectAssign } from '../../utils/lang/objectAssign';
8
9
  /**
9
10
  * Converts data from telemetry cache into /metrics/usage request payload.
10
11
  */
@@ -46,6 +47,11 @@ var IMPRESSIONS_MODE_MAP = (_b = {},
46
47
  _b[OPTIMIZED] = OPTIMIZED_ENUM,
47
48
  _b[DEBUG] = DEBUG_ENUM,
48
49
  _b);
50
+ var USER_CONSENT_MAP = (_c = {},
51
+ _c[CONSENT_UNKNOWN] = 1,
52
+ _c[CONSENT_GRANTED] = 2,
53
+ _c[CONSENT_DECLINED] = 3,
54
+ _c);
49
55
  function getActiveFactories() {
50
56
  return Object.keys(usedKeysMap).length;
51
57
  }
@@ -54,6 +60,14 @@ function getRedundantActiveFactories() {
54
60
  return acum + usedKeysMap[apiKey] - 1;
55
61
  }, 0);
56
62
  }
63
+ export function getTelemetryConfigStats(mode, storageType) {
64
+ return {
65
+ oM: OPERATION_MODE_MAP[mode],
66
+ st: storageType.toLowerCase(),
67
+ aF: getActiveFactories(),
68
+ rF: getRedundantActiveFactories(),
69
+ };
70
+ }
57
71
  /**
58
72
  * Converts data from telemetry cache and settings into /metrics/config request payload.
59
73
  */
@@ -63,9 +77,7 @@ export function telemetryCacheConfigAdapter(telemetry, settings) {
63
77
  clear: function () { },
64
78
  state: function () {
65
79
  var urls = settings.urls, scheduler = settings.scheduler;
66
- return {
67
- oM: OPERATION_MODE_MAP[settings.mode],
68
- st: settings.storage.type.toLowerCase(),
80
+ return objectAssign(getTelemetryConfigStats(settings.mode, settings.storage.type), {
69
81
  sE: settings.streamingEnabled,
70
82
  rR: {
71
83
  sp: scheduler.featuresRefreshRate,
@@ -86,14 +98,13 @@ export function telemetryCacheConfigAdapter(telemetry, settings) {
86
98
  iM: IMPRESSIONS_MODE_MAP[settings.sync.impressionsMode],
87
99
  iL: settings.impressionListener ? true : false,
88
100
  hP: false,
89
- aF: getActiveFactories(),
90
- rF: getRedundantActiveFactories(),
91
101
  tR: telemetry.getTimeUntilReady(),
92
102
  tC: telemetry.getTimeUntilReadyFromCache(),
93
103
  nR: telemetry.getNonReadyUsage(),
94
104
  t: telemetry.popTags(),
95
105
  i: settings.integrations && settings.integrations.map(function (int) { return int.type; }),
96
- };
106
+ uC: settings.userConsent ? USER_CONSENT_MAP[settings.userConsent] : 0
107
+ });
97
108
  }
98
109
  };
99
110
  }
@@ -104,12 +115,13 @@ export function telemetrySubmitterFactory(params) {
104
115
  var _a = params.storage, splits = _a.splits, segments = _a.segments, telemetry = _a.telemetry;
105
116
  if (!telemetry)
106
117
  return; // No submitter created if telemetry cache is not defined
107
- var settings = params.settings, _b = params.settings, log = _b.log, telemetryRefreshRate = _b.scheduler.telemetryRefreshRate, splitApi = params.splitApi, now = params.platform.now, readiness = params.readiness;
118
+ var settings = params.settings, _b = params.settings, log = _b.log, telemetryRefreshRate = _b.scheduler.telemetryRefreshRate, splitApi = params.splitApi, now = params.platform.now, readiness = params.readiness, sdkReadinessManager = params.sdkReadinessManager;
108
119
  var startTime = timer(now || Date.now);
109
120
  var submitter = firstPushWindowDecorator(submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(telemetry, splits, segments), telemetryRefreshRate, 'telemetry stats', undefined, 0, true), telemetryRefreshRate);
110
121
  readiness.gate.once(SDK_READY_FROM_CACHE, function () {
111
122
  telemetry.recordTimeUntilReadyFromCache(startTime());
112
123
  });
124
+ sdkReadinessManager.incInternalReadyCbCount();
113
125
  readiness.gate.once(SDK_READY, function () {
114
126
  telemetry.recordTimeUntilReady(startTime());
115
127
  // Post config data when the SDK is ready and if the telemetry submitter was started
@@ -1,7 +1,7 @@
1
1
  import { merge } from '../lang';
2
2
  import { mode } from './mode';
3
3
  import { validateSplitFilters } from './splitFilters';
4
- import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE } from '../constants';
4
+ import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE, DEBUG } from '../constants';
5
5
  import { validImpressionsMode } from './impressionsMode';
6
6
  import { validateKey } from '../inputValidation/key';
7
7
  import { validateTrafficType } from '../inputValidation/trafficType';
@@ -29,8 +29,8 @@ export var base = {
29
29
  segmentsRefreshRate: 60,
30
30
  // publish telemetry stats each 3600 secs (1 hour)
31
31
  telemetryRefreshRate: 3600,
32
- // publish evaluations each 60 sec
33
- impressionsRefreshRate: 60,
32
+ // publish evaluations each 300 sec (default value for OPTIMIZED impressions mode)
33
+ impressionsRefreshRate: 300,
34
34
  // fetch offline changes each 15 sec
35
35
  offlineRefreshRate: 15,
36
36
  // publish events every 60 seconds after the first flush
@@ -52,7 +52,7 @@ export var base = {
52
52
  // Streaming Server
53
53
  streaming: 'https://streaming.split.io',
54
54
  // Telemetry Server
55
- telemetry: 'https://telemetry.split.io',
55
+ telemetry: 'https://telemetry.split.io/api',
56
56
  },
57
57
  // Defines which kind of storage we should instanciate.
58
58
  storage: undefined,
@@ -93,6 +93,8 @@ export function settingsValidation(config, validationParams) {
93
93
  // First thing to validate, since other validators might use the logger.
94
94
  var log = logger(withDefaults); // @ts-ignore, modify readonly prop
95
95
  withDefaults.log = log;
96
+ // ensure a valid impressionsMode
97
+ withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);
96
98
  function validateMinValue(paramName, actualValue, minValue) {
97
99
  if (actualValue >= minValue)
98
100
  return actualValue;
@@ -104,10 +106,20 @@ export function settingsValidation(config, validationParams) {
104
106
  var scheduler = withDefaults.scheduler, startup = withDefaults.startup;
105
107
  scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate);
106
108
  scheduler.segmentsRefreshRate = fromSecondsToMillis(scheduler.segmentsRefreshRate);
107
- scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate);
108
109
  scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate);
109
110
  scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate);
110
111
  scheduler.telemetryRefreshRate = fromSecondsToMillis(validateMinValue('telemetryRefreshRate', scheduler.telemetryRefreshRate, 60));
112
+ if (scheduler.impressionsRefreshRate !== base.scheduler.impressionsRefreshRate) {
113
+ // Validate impressionsRefreshRate defined by user
114
+ scheduler.impressionsRefreshRate = validateMinValue('impressionsRefreshRate', scheduler.impressionsRefreshRate, withDefaults.sync.impressionsMode === DEBUG ? 1 : 60 // Min is 1 sec for DEBUG and 60 secs for OPTIMIZED
115
+ );
116
+ }
117
+ else {
118
+ // Default impressionsRefreshRate for DEBUG mode is 60 secs
119
+ if (withDefaults.sync.impressionsMode === DEBUG)
120
+ scheduler.impressionsRefreshRate = 60;
121
+ }
122
+ scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate);
111
123
  // Log deprecation for old telemetry param
112
124
  if (scheduler.metricsRefreshRate)
113
125
  log.warn('`metricsRefreshRate` will be deprecated soon. For configuring telemetry rates, update `telemetryRefreshRate` value in configs');
@@ -163,8 +175,6 @@ export function settingsValidation(config, validationParams) {
163
175
  var splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode);
164
176
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
165
177
  withDefaults.sync.__splitFiltersValidation = splitFiltersValidation;
166
- // ensure a valid impressionsMode
167
- withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);
168
178
  // ensure a valid user consent value
169
179
  // @ts-ignore, modify readonly prop
170
180
  withDefaults.userConsent = consent(withDefaults);
@@ -1,4 +1,4 @@
1
- var telemetryEndpointMatcher = /^\/metrics\/(config|usage)/;
1
+ var telemetryEndpointMatcher = /^\/v1\/metrics\/(config|usage)/;
2
2
  var eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/;
3
3
  var authEndpointMatcher = /^\/v2\/auth/;
4
4
  var streamingEndpointMatcher = /^\/(sse|event-stream)/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.3.2-rc.1",
3
+ "version": "1.3.2-rc.4",
4
4
  "description": "Split Javascript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -12,6 +12,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
12
12
  import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants';
13
13
  import { ISyncManager } from '../sync/types';
14
14
  import { isConsentGranted } from '../consent';
15
+ import { telemetryCacheStatsAdapter } from '../sync/submitters/telemetrySubmitter';
15
16
 
16
17
  // 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring.
17
18
  const UNLOAD_DOM_EVENT = 'unload';
@@ -77,7 +78,11 @@ export class BrowserSignalListener implements ISignalListener {
77
78
  this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
78
79
  this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
79
80
  if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
80
- // No beacon endpoint for `/metrics/usage`
81
+ if (this.storage.telemetry) {
82
+ const telemetryUrl = this.settings.urls.telemetry;
83
+ const telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments);
84
+ this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
85
+ }
81
86
  }
82
87
 
83
88
  // Close streaming connection
@@ -15,18 +15,16 @@ const REMOVE_LISTENER_EVENT = 'removeListener';
15
15
  * It also updates logs related warnings and errors.
16
16
  *
17
17
  * @param readyTimeout time in millis to emit SDK_READY_TIME_OUT event
18
- * @param internalReadyCbCount offset value of SDK_READY listeners that are added/removed internally
19
- * by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected'
20
18
  * @param readinessManager optional readinessManager to use. only used internally for `shared` method
21
19
  */
22
20
  export function sdkReadinessManagerFactory(
23
21
  log: ILogger,
24
22
  EventEmitter: new () => IEventEmitter,
25
23
  readyTimeout = 0,
26
- internalReadyCbCount = 0,
27
24
  readinessManager = readinessManagerFactory(EventEmitter, readyTimeout)): ISdkReadinessManager {
28
25
 
29
26
  /** Ready callback warning */
27
+ let internalReadyCbCount = 0;
30
28
  let readyCbCount = 0;
31
29
  readinessManager.gate.on(REMOVE_LISTENER_EVENT, (event: any) => {
32
30
  if (event === SDK_READY) readyCbCount--;
@@ -74,8 +72,12 @@ export function sdkReadinessManagerFactory(
74
72
  return {
75
73
  readinessManager,
76
74
 
77
- shared(readyTimeout = 0, internalReadyCbCount = 0) {
78
- return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, internalReadyCbCount, readinessManager.shared(readyTimeout));
75
+ shared(readyTimeout = 0) {
76
+ return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, readinessManager.shared(readyTimeout));
77
+ },
78
+
79
+ incInternalReadyCbCount() {
80
+ internalReadyCbCount++;
79
81
  },
80
82
 
81
83
  sdkStatus: objectAssign(
@@ -66,6 +66,12 @@ export interface ISdkReadinessManager {
66
66
  readinessManager: IReadinessManager
67
67
  sdkStatus: IStatusInterface
68
68
 
69
+ /**
70
+ * Increment internalReadyCbCount, an offset value of SDK_READY listeners that are added/removed internally
71
+ * by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected'
72
+ */
73
+ incInternalReadyCbCount(): void
74
+
69
75
  /** for client-side */
70
- shared(readyTimeout?: number, internalReadyCbCount?: number): ISdkReadinessManager
76
+ shared(readyTimeout?: number): ISdkReadinessManager
71
77
  }
@@ -108,12 +108,12 @@ export function splitApiFactory(
108
108
  },
109
109
 
110
110
  postMetricsConfig(body: string) {
111
- const url = `${urls.telemetry}/metrics/config`;
111
+ const url = `${urls.telemetry}/v1/metrics/config`;
112
112
  return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true);
113
113
  },
114
114
 
115
115
  postMetricsUsage(body: string) {
116
- const url = `${urls.telemetry}/metrics/usage`;
116
+ const url = `${urls.telemetry}/v1/metrics/usage`;
117
117
  return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true);
118
118
  }
119
119
  };
@@ -23,10 +23,6 @@ export class KeyBuilderSS extends KeyBuilder {
23
23
  return `${this.prefix}.segments.registered`;
24
24
  }
25
25
 
26
- private buildVersionablePrefix() {
27
- return `${this.metadata.s}/${this.metadata.n}/${this.metadata.i}`;
28
- }
29
-
30
26
  buildImpressionsKey() {
31
27
  return `${this.prefix}.impressions`;
32
28
  }
@@ -35,6 +31,12 @@ export class KeyBuilderSS extends KeyBuilder {
35
31
  return `${this.prefix}.events`;
36
32
  }
37
33
 
34
+ searchPatternForSplitKeys() {
35
+ return `${this.buildSplitKeyPrefix()}*`;
36
+ }
37
+
38
+ /* Telemetry keys */
39
+
38
40
  buildLatencyKey(method: Method, bucket: number) {
39
41
  return `${this.prefix}.telemetry.latencies::${this.buildVersionablePrefix()}/${methodNames[method]}/${bucket}`;
40
42
  }
@@ -43,8 +45,12 @@ export class KeyBuilderSS extends KeyBuilder {
43
45
  return `${this.prefix}.telemetry.exceptions::${this.buildVersionablePrefix()}/${methodNames[method]}`;
44
46
  }
45
47
 
46
- searchPatternForSplitKeys() {
47
- return `${this.buildSplitKeyPrefix()}*`;
48
+ buildInitKey() {
49
+ return `${this.prefix}.telemetry.init::${this.buildVersionablePrefix()}`;
50
+ }
51
+
52
+ private buildVersionablePrefix() {
53
+ return `${this.metadata.s}/${this.metadata.n}/${this.metadata.i}`;
48
54
  }
49
55
 
50
56
  }
@@ -8,7 +8,7 @@ import { timeout } from '../../utils/promise/timeout';
8
8
  const LOG_PREFIX = 'storage:redis-adapter: ';
9
9
 
10
10
  // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis.
11
- const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim'];
11
+ const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim', 'hset'];
12
12
 
13
13
  // Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere.
14
14
  const DEFAULT_OPTIONS = {
@@ -4,6 +4,8 @@ import { KeyBuilderSS } from '../KeyBuilderSS';
4
4
  import { ITelemetryCacheAsync } from '../types';
5
5
  import { findLatencyIndex } from '../findLatencyIndex';
6
6
  import { Redis } from 'ioredis';
7
+ import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter';
8
+ import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants';
7
9
 
8
10
  export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
9
11
 
@@ -26,4 +28,9 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
26
28
  .catch(() => { /* Handle rejections for telemetry */ });
27
29
  }
28
30
 
31
+ recordConfig() {
32
+ const [key, field] = this.keys.buildInitKey().split('::');
33
+ const value = JSON.stringify(getTelemetryConfigStats(CONSUMER_MODE, STORAGE_REDIS));
34
+ return this.redis.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ });
35
+ }
29
36
  }
@@ -26,10 +26,14 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
26
26
 
27
27
  const keys = new KeyBuilderSS(prefix, metadata);
28
28
  const redisClient = new RedisAdapter(log, options.options || {});
29
+ const telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
29
30
 
30
31
  // subscription to Redis connect event in order to emit SDK_READY event on consumer mode
31
32
  redisClient.on('connect', () => {
32
33
  onReadyCb();
34
+
35
+ // Synchronize config
36
+ telemetry.recordConfig();
33
37
  });
34
38
 
35
39
  return {
@@ -37,7 +41,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
37
41
  segments: new SegmentsCacheInRedis(log, keys, redisClient),
38
42
  impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata),
39
43
  events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata),
40
- telemetry: new TelemetryCacheInRedis(log, keys, redisClient),
44
+ telemetry,
41
45
 
42
46
  // When using REDIS we should:
43
47
  // 1- Disconnect from the storage
@@ -39,6 +39,7 @@ function validatePluggableStorageOptions(options: any) {
39
39
  function wrapperConnect(wrapper: IPluggableStorageWrapper, onReadyCb: (error?: any) => void) {
40
40
  wrapper.connect().then(() => {
41
41
  onReadyCb();
42
+ // At the moment, we don't synchronize config with pluggable storage
42
43
  }).catch((e) => {
43
44
  onReadyCb(e || new Error('Error connecting wrapper'));
44
45
  });
@@ -77,7 +78,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn
77
78
  impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata),
78
79
  impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined,
79
80
  events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata),
80
- // @TODO Not using TelemetryCachePluggable yet, because it is not supported by the Split Synchronizer
81
+ // @TODO Not using TelemetryCachePluggable yet because it's not supported by the Split Synchronizer, and needs to drop or queue operations while the wrapper is not ready
81
82
  // telemetry: isPartialConsumer ? new TelemetryCacheInMemory() : new TelemetryCachePluggable(log, keys, wrapper),
82
83
 
83
84
  // Disconnect the underlying storage
@@ -16,7 +16,7 @@ export function submitterFactory<TState>(
16
16
  dataName: string,
17
17
  fromCacheToPayload?: (cacheData: TState) => any,
18
18
  maxRetries: number = 0,
19
- debugLogs?: boolean
19
+ debugLogs?: boolean // true for telemetry submitters
20
20
  ): ISyncTask<[], void> {
21
21
 
22
22
  let retries = 0;
@@ -37,14 +37,14 @@ export function submitterFactory<TState>(
37
37
  sourceCache.clear(); // we clear the queue if request successes.
38
38
  }).catch(err => {
39
39
  if (!maxRetries) {
40
- log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
40
+ log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
41
41
  } else if (retries === maxRetries) {
42
42
  retries = 0;
43
43
  sourceCache.clear(); // we clear the queue if request fails after retries.
44
- log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
44
+ log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]);
45
45
  } else {
46
46
  retries++;
47
- log.warn(SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]);
47
+ log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]);
48
48
  }
49
49
  });
50
50
  }
@@ -1,13 +1,14 @@
1
1
  import { ISegmentsCacheSync, ISplitsCacheSync, ITelemetryCacheSync } from '../../storages/types';
2
2
  import { submitterFactory, firstPushWindowDecorator } from './submitter';
3
- import { TelemetryUsageStatsPayload, TelemetryConfigStatsPayload } from './types';
4
- import { QUEUED, DEDUPED, DROPPED, CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, DEBUG_ENUM, OPTIMIZED_ENUM } from '../../utils/constants';
3
+ import { TelemetryUsageStatsPayload, TelemetryConfigStatsPayload, TelemetryConfigStats } from './types';
4
+ import { QUEUED, DEDUPED, DROPPED, CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, DEBUG_ENUM, OPTIMIZED_ENUM, CONSENT_GRANTED, CONSENT_DECLINED, CONSENT_UNKNOWN } from '../../utils/constants';
5
5
  import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants';
6
- import { ISettings } from '../../types';
6
+ import { ConsentStatus, ISettings, SDKMode } from '../../types';
7
7
  import { base } from '../../utils/settingsValidation';
8
8
  import { usedKeysMap } from '../../utils/inputValidation/apiKey';
9
9
  import { timer } from '../../utils/timeTracker/timer';
10
10
  import { ISdkFactoryContextSync } from '../../sdkFactory/types';
11
+ import { objectAssign } from '../../utils/lang/objectAssign';
11
12
 
12
13
  /**
13
14
  * Converts data from telemetry cache into /metrics/usage request payload.
@@ -54,6 +55,12 @@ const IMPRESSIONS_MODE_MAP = {
54
55
  [DEBUG]: DEBUG_ENUM
55
56
  } as Record<ISettings['sync']['impressionsMode'], (0 | 1)>;
56
57
 
58
+ const USER_CONSENT_MAP = {
59
+ [CONSENT_UNKNOWN]: 1,
60
+ [CONSENT_GRANTED]: 2,
61
+ [CONSENT_DECLINED]: 3
62
+ } as Record<ConsentStatus, (0 | 1 | 2 | 3)>;
63
+
57
64
  function getActiveFactories() {
58
65
  return Object.keys(usedKeysMap).length;
59
66
  }
@@ -64,6 +71,15 @@ function getRedundantActiveFactories() {
64
71
  }, 0);
65
72
  }
66
73
 
74
+ export function getTelemetryConfigStats(mode: SDKMode, storageType: string): TelemetryConfigStats {
75
+ return {
76
+ oM: OPERATION_MODE_MAP[mode], // @ts-ignore lower case of storage type
77
+ st: storageType.toLowerCase(),
78
+ aF: getActiveFactories(),
79
+ rF: getRedundantActiveFactories(),
80
+ };
81
+ }
82
+
67
83
  /**
68
84
  * Converts data from telemetry cache and settings into /metrics/config request payload.
69
85
  */
@@ -75,9 +91,7 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett
75
91
  state(): TelemetryConfigStatsPayload {
76
92
  const { urls, scheduler } = settings;
77
93
 
78
- return {
79
- oM: OPERATION_MODE_MAP[settings.mode], // @ts-ignore lower case of storage type
80
- st: settings.storage.type.toLowerCase(),
94
+ return objectAssign(getTelemetryConfigStats(settings.mode, settings.storage.type), {
81
95
  sE: settings.streamingEnabled,
82
96
  rR: {
83
97
  sp: scheduler.featuresRefreshRate,
@@ -98,14 +112,13 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett
98
112
  iM: IMPRESSIONS_MODE_MAP[settings.sync.impressionsMode],
99
113
  iL: settings.impressionListener ? true : false,
100
114
  hP: false, // @TODO proxy not supported
101
- aF: getActiveFactories(),
102
- rF: getRedundantActiveFactories(),
103
115
  tR: telemetry.getTimeUntilReady() as number,
104
116
  tC: telemetry.getTimeUntilReadyFromCache(),
105
117
  nR: telemetry.getNonReadyUsage(),
106
118
  t: telemetry.popTags(),
107
119
  i: settings.integrations && settings.integrations.map(int => int.type),
108
- };
120
+ uC: settings.userConsent ? USER_CONSENT_MAP[settings.userConsent] : 0
121
+ });
109
122
  }
110
123
  };
111
124
  }
@@ -117,7 +130,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) {
117
130
  const { storage: { splits, segments, telemetry } } = params;
118
131
  if (!telemetry) return; // No submitter created if telemetry cache is not defined
119
132
 
120
- const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness } = params;
133
+ const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness, sdkReadinessManager } = params;
121
134
  const startTime = timer(now || Date.now);
122
135
 
123
136
  const submitter = firstPushWindowDecorator(
@@ -129,6 +142,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) {
129
142
  telemetry.recordTimeUntilReadyFromCache(startTime());
130
143
  });
131
144
 
145
+ sdkReadinessManager.incInternalReadyCbCount();
132
146
  readiness.gate.once(SDK_READY, () => {
133
147
  telemetry.recordTimeUntilReady(startTime());
134
148
 
@@ -165,10 +165,17 @@ export type UrlOverrides = {
165
165
  t: boolean, // telemetry
166
166
  }
167
167
 
168
- // 'metrics/config' JSON request body
169
- export type TelemetryConfigStatsPayload = {
170
- oM?: OperationMode, // operationMode
168
+ // 'telemetry.init' Redis/Pluggable key
169
+ export type TelemetryConfigStats = {
170
+ oM: OperationMode, // operationMode
171
171
  st: 'memory' | 'redis' | 'pluggable' | 'localstorage', // storage
172
+ aF: number, // activeFactories
173
+ rF: number, // redundantActiveFactories
174
+ t?: Array<string>, // tags
175
+ }
176
+
177
+ // 'metrics/config' JSON request body
178
+ export type TelemetryConfigStatsPayload = TelemetryConfigStats & {
172
179
  sE: boolean, // streamingEnabled
173
180
  rR: RefreshRates, // refreshRates
174
181
  uO: UrlOverrides, // urlOverrides
@@ -177,11 +184,9 @@ export type TelemetryConfigStatsPayload = {
177
184
  iM: ImpressionsMode, // impressionsMode
178
185
  iL: boolean, // impressionsListenerEnabled
179
186
  hP: boolean, // httpProxyDetected
180
- aF: number, // activeFactories
181
- rF: number, // redundantActiveFactories
182
187
  tR: number, // timeUntilSDKReady
183
188
  tC?: number, // timeUntilSDKReadyFromCache
184
189
  nR: number, // SDKNotReadyUsage
185
- t?: Array<string>, // tags
186
190
  i?: Array<string>, // integrations
191
+ uC: number, // userConsent
187
192
  }
package/src/types.ts CHANGED
@@ -684,7 +684,7 @@ export namespace SplitIO {
684
684
  /**
685
685
  * String property to override the base URL where the SDK will post telemetry data.
686
686
  * @property {string} telemetry
687
- * @default 'https://telemetry.split.io'
687
+ * @default 'https://telemetry.split.io/api'
688
688
  */
689
689
  telemetry?: string
690
690
  };
@@ -8,8 +8,7 @@
8
8
  */
9
9
 
10
10
  export interface utfx {
11
- encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void,
12
-
11
+ encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void
13
12
  }
14
13
 
15
14
 
@@ -1,7 +1,7 @@
1
1
  import { merge } from '../lang';
2
2
  import { mode } from './mode';
3
3
  import { validateSplitFilters } from './splitFilters';
4
- import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE } from '../constants';
4
+ import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE, DEBUG } from '../constants';
5
5
  import { validImpressionsMode } from './impressionsMode';
6
6
  import { ISettingsValidationParams } from './types';
7
7
  import { ISettings } from '../../types';
@@ -34,8 +34,8 @@ export const base = {
34
34
  segmentsRefreshRate: 60,
35
35
  // publish telemetry stats each 3600 secs (1 hour)
36
36
  telemetryRefreshRate: 3600,
37
- // publish evaluations each 60 sec
38
- impressionsRefreshRate: 60,
37
+ // publish evaluations each 300 sec (default value for OPTIMIZED impressions mode)
38
+ impressionsRefreshRate: 300,
39
39
  // fetch offline changes each 15 sec
40
40
  offlineRefreshRate: 15,
41
41
  // publish events every 60 seconds after the first flush
@@ -58,7 +58,7 @@ export const base = {
58
58
  // Streaming Server
59
59
  streaming: 'https://streaming.split.io',
60
60
  // Telemetry Server
61
- telemetry: 'https://telemetry.split.io',
61
+ telemetry: 'https://telemetry.split.io/api',
62
62
  },
63
63
 
64
64
  // Defines which kind of storage we should instanciate.
@@ -113,6 +113,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
113
113
  const log = logger(withDefaults); // @ts-ignore, modify readonly prop
114
114
  withDefaults.log = log;
115
115
 
116
+ // ensure a valid impressionsMode
117
+ withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);
118
+
116
119
  function validateMinValue(paramName: string, actualValue: number, minValue: number) {
117
120
  if (actualValue >= minValue) return actualValue;
118
121
  // actualValue is not a number or is lower than minValue
@@ -124,11 +127,21 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
124
127
  const { scheduler, startup } = withDefaults;
125
128
  scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate);
126
129
  scheduler.segmentsRefreshRate = fromSecondsToMillis(scheduler.segmentsRefreshRate);
127
- scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate);
128
130
  scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate);
129
131
  scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate);
130
132
  scheduler.telemetryRefreshRate = fromSecondsToMillis(validateMinValue('telemetryRefreshRate', scheduler.telemetryRefreshRate, 60));
131
133
 
134
+ if (scheduler.impressionsRefreshRate !== base.scheduler.impressionsRefreshRate) {
135
+ // Validate impressionsRefreshRate defined by user
136
+ scheduler.impressionsRefreshRate = validateMinValue('impressionsRefreshRate', scheduler.impressionsRefreshRate,
137
+ withDefaults.sync.impressionsMode === DEBUG ? 1 : 60 // Min is 1 sec for DEBUG and 60 secs for OPTIMIZED
138
+ );
139
+ } else {
140
+ // Default impressionsRefreshRate for DEBUG mode is 60 secs
141
+ if (withDefaults.sync.impressionsMode === DEBUG) scheduler.impressionsRefreshRate = 60;
142
+ }
143
+ scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate);
144
+
132
145
  // Log deprecation for old telemetry param
133
146
  if (scheduler.metricsRefreshRate) log.warn('`metricsRefreshRate` will be deprecated soon. For configuring telemetry rates, update `telemetryRefreshRate` value in configs');
134
147
 
@@ -190,9 +203,6 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
190
203
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
191
204
  withDefaults.sync.__splitFiltersValidation = splitFiltersValidation;
192
205
 
193
- // ensure a valid impressionsMode
194
- withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);
195
-
196
206
  // ensure a valid user consent value
197
207
  // @ts-ignore, modify readonly prop
198
208
  withDefaults.userConsent = consent(withDefaults);
@@ -1,6 +1,6 @@
1
1
  import { ISettings } from '../../types';
2
2
 
3
- const telemetryEndpointMatcher = /^\/metrics\/(config|usage)/;
3
+ const telemetryEndpointMatcher = /^\/v1\/metrics\/(config|usage)/;
4
4
  const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/;
5
5
  const authEndpointMatcher = /^\/v2\/auth/;
6
6
  const streamingEndpointMatcher = /^\/(sse|event-stream)/;
@@ -6,8 +6,6 @@ import { ILogger } from '../logger/types';
6
6
  * It also updates logs related warnings and errors.
7
7
  *
8
8
  * @param readyTimeout time in millis to emit SDK_READY_TIME_OUT event
9
- * @param internalReadyCbCount offset value of SDK_READY listeners that are added/removed internally
10
- * by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected'
11
9
  * @param readinessManager optional readinessManager to use. only used internally for `shared` method
12
10
  */
13
- export declare function sdkReadinessManagerFactory(log: ILogger, EventEmitter: new () => IEventEmitter, readyTimeout?: number, internalReadyCbCount?: number, readinessManager?: import("./types").IReadinessManager): ISdkReadinessManager;
11
+ export declare function sdkReadinessManagerFactory(log: ILogger, EventEmitter: new () => IEventEmitter, readyTimeout?: number, readinessManager?: import("./types").IReadinessManager): ISdkReadinessManager;
@@ -49,7 +49,12 @@ export interface IReadinessManager {
49
49
  export interface ISdkReadinessManager {
50
50
  readinessManager: IReadinessManager;
51
51
  sdkStatus: IStatusInterface;
52
+ /**
53
+ * Increment internalReadyCbCount, an offset value of SDK_READY listeners that are added/removed internally
54
+ * by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected'
55
+ */
56
+ incInternalReadyCbCount(): void;
52
57
  /** for client-side */
53
- shared(readyTimeout?: number, internalReadyCbCount?: number): ISdkReadinessManager;
58
+ shared(readyTimeout?: number): ISdkReadinessManager;
54
59
  }
55
60
  export {};