flagsmith-nodejs 6.1.0 → 7.0.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.
Files changed (117) hide show
  1. package/.github/workflows/conventional-commit.yml +29 -0
  2. package/.github/workflows/publish.yml +20 -17
  3. package/.github/workflows/pull_request.yaml +36 -33
  4. package/.github/workflows/release-please.yml +18 -0
  5. package/.gitmodules +1 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.cjs +9 -1
  9. package/.release-please-manifest.json +1 -0
  10. package/CHANGELOG.md +592 -0
  11. package/CODEOWNERS +1 -0
  12. package/README.md +0 -2
  13. package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
  14. package/build/cjs/flagsmith-engine/environments/models.js +3 -1
  15. package/build/cjs/flagsmith-engine/environments/util.js +1 -1
  16. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  17. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
  18. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  19. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
  20. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  21. package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
  22. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  23. package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
  24. package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
  25. package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
  26. package/build/cjs/flagsmith-engine/features/models.js +1 -1
  27. package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
  28. package/build/cjs/flagsmith-engine/features/types.js +9 -0
  29. package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
  30. package/build/cjs/flagsmith-engine/features/util.js +5 -1
  31. package/build/cjs/flagsmith-engine/index.d.ts +61 -9
  32. package/build/cjs/flagsmith-engine/index.js +176 -56
  33. package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
  34. package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
  35. package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
  36. package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
  37. package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
  38. package/build/cjs/flagsmith-engine/segments/models.js +115 -13
  39. package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  40. package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
  41. package/build/cjs/sdk/analytics.js +3 -1
  42. package/build/cjs/sdk/index.d.ts +1 -3
  43. package/build/cjs/sdk/index.js +63 -24
  44. package/build/cjs/sdk/models.d.ts +8 -1
  45. package/build/cjs/sdk/models.js +29 -1
  46. package/build/cjs/sdk/utils.d.ts +1 -0
  47. package/build/cjs/sdk/utils.js +14 -1
  48. package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
  49. package/build/esm/flagsmith-engine/environments/models.js +3 -1
  50. package/build/esm/flagsmith-engine/environments/util.js +1 -1
  51. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
  52. package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
  53. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
  54. package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
  55. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
  56. package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
  57. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
  58. package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
  59. package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
  60. package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
  61. package/build/esm/flagsmith-engine/features/models.js +2 -2
  62. package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
  63. package/build/esm/flagsmith-engine/features/types.js +6 -0
  64. package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
  65. package/build/esm/flagsmith-engine/features/util.js +3 -0
  66. package/build/esm/flagsmith-engine/index.d.ts +61 -9
  67. package/build/esm/flagsmith-engine/index.js +161 -43
  68. package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
  69. package/build/esm/flagsmith-engine/segments/constants.js +1 -0
  70. package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
  71. package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
  72. package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
  73. package/build/esm/flagsmith-engine/segments/models.js +115 -13
  74. package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
  75. package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
  76. package/build/esm/sdk/analytics.js +3 -1
  77. package/build/esm/sdk/index.d.ts +1 -3
  78. package/build/esm/sdk/index.js +63 -24
  79. package/build/esm/sdk/models.d.ts +8 -1
  80. package/build/esm/sdk/models.js +29 -1
  81. package/build/esm/sdk/utils.d.ts +1 -0
  82. package/build/esm/sdk/utils.js +12 -0
  83. package/flagsmith-engine/environments/models.ts +3 -1
  84. package/flagsmith-engine/environments/util.ts +2 -1
  85. package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
  86. package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
  87. package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
  88. package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
  89. package/flagsmith-engine/evaluation/models.ts +96 -0
  90. package/flagsmith-engine/features/models.ts +3 -2
  91. package/flagsmith-engine/features/types.ts +5 -0
  92. package/flagsmith-engine/features/util.ts +4 -0
  93. package/flagsmith-engine/index.ts +229 -72
  94. package/flagsmith-engine/segments/constants.ts +1 -0
  95. package/flagsmith-engine/segments/evaluators.ts +178 -62
  96. package/flagsmith-engine/segments/models.ts +171 -23
  97. package/flagsmith-engine/utils/hashing/index.ts +2 -2
  98. package/package.json +13 -2
  99. package/release-please-config.json +62 -0
  100. package/sdk/analytics.ts +3 -1
  101. package/sdk/index.ts +89 -30
  102. package/sdk/models.ts +44 -2
  103. package/sdk/utils.ts +13 -0
  104. package/tests/engine/e2e/engine.test.ts +43 -38
  105. package/tests/engine/unit/engine.test.ts +306 -60
  106. package/tests/engine/unit/mappers.test.ts +353 -0
  107. package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
  108. package/tests/engine/unit/segments/segments_model.test.ts +85 -0
  109. package/tests/engine/unit/utils/utils.test.ts +7 -7
  110. package/tests/engine/unit/utils.ts +1 -1
  111. package/tests/sdk/analytics.test.ts +6 -1
  112. package/tests/sdk/data/environment.json +1 -0
  113. package/tests/sdk/flagsmith-environment-flags.test.ts +28 -0
  114. package/tests/sdk/flagsmith-identity-flags.test.ts +11 -2
  115. package/tests/sdk/flagsmith.test.ts +190 -3
  116. package/tests/sdk/offline-handlers.test.ts +3 -1
  117. package/vitest.config.esm.ts +34 -0
@@ -1,7 +1,10 @@
1
1
  import * as semver from 'semver';
2
+ import { FeatureModel, FeatureStateModel, MultivariateFeatureOptionModel, MultivariateFeatureStateValueModel } from '../features/models.js';
2
3
  import { getCastingFunction as getCastingFunction } from '../utils/index.js';
3
4
  import { ALL_RULE, ANY_RULE, NONE_RULE, NOT_CONTAINS, REGEX, MODULO, IN, CONDITION_OPERATORS } from './constants.js';
4
5
  import { isSemver } from './util.js';
6
+ import { CONSTANTS } from '../features/constants.js';
7
+ import { SegmentSource } from '../evaluation/models.js';
5
8
  export const all = (iterable) => iterable.filter(e => !!e).length === iterable.length;
6
9
  export const any = (iterable) => iterable.filter(e => !!e).length > 0;
7
10
  export const matchingFunctions = {
@@ -11,15 +14,33 @@ export const matchingFunctions = {
11
14
  [CONDITION_OPERATORS.LESS_THAN]: (thisValue, otherValue) => thisValue > otherValue,
12
15
  [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue, otherValue) => thisValue >= otherValue,
13
16
  [CONDITION_OPERATORS.NOT_EQUAL]: (thisValue, otherValue) => thisValue != otherValue,
14
- [CONDITION_OPERATORS.CONTAINS]: (thisValue, otherValue) => !!otherValue && otherValue.includes(thisValue)
17
+ [CONDITION_OPERATORS.CONTAINS]: (thisValue, otherValue) => {
18
+ try {
19
+ return !!otherValue && otherValue.includes(thisValue);
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ };
26
+ // Semver library throws an error if the version is invalid, in this case, we want to catch and return false
27
+ const safeSemverCompare = (semverMatchingFunction) => {
28
+ return (conditionValue, traitValue) => {
29
+ try {
30
+ return semverMatchingFunction(conditionValue, traitValue);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ };
15
36
  };
16
37
  export const semverMatchingFunction = {
17
38
  ...matchingFunctions,
18
- [CONDITION_OPERATORS.EQUAL]: (thisValue, otherValue) => semver.eq(thisValue, otherValue),
19
- [CONDITION_OPERATORS.GREATER_THAN]: (thisValue, otherValue) => semver.gt(otherValue, thisValue),
20
- [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue, otherValue) => semver.gte(otherValue, thisValue),
21
- [CONDITION_OPERATORS.LESS_THAN]: (thisValue, otherValue) => semver.gt(thisValue, otherValue),
22
- [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue, otherValue) => semver.gte(thisValue, otherValue)
39
+ [CONDITION_OPERATORS.EQUAL]: safeSemverCompare((conditionValue, traitValue) => semver.eq(traitValue, conditionValue)),
40
+ [CONDITION_OPERATORS.GREATER_THAN]: safeSemverCompare((conditionValue, traitValue) => semver.gt(traitValue, conditionValue)),
41
+ [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) => semver.gte(traitValue, conditionValue)),
42
+ [CONDITION_OPERATORS.LESS_THAN]: safeSemverCompare((conditionValue, traitValue) => semver.lt(traitValue, conditionValue)),
43
+ [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) => semver.lte(traitValue, conditionValue))
23
44
  };
24
45
  export const getMatchingFunctions = (semver) => semver ? semverMatchingFunction : matchingFunctions;
25
46
  export class SegmentConditionModel {
@@ -31,11 +52,11 @@ export class SegmentConditionModel {
31
52
  };
32
53
  operator;
33
54
  value;
34
- property_;
55
+ property;
35
56
  constructor(operator, value, property) {
36
57
  this.operator = operator;
37
58
  this.value = value;
38
- this.property_ = property;
59
+ this.property = property;
39
60
  }
40
61
  matchesTraitValue(traitValue) {
41
62
  const evaluators = {
@@ -45,17 +66,49 @@ export class SegmentConditionModel {
45
66
  !traitValue.includes(this.value?.toString()));
46
67
  },
47
68
  evaluateRegex: (traitValue) => {
48
- return !!this.value && !!traitValue?.toString().match(new RegExp(this.value));
69
+ try {
70
+ if (!this.value) {
71
+ return false;
72
+ }
73
+ const regex = new RegExp(this.value?.toString());
74
+ return !!traitValue?.toString().match(regex);
75
+ }
76
+ catch {
77
+ return false;
78
+ }
49
79
  },
50
80
  evaluateModulo: (traitValue) => {
51
- if (isNaN(parseFloat(traitValue)) || !this.value) {
81
+ const parsedTraitValue = parseFloat(traitValue);
82
+ if (isNaN(parsedTraitValue) || !this.value) {
83
+ return false;
84
+ }
85
+ const parts = this.value.toString().split('|');
86
+ if (parts.length !== 2) {
87
+ return false;
88
+ }
89
+ const divisor = parseFloat(parts[0]);
90
+ const remainder = parseFloat(parts[1]);
91
+ if (isNaN(divisor) || isNaN(remainder) || divisor === 0) {
52
92
  return false;
53
93
  }
54
- const parts = this.value.split('|');
55
- const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])];
56
- return traitValue % divisor === reminder;
94
+ return parsedTraitValue % divisor === remainder;
57
95
  },
58
96
  evaluateIn: (traitValue) => {
97
+ if (!traitValue || typeof traitValue === 'boolean') {
98
+ return false;
99
+ }
100
+ if (Array.isArray(this.value)) {
101
+ return this.value.includes(traitValue.toString());
102
+ }
103
+ if (typeof this.value === 'string') {
104
+ try {
105
+ const parsed = JSON.parse(this.value);
106
+ if (Array.isArray(parsed)) {
107
+ return parsed.includes(traitValue.toString());
108
+ }
109
+ }
110
+ catch { }
111
+ }
59
112
  return this.value?.split(',').includes(traitValue.toString());
60
113
  }
61
114
  };
@@ -99,4 +152,53 @@ export class SegmentModel {
99
152
  this.id = id;
100
153
  this.name = name;
101
154
  }
155
+ static fromSegmentResult(segmentResults, evaluationContext) {
156
+ const segmentModels = [];
157
+ if (!evaluationContext.segments) {
158
+ return [];
159
+ }
160
+ for (const segmentResult of segmentResults) {
161
+ if (segmentResult.metadata?.source === SegmentSource.IDENTITY_OVERRIDE) {
162
+ continue;
163
+ }
164
+ const segmentMetadataId = segmentResult.metadata?.id;
165
+ if (!segmentMetadataId) {
166
+ continue;
167
+ }
168
+ const segmentContext = evaluationContext.segments[segmentMetadataId.toString()];
169
+ if (segmentContext) {
170
+ const segment = new SegmentModel(segmentMetadataId, segmentContext.name);
171
+ segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type));
172
+ segment.featureStates = SegmentModel.createFeatureStatesFromOverrides(segmentContext.overrides || []);
173
+ segmentModels.push(segment);
174
+ }
175
+ }
176
+ return segmentModels;
177
+ }
178
+ static createFeatureStatesFromOverrides(overrides) {
179
+ if (!overrides)
180
+ return [];
181
+ return overrides
182
+ .filter(override => {
183
+ const overrideMetadataId = override?.metadata?.id;
184
+ return typeof overrideMetadataId === 'number';
185
+ })
186
+ .map(override => {
187
+ const overrideMetadataId = override.metadata.id;
188
+ const feature = new FeatureModel(overrideMetadataId, override.name, override.variants?.length && override.variants.length > 0
189
+ ? CONSTANTS.MULTIVARIATE
190
+ : CONSTANTS.STANDARD);
191
+ const featureState = new FeatureStateModel(feature, override.enabled, override.priority || 0);
192
+ if (override.value !== undefined) {
193
+ featureState.setValue(override.value);
194
+ }
195
+ if (override.variants && override.variants.length > 0) {
196
+ featureState.multivariateFeatureStateValues = this.createMultivariateValues(override.variants);
197
+ }
198
+ return featureState;
199
+ });
200
+ }
201
+ static createMultivariateValues(variants) {
202
+ return variants.map(variant => new MultivariateFeatureStateValueModel(new MultivariateFeatureOptionModel(variant.value, variant.id), variant.weight, variant.id));
203
+ }
102
204
  }
@@ -6,4 +6,4 @@
6
6
  * @param {} iterations=1 num times to include each id in the generated string to hash
7
7
  * @returns number number between 0 (inclusive) and 100 (exclusive)
8
8
  */
9
- export declare function getHashedPercentateForObjIds(objectIds: Array<any>, iterations?: number): number;
9
+ export declare function getHashedPercentageForObjIds(objectIds: Array<any>, iterations?: number): number;
@@ -10,7 +10,7 @@ const makeRepeated = (arr, repeats) => Array.from({ length: repeats }, () => arr
10
10
  * @param {} iterations=1 num times to include each id in the generated string to hash
11
11
  * @returns number number between 0 (inclusive) and 100 (exclusive)
12
12
  */
13
- export function getHashedPercentateForObjIds(objectIds, iterations = 1) {
13
+ export function getHashedPercentageForObjIds(objectIds, iterations = 1) {
14
14
  let toHash = makeRepeated(objectIds, iterations).join(',');
15
15
  const hashedValue = md5(toHash);
16
16
  const hashedInt = BigInt('0x' + hashedValue);
@@ -19,7 +19,7 @@ export function getHashedPercentateForObjIds(objectIds, iterations = 1) {
19
19
  /* istanbul ignore next */
20
20
  if (value === 100) {
21
21
  /* istanbul ignore next */
22
- return getHashedPercentateForObjIds(objectIds, iterations + 1);
22
+ return getHashedPercentageForObjIds(objectIds, iterations + 1);
23
23
  }
24
24
  return value;
25
25
  }
@@ -1,4 +1,5 @@
1
1
  import { pino } from 'pino';
2
+ import { getUserAgent } from './utils.js';
2
3
  export const ANALYTICS_ENDPOINT = './analytics/flags/';
3
4
  /** Duration in seconds to wait before trying to flush collected data after {@link trackFeature} is called. **/
4
5
  const ANALYTICS_TIMER = 10;
@@ -44,7 +45,8 @@ export class AnalyticsProcessor {
44
45
  signal: AbortSignal.timeout(this.requestTimeoutMs),
45
46
  headers: {
46
47
  'Content-Type': 'application/json',
47
- 'X-Environment-Key': this.environmentKey
48
+ 'X-Environment-Key': this.environmentKey,
49
+ 'User-Agent': getUserAgent()
48
50
  }
49
51
  });
50
52
  await this.currentFlush;
@@ -1,10 +1,8 @@
1
1
  import { Dispatcher } from 'undici-types';
2
- import { EnvironmentModel } from '../flagsmith-engine/index.js';
3
- import { IdentityModel } from '../flagsmith-engine/index.js';
4
2
  import { BaseOfflineHandler } from './offline_handlers.js';
5
3
  import { DefaultFlag, Flags } from './models.js';
6
4
  import { EnvironmentDataPollingManager } from './polling_manager.js';
7
- import { SegmentModel } from '../flagsmith-engine/index.js';
5
+ import { SegmentModel, EnvironmentModel, IdentityModel } from '../flagsmith-engine/index.js';
8
6
  import { FlagsmithConfig, FlagsmithTraitValue, TraitConfig } from './types.js';
9
7
  export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js';
10
8
  export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
@@ -1,14 +1,12 @@
1
- import { getEnvironmentFeatureStates, getIdentityFeatureStates } from '../flagsmith-engine/index.js';
2
1
  import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js';
3
- import { IdentityModel } from '../flagsmith-engine/index.js';
4
- import { TraitModel } from '../flagsmith-engine/index.js';
5
2
  import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js';
6
- import { FlagsmithAPIError } from './errors.js';
3
+ import { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
7
4
  import { Flags } from './models.js';
8
5
  import { EnvironmentDataPollingManager } from './polling_manager.js';
9
- import { Deferred, generateIdentitiesData, retryFetch } from './utils.js';
10
- import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js';
6
+ import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js';
7
+ import { SegmentModel, IdentityModel, TraitModel, getEvaluationResult } from '../flagsmith-engine/index.js';
11
8
  import { pino } from 'pino';
9
+ import { getEvaluationContext } from '../flagsmith-engine/evaluation/evaluationContext/mappers.js';
12
10
  export { AnalyticsProcessor } from './analytics.js';
13
11
  export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
14
12
  export { BaseFlag, DefaultFlag, Flags } from './models.js';
@@ -220,7 +218,12 @@ export class Flagsmith {
220
218
  key,
221
219
  value: traits?.[key]
222
220
  })));
223
- return getIdentitySegments(environment, identityModel);
221
+ const context = getEvaluationContext(environment, identityModel);
222
+ if (!context) {
223
+ throw new FlagsmithClientError('Local evaluation required to obtain identity segments');
224
+ }
225
+ const evaluationResult = getEvaluationResult(context);
226
+ return SegmentModel.fromSegmentResult(evaluationResult.segments, context);
224
227
  }
225
228
  async fetchEnvironment() {
226
229
  const deferred = new Deferred();
@@ -269,6 +272,7 @@ export class Flagsmith {
269
272
  if (this.environmentKey) {
270
273
  headers['X-Environment-Key'] = this.environmentKey;
271
274
  }
275
+ headers['User-Agent'] = getUserAgent();
272
276
  if (this.customHeaders) {
273
277
  for (const [k, v] of Object.entries(this.customHeaders)) {
274
278
  headers[k] = v;
@@ -283,7 +287,7 @@ export class Flagsmith {
283
287
  if (data.status !== 200) {
284
288
  throw new FlagsmithAPIError(`Invalid request made to Flagsmith API. Response status code: ${data.status}`);
285
289
  }
286
- return data.json();
290
+ return { response: data, data: await data.json() };
287
291
  }
288
292
  /**
289
293
  * This promise ensures that the environment is retrieved before attempting to locally evaluate.
@@ -310,16 +314,52 @@ export class Flagsmith {
310
314
  if (!this.environmentUrl) {
311
315
  throw new Error('`apiUrl` argument is missing or invalid.');
312
316
  }
313
- const environment_data = await this.getJSONResponse(this.environmentUrl, 'GET');
314
- return buildEnvironmentModel(environment_data);
317
+ const startTime = Date.now();
318
+ const documents = [];
319
+ let url = this.environmentUrl;
320
+ let loggedWarning = false;
321
+ while (true) {
322
+ try {
323
+ if (!loggedWarning) {
324
+ const elapsedMs = Date.now() - startTime;
325
+ if (elapsedMs > this.environmentRefreshIntervalSeconds * 1000) {
326
+ this.logger.warn(`Environment document retrieval exceeded the polling interval of ${this.environmentRefreshIntervalSeconds} seconds.`);
327
+ loggedWarning = true;
328
+ }
329
+ }
330
+ const { response, data } = await this.getJSONResponse(url, 'GET');
331
+ documents.push(data);
332
+ const linkHeader = response.headers.get('link');
333
+ if (linkHeader) {
334
+ const nextMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
335
+ if (nextMatch) {
336
+ const relativeUrl = decodeURIComponent(nextMatch[1]);
337
+ url = new URL(relativeUrl, this.apiUrl).href;
338
+ continue;
339
+ }
340
+ }
341
+ break;
342
+ }
343
+ catch (error) {
344
+ throw error;
345
+ }
346
+ }
347
+ // Compile the document
348
+ const compiledDocument = documents[0];
349
+ for (let i = 1; i < documents.length; i++) {
350
+ compiledDocument.identity_overrides = compiledDocument.identity_overrides || [];
351
+ compiledDocument.identity_overrides.push(...(documents[i].identity_overrides || []));
352
+ }
353
+ return buildEnvironmentModel(compiledDocument);
315
354
  }
316
355
  async getEnvironmentFlagsFromDocument() {
317
356
  const environment = await this.getEnvironment();
318
- const flags = Flags.fromFeatureStateModels({
319
- featureStates: getEnvironmentFeatureStates(environment),
320
- analyticsProcessor: this.analyticsProcessor,
321
- defaultFlagHandler: this.defaultFlagHandler
322
- });
357
+ const context = getEvaluationContext(environment, undefined, undefined, true);
358
+ if (!context) {
359
+ throw new FlagsmithClientError('Unable to get flags. No environment present.');
360
+ }
361
+ const evaluationResult = getEvaluationResult(context);
362
+ const flags = Flags.fromEvaluationResult(evaluationResult);
323
363
  if (!!this.cache) {
324
364
  await this.cache.set('flags', flags);
325
365
  }
@@ -331,13 +371,12 @@ export class Flagsmith {
331
371
  key,
332
372
  value: traits[key]
333
373
  })));
334
- const featureStates = getIdentityFeatureStates(environment, identityModel);
335
- const flags = Flags.fromFeatureStateModels({
336
- featureStates: featureStates,
337
- analyticsProcessor: this.analyticsProcessor,
338
- defaultFlagHandler: this.defaultFlagHandler,
339
- identityID: identityModel.djangoID || identityModel.compositeKey
340
- });
374
+ const context = getEvaluationContext(environment, identityModel);
375
+ if (!context) {
376
+ throw new FlagsmithClientError('Unable to get flags. No environment present.');
377
+ }
378
+ const evaluationResult = getEvaluationResult(context);
379
+ const flags = Flags.fromEvaluationResult(evaluationResult, this.defaultFlagHandler, this.analyticsProcessor);
341
380
  if (!!this.cache) {
342
381
  await this.cache.set(`flags-${identifier}`, flags);
343
382
  }
@@ -347,7 +386,7 @@ export class Flagsmith {
347
386
  if (!this.environmentFlagsUrl) {
348
387
  throw new Error('`apiUrl` argument is missing or invalid.');
349
388
  }
350
- const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
389
+ const { data: apiFlags } = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
351
390
  const flags = Flags.fromAPIFlags({
352
391
  apiFlags: apiFlags,
353
392
  analyticsProcessor: this.analyticsProcessor,
@@ -363,7 +402,7 @@ export class Flagsmith {
363
402
  throw new Error('`apiUrl` argument is missing or invalid.');
364
403
  }
365
404
  const data = generateIdentitiesData(identifier, traits, transient);
366
- const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
405
+ const { data: jsonResponse } = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
367
406
  const flags = Flags.fromAPIFlags({
368
407
  apiFlags: jsonResponse['flags'],
369
408
  analyticsProcessor: this.analyticsProcessor,
@@ -1,6 +1,7 @@
1
+ import { EvaluationResultWithMetadata } from '../flagsmith-engine/evaluation/models.js';
1
2
  import { FeatureStateModel } from '../flagsmith-engine/features/models.js';
2
3
  import { AnalyticsProcessor } from './analytics.js';
3
- type FlagValue = string | number | boolean | undefined;
4
+ type FlagValue = string | number | boolean | undefined | null;
4
5
  /**
5
6
  * A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}.
6
7
  */
@@ -38,12 +39,17 @@ export declare class Flag extends BaseFlag {
38
39
  * The programmatic name for this feature, unique per Flagsmith project.
39
40
  */
40
41
  featureName: string;
42
+ /**
43
+ * The reason for this feature, unique per Flagsmith project.
44
+ */
45
+ reason?: string;
41
46
  constructor(params: {
42
47
  value: FlagValue;
43
48
  enabled: boolean;
44
49
  isDefault?: boolean;
45
50
  featureId: number;
46
51
  featureName: string;
52
+ reason?: string;
47
53
  });
48
54
  static fromFeatureStateModel(fsm: FeatureStateModel, identityId: number | string | undefined): Flag;
49
55
  static fromAPIFlag(flagData: any): Flag;
@@ -61,6 +67,7 @@ export declare class Flags {
61
67
  defaultFlagHandler?: (v: string) => DefaultFlag;
62
68
  analyticsProcessor?: AnalyticsProcessor;
63
69
  });
70
+ static fromEvaluationResult(evaluationResult: EvaluationResultWithMetadata, defaultFlagHandler?: (v: string) => DefaultFlag, analyticsProcessor?: AnalyticsProcessor): Flags;
64
71
  static fromFeatureStateModels(data: {
65
72
  featureStates: FeatureStateModel[];
66
73
  analyticsProcessor?: AnalyticsProcessor;
@@ -41,10 +41,15 @@ export class Flag extends BaseFlag {
41
41
  * The programmatic name for this feature, unique per Flagsmith project.
42
42
  */
43
43
  featureName;
44
+ /**
45
+ * The reason for this feature, unique per Flagsmith project.
46
+ */
47
+ reason;
44
48
  constructor(params) {
45
49
  super(params.value, params.enabled, !!params.isDefault);
46
50
  this.featureId = params.featureId;
47
51
  this.featureName = params.featureName;
52
+ this.reason = params.reason;
48
53
  }
49
54
  static fromFeatureStateModel(fsm, identityId) {
50
55
  return new Flag({
@@ -59,7 +64,8 @@ export class Flag extends BaseFlag {
59
64
  enabled: flagData['enabled'],
60
65
  value: flagData['feature_state_value'] ?? flagData['value'],
61
66
  featureId: flagData['feature']['id'],
62
- featureName: flagData['feature']['name']
67
+ featureName: flagData['feature']['name'],
68
+ reason: flagData['feature']['reason']
63
69
  });
64
70
  }
65
71
  }
@@ -72,6 +78,28 @@ export class Flags {
72
78
  this.defaultFlagHandler = data.defaultFlagHandler;
73
79
  this.analyticsProcessor = data.analyticsProcessor;
74
80
  }
81
+ static fromEvaluationResult(evaluationResult, defaultFlagHandler, analyticsProcessor) {
82
+ const flags = {};
83
+ for (const flag of Object.values(evaluationResult.flags)) {
84
+ const flagMetadataId = flag.metadata?.id;
85
+ if (!flagMetadataId) {
86
+ throw new Error(`FlagResult metadata.id is missing for feature "${flag.name}". ` +
87
+ `This indicates a bug in the SDK, please report it.`);
88
+ }
89
+ flags[flag.name] = new Flag({
90
+ enabled: flag.enabled,
91
+ value: flag.value ?? null,
92
+ featureId: flagMetadataId,
93
+ featureName: flag.name,
94
+ reason: flag.reason
95
+ });
96
+ }
97
+ return new Flags({
98
+ flags: flags,
99
+ defaultFlagHandler: defaultFlagHandler,
100
+ analyticsProcessor: analyticsProcessor
101
+ });
102
+ }
75
103
  static fromFeatureStateModels(data) {
76
104
  const flags = {};
77
105
  for (const fs of data.featureStates) {
@@ -56,4 +56,5 @@ export declare class Deferred<T> {
56
56
  resolve(value: T | PromiseLike<T>): void;
57
57
  reject(reason?: unknown): void;
58
58
  }
59
+ export declare function getUserAgent(): string;
59
60
  export {};
@@ -1,3 +1,5 @@
1
+ const FLAGSMITH_USER_AGENT = 'flagsmith-nodejs-sdk';
2
+ const FLAGSMITH_UNKNOWN_VERSION = 'unknown';
1
3
  export function isTraitConfig(traitValue) {
2
4
  return !!traitValue && typeof traitValue == 'object' && traitValue.value !== undefined;
3
5
  }
@@ -85,3 +87,13 @@ export class Deferred {
85
87
  this.rejectPromise(reason);
86
88
  }
87
89
  }
90
+ export function getUserAgent() {
91
+ try {
92
+ const packageJson = require('../package.json');
93
+ const version = packageJson?.version;
94
+ return version ? `${FLAGSMITH_USER_AGENT}/${version}` : FLAGSMITH_UNKNOWN_VERSION;
95
+ }
96
+ catch {
97
+ return FLAGSMITH_UNKNOWN_VERSION;
98
+ }
99
+ }
@@ -38,10 +38,12 @@ export class EnvironmentModel {
38
38
  project: ProjectModel;
39
39
  featureStates: FeatureStateModel[] = [];
40
40
  identityOverrides: IdentityModel[] = [];
41
+ name: string;
41
42
 
42
- constructor(id: number, apiKey: string, project: ProjectModel) {
43
+ constructor(id: number, apiKey: string, project: ProjectModel, name: string) {
43
44
  this.id = id;
44
45
  this.apiKey = apiKey;
45
46
  this.project = project;
47
+ this.name = name;
46
48
  }
47
49
  }
@@ -11,7 +11,8 @@ export function buildEnvironmentModel(environmentJSON: any) {
11
11
  const environmentModel = new EnvironmentModel(
12
12
  environmentJSON.id,
13
13
  environmentJSON.api_key,
14
- project
14
+ project,
15
+ environmentJSON.name
15
16
  );
16
17
  environmentModel.featureStates = featureStates;
17
18
  if (!!environmentJSON.identity_overrides) {