Skip to content

Commit 92b613b

Browse files
Merge pull request #450 from splitio/fme-10825
add fallbackTreatments to shared settings
2 parents 1ded639 + 1f02570 commit 92b613b

File tree

11 files changed

+136
-173
lines changed

11 files changed

+136
-173
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ jobs:
2424
- name: Set up nodejs
2525
uses: actions/setup-node@v4
2626
with:
27-
node-version: 'lts/*'
27+
# @TODO: rollback to 'lts/*'
28+
node-version: '22'
2829
cache: 'npm'
2930

3031
- name: npm CI

CHANGES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
2.8.0 (October 28, 2025)
1+
2.8.0 (October 30, 2025)
22
- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs.
33
- Added `client.getStatus()` method to retrieve the client readiness status properties (`isReady`, `isReadyFromCache`, etc).
44
- Added `client.whenReady()` and `client.whenReadyFromCache()` methods to replace the deprecated `client.ready()` method, which has an issue causing the returned promise to hang when using async/await syntax if it was rejected.

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

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,15 @@
11
import { FallbackTreatmentsCalculator } from '../';
22
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio';
3-
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
43
import { CONTROL } from '../../../utils/constants';
54

65
describe('FallbackTreatmentsCalculator' , () => {
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(loggerMock, config);
16-
expect(loggerMock.error.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(loggerMock, config);
25-
expect(loggerMock.error.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(loggerMock, config);
35-
expect(loggerMock.error.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(loggerMock, config);
46-
expect(loggerMock.error.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(loggerMock, config);
57-
expect(loggerMock.error.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(loggerMock, config);
67-
expect(loggerMock.error.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(loggerMock, config);
76-
expect(loggerMock.error.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(loggerMock, config);
85-
expect(loggerMock.error.mock.calls[4][0]).toBe(
86-
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
87-
);
88-
});
89-
906
test('returns specific fallback if flag exists', () => {
917
const config: FallbackTreatmentConfiguration = {
928
byFlag: {
939
'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
9410
},
9511
};
96-
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
12+
const calculator = new FallbackTreatmentsCalculator(config);
9713
const result = calculator.resolve('featureA', 'label by flag');
9814

9915
expect(result).toEqual({
@@ -108,7 +24,7 @@ describe('FallbackTreatmentsCalculator' , () => {
10824
byFlag: {},
10925
global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' },
11026
};
111-
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
27+
const calculator = new FallbackTreatmentsCalculator(config);
11228
const result = calculator.resolve('missingFlag', 'label by global');
11329

11430
expect(result).toEqual({
@@ -122,7 +38,7 @@ describe('FallbackTreatmentsCalculator' , () => {
12238
const config: FallbackTreatmentConfiguration = {
12339
byFlag: {},
12440
};
125-
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
41+
const calculator = new FallbackTreatmentsCalculator(config);
12642
const result = calculator.resolve('missingFlag', 'label by noFallback');
12743

12844
expect(result).toEqual({
Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,70 @@
1-
import { FallbacksSanitizer } from '../fallbackSanitizer';
1+
import { isValidFlagName, isValidTreatment, sanitizeFallbacks } from '../fallbackSanitizer';
22
import { TreatmentWithConfig } from '../../../../types/splitio';
33
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
44

55
describe('FallbacksSanitizer', () => {
66
const validTreatment: TreatmentWithConfig = { treatment: 'on', config: '{"color":"blue"}' };
77
const invalidTreatment: TreatmentWithConfig = { treatment: ' ', config: null };
8+
const fallbackMock = {
9+
global: undefined,
10+
byFlag: {}
11+
};
812

913
beforeEach(() => {
10-
jest.spyOn(console, 'error').mockImplementation(() => {});
11-
});
12-
13-
afterEach(() => {
14-
(loggerMock.error as jest.Mock).mockRestore();
14+
loggerMock.mockClear();
1515
});
1616

1717
describe('isValidFlagName', () => {
1818
test('returns true for a valid flag name', () => {
1919
// @ts-expect-private-access
20-
expect((FallbacksSanitizer as any).isValidFlagName('my_flag')).toBe(true);
20+
expect(isValidFlagName('my_flag')).toBe(true);
2121
});
2222

2323
test('returns false for a name longer than 100 chars', () => {
2424
const longName = 'a'.repeat(101);
25-
expect((FallbacksSanitizer as any).isValidFlagName(longName)).toBe(false);
25+
expect(isValidFlagName(longName)).toBe(false);
26+
});
27+
28+
test('returns false if the name contains spaces', () => {
29+
expect(isValidFlagName('invalid flag')).toBe(false);
2630
});
2731

2832
test('returns false if the name contains spaces', () => {
29-
expect((FallbacksSanitizer as any).isValidFlagName('invalid flag')).toBe(false);
33+
// @ts-ignore
34+
expect(isValidFlagName(true)).toBe(false);
3035
});
3136
});
3237

3338
describe('isValidTreatment', () => {
3439
test('returns true for a valid treatment string', () => {
35-
expect((FallbacksSanitizer as any).isValidTreatment(validTreatment)).toBe(true);
40+
expect(isValidTreatment(validTreatment)).toBe(true);
3641
});
3742

3843
test('returns false for null or undefined', () => {
39-
expect((FallbacksSanitizer as any).isValidTreatment(null)).toBe(false);
40-
expect((FallbacksSanitizer as any).isValidTreatment(undefined)).toBe(false);
44+
expect(isValidTreatment()).toBe(false);
45+
expect(isValidTreatment(undefined)).toBe(false);
4146
});
4247

4348
test('returns false for a treatment longer than 100 chars', () => {
44-
const long = { treatment: 'a'.repeat(101) };
45-
expect((FallbacksSanitizer as any).isValidTreatment(long)).toBe(false);
49+
const long = { treatment: 'a'.repeat(101), config: null };
50+
expect(isValidTreatment(long)).toBe(false);
4651
});
4752

4853
test('returns false if treatment does not match regex pattern', () => {
49-
const invalid = { treatment: 'invalid treatment!' };
50-
expect((FallbacksSanitizer as any).isValidTreatment(invalid)).toBe(false);
54+
const invalid = { treatment: 'invalid treatment!', config: null };
55+
expect(isValidTreatment(invalid)).toBe(false);
5156
});
5257
});
5358

5459
describe('sanitizeGlobal', () => {
5560
test('returns the treatment if valid', () => {
56-
expect(FallbacksSanitizer.sanitizeGlobal(loggerMock, validTreatment)).toEqual(validTreatment);
61+
expect(sanitizeFallbacks(loggerMock, { ...fallbackMock, global: validTreatment })).toEqual({ ...fallbackMock, global: validTreatment });
5762
expect(loggerMock.error).not.toHaveBeenCalled();
5863
});
5964

6065
test('returns undefined and logs error if invalid', () => {
61-
const result = FallbacksSanitizer.sanitizeGlobal(loggerMock, invalidTreatment);
62-
expect(result).toBeUndefined();
66+
const result = sanitizeFallbacks(loggerMock, { ...fallbackMock, global: invalidTreatment });
67+
expect(result).toEqual(fallbackMock);
6368
expect(loggerMock.error).toHaveBeenCalledWith(
6469
expect.stringContaining('Fallback treatments - Discarded fallback')
6570
);
@@ -74,9 +79,9 @@ describe('FallbacksSanitizer', () => {
7479
bad_treatment: invalidTreatment,
7580
};
7681

77-
const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input);
82+
const result = sanitizeFallbacks(loggerMock, {...fallbackMock, byFlag: input});
7883

79-
expect(result).toEqual({ valid_flag: validTreatment });
84+
expect(result).toEqual({ ...fallbackMock, byFlag: { valid_flag: validTreatment } });
8085
expect(loggerMock.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
8186
});
8287

@@ -85,20 +90,46 @@ describe('FallbacksSanitizer', () => {
8590
'invalid flag': invalidTreatment,
8691
};
8792

88-
const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input);
89-
expect(result).toEqual({});
93+
const result = sanitizeFallbacks(loggerMock, {...fallbackMock, byFlag: input});
94+
expect(result).toEqual(fallbackMock);
9095
expect(loggerMock.error).toHaveBeenCalled();
9196
});
9297

9398
test('returns same object if all valid', () => {
9499
const input = {
95-
flag_one: validTreatment,
96-
flag_two: { treatment: 'valid_2', config: null },
100+
...fallbackMock,
101+
byFlag:{
102+
flag_one: validTreatment,
103+
flag_two: { treatment: 'valid_2', config: null },
104+
}
97105
};
98106

99-
const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input);
107+
const result = sanitizeFallbacks(loggerMock, input);
100108
expect(result).toEqual(input);
101109
expect(loggerMock.error).not.toHaveBeenCalled();
102110
});
103111
});
112+
113+
describe('sanitizeFallbacks', () => {
114+
test('returns undefined and logs error if fallbacks is not an object', () => { // @ts-expect-error
115+
const result = sanitizeFallbacks(loggerMock, 'invalid_fallbacks');
116+
expect(result).toBeUndefined();
117+
expect(loggerMock.error).toHaveBeenCalledWith(
118+
'Fallback treatments - Discarded configuration: it must be an object with optional `global` and `byFlag` properties'
119+
);
120+
});
121+
122+
test('returns undefined and logs error if fallbacks is not an object', () => { // @ts-expect-error
123+
const result = sanitizeFallbacks(loggerMock, true);
124+
expect(result).toBeUndefined();
125+
expect(loggerMock.error).toHaveBeenCalledWith(
126+
'Fallback treatments - Discarded configuration: it must be an object with optional `global` and `byFlag` properties'
127+
);
128+
});
129+
130+
test('sanitizes both global and byFlag fallbacks for empty object', () => { // @ts-expect-error
131+
const result = sanitizeFallbacks(loggerMock, { global: {} });
132+
expect(result).toEqual({ global: undefined, byFlag: {} });
133+
});
134+
});
104135
});

0 commit comments

Comments
 (0)