flagsmith-nodejs 6.0.1 → 6.2.0
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/pull_request.yaml +1 -1
- package/.github/workflows/release-please.yml +18 -0
- package/.gitmodules +1 -0
- package/.husky/pre-commit +0 -0
- package/.prettierignore +2 -1
- package/.prettierrc.cjs +9 -1
- package/.release-please-manifest.json +1 -0
- package/CHANGELOG.md +552 -0
- package/CODEOWNERS +1 -0
- package/README.md +2 -3
- package/build/cjs/flagsmith-engine/features/util.js +3 -3
- package/build/cjs/flagsmith-engine/index.d.ts +1 -1
- package/build/cjs/flagsmith-engine/index.js +2 -1
- package/build/cjs/flagsmith-engine/segments/models.js +7 -7
- package/build/cjs/flagsmith-engine/utils/hashing/index.js +1 -1
- package/build/cjs/index.d.ts +4 -4
- package/build/cjs/index.js +3 -1
- package/build/cjs/sdk/analytics.d.ts +1 -1
- package/build/cjs/sdk/analytics.js +3 -1
- package/build/cjs/sdk/index.d.ts +5 -5
- package/build/cjs/sdk/index.js +48 -10
- package/build/cjs/sdk/models.d.ts +25 -0
- package/build/cjs/sdk/models.js +25 -0
- package/build/cjs/sdk/types.d.ts +14 -4
- package/build/cjs/sdk/utils.d.ts +5 -4
- package/build/cjs/sdk/utils.js +16 -3
- package/build/esm/flagsmith-engine/features/models.js +1 -1
- package/build/esm/flagsmith-engine/features/util.js +3 -3
- package/build/esm/flagsmith-engine/index.d.ts +1 -1
- package/build/esm/flagsmith-engine/index.js +1 -1
- package/build/esm/flagsmith-engine/segments/models.js +7 -7
- package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
- package/build/esm/flagsmith-engine/utils/index.js +1 -1
- package/build/esm/index.d.ts +4 -4
- package/build/esm/index.js +3 -3
- package/build/esm/sdk/analytics.d.ts +1 -1
- package/build/esm/sdk/analytics.js +3 -1
- package/build/esm/sdk/index.d.ts +5 -5
- package/build/esm/sdk/index.js +48 -11
- package/build/esm/sdk/models.d.ts +25 -0
- package/build/esm/sdk/models.js +25 -0
- package/build/esm/sdk/types.d.ts +14 -4
- package/build/esm/sdk/utils.d.ts +5 -4
- package/build/esm/sdk/utils.js +14 -2
- package/flagsmith-engine/environments/util.ts +2 -2
- package/flagsmith-engine/features/models.ts +1 -1
- package/flagsmith-engine/features/util.ts +14 -14
- package/flagsmith-engine/identities/models.ts +1 -1
- package/flagsmith-engine/index.ts +1 -1
- package/flagsmith-engine/segments/evaluators.ts +2 -3
- package/flagsmith-engine/segments/models.ts +25 -15
- package/flagsmith-engine/utils/hashing/index.ts +3 -3
- package/flagsmith-engine/utils/index.ts +4 -2
- package/index.ts +19 -22
- package/package.json +1 -1
- package/release-please-config.json +62 -0
- package/sdk/analytics.ts +10 -6
- package/sdk/index.ts +91 -28
- package/sdk/models.ts +25 -0
- package/sdk/offline_handlers.ts +1 -1
- package/sdk/types.ts +17 -8
- package/sdk/utils.ts +21 -8
- package/tests/engine/e2e/engine.test.ts +2 -4
- package/tests/engine/unit/engine.test.ts +1 -6
- package/tests/engine/unit/features/models.test.ts +2 -2
- package/tests/engine/unit/identities/identities_builders.test.ts +1 -1
- package/tests/engine/unit/segments/segment_evaluators.test.ts +52 -23
- package/tests/engine/unit/segments/segments_model.test.ts +35 -37
- package/tests/engine/unit/utils/utils.test.ts +28 -30
- package/tests/sdk/analytics.test.ts +30 -26
- package/tests/sdk/flagsmith-cache.test.ts +84 -76
- package/tests/sdk/flagsmith-environment-flags.test.ts +121 -93
- package/tests/sdk/flagsmith-identity-flags.test.ts +155 -149
- package/tests/sdk/flagsmith.test.ts +202 -43
- package/tests/sdk/offline-handlers.test.ts +32 -32
- package/tests/sdk/polling.test.ts +0 -1
- package/tests/sdk/utils.ts +26 -18
- package/vitest.config.ts +10 -15
|
@@ -1,44 +1,70 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ALL_RULE,
|
|
3
3
|
CONDITION_OPERATORS,
|
|
4
|
-
PERCENTAGE_SPLIT
|
|
4
|
+
PERCENTAGE_SPLIT
|
|
5
5
|
} from '../../../../flagsmith-engine/segments/constants.js';
|
|
6
|
-
import {SegmentConditionModel} from '../../../../flagsmith-engine/segments/models.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js';
|
|
7
|
+
import {
|
|
8
|
+
traitsMatchSegmentCondition,
|
|
9
|
+
evaluateIdentityInSegment
|
|
10
|
+
} from '../../../../flagsmith-engine/segments/evaluators.js';
|
|
11
|
+
import { TraitModel, IdentityModel } from '../../../../flagsmith-engine/index.js';
|
|
12
|
+
import { environment } from '../utils.js';
|
|
10
13
|
import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util.js';
|
|
11
14
|
import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
|
|
12
15
|
|
|
13
|
-
|
|
14
16
|
// todo: work out how to implement this in a test function or before hook
|
|
15
17
|
vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({
|
|
16
18
|
getHashedPercentateForObjIds: vi.fn(() => 1)
|
|
17
19
|
}));
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[
|
|
21
|
+
let traitExistenceTestCases: [
|
|
22
|
+
string,
|
|
23
|
+
string | null | undefined,
|
|
24
|
+
string | null | undefined,
|
|
25
|
+
TraitModel[],
|
|
26
|
+
boolean
|
|
27
|
+
][] = [
|
|
28
|
+
[CONDITION_OPERATORS.IS_SET, 'foo', null, [], false],
|
|
29
|
+
[CONDITION_OPERATORS.IS_SET, 'foo', undefined, [new TraitModel('foo', 'bar')], true],
|
|
30
|
+
[
|
|
31
|
+
CONDITION_OPERATORS.IS_SET,
|
|
32
|
+
'foo',
|
|
33
|
+
undefined,
|
|
34
|
+
[new TraitModel('foo', 'bar'), new TraitModel('fooBaz', 'baz')],
|
|
35
|
+
true
|
|
36
|
+
],
|
|
24
37
|
[CONDITION_OPERATORS.IS_NOT_SET, 'foo', undefined, [], true],
|
|
25
|
-
[CONDITION_OPERATORS.IS_NOT_SET, 'foo', null, [new TraitModel('foo','bar')], false],
|
|
26
|
-
[
|
|
38
|
+
[CONDITION_OPERATORS.IS_NOT_SET, 'foo', null, [new TraitModel('foo', 'bar')], false],
|
|
39
|
+
[
|
|
40
|
+
CONDITION_OPERATORS.IS_NOT_SET,
|
|
41
|
+
'foo',
|
|
42
|
+
null,
|
|
43
|
+
[new TraitModel('foo', 'bar'), new TraitModel('fooBaz', 'baz')],
|
|
44
|
+
false
|
|
45
|
+
]
|
|
27
46
|
];
|
|
28
47
|
|
|
29
48
|
test('test_traits_match_segment_condition_for_trait_existence_operators', () => {
|
|
30
49
|
for (const testCase of traitExistenceTestCases) {
|
|
31
|
-
const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase
|
|
32
|
-
let segmentModel = new SegmentConditionModel(operator, conditionValue, conditionProperty)
|
|
33
|
-
expect(
|
|
34
|
-
|
|
35
|
-
)
|
|
50
|
+
const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase;
|
|
51
|
+
let segmentModel = new SegmentConditionModel(operator, conditionValue, conditionProperty);
|
|
52
|
+
expect(traitsMatchSegmentCondition(traits, segmentModel, 'any', 'any')).toBe(
|
|
53
|
+
expectedResult
|
|
54
|
+
);
|
|
36
55
|
}
|
|
37
56
|
});
|
|
38
57
|
|
|
39
|
-
|
|
40
58
|
test('evaluateIdentityInSegment uses django ID for hashed percentage when present', () => {
|
|
41
|
-
var identityModel = new IdentityModel(
|
|
59
|
+
var identityModel = new IdentityModel(
|
|
60
|
+
Date.now().toString(),
|
|
61
|
+
[],
|
|
62
|
+
[],
|
|
63
|
+
environment().apiKey,
|
|
64
|
+
'identity_1',
|
|
65
|
+
undefined,
|
|
66
|
+
1
|
|
67
|
+
);
|
|
42
68
|
const segmentDefinition = {
|
|
43
69
|
id: 1,
|
|
44
70
|
name: 'percentage_split_segment',
|
|
@@ -49,7 +75,7 @@ test('evaluateIdentityInSegment uses django ID for hashed percentage when presen
|
|
|
49
75
|
{
|
|
50
76
|
operator: PERCENTAGE_SPLIT,
|
|
51
77
|
property_: null,
|
|
52
|
-
value:
|
|
78
|
+
value: '10'
|
|
53
79
|
}
|
|
54
80
|
],
|
|
55
81
|
rules: []
|
|
@@ -62,6 +88,9 @@ test('evaluateIdentityInSegment uses django ID for hashed percentage when presen
|
|
|
62
88
|
var result = evaluateIdentityInSegment(identityModel, segmentModel);
|
|
63
89
|
|
|
64
90
|
expect(result).toBe(true);
|
|
65
|
-
expect(getHashedPercentateForObjIds).toHaveBeenCalledTimes(1)
|
|
66
|
-
expect(getHashedPercentateForObjIds).toHaveBeenCalledWith([
|
|
91
|
+
expect(getHashedPercentateForObjIds).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(getHashedPercentateForObjIds).toHaveBeenCalledWith([
|
|
93
|
+
segmentModel.id,
|
|
94
|
+
identityModel.djangoID
|
|
95
|
+
]);
|
|
67
96
|
});
|
|
@@ -65,49 +65,47 @@ const conditionMatchCases: [string, string | number | boolean | null, string, bo
|
|
|
65
65
|
[CONDITION_OPERATORS.REGEX, 'foo', '[a-z]+', true],
|
|
66
66
|
[CONDITION_OPERATORS.REGEX, 'FOO', '[a-z]+', false],
|
|
67
67
|
[CONDITION_OPERATORS.REGEX, null, '[a-z]+', false],
|
|
68
|
-
[CONDITION_OPERATORS.EQUAL,
|
|
69
|
-
[CONDITION_OPERATORS.EQUAL,
|
|
70
|
-
[CONDITION_OPERATORS.EQUAL,
|
|
71
|
-
[CONDITION_OPERATORS.NOT_EQUAL,
|
|
72
|
-
[CONDITION_OPERATORS.NOT_EQUAL,
|
|
73
|
-
[CONDITION_OPERATORS.GREATER_THAN,
|
|
74
|
-
[CONDITION_OPERATORS.GREATER_THAN,
|
|
75
|
-
[CONDITION_OPERATORS.GREATER_THAN,
|
|
76
|
-
[CONDITION_OPERATORS.GREATER_THAN,
|
|
77
|
-
[CONDITION_OPERATORS.GREATER_THAN,
|
|
78
|
-
[CONDITION_OPERATORS.LESS_THAN,
|
|
79
|
-
[CONDITION_OPERATORS.LESS_THAN,
|
|
80
|
-
[CONDITION_OPERATORS.LESS_THAN,
|
|
81
|
-
[CONDITION_OPERATORS.LESS_THAN,
|
|
82
|
-
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE,
|
|
83
|
-
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE,
|
|
84
|
-
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE,
|
|
85
|
-
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE,
|
|
86
|
-
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE,
|
|
87
|
-
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE,
|
|
88
|
-
[CONDITION_OPERATORS.MODULO, 1,
|
|
89
|
-
[CONDITION_OPERATORS.MODULO, 2,
|
|
90
|
-
[CONDITION_OPERATORS.MODULO, 3,
|
|
91
|
-
[CONDITION_OPERATORS.MODULO, 34.2,
|
|
92
|
-
[CONDITION_OPERATORS.MODULO, 35.0,
|
|
93
|
-
[CONDITION_OPERATORS.MODULO,
|
|
94
|
-
[CONDITION_OPERATORS.MODULO, 35.0,
|
|
95
|
-
[CONDITION_OPERATORS.IN,
|
|
96
|
-
[CONDITION_OPERATORS.IN,
|
|
97
|
-
[CONDITION_OPERATORS.IN,
|
|
98
|
-
[CONDITION_OPERATORS.IN, 1,
|
|
99
|
-
[CONDITION_OPERATORS.IN, 1,
|
|
100
|
-
[CONDITION_OPERATORS.IN, 1,
|
|
68
|
+
[CONDITION_OPERATORS.EQUAL, '1.0.0', '1.0.0:semver', true],
|
|
69
|
+
[CONDITION_OPERATORS.EQUAL, '1.0.0', '1.0.0:semver', true],
|
|
70
|
+
[CONDITION_OPERATORS.EQUAL, '1.0.0', '1.0.1:semver', false],
|
|
71
|
+
[CONDITION_OPERATORS.NOT_EQUAL, '1.0.0', '1.0.0:semver', false],
|
|
72
|
+
[CONDITION_OPERATORS.NOT_EQUAL, '1.0.0', '1.0.1:semver', true],
|
|
73
|
+
[CONDITION_OPERATORS.GREATER_THAN, '1.0.1', '1.0.0:semver', true],
|
|
74
|
+
[CONDITION_OPERATORS.GREATER_THAN, '1.0.0', '1.0.0-beta:semver', true],
|
|
75
|
+
[CONDITION_OPERATORS.GREATER_THAN, '1.0.1', '1.2.0:semver', false],
|
|
76
|
+
[CONDITION_OPERATORS.GREATER_THAN, '1.0.1', '1.0.1:semver', false],
|
|
77
|
+
[CONDITION_OPERATORS.GREATER_THAN, '1.2.4', '1.2.3-pre.2+build.4:semver', true],
|
|
78
|
+
[CONDITION_OPERATORS.LESS_THAN, '1.0.0', '1.0.1:semver', true],
|
|
79
|
+
[CONDITION_OPERATORS.LESS_THAN, '1.0.0', '1.0.0:semver', false],
|
|
80
|
+
[CONDITION_OPERATORS.LESS_THAN, '1.0.1', '1.0.0:semver', false],
|
|
81
|
+
[CONDITION_OPERATORS.LESS_THAN, '1.0.0-rc.2', '1.0.0-rc.3:semver', true],
|
|
82
|
+
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, '1.0.1', '1.0.0:semver', true],
|
|
83
|
+
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, '1.0.1', '1.2.0:semver', false],
|
|
84
|
+
[CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE, '1.0.1', '1.0.1:semver', true],
|
|
85
|
+
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, '1.0.0', '1.0.1:semver', true],
|
|
86
|
+
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, '1.0.0', '1.0.0:semver', true],
|
|
87
|
+
[CONDITION_OPERATORS.LESS_THAN_INCLUSIVE, '1.0.1', '1.0.0:semver', false],
|
|
88
|
+
[CONDITION_OPERATORS.MODULO, 1, '2|0', false],
|
|
89
|
+
[CONDITION_OPERATORS.MODULO, 2, '2|0', true],
|
|
90
|
+
[CONDITION_OPERATORS.MODULO, 3, '2|0', false],
|
|
91
|
+
[CONDITION_OPERATORS.MODULO, 34.2, '4|3', false],
|
|
92
|
+
[CONDITION_OPERATORS.MODULO, 35.0, '4|3', true],
|
|
93
|
+
[CONDITION_OPERATORS.MODULO, 'foo', '4|3', false],
|
|
94
|
+
[CONDITION_OPERATORS.MODULO, 35.0, 'foo|bar', false],
|
|
95
|
+
[CONDITION_OPERATORS.IN, 'foo', '', false],
|
|
96
|
+
[CONDITION_OPERATORS.IN, 'foo', 'foo, bar', true],
|
|
97
|
+
[CONDITION_OPERATORS.IN, 'foo', 'foo', true],
|
|
98
|
+
[CONDITION_OPERATORS.IN, 1, '1,2,3,4', true],
|
|
99
|
+
[CONDITION_OPERATORS.IN, 1, '', false],
|
|
100
|
+
[CONDITION_OPERATORS.IN, 1, '1', true],
|
|
101
101
|
['BAD_OP', 'a', 'a', false]
|
|
102
102
|
];
|
|
103
103
|
|
|
104
104
|
test('test_segment_condition_matches_trait_value', () => {
|
|
105
105
|
for (const testCase of conditionMatchCases) {
|
|
106
|
-
const [operator, traitValue, conditionValue, expectedResult] = testCase
|
|
106
|
+
const [operator, traitValue, conditionValue, expectedResult] = testCase;
|
|
107
107
|
expect(
|
|
108
|
-
new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(
|
|
109
|
-
traitValue
|
|
110
|
-
)
|
|
108
|
+
new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(traitValue)
|
|
111
109
|
).toBe(expectedResult);
|
|
112
110
|
}
|
|
113
111
|
});
|
|
@@ -2,60 +2,58 @@ import { randomUUID as uuidv4 } from 'node:crypto';
|
|
|
2
2
|
import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
|
|
3
3
|
|
|
4
4
|
describe('getHashedPercentageForObjIds', () => {
|
|
5
|
-
it.each([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
let resultOne = getHashedPercentateForObjIds(objIds);
|
|
23
|
-
let resultTwo = getHashedPercentateForObjIds(objIds);
|
|
24
|
-
expect(resultOne).toEqual(resultTwo);
|
|
25
|
-
})
|
|
5
|
+
it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])(
|
|
6
|
+
'returns x where 0 <= x < 100',
|
|
7
|
+
(objIds: (string | number)[]) => {
|
|
8
|
+
let result = getHashedPercentateForObjIds(objIds);
|
|
9
|
+
expect(result).toBeLessThan(100);
|
|
10
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
11
|
+
}
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])(
|
|
15
|
+
'returns the same value each time',
|
|
16
|
+
(objIds: (string | number)[]) => {
|
|
17
|
+
let resultOne = getHashedPercentateForObjIds(objIds);
|
|
18
|
+
let resultTwo = getHashedPercentateForObjIds(objIds);
|
|
19
|
+
expect(resultOne).toEqual(resultTwo);
|
|
20
|
+
}
|
|
21
|
+
);
|
|
26
22
|
|
|
27
23
|
it('is unique for different object ids', () => {
|
|
28
24
|
let resultOne = getHashedPercentateForObjIds([14, 106]);
|
|
29
25
|
let resultTwo = getHashedPercentateForObjIds([53, 200]);
|
|
30
26
|
expect(resultOne).not.toEqual(resultTwo);
|
|
31
|
-
})
|
|
27
|
+
});
|
|
32
28
|
|
|
33
29
|
it('is evenly distributed', () => {
|
|
34
30
|
// copied from python test here:
|
|
35
31
|
// https://github.com/Flagsmith/flagsmith-engine/blob/main/tests/unit/utils/test_utils_hashing.py#L56
|
|
36
32
|
const testSample = 500;
|
|
37
33
|
const numTestBuckets = 50;
|
|
38
|
-
const testBucketSize = Math.floor(testSample / numTestBuckets)
|
|
39
|
-
const errorFactor = 0.1
|
|
34
|
+
const testBucketSize = Math.floor(testSample / numTestBuckets);
|
|
35
|
+
const errorFactor = 0.1;
|
|
40
36
|
|
|
41
37
|
// Given
|
|
42
|
-
let objectIdPairs = Array.from(Array(testSample).keys()).flatMap(d =>
|
|
38
|
+
let objectIdPairs = Array.from(Array(testSample).keys()).flatMap(d =>
|
|
39
|
+
Array.from(Array(testSample).keys()).map(e => [d, e].flat())
|
|
40
|
+
);
|
|
43
41
|
|
|
44
42
|
// When
|
|
45
|
-
let values = objectIdPairs.map(
|
|
43
|
+
let values = objectIdPairs.map(objIds => getHashedPercentateForObjIds(objIds));
|
|
46
44
|
|
|
47
45
|
// Then
|
|
48
46
|
for (let i = 0; i++; i < numTestBuckets) {
|
|
49
47
|
let bucketStart = i * testBucketSize;
|
|
50
48
|
let bucketEnd = (i + 1) * testBucketSize;
|
|
51
49
|
let bucketValueLimit = Math.min(
|
|
52
|
-
(i + 1) / numTestBuckets + errorFactor + (
|
|
50
|
+
(i + 1) / numTestBuckets + errorFactor + (i + 1) / numTestBuckets,
|
|
53
51
|
1
|
|
54
|
-
)
|
|
52
|
+
);
|
|
55
53
|
|
|
56
54
|
for (let i = bucketStart; i++; i < bucketEnd) {
|
|
57
55
|
expect(values[i]).toBeLessThanOrEqual(bucketValueLimit);
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
|
-
})
|
|
61
|
-
})
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,40 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getUserAgent } from '../../sdk/utils.js';
|
|
2
|
+
import { analyticsProcessor, fetch } from './utils.js';
|
|
2
3
|
|
|
3
4
|
test('test_analytics_processor_track_feature_updates_analytics_data', () => {
|
|
4
5
|
const aP = analyticsProcessor();
|
|
5
|
-
aP.trackFeature(
|
|
6
|
-
expect(aP.analyticsData[
|
|
6
|
+
aP.trackFeature('myFeature');
|
|
7
|
+
expect(aP.analyticsData['myFeature']).toBe(1);
|
|
7
8
|
|
|
8
|
-
aP.trackFeature(
|
|
9
|
-
expect(aP.analyticsData[
|
|
9
|
+
aP.trackFeature('myFeature');
|
|
10
|
+
expect(aP.analyticsData['myFeature']).toBe(2);
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
test('test_analytics_processor_flush_clears_analytics_data', async () => {
|
|
13
14
|
const aP = analyticsProcessor();
|
|
14
|
-
aP.trackFeature(
|
|
15
|
+
aP.trackFeature('myFeature');
|
|
15
16
|
await aP.flush();
|
|
16
17
|
expect(aP.analyticsData).toStrictEqual({});
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
test('test_analytics_processor_flush_post_request_data_match_ananlytics_data', async () => {
|
|
20
21
|
const aP = analyticsProcessor();
|
|
21
|
-
aP.trackFeature(
|
|
22
|
-
aP.trackFeature(
|
|
22
|
+
aP.trackFeature('myFeature1');
|
|
23
|
+
aP.trackFeature('myFeature2');
|
|
23
24
|
await aP.flush();
|
|
24
25
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
25
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
27
|
+
'http://testUrl/analytics/flags/',
|
|
28
|
+
expect.objectContaining({
|
|
29
|
+
body: '{"myFeature1":1,"myFeature2":1}',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'X-Environment-Key': 'test-key',
|
|
33
|
+
'User-Agent': getUserAgent()
|
|
34
|
+
},
|
|
35
|
+
method: 'POST'
|
|
36
|
+
})
|
|
37
|
+
);
|
|
30
38
|
});
|
|
31
39
|
|
|
32
|
-
vi.useFakeTimers()
|
|
40
|
+
vi.useFakeTimers();
|
|
33
41
|
test('test_analytics_processor_flush_post_request_data_match_ananlytics_data_test', async () => {
|
|
34
42
|
const aP = analyticsProcessor();
|
|
35
|
-
aP.trackFeature(
|
|
43
|
+
aP.trackFeature('myFeature1');
|
|
36
44
|
setTimeout(() => {
|
|
37
|
-
aP.trackFeature(
|
|
45
|
+
aP.trackFeature('myFeature2');
|
|
38
46
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
39
47
|
}, 15000);
|
|
40
48
|
vi.runOnlyPendingTimers();
|
|
@@ -46,7 +54,6 @@ test('test_analytics_processor_flush_early_exit_if_analytics_data_is_empty', asy
|
|
|
46
54
|
expect(fetch).not.toHaveBeenCalled();
|
|
47
55
|
});
|
|
48
56
|
|
|
49
|
-
|
|
50
57
|
test('errors in fetch sending analytics data are swallowed', async () => {
|
|
51
58
|
// Given
|
|
52
59
|
// we mock the fetch function to throw and error to mimick a network failure
|
|
@@ -63,21 +70,18 @@ test('errors in fetch sending analytics data are swallowed', async () => {
|
|
|
63
70
|
// Then
|
|
64
71
|
// we expect that fetch was called but the exception was handled
|
|
65
72
|
expect(fetch).toHaveBeenCalled();
|
|
66
|
-
})
|
|
73
|
+
});
|
|
67
74
|
|
|
68
75
|
test('analytics is only flushed once even if requested concurrently', async () => {
|
|
69
76
|
const processor = analyticsProcessor();
|
|
70
77
|
processor.trackFeature('myFeature');
|
|
71
78
|
fetch.mockImplementation(() => {
|
|
72
79
|
return new Promise((resolve, _) => {
|
|
73
|
-
setTimeout(resolve, 1000)
|
|
74
|
-
})
|
|
80
|
+
setTimeout(resolve, 1000);
|
|
81
|
+
});
|
|
75
82
|
});
|
|
76
|
-
const flushes = Promise.all([
|
|
77
|
-
processor.flush(),
|
|
78
|
-
processor.flush(),
|
|
79
|
-
])
|
|
83
|
+
const flushes = Promise.all([processor.flush(), processor.flush()]);
|
|
80
84
|
vi.runOnlyPendingTimers();
|
|
81
85
|
await flushes;
|
|
82
|
-
expect(fetch).toHaveBeenCalledTimes(1)
|
|
83
|
-
})
|
|
86
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
@@ -1,113 +1,121 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
fetch,
|
|
3
|
+
environmentJSON,
|
|
4
|
+
environmentModel,
|
|
5
|
+
flagsJSON,
|
|
6
|
+
flagsmith,
|
|
7
|
+
identitiesJSON,
|
|
8
|
+
TestCache
|
|
9
|
+
} from './utils.js';
|
|
2
10
|
|
|
3
11
|
test('test_empty_cache_not_read_but_populated', async () => {
|
|
4
|
-
|
|
5
|
-
|
|
12
|
+
const cache = new TestCache();
|
|
13
|
+
const set = vi.spyOn(cache, 'set');
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
const flg = flagsmith({ cache });
|
|
16
|
+
const allFlags = (await flg.getEnvironmentFlags()).allFlags();
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
expect(set).toBeCalled();
|
|
19
|
+
expect(await cache.get('flags')).toBeTruthy();
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
expect(fetch).toBeCalledTimes(1);
|
|
22
|
+
expect(allFlags[0].enabled).toBe(true);
|
|
23
|
+
expect(allFlags[0].value).toBe('some-value');
|
|
24
|
+
expect(allFlags[0].featureName).toBe('some_feature');
|
|
17
25
|
});
|
|
18
26
|
|
|
19
27
|
test('test_api_not_called_when_cache_present', async () => {
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
const cache = new TestCache();
|
|
29
|
+
const set = vi.spyOn(cache, 'set');
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
const flg = flagsmith({ cache });
|
|
32
|
+
await (await flg.getEnvironmentFlags()).allFlags();
|
|
33
|
+
const allFlags = await (await flg.getEnvironmentFlags()).allFlags();
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
expect(set).toBeCalled();
|
|
36
|
+
expect(await cache.get('flags')).toBeTruthy();
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
expect(fetch).toBeCalledTimes(1);
|
|
39
|
+
expect(allFlags[0].enabled).toBe(true);
|
|
40
|
+
expect(allFlags[0].value).toBe('some-value');
|
|
41
|
+
expect(allFlags[0].featureName).toBe('some_feature');
|
|
34
42
|
});
|
|
35
43
|
|
|
36
44
|
test('test_api_called_twice_when_no_cache', async () => {
|
|
37
|
-
|
|
45
|
+
fetch.mockImplementation(() => Promise.resolve(new Response(flagsJSON)));
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const flg = flagsmith();
|
|
48
|
+
await (await flg.getEnvironmentFlags()).allFlags();
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
const allFlags = await (await flg.getEnvironmentFlags()).allFlags();
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
expect(fetch).toBeCalledTimes(2);
|
|
53
|
+
expect(allFlags[0].enabled).toBe(true);
|
|
54
|
+
expect(allFlags[0].value).toBe('some-value');
|
|
55
|
+
expect(allFlags[0].featureName).toBe('some_feature');
|
|
48
56
|
});
|
|
49
57
|
|
|
50
58
|
test('test_get_environment_flags_uses_local_environment_when_available', async () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
const cache = new TestCache();
|
|
60
|
+
const set = vi.spyOn(cache, 'set');
|
|
61
|
+
|
|
62
|
+
const flg = flagsmith({ cache, environmentKey: 'ser.key', enableLocalEvaluation: true });
|
|
63
|
+
const model = environmentModel(JSON.parse(environmentJSON));
|
|
64
|
+
const getEnvironment = vi.spyOn(flg, 'getEnvironment');
|
|
65
|
+
getEnvironment.mockResolvedValue(model);
|
|
66
|
+
|
|
67
|
+
const allFlags = (await flg.getEnvironmentFlags()).allFlags();
|
|
68
|
+
|
|
69
|
+
expect(set).toBeCalled();
|
|
70
|
+
expect(fetch).toBeCalledTimes(0);
|
|
71
|
+
expect(getEnvironment).toBeCalledTimes(1);
|
|
72
|
+
expect(allFlags[0].enabled).toBe(model.featureStates[0].enabled);
|
|
73
|
+
expect(allFlags[0].value).toBe(model.featureStates[0].getValue());
|
|
74
|
+
expect(allFlags[0].featureName).toBe(model.featureStates[0].feature.name);
|
|
67
75
|
});
|
|
68
76
|
|
|
69
77
|
test('test_cache_used_for_identity_flags', async () => {
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
const cache = new TestCache();
|
|
79
|
+
const set = vi.spyOn(cache, 'set');
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
const identifier = 'identifier';
|
|
82
|
+
const traits = { some_trait: 'some_value' };
|
|
83
|
+
const flg = flagsmith({ cache });
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
(await flg.getIdentityFlags(identifier, traits)).allFlags();
|
|
86
|
+
const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags();
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
expect(set).toBeCalled();
|
|
89
|
+
expect(await cache.get('flags-identifier')).toBeTruthy();
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
expect(fetch).toBeCalledTimes(1);
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
expect(identityFlags[0].enabled).toBe(true);
|
|
94
|
+
expect(identityFlags[0].value).toBe('some-value');
|
|
95
|
+
expect(identityFlags[0].featureName).toBe('some_feature');
|
|
88
96
|
});
|
|
89
97
|
|
|
90
98
|
test('test_cache_used_for_identity_flags_local_evaluation', async () => {
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
const cache = new TestCache();
|
|
100
|
+
const set = vi.spyOn(cache, 'set');
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
const identifier = 'identifier';
|
|
103
|
+
const traits = { some_trait: 'some_value' };
|
|
104
|
+
const flg = flagsmith({
|
|
105
|
+
cache,
|
|
106
|
+
environmentKey: 'ser.key',
|
|
107
|
+
enableLocalEvaluation: true
|
|
108
|
+
});
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
(await flg.getIdentityFlags(identifier, traits)).allFlags();
|
|
111
|
+
const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags();
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
expect(set).toBeCalled();
|
|
114
|
+
expect(await cache.get('flags-identifier')).toBeTruthy();
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
expect(fetch).toBeCalledTimes(1);
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
expect(identityFlags[0].enabled).toBe(true);
|
|
119
|
+
expect(identityFlags[0].value).toBe('some-value');
|
|
120
|
+
expect(identityFlags[0].featureName).toBe('some_feature');
|
|
113
121
|
});
|