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.
- package/.github/workflows/conventional-commit.yml +29 -0
- package/.github/workflows/publish.yml +20 -17
- package/.github/workflows/pull_request.yaml +36 -33
- package/.github/workflows/release-please.yml +18 -0
- package/.gitmodules +1 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc.cjs +9 -1
- package/.release-please-manifest.json +1 -0
- package/CHANGELOG.md +592 -0
- package/CODEOWNERS +1 -0
- package/README.md +0 -2
- package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
- package/build/cjs/flagsmith-engine/environments/models.js +3 -1
- package/build/cjs/flagsmith-engine/environments/util.js +1 -1
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
- package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
- package/build/cjs/flagsmith-engine/features/models.js +1 -1
- package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
- package/build/cjs/flagsmith-engine/features/types.js +9 -0
- package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
- package/build/cjs/flagsmith-engine/features/util.js +5 -1
- package/build/cjs/flagsmith-engine/index.d.ts +61 -9
- package/build/cjs/flagsmith-engine/index.js +176 -56
- package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
- package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
- package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
- package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
- package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
- package/build/cjs/flagsmith-engine/segments/models.js +115 -13
- package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
- package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
- package/build/cjs/sdk/analytics.js +3 -1
- package/build/cjs/sdk/index.d.ts +1 -3
- package/build/cjs/sdk/index.js +63 -24
- package/build/cjs/sdk/models.d.ts +8 -1
- package/build/cjs/sdk/models.js +29 -1
- package/build/cjs/sdk/utils.d.ts +1 -0
- package/build/cjs/sdk/utils.js +14 -1
- package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
- package/build/esm/flagsmith-engine/environments/models.js +3 -1
- package/build/esm/flagsmith-engine/environments/util.js +1 -1
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
- package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
- package/build/esm/flagsmith-engine/features/models.js +2 -2
- package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
- package/build/esm/flagsmith-engine/features/types.js +6 -0
- package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
- package/build/esm/flagsmith-engine/features/util.js +3 -0
- package/build/esm/flagsmith-engine/index.d.ts +61 -9
- package/build/esm/flagsmith-engine/index.js +161 -43
- package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
- package/build/esm/flagsmith-engine/segments/constants.js +1 -0
- package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
- package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
- package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
- package/build/esm/flagsmith-engine/segments/models.js +115 -13
- package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
- package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
- package/build/esm/sdk/analytics.js +3 -1
- package/build/esm/sdk/index.d.ts +1 -3
- package/build/esm/sdk/index.js +63 -24
- package/build/esm/sdk/models.d.ts +8 -1
- package/build/esm/sdk/models.js +29 -1
- package/build/esm/sdk/utils.d.ts +1 -0
- package/build/esm/sdk/utils.js +12 -0
- package/flagsmith-engine/environments/models.ts +3 -1
- package/flagsmith-engine/environments/util.ts +2 -1
- package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
- package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
- package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
- package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
- package/flagsmith-engine/evaluation/models.ts +96 -0
- package/flagsmith-engine/features/models.ts +3 -2
- package/flagsmith-engine/features/types.ts +5 -0
- package/flagsmith-engine/features/util.ts +4 -0
- package/flagsmith-engine/index.ts +229 -72
- package/flagsmith-engine/segments/constants.ts +1 -0
- package/flagsmith-engine/segments/evaluators.ts +178 -62
- package/flagsmith-engine/segments/models.ts +171 -23
- package/flagsmith-engine/utils/hashing/index.ts +2 -2
- package/package.json +13 -2
- package/release-please-config.json +62 -0
- package/sdk/analytics.ts +3 -1
- package/sdk/index.ts +89 -30
- package/sdk/models.ts +44 -2
- package/sdk/utils.ts +13 -0
- package/tests/engine/e2e/engine.test.ts +43 -38
- package/tests/engine/unit/engine.test.ts +306 -60
- package/tests/engine/unit/mappers.test.ts +353 -0
- package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
- package/tests/engine/unit/segments/segments_model.test.ts +85 -0
- package/tests/engine/unit/utils/utils.test.ts +7 -7
- package/tests/engine/unit/utils.ts +1 -1
- package/tests/sdk/analytics.test.ts +6 -1
- package/tests/sdk/data/environment.json +1 -0
- package/tests/sdk/flagsmith-environment-flags.test.ts +28 -0
- package/tests/sdk/flagsmith-identity-flags.test.ts +11 -2
- package/tests/sdk/flagsmith.test.ts +190 -3
- package/tests/sdk/offline-handlers.test.ts +3 -1
- package/vitest.config.esm.ts +34 -0
|
@@ -1,76 +1,192 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import * as jsonpathModule from 'jsonpath';
|
|
2
|
+
import {
|
|
3
|
+
GenericEvaluationContext,
|
|
4
|
+
InSegmentCondition,
|
|
5
|
+
SegmentCondition,
|
|
6
|
+
SegmentContext,
|
|
7
|
+
SegmentRule
|
|
8
|
+
} from '../evaluation/models.js';
|
|
9
|
+
import { getHashedPercentageForObjIds } from '../utils/hashing/index.js';
|
|
10
|
+
import { SegmentConditionModel } from './models.js';
|
|
11
|
+
import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js';
|
|
12
|
+
|
|
13
|
+
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
14
|
+
const jsonpath = (jsonpathModule as any).default || jsonpathModule;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
18
|
+
*
|
|
19
|
+
* An identity belongs to a segment if it matches ALL of the segment's rules.
|
|
20
|
+
* If the context has no identity or segments, returns an empty array.
|
|
21
|
+
*
|
|
22
|
+
* @param context - Evaluation context containing identity and segment definitions
|
|
23
|
+
* @returns Array of segments that the identity matches
|
|
24
|
+
*/
|
|
25
|
+
export function getIdentitySegments(context: GenericEvaluationContext): SegmentContext[] {
|
|
26
|
+
if (!context.identity || !context.segments) return [];
|
|
27
|
+
|
|
28
|
+
return Object.values(context.segments).filter(segment => {
|
|
29
|
+
if (segment.rules.length === 0) return false;
|
|
30
|
+
return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context));
|
|
31
|
+
});
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Evaluates whether a segment condition matches the identity's traits or context values.
|
|
36
|
+
*
|
|
37
|
+
* Handles different types of conditions:
|
|
38
|
+
* - PERCENTAGE_SPLIT: Deterministic percentage-based bucketing using identity key
|
|
39
|
+
* - IS_SET/IS_NOT_SET: Checks for trait existence
|
|
40
|
+
* - Standard operators: EQUAL, NOT_EQUAL, etc. via SegmentConditionModel
|
|
41
|
+
* - JSONPath expressions: $.identity.identifier, $.environment.name, etc.
|
|
42
|
+
*
|
|
43
|
+
* @param condition - The condition to evaluate (property, operator, value)
|
|
44
|
+
* @param segmentKey - Key of the segment (used for percentage split hashing)
|
|
45
|
+
* @param context - Evaluation context containing identity, traits, and environment
|
|
46
|
+
* @returns true if the condition matches
|
|
47
|
+
*/
|
|
48
|
+
export function traitsMatchSegmentCondition(
|
|
49
|
+
condition: SegmentCondition | InSegmentCondition,
|
|
50
|
+
segmentKey: string,
|
|
51
|
+
context?: GenericEvaluationContext
|
|
22
52
|
): boolean {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
53
|
+
if (condition.operator === PERCENTAGE_SPLIT) {
|
|
54
|
+
let splitKey: string | undefined;
|
|
55
|
+
|
|
56
|
+
if (!condition.property) {
|
|
57
|
+
splitKey = context?.identity?.key;
|
|
58
|
+
} else {
|
|
59
|
+
splitKey = getContextValue(condition.property, context);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!splitKey) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const hashedPercentage = getHashedPercentageForObjIds([segmentKey, splitKey]);
|
|
66
|
+
return hashedPercentage <= parseFloat(String(condition.value));
|
|
67
|
+
}
|
|
68
|
+
if (!condition.property) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const traitValue = getTraitValue(condition.property, context);
|
|
73
|
+
|
|
74
|
+
if (condition.operator === IS_SET) {
|
|
75
|
+
return traitValue !== undefined && traitValue !== null;
|
|
76
|
+
}
|
|
77
|
+
if (condition.operator === IS_NOT_SET) {
|
|
78
|
+
return traitValue === undefined || traitValue === null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (traitValue !== undefined && traitValue !== null) {
|
|
82
|
+
const segmentCondition = new SegmentConditionModel(
|
|
83
|
+
condition.operator,
|
|
84
|
+
condition.value as string,
|
|
85
|
+
condition.property
|
|
86
|
+
);
|
|
87
|
+
return segmentCondition.matchesTraitValue(traitValue);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
34
91
|
}
|
|
35
92
|
|
|
36
93
|
function traitsMatchSegmentRule(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
94
|
+
rule: SegmentRule,
|
|
95
|
+
segmentKey: string,
|
|
96
|
+
context?: GenericEvaluationContext
|
|
97
|
+
): boolean {
|
|
98
|
+
const matchesConditions = evaluateConditions(rule, segmentKey, context);
|
|
99
|
+
const matchesSubRules = evaluateSubRules(rule, segmentKey, context);
|
|
100
|
+
|
|
101
|
+
return matchesConditions && matchesSubRules;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function evaluateConditions(
|
|
105
|
+
rule: SegmentRule,
|
|
106
|
+
segmentKey: string,
|
|
107
|
+
context?: GenericEvaluationContext
|
|
41
108
|
): boolean {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
traitsMatchSegmentCondition(identityTraits, condition, segmentId, identityId)
|
|
47
|
-
)
|
|
48
|
-
)
|
|
49
|
-
: true;
|
|
50
|
-
return (
|
|
51
|
-
matchesConditions &&
|
|
52
|
-
rule.rules.filter(rule =>
|
|
53
|
-
traitsMatchSegmentRule(identityTraits, rule, segmentId, identityId)
|
|
54
|
-
).length === rule.rules.length
|
|
109
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
110
|
+
|
|
111
|
+
const conditionResults = rule.conditions.map((condition: SegmentCondition) =>
|
|
112
|
+
traitsMatchSegmentCondition(condition, segmentKey, context)
|
|
55
113
|
);
|
|
114
|
+
|
|
115
|
+
return evaluateRuleConditions(rule.type, conditionResults);
|
|
56
116
|
}
|
|
57
117
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
identityId: number | string
|
|
118
|
+
function evaluateSubRules(
|
|
119
|
+
rule: SegmentRule,
|
|
120
|
+
segmentKey: string,
|
|
121
|
+
context?: GenericEvaluationContext
|
|
63
122
|
): boolean {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
123
|
+
if (!rule.rules || rule.rules.length === 0) return true;
|
|
124
|
+
|
|
125
|
+
return rule.rules.every((subRule: SegmentRule) =>
|
|
126
|
+
traitsMatchSegmentRule(subRule, segmentKey, context)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): boolean {
|
|
131
|
+
switch (ruleType) {
|
|
132
|
+
case 'ALL':
|
|
133
|
+
return conditionResults.length === 0 || conditionResults.every(result => result);
|
|
134
|
+
case 'ANY':
|
|
135
|
+
return conditionResults.length > 0 && conditionResults.some(result => result);
|
|
136
|
+
case 'NONE':
|
|
137
|
+
return conditionResults.length === 0 || conditionResults.every(result => !result);
|
|
138
|
+
default:
|
|
139
|
+
return false;
|
|
67
140
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getTraitValue(property: string, context?: GenericEvaluationContext): any {
|
|
144
|
+
if (property.startsWith('$.')) {
|
|
145
|
+
const contextValue = getContextValue(property, context);
|
|
146
|
+
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
147
|
+
return contextValue;
|
|
148
|
+
}
|
|
74
149
|
}
|
|
75
|
-
|
|
150
|
+
const traits = context?.identity?.traits || {};
|
|
151
|
+
|
|
152
|
+
return traits[property];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isPrimitive(value: any): boolean {
|
|
156
|
+
if (value === null || value === undefined) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Objects and arrays are non-primitive
|
|
161
|
+
return typeof value !== 'object';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Evaluates JSONPath expressions against the evaluation context.
|
|
166
|
+
*
|
|
167
|
+
* Supports accessing nested context values using JSONPath syntax.
|
|
168
|
+
* Commonly used paths:
|
|
169
|
+
* - $.identity.identifier - User's unique identifier
|
|
170
|
+
* - $.identity.key - User's internal key
|
|
171
|
+
* - $.environment.name - Environment name
|
|
172
|
+
* - $.environment.key - Environment key
|
|
173
|
+
*
|
|
174
|
+
* @param jsonPath - JSONPath expression starting with '$.'
|
|
175
|
+
* @param context - Evaluation context to query against
|
|
176
|
+
* @returns The resolved value, or undefined if path doesn't exist or is invalid
|
|
177
|
+
*/
|
|
178
|
+
export function getContextValue(jsonPath: string, context?: GenericEvaluationContext): any {
|
|
179
|
+
if (!context || !jsonPath?.startsWith('$.')) return undefined;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const normalizedPath = normalizeJsonPath(jsonPath);
|
|
183
|
+
const results = jsonpath.query(context, normalizedPath);
|
|
184
|
+
return results.length > 0 ? results[0] : undefined;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeJsonPath(jsonPath: string): string {
|
|
191
|
+
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
76
192
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as semver from 'semver';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FeatureModel,
|
|
5
|
+
FeatureStateModel,
|
|
6
|
+
MultivariateFeatureOptionModel,
|
|
7
|
+
MultivariateFeatureStateValueModel
|
|
8
|
+
} from '../features/models.js';
|
|
4
9
|
import { getCastingFunction as getCastingFunction } from '../utils/index.js';
|
|
5
10
|
import {
|
|
6
11
|
ALL_RULE,
|
|
@@ -13,6 +18,12 @@ import {
|
|
|
13
18
|
CONDITION_OPERATORS
|
|
14
19
|
} from './constants.js';
|
|
15
20
|
import { isSemver } from './util.js';
|
|
21
|
+
import {
|
|
22
|
+
EvaluationContext,
|
|
23
|
+
Overrides
|
|
24
|
+
} from '../evaluation/evaluationContext/evaluationContext.types.js';
|
|
25
|
+
import { CONSTANTS } from '../features/constants.js';
|
|
26
|
+
import { EvaluationResultSegments, SegmentSource } from '../evaluation/models.js';
|
|
16
27
|
|
|
17
28
|
export const all = (iterable: Array<any>) => iterable.filter(e => !!e).length === iterable.length;
|
|
18
29
|
export const any = (iterable: Array<any>) => iterable.filter(e => !!e).length > 0;
|
|
@@ -26,22 +37,45 @@ export const matchingFunctions = {
|
|
|
26
37
|
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) =>
|
|
27
38
|
thisValue >= otherValue,
|
|
28
39
|
[CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue,
|
|
29
|
-
[CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) =>
|
|
30
|
-
|
|
40
|
+
[CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) => {
|
|
41
|
+
try {
|
|
42
|
+
return !!otherValue && otherValue.includes(thisValue);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Semver library throws an error if the version is invalid, in this case, we want to catch and return false
|
|
50
|
+
const safeSemverCompare = (
|
|
51
|
+
semverMatchingFunction: (conditionValue: any, traitValue: any) => boolean
|
|
52
|
+
) => {
|
|
53
|
+
return (conditionValue: any, traitValue: any) => {
|
|
54
|
+
try {
|
|
55
|
+
return semverMatchingFunction(conditionValue, traitValue);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
31
60
|
};
|
|
32
61
|
|
|
33
62
|
export const semverMatchingFunction = {
|
|
34
63
|
...matchingFunctions,
|
|
35
|
-
[CONDITION_OPERATORS.EQUAL]: (
|
|
36
|
-
semver.eq(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[CONDITION_OPERATORS.
|
|
42
|
-
semver.
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
[CONDITION_OPERATORS.EQUAL]: safeSemverCompare((conditionValue, traitValue) =>
|
|
65
|
+
semver.eq(traitValue, conditionValue)
|
|
66
|
+
),
|
|
67
|
+
[CONDITION_OPERATORS.GREATER_THAN]: safeSemverCompare((conditionValue, traitValue) =>
|
|
68
|
+
semver.gt(traitValue, conditionValue)
|
|
69
|
+
),
|
|
70
|
+
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) =>
|
|
71
|
+
semver.gte(traitValue, conditionValue)
|
|
72
|
+
),
|
|
73
|
+
[CONDITION_OPERATORS.LESS_THAN]: safeSemverCompare((conditionValue, traitValue) =>
|
|
74
|
+
semver.lt(traitValue, conditionValue)
|
|
75
|
+
),
|
|
76
|
+
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) =>
|
|
77
|
+
semver.lte(traitValue, conditionValue)
|
|
78
|
+
)
|
|
45
79
|
};
|
|
46
80
|
|
|
47
81
|
export const getMatchingFunctions = (semver: boolean) =>
|
|
@@ -56,17 +90,17 @@ export class SegmentConditionModel {
|
|
|
56
90
|
};
|
|
57
91
|
|
|
58
92
|
operator: string;
|
|
59
|
-
value: string | null | undefined;
|
|
60
|
-
|
|
93
|
+
value: string | null | undefined | string[];
|
|
94
|
+
property: string | null | undefined;
|
|
61
95
|
|
|
62
96
|
constructor(
|
|
63
97
|
operator: string,
|
|
64
|
-
value?: string | null | undefined,
|
|
98
|
+
value?: string | null | undefined | string[],
|
|
65
99
|
property?: string | null | undefined
|
|
66
100
|
) {
|
|
67
101
|
this.operator = operator;
|
|
68
102
|
this.value = value;
|
|
69
|
-
this.
|
|
103
|
+
this.property = property;
|
|
70
104
|
}
|
|
71
105
|
|
|
72
106
|
matchesTraitValue(traitValue: any) {
|
|
@@ -79,17 +113,52 @@ export class SegmentConditionModel {
|
|
|
79
113
|
);
|
|
80
114
|
},
|
|
81
115
|
evaluateRegex: (traitValue: any) => {
|
|
82
|
-
|
|
116
|
+
try {
|
|
117
|
+
if (!this.value) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const regex = new RegExp(this.value?.toString());
|
|
121
|
+
return !!traitValue?.toString().match(regex);
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
83
125
|
},
|
|
84
126
|
evaluateModulo: (traitValue: any) => {
|
|
85
|
-
|
|
127
|
+
const parsedTraitValue = parseFloat(traitValue);
|
|
128
|
+
if (isNaN(parsedTraitValue) || !this.value) {
|
|
86
129
|
return false;
|
|
87
130
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
131
|
+
|
|
132
|
+
const parts = this.value.toString().split('|');
|
|
133
|
+
if (parts.length !== 2) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const divisor = parseFloat(parts[0]);
|
|
138
|
+
const remainder = parseFloat(parts[1]);
|
|
139
|
+
|
|
140
|
+
if (isNaN(divisor) || isNaN(remainder) || divisor === 0) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return parsedTraitValue % divisor === remainder;
|
|
91
145
|
},
|
|
92
|
-
evaluateIn: (traitValue:
|
|
146
|
+
evaluateIn: (traitValue: string[] | string) => {
|
|
147
|
+
if (!traitValue || typeof traitValue === 'boolean') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(this.value)) {
|
|
151
|
+
return this.value.includes(traitValue.toString());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof this.value === 'string') {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(this.value);
|
|
157
|
+
if (Array.isArray(parsed)) {
|
|
158
|
+
return parsed.includes(traitValue.toString());
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
93
162
|
return this.value?.split(',').includes(traitValue.toString());
|
|
94
163
|
}
|
|
95
164
|
};
|
|
@@ -144,4 +213,83 @@ export class SegmentModel {
|
|
|
144
213
|
this.id = id;
|
|
145
214
|
this.name = name;
|
|
146
215
|
}
|
|
216
|
+
|
|
217
|
+
static fromSegmentResult(
|
|
218
|
+
segmentResults: EvaluationResultSegments,
|
|
219
|
+
evaluationContext: EvaluationContext
|
|
220
|
+
): SegmentModel[] {
|
|
221
|
+
const segmentModels: SegmentModel[] = [];
|
|
222
|
+
if (!evaluationContext.segments) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const segmentResult of segmentResults) {
|
|
227
|
+
if (segmentResult.metadata?.source === SegmentSource.IDENTITY_OVERRIDE) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const segmentMetadataId = segmentResult.metadata?.id;
|
|
231
|
+
if (!segmentMetadataId) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const segmentContext = evaluationContext.segments[segmentMetadataId.toString()];
|
|
235
|
+
if (segmentContext) {
|
|
236
|
+
const segment = new SegmentModel(segmentMetadataId, segmentContext.name);
|
|
237
|
+
segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type));
|
|
238
|
+
segment.featureStates = SegmentModel.createFeatureStatesFromOverrides(
|
|
239
|
+
segmentContext.overrides || []
|
|
240
|
+
);
|
|
241
|
+
segmentModels.push(segment);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return segmentModels;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static createFeatureStatesFromOverrides(overrides: Overrides): FeatureStateModel[] {
|
|
249
|
+
if (!overrides) return [];
|
|
250
|
+
return overrides
|
|
251
|
+
.filter(override => {
|
|
252
|
+
const overrideMetadataId = override?.metadata?.id;
|
|
253
|
+
return typeof overrideMetadataId === 'number';
|
|
254
|
+
})
|
|
255
|
+
.map(override => {
|
|
256
|
+
const overrideMetadataId = override.metadata!.id as number;
|
|
257
|
+
const feature = new FeatureModel(
|
|
258
|
+
overrideMetadataId,
|
|
259
|
+
override.name,
|
|
260
|
+
override.variants?.length && override.variants.length > 0
|
|
261
|
+
? CONSTANTS.MULTIVARIATE
|
|
262
|
+
: CONSTANTS.STANDARD
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const featureState = new FeatureStateModel(
|
|
266
|
+
feature,
|
|
267
|
+
override.enabled,
|
|
268
|
+
override.priority || 0
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (override.value !== undefined) {
|
|
272
|
+
featureState.setValue(override.value);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (override.variants && override.variants.length > 0) {
|
|
276
|
+
featureState.multivariateFeatureStateValues = this.createMultivariateValues(
|
|
277
|
+
override.variants
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return featureState;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static createMultivariateValues(variants: any[]): MultivariateFeatureStateValueModel[] {
|
|
286
|
+
return variants.map(
|
|
287
|
+
variant =>
|
|
288
|
+
new MultivariateFeatureStateValueModel(
|
|
289
|
+
new MultivariateFeatureOptionModel(variant.value, variant.id as number),
|
|
290
|
+
variant.weight as number,
|
|
291
|
+
variant.id as number
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
147
295
|
}
|
|
@@ -14,7 +14,7 @@ const makeRepeated = (arr: Array<any>, repeats: number) =>
|
|
|
14
14
|
* @param {} iterations=1 num times to include each id in the generated string to hash
|
|
15
15
|
* @returns number number between 0 (inclusive) and 100 (exclusive)
|
|
16
16
|
*/
|
|
17
|
-
export function
|
|
17
|
+
export function getHashedPercentageForObjIds(objectIds: Array<any>, iterations = 1): number {
|
|
18
18
|
let toHash = makeRepeated(objectIds, iterations).join(',');
|
|
19
19
|
const hashedValue = md5(toHash);
|
|
20
20
|
const hashedInt = BigInt('0x' + hashedValue);
|
|
@@ -24,7 +24,7 @@ export function getHashedPercentateForObjIds(objectIds: Array<any>, iterations =
|
|
|
24
24
|
/* istanbul ignore next */
|
|
25
25
|
if (value === 100) {
|
|
26
26
|
/* istanbul ignore next */
|
|
27
|
-
return
|
|
27
|
+
return getHashedPercentageForObjIds(objectIds, iterations + 1);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
return value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.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",
|
|
@@ -51,26 +51,37 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"lint": "prettier --write .",
|
|
53
53
|
"test": "vitest --coverage --run",
|
|
54
|
+
"test:esm-build": "npm run build && ESM_BUILD=true vitest --config vitest.config.esm.ts --run",
|
|
54
55
|
"test:watch": "vitest",
|
|
55
56
|
"test:debug": "vitest --inspect-brk --no-file-parallelism --coverage",
|
|
56
57
|
"prebuild": "rm -rf ./build",
|
|
57
58
|
"build": "tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}'> build/cjs/package.json",
|
|
58
59
|
"deploy": "npm i && npm run build && npm publish",
|
|
59
60
|
"deploy:beta": "npm i && npm run build && npm publish --tag beta",
|
|
60
|
-
"prepare": "husky install"
|
|
61
|
+
"prepare": "husky install",
|
|
62
|
+
"generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json",
|
|
63
|
+
"generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json",
|
|
64
|
+
"generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types"
|
|
61
65
|
},
|
|
62
66
|
"dependencies": {
|
|
67
|
+
"jsonpath": "^1.1.1",
|
|
63
68
|
"pino": "^8.8.0",
|
|
64
69
|
"semver": "^7.3.7",
|
|
65
70
|
"undici-types": "^6.19.8"
|
|
66
71
|
},
|
|
67
72
|
"devDependencies": {
|
|
73
|
+
"@types/jest": "^30.0.0",
|
|
74
|
+
"@types/jsonpath": "^0.2.4",
|
|
68
75
|
"@types/node": "^20.16.10",
|
|
69
76
|
"@types/semver": "^7.3.9",
|
|
70
77
|
"@types/uuid": "^8.3.4",
|
|
71
78
|
"@vitest/coverage-v8": "^2.1.2",
|
|
72
79
|
"esbuild": "^0.25.0",
|
|
73
80
|
"husky": "^7.0.4",
|
|
81
|
+
"install": "^0.13.0",
|
|
82
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
83
|
+
"jsonc-parser": "^3.3.1",
|
|
84
|
+
"npm": "^11.6.1",
|
|
74
85
|
"prettier": "^2.2.1",
|
|
75
86
|
"typescript": "^4.9.5",
|
|
76
87
|
"undici": "^6.19.8",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bootstrap-sha": "644c5c883ecbb3786507b50cea01903dc2e533bf",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "node",
|
|
6
|
+
"changelog-path": "CHANGELOG.md",
|
|
7
|
+
"bump-minor-pre-major": false,
|
|
8
|
+
"bump-patch-for-minor-pre-major": false,
|
|
9
|
+
"draft": false,
|
|
10
|
+
"prerelease": false,
|
|
11
|
+
"include-component-in-tag": false
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
15
|
+
"changelog-sections": [
|
|
16
|
+
{
|
|
17
|
+
"type": "feat",
|
|
18
|
+
"hidden": false,
|
|
19
|
+
"section": "Features"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"hidden": false,
|
|
24
|
+
"section": "Bug Fixes"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "ci",
|
|
28
|
+
"hidden": false,
|
|
29
|
+
"section": "CI"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "docs",
|
|
33
|
+
"hidden": false,
|
|
34
|
+
"section": "Docs"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "deps",
|
|
38
|
+
"hidden": false,
|
|
39
|
+
"section": "Dependency Updates"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "perf",
|
|
43
|
+
"hidden": false,
|
|
44
|
+
"section": "Performance Improvements"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "refactor",
|
|
48
|
+
"hidden": false,
|
|
49
|
+
"section": "Refactoring"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "test",
|
|
53
|
+
"hidden": false,
|
|
54
|
+
"section": "Tests"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "chore",
|
|
58
|
+
"hidden": false,
|
|
59
|
+
"section": "Other"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
package/sdk/analytics.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pino, Logger } from 'pino';
|
|
2
2
|
import { Fetch } from './types.js';
|
|
3
3
|
import { FlagsmithConfig } from './types.js';
|
|
4
|
+
import { getUserAgent } from './utils.js';
|
|
4
5
|
|
|
5
6
|
export const ANALYTICS_ENDPOINT = './analytics/flags/';
|
|
6
7
|
|
|
@@ -69,7 +70,8 @@ export class AnalyticsProcessor {
|
|
|
69
70
|
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
70
71
|
headers: {
|
|
71
72
|
'Content-Type': 'application/json',
|
|
72
|
-
'X-Environment-Key': this.environmentKey
|
|
73
|
+
'X-Environment-Key': this.environmentKey,
|
|
74
|
+
'User-Agent': getUserAgent()
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
await this.currentFlush;
|