Skip to content

Commit 70f4a23

Browse files
[FME-10568] Add fallbackTreatmentCalculator to evaluator
1 parent 381b458 commit 70f4a23

File tree

17 files changed

+216
-67
lines changed

17 files changed

+216
-67
lines changed

src/evaluator/Engine.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types';
1111
import { ILogger } from '../logger/types';
1212
import { ENGINE_DEFAULT } from '../logger/constants';
1313
import { prerequisitesMatcherContext } from './matchers/prerequisites';
14+
import { FallbackTreatmentsCalculator } from './fallbackTreatmentsCalculator';
1415

1516
function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult {
1617
return {
@@ -19,13 +20,13 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str
1920
};
2021
}
2122

22-
export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) {
23+
export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator) {
2324
const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions, prerequisites } = split;
2425

2526
const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL;
2627

2728
const evaluator = parser(log, conditions, storage);
28-
const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log);
29+
const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log, fallbackTreatmentsCalculator);
2930

3031
return {
3132

src/evaluator/__tests__/evaluate-feature.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { evaluateFeature } from '../index';
33
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
44
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
5+
import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator';
56

67
const splitsMock = {
78
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': '', 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': '', 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
@@ -25,6 +26,8 @@ const mockStorage = {
2526
}
2627
};
2728

29+
const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator();
30+
2831
test('EVALUATOR / should return label exception, treatment control and config null on error', async () => {
2932
const expectedOutput = {
3033
treatment: 'control',
@@ -37,6 +40,7 @@ test('EVALUATOR / should return label exception, treatment control and config nu
3740
'throw_exception',
3841
null,
3942
mockStorage,
43+
fallbackTreatmentsCalculator
4044
);
4145

4246
// This validation is async because the only exception possible when retrieving a Split would happen with Async storages.
@@ -61,6 +65,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
6165
'config',
6266
null,
6367
mockStorage,
68+
fallbackTreatmentsCalculator
6469
);
6570
expect(evaluationWithConfig).toEqual(expectedOutput); // If the split is retrieved successfully we should get the right evaluation result, label and config.
6671

@@ -70,6 +75,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
7075
'not_existent_split',
7176
null,
7277
mockStorage,
78+
fallbackTreatmentsCalculator
7379
);
7480
expect(evaluationNotFound).toEqual(expectedOutputControl); // If the split is not retrieved successfully because it does not exist, we should get the right evaluation result, label and config.
7581

@@ -79,6 +85,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
7985
'regular',
8086
null,
8187
mockStorage,
88+
fallbackTreatmentsCalculator
8289
);
8390
expect(evaluation).toEqual({ ...expectedOutput, config: null }); // If the split is retrieved successfully we should get the right evaluation result, label and config. If Split has no config it should have config equal null.
8491

@@ -88,6 +95,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
8895
'killed',
8996
null,
9097
mockStorage,
98+
fallbackTreatmentsCalculator
9199
);
92100
expect(evaluationKilled).toEqual({ ...expectedOutput, treatment: 'off', config: null, label: SPLIT_KILLED });
93101
// If the split is retrieved but is killed, we should get the right evaluation result, label and config.
@@ -98,6 +106,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
98106
'archived',
99107
null,
100108
mockStorage,
109+
fallbackTreatmentsCalculator
101110
);
102111
expect(evaluationArchived).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, config: null });
103112
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.
@@ -108,6 +117,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
108117
'trafficAlocation1',
109118
null,
110119
mockStorage,
120+
fallbackTreatmentsCalculator
111121
);
112122
expect(evaluationtrafficAlocation1).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, config: null, treatment: 'off' });
113123
// If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config.
@@ -118,6 +128,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
118128
'killedWithConfig',
119129
null,
120130
mockStorage,
131+
fallbackTreatmentsCalculator
121132
);
122133
expect(evaluationKilledWithConfig).toEqual({ ...expectedOutput, treatment: 'off', label: SPLIT_KILLED });
123134
// If the split is retrieved but is killed, we should get the right evaluation result, label and config.
@@ -128,6 +139,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
128139
'archivedWithConfig',
129140
null,
130141
mockStorage,
142+
fallbackTreatmentsCalculator
131143
);
132144
expect(evaluationArchivedWithConfig).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, config: null });
133145
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.
@@ -138,6 +150,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
138150
'trafficAlocation1WithConfig',
139151
null,
140152
mockStorage,
153+
fallbackTreatmentsCalculator
141154
);
142155
expect(evaluationtrafficAlocation1WithConfig).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, treatment: 'off' });
143156
// If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config.

src/evaluator/__tests__/evaluate-features.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index';
33
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
44
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
55
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants';
6+
import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator';
67

78
const splitsMock = {
89
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': '', 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': '', 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
@@ -42,6 +43,8 @@ const mockStorage = {
4243
}
4344
};
4445

46+
const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator({});
47+
4548
test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async () => {
4649
const expectedOutput = {
4750
throw_exception: {
@@ -82,6 +85,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre
8285
['config', 'not_existent_split', 'regular', 'killed', 'archived', 'trafficAlocation1', 'killedWithConfig', 'archivedWithConfig', 'trafficAlocation1WithConfig'],
8386
null,
8487
mockStorage,
88+
fallbackTreatmentsCalculator
8589
);
8690
// assert evaluationWithConfig
8791
expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config.
@@ -134,7 +138,8 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => {
134138
flagSets,
135139
null,
136140
storage,
137-
'method-name'
141+
'method-name',
142+
fallbackTreatmentsCalculator
138143
);
139144
};
140145

src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,91 @@
11
import { FallbackTreatmentsCalculator } from '../';
2-
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; // adjust path if needed
2+
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio';
3+
import { CONTROL } from '../../../utils/constants';
34

45
describe('FallbackTreatmentsCalculator', () => {
6+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
7+
const longName = 'a'.repeat(101);
8+
9+
test('logs an error if flag name is invalid - by Flag', () => {
10+
let config: FallbackTreatmentConfiguration = {
11+
byFlag: {
12+
'feature A': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
13+
},
14+
};
15+
new FallbackTreatmentsCalculator(config);
16+
expect(spy.mock.calls[0][0]).toBe(
17+
'Fallback treatments - Discarded flag \'feature A\': Invalid flag name (max 100 chars, no spaces)'
18+
);
19+
config = {
20+
byFlag: {
21+
[longName]: { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
22+
},
23+
};
24+
new FallbackTreatmentsCalculator(config);
25+
expect(spy.mock.calls[1][0]).toBe(
26+
`Fallback treatments - Discarded flag '${longName}': Invalid flag name (max 100 chars, no spaces)`
27+
);
28+
29+
config = {
30+
byFlag: {
31+
'featureB': { treatment: longName, config: '{ value: 1 }' },
32+
},
33+
};
34+
new FallbackTreatmentsCalculator(config);
35+
expect(spy.mock.calls[2][0]).toBe(
36+
'Fallback treatments - Discarded treatment for flag \'featureB\': Invalid treatment (max 100 chars and must match pattern)'
37+
);
38+
39+
config = {
40+
byFlag: {
41+
// @ts-ignore
42+
'featureC': { config: '{ global: true }' },
43+
},
44+
};
45+
new FallbackTreatmentsCalculator(config);
46+
expect(spy.mock.calls[3][0]).toBe(
47+
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
48+
);
49+
50+
config = {
51+
byFlag: {
52+
// @ts-ignore
53+
'featureC': { treatment: 'invalid treatment!', config: '{ global: true }' },
54+
},
55+
};
56+
new FallbackTreatmentsCalculator(config);
57+
expect(spy.mock.calls[4][0]).toBe(
58+
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
59+
);
60+
});
61+
62+
test('logs an error if flag name is invalid - global', () => {
63+
let config: FallbackTreatmentConfiguration = {
64+
global: { treatment: longName, config: '{ value: 1 }' },
65+
};
66+
new FallbackTreatmentsCalculator(config);
67+
expect(spy.mock.calls[2][0]).toBe(
68+
'Fallback treatments - Discarded treatment for flag \'featureB\': Invalid treatment (max 100 chars and must match pattern)'
69+
);
70+
71+
config = {
72+
// @ts-ignore
73+
global: { config: '{ global: true }' },
74+
};
75+
new FallbackTreatmentsCalculator(config);
76+
expect(spy.mock.calls[3][0]).toBe(
77+
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
78+
);
79+
80+
config = {
81+
// @ts-ignore
82+
global: { treatment: 'invalid treatment!', config: '{ global: true }' },
83+
};
84+
new FallbackTreatmentsCalculator(config);
85+
expect(spy.mock.calls[4][0]).toBe(
86+
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
87+
);
88+
});
589

690
test('returns specific fallback if flag exists', () => {
791
const config: FallbackTreatmentConfiguration = {
@@ -42,9 +126,9 @@ describe('FallbackTreatmentsCalculator', () => {
42126
const result = calculator.resolve('missingFlag', 'label by noFallback');
43127

44128
expect(result).toEqual({
45-
treatment: 'CONTROL',
129+
treatment: CONTROL,
46130
config: null,
47-
label: 'fallback - label by noFallback',
131+
label: 'label by noFallback',
48132
});
49133
});
50134

@@ -60,7 +144,7 @@ describe('FallbackTreatmentsCalculator', () => {
60144
expect(result).toEqual({
61145
treatment: 'TREATMENT_B',
62146
config: undefined,
63-
label: undefined,
147+
label: '',
64148
});
65149
});
66150
});

src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class FallbacksSanitizer {
1010
return name.length <= 100 && !name.includes(' ');
1111
}
1212

13-
private static isValidTreatment(t?: FallbackTreatment): boolean {
13+
private static isValidTreatment(t?: string | FallbackTreatment): boolean {
1414
if (!t) {
1515
return false;
1616
}
@@ -19,30 +19,30 @@ export class FallbacksSanitizer {
1919
if (t.length > 100) {
2020
return false;
2121
}
22-
return FallbacksSanitizer.pattern.test(t);
22+
return this.pattern.test(t);
2323
}
2424

2525
const { treatment } = t;
2626
if (!treatment || treatment.length > 100) {
2727
return false;
2828
}
29-
return FallbacksSanitizer.pattern.test(treatment);
29+
return this.pattern.test(treatment);
3030
}
3131

32-
static sanitizeGlobal(treatment?: FallbackTreatment): FallbackTreatment | undefined {
32+
static sanitizeGlobal(treatment?: string | FallbackTreatment): string | FallbackTreatment | undefined {
3333
if (!this.isValidTreatment(treatment)) {
3434
console.error(
3535
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
3636
);
3737
return undefined;
3838
}
39-
return treatment!;
39+
return treatment;
4040
}
4141

4242
static sanitizeByFlag(
43-
byFlagFallbacks: Record<string, FallbackTreatment>
44-
): Record<string, FallbackTreatment> {
45-
const sanitizedByFlag: Record<string, FallbackTreatment> = {};
43+
byFlagFallbacks: Record<string, string | FallbackTreatment>
44+
): Record<string, string | FallbackTreatment> {
45+
const sanitizedByFlag: Record<string, string | FallbackTreatment> = {};
4646

4747
const entries = Object.entries(byFlagFallbacks);
4848
entries.forEach(([flag, t]) => {

src/evaluator/fallbackTreatmentsCalculator/index.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { FallbackTreatmentConfiguration, FallbackTreatment, IFallbackTreatmentsCalculator} from '../../../types/splitio';
2+
import { FallbacksSanitizer } from './fallbackSanitizer';
3+
import { CONTROL } from '../../utils/constants';
4+
25

36
export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator {
47
private readonly labelPrefix = 'fallback - ';
5-
private readonly control = 'CONTROL';
68
private readonly fallbacks: FallbackTreatmentConfiguration;
79

8-
constructor(fallbacks: FallbackTreatmentConfiguration) {
9-
this.fallbacks = fallbacks;
10+
constructor(fallbacks?: FallbackTreatmentConfiguration) {
11+
const sanitizedGlobal = fallbacks?.global ? FallbacksSanitizer.sanitizeGlobal(fallbacks.global) : undefined;
12+
const sanitizedByFlag = fallbacks?.byFlag ? FallbacksSanitizer.sanitizeByFlag(fallbacks.byFlag) : {};
13+
this.fallbacks = {
14+
global: sanitizedGlobal,
15+
byFlag: sanitizedByFlag
16+
};
1017
}
1118

1219
resolve(flagName: string, label?: string | undefined): FallbackTreatment {
13-
const treatment = this.fallbacks.byFlag[flagName];
20+
const treatment = this.fallbacks.byFlag?.[flagName];
1421
if (treatment) {
1522
return this.copyWithLabel(treatment, label);
1623
}
@@ -20,13 +27,13 @@ export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculat
2027
}
2128

2229
return {
23-
treatment: this.control,
30+
treatment: CONTROL,
2431
config: null,
25-
label: this.resolveLabel(label),
32+
label,
2633
};
2734
}
2835

29-
private copyWithLabel(fallback: FallbackTreatment, label: string | undefined): FallbackTreatment {
36+
private copyWithLabel(fallback: string | FallbackTreatment, label: string | undefined): FallbackTreatment {
3037
if (typeof fallback === 'string') {
3138
return {
3239
treatment: fallback,
@@ -42,8 +49,8 @@ export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculat
4249
};
4350
}
4451

45-
private resolveLabel(label?: string | undefined): string | undefined {
46-
return label ? `${this.labelPrefix}${label}` : undefined;
52+
private resolveLabel(label?: string | undefined): string {
53+
return label ? `${this.labelPrefix}${label}` : '';
4754
}
4855

4956
}

0 commit comments

Comments
 (0)