@splitsoftware/splitio-commons 1.6.2-rc.13 → 1.6.2-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/storages/KeyBuilderSS.js +4 -43
- package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +3 -3
- package/cjs/storages/inRedis/ImpressionsCacheInRedis.js +2 -19
- package/cjs/storages/inRedis/TelemetryCacheInRedis.js +4 -4
- package/cjs/storages/inRedis/index.js +2 -2
- package/cjs/storages/pluggable/ImpressionCountsCachePluggable.js +3 -3
- package/cjs/storages/pluggable/ImpressionsCachePluggable.js +2 -19
- package/cjs/storages/pluggable/TelemetryCachePluggable.js +4 -4
- package/cjs/storages/pluggable/inMemoryWrapper.js +8 -6
- package/cjs/storages/pluggable/index.js +2 -2
- package/cjs/storages/utils.js +73 -0
- package/esm/storages/KeyBuilderSS.js +1 -37
- package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +3 -3
- package/esm/storages/inRedis/ImpressionsCacheInRedis.js +2 -19
- package/esm/storages/inRedis/TelemetryCacheInRedis.js +1 -1
- package/esm/storages/inRedis/index.js +1 -1
- package/esm/storages/pluggable/ImpressionCountsCachePluggable.js +3 -3
- package/esm/storages/pluggable/ImpressionsCachePluggable.js +2 -19
- package/esm/storages/pluggable/TelemetryCachePluggable.js +1 -1
- package/esm/storages/pluggable/inMemoryWrapper.js +8 -6
- package/esm/storages/pluggable/index.js +1 -1
- package/esm/storages/utils.js +65 -0
- package/package.json +1 -1
- package/src/services/splitApi.ts +2 -2
- package/src/storages/KeyBuilderSS.ts +2 -44
- package/src/storages/inMemory/AttributesCacheInMemory.ts +7 -7
- package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +3 -3
- package/src/storages/inRedis/ImpressionsCacheInRedis.ts +2 -22
- package/src/storages/inRedis/TelemetryCacheInRedis.ts +2 -1
- package/src/storages/inRedis/index.ts +1 -1
- package/src/storages/pluggable/ImpressionCountsCachePluggable.ts +3 -3
- package/src/storages/pluggable/ImpressionsCachePluggable.ts +3 -23
- package/src/storages/pluggable/TelemetryCachePluggable.ts +2 -1
- package/src/storages/pluggable/inMemoryWrapper.ts +6 -6
- package/src/storages/pluggable/index.ts +1 -1
- package/src/storages/types.ts +4 -4
- package/src/storages/utils.ts +78 -0
- package/src/sync/submitters/types.ts +2 -0
- package/src/trackers/impressionsTracker.ts +2 -2
- package/src/trackers/strategy/strategyDebug.ts +4 -4
- package/src/utils/redis/RedisMock.ts +5 -5
- package/types/storages/KeyBuilderSS.d.ts +1 -3
- package/types/storages/inRedis/ImpressionsCacheInRedis.d.ts +0 -1
- package/types/storages/pluggable/ImpressionsCachePluggable.d.ts +1 -2
- package/types/storages/types.d.ts +4 -4
- package/types/storages/utils.d.ts +8 -0
- package/types/sync/submitters/types.d.ts +2 -0
- package/cjs/storages/metadataBuilder.js +0 -12
- package/esm/storages/metadataBuilder.js +0 -8
- package/src/storages/metadataBuilder.ts +0 -11
|
@@ -34,29 +34,31 @@ export function inMemoryWrapperFactory(connDelay) {
|
|
|
34
34
|
getKeysByPrefix: function (prefix) {
|
|
35
35
|
return Promise.resolve(Object.keys(_cache).filter(function (key) { return startsWith(key, prefix); }));
|
|
36
36
|
},
|
|
37
|
-
incr: function (key) {
|
|
37
|
+
incr: function (key, increment) {
|
|
38
|
+
if (increment === void 0) { increment = 1; }
|
|
38
39
|
if (key in _cache) {
|
|
39
|
-
var count = toNumber(_cache[key]) +
|
|
40
|
+
var count = toNumber(_cache[key]) + increment;
|
|
40
41
|
if (isNaN(count))
|
|
41
42
|
return Promise.reject('Given key is not a number');
|
|
42
43
|
_cache[key] = count + '';
|
|
43
44
|
return Promise.resolve(count);
|
|
44
45
|
}
|
|
45
46
|
else {
|
|
46
|
-
_cache[key] = '
|
|
47
|
+
_cache[key] = '' + increment;
|
|
47
48
|
return Promise.resolve(1);
|
|
48
49
|
}
|
|
49
50
|
},
|
|
50
|
-
decr: function (key) {
|
|
51
|
+
decr: function (key, decrement) {
|
|
52
|
+
if (decrement === void 0) { decrement = 1; }
|
|
51
53
|
if (key in _cache) {
|
|
52
|
-
var count = toNumber(_cache[key]) -
|
|
54
|
+
var count = toNumber(_cache[key]) - decrement;
|
|
53
55
|
if (isNaN(count))
|
|
54
56
|
return Promise.reject('Given key is not a number');
|
|
55
57
|
_cache[key] = count + '';
|
|
56
58
|
return Promise.resolve(count);
|
|
57
59
|
}
|
|
58
60
|
else {
|
|
59
|
-
_cache[key] = '-
|
|
61
|
+
_cache[key] = '-' + decrement;
|
|
60
62
|
return Promise.resolve(-1);
|
|
61
63
|
}
|
|
62
64
|
},
|
|
@@ -17,7 +17,7 @@ import { ImpressionCountsCachePluggable } from './ImpressionCountsCachePluggable
|
|
|
17
17
|
import { UniqueKeysCachePluggable } from './UniqueKeysCachePluggable';
|
|
18
18
|
import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
|
|
19
19
|
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
|
|
20
|
-
import { metadataBuilder } from '../
|
|
20
|
+
import { metadataBuilder } from '../utils';
|
|
21
21
|
var NO_VALID_WRAPPER = 'Expecting pluggable storage `wrapper` in options, but no valid wrapper instance was provided.';
|
|
22
22
|
var NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.';
|
|
23
23
|
/**
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Shared utils for Redis and Pluggable storage
|
|
2
|
+
import { UNKNOWN } from '../utils/constants';
|
|
3
|
+
import { MAX_LATENCY_BUCKET_COUNT } from './inMemory/TelemetryCacheInMemory';
|
|
4
|
+
import { METHOD_NAMES } from './KeyBuilderSS';
|
|
5
|
+
export function metadataBuilder(settings) {
|
|
6
|
+
return {
|
|
7
|
+
s: settings.version,
|
|
8
|
+
i: settings.runtime.ip || UNKNOWN,
|
|
9
|
+
n: settings.runtime.hostname || UNKNOWN,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
// Converts impressions to be stored in Redis or pluggable storage.
|
|
13
|
+
export function impressionsToJSON(impressions, metadata) {
|
|
14
|
+
return impressions.map(function (impression) {
|
|
15
|
+
var impressionWithMetadata = {
|
|
16
|
+
m: metadata,
|
|
17
|
+
i: {
|
|
18
|
+
k: impression.keyName,
|
|
19
|
+
b: impression.bucketingKey,
|
|
20
|
+
f: impression.feature,
|
|
21
|
+
t: impression.treatment,
|
|
22
|
+
r: impression.label,
|
|
23
|
+
c: impression.changeNumber,
|
|
24
|
+
m: impression.time,
|
|
25
|
+
pt: impression.pt,
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return JSON.stringify(impressionWithMetadata);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Utilities used by TelemetryCacheInRedis and TelemetryCachePluggable
|
|
32
|
+
var REVERSE_METHOD_NAMES = Object.keys(METHOD_NAMES).reduce(function (acc, key) {
|
|
33
|
+
acc[METHOD_NAMES[key]] = key;
|
|
34
|
+
return acc;
|
|
35
|
+
}, {});
|
|
36
|
+
export function parseMetadata(field) {
|
|
37
|
+
var parts = field.split('/');
|
|
38
|
+
if (parts.length !== 3)
|
|
39
|
+
return "invalid subsection count. Expected 3, got: " + parts.length;
|
|
40
|
+
var s = parts[0] /* metadata.s */, n = parts[1] /* metadata.n */, i = parts[2] /* metadata.i */;
|
|
41
|
+
return [JSON.stringify({ s: s, n: n, i: i })];
|
|
42
|
+
}
|
|
43
|
+
export function parseExceptionField(field) {
|
|
44
|
+
var parts = field.split('/');
|
|
45
|
+
if (parts.length !== 4)
|
|
46
|
+
return "invalid subsection count. Expected 4, got: " + parts.length;
|
|
47
|
+
var s = parts[0] /* metadata.s */, n = parts[1] /* metadata.n */, i = parts[2] /* metadata.i */, m = parts[3];
|
|
48
|
+
var method = REVERSE_METHOD_NAMES[m];
|
|
49
|
+
if (!method)
|
|
50
|
+
return "unknown method '" + m + "'";
|
|
51
|
+
return [JSON.stringify({ s: s, n: n, i: i }), method];
|
|
52
|
+
}
|
|
53
|
+
export function parseLatencyField(field) {
|
|
54
|
+
var parts = field.split('/');
|
|
55
|
+
if (parts.length !== 5)
|
|
56
|
+
return "invalid subsection count. Expected 5, got: " + parts.length;
|
|
57
|
+
var s = parts[0] /* metadata.s */, n = parts[1] /* metadata.n */, i = parts[2] /* metadata.i */, m = parts[3], b = parts[4];
|
|
58
|
+
var method = REVERSE_METHOD_NAMES[m];
|
|
59
|
+
if (!method)
|
|
60
|
+
return "unknown method '" + m + "'";
|
|
61
|
+
var bucket = parseInt(b);
|
|
62
|
+
if (isNaN(bucket) || bucket >= MAX_LATENCY_BUCKET_COUNT)
|
|
63
|
+
return "invalid bucket. Expected a number between 0 and " + (MAX_LATENCY_BUCKET_COUNT - 1) + ", got: " + b;
|
|
64
|
+
return [JSON.stringify({ s: s, n: n, i: i }), method, bucket];
|
|
65
|
+
}
|
package/package.json
CHANGED
package/src/services/splitApi.ts
CHANGED
|
@@ -106,7 +106,7 @@ export function splitApiFactory(
|
|
|
106
106
|
const url = `${urls.events}/testImpressions/count`;
|
|
107
107
|
return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT));
|
|
108
108
|
},
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
/**
|
|
111
111
|
* Post unique keys for client side.
|
|
112
112
|
*
|
|
@@ -117,7 +117,7 @@ export function splitApiFactory(
|
|
|
117
117
|
const url = `${urls.telemetry}/v1/keys/cs`;
|
|
118
118
|
return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY));
|
|
119
119
|
},
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
/**
|
|
122
122
|
* Post unique keys for server side.
|
|
123
123
|
*
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { KeyBuilder } from './KeyBuilder';
|
|
2
2
|
import { IMetadata } from '../dtos/types';
|
|
3
3
|
import { Method } from '../sync/submitters/types';
|
|
4
|
-
import { MAX_LATENCY_BUCKET_COUNT } from './inMemory/TelemetryCacheInMemory';
|
|
5
4
|
|
|
6
|
-
const METHOD_NAMES: Record<Method, string> = {
|
|
5
|
+
export const METHOD_NAMES: Record<Method, string> = {
|
|
7
6
|
t: 'treatment',
|
|
8
7
|
ts: 'treatments',
|
|
9
8
|
tc: 'treatmentWithConfig',
|
|
@@ -37,7 +36,7 @@ export class KeyBuilderSS extends KeyBuilder {
|
|
|
37
36
|
buildImpressionsCountKey() {
|
|
38
37
|
return `${this.prefix}.impressions.count`;
|
|
39
38
|
}
|
|
40
|
-
|
|
39
|
+
|
|
41
40
|
buildUniqueKeysKey() {
|
|
42
41
|
return `${this.prefix}.uniquekeys`;
|
|
43
42
|
}
|
|
@@ -65,44 +64,3 @@ export class KeyBuilderSS extends KeyBuilder {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
// Used by consumer methods of TelemetryCacheInRedis and TelemetryCachePluggable
|
|
70
|
-
|
|
71
|
-
const REVERSE_METHOD_NAMES = Object.keys(METHOD_NAMES).reduce((acc, key) => {
|
|
72
|
-
acc[METHOD_NAMES[key as Method]] = key as Method;
|
|
73
|
-
return acc;
|
|
74
|
-
}, {} as Record<string, Method>);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
export function parseMetadata(field: string): [metadata: string] | string {
|
|
78
|
-
const parts = field.split('/');
|
|
79
|
-
if (parts.length !== 3) return `invalid subsection count. Expected 3, got: ${parts.length}`;
|
|
80
|
-
|
|
81
|
-
const [s /* metadata.s */, n /* metadata.n */, i /* metadata.i */] = parts;
|
|
82
|
-
return [JSON.stringify({ s, n, i })];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function parseExceptionField(field: string): [metadata: string, method: Method] | string {
|
|
86
|
-
const parts = field.split('/');
|
|
87
|
-
if (parts.length !== 4) return `invalid subsection count. Expected 4, got: ${parts.length}`;
|
|
88
|
-
|
|
89
|
-
const [s /* metadata.s */, n /* metadata.n */, i /* metadata.i */, m] = parts;
|
|
90
|
-
const method = REVERSE_METHOD_NAMES[m];
|
|
91
|
-
if (!method) return `unknown method '${m}'`;
|
|
92
|
-
|
|
93
|
-
return [JSON.stringify({ s, n, i }), method];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function parseLatencyField(field: string): [metadata: string, method: Method, bucket: number] | string {
|
|
97
|
-
const parts = field.split('/');
|
|
98
|
-
if (parts.length !== 5) return `invalid subsection count. Expected 5, got: ${parts.length}`;
|
|
99
|
-
|
|
100
|
-
const [s /* metadata.s */, n /* metadata.n */, i /* metadata.i */, m, b] = parts;
|
|
101
|
-
const method = REVERSE_METHOD_NAMES[m];
|
|
102
|
-
if (!method) return `unknown method '${m}'`;
|
|
103
|
-
|
|
104
|
-
const bucket = parseInt(b);
|
|
105
|
-
if (isNaN(bucket) || bucket >= MAX_LATENCY_BUCKET_COUNT) return `invalid bucket. Expected a number between 0 and ${MAX_LATENCY_BUCKET_COUNT - 1}, got: ${b}`;
|
|
106
|
-
|
|
107
|
-
return [JSON.stringify({ s, n, i }), method, bucket];
|
|
108
|
-
}
|
|
@@ -7,10 +7,10 @@ export class AttributesCacheInMemory {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Create or update the value for the given attribute
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* @param {string} attributeName attribute name
|
|
12
12
|
* @param {Object} attributeValue attribute value
|
|
13
|
-
* @returns {boolean} the attribute was stored
|
|
13
|
+
* @returns {boolean} the attribute was stored
|
|
14
14
|
*/
|
|
15
15
|
setAttribute(attributeName: string, attributeValue: Object): boolean {
|
|
16
16
|
this.attributesCache[attributeName] = attributeValue;
|
|
@@ -19,7 +19,7 @@ export class AttributesCacheInMemory {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Retrieves the value of a given attribute
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* @param {string} attributeName attribute name
|
|
24
24
|
* @returns {Object?} stored attribute value
|
|
25
25
|
*/
|
|
@@ -29,7 +29,7 @@ export class AttributesCacheInMemory {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Create or update all the given attributes
|
|
32
|
-
*
|
|
32
|
+
*
|
|
33
33
|
* @param {[string, Object]} attributes attributes to create or update
|
|
34
34
|
* @returns {boolean} attributes were stored
|
|
35
35
|
*/
|
|
@@ -40,7 +40,7 @@ export class AttributesCacheInMemory {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Retrieve the full attributes map
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
44
|
* @returns {Map<string, Object>} stored attributes
|
|
45
45
|
*/
|
|
46
46
|
getAll(): Record<string, Object> {
|
|
@@ -49,7 +49,7 @@ export class AttributesCacheInMemory {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Removes a given attribute from the map
|
|
52
|
-
*
|
|
52
|
+
*
|
|
53
53
|
* @param {string} attributeName attribute to remove
|
|
54
54
|
* @returns {boolean} attribute removed
|
|
55
55
|
*/
|
|
@@ -63,7 +63,7 @@ export class AttributesCacheInMemory {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Clears all attributes stored in the SDK
|
|
66
|
-
*
|
|
66
|
+
*
|
|
67
67
|
*/
|
|
68
68
|
clear() {
|
|
69
69
|
this.attributesCache = {};
|
|
@@ -61,7 +61,7 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory
|
|
|
61
61
|
|
|
62
62
|
this.redis.del(this.key).catch(() => { /* no-op */ });
|
|
63
63
|
|
|
64
|
-
const
|
|
64
|
+
const pf: ImpressionCountsPayload['pf'] = [];
|
|
65
65
|
|
|
66
66
|
forOwn(counts, (count, key) => {
|
|
67
67
|
const nameAndTime = key.split('::');
|
|
@@ -82,14 +82,14 @@ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
pf.push({
|
|
86
86
|
f: nameAndTime[0],
|
|
87
87
|
m: timeFrame,
|
|
88
88
|
rc: rawCount,
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
return
|
|
92
|
+
return { pf };
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
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.
|
|
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
|
|
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
|
|
|
@@ -10,7 +10,7 @@ import { DEBUG, NONE, STORAGE_REDIS } from '../../utils/constants';
|
|
|
10
10
|
import { TelemetryCacheInRedis } from './TelemetryCacheInRedis';
|
|
11
11
|
import { UniqueKeysCacheInRedis } from './UniqueKeysCacheInRedis';
|
|
12
12
|
import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis';
|
|
13
|
-
import { metadataBuilder } from '../
|
|
13
|
+
import { metadataBuilder } from '../utils';
|
|
14
14
|
|
|
15
15
|
export interface InRedisStorageOptions {
|
|
16
16
|
prefix?: string
|
|
@@ -54,7 +54,7 @@ export class ImpressionCountsCachePluggable extends ImpressionCountsCacheInMemor
|
|
|
54
54
|
.then(counts => {
|
|
55
55
|
keys.forEach(key => this.wrapper.del(key).catch(() => { /* noop */ }));
|
|
56
56
|
|
|
57
|
-
const
|
|
57
|
+
const pf = [];
|
|
58
58
|
|
|
59
59
|
for (let i = 0; i < keys.length; i++) {
|
|
60
60
|
const key = keys[i];
|
|
@@ -78,14 +78,14 @@ export class ImpressionCountsCachePluggable extends ImpressionCountsCacheInMemor
|
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
pf.push({
|
|
82
82
|
f: keyFeatureNameAndTime[1],
|
|
83
83
|
m: timeFrame,
|
|
84
84
|
rc: rawCount,
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
return
|
|
88
|
+
return { pf };
|
|
89
89
|
}) : undefined;
|
|
90
90
|
});
|
|
91
91
|
}
|
|
@@ -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.
|
|
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
|
|
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
|
|
|
@@ -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]) +
|
|
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] = '
|
|
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]) -
|
|
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] = '-
|
|
60
|
+
_cache[key] = '-' + decrement;
|
|
61
61
|
return Promise.resolve(-1);
|
|
62
62
|
}
|
|
63
63
|
},
|
|
@@ -18,7 +18,7 @@ import { ImpressionCountsCachePluggable } from './ImpressionCountsCachePluggable
|
|
|
18
18
|
import { UniqueKeysCachePluggable } from './UniqueKeysCachePluggable';
|
|
19
19
|
import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory';
|
|
20
20
|
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
|
|
21
|
-
import { metadataBuilder } from '../
|
|
21
|
+
import { metadataBuilder } from '../utils';
|
|
22
22
|
|
|
23
23
|
const NO_VALID_WRAPPER = 'Expecting pluggable storage `wrapper` in options, but no valid wrapper instance was provided.';
|
|
24
24
|
const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.';
|
package/src/storages/types.ts
CHANGED
|
@@ -69,21 +69,21 @@ export interface IPluggableStorageWrapper {
|
|
|
69
69
|
/** Integer operations */
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Increments the number stored at `key` by `increment
|
|
72
|
+
* Increments the number stored at `key` by `increment`, or set it to `increment` if the value doesn't exist.
|
|
73
73
|
*
|
|
74
74
|
* @function incr
|
|
75
75
|
* @param {string} key Key to increment
|
|
76
|
-
* @param {number} increment Value to increment by
|
|
76
|
+
* @param {number} increment Value to increment by. Defaults to 1.
|
|
77
77
|
* @returns {Promise<number>} A promise that resolves with the value of key after the increment. The promise rejects if the operation fails,
|
|
78
78
|
* for example, if there is a connection error or the key contains a string that can not be represented as integer.
|
|
79
79
|
*/
|
|
80
80
|
incr: (key: string, increment?: number) => Promise<number>
|
|
81
81
|
/**
|
|
82
|
-
* Decrements the number stored at `key` by `decrement
|
|
82
|
+
* Decrements the number stored at `key` by `decrement`, or set it to minus `decrement` if the value doesn't exist.
|
|
83
83
|
*
|
|
84
84
|
* @function decr
|
|
85
85
|
* @param {string} key Key to decrement
|
|
86
|
-
* @param {number} decrement Value to decrement by
|
|
86
|
+
* @param {number} decrement Value to decrement by. Defaults to 1.
|
|
87
87
|
* @returns {Promise<number>} A promise that resolves with the value of key after the decrement. The promise rejects if the operation fails,
|
|
88
88
|
* for example, if there is a connection error or the key contains a string that can not be represented as integer.
|
|
89
89
|
*/
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Shared utils for Redis and Pluggable storage
|
|
2
|
+
|
|
3
|
+
import { IMetadata } from '../dtos/types';
|
|
4
|
+
import { Method, StoredImpressionWithMetadata } from '../sync/submitters/types';
|
|
5
|
+
import { ImpressionDTO, ISettings } from '../types';
|
|
6
|
+
import { UNKNOWN } from '../utils/constants';
|
|
7
|
+
import { MAX_LATENCY_BUCKET_COUNT } from './inMemory/TelemetryCacheInMemory';
|
|
8
|
+
import { METHOD_NAMES } from './KeyBuilderSS';
|
|
9
|
+
|
|
10
|
+
export function metadataBuilder(settings: Pick<ISettings, 'version' | 'runtime'>): IMetadata {
|
|
11
|
+
return {
|
|
12
|
+
s: settings.version,
|
|
13
|
+
i: settings.runtime.ip || UNKNOWN,
|
|
14
|
+
n: settings.runtime.hostname || UNKNOWN,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Converts impressions to be stored in Redis or pluggable storage.
|
|
19
|
+
export function impressionsToJSON(impressions: ImpressionDTO[], metadata: IMetadata): string[] {
|
|
20
|
+
return impressions.map(impression => {
|
|
21
|
+
const impressionWithMetadata: StoredImpressionWithMetadata = {
|
|
22
|
+
m: metadata,
|
|
23
|
+
i: {
|
|
24
|
+
k: impression.keyName,
|
|
25
|
+
b: impression.bucketingKey,
|
|
26
|
+
f: impression.feature,
|
|
27
|
+
t: impression.treatment,
|
|
28
|
+
r: impression.label,
|
|
29
|
+
c: impression.changeNumber,
|
|
30
|
+
m: impression.time,
|
|
31
|
+
pt: impression.pt,
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return JSON.stringify(impressionWithMetadata);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Utilities used by TelemetryCacheInRedis and TelemetryCachePluggable
|
|
40
|
+
|
|
41
|
+
const REVERSE_METHOD_NAMES = Object.keys(METHOD_NAMES).reduce((acc, key) => {
|
|
42
|
+
acc[METHOD_NAMES[key as Method]] = key as Method;
|
|
43
|
+
return acc;
|
|
44
|
+
}, {} as Record<string, Method>);
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export function parseMetadata(field: string): [metadata: string] | string {
|
|
48
|
+
const parts = field.split('/');
|
|
49
|
+
if (parts.length !== 3) return `invalid subsection count. Expected 3, got: ${parts.length}`;
|
|
50
|
+
|
|
51
|
+
const [s /* metadata.s */, n /* metadata.n */, i /* metadata.i */] = parts;
|
|
52
|
+
return [JSON.stringify({ s, n, i })];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseExceptionField(field: string): [metadata: string, method: Method] | string {
|
|
56
|
+
const parts = field.split('/');
|
|
57
|
+
if (parts.length !== 4) return `invalid subsection count. Expected 4, got: ${parts.length}`;
|
|
58
|
+
|
|
59
|
+
const [s /* metadata.s */, n /* metadata.n */, i /* metadata.i */, m] = parts;
|
|
60
|
+
const method = REVERSE_METHOD_NAMES[m];
|
|
61
|
+
if (!method) return `unknown method '${m}'`;
|
|
62
|
+
|
|
63
|
+
return [JSON.stringify({ s, n, i }), method];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseLatencyField(field: string): [metadata: string, method: Method, bucket: number] | string {
|
|
67
|
+
const parts = field.split('/');
|
|
68
|
+
if (parts.length !== 5) return `invalid subsection count. Expected 5, got: ${parts.length}`;
|
|
69
|
+
|
|
70
|
+
const [s /* metadata.s */, n /* metadata.n */, i /* metadata.i */, m, b] = parts;
|
|
71
|
+
const method = REVERSE_METHOD_NAMES[m];
|
|
72
|
+
if (!method) return `unknown method '${m}'`;
|
|
73
|
+
|
|
74
|
+
const bucket = parseInt(b);
|
|
75
|
+
if (isNaN(bucket) || bucket >= MAX_LATENCY_BUCKET_COUNT) return `invalid bucket. Expected a number between 0 and ${MAX_LATENCY_BUCKET_COUNT - 1}, got: ${b}`;
|
|
76
|
+
|
|
77
|
+
return [JSON.stringify({ s, n, i }), method, bucket];
|
|
78
|
+
}
|
|
@@ -31,9 +31,9 @@ export function impressionsTrackerFactory(
|
|
|
31
31
|
|
|
32
32
|
const impressionsCount = impressions.length;
|
|
33
33
|
const { impressionsToStore, impressionsToListener, deduped } = strategy.process(impressions);
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
const impressionsToListenerCount = impressionsToListener.length;
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
if ( impressionsToStore.length>0 ){
|
|
38
38
|
const res = impressionsCache.track(impressionsToStore);
|
|
39
39
|
|
|
@@ -4,14 +4,14 @@ import { IStrategy } from '../types';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Debug strategy for impressions tracker. Wraps impressions to store and adds previousTime if it corresponds
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param impressionsObserver impression observer. Previous time (pt property) is included in impression instances
|
|
9
9
|
* @returns IStrategyResult
|
|
10
10
|
*/
|
|
11
11
|
export function strategyDebugFactory(
|
|
12
12
|
impressionsObserver: IImpressionObserver
|
|
13
13
|
): IStrategy {
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
return {
|
|
16
16
|
process(impressions: ImpressionDTO[]) {
|
|
17
17
|
impressions.forEach((impression) => {
|
|
@@ -19,8 +19,8 @@ export function strategyDebugFactory(
|
|
|
19
19
|
impression.pt = impressionsObserver.testAndSet(impression);
|
|
20
20
|
});
|
|
21
21
|
return {
|
|
22
|
-
impressionsToStore: impressions,
|
|
23
|
-
impressionsToListener: impressions,
|
|
22
|
+
impressionsToStore: impressions,
|
|
23
|
+
impressionsToListener: impressions,
|
|
24
24
|
deduped: 0
|
|
25
25
|
};
|
|
26
26
|
}
|