flagsmith-nodejs 8.0.0 → 8.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.
@@ -1 +1 @@
1
- {".":"8.0.0"}
1
+ {".":"8.0.2"}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [8.0.2](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v8.0.1...v8.0.2) (2026-03-19)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Missing variants in feature engine context ([#257](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/257)) ([7e191b8](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/7e191b8fff2634e58ff4ade92d4729b0b51ed6dc))
9
+
10
+
11
+ ### Other
12
+
13
+ * **deps:** bump minimatch and npm ([#255](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/255)) ([c18c3b3](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/c18c3b357243df72708fdc3e0184589b25efa8a1))
14
+
15
+ ## [8.0.1](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v8.0.0...v8.0.1) (2026-03-16)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * unwrap TraitConfig values in local evaluation before segment matching ([#252](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/252)) ([4e37994](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/4e37994829bb741665a26aa38d543bebe51231d8))
21
+
3
22
  ## [8.0.0](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v7.0.3...v8.0.0) (2026-02-25)
4
23
 
5
24
 
@@ -27,19 +27,12 @@ function mapEnvironmentModelToEvaluationContext(environment) {
27
27
  };
28
28
  const features = {};
29
29
  for (const fs of environment.featureStates) {
30
- const variants = fs.multivariateFeatureStateValues?.length > 0
31
- ? fs.multivariateFeatureStateValues.map(mv => ({
32
- value: mv.multivariateFeatureOption.value,
33
- weight: mv.percentageAllocation,
34
- priority: mv.id ?? (0, util_js_1.uuidToBigInt)(mv.mvFsValueUuid)
35
- }))
36
- : undefined;
37
30
  features[fs.feature.name] = {
38
31
  key: fs.djangoID?.toString() || fs.featurestateUUID,
39
32
  name: fs.feature.name,
40
33
  enabled: fs.enabled,
41
34
  value: fs.getValue(),
42
- variants,
35
+ variants: mapFeatureStateVariants(fs),
43
36
  priority: fs.featureSegment?.priority,
44
37
  metadata: {
45
38
  id: fs.feature.id
@@ -58,6 +51,7 @@ function mapEnvironmentModelToEvaluationContext(environment) {
58
51
  name: fs.feature.name,
59
52
  enabled: fs.enabled,
60
53
  value: fs.getValue(),
54
+ variants: mapFeatureStateVariants(fs),
61
55
  priority: fs.featureSegment?.priority,
62
56
  metadata: {
63
57
  id: fs.feature.id
@@ -95,6 +89,15 @@ function mapIdentityModelToIdentityContext(identity, overrideTraits) {
95
89
  };
96
90
  return identityContext;
97
91
  }
92
+ function mapFeatureStateVariants(fs) {
93
+ return fs.multivariateFeatureStateValues?.length > 0
94
+ ? fs.multivariateFeatureStateValues.map(mv => ({
95
+ value: mv.multivariateFeatureOption.value,
96
+ weight: mv.percentageAllocation,
97
+ priority: mv.id ?? (0, util_js_1.uuidToBigInt)(mv.mvFsValueUuid)
98
+ }))
99
+ : undefined;
100
+ }
98
101
  function mapSegmentRuleModelToRule(rule) {
99
102
  return {
100
103
  type: rule.type,
@@ -118,6 +121,7 @@ function mapIdentityOverridesToSegments(identityOverrides) {
118
121
  name: fs.feature.name,
119
122
  enabled: fs.enabled,
120
123
  value: fs.getValue(),
124
+ variants: mapFeatureStateVariants(fs),
121
125
  priority: -Infinity,
122
126
  metadata: {
123
127
  id: fs.feature.id
@@ -226,7 +226,7 @@ class Flagsmith {
226
226
  const environment = await this.getEnvironment();
227
227
  const identityModel = this.getIdentityModel(environment, identifier, Object.keys(traits || {}).map(key => ({
228
228
  key,
229
- value: traits?.[key]
229
+ value: (0, utils_js_1.isTraitConfig)(traits?.[key]) ? traits[key].value : traits?.[key]
230
230
  })));
231
231
  const context = (0, mappers_js_1.getEvaluationContext)(environment, identityModel);
232
232
  if (!context) {
@@ -379,7 +379,7 @@ class Flagsmith {
379
379
  const environment = await this.getEnvironment();
380
380
  const identityModel = this.getIdentityModel(environment, identifier, Object.keys(traits).map(key => ({
381
381
  key,
382
- value: traits[key]
382
+ value: (0, utils_js_1.isTraitConfig)(traits[key]) ? traits[key].value : traits[key]
383
383
  })));
384
384
  const context = (0, mappers_js_1.getEvaluationContext)(environment, identityModel);
385
385
  if (!context) {
@@ -23,19 +23,12 @@ function mapEnvironmentModelToEvaluationContext(environment) {
23
23
  };
24
24
  const features = {};
25
25
  for (const fs of environment.featureStates) {
26
- const variants = fs.multivariateFeatureStateValues?.length > 0
27
- ? fs.multivariateFeatureStateValues.map(mv => ({
28
- value: mv.multivariateFeatureOption.value,
29
- weight: mv.percentageAllocation,
30
- priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid)
31
- }))
32
- : undefined;
33
26
  features[fs.feature.name] = {
34
27
  key: fs.djangoID?.toString() || fs.featurestateUUID,
35
28
  name: fs.feature.name,
36
29
  enabled: fs.enabled,
37
30
  value: fs.getValue(),
38
- variants,
31
+ variants: mapFeatureStateVariants(fs),
39
32
  priority: fs.featureSegment?.priority,
40
33
  metadata: {
41
34
  id: fs.feature.id
@@ -54,6 +47,7 @@ function mapEnvironmentModelToEvaluationContext(environment) {
54
47
  name: fs.feature.name,
55
48
  enabled: fs.enabled,
56
49
  value: fs.getValue(),
50
+ variants: mapFeatureStateVariants(fs),
57
51
  priority: fs.featureSegment?.priority,
58
52
  metadata: {
59
53
  id: fs.feature.id
@@ -91,6 +85,15 @@ function mapIdentityModelToIdentityContext(identity, overrideTraits) {
91
85
  };
92
86
  return identityContext;
93
87
  }
88
+ function mapFeatureStateVariants(fs) {
89
+ return fs.multivariateFeatureStateValues?.length > 0
90
+ ? fs.multivariateFeatureStateValues.map(mv => ({
91
+ value: mv.multivariateFeatureOption.value,
92
+ weight: mv.percentageAllocation,
93
+ priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid)
94
+ }))
95
+ : undefined;
96
+ }
94
97
  function mapSegmentRuleModelToRule(rule) {
95
98
  return {
96
99
  type: rule.type,
@@ -114,6 +117,7 @@ function mapIdentityOverridesToSegments(identityOverrides) {
114
117
  name: fs.feature.name,
115
118
  enabled: fs.enabled,
116
119
  value: fs.getValue(),
120
+ variants: mapFeatureStateVariants(fs),
117
121
  priority: -Infinity,
118
122
  metadata: {
119
123
  id: fs.feature.id
@@ -3,7 +3,7 @@ import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js';
3
3
  import { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
4
4
  import { Flags } from './models.js';
5
5
  import { EnvironmentDataPollingManager } from './polling_manager.js';
6
- import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js';
6
+ import { Deferred, generateIdentitiesData, getUserAgent, isTraitConfig, retryFetch } from './utils.js';
7
7
  import { SegmentModel, IdentityModel, TraitModel, getEvaluationResult } from '../flagsmith-engine/index.js';
8
8
  import { pino } from 'pino';
9
9
  import { getEvaluationContext } from '../flagsmith-engine/evaluation/evaluationContext/mappers.js';
@@ -216,7 +216,7 @@ export class Flagsmith {
216
216
  const environment = await this.getEnvironment();
217
217
  const identityModel = this.getIdentityModel(environment, identifier, Object.keys(traits || {}).map(key => ({
218
218
  key,
219
- value: traits?.[key]
219
+ value: isTraitConfig(traits?.[key]) ? traits[key].value : traits?.[key]
220
220
  })));
221
221
  const context = getEvaluationContext(environment, identityModel);
222
222
  if (!context) {
@@ -369,7 +369,7 @@ export class Flagsmith {
369
369
  const environment = await this.getEnvironment();
370
370
  const identityModel = this.getIdentityModel(environment, identifier, Object.keys(traits).map(key => ({
371
371
  key,
372
- value: traits[key]
372
+ value: isTraitConfig(traits[key]) ? traits[key].value : traits[key]
373
373
  })));
374
374
  const context = getEvaluationContext(environment, identityModel);
375
375
  if (!context) {
@@ -12,6 +12,7 @@ import {
12
12
  import { EnvironmentModel } from '../../environments/models.js';
13
13
  import { IdentityModel } from '../../identities/models.js';
14
14
  import { TraitModel } from '../../identities/traits/models.js';
15
+ import { FeatureStateModel } from '../../features/models.js';
15
16
  import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../segments/constants.js';
16
17
  import { createHash } from 'node:crypto';
17
18
  import { uuidToBigInt } from '../../features/util.js';
@@ -48,21 +49,12 @@ function mapEnvironmentModelToEvaluationContext(
48
49
 
49
50
  const features: FeaturesWithMetadata<SDKFeatureMetadata> = {};
50
51
  for (const fs of environment.featureStates) {
51
- const variants =
52
- fs.multivariateFeatureStateValues?.length > 0
53
- ? fs.multivariateFeatureStateValues.map(mv => ({
54
- value: mv.multivariateFeatureOption.value,
55
- weight: mv.percentageAllocation,
56
- priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid)
57
- }))
58
- : undefined;
59
-
60
52
  features[fs.feature.name] = {
61
53
  key: fs.djangoID?.toString() || fs.featurestateUUID,
62
54
  name: fs.feature.name,
63
55
  enabled: fs.enabled,
64
56
  value: fs.getValue(),
65
- variants,
57
+ variants: mapFeatureStateVariants(fs),
66
58
  priority: fs.featureSegment?.priority,
67
59
  metadata: {
68
60
  id: fs.feature.id
@@ -83,6 +75,7 @@ function mapEnvironmentModelToEvaluationContext(
83
75
  name: fs.feature.name,
84
76
  enabled: fs.enabled,
85
77
  value: fs.getValue(),
78
+ variants: mapFeatureStateVariants(fs),
86
79
  priority: fs.featureSegment?.priority,
87
80
  metadata: {
88
81
  id: fs.feature.id
@@ -130,6 +123,16 @@ function mapIdentityModelToIdentityContext(
130
123
  return identityContext;
131
124
  }
132
125
 
126
+ function mapFeatureStateVariants(fs: FeatureStateModel) {
127
+ return fs.multivariateFeatureStateValues?.length > 0
128
+ ? fs.multivariateFeatureStateValues.map(mv => ({
129
+ value: mv.multivariateFeatureOption.value,
130
+ weight: mv.percentageAllocation,
131
+ priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid)
132
+ }))
133
+ : undefined;
134
+ }
135
+
133
136
  function mapSegmentRuleModelToRule(rule: any): any {
134
137
  return {
135
138
  type: rule.type,
@@ -160,6 +163,7 @@ function mapIdentityOverridesToSegments(
160
163
  name: fs.feature.name,
161
164
  enabled: fs.enabled,
162
165
  value: fs.getValue(),
166
+ variants: mapFeatureStateVariants(fs),
163
167
  priority: -Infinity,
164
168
  metadata: {
165
169
  id: fs.feature.id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagsmith-nodejs",
3
- "version": "8.0.0",
3
+ "version": "8.0.2",
4
4
  "description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
5
5
  "main": "./build/cjs/index.js",
6
6
  "type": "module",
package/sdk/index.ts CHANGED
@@ -8,7 +8,13 @@ import { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
8
8
 
9
9
  import { DefaultFlag, Flags } from './models.js';
10
10
  import { EnvironmentDataPollingManager } from './polling_manager.js';
11
- import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js';
11
+ import {
12
+ Deferred,
13
+ generateIdentitiesData,
14
+ getUserAgent,
15
+ isTraitConfig,
16
+ retryFetch
17
+ } from './utils.js';
12
18
  import {
13
19
  SegmentModel,
14
20
  EnvironmentModel,
@@ -275,7 +281,7 @@ export class Flagsmith {
275
281
  identifier,
276
282
  Object.keys(traits || {}).map(key => ({
277
283
  key,
278
- value: traits?.[key]
284
+ value: isTraitConfig(traits?.[key]) ? traits![key].value : traits?.[key]
279
285
  }))
280
286
  );
281
287
 
@@ -474,7 +480,7 @@ export class Flagsmith {
474
480
  identifier,
475
481
  Object.keys(traits).map(key => ({
476
482
  key,
477
- value: traits[key]
483
+ value: isTraitConfig(traits[key]) ? traits[key].value : traits[key]
478
484
  }))
479
485
  );
480
486
 
@@ -7,7 +7,13 @@ import {
7
7
  shouldApplyOverride
8
8
  } from '../../../flagsmith-engine/index.js';
9
9
  import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js';
10
- import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js';
10
+ import {
11
+ FeatureModel,
12
+ FeatureSegment,
13
+ FeatureStateModel,
14
+ MultivariateFeatureOptionModel,
15
+ MultivariateFeatureStateValueModel
16
+ } from '../../../flagsmith-engine/features/models.js';
11
17
  import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js';
12
18
  import {
13
19
  environment,
@@ -15,12 +21,15 @@ import {
15
21
  feature1,
16
22
  identity,
17
23
  identityInSegment,
24
+ segment,
18
25
  segmentConditionProperty,
19
- segmentConditionStringValue
26
+ segmentConditionStringValue,
27
+ traitMatchingSegment
20
28
  } from './utils.js';
21
29
  import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js';
22
30
  import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js';
23
31
  import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js';
32
+ import { EvaluationContextWithMetadata } from '../../../flagsmith-engine/evaluation/models.js';
24
33
  import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js';
25
34
 
26
35
  test('test_get_evaluation_result_without_any_override', () => {
@@ -356,3 +365,84 @@ test('evaluateFeatures with multivariate evaluation', () => {
356
365
  const flags = evaluateFeatures(context as any, {});
357
366
  expect(flags['Multivariate Feature'].value).toBe('variant_b');
358
367
  });
368
+
369
+ test('local evaluation returns correct multivariate value for segment override with 100% weight', () => {
370
+ // Given
371
+ // a feature with two multivariate variants where the segment override
372
+ // assigns 100% weight to the second variant
373
+ const env = environment();
374
+ const seg = segment();
375
+
376
+ const mvFeature = new FeatureModel(10, 'mv_feature', CONSTANTS.STANDARD);
377
+
378
+ const controlOption = new MultivariateFeatureOptionModel('control', 1);
379
+ const variantOption = new MultivariateFeatureOptionModel('variant_b', 2);
380
+
381
+ const envFs = new FeatureStateModel(mvFeature, true, 10, 'default');
382
+ envFs.multivariateFeatureStateValues = [
383
+ new MultivariateFeatureStateValueModel(controlOption, 0, 1),
384
+ new MultivariateFeatureStateValueModel(variantOption, 100, 2)
385
+ ];
386
+ env.featureStates.push(envFs);
387
+
388
+ const overrideFs = new FeatureStateModel(mvFeature, true, 11, 'default');
389
+ overrideFs.featureSegment = new FeatureSegment(0);
390
+ overrideFs.multivariateFeatureStateValues = [
391
+ new MultivariateFeatureStateValueModel(controlOption, 0, 1),
392
+ new MultivariateFeatureStateValueModel(variantOption, 100, 2)
393
+ ];
394
+ seg.featureStates.push(overrideFs);
395
+ env.project.segments = [seg];
396
+
397
+ // When
398
+ // evaluating flags for an identity that matches the segment
399
+ const context = getEvaluationContext(env, identityInSegment(), [traitMatchingSegment()]);
400
+ const result = getEvaluationResult(context as EvaluationContextWithMetadata);
401
+ const flag = result.flags['mv_feature'];
402
+
403
+ // Then
404
+ // the flag value should be the 100%-weighted variant, not the base default
405
+ expect(flag).toBeDefined();
406
+ expect(flag.value).toBe('variant_b');
407
+ });
408
+
409
+ test('getEvaluationContext maps multivariate variants onto segment override feature states', () => {
410
+ // Given
411
+ // a segment override feature state with multivariate values
412
+ const env = environment();
413
+ const seg = segment();
414
+
415
+ const mvFeature = new FeatureModel(10, 'mv_feature', CONSTANTS.STANDARD);
416
+ env.featureStates.push(new FeatureStateModel(mvFeature, true, 10, 'default'));
417
+
418
+ const overrideFs = new FeatureStateModel(mvFeature, true, 11, 'default');
419
+ overrideFs.featureSegment = new FeatureSegment(0);
420
+ overrideFs.multivariateFeatureStateValues = [
421
+ new MultivariateFeatureStateValueModel(
422
+ new MultivariateFeatureOptionModel('variant_value', 1),
423
+ 100,
424
+ 1
425
+ )
426
+ ];
427
+ seg.featureStates.push(overrideFs);
428
+ env.project.segments = [seg];
429
+
430
+ // When
431
+ // mapping the environment model to an evaluation context
432
+ const context = getEvaluationContext(env, identityInSegment(), [traitMatchingSegment()]);
433
+
434
+ // Then
435
+ // the segment override should include the variants array
436
+ const segmentOverrides = Object.values(context.segments || {});
437
+ const segWithOverrides = segmentOverrides.find(
438
+ s => s.overrides && s.overrides.some((o: any) => o.name === 'mv_feature')
439
+ );
440
+ expect(segWithOverrides).toBeDefined();
441
+
442
+ const mvOverride = segWithOverrides!.overrides!.find((o: any) => o.name === 'mv_feature');
443
+ expect(mvOverride).toBeDefined();
444
+ expect((mvOverride as any).variants).toBeDefined();
445
+ expect((mvOverride as any).variants).toHaveLength(1);
446
+ expect((mvOverride as any).variants[0].value).toBe('variant_value');
447
+ expect((mvOverride as any).variants[0].weight).toBe(100);
448
+ });
@@ -209,6 +209,76 @@ test('test_identity_with_transient_traits', async () => {
209
209
  expect(identityFlags[0].featureName).toBe('some_feature');
210
210
  });
211
211
 
212
+ test('getIdentityFlags local evaluation with plain traits matches segment', async () => {
213
+ const identifier = 'identifier';
214
+ // Plain trait format: age=30 should match segment rule "age LESS_THAN 40"
215
+ const traits = { age: 30 };
216
+
217
+ const flg = flagsmith({
218
+ environmentKey: 'ser.key',
219
+ enableLocalEvaluation: true
220
+ });
221
+
222
+ const flags = await flg.getIdentityFlags(identifier, traits);
223
+
224
+ // Should get segment override value, not the default
225
+ expect(flags.getFeatureValue('some_feature')).toBe('segment_override');
226
+ expect(flags.isFeatureEnabled('some_feature')).toBe(false);
227
+ });
228
+
229
+ test('getIdentityFlags local evaluation with TraitConfig format matches segment', async () => {
230
+ const identifier = 'identifier';
231
+ // TraitConfig format: same trait value wrapped with transient metadata
232
+ const traits = { age: { value: 30, transient: true } };
233
+
234
+ const flg = flagsmith({
235
+ environmentKey: 'ser.key',
236
+ enableLocalEvaluation: true
237
+ });
238
+
239
+ const flags = await flg.getIdentityFlags(identifier, traits);
240
+
241
+ // Should get segment override value — same result as plain trait format
242
+ expect(flags.getFeatureValue('some_feature')).toBe('segment_override');
243
+ expect(flags.isFeatureEnabled('some_feature')).toBe(false);
244
+ });
245
+
246
+ test('getIdentityFlags local evaluation with mixed trait formats matches segment', async () => {
247
+ const identifier = 'identifier';
248
+ // Mix of plain and TraitConfig formats
249
+ const traits = {
250
+ age: { value: 30, transient: true },
251
+ some_other_trait: 'plain_value'
252
+ };
253
+
254
+ const flg = flagsmith({
255
+ environmentKey: 'ser.key',
256
+ enableLocalEvaluation: true
257
+ });
258
+
259
+ const flags = await flg.getIdentityFlags(identifier, traits);
260
+
261
+ // Should get segment override value
262
+ expect(flags.getFeatureValue('some_feature')).toBe('segment_override');
263
+ expect(flags.isFeatureEnabled('some_feature')).toBe(false);
264
+ });
265
+
266
+ test('getIdentitySegments with TraitConfig format matches segment', async () => {
267
+ const identifier = 'identifier';
268
+ // TraitConfig format should work for getIdentitySegments too
269
+ const traits = { age: { value: 30, transient: true } };
270
+
271
+ const flg = flagsmith({
272
+ environmentKey: 'ser.key',
273
+ enableLocalEvaluation: true
274
+ });
275
+
276
+ const segments = await flg.getIdentitySegments(identifier, traits);
277
+
278
+ expect(segments).toHaveLength(1);
279
+ expect(segments[0].name).toBe('regular_segment');
280
+ });
281
+
212
282
  test('getIdentityFlags fails if API call failed and no default flag handler was provided', async () => {
213
283
  const flg = flagsmith({
214
284
  fetch: badFetch