@splitsoftware/splitio-commons 1.6.2-rc.8 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGES.txt +4 -1
  2. package/cjs/evaluator/index.js +5 -5
  3. package/cjs/listeners/browser.js +9 -11
  4. package/cjs/sdkClient/client.js +19 -7
  5. package/cjs/sdkFactory/index.js +7 -25
  6. package/cjs/services/splitApi.js +4 -4
  7. package/cjs/storages/AbstractSplitsCacheAsync.js +1 -1
  8. package/cjs/storages/AbstractSplitsCacheSync.js +1 -1
  9. package/cjs/storages/KeyBuilderSS.js +9 -9
  10. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +0 -1
  11. package/cjs/storages/inLocalStorage/index.js +15 -11
  12. package/cjs/storages/inMemory/InMemoryStorage.js +11 -8
  13. package/cjs/storages/inMemory/InMemoryStorageCS.js +11 -8
  14. package/cjs/storages/inMemory/TelemetryCacheInMemory.js +65 -37
  15. package/cjs/storages/inMemory/{uniqueKeysCacheInMemory.js → UniqueKeysCacheInMemory.js} +24 -25
  16. package/cjs/storages/inMemory/{uniqueKeysCacheInMemoryCS.js → UniqueKeysCacheInMemoryCS.js} +10 -12
  17. package/cjs/storages/inRedis/EventsCacheInRedis.js +1 -1
  18. package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +37 -2
  19. package/cjs/storages/inRedis/ImpressionsCacheInRedis.js +2 -19
  20. package/cjs/storages/inRedis/TelemetryCacheInRedis.js +100 -0
  21. package/cjs/storages/inRedis/{uniqueKeysCacheInRedis.js → UniqueKeysCacheInRedis.js} +16 -4
  22. package/cjs/storages/inRedis/index.js +6 -4
  23. package/cjs/storages/pluggable/ImpressionCountsCachePluggable.js +81 -0
  24. package/cjs/storages/pluggable/ImpressionsCachePluggable.js +2 -19
  25. package/cjs/storages/pluggable/TelemetryCachePluggable.js +126 -0
  26. package/cjs/storages/pluggable/UniqueKeysCachePluggable.js +61 -0
  27. package/cjs/storages/pluggable/inMemoryWrapper.js +8 -6
  28. package/cjs/storages/pluggable/index.js +51 -18
  29. package/cjs/storages/utils.js +73 -0
  30. package/cjs/sync/submitters/submitterManager.js +1 -1
  31. package/cjs/sync/submitters/telemetrySubmitter.js +4 -37
  32. package/cjs/sync/submitters/uniqueKeysSubmitter.js +4 -3
  33. package/cjs/trackers/impressionObserver/utils.js +1 -17
  34. package/cjs/trackers/uniqueKeysTracker.js +1 -1
  35. package/cjs/utils/lang/maps.js +15 -7
  36. package/cjs/utils/redis/RedisMock.js +31 -0
  37. package/cjs/utils/settingsValidation/index.js +7 -4
  38. package/esm/evaluator/index.js +5 -5
  39. package/esm/listeners/browser.js +9 -11
  40. package/esm/sdkClient/client.js +19 -7
  41. package/esm/sdkFactory/index.js +7 -25
  42. package/esm/services/splitApi.js +4 -4
  43. package/esm/storages/AbstractSplitsCacheAsync.js +1 -1
  44. package/esm/storages/AbstractSplitsCacheSync.js +1 -1
  45. package/esm/storages/KeyBuilderSS.js +8 -8
  46. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +0 -1
  47. package/esm/storages/inLocalStorage/index.js +16 -12
  48. package/esm/storages/inMemory/InMemoryStorage.js +13 -10
  49. package/esm/storages/inMemory/InMemoryStorageCS.js +12 -9
  50. package/esm/storages/inMemory/TelemetryCacheInMemory.js +64 -37
  51. package/esm/storages/inMemory/{uniqueKeysCacheInMemory.js → UniqueKeysCacheInMemory.js} +22 -24
  52. package/esm/storages/inMemory/{uniqueKeysCacheInMemoryCS.js → UniqueKeysCacheInMemoryCS.js} +10 -12
  53. package/esm/storages/inRedis/EventsCacheInRedis.js +1 -1
  54. package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +37 -2
  55. package/esm/storages/inRedis/ImpressionsCacheInRedis.js +2 -19
  56. package/esm/storages/inRedis/TelemetryCacheInRedis.js +100 -0
  57. package/esm/storages/inRedis/{uniqueKeysCacheInRedis.js → UniqueKeysCacheInRedis.js} +15 -3
  58. package/esm/storages/inRedis/index.js +5 -3
  59. package/esm/storages/pluggable/ImpressionCountsCachePluggable.js +78 -0
  60. package/esm/storages/pluggable/ImpressionsCachePluggable.js +2 -19
  61. package/esm/storages/pluggable/TelemetryCachePluggable.js +126 -0
  62. package/esm/storages/pluggable/UniqueKeysCachePluggable.js +58 -0
  63. package/esm/storages/pluggable/inMemoryWrapper.js +8 -6
  64. package/esm/storages/pluggable/index.js +52 -19
  65. package/esm/storages/utils.js +65 -0
  66. package/esm/sync/submitters/submitterManager.js +1 -1
  67. package/esm/sync/submitters/telemetrySubmitter.js +4 -36
  68. package/esm/sync/submitters/uniqueKeysSubmitter.js +4 -3
  69. package/esm/trackers/impressionObserver/utils.js +1 -15
  70. package/esm/trackers/uniqueKeysTracker.js +1 -1
  71. package/esm/utils/lang/maps.js +15 -7
  72. package/esm/utils/redis/RedisMock.js +28 -0
  73. package/esm/utils/settingsValidation/index.js +7 -4
  74. package/package.json +2 -2
  75. package/src/consent/sdkUserConsent.ts +1 -1
  76. package/src/evaluator/index.ts +6 -6
  77. package/src/listeners/browser.ts +9 -13
  78. package/src/logger/.DS_Store +0 -0
  79. package/src/sdkClient/client.ts +21 -8
  80. package/src/sdkClient/sdkClient.ts +1 -1
  81. package/src/sdkFactory/index.ts +10 -33
  82. package/src/sdkFactory/types.ts +2 -2
  83. package/src/services/splitApi.ts +6 -6
  84. package/src/services/types.ts +2 -2
  85. package/src/storages/AbstractSplitsCacheAsync.ts +1 -1
  86. package/src/storages/AbstractSplitsCacheSync.ts +1 -1
  87. package/src/storages/KeyBuilderSS.ts +13 -11
  88. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +0 -1
  89. package/src/storages/inLocalStorage/index.ts +17 -12
  90. package/src/storages/inMemory/AttributesCacheInMemory.ts +7 -7
  91. package/src/storages/inMemory/ImpressionCountsCacheInMemory.ts +2 -2
  92. package/src/storages/inMemory/InMemoryStorage.ts +14 -10
  93. package/src/storages/inMemory/InMemoryStorageCS.ts +13 -10
  94. package/src/storages/inMemory/TelemetryCacheInMemory.ts +72 -35
  95. package/src/storages/inMemory/{uniqueKeysCacheInMemory.ts → UniqueKeysCacheInMemory.ts} +26 -28
  96. package/src/storages/inMemory/{uniqueKeysCacheInMemoryCS.ts → UniqueKeysCacheInMemoryCS.ts} +15 -17
  97. package/src/storages/inRedis/EventsCacheInRedis.ts +1 -1
  98. package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +51 -8
  99. package/src/storages/inRedis/ImpressionsCacheInRedis.ts +2 -22
  100. package/src/storages/inRedis/TelemetryCacheInRedis.ts +122 -1
  101. package/src/storages/inRedis/{uniqueKeysCacheInRedis.ts → UniqueKeysCacheInRedis.ts} +25 -12
  102. package/src/storages/inRedis/index.ts +6 -3
  103. package/src/storages/pluggable/ImpressionCountsCachePluggable.ts +92 -0
  104. package/src/storages/pluggable/ImpressionsCachePluggable.ts +3 -23
  105. package/src/storages/pluggable/TelemetryCachePluggable.ts +147 -1
  106. package/src/storages/pluggable/UniqueKeysCachePluggable.ts +67 -0
  107. package/src/storages/pluggable/inMemoryWrapper.ts +6 -6
  108. package/src/storages/pluggable/index.ts +56 -20
  109. package/src/storages/types.ts +53 -70
  110. package/src/storages/utils.ts +78 -0
  111. package/src/sync/submitters/submitter.ts +2 -2
  112. package/src/sync/submitters/submitterManager.ts +1 -1
  113. package/src/sync/submitters/telemetrySubmitter.ts +9 -39
  114. package/src/sync/submitters/types.ts +33 -17
  115. package/src/sync/submitters/uniqueKeysSubmitter.ts +6 -5
  116. package/src/trackers/impressionObserver/utils.ts +1 -16
  117. package/src/trackers/impressionsTracker.ts +2 -2
  118. package/src/trackers/strategy/strategyDebug.ts +4 -4
  119. package/src/trackers/strategy/strategyNone.ts +9 -9
  120. package/src/trackers/strategy/strategyOptimized.ts +9 -9
  121. package/src/trackers/uniqueKeysTracker.ts +6 -6
  122. package/src/types.ts +0 -2
  123. package/src/utils/lang/maps.ts +20 -8
  124. package/src/utils/redis/RedisMock.ts +33 -0
  125. package/src/utils/settingsValidation/index.ts +5 -5
  126. package/types/services/types.d.ts +2 -2
  127. package/types/storages/AbstractSplitsCacheAsync.d.ts +1 -1
  128. package/types/storages/AbstractSplitsCacheSync.d.ts +1 -1
  129. package/types/storages/KeyBuilderSS.d.ts +5 -2
  130. package/types/storages/inLocalStorage/SplitsCacheInLocal.d.ts +0 -1
  131. package/types/storages/inMemory/TelemetryCacheInMemory.d.ts +23 -9
  132. package/types/storages/inMemory/uniqueKeysCacheInMemory.d.ts +9 -9
  133. package/types/storages/inMemory/uniqueKeysCacheInMemoryCS.d.ts +2 -4
  134. package/types/storages/inRedis/EventsCacheInRedis.d.ts +1 -1
  135. package/types/storages/inRedis/ImpressionCountsCacheInRedis.d.ts +3 -1
  136. package/types/storages/inRedis/ImpressionsCacheInRedis.d.ts +0 -1
  137. package/types/storages/inRedis/TelemetryCacheInRedis.d.ts +16 -1
  138. package/types/storages/inRedis/uniqueKeysCacheInRedis.d.ts +8 -2
  139. package/types/storages/pluggable/ImpressionCountsCachePluggable.d.ts +16 -0
  140. package/types/storages/pluggable/ImpressionsCachePluggable.d.ts +1 -2
  141. package/types/storages/pluggable/TelemetryCachePluggable.d.ts +17 -1
  142. package/types/storages/pluggable/UniqueKeysCachePluggable.d.ts +20 -0
  143. package/types/storages/types.d.ts +42 -49
  144. package/types/storages/utils.d.ts +8 -0
  145. package/types/sync/submitters/submitter.d.ts +2 -2
  146. package/types/sync/submitters/telemetrySubmitter.d.ts +2 -10
  147. package/types/sync/submitters/types.d.ts +27 -18
  148. package/types/trackers/impressionObserver/utils.d.ts +0 -8
  149. package/types/trackers/strategy/strategyNone.d.ts +2 -2
  150. package/types/trackers/strategy/strategyOptimized.d.ts +2 -2
  151. package/types/trackers/uniqueKeysTracker.d.ts +1 -1
  152. package/types/types.d.ts +0 -2
  153. package/types/utils/lang/maps.d.ts +6 -2
  154. package/types/utils/redis/RedisMock.d.ts +4 -0
  155. package/types/utils/settingsValidation/index.d.ts +0 -1
  156. package/cjs/storages/metadataBuilder.js +0 -12
  157. package/esm/storages/metadataBuilder.js +0 -8
  158. package/src/storages/metadataBuilder.ts +0 -11
  159. package/types/sdkClient/types.d.ts +0 -18
  160. package/types/storages/inMemory/CountsCacheInMemory.d.ts +0 -20
  161. package/types/storages/inMemory/LatenciesCacheInMemory.d.ts +0 -20
  162. package/types/storages/inRedis/CountsCacheInRedis.d.ts +0 -9
  163. package/types/storages/inRedis/LatenciesCacheInRedis.d.ts +0 -9
  164. package/types/sync/offline/LocalhostFromFile.d.ts +0 -2
  165. package/types/sync/offline/splitsParser/splitsParserFromFile.d.ts +0 -2
  166. package/types/sync/submitters/eventsSyncTask.d.ts +0 -8
  167. package/types/sync/submitters/impressionCountsSubmitterInRedis.d.ts +0 -5
  168. package/types/sync/submitters/impressionCountsSyncTask.d.ts +0 -13
  169. package/types/sync/submitters/impressionsSyncTask.d.ts +0 -14
  170. package/types/sync/submitters/metricsSyncTask.d.ts +0 -12
  171. package/types/sync/submitters/submitterSyncTask.d.ts +0 -10
  172. package/types/sync/submitters/uniqueKeysSubmitterInRedis.d.ts +0 -5
  173. package/types/sync/syncTaskComposite.d.ts +0 -5
  174. package/types/trackers/filter/bloomFilter.d.ts +0 -10
  175. package/types/trackers/filter/dictionaryFilter.d.ts +0 -8
  176. package/types/trackers/filter/types.d.ts +0 -5
  177. package/types/utils/timeTracker/index.d.ts +0 -70
@@ -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,11 +1,15 @@
1
1
  import { ILogger } from '../../logger/types';
2
- import { Method } from '../../sync/submitters/types';
2
+ import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } from '../../sync/submitters/types';
3
3
  import { KeyBuilderSS } from '../KeyBuilderSS';
4
4
  import { ITelemetryCacheAsync } from '../types';
5
5
  import { findLatencyIndex } from '../findLatencyIndex';
6
6
  import { Redis } from 'ioredis';
7
7
  import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter';
8
8
  import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants';
9
+ import { isNaNNumber, isString } from '../../utils/lang';
10
+ import { _Map } from '../../utils/lang/maps';
11
+ import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory';
12
+ import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils';
9
13
 
10
14
  export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
11
15
 
@@ -22,6 +26,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
22
26
  return this.redis.hincrby(key, field, 1)
23
27
  .catch(() => { /* Handle rejections for telemetry */ });
24
28
  }
29
+
25
30
  recordException(method: Method) {
26
31
  const [key, field] = this.keys.buildExceptionKey(method).split('::');
27
32
  return this.redis.hincrby(key, field, 1)
@@ -33,4 +38,120 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
33
38
  const value = JSON.stringify(getTelemetryConfigStats(CONSUMER_MODE, STORAGE_REDIS));
34
39
  return this.redis.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ });
35
40
  }
41
+
42
+ /**
43
+ * Pop telemetry latencies.
44
+ * The returned promise rejects if redis operations fail.
45
+ */
46
+ popLatencies(): Promise<MultiMethodLatencies> {
47
+ return this.redis.hgetall(this.keys.latencyPrefix).then(latencies => {
48
+
49
+ const result: MultiMethodLatencies = new _Map();
50
+
51
+ Object.keys(latencies).forEach(field => {
52
+
53
+ const parsedField = parseLatencyField(field);
54
+ if (isString(parsedField)) {
55
+ this.log.error(`Ignoring invalid latency field: ${field}: ${parsedField}`);
56
+ return;
57
+ }
58
+
59
+ const count = parseInt(latencies[field]);
60
+ if (isNaNNumber(count)) {
61
+ this.log.error(`Ignoring latency with invalid count: ${latencies[field]}`);
62
+ return;
63
+ }
64
+
65
+ const [metadata, method, bucket] = parsedField;
66
+
67
+ if (bucket >= MAX_LATENCY_BUCKET_COUNT) {
68
+ this.log.error(`Ignoring latency with invalid bucket: ${bucket}`);
69
+ return;
70
+ }
71
+
72
+ if (!result.has(metadata)) result.set(metadata, {
73
+ t: newBuckets(),
74
+ ts: newBuckets(),
75
+ tc: newBuckets(),
76
+ tcs: newBuckets(),
77
+ tr: newBuckets(),
78
+ });
79
+
80
+ result.get(metadata)![method]![bucket] = count;
81
+ });
82
+
83
+ return this.redis.del(this.keys.latencyPrefix).then(() => result);
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Pop telemetry exceptions.
89
+ * The returned promise rejects if redis operations fail.
90
+ */
91
+ popExceptions(): Promise<MultiMethodExceptions> {
92
+ return this.redis.hgetall(this.keys.exceptionPrefix).then(exceptions => {
93
+
94
+ const result: MultiMethodExceptions = new _Map();
95
+
96
+ Object.keys(exceptions).forEach(field => {
97
+
98
+ const parsedField = parseExceptionField(field);
99
+ if (isString(parsedField)) {
100
+ this.log.error(`Ignoring invalid exception field: ${field}: ${parsedField}`);
101
+ return;
102
+ }
103
+
104
+ const count = parseInt(exceptions[field]);
105
+ if (isNaNNumber(count)) {
106
+ this.log.error(`Ignoring exception with invalid count: ${exceptions[field]}`);
107
+ return;
108
+ }
109
+
110
+ const [metadata, method] = parsedField;
111
+
112
+ if (!result.has(metadata)) result.set(metadata, {
113
+ t: 0,
114
+ ts: 0,
115
+ tc: 0,
116
+ tcs: 0,
117
+ tr: 0,
118
+ });
119
+
120
+ result.get(metadata)![method] = count;
121
+ });
122
+
123
+ return this.redis.del(this.keys.exceptionPrefix).then(() => result);
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Pop telemetry configs.
129
+ * The returned promise rejects if redis operations fail.
130
+ */
131
+ popConfigs(): Promise<MultiConfigs> {
132
+ return this.redis.hgetall(this.keys.initPrefix).then(configs => {
133
+
134
+ const result: MultiConfigs = new _Map();
135
+
136
+ Object.keys(configs).forEach(field => {
137
+
138
+ const parsedField = parseMetadata(field);
139
+ if (isString(parsedField)) {
140
+ this.log.error(`Ignoring invalid config field: ${field}: ${parsedField}`);
141
+ return;
142
+ }
143
+
144
+ const [metadata] = parsedField;
145
+
146
+ try {
147
+ const config = JSON.parse(configs[field]);
148
+ result.set(metadata, config);
149
+ } catch (e) {
150
+ this.log.error(`Ignoring invalid config: ${configs[field]}`);
151
+ }
152
+ });
153
+
154
+ return this.redis.del(this.keys.initPrefix).then(() => result);
155
+ });
156
+ }
36
157
  }
@@ -1,10 +1,11 @@
1
1
  import { IUniqueKeysCacheBase } from '../types';
2
2
  import { Redis } from 'ioredis';
3
- import { UniqueKeysCacheInMemory } from '../inMemory/uniqueKeysCacheInMemory';
3
+ import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
4
4
  import { setToArray } from '../../utils/lang/sets';
5
5
  import { DEFAULT_CACHE_SIZE, REFRESH_RATE, TTL_REFRESH } from './constants';
6
6
  import { LOG_PREFIX } from './constants';
7
7
  import { ILogger } from '../../logger/types';
8
+ import { UniqueKeysItemSs } from '../../sync/submitters/types';
8
9
 
9
10
  export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
10
11
 
@@ -13,19 +14,20 @@ export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements I
13
14
  private readonly redis: Redis;
14
15
  private readonly refreshRate: number;
15
16
  private intervalId: any;
16
-
17
- constructor(log: ILogger, key: string, redis: Redis, uniqueKeysQueueSize: number = DEFAULT_CACHE_SIZE, refreshRate: number = REFRESH_RATE) {
17
+
18
+ constructor(log: ILogger, key: string, redis: Redis, uniqueKeysQueueSize = DEFAULT_CACHE_SIZE, refreshRate = REFRESH_RATE) {
18
19
  super(uniqueKeysQueueSize);
19
20
  this.log = log;
20
21
  this.key = key;
21
22
  this.redis = redis;
22
23
  this.refreshRate = refreshRate;
23
- this.onFullQueue = () => {this.postUniqueKeysInRedis();};
24
+ this.onFullQueue = () => { this.postUniqueKeysInRedis(); };
24
25
  }
25
-
26
- postUniqueKeysInRedis() {
26
+
27
+ private postUniqueKeysInRedis() {
27
28
  const featureNames = Object.keys(this.uniqueKeysTracker);
28
- if (!featureNames) return Promise.resolve(false);
29
+ if (!featureNames.length) return Promise.resolve(false);
30
+
29
31
  const pipeline = this.redis.pipeline();
30
32
  for (let i = 0; i < featureNames.length; i++) {
31
33
  const featureName = featureNames[i];
@@ -47,18 +49,29 @@ export class UniqueKeysCacheInRedis extends UniqueKeysCacheInMemory implements I
47
49
  })
48
50
  .catch(err => {
49
51
  this.log.error(`${LOG_PREFIX}Error in uniqueKeys pipeline: ${err}.`);
50
- return Promise.resolve(false);
52
+ return false;
51
53
  });
52
54
  }
53
-
54
-
55
+
56
+
55
57
  start() {
56
58
  this.intervalId = setInterval(this.postUniqueKeysInRedis.bind(this), this.refreshRate);
57
59
  }
58
-
60
+
59
61
  stop() {
60
62
  clearInterval(this.intervalId);
61
63
  return this.postUniqueKeysInRedis();
62
64
  }
63
-
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
+
64
77
  }
@@ -8,8 +8,9 @@ import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis';
8
8
  import { EventsCacheInRedis } from './EventsCacheInRedis';
9
9
  import { DEBUG, NONE, STORAGE_REDIS } from '../../utils/constants';
10
10
  import { TelemetryCacheInRedis } from './TelemetryCacheInRedis';
11
- import { UniqueKeysCacheInRedis } from './uniqueKeysCacheInRedis';
11
+ import { UniqueKeysCacheInRedis } from './UniqueKeysCacheInRedis';
12
12
  import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis';
13
+ import { metadataBuilder } from '../utils';
13
14
 
14
15
  export interface InRedisStorageOptions {
15
16
  prefix?: string
@@ -24,7 +25,9 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
24
25
 
25
26
  const prefix = validatePrefix(options.prefix);
26
27
 
27
- function InRedisStorageFactory({ log, metadata, onReadyCb, impressionsMode }: IStorageFactoryParams): IStorageAsync {
28
+ function InRedisStorageFactory(params: IStorageFactoryParams): IStorageAsync {
29
+ const { onReadyCb, settings, settings: { log, sync: { impressionsMode } } } = params;
30
+ const metadata = metadataBuilder(settings);
28
31
  const keys = new KeyBuilderSS(prefix, metadata);
29
32
  const redisClient = new RedisAdapter(log, options.options || {});
30
33
  const telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
@@ -52,7 +55,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
52
55
 
53
56
  // When using REDIS we should:
54
57
  // 1- Disconnect from the storage
55
- destroy(): Promise<void>{
58
+ destroy(): Promise<void> {
56
59
  let promises = [];
57
60
  if (impressionCountsCache) promises.push(impressionCountsCache.stop());
58
61
  if (uniqueKeysCache) promises.push(uniqueKeysCache.stop());
@@ -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,8 +1,14 @@
1
1
  import { ILogger } from '../../logger/types';
2
- import { Method } from '../../sync/submitters/types';
2
+ import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } from '../../sync/submitters/types';
3
3
  import { KeyBuilderSS } from '../KeyBuilderSS';
4
4
  import { IPluggableStorageWrapper, ITelemetryCacheAsync } from '../types';
5
5
  import { findLatencyIndex } from '../findLatencyIndex';
6
+ import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter';
7
+ import { CONSUMER_MODE, STORAGE_PLUGGABLE } from '../../utils/constants';
8
+ import { isString, isNaNNumber } from '../../utils/lang';
9
+ import { _Map } from '../../utils/lang/maps';
10
+ import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory';
11
+ import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils';
6
12
 
7
13
  export class TelemetryCachePluggable implements ITelemetryCacheAsync {
8
14
 
@@ -23,4 +29,144 @@ export class TelemetryCachePluggable implements ITelemetryCacheAsync {
23
29
  .catch(() => { /* Handle rejections for telemetry */ });
24
30
  }
25
31
 
32
+ recordConfig() {
33
+ const value = JSON.stringify(getTelemetryConfigStats(CONSUMER_MODE, STORAGE_PLUGGABLE));
34
+ return this.wrapper.set(this.keys.buildInitKey(), value).catch(() => { /* Handle rejections for telemetry */ });
35
+ }
36
+
37
+ /**
38
+ * Pop telemetry latencies.
39
+ * The returned promise rejects if wrapper operations fail.
40
+ */
41
+ popLatencies(): Promise<MultiMethodLatencies> {
42
+ return this.wrapper.getKeysByPrefix(this.keys.latencyPrefix).then(latencyKeys => {
43
+ return latencyKeys.length ?
44
+ this.wrapper.getMany(latencyKeys).then(latencies => {
45
+
46
+ const result: MultiMethodLatencies = new _Map();
47
+
48
+ for (let i = 0; i < latencyKeys.length; i++) {
49
+ const field = latencyKeys[i].split('::')[1];
50
+
51
+ const parsedField = parseLatencyField(field);
52
+ if (isString(parsedField)) {
53
+ this.log.error(`Ignoring invalid latency field: ${field}: ${parsedField}`);
54
+ continue;
55
+ }
56
+
57
+ // @ts-ignore
58
+ const count = parseInt(latencies[i]);
59
+ if (isNaNNumber(count)) {
60
+ this.log.error(`Ignoring latency with invalid count: ${latencies[i]}`);
61
+ continue;
62
+ }
63
+
64
+ const [metadata, method, bucket] = parsedField;
65
+
66
+ if (bucket >= MAX_LATENCY_BUCKET_COUNT) {
67
+ this.log.error(`Ignoring latency with invalid bucket: ${bucket}`);
68
+ continue;
69
+ }
70
+
71
+ if (!result.has(metadata)) result.set(metadata, {
72
+ t: newBuckets(),
73
+ ts: newBuckets(),
74
+ tc: newBuckets(),
75
+ tcs: newBuckets(),
76
+ tr: newBuckets(),
77
+ });
78
+
79
+ result.get(metadata)![method]![bucket] = count;
80
+ }
81
+
82
+ return Promise.all(latencyKeys.map((latencyKey) => this.wrapper.del(latencyKey))).then(() => result);
83
+ }) :
84
+ // If latencyKeys is empty, return an empty map.
85
+ new _Map();
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Pop telemetry exceptions.
91
+ * The returned promise rejects if wrapper operations fail.
92
+ */
93
+ popExceptions(): Promise<MultiMethodExceptions> {
94
+ return this.wrapper.getKeysByPrefix(this.keys.exceptionPrefix).then(exceptionKeys => {
95
+ return exceptionKeys.length ?
96
+ this.wrapper.getMany(exceptionKeys).then(exceptions => {
97
+
98
+ const result: MultiMethodExceptions = new _Map();
99
+
100
+ for (let i = 0; i < exceptionKeys.length; i++) {
101
+ const field = exceptionKeys[i].split('::')[1];
102
+
103
+ const parsedField = parseExceptionField(field);
104
+ if (isString(parsedField)) {
105
+ this.log.error(`Ignoring invalid exception field: ${field}: ${parsedField}`);
106
+ continue;
107
+ }
108
+
109
+ // @ts-ignore
110
+ const count = parseInt(exceptions[i]);
111
+ if (isNaNNumber(count)) {
112
+ this.log.error(`Ignoring exception with invalid count: ${exceptions[i]}`);
113
+ continue;
114
+ }
115
+
116
+ const [metadata, method] = parsedField;
117
+
118
+ if (!result.has(metadata)) result.set(metadata, {
119
+ t: 0,
120
+ ts: 0,
121
+ tc: 0,
122
+ tcs: 0,
123
+ tr: 0,
124
+ });
125
+
126
+ result.get(metadata)![method] = count;
127
+ }
128
+
129
+ return Promise.all(exceptionKeys.map((exceptionKey) => this.wrapper.del(exceptionKey))).then(() => result);
130
+ }) :
131
+ // If exceptionKeys is empty, return an empty map.
132
+ new _Map();
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Pop telemetry configs.
138
+ * The returned promise rejects if wrapper operations fail.
139
+ */
140
+ popConfigs(): Promise<MultiConfigs> {
141
+ return this.wrapper.getKeysByPrefix(this.keys.initPrefix).then(configKeys => {
142
+ return configKeys.length ?
143
+ this.wrapper.getMany(configKeys).then(configs => {
144
+
145
+ const result: MultiConfigs = new _Map();
146
+
147
+ for (let i = 0; i < configKeys.length; i++) {
148
+ const field = configKeys[i].split('::')[1];
149
+
150
+ const parsedField = parseMetadata(field);
151
+ if (isString(parsedField)) {
152
+ this.log.error(`Ignoring invalid config field: ${field}: ${parsedField}`);
153
+ continue;
154
+ }
155
+
156
+ const [metadata] = parsedField;
157
+
158
+ try { // @ts-ignore
159
+ const config = JSON.parse(configs[i]);
160
+ result.set(metadata, config);
161
+ } catch (e) {
162
+ this.log.error(`Ignoring invalid config: ${configs[i]}`);
163
+ }
164
+ }
165
+
166
+ return Promise.all(configKeys.map((configKey) => this.wrapper.del(configKey))).then(() => result);
167
+ }) :
168
+ // If configKeys is empty, return an empty map.
169
+ new _Map();
170
+ });
171
+ }
26
172
  }
@@ -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
+ }