flagsmith-nodejs 2.5.0 → 2.5.2
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/build/sdk/index.js +8 -3
- package/build/sdk/utils.js +13 -7
- package/package.json +1 -1
- package/sdk/index.ts +11 -3
- package/sdk/utils.ts +13 -7
- package/tests/engine/unit/utils/utils.test.ts +61 -0
- package/tests/sdk/flagsmith.test.ts +43 -0
package/build/sdk/index.js
CHANGED
|
@@ -223,6 +223,9 @@ var Flagsmith = /** @class */ (function () {
|
|
|
223
223
|
return __generator(this, function (_b) {
|
|
224
224
|
switch (_b.label) {
|
|
225
225
|
case 0:
|
|
226
|
+
if (!identifier) {
|
|
227
|
+
throw new Error("`identifier` argument is missing or invalid.");
|
|
228
|
+
}
|
|
226
229
|
_a = !!this.cache;
|
|
227
230
|
if (!_a) return [3 /*break*/, 2];
|
|
228
231
|
return [4 /*yield*/, this.cache.get("flags-".concat(identifier))];
|
|
@@ -260,6 +263,9 @@ var Flagsmith = /** @class */ (function () {
|
|
|
260
263
|
*/
|
|
261
264
|
Flagsmith.prototype.getIdentitySegments = function (identifier, traits) {
|
|
262
265
|
var _this = this;
|
|
266
|
+
if (!identifier) {
|
|
267
|
+
throw new Error("`identifier` argument is missing or invalid.");
|
|
268
|
+
}
|
|
263
269
|
traits = traits || {};
|
|
264
270
|
if (this.enableLocalEvaluation) {
|
|
265
271
|
return new Promise(function (resolve, reject) {
|
|
@@ -358,10 +364,9 @@ var Flagsmith = /** @class */ (function () {
|
|
|
358
364
|
return [4 /*yield*/, (0, utils_1.retryFetch)(url, {
|
|
359
365
|
agent: this.agent,
|
|
360
366
|
method: method,
|
|
361
|
-
timeout: this.requestTimeoutMs || undefined,
|
|
362
367
|
body: JSON.stringify(body),
|
|
363
368
|
headers: headers
|
|
364
|
-
}, this.retries)];
|
|
369
|
+
}, this.retries, this.requestTimeoutMs || undefined)];
|
|
365
370
|
case 1:
|
|
366
371
|
data = _e.sent();
|
|
367
372
|
if (data.status !== 200) {
|
|
@@ -423,7 +428,7 @@ var Flagsmith = /** @class */ (function () {
|
|
|
423
428
|
featureStates: featureStates,
|
|
424
429
|
analyticsProcessor: this.analyticsProcessor,
|
|
425
430
|
defaultFlagHandler: this.defaultFlagHandler,
|
|
426
|
-
identityID: identityModel.djangoID || identityModel.
|
|
431
|
+
identityID: identityModel.djangoID || identityModel.compositeKey
|
|
427
432
|
});
|
|
428
433
|
if (!!!this.cache) return [3 /*break*/, 2];
|
|
429
434
|
// @ts-ignore node-cache types are incorrect, ttl should be optional
|
package/build/sdk/utils.js
CHANGED
|
@@ -63,11 +63,8 @@ var retryFetch = function (url, fetchOptions, retries, timeout // set an overall
|
|
|
63
63
|
) {
|
|
64
64
|
if (retries === void 0) { retries = 3; }
|
|
65
65
|
return new Promise(function (resolve, reject) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
setTimeout(function () { return reject('error: timeout'); }, timeout);
|
|
69
|
-
var wrapper = function (n) {
|
|
70
|
-
(0, node_fetch_1.default)(url, fetchOptions)
|
|
66
|
+
var retryWrapper = function (n) {
|
|
67
|
+
requestWrapper()
|
|
71
68
|
.then(function (res) { return resolve(res); })
|
|
72
69
|
.catch(function (err) { return __awaiter(void 0, void 0, void 0, function () {
|
|
73
70
|
return __generator(this, function (_a) {
|
|
@@ -77,7 +74,7 @@ var retryFetch = function (url, fetchOptions, retries, timeout // set an overall
|
|
|
77
74
|
return [4 /*yield*/, (0, exports.delay)(1000)];
|
|
78
75
|
case 1:
|
|
79
76
|
_a.sent();
|
|
80
|
-
|
|
77
|
+
retryWrapper(--n);
|
|
81
78
|
return [3 /*break*/, 3];
|
|
82
79
|
case 2:
|
|
83
80
|
reject(err);
|
|
@@ -87,7 +84,16 @@ var retryFetch = function (url, fetchOptions, retries, timeout // set an overall
|
|
|
87
84
|
});
|
|
88
85
|
}); });
|
|
89
86
|
};
|
|
90
|
-
|
|
87
|
+
var requestWrapper = function () {
|
|
88
|
+
return new Promise(function (resolve, reject) {
|
|
89
|
+
if (timeout)
|
|
90
|
+
setTimeout(function () { return reject('error: timeout'); }, timeout);
|
|
91
|
+
return (0, node_fetch_1.default)(url, fetchOptions)
|
|
92
|
+
.then(function (res) { return resolve(res); })
|
|
93
|
+
.catch(function (err) { return reject(err); });
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
retryWrapper(retries);
|
|
91
97
|
});
|
|
92
98
|
};
|
|
93
99
|
exports.retryFetch = retryFetch;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"repository": {
|
package/sdk/index.ts
CHANGED
|
@@ -173,6 +173,10 @@ export class Flagsmith {
|
|
|
173
173
|
* @returns Flags object holding all the flags for the given identity.
|
|
174
174
|
*/
|
|
175
175
|
async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
|
|
176
|
+
if (!identifier) {
|
|
177
|
+
throw new Error("`identifier` argument is missing or invalid.")
|
|
178
|
+
}
|
|
179
|
+
|
|
176
180
|
const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
|
|
177
181
|
if (!!cachedItem) {
|
|
178
182
|
return cachedItem;
|
|
@@ -203,6 +207,10 @@ export class Flagsmith {
|
|
|
203
207
|
identifier: string,
|
|
204
208
|
traits?: { [key: string]: any }
|
|
205
209
|
): Promise<SegmentModel[]> {
|
|
210
|
+
if (!identifier) {
|
|
211
|
+
throw new Error("`identifier` argument is missing or invalid.")
|
|
212
|
+
}
|
|
213
|
+
|
|
206
214
|
traits = traits || {};
|
|
207
215
|
if (this.enableLocalEvaluation) {
|
|
208
216
|
return new Promise((resolve, reject) => {
|
|
@@ -275,11 +283,11 @@ export class Flagsmith {
|
|
|
275
283
|
{
|
|
276
284
|
agent: this.agent,
|
|
277
285
|
method: method,
|
|
278
|
-
timeout: this.requestTimeoutMs || undefined,
|
|
279
286
|
body: JSON.stringify(body),
|
|
280
287
|
headers: headers
|
|
281
288
|
},
|
|
282
|
-
this.retries
|
|
289
|
+
this.retries,
|
|
290
|
+
this.requestTimeoutMs || undefined,
|
|
283
291
|
);
|
|
284
292
|
|
|
285
293
|
if (data.status !== 200) {
|
|
@@ -329,7 +337,7 @@ export class Flagsmith {
|
|
|
329
337
|
featureStates: featureStates,
|
|
330
338
|
analyticsProcessor: this.analyticsProcessor,
|
|
331
339
|
defaultFlagHandler: this.defaultFlagHandler,
|
|
332
|
-
identityID: identityModel.djangoID || identityModel.
|
|
340
|
+
identityID: identityModel.djangoID || identityModel.compositeKey
|
|
333
341
|
});
|
|
334
342
|
|
|
335
343
|
if (!!this.cache) {
|
package/sdk/utils.ts
CHANGED
|
@@ -23,22 +23,28 @@ export const retryFetch = (
|
|
|
23
23
|
timeout?: number // set an overall timeout for this function
|
|
24
24
|
): Promise<Response> => {
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const wrapper = (n: number) => {
|
|
30
|
-
fetch(url, fetchOptions)
|
|
26
|
+
const retryWrapper = (n: number) => {
|
|
27
|
+
requestWrapper()
|
|
31
28
|
.then(res => resolve(res))
|
|
32
29
|
.catch(async err => {
|
|
33
30
|
if (n > 0) {
|
|
34
31
|
await delay(1000);
|
|
35
|
-
|
|
32
|
+
retryWrapper(--n);
|
|
36
33
|
} else {
|
|
37
34
|
reject(err);
|
|
38
35
|
}
|
|
39
36
|
});
|
|
40
37
|
};
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
const requestWrapper = (): Promise<Response> => {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
if (timeout) setTimeout(() => reject('error: timeout'), timeout);
|
|
42
|
+
return fetch(url, fetchOptions)
|
|
43
|
+
.then(res => resolve(res))
|
|
44
|
+
.catch(err => reject(err))
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
retryWrapper(retries);
|
|
43
49
|
});
|
|
44
50
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing';
|
|
3
|
+
|
|
4
|
+
describe('getHashedPercentageForObjIds', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
[[12, 93]],
|
|
7
|
+
[[uuidv4(), 99]],
|
|
8
|
+
[[99, uuidv4()]],
|
|
9
|
+
[[uuidv4(), uuidv4()]]
|
|
10
|
+
])('returns x where 0 <= x < 100', (objIds: (string|number)[]) => {
|
|
11
|
+
let result = getHashedPercentateForObjIds(objIds);
|
|
12
|
+
expect(result).toBeLessThan(100);
|
|
13
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
[[12, 93]],
|
|
18
|
+
[[uuidv4(), 99]],
|
|
19
|
+
[[99, uuidv4()]],
|
|
20
|
+
[[uuidv4(), uuidv4()]]
|
|
21
|
+
])('returns the same value each time', (objIds: (string|number)[]) => {
|
|
22
|
+
let resultOne = getHashedPercentateForObjIds(objIds);
|
|
23
|
+
let resultTwo = getHashedPercentateForObjIds(objIds);
|
|
24
|
+
expect(resultOne).toEqual(resultTwo);
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('is unique for different object ids', () => {
|
|
28
|
+
let resultOne = getHashedPercentateForObjIds([14, 106]);
|
|
29
|
+
let resultTwo = getHashedPercentateForObjIds([53, 200]);
|
|
30
|
+
expect(resultOne).not.toEqual(resultTwo);
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('is evenly distributed', () => {
|
|
34
|
+
// copied from python test here:
|
|
35
|
+
// https://github.com/Flagsmith/flagsmith-engine/blob/main/tests/unit/utils/test_utils_hashing.py#L56
|
|
36
|
+
const testSample = 500;
|
|
37
|
+
const numTestBuckets = 50;
|
|
38
|
+
const testBucketSize = Math.floor(testSample / numTestBuckets)
|
|
39
|
+
const errorFactor = 0.1
|
|
40
|
+
|
|
41
|
+
// Given
|
|
42
|
+
let objectIdPairs = Array.from(Array(testSample).keys()).flatMap(d => Array.from(Array(testSample).keys()).map(e => [d, e].flat()))
|
|
43
|
+
|
|
44
|
+
// When
|
|
45
|
+
let values = objectIdPairs.map((objIds) => getHashedPercentateForObjIds(objIds));
|
|
46
|
+
|
|
47
|
+
// Then
|
|
48
|
+
for (let i = 0; i++; i < numTestBuckets) {
|
|
49
|
+
let bucketStart = i * testBucketSize;
|
|
50
|
+
let bucketEnd = (i + 1) * testBucketSize;
|
|
51
|
+
let bucketValueLimit = Math.min(
|
|
52
|
+
(i + 1) / numTestBuckets + errorFactor + ((i + 1) / numTestBuckets),
|
|
53
|
+
1
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for (let i = bucketStart; i++; i < bucketEnd) {
|
|
57
|
+
expect(values[i]).toBeLessThanOrEqual(bucketValueLimit);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -172,6 +172,27 @@ test('test_default_flag_used_after_multiple_API_errors', async () => {
|
|
|
172
172
|
expect(flag.value).toBe(defaultFlag.value);
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test('default flag handler used when timeout occurs', async () => {
|
|
176
|
+
// @ts-ignore
|
|
177
|
+
fetch.mockReturnValue(Promise.resolve(sleep(10000)));
|
|
178
|
+
|
|
179
|
+
const defaultFlag = new DefaultFlag('some-default-value', true);
|
|
180
|
+
|
|
181
|
+
const defaultFlagHandler = (featureName: string) => defaultFlag;
|
|
182
|
+
|
|
183
|
+
const flg = new Flagsmith({
|
|
184
|
+
environmentKey: 'key',
|
|
185
|
+
defaultFlagHandler: defaultFlagHandler,
|
|
186
|
+
requestTimeoutSeconds: 0.1,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const flags = await flg.getEnvironmentFlags();
|
|
190
|
+
const flag = flags.getFlag('some_feature');
|
|
191
|
+
expect(flag.isDefault).toBe(true);
|
|
192
|
+
expect(flag.enabled).toBe(defaultFlag.enabled);
|
|
193
|
+
expect(flag.value).toBe(defaultFlag.value);
|
|
194
|
+
})
|
|
195
|
+
|
|
175
196
|
test('test_throws_when_no_identity_flags_returned_due_to_error', async () => {
|
|
176
197
|
// @ts-ignore
|
|
177
198
|
fetch.mockReturnValue(Promise.resolve(new Response('bad data')));
|
|
@@ -227,6 +248,22 @@ test('test onEnvironmentChange is called after error', async () => {
|
|
|
227
248
|
expect(callbackSpy).toBeCalled();
|
|
228
249
|
});
|
|
229
250
|
|
|
251
|
+
test('getIdentityFlags throws error if identifier is empty string', async () => {
|
|
252
|
+
const flagsmith = new Flagsmith({
|
|
253
|
+
environmentKey: 'key',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await expect(flagsmith.getIdentityFlags('')).rejects.toThrow('`identifier` argument is missing or invalid.');
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
test('getIdentitySegments throws error if identifier is empty string', () => {
|
|
261
|
+
const flagsmith = new Flagsmith({
|
|
262
|
+
environmentKey: 'key',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(() => { flagsmith.getIdentitySegments(''); }).toThrow('`identifier` argument is missing or invalid.');
|
|
266
|
+
})
|
|
230
267
|
|
|
231
268
|
|
|
232
269
|
async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
|
|
@@ -247,3 +284,9 @@ async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
|
|
|
247
284
|
})
|
|
248
285
|
})
|
|
249
286
|
}
|
|
287
|
+
|
|
288
|
+
function sleep(ms: number) {
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
setTimeout(resolve, ms);
|
|
291
|
+
});
|
|
292
|
+
}
|