flagsmith-nodejs 2.4.0 → 2.5.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.
- package/.vscode/launch.json +18 -0
- 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 +10 -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 +8 -2
- package/sdk/types.ts +2 -0
- package/tests/engine/unit/segments/segments_model.test.ts +10 -3
- package/tests/sdk/analytics.test.ts +21 -1
- package/tests/sdk/data/environment.json +23 -1
- package/tests/sdk/flagsmith-identity-flags.test.ts +18 -1
- package/tests/sdk/flagsmith.test.ts +25 -9
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
|
}
|
|
@@ -323,7 +328,8 @@ export class Flagsmith {
|
|
|
323
328
|
const flags = Flags.fromFeatureStateModels({
|
|
324
329
|
featureStates: featureStates,
|
|
325
330
|
analyticsProcessor: this.analyticsProcessor,
|
|
326
|
-
defaultFlagHandler: this.defaultFlagHandler
|
|
331
|
+
defaultFlagHandler: this.defaultFlagHandler,
|
|
332
|
+
identityID: identityModel.djangoID || identityModel.identityUuid
|
|
327
333
|
});
|
|
328
334
|
|
|
329
335
|
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
|
|
|
@@ -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
|
+
})
|
|
@@ -63,8 +63,30 @@
|
|
|
63
63
|
"type": "STANDARD",
|
|
64
64
|
"id": 1
|
|
65
65
|
},
|
|
66
|
-
"
|
|
66
|
+
"feature_segment": null,
|
|
67
67
|
"enabled": true
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"multivariate_feature_state_values": [
|
|
71
|
+
{
|
|
72
|
+
"percentage_allocation": 100,
|
|
73
|
+
"multivariate_feature_option": {
|
|
74
|
+
"value": "bar",
|
|
75
|
+
"id": 1
|
|
76
|
+
},
|
|
77
|
+
"mv_fs_value_uuid": "42d5cdf9-8ec9-4b8d-a3ca-fd43c64d5f05",
|
|
78
|
+
"id": 1
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"feature_state_value": "foo",
|
|
82
|
+
"feature": {
|
|
83
|
+
"name": "mv_feature",
|
|
84
|
+
"type": "MULTIVARIATE",
|
|
85
|
+
"id": 2
|
|
86
|
+
},
|
|
87
|
+
"feature_segment": null,
|
|
88
|
+
"featurestate_uuid": "96fc3503-09d7-48f1-a83b-2dc903d5c08a",
|
|
89
|
+
"enabled": false
|
|
68
90
|
}
|
|
69
91
|
]
|
|
70
92
|
}
|
|
@@ -27,7 +27,7 @@ test('test_get_identity_flags_calls_api_when_no_local_environment_no_traits', as
|
|
|
27
27
|
expect(identityFlags[0].featureName).toBe('some_feature');
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test('
|
|
30
|
+
test('test_get_identity_flags_uses_environment_when_local_environment_no_traits', async () => {
|
|
31
31
|
// @ts-ignore
|
|
32
32
|
fetch.mockReturnValue(Promise.resolve(new Response(environmentJSON())));
|
|
33
33
|
const identifier = 'identifier';
|
|
@@ -138,3 +138,20 @@ test('test_default_flag_is_used_when_no_identity_flags_returned_and_no_custom_de
|
|
|
138
138
|
expect(flag.enabled).toBe(false);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
|
|
142
|
+
test('test_get_identity_flags_multivariate_value_with_local_evaluation_enabled', async () => {
|
|
143
|
+
// @ts-ignore
|
|
144
|
+
fetch.mockReturnValue(Promise.resolve(new Response(environmentJSON())));
|
|
145
|
+
const identifier = 'identifier';
|
|
146
|
+
|
|
147
|
+
const flg = flagsmith({
|
|
148
|
+
environmentKey: 'ser.key',
|
|
149
|
+
enableLocalEvaluation: true,
|
|
150
|
+
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const identityFlags = (await flg.getIdentityFlags(identifier))
|
|
154
|
+
|
|
155
|
+
expect(identityFlags.getFeatureValue('mv_feature')).toBe('bar');
|
|
156
|
+
expect(identityFlags.isFeatureEnabled('mv_feature')).toBe(false);
|
|
157
|
+
});
|
|
@@ -45,16 +45,11 @@ test('test_update_environment_sets_environment', async () => {
|
|
|
45
45
|
await flg.updateEnvironment();
|
|
46
46
|
expect(flg.environment).toBeDefined();
|
|
47
47
|
|
|
48
|
-
// @ts-ignore
|
|
49
|
-
flg.environment.featureStates[0].featurestateUUID = undefined;
|
|
50
|
-
// @ts-ignore
|
|
51
|
-
flg.environment.project.segments[0].featureStates[0].featurestateUUID = undefined;
|
|
52
|
-
// @ts-ignore
|
|
53
48
|
const model = environmentModel(JSON.parse(environmentJSON()));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
|
|
50
|
+
wipeFeatureStateUUIDs(flg.environment)
|
|
51
|
+
wipeFeatureStateUUIDs(model)
|
|
52
|
+
|
|
58
53
|
expect(flg.environment).toStrictEqual(model);
|
|
59
54
|
});
|
|
60
55
|
|
|
@@ -231,3 +226,24 @@ test('test onEnvironmentChange is called after error', async () => {
|
|
|
231
226
|
|
|
232
227
|
expect(callbackSpy).toBeCalled();
|
|
233
228
|
});
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
|
|
233
|
+
// TODO: this has been pulled out of tests above as a helper function.
|
|
234
|
+
// I'm not entirely sure why it's necessary, however, we should look to remove.
|
|
235
|
+
enviromentModel.featureStates.forEach(fs => {
|
|
236
|
+
// @ts-ignore
|
|
237
|
+
fs.featurestateUUID = undefined;
|
|
238
|
+
fs.multivariateFeatureStateValues.forEach(mvfsv => {
|
|
239
|
+
// @ts-ignore
|
|
240
|
+
mvfsv.mvFsValueUuid = undefined;
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
enviromentModel.project.segments.forEach(s => {
|
|
244
|
+
s.featureStates.forEach(fs => {
|
|
245
|
+
// @ts-ignore
|
|
246
|
+
fs.featurestateUUID = undefined;
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
}
|