flagsmith-nodejs 2.4.1 → 2.5.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.
- package/build/flagsmith-engine/segments/constants.d.ts +2 -0
- package/build/flagsmith-engine/segments/constants.js +4 -2
- package/build/flagsmith-engine/segments/models.js +6 -1
- package/build/sdk/analytics.d.ts +3 -0
- package/build/sdk/analytics.js +15 -1
- package/build/sdk/index.d.ts +2 -0
- package/build/sdk/index.js +15 -2
- package/build/sdk/types.d.ts +2 -0
- package/examples/api-proxy/package-lock.json +19 -25
- package/examples/basic/package-lock.json +29 -32
- package/examples/caching/package-lock.json +158 -78
- package/examples/custom-fetch-agent/package-lock.json +7 -13
- package/examples/local-evaluation/package-lock.json +7 -13
- package/flagsmith-engine/segments/constants.ts +3 -1
- package/flagsmith-engine/segments/models.ts +6 -1
- package/package.json +2 -1
- package/sdk/analytics.ts +23 -12
- package/sdk/index.ts +15 -2
- package/sdk/types.ts +2 -0
- package/tests/engine/unit/segments/segments_model.test.ts +10 -3
- package/tests/engine/unit/utils/utils.test.ts +62 -0
- package/tests/sdk/analytics.test.ts +21 -1
- package/tests/sdk/flagsmith.test.ts +17 -0
package/sdk/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { generateIdentitiesData, retryFetch } from './utils';
|
|
|
14
14
|
import { SegmentModel } from '../flagsmith-engine/segments/models';
|
|
15
15
|
import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators';
|
|
16
16
|
import { FlagsmithCache, FlagsmithConfig } from './types';
|
|
17
|
+
import pino, { Logger } from "pino";
|
|
17
18
|
|
|
18
19
|
export { AnalyticsProcessor } from './analytics';
|
|
19
20
|
export { FlagsmithAPIError, FlagsmithClientError } from './errors';
|
|
@@ -49,6 +50,7 @@ export class Flagsmith {
|
|
|
49
50
|
private cache?: FlagsmithCache;
|
|
50
51
|
private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
|
|
51
52
|
private analyticsProcessor?: AnalyticsProcessor;
|
|
53
|
+
private logger: Logger;
|
|
52
54
|
/**
|
|
53
55
|
* A Flagsmith client.
|
|
54
56
|
*
|
|
@@ -78,6 +80,7 @@ export class Flagsmith {
|
|
|
78
80
|
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
79
81
|
flags cannot be retrieved from the API or a non existent feature is
|
|
80
82
|
requested
|
|
83
|
+
@param data.logger: an instance of the pino Logger class to use for logging
|
|
81
84
|
*/
|
|
82
85
|
constructor(data: FlagsmithConfig) {
|
|
83
86
|
this.agent = data.agent;
|
|
@@ -97,6 +100,7 @@ export class Flagsmith {
|
|
|
97
100
|
this.identitiesUrl = `${this.apiUrl}identities/`;
|
|
98
101
|
this.environmentUrl = `${this.apiUrl}environment-document/`;
|
|
99
102
|
this.onEnvironmentChange = data.onEnvironmentChange;
|
|
103
|
+
this.logger = data.logger || pino();
|
|
100
104
|
|
|
101
105
|
if (!!data.cache) {
|
|
102
106
|
const missingMethods: string[] = ['has', 'get', 'set'].filter(method => data.cache && !data.cache[method]);
|
|
@@ -129,7 +133,8 @@ export class Flagsmith {
|
|
|
129
133
|
? new AnalyticsProcessor({
|
|
130
134
|
environmentKey: this.environmentKey,
|
|
131
135
|
baseApiUrl: this.apiUrl,
|
|
132
|
-
requestTimeoutMs: this.requestTimeoutMs
|
|
136
|
+
requestTimeoutMs: this.requestTimeoutMs,
|
|
137
|
+
logger: this.logger
|
|
133
138
|
})
|
|
134
139
|
: undefined;
|
|
135
140
|
}
|
|
@@ -168,6 +173,10 @@ export class Flagsmith {
|
|
|
168
173
|
* @returns Flags object holding all the flags for the given identity.
|
|
169
174
|
*/
|
|
170
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
|
+
|
|
171
180
|
const cachedItem = !!this.cache && await this.cache.get(`flags-${identifier}`);
|
|
172
181
|
if (!!cachedItem) {
|
|
173
182
|
return cachedItem;
|
|
@@ -198,6 +207,10 @@ export class Flagsmith {
|
|
|
198
207
|
identifier: string,
|
|
199
208
|
traits?: { [key: string]: any }
|
|
200
209
|
): Promise<SegmentModel[]> {
|
|
210
|
+
if (!identifier) {
|
|
211
|
+
throw new Error("`identifier` argument is missing or invalid.")
|
|
212
|
+
}
|
|
213
|
+
|
|
201
214
|
traits = traits || {};
|
|
202
215
|
if (this.enableLocalEvaluation) {
|
|
203
216
|
return new Promise((resolve, reject) => {
|
|
@@ -324,7 +337,7 @@ export class Flagsmith {
|
|
|
324
337
|
featureStates: featureStates,
|
|
325
338
|
analyticsProcessor: this.analyticsProcessor,
|
|
326
339
|
defaultFlagHandler: this.defaultFlagHandler,
|
|
327
|
-
identityID: identityModel.djangoID || identityModel.
|
|
340
|
+
identityID: identityModel.djangoID || identityModel.compositeKey
|
|
328
341
|
});
|
|
329
342
|
|
|
330
343
|
if (!!this.cache) {
|
package/sdk/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DefaultFlag, Flags } from "./models";
|
|
2
2
|
import { EnvironmentModel } from "../flagsmith-engine";
|
|
3
3
|
import { RequestInit } from "node-fetch";
|
|
4
|
+
import { Logger } from "pino";
|
|
4
5
|
|
|
5
6
|
export interface FlagsmithCache {
|
|
6
7
|
get(key: string): Promise<Flags|undefined> | undefined;
|
|
@@ -22,4 +23,5 @@ export interface FlagsmithConfig {
|
|
|
22
23
|
defaultFlagHandler?: (featureName: string) => DefaultFlag;
|
|
23
24
|
cache?: FlagsmithCache,
|
|
24
25
|
onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void,
|
|
26
|
+
logger?: Logger
|
|
25
27
|
}
|
|
@@ -89,16 +89,23 @@ const conditionMatchCases: [string, string | number | boolean, string, boolean][
|
|
|
89
89
|
[CONDITION_OPERATORS.MODULO, 35.0, "4|3", true],
|
|
90
90
|
[CONDITION_OPERATORS.MODULO, "foo", "4|3", false],
|
|
91
91
|
[CONDITION_OPERATORS.MODULO, 35.0, "foo|bar", false],
|
|
92
|
+
[CONDITION_OPERATORS.IN, "foo", "", false],
|
|
93
|
+
[CONDITION_OPERATORS.IN, "foo", "foo, bar", true],
|
|
94
|
+
[CONDITION_OPERATORS.IN, "foo", "foo", true],
|
|
95
|
+
[CONDITION_OPERATORS.IN, 1, "1,2,3,4", true],
|
|
96
|
+
[CONDITION_OPERATORS.IN, 1, "", false],
|
|
97
|
+
[CONDITION_OPERATORS.IN, 1, "1", true],
|
|
92
98
|
['BAD_OP', 'a', 'a', false]
|
|
93
99
|
];
|
|
94
100
|
|
|
95
101
|
test('test_segment_condition_matches_trait_value', () => {
|
|
96
102
|
for (const testCase of conditionMatchCases) {
|
|
103
|
+
const [operator, traitValue, conditionValue, expectedResult] = testCase
|
|
97
104
|
expect(
|
|
98
|
-
new SegmentConditionModel(
|
|
99
|
-
|
|
105
|
+
new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(
|
|
106
|
+
traitValue
|
|
100
107
|
)
|
|
101
|
-
).toBe(
|
|
108
|
+
).toBe(expectedResult);
|
|
102
109
|
}
|
|
103
110
|
});
|
|
104
111
|
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
console.log(objectIdPairs);
|
|
46
|
+
let values = objectIdPairs.map((objIds) => getHashedPercentateForObjIds(objIds));
|
|
47
|
+
|
|
48
|
+
// Then
|
|
49
|
+
for (let i = 0; i++; i < numTestBuckets) {
|
|
50
|
+
let bucketStart = i * testBucketSize;
|
|
51
|
+
let bucketEnd = (i + 1) * testBucketSize;
|
|
52
|
+
let bucketValueLimit = Math.min(
|
|
53
|
+
(i + 1) / numTestBuckets + errorFactor + ((i + 1) / numTestBuckets),
|
|
54
|
+
1
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for (let i = bucketStart; i++; i < bucketEnd) {
|
|
58
|
+
expect(values[i]).toBeLessThanOrEqual(bucketValueLimit);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
jest.mock('node-fetch');
|
|
2
1
|
import fetch from 'node-fetch';
|
|
3
2
|
import { analyticsProcessor } from './utils';
|
|
4
3
|
|
|
4
|
+
jest.mock('node-fetch', () => jest.fn());
|
|
5
|
+
|
|
5
6
|
afterEach(() => {
|
|
6
7
|
jest.clearAllMocks();
|
|
7
8
|
});
|
|
@@ -52,3 +53,22 @@ test('test_analytics_processor_flush_early_exit_if_analytics_data_is_empty', asy
|
|
|
52
53
|
await aP.flush();
|
|
53
54
|
expect(fetch).not.toHaveBeenCalled();
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
test('errors in fetch sending analytics data are swallowed', async () => {
|
|
59
|
+
// Given
|
|
60
|
+
// we mock the fetch function to throw and error to mimick a network failure
|
|
61
|
+
(fetch as jest.MockedFunction<typeof fetch>).mockRejectedValue(new Error('some error'));
|
|
62
|
+
|
|
63
|
+
// and create the processor and track a feature so there is some analytics data
|
|
64
|
+
const processor = analyticsProcessor();
|
|
65
|
+
processor.trackFeature('myFeature');
|
|
66
|
+
|
|
67
|
+
// When
|
|
68
|
+
// we flush the data to trigger the call to fetch
|
|
69
|
+
await processor.flush();
|
|
70
|
+
|
|
71
|
+
// Then
|
|
72
|
+
// we expect that fetch was called but the exception was handled
|
|
73
|
+
expect(fetch).toHaveBeenCalled();
|
|
74
|
+
})
|
|
@@ -227,6 +227,23 @@ test('test onEnvironmentChange is called after error', async () => {
|
|
|
227
227
|
expect(callbackSpy).toBeCalled();
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
test('getIdentityFlags throws error if identifier is empty string', async () => {
|
|
231
|
+
const flagsmith = new Flagsmith({
|
|
232
|
+
environmentKey: 'key',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await expect(flagsmith.getIdentityFlags('')).rejects.toThrow('`identifier` argument is missing or invalid.');
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
test('getIdentitySegments throws error if identifier is empty string', () => {
|
|
240
|
+
const flagsmith = new Flagsmith({
|
|
241
|
+
environmentKey: 'key',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(() => { flagsmith.getIdentitySegments(''); }).toThrow('`identifier` argument is missing or invalid.');
|
|
245
|
+
})
|
|
246
|
+
|
|
230
247
|
|
|
231
248
|
|
|
232
249
|
async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
|