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,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) =>
|
|
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]: (
|
|
19
|
-
[CONDITION_OPERATORS.GREATER_THAN]: (
|
|
20
|
-
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (
|
|
21
|
-
[CONDITION_OPERATORS.LESS_THAN]: (
|
|
22
|
-
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (
|
|
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
|
-
|
|
55
|
+
property;
|
|
35
56
|
constructor(operator, value, property) {
|
|
36
57
|
this.operator = operator;
|
|
37
58
|
this.value = value;
|
|
38
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/build/esm/sdk/index.d.ts
CHANGED
|
@@ -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';
|
package/build/esm/sdk/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
314
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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;
|
package/build/esm/sdk/models.js
CHANGED
|
@@ -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) {
|
package/build/esm/sdk/utils.d.ts
CHANGED
package/build/esm/sdk/utils.js
CHANGED
|
@@ -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) {
|