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/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.identityUuid
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(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
 
@@ -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) {