flagsmith-nodejs 7.0.3 → 8.0.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/.github/workflows/pull_request.yaml +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +28 -0
- package/build/cjs/flagsmith-engine/segments/evaluators.js +14 -8
- package/build/cjs/sdk/index.js +2 -2
- package/build/esm/flagsmith-engine/segments/evaluators.js +14 -8
- package/build/esm/sdk/index.js +3 -3
- package/flagsmith-engine/segments/evaluators.ts +16 -10
- package/package.json +4 -4
- package/sdk/index.ts +9 -3
- package/tests/engine/unit/segments/segment_evaluators.test.ts +71 -1
- package/tests/sdk/flagsmith-identity-flags.test.ts +70 -0
- package/.github/workflows/conventional-commit.yml +0 -29
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"
|
|
1
|
+
{".":"8.0.1"}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [8.0.1](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v8.0.0...v8.0.1) (2026-03-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 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))
|
|
9
|
+
|
|
10
|
+
## [8.0.0](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v7.0.3...v8.0.0) (2026-02-25)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⚠ BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* remove node18 support and update pino ([#220](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/220))
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **CVE-2026-1615:** Replace jsonpath with jsonpath-plus to fix security vulnerability ([#247](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/247)) ([56a285c](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/56a285c610a51f1fe3f7d2cd561566d3aa1c6018))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Dependency Updates
|
|
23
|
+
|
|
24
|
+
* remove node18 support and update pino ([#220](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/220)) ([a246c06](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/a246c066b062d5897abead34c0f1b8ee1d687d20))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Other
|
|
28
|
+
|
|
29
|
+
* Remove amannn/action-semantic-pull-request workflow ([#243](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/243)) ([980728a](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/980728a380518e123e7ce8f6ede98842c915fcae))
|
|
30
|
+
|
|
3
31
|
## [7.0.3](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v7.0.2...v7.0.3) (2026-01-21)
|
|
4
32
|
|
|
5
33
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getContextValue = exports.traitsMatchSegmentCondition = exports.getIdentitySegments = void 0;
|
|
4
|
-
const
|
|
4
|
+
const jsonpath_plus_1 = require("jsonpath-plus");
|
|
5
5
|
const index_js_1 = require("../utils/hashing/index.js");
|
|
6
6
|
const models_js_1 = require("./models.js");
|
|
7
7
|
const constants_js_1 = require("./constants.js");
|
|
8
|
-
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
9
|
-
const jsonpath = jsonpathModule.default || jsonpathModule;
|
|
10
8
|
/**
|
|
11
9
|
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
12
10
|
*
|
|
@@ -100,8 +98,20 @@ function evaluateRuleConditions(ruleType, conditionResults) {
|
|
|
100
98
|
return false;
|
|
101
99
|
}
|
|
102
100
|
}
|
|
101
|
+
const TRAITS_DOT_PATTERN = /^\$\.identity\.traits\.(.+)$/;
|
|
102
|
+
const TRAITS_BRACKET_PATTERN = /^\$\.identity\.traits\['(.+)'\]$/;
|
|
103
|
+
function extractTraitNameFromPath(property) {
|
|
104
|
+
return TRAITS_DOT_PATTERN.exec(property)?.[1] ?? TRAITS_BRACKET_PATTERN.exec(property)?.[1];
|
|
105
|
+
}
|
|
103
106
|
function getTraitValue(property, context) {
|
|
104
107
|
if (property.startsWith('$.')) {
|
|
108
|
+
// Look up $.identity.traits.X and $.identity.traits['X'] paths directly
|
|
109
|
+
// to avoid jsonpath-plus mis-parsing special characters (e.g. $, [, ]) in
|
|
110
|
+
// trait names that appear inside bracket-notation strings.
|
|
111
|
+
const traitName = extractTraitNameFromPath(property);
|
|
112
|
+
if (traitName !== undefined) {
|
|
113
|
+
return context?.identity?.traits?.[traitName];
|
|
114
|
+
}
|
|
105
115
|
const contextValue = getContextValue(property, context);
|
|
106
116
|
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
107
117
|
return contextValue;
|
|
@@ -135,8 +145,7 @@ function getContextValue(jsonPath, context) {
|
|
|
135
145
|
if (!context || !jsonPath?.startsWith('$.'))
|
|
136
146
|
return undefined;
|
|
137
147
|
try {
|
|
138
|
-
const
|
|
139
|
-
const results = jsonpath.query(context, normalizedPath);
|
|
148
|
+
const results = (0, jsonpath_plus_1.JSONPath)({ path: jsonPath, json: context });
|
|
140
149
|
return results.length > 0 ? results[0] : undefined;
|
|
141
150
|
}
|
|
142
151
|
catch (error) {
|
|
@@ -144,6 +153,3 @@ function getContextValue(jsonPath, context) {
|
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
exports.getContextValue = getContextValue;
|
|
147
|
-
function normalizeJsonPath(jsonPath) {
|
|
148
|
-
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
149
|
-
}
|
package/build/cjs/sdk/index.js
CHANGED
|
@@ -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) {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
2
2
|
import { getHashedPercentageForObjIds } from '../utils/hashing/index.js';
|
|
3
3
|
import { SegmentConditionModel } from './models.js';
|
|
4
4
|
import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js';
|
|
5
|
-
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
6
|
-
const jsonpath = jsonpathModule.default || jsonpathModule;
|
|
7
5
|
/**
|
|
8
6
|
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
9
7
|
*
|
|
@@ -95,8 +93,20 @@ function evaluateRuleConditions(ruleType, conditionResults) {
|
|
|
95
93
|
return false;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
96
|
+
const TRAITS_DOT_PATTERN = /^\$\.identity\.traits\.(.+)$/;
|
|
97
|
+
const TRAITS_BRACKET_PATTERN = /^\$\.identity\.traits\['(.+)'\]$/;
|
|
98
|
+
function extractTraitNameFromPath(property) {
|
|
99
|
+
return TRAITS_DOT_PATTERN.exec(property)?.[1] ?? TRAITS_BRACKET_PATTERN.exec(property)?.[1];
|
|
100
|
+
}
|
|
98
101
|
function getTraitValue(property, context) {
|
|
99
102
|
if (property.startsWith('$.')) {
|
|
103
|
+
// Look up $.identity.traits.X and $.identity.traits['X'] paths directly
|
|
104
|
+
// to avoid jsonpath-plus mis-parsing special characters (e.g. $, [, ]) in
|
|
105
|
+
// trait names that appear inside bracket-notation strings.
|
|
106
|
+
const traitName = extractTraitNameFromPath(property);
|
|
107
|
+
if (traitName !== undefined) {
|
|
108
|
+
return context?.identity?.traits?.[traitName];
|
|
109
|
+
}
|
|
100
110
|
const contextValue = getContextValue(property, context);
|
|
101
111
|
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
102
112
|
return contextValue;
|
|
@@ -130,14 +140,10 @@ export function getContextValue(jsonPath, context) {
|
|
|
130
140
|
if (!context || !jsonPath?.startsWith('$.'))
|
|
131
141
|
return undefined;
|
|
132
142
|
try {
|
|
133
|
-
const
|
|
134
|
-
const results = jsonpath.query(context, normalizedPath);
|
|
143
|
+
const results = JSONPath({ path: jsonPath, json: context });
|
|
135
144
|
return results.length > 0 ? results[0] : undefined;
|
|
136
145
|
}
|
|
137
146
|
catch (error) {
|
|
138
147
|
return undefined;
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
|
-
function normalizeJsonPath(jsonPath) {
|
|
142
|
-
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
143
|
-
}
|
package/build/esm/sdk/index.js
CHANGED
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
2
2
|
import {
|
|
3
3
|
GenericEvaluationContext,
|
|
4
4
|
InSegmentCondition,
|
|
@@ -10,9 +10,6 @@ import { getHashedPercentageForObjIds } from '../utils/hashing/index.js';
|
|
|
10
10
|
import { SegmentConditionModel } from './models.js';
|
|
11
11
|
import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js';
|
|
12
12
|
|
|
13
|
-
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
14
|
-
const jsonpath = (jsonpathModule as any).default || jsonpathModule;
|
|
15
|
-
|
|
16
13
|
/**
|
|
17
14
|
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
18
15
|
*
|
|
@@ -140,8 +137,22 @@ function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]):
|
|
|
140
137
|
}
|
|
141
138
|
}
|
|
142
139
|
|
|
140
|
+
const TRAITS_DOT_PATTERN = /^\$\.identity\.traits\.(.+)$/;
|
|
141
|
+
const TRAITS_BRACKET_PATTERN = /^\$\.identity\.traits\['(.+)'\]$/;
|
|
142
|
+
|
|
143
|
+
function extractTraitNameFromPath(property: string): string | undefined {
|
|
144
|
+
return TRAITS_DOT_PATTERN.exec(property)?.[1] ?? TRAITS_BRACKET_PATTERN.exec(property)?.[1];
|
|
145
|
+
}
|
|
146
|
+
|
|
143
147
|
function getTraitValue(property: string, context?: GenericEvaluationContext): any {
|
|
144
148
|
if (property.startsWith('$.')) {
|
|
149
|
+
// Look up $.identity.traits.X and $.identity.traits['X'] paths directly
|
|
150
|
+
// to avoid jsonpath-plus mis-parsing special characters (e.g. $, [, ]) in
|
|
151
|
+
// trait names that appear inside bracket-notation strings.
|
|
152
|
+
const traitName = extractTraitNameFromPath(property);
|
|
153
|
+
if (traitName !== undefined) {
|
|
154
|
+
return context?.identity?.traits?.[traitName];
|
|
155
|
+
}
|
|
145
156
|
const contextValue = getContextValue(property, context);
|
|
146
157
|
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
147
158
|
return contextValue;
|
|
@@ -179,14 +190,9 @@ export function getContextValue(jsonPath: string, context?: GenericEvaluationCon
|
|
|
179
190
|
if (!context || !jsonPath?.startsWith('$.')) return undefined;
|
|
180
191
|
|
|
181
192
|
try {
|
|
182
|
-
const
|
|
183
|
-
const results = jsonpath.query(context, normalizedPath);
|
|
193
|
+
const results = JSONPath({ path: jsonPath, json: context });
|
|
184
194
|
return results.length > 0 ? results[0] : undefined;
|
|
185
195
|
} catch (error) {
|
|
186
196
|
return undefined;
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
|
-
|
|
190
|
-
function normalizeJsonPath(jsonPath: string): string {
|
|
191
|
-
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
192
|
-
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
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",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=20"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
"import": "./build/esm/index.js",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"jsonpath": "^
|
|
68
|
-
"pino": "^
|
|
67
|
+
"jsonpath-plus": "^10.4.0",
|
|
68
|
+
"pino": "^10",
|
|
69
69
|
"semver": "^7.3.7",
|
|
70
70
|
"undici-types": "^6.19.8"
|
|
71
71
|
},
|
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 {
|
|
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
|
|
|
@@ -345,6 +345,75 @@ describe('getIdentitySegments single segment evaluation', () => {
|
|
|
345
345
|
});
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
+
describe('traitsMatchSegmentCondition with $.identity.traits.* properties', () => {
|
|
349
|
+
const mockContext: EvaluationContext = {
|
|
350
|
+
environment: { key: 'env', name: 'test' },
|
|
351
|
+
identity: {
|
|
352
|
+
key: 'user',
|
|
353
|
+
identifier: 'user@example.com',
|
|
354
|
+
traits: {
|
|
355
|
+
age: 25,
|
|
356
|
+
tamaño: 'grande',
|
|
357
|
+
サイズ: 'medium',
|
|
358
|
+
'[$the.size$]': 'small',
|
|
359
|
+
'my.foo.bar': 'dotted'
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
segments: {},
|
|
363
|
+
features: {}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
test.each([
|
|
367
|
+
// dot notation – normal trait name
|
|
368
|
+
[{ property: '$.identity.traits.age', operator: 'EQUAL', value: '25' }, true],
|
|
369
|
+
[{ property: '$.identity.traits.age', operator: 'EQUAL', value: '30' }, false],
|
|
370
|
+
// dot notation – unicode trait name
|
|
371
|
+
[{ property: '$.identity.traits.tamaño', operator: 'EQUAL', value: 'grande' }, true],
|
|
372
|
+
[{ property: '$.identity.traits.サイズ', operator: 'EQUAL', value: 'medium' }, true],
|
|
373
|
+
// dot notation – trait name that itself contains dots (everything after $.identity.traits. is the key)
|
|
374
|
+
[{ property: '$.identity.traits.my.foo.bar', operator: 'EQUAL', value: 'dotted' }, true],
|
|
375
|
+
[{ property: '$.identity.traits.my.foo.bar', operator: 'EQUAL', value: 'other' }, false],
|
|
376
|
+
// bracket notation – special characters in trait name that break jsonpath-plus
|
|
377
|
+
[
|
|
378
|
+
{ property: "$.identity.traits['[$the.size$]']", operator: 'EQUAL', value: 'small' },
|
|
379
|
+
true
|
|
380
|
+
],
|
|
381
|
+
[
|
|
382
|
+
{ property: "$.identity.traits['[$the.size$]']", operator: 'EQUAL', value: 'large' },
|
|
383
|
+
false
|
|
384
|
+
],
|
|
385
|
+
// non-existent trait
|
|
386
|
+
[{ property: '$.identity.traits.nonexistent', operator: 'EQUAL', value: 'any' }, false],
|
|
387
|
+
// IS_SET / IS_NOT_SET
|
|
388
|
+
[{ property: '$.identity.traits.age', operator: 'IS_SET', value: null }, true],
|
|
389
|
+
[{ property: '$.identity.traits.nonexistent', operator: 'IS_SET', value: null }, false],
|
|
390
|
+
[{ property: '$.identity.traits.nonexistent', operator: 'IS_NOT_SET', value: null }, true],
|
|
391
|
+
[{ property: '$.identity.traits.age', operator: 'IS_NOT_SET', value: null }, false],
|
|
392
|
+
// IN operator
|
|
393
|
+
[
|
|
394
|
+
{
|
|
395
|
+
property: '$.identity.traits.tamaño',
|
|
396
|
+
operator: CONDITION_OPERATORS.IN,
|
|
397
|
+
value: ['grande', 'pequeño']
|
|
398
|
+
},
|
|
399
|
+
true
|
|
400
|
+
],
|
|
401
|
+
[
|
|
402
|
+
{
|
|
403
|
+
property: '$.identity.traits.tamaño',
|
|
404
|
+
operator: CONDITION_OPERATORS.IN,
|
|
405
|
+
value: ['pequeño']
|
|
406
|
+
},
|
|
407
|
+
false
|
|
408
|
+
]
|
|
409
|
+
] as Array<[SegmentCondition | InSegmentCondition, boolean]>)(
|
|
410
|
+
'evaluates %j to %s',
|
|
411
|
+
(condition, expected) => {
|
|
412
|
+
expect(traitsMatchSegmentCondition(condition, 'seg', mockContext)).toBe(expected);
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
348
417
|
describe('getContextValue', () => {
|
|
349
418
|
const mockContext: EvaluationContext = {
|
|
350
419
|
environment: {
|
|
@@ -354,6 +423,7 @@ describe('getContextValue', () => {
|
|
|
354
423
|
identity: {
|
|
355
424
|
key: 'user-123',
|
|
356
425
|
identifier: 'user@example.com'
|
|
426
|
+
// intentionally no traits – tests below confirm paths that require traits return undefined
|
|
357
427
|
},
|
|
358
428
|
segments: {},
|
|
359
429
|
features: {}
|
|
@@ -371,7 +441,7 @@ describe('getContextValue', () => {
|
|
|
371
441
|
|
|
372
442
|
// Undefined or invalid cases
|
|
373
443
|
test.each([
|
|
374
|
-
['$.identity.traits.user_type', '
|
|
444
|
+
['$.identity.traits.user_type', 'no traits in context'],
|
|
375
445
|
['identity.identifier', 'missing $ prefix'],
|
|
376
446
|
['$.invalid.path', 'completely invalid path'],
|
|
377
447
|
['$.identity.nonexistent', 'valid structure but missing property'],
|
|
@@ -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
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Conventional Commit
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types:
|
|
6
|
-
- edited
|
|
7
|
-
- opened
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
conventional-commit:
|
|
11
|
-
name: Conventional Commit
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- name: Check PR Conventional Commit title
|
|
15
|
-
uses: amannn/action-semantic-pull-request@v5
|
|
16
|
-
env:
|
|
17
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
18
|
-
with:
|
|
19
|
-
types: | # mirrors changelog-sections in the /release-please-config.json
|
|
20
|
-
feat
|
|
21
|
-
fix
|
|
22
|
-
infra
|
|
23
|
-
ci
|
|
24
|
-
docs
|
|
25
|
-
deps
|
|
26
|
-
perf
|
|
27
|
-
refactor
|
|
28
|
-
test
|
|
29
|
-
chore
|