@splitsoftware/splitio-commons 1.6.2-rc.9 → 1.7.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.
Files changed (176) hide show
  1. package/CHANGES.txt +6 -1
  2. package/cjs/consent/sdkUserConsent.js +2 -2
  3. package/cjs/listeners/browser.js +11 -12
  4. package/cjs/logger/constants.js +2 -1
  5. package/cjs/sdkClient/sdkClient.js +3 -1
  6. package/cjs/sdkFactory/index.js +26 -26
  7. package/cjs/services/splitApi.js +20 -0
  8. package/cjs/storages/AbstractSplitsCacheAsync.js +1 -1
  9. package/cjs/storages/AbstractSplitsCacheSync.js +1 -1
  10. package/cjs/storages/KeyBuilderSS.js +10 -43
  11. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +0 -1
  12. package/cjs/storages/inLocalStorage/index.js +17 -9
  13. package/cjs/storages/inMemory/ImpressionCountsCacheInMemory.js +12 -1
  14. package/cjs/storages/inMemory/InMemoryStorage.js +13 -6
  15. package/cjs/storages/inMemory/InMemoryStorageCS.js +13 -6
  16. package/cjs/storages/inMemory/TelemetryCacheInMemory.js +60 -35
  17. package/cjs/storages/inMemory/UniqueKeysCacheInMemory.js +72 -0
  18. package/cjs/storages/inMemory/UniqueKeysCacheInMemoryCS.js +76 -0
  19. package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +85 -0
  20. package/cjs/storages/inRedis/ImpressionsCacheInRedis.js +2 -19
  21. package/cjs/storages/inRedis/TelemetryCacheInRedis.js +4 -4
  22. package/cjs/storages/inRedis/UniqueKeysCacheInRedis.js +71 -0
  23. package/cjs/storages/inRedis/constants.js +4 -1
  24. package/cjs/storages/inRedis/index.js +20 -3
  25. package/cjs/storages/pluggable/ImpressionCountsCachePluggable.js +81 -0
  26. package/cjs/storages/pluggable/ImpressionsCachePluggable.js +2 -19
  27. package/cjs/storages/pluggable/TelemetryCachePluggable.js +4 -4
  28. package/cjs/storages/pluggable/UniqueKeysCachePluggable.js +61 -0
  29. package/cjs/storages/pluggable/inMemoryWrapper.js +8 -6
  30. package/cjs/storages/pluggable/index.js +38 -9
  31. package/cjs/storages/utils.js +73 -0
  32. package/cjs/sync/submitters/submitterManager.js +3 -0
  33. package/cjs/sync/submitters/telemetrySubmitter.js +5 -40
  34. package/cjs/sync/submitters/uniqueKeysSubmitter.js +27 -0
  35. package/cjs/trackers/impressionObserver/utils.js +1 -17
  36. package/cjs/trackers/impressionsTracker.js +22 -41
  37. package/cjs/trackers/strategy/strategyDebug.js +25 -0
  38. package/cjs/trackers/strategy/strategyNone.js +29 -0
  39. package/cjs/trackers/strategy/strategyOptimized.js +35 -0
  40. package/cjs/trackers/uniqueKeysTracker.js +38 -0
  41. package/cjs/utils/constants/index.js +4 -2
  42. package/cjs/utils/redis/RedisMock.js +31 -0
  43. package/cjs/utils/settingsValidation/impressionsMode.js +2 -2
  44. package/cjs/utils/settingsValidation/index.js +9 -3
  45. package/esm/consent/sdkUserConsent.js +2 -2
  46. package/esm/listeners/browser.js +12 -13
  47. package/esm/logger/constants.js +1 -0
  48. package/esm/sdkClient/sdkClient.js +3 -1
  49. package/esm/sdkFactory/index.js +26 -26
  50. package/esm/services/splitApi.js +20 -0
  51. package/esm/storages/AbstractSplitsCacheAsync.js +1 -1
  52. package/esm/storages/AbstractSplitsCacheSync.js +1 -1
  53. package/esm/storages/KeyBuilderSS.js +7 -37
  54. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +0 -1
  55. package/esm/storages/inLocalStorage/index.js +18 -10
  56. package/esm/storages/inMemory/ImpressionCountsCacheInMemory.js +12 -1
  57. package/esm/storages/inMemory/InMemoryStorage.js +14 -7
  58. package/esm/storages/inMemory/InMemoryStorageCS.js +14 -7
  59. package/esm/storages/inMemory/TelemetryCacheInMemory.js +61 -36
  60. package/esm/storages/inMemory/UniqueKeysCacheInMemory.js +68 -0
  61. package/esm/storages/inMemory/UniqueKeysCacheInMemoryCS.js +73 -0
  62. package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +82 -0
  63. package/esm/storages/inRedis/ImpressionsCacheInRedis.js +2 -19
  64. package/esm/storages/inRedis/TelemetryCacheInRedis.js +1 -1
  65. package/esm/storages/inRedis/UniqueKeysCacheInRedis.js +68 -0
  66. package/esm/storages/inRedis/constants.js +3 -0
  67. package/esm/storages/inRedis/index.js +21 -4
  68. package/esm/storages/pluggable/ImpressionCountsCachePluggable.js +78 -0
  69. package/esm/storages/pluggable/ImpressionsCachePluggable.js +2 -19
  70. package/esm/storages/pluggable/TelemetryCachePluggable.js +1 -1
  71. package/esm/storages/pluggable/UniqueKeysCachePluggable.js +58 -0
  72. package/esm/storages/pluggable/inMemoryWrapper.js +8 -6
  73. package/esm/storages/pluggable/index.js +39 -10
  74. package/esm/storages/utils.js +65 -0
  75. package/esm/sync/submitters/submitterManager.js +3 -0
  76. package/esm/sync/submitters/telemetrySubmitter.js +5 -39
  77. package/esm/sync/submitters/uniqueKeysSubmitter.js +23 -0
  78. package/esm/trackers/impressionObserver/utils.js +1 -15
  79. package/esm/trackers/impressionsTracker.js +22 -41
  80. package/esm/trackers/strategy/strategyDebug.js +21 -0
  81. package/esm/trackers/strategy/strategyNone.js +25 -0
  82. package/esm/trackers/strategy/strategyOptimized.js +31 -0
  83. package/esm/trackers/uniqueKeysTracker.js +34 -0
  84. package/esm/utils/constants/index.js +2 -0
  85. package/esm/utils/redis/RedisMock.js +28 -0
  86. package/esm/utils/settingsValidation/impressionsMode.js +3 -3
  87. package/esm/utils/settingsValidation/index.js +9 -3
  88. package/package.json +1 -2
  89. package/src/consent/sdkUserConsent.ts +2 -2
  90. package/src/listeners/browser.ts +12 -15
  91. package/src/logger/constants.ts +1 -0
  92. package/src/sdkClient/sdkClient.ts +3 -1
  93. package/src/sdkFactory/index.ts +29 -31
  94. package/src/sdkFactory/types.ts +7 -4
  95. package/src/services/splitApi.ts +22 -0
  96. package/src/services/types.ts +6 -0
  97. package/src/storages/AbstractSplitsCacheAsync.ts +1 -1
  98. package/src/storages/AbstractSplitsCacheSync.ts +1 -1
  99. package/src/storages/KeyBuilderSS.ts +9 -43
  100. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +0 -1
  101. package/src/storages/inLocalStorage/index.ts +18 -10
  102. package/src/storages/inMemory/AttributesCacheInMemory.ts +7 -7
  103. package/src/storages/inMemory/ImpressionCountsCacheInMemory.ts +16 -1
  104. package/src/storages/inMemory/InMemoryStorage.ts +14 -7
  105. package/src/storages/inMemory/InMemoryStorageCS.ts +14 -7
  106. package/src/storages/inMemory/TelemetryCacheInMemory.ts +69 -34
  107. package/src/storages/inMemory/UniqueKeysCacheInMemory.ts +80 -0
  108. package/src/storages/inMemory/UniqueKeysCacheInMemoryCS.ts +86 -0
  109. package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +95 -0
  110. package/src/storages/inRedis/ImpressionsCacheInRedis.ts +2 -22
  111. package/src/storages/inRedis/TelemetryCacheInRedis.ts +3 -2
  112. package/src/storages/inRedis/UniqueKeysCacheInRedis.ts +77 -0
  113. package/src/storages/inRedis/constants.ts +3 -0
  114. package/src/storages/inRedis/index.ts +18 -5
  115. package/src/storages/pluggable/ImpressionCountsCachePluggable.ts +92 -0
  116. package/src/storages/pluggable/ImpressionsCachePluggable.ts +3 -23
  117. package/src/storages/pluggable/TelemetryCachePluggable.ts +3 -2
  118. package/src/storages/pluggable/UniqueKeysCachePluggable.ts +67 -0
  119. package/src/storages/pluggable/inMemoryWrapper.ts +6 -6
  120. package/src/storages/pluggable/index.ts +41 -9
  121. package/src/storages/types.ts +56 -55
  122. package/src/storages/utils.ts +78 -0
  123. package/src/sync/submitters/submitter.ts +2 -2
  124. package/src/sync/submitters/submitterManager.ts +2 -0
  125. package/src/sync/submitters/telemetrySubmitter.ts +8 -43
  126. package/src/sync/submitters/types.ts +29 -27
  127. package/src/sync/submitters/uniqueKeysSubmitter.ts +36 -0
  128. package/src/trackers/impressionObserver/utils.ts +1 -16
  129. package/src/trackers/impressionsTracker.ts +25 -46
  130. package/src/trackers/strategy/strategyDebug.ts +28 -0
  131. package/src/trackers/strategy/strategyNone.ts +34 -0
  132. package/src/trackers/strategy/strategyOptimized.ts +42 -0
  133. package/src/trackers/types.ts +28 -0
  134. package/src/trackers/uniqueKeysTracker.ts +48 -0
  135. package/src/types.ts +1 -1
  136. package/src/utils/constants/index.ts +2 -0
  137. package/src/utils/redis/RedisMock.ts +33 -0
  138. package/src/utils/settingsValidation/impressionsMode.ts +3 -3
  139. package/src/utils/settingsValidation/index.ts +7 -3
  140. package/types/logger/constants.d.ts +1 -0
  141. package/types/sdkFactory/types.d.ts +4 -2
  142. package/types/services/types.d.ts +4 -0
  143. package/types/storages/AbstractSplitsCacheAsync.d.ts +1 -1
  144. package/types/storages/AbstractSplitsCacheSync.d.ts +1 -1
  145. package/types/storages/KeyBuilderSS.d.ts +3 -3
  146. package/types/storages/inLocalStorage/SplitsCacheInLocal.d.ts +0 -1
  147. package/types/storages/inMemory/ImpressionCountsCacheInMemory.d.ts +5 -1
  148. package/types/storages/inMemory/TelemetryCacheInMemory.d.ts +20 -9
  149. package/types/storages/inMemory/uniqueKeysCacheInMemory.d.ts +35 -0
  150. package/types/storages/inMemory/uniqueKeysCacheInMemoryCS.d.ts +35 -0
  151. package/types/storages/inRedis/ImpressionCountsCacheInRedis.d.ts +16 -0
  152. package/types/storages/inRedis/ImpressionsCacheInRedis.d.ts +0 -1
  153. package/types/storages/inRedis/constants.d.ts +3 -0
  154. package/types/storages/inRedis/uniqueKeysCacheInRedis.d.ts +21 -0
  155. package/types/storages/pluggable/ImpressionCountsCachePluggable.d.ts +16 -0
  156. package/types/storages/pluggable/ImpressionsCachePluggable.d.ts +1 -2
  157. package/types/storages/pluggable/UniqueKeysCachePluggable.d.ts +20 -0
  158. package/types/storages/types.d.ts +39 -38
  159. package/types/storages/utils.d.ts +8 -0
  160. package/types/sync/submitters/submitter.d.ts +2 -2
  161. package/types/sync/submitters/telemetrySubmitter.d.ts +2 -10
  162. package/types/sync/submitters/types.d.ts +27 -7
  163. package/types/sync/submitters/uniqueKeysSubmitter.d.ts +5 -0
  164. package/types/trackers/impressionObserver/utils.d.ts +0 -8
  165. package/types/trackers/impressionsTracker.d.ts +4 -6
  166. package/types/trackers/strategy/strategyDebug.d.ts +9 -0
  167. package/types/trackers/strategy/strategyNone.d.ts +10 -0
  168. package/types/trackers/strategy/strategyOptimized.d.ts +11 -0
  169. package/types/trackers/types.d.ts +23 -0
  170. package/types/trackers/uniqueKeysTracker.d.ts +13 -0
  171. package/types/types.d.ts +1 -1
  172. package/types/utils/constants/index.d.ts +2 -0
  173. package/types/utils/redis/RedisMock.d.ts +4 -0
  174. package/cjs/storages/metadataBuilder.js +0 -12
  175. package/esm/storages/metadataBuilder.js +0 -8
  176. package/src/storages/metadataBuilder.ts +0 -11
@@ -0,0 +1,86 @@
1
+ import { IUniqueKeysCacheBase } from '../types';
2
+ import { ISet, setToArray, _Set } from '../../utils/lang/sets';
3
+ import { UniqueKeysPayloadCs } from '../../sync/submitters/types';
4
+ import { DEFAULT_CACHE_SIZE } from '../inRedis/constants';
5
+
6
+ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
7
+
8
+ private onFullQueue?: () => void;
9
+ private readonly maxStorage: number;
10
+ private uniqueTrackerSize = 0;
11
+ private uniqueKeysTracker: { [userKey: string]: ISet<string> };
12
+
13
+ /**
14
+ *
15
+ * @param impressionsQueueSize number of queued impressions to call onFullQueueCb.
16
+ * Default value is 0, that means no maximum value, in case we want to avoid this being triggered.
17
+ */
18
+ constructor(uniqueKeysQueueSize = DEFAULT_CACHE_SIZE) {
19
+ this.maxStorage = uniqueKeysQueueSize;
20
+ this.uniqueKeysTracker = {};
21
+ }
22
+
23
+ setOnFullQueueCb(cb: () => void) {
24
+ this.onFullQueue = cb;
25
+ }
26
+
27
+ /**
28
+ * Store unique keys per feature.
29
+ */
30
+ track(userKey: string, featureName: string) {
31
+
32
+ if (!this.uniqueKeysTracker[userKey]) this.uniqueKeysTracker[userKey] = new _Set();
33
+ const tracker = this.uniqueKeysTracker[userKey];
34
+ if (!tracker.has(featureName)) {
35
+ tracker.add(featureName);
36
+ this.uniqueTrackerSize++;
37
+ }
38
+ if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
39
+ this.uniqueTrackerSize = 0;
40
+ this.onFullQueue();
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Clear the data stored on the cache.
46
+ */
47
+ clear() {
48
+ this.uniqueKeysTracker = {};
49
+ }
50
+
51
+ /**
52
+ * Pop the collected data, used as payload for posting.
53
+ */
54
+ pop() {
55
+ const data = this.uniqueKeysTracker;
56
+ this.uniqueKeysTracker = {};
57
+ return this.fromUniqueKeysCollector(data);
58
+ }
59
+
60
+ /**
61
+ * Check if the cache is empty.
62
+ */
63
+ isEmpty() {
64
+ return Object.keys(this.uniqueKeysTracker).length === 0;
65
+ }
66
+
67
+ /**
68
+ * Converts `uniqueKeys` data from cache into request payload.
69
+ */
70
+ private fromUniqueKeysCollector(uniqueKeys: { [userKey: string]: ISet<string> }): UniqueKeysPayloadCs {
71
+ const payload = [];
72
+ const userKeys = Object.keys(uniqueKeys);
73
+ for (let k = 0; k < userKeys.length; k++) {
74
+ const userKey = userKeys[k];
75
+ const featureNames = setToArray(uniqueKeys[userKey]);
76
+ const uniqueKeysPayload = {
77
+ k: userKey,
78
+ fs: featureNames
79
+ };
80
+
81
+ payload.push(uniqueKeysPayload);
82
+ }
83
+ return { keys: payload };
84
+ }
85
+
86
+ }
@@ -0,0 +1,95 @@
1
+ import { Redis } from 'ioredis';
2
+ import { ILogger } from '../../logger/types';
3
+ import { ImpressionCountsPayload } from '../../sync/submitters/types';
4
+ import { forOwn } from '../../utils/lang';
5
+ import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
6
+ import { LOG_PREFIX, REFRESH_RATE, TTL_REFRESH } from './constants';
7
+
8
+ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory {
9
+
10
+ private readonly log: ILogger;
11
+ private readonly key: string;
12
+ private readonly redis: Redis;
13
+ private readonly refreshRate: number;
14
+ private intervalId: any;
15
+
16
+ constructor(log: ILogger, key: string, redis: Redis, impressionCountsCacheSize?: number, refreshRate = REFRESH_RATE) {
17
+ super(impressionCountsCacheSize);
18
+ this.log = log;
19
+ this.key = key;
20
+ this.redis = redis;
21
+ this.refreshRate = refreshRate;
22
+ this.onFullQueue = () => { this.postImpressionCountsInRedis(); };
23
+ }
24
+
25
+ private postImpressionCountsInRedis() {
26
+ const counts = this.pop();
27
+ const keys = Object.keys(counts);
28
+ if (!keys.length) return Promise.resolve(false);
29
+
30
+ const pipeline = this.redis.pipeline();
31
+ keys.forEach(key => {
32
+ pipeline.hincrby(this.key, key, counts[key]);
33
+ });
34
+ return pipeline.exec()
35
+ .then(data => {
36
+ // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
37
+ if (data.length && data.length === keys.length) {
38
+ return this.redis.expire(this.key, TTL_REFRESH);
39
+ }
40
+ })
41
+ .catch(err => {
42
+ this.log.error(`${LOG_PREFIX}Error in impression counts pipeline: ${err}.`);
43
+ return false;
44
+ });
45
+ }
46
+
47
+ start() {
48
+ this.intervalId = setInterval(this.postImpressionCountsInRedis.bind(this), this.refreshRate);
49
+ }
50
+
51
+ stop() {
52
+ clearInterval(this.intervalId);
53
+ return this.postImpressionCountsInRedis();
54
+ }
55
+
56
+ // Async consumer API, used by synchronizer
57
+ getImpressionsCount(): Promise<ImpressionCountsPayload | undefined> {
58
+ return this.redis.hgetall(this.key)
59
+ .then(counts => {
60
+ if (!Object.keys(counts).length) return undefined;
61
+
62
+ this.redis.del(this.key).catch(() => { /* no-op */ });
63
+
64
+ const pf: ImpressionCountsPayload['pf'] = [];
65
+
66
+ forOwn(counts, (count, key) => {
67
+ const nameAndTime = key.split('::');
68
+ if (nameAndTime.length !== 2) {
69
+ this.log.error(`${LOG_PREFIX}Error spliting key ${key}`);
70
+ return;
71
+ }
72
+
73
+ const timeFrame = parseInt(nameAndTime[1]);
74
+ if (isNaN(timeFrame)) {
75
+ this.log.error(`${LOG_PREFIX}Error parsing time frame ${nameAndTime[1]}`);
76
+ return;
77
+ }
78
+
79
+ const rawCount = parseInt(count);
80
+ if (isNaN(rawCount)) {
81
+ this.log.error(`${LOG_PREFIX}Error parsing raw count ${count}`);
82
+ return;
83
+ }
84
+
85
+ pf.push({
86
+ f: nameAndTime[0],
87
+ m: timeFrame,
88
+ rc: rawCount,
89
+ });
90
+ });
91
+
92
+ return { pf };
93
+ });
94
+ }
95
+ }
@@ -4,6 +4,7 @@ import { ImpressionDTO } from '../../types';
4
4
  import { Redis } from 'ioredis';
5
5
  import { StoredImpressionWithMetadata } from '../../sync/submitters/types';
6
6
  import { ILogger } from '../../logger/types';
7
+ import { impressionsToJSON } from '../utils';
7
8
 
8
9
  const IMPRESSIONS_TTL_REFRESH = 3600; // 1 hr
9
10
 
@@ -24,7 +25,7 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync {
24
25
  track(impressions: ImpressionDTO[]): Promise<void> { // @ts-ignore
25
26
  return this.redis.rpush(
26
27
  this.key,
27
- this._toJSON(impressions)
28
+ impressionsToJSON(impressions, this.metadata),
28
29
  ).then(queuedCount => {
29
30
  // If this is the creation of the key on Redis, set the expiration for it in 1hr.
30
31
  if (queuedCount === impressions.length) {
@@ -33,27 +34,6 @@ export class ImpressionsCacheInRedis implements IImpressionsCacheAsync {
33
34
  });
34
35
  }
35
36
 
36
- private _toJSON(impressions: ImpressionDTO[]): string[] {
37
- return impressions.map(impression => {
38
- const {
39
- keyName, bucketingKey, feature, treatment, label, time, changeNumber
40
- } = impression;
41
-
42
- return JSON.stringify({
43
- m: this.metadata,
44
- i: {
45
- k: keyName,
46
- b: bucketingKey,
47
- f: feature,
48
- t: treatment,
49
- r: label,
50
- c: changeNumber,
51
- m: time
52
- }
53
- });
54
- });
55
- }
56
-
57
37
  count(): Promise<number> {
58
38
  return this.redis.llen(this.key).catch(() => 0);
59
39
  }
@@ -1,6 +1,6 @@
1
1
  import { ILogger } from '../../logger/types';
2
2
  import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } from '../../sync/submitters/types';
3
- import { KeyBuilderSS, parseExceptionField, parseLatencyField, parseMetadata } from '../KeyBuilderSS';
3
+ import { KeyBuilderSS } from '../KeyBuilderSS';
4
4
  import { ITelemetryCacheAsync } from '../types';
5
5
  import { findLatencyIndex } from '../findLatencyIndex';
6
6
  import { Redis } from 'ioredis';
@@ -9,6 +9,7 @@ import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants';
9
9
  import { isNaNNumber, isString } from '../../utils/lang';
10
10
  import { _Map } from '../../utils/lang/maps';
11
11
  import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory';
12
+ import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils';
12
13
 
13
14
  export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
14
15
 
@@ -76,7 +77,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
76
77
  tr: newBuckets(),
77
78
  });
78
79
 
79
- result.get(metadata)![method][bucket] = count;
80
+ result.get(metadata)![method]![bucket] = count;
80
81
  });
81
82
 
82
83
  return this.redis.del(this.keys.latencyPrefix).then(() => result);
@@ -0,0 +1,77 @@
1
+ import { IUniqueKeysCacheBase } from '../types';
2
+ import { Redis } from 'ioredis';
3
+ import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
4
+ import { setToArray } from '../../utils/lang/sets';
5
+ import { DEFAULT_CACHE_SIZE, REFRESH_RATE, TTL_REFRESH } from './constants';
6
+ import { LOG_PREFIX } from './constants';
7
+ import { ILogger } from '../../logger/types';
8
+ import { UniqueKeysItemSs } from '../../sync/submitters/types';
9
+
10
+ export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
11
+
12
+ private readonly log: ILogger;
13
+ private readonly key: string;
14
+ private readonly redis: Redis;
15
+ private readonly refreshRate: number;
16
+ private intervalId: any;
17
+
18
+ constructor(log: ILogger, key: string, redis: Redis, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) {
19
+ super(uniqueKeysQueueSize);
20
+ this.log = log;
21
+ this.key = key;
22
+ this.redis = redis;
23
+ this.refreshRate = refreshRate;
24
+ this.onFullQueue = () => { this.postUniqueKeysInRedis(); };
25
+ }
26
+
27
+ private postUniqueKeysInRedis() {
28
+ const featureNames = Object.keys(this.uniqueKeysTracker);
29
+ if (!featureNames.length) return Promise.resolve(false);
30
+
31
+ const pipeline = this.redis.pipeline();
32
+ for (let i = 0; i < featureNames.length; i++) {
33
+ const featureName = featureNames[i];
34
+ const featureKeys = setToArray(this.uniqueKeysTracker[featureName]);
35
+ const uniqueKeysPayload = {
36
+ f: featureName,
37
+ ks: featureKeys
38
+ };
39
+
40
+ pipeline.rpush(this.key, JSON.stringify(uniqueKeysPayload));
41
+ }
42
+ this.clear();
43
+ return pipeline.exec()
44
+ .then(data => {
45
+ // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
46
+ if (data.length && data.length === featureNames.length) {
47
+ return this.redis.expire(this.key, TTL_REFRESH);
48
+ }
49
+ })
50
+ .catch(err => {
51
+ this.log.error(`${LOG_PREFIX}Error in uniqueKeys pipeline: ${err}.`);
52
+ return false;
53
+ });
54
+ }
55
+
56
+
57
+ start() {
58
+ this.intervalId = setInterval(this.postUniqueKeysInRedis.bind(this), this.refreshRate);
59
+ }
60
+
61
+ stop() {
62
+ clearInterval(this.intervalId);
63
+ return this.postUniqueKeysInRedis();
64
+ }
65
+
66
+ /**
67
+ * Async consumer API, used by synchronizer.
68
+ * @param count number of items to pop from the queue. If not provided or equal 0, all items will be popped.
69
+ */
70
+ popNRaw(count = 0): Promise<UniqueKeysItemSs[]> {
71
+ return this.redis.lrange(this.key, 0, count - 1).then(uniqueKeyItems => {
72
+ return this.redis.ltrim(this.key, uniqueKeyItems.length, -1)
73
+ .then(() => uniqueKeyItems.map(uniqueKeyItem => JSON.parse(uniqueKeyItem)));
74
+ });
75
+ }
76
+
77
+ }
@@ -1 +1,4 @@
1
1
  export const LOG_PREFIX = 'storage:redis: ';
2
+ export const DEFAULT_CACHE_SIZE = 30000;
3
+ export const REFRESH_RATE = 300000; // 300.000 ms = start after 5 mins
4
+ export const TTL_REFRESH = 3600; // 1hr
@@ -6,8 +6,11 @@ import { SplitsCacheInRedis } from './SplitsCacheInRedis';
6
6
  import { SegmentsCacheInRedis } from './SegmentsCacheInRedis';
7
7
  import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis';
8
8
  import { EventsCacheInRedis } from './EventsCacheInRedis';
9
- import { STORAGE_REDIS } from '../../utils/constants';
9
+ import { DEBUG, NONE, STORAGE_REDIS } from '../../utils/constants';
10
10
  import { TelemetryCacheInRedis } from './TelemetryCacheInRedis';
11
+ import { UniqueKeysCacheInRedis } from './UniqueKeysCacheInRedis';
12
+ import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis';
13
+ import { metadataBuilder } from '../utils';
11
14
 
12
15
  export interface InRedisStorageOptions {
13
16
  prefix?: string
@@ -22,15 +25,20 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
22
25
 
23
26
  const prefix = validatePrefix(options.prefix);
24
27
 
25
- function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync {
26
-
28
+ function InRedisStorageFactory(params: IStorageFactoryParams): IStorageAsync {
29
+ const { onReadyCb, settings, settings: { log, sync: { impressionsMode } } } = params;
30
+ const metadata = metadataBuilder(settings);
27
31
  const keys = new KeyBuilderSS(prefix, metadata);
28
32
  const redisClient = new RedisAdapter(log, options.options || {});
29
33
  const telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
34
+ const impressionCountsCache = impressionsMode !== DEBUG ? new ImpressionCountsCacheInRedis(log, keys.buildImpressionsCountKey(), redisClient) : undefined;
35
+ const uniqueKeysCache = impressionsMode === NONE ? new UniqueKeysCacheInRedis(log, keys.buildUniqueKeysKey(), redisClient) : undefined;
30
36
 
31
37
  // subscription to Redis connect event in order to emit SDK_READY event on consumer mode
32
38
  redisClient.on('connect', () => {
33
39
  onReadyCb();
40
+ if (impressionCountsCache) impressionCountsCache.start();
41
+ if (uniqueKeysCache) uniqueKeysCache.start();
34
42
 
35
43
  // Synchronize config
36
44
  telemetry.recordConfig();
@@ -40,13 +48,18 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
40
48
  splits: new SplitsCacheInRedis(log, keys, redisClient),
41
49
  segments: new SegmentsCacheInRedis(log, keys, redisClient),
42
50
  impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata),
51
+ impressionCounts: impressionCountsCache,
43
52
  events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata),
44
53
  telemetry,
54
+ uniqueKeys: uniqueKeysCache,
45
55
 
46
56
  // When using REDIS we should:
47
57
  // 1- Disconnect from the storage
48
- destroy() {
49
- redisClient.disconnect();
58
+ destroy(): Promise<void> {
59
+ let promises = [];
60
+ if (impressionCountsCache) promises.push(impressionCountsCache.stop());
61
+ if (uniqueKeysCache) promises.push(uniqueKeysCache.stop());
62
+ return Promise.all(promises).then(() => { redisClient.disconnect(); });
50
63
  // @TODO check that caches works as expected when redisClient is disconnected
51
64
  }
52
65
  };
@@ -0,0 +1,92 @@
1
+ import { ILogger } from '../../logger/types';
2
+ import { ImpressionCountsPayload } from '../../sync/submitters/types';
3
+ import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
4
+ import { REFRESH_RATE } from '../inRedis/constants';
5
+ import { IPluggableStorageWrapper } from '../types';
6
+ import { LOG_PREFIX } from './constants';
7
+
8
+ export class ImpressionCountsCachePluggable extends ImpressionCountsCacheInMemory {
9
+
10
+ private readonly log: ILogger;
11
+ private readonly key: string;
12
+ private readonly wrapper: IPluggableStorageWrapper;
13
+ private readonly refreshRate: number;
14
+ private intervalId: any;
15
+
16
+ constructor(log: ILogger, key: string, wrapper: IPluggableStorageWrapper, impressionCountsCacheSize?: number, refreshRate = REFRESH_RATE) {
17
+ super(impressionCountsCacheSize);
18
+ this.log = log;
19
+ this.key = key;
20
+ this.wrapper = wrapper;
21
+ this.refreshRate = refreshRate;
22
+ this.onFullQueue = () => { this.storeImpressionCounts(); };
23
+ }
24
+
25
+ private storeImpressionCounts() {
26
+ const counts = this.pop();
27
+ const keys = Object.keys(counts);
28
+ if (!keys.length) return Promise.resolve(false);
29
+
30
+ const pipeline = keys.reduce<Promise<any>>((pipeline, key) => {
31
+ return pipeline.then(() => this.wrapper.incr(`${this.key}::${key}`, counts[key]));
32
+ }, Promise.resolve());
33
+
34
+ return pipeline.catch(err => {
35
+ this.log.error(`${LOG_PREFIX}Error in impression counts pipeline: ${err}.`);
36
+ return false;
37
+ });
38
+ }
39
+
40
+ start() {
41
+ this.intervalId = setInterval(this.storeImpressionCounts.bind(this), this.refreshRate);
42
+ }
43
+
44
+ stop() {
45
+ clearInterval(this.intervalId);
46
+ return this.storeImpressionCounts();
47
+ }
48
+
49
+ // Async consumer API, used by synchronizer
50
+ getImpressionsCount(): Promise<ImpressionCountsPayload | undefined> {
51
+ return this.wrapper.getKeysByPrefix(this.key)
52
+ .then(keys => {
53
+ return keys.length ? Promise.all(keys.map(key => this.wrapper.get(key)))
54
+ .then(counts => {
55
+ keys.forEach(key => this.wrapper.del(key).catch(() => { /* noop */ }));
56
+
57
+ const pf = [];
58
+
59
+ for (let i = 0; i < keys.length; i++) {
60
+ const key = keys[i];
61
+ const count = counts[i];
62
+
63
+ const keyFeatureNameAndTime = key.split('::');
64
+ if (keyFeatureNameAndTime.length !== 3) {
65
+ this.log.error(`${LOG_PREFIX}Error spliting key ${key}`);
66
+ continue;
67
+ }
68
+
69
+ const timeFrame = parseInt(keyFeatureNameAndTime[2]);
70
+ if (isNaN(timeFrame)) {
71
+ this.log.error(`${LOG_PREFIX}Error parsing time frame ${keyFeatureNameAndTime[2]}`);
72
+ continue;
73
+ }
74
+ // @ts-ignore
75
+ const rawCount = parseInt(count);
76
+ if (isNaN(rawCount)) {
77
+ this.log.error(`${LOG_PREFIX}Error parsing raw count ${count}`);
78
+ continue;
79
+ }
80
+
81
+ pf.push({
82
+ f: keyFeatureNameAndTime[1],
83
+ m: timeFrame,
84
+ rc: rawCount,
85
+ });
86
+ }
87
+
88
+ return { pf };
89
+ }) : undefined;
90
+ });
91
+ }
92
+ }
@@ -1,8 +1,9 @@
1
1
  import { IPluggableStorageWrapper, IImpressionsCacheAsync } from '../types';
2
2
  import { IMetadata } from '../../dtos/types';
3
3
  import { ImpressionDTO } from '../../types';
4
- import { ILogger } from '../../logger/types';
5
4
  import { StoredImpressionWithMetadata } from '../../sync/submitters/types';
5
+ import { ILogger } from '../../logger/types';
6
+ import { impressionsToJSON } from '../utils';
6
7
 
7
8
  export class ImpressionsCachePluggable implements IImpressionsCacheAsync {
8
9
 
@@ -27,31 +28,10 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync {
27
28
  track(impressions: ImpressionDTO[]): Promise<void> {
28
29
  return this.wrapper.pushItems(
29
30
  this.key,
30
- this._toJSON(impressions)
31
+ impressionsToJSON(impressions, this.metadata)
31
32
  );
32
33
  }
33
34
 
34
- private _toJSON(impressions: ImpressionDTO[]): string[] {
35
- return impressions.map(impression => {
36
- const {
37
- keyName, bucketingKey, feature, treatment, label, time, changeNumber
38
- } = impression;
39
-
40
- return JSON.stringify({
41
- m: this.metadata,
42
- i: {
43
- k: keyName,
44
- b: bucketingKey,
45
- f: feature,
46
- t: treatment,
47
- r: label,
48
- c: changeNumber,
49
- m: time
50
- }
51
- } as StoredImpressionWithMetadata);
52
- });
53
- }
54
-
55
35
  /**
56
36
  * Returns a promise that resolves with the count of stored impressions, or 0 if there was some error.
57
37
  * The promise will never be rejected.
@@ -1,6 +1,6 @@
1
1
  import { ILogger } from '../../logger/types';
2
2
  import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } from '../../sync/submitters/types';
3
- import { KeyBuilderSS, parseExceptionField, parseLatencyField, parseMetadata } from '../KeyBuilderSS';
3
+ import { KeyBuilderSS } from '../KeyBuilderSS';
4
4
  import { IPluggableStorageWrapper, ITelemetryCacheAsync } from '../types';
5
5
  import { findLatencyIndex } from '../findLatencyIndex';
6
6
  import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter';
@@ -8,6 +8,7 @@ import { CONSUMER_MODE, STORAGE_PLUGGABLE } from '../../utils/constants';
8
8
  import { isString, isNaNNumber } from '../../utils/lang';
9
9
  import { _Map } from '../../utils/lang/maps';
10
10
  import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory';
11
+ import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils';
11
12
 
12
13
  export class TelemetryCachePluggable implements ITelemetryCacheAsync {
13
14
 
@@ -75,7 +76,7 @@ export class TelemetryCachePluggable implements ITelemetryCacheAsync {
75
76
  tr: newBuckets(),
76
77
  });
77
78
 
78
- result.get(metadata)![method][bucket] = count;
79
+ result.get(metadata)![method]![bucket] = count;
79
80
  }
80
81
 
81
82
  return Promise.all(latencyKeys.map((latencyKey) => this.wrapper.del(latencyKey))).then(() => result);
@@ -0,0 +1,67 @@
1
+ import { IPluggableStorageWrapper, IUniqueKeysCacheBase } from '../types';
2
+ import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
3
+ import { setToArray } from '../../utils/lang/sets';
4
+ import { DEFAULT_CACHE_SIZE, REFRESH_RATE } from '../inRedis/constants';
5
+ import { LOG_PREFIX } from './constants';
6
+ import { ILogger } from '../../logger/types';
7
+ import { UniqueKeysItemSs } from '../../sync/submitters/types';
8
+
9
+ export class UniqueKeysCachePluggable extends UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
10
+
11
+ private readonly log: ILogger;
12
+ private readonly key: string;
13
+ private readonly wrapper: IPluggableStorageWrapper;
14
+ private readonly refreshRate: number;
15
+ private intervalId: any;
16
+
17
+ constructor(log: ILogger, key: string, wrapper: IPluggableStorageWrapper, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) {
18
+ super(uniqueKeysQueueSize);
19
+ this.log = log;
20
+ this.key = key;
21
+ this.wrapper = wrapper;
22
+ this.refreshRate = refreshRate;
23
+ this.onFullQueue = () => { this.storeUniqueKeys(); };
24
+ }
25
+
26
+ storeUniqueKeys() {
27
+ const featureNames = Object.keys(this.uniqueKeysTracker);
28
+ if (!featureNames.length) return Promise.resolve(false);
29
+
30
+ const pipeline = featureNames.reduce<Promise<any>>((pipeline, featureName) => {
31
+ const featureKeys = setToArray(this.uniqueKeysTracker[featureName]);
32
+ const uniqueKeysPayload = {
33
+ f: featureName,
34
+ ks: featureKeys
35
+ };
36
+
37
+ return pipeline.then(() => this.wrapper.pushItems(this.key, [JSON.stringify(uniqueKeysPayload)]));
38
+ }, Promise.resolve());
39
+
40
+ this.clear();
41
+ return pipeline.catch(err => {
42
+ this.log.error(`${LOG_PREFIX}Error in uniqueKeys pipeline: ${err}.`);
43
+ return false;
44
+ });
45
+ }
46
+
47
+
48
+ start() {
49
+ this.intervalId = setInterval(this.storeUniqueKeys.bind(this), this.refreshRate);
50
+ }
51
+
52
+ stop() {
53
+ clearInterval(this.intervalId);
54
+ return this.storeUniqueKeys();
55
+ }
56
+
57
+ /**
58
+ * Async consumer API, used by synchronizer.
59
+ * @param count number of items to pop from the queue. If not provided or equal 0, all items will be popped.
60
+ */
61
+ popNRaw(count = 0): Promise<UniqueKeysItemSs[]> {
62
+ return Promise.resolve(count || this.wrapper.getItemsCount(this.key))
63
+ .then(count => this.wrapper.popItems(this.key, count))
64
+ .then((uniqueKeyItems) => uniqueKeyItems.map(uniqueKeyItem => JSON.parse(uniqueKeyItem)));
65
+ }
66
+
67
+ }
@@ -39,25 +39,25 @@ export function inMemoryWrapperFactory(connDelay?: number): IPluggableStorageWra
39
39
  getKeysByPrefix(prefix: string) {
40
40
  return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)));
41
41
  },
42
- incr(key: string) {
42
+ incr(key: string, increment = 1) {
43
43
  if (key in _cache) {
44
- const count = toNumber(_cache[key]) + 1;
44
+ const count = toNumber(_cache[key]) + increment;
45
45
  if (isNaN(count)) return Promise.reject('Given key is not a number');
46
46
  _cache[key] = count + '';
47
47
  return Promise.resolve(count);
48
48
  } else {
49
- _cache[key] = '1';
49
+ _cache[key] = '' + increment;
50
50
  return Promise.resolve(1);
51
51
  }
52
52
  },
53
- decr(key: string) {
53
+ decr(key: string, decrement = 1) {
54
54
  if (key in _cache) {
55
- const count = toNumber(_cache[key]) - 1;
55
+ const count = toNumber(_cache[key]) - decrement;
56
56
  if (isNaN(count)) return Promise.reject('Given key is not a number');
57
57
  _cache[key] = count + '';
58
58
  return Promise.resolve(count);
59
59
  } else {
60
- _cache[key] = '-1';
60
+ _cache[key] = '-' + decrement;
61
61
  return Promise.resolve(-1);
62
62
  }
63
63
  },