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/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(testCase[0], testCase[2], 'foo').matchesTraitValue(
99
- testCase[1]
105
+ new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(
106
+ traitValue
100
107
  )
101
- ).toBe(testCase[3]);
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
- "segment_id": null,
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('test_get_identity_flags_calls_api_when_local_environment_no_traits', async () => {
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
- // @ts-ignore
55
- model.featureStates[0].featurestateUUID = undefined;
56
- // @ts-ignore
57
- model.project.segments[0].featureStates[0].featurestateUUID = undefined;
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
+ }