Skip to content

Commit e39f22b

Browse files
Merge pull request #437 from splitio/fme-10504
Implement FallbackSanitizer and add fallback to config
2 parents dc05461 + 7371aad commit e39f22b

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { FallbacksSanitizer } from '../fallbackSanitizer';
2+
import { TreatmentWithConfig } from '../../../../types/splitio';
3+
4+
describe('FallbacksSanitizer', () => {
5+
const validTreatment: TreatmentWithConfig = { treatment: 'on', config: '{"color":"blue"}' };
6+
const invalidTreatment: TreatmentWithConfig = { treatment: ' ', config: null };
7+
8+
beforeEach(() => {
9+
jest.spyOn(console, 'error').mockImplementation(() => {});
10+
});
11+
12+
afterEach(() => {
13+
(console.error as jest.Mock).mockRestore();
14+
});
15+
16+
describe('isValidFlagName', () => {
17+
it('returns true for a valid flag name', () => {
18+
// @ts-expect-private-access
19+
expect((FallbacksSanitizer as any).isValidFlagName('my_flag')).toBe(true);
20+
});
21+
22+
it('returns false for a name longer than 100 chars', () => {
23+
const longName = 'a'.repeat(101);
24+
expect((FallbacksSanitizer as any).isValidFlagName(longName)).toBe(false);
25+
});
26+
27+
it('returns false if the name contains spaces', () => {
28+
expect((FallbacksSanitizer as any).isValidFlagName('invalid flag')).toBe(false);
29+
});
30+
});
31+
32+
describe('isValidTreatment', () => {
33+
it('returns true for a valid treatment string', () => {
34+
expect((FallbacksSanitizer as any).isValidTreatment(validTreatment)).toBe(true);
35+
});
36+
37+
it('returns false for null or undefined', () => {
38+
expect((FallbacksSanitizer as any).isValidTreatment(null)).toBe(false);
39+
expect((FallbacksSanitizer as any).isValidTreatment(undefined)).toBe(false);
40+
});
41+
42+
it('returns false for a treatment longer than 100 chars', () => {
43+
const long = { treatment: 'a'.repeat(101) };
44+
expect((FallbacksSanitizer as any).isValidTreatment(long)).toBe(false);
45+
});
46+
47+
it('returns false if treatment does not match regex pattern', () => {
48+
const invalid = { treatment: 'invalid treatment!' };
49+
expect((FallbacksSanitizer as any).isValidTreatment(invalid)).toBe(false);
50+
});
51+
});
52+
53+
describe('sanitizeGlobal', () => {
54+
it('returns the treatment if valid', () => {
55+
expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment);
56+
expect(console.error).not.toHaveBeenCalled();
57+
});
58+
59+
it('returns undefined and logs error if invalid', () => {
60+
const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment);
61+
expect(result).toBeUndefined();
62+
expect(console.error).toHaveBeenCalledWith(
63+
expect.stringContaining('Fallback treatments - Discarded fallback')
64+
);
65+
});
66+
});
67+
68+
describe('sanitizeByFlag', () => {
69+
it('returns a sanitized map with valid entries only', () => {
70+
const input = {
71+
valid_flag: validTreatment,
72+
'invalid flag': validTreatment,
73+
bad_treatment: invalidTreatment,
74+
};
75+
76+
const result = FallbacksSanitizer.sanitizeByFlag(input);
77+
78+
expect(result).toEqual({ valid_flag: validTreatment });
79+
expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
80+
});
81+
82+
it('returns empty object if all invalid', () => {
83+
const input = {
84+
'invalid flag': invalidTreatment,
85+
};
86+
87+
const result = FallbacksSanitizer.sanitizeByFlag(input);
88+
expect(result).toEqual({});
89+
expect(console.error).toHaveBeenCalled();
90+
});
91+
92+
it('returns same object if all valid', () => {
93+
const input = {
94+
flag_one: validTreatment,
95+
flag_two: { treatment: 'valid_2', config: null },
96+
};
97+
98+
const result = FallbacksSanitizer.sanitizeByFlag(input);
99+
expect(result).toEqual(input);
100+
expect(console.error).not.toHaveBeenCalled();
101+
});
102+
});
103+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum FallbackDiscardReason {
2+
FlagName = 'Invalid flag name (max 100 chars, no spaces)',
3+
Treatment = 'Invalid treatment (max 100 chars and must match pattern)',
4+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { TreatmentWithConfig } from '../../../../types/splitio';
2+
import { FallbackDiscardReason } from '../constants';
3+
4+
5+
export class FallbacksSanitizer {
6+
7+
private static readonly pattern = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/;
8+
9+
private static isValidFlagName(name: string): boolean {
10+
return name.length <= 100 && !name.includes(' ');
11+
}
12+
13+
private static isValidTreatment(t?: TreatmentWithConfig): boolean {
14+
if (!t || !t.treatment) {
15+
return false;
16+
}
17+
18+
const { treatment } = t;
19+
20+
if (treatment.length > 100) {
21+
return false;
22+
}
23+
24+
return FallbacksSanitizer.pattern.test(treatment);
25+
}
26+
27+
static sanitizeGlobal(treatment?: TreatmentWithConfig): TreatmentWithConfig | undefined {
28+
if (!this.isValidTreatment(treatment)) {
29+
console.error(
30+
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
31+
);
32+
return undefined;
33+
}
34+
return treatment!;
35+
}
36+
37+
static sanitizeByFlag(
38+
byFlagFallbacks: Record<string, TreatmentWithConfig>
39+
): Record<string, TreatmentWithConfig> {
40+
const sanitizedByFlag: Record<string, TreatmentWithConfig> = {};
41+
42+
const entries = Object.entries(byFlagFallbacks);
43+
entries.forEach(([flag, t]) => {
44+
if (!this.isValidFlagName(flag)) {
45+
console.error(
46+
`Fallback treatments - Discarded flag '${flag}': ${FallbackDiscardReason.FlagName}`
47+
);
48+
return;
49+
}
50+
51+
if (!this.isValidTreatment(t)) {
52+
console.error(
53+
`Fallback treatments - Discarded treatment for flag '${flag}': ${FallbackDiscardReason.Treatment}`
54+
);
55+
return;
56+
}
57+
58+
sanitizedByFlag[flag] = t;
59+
});
60+
61+
return sanitizedByFlag;
62+
}
63+
}

types/splitio.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,10 @@ declare namespace SplitIO {
621621
* User consent status if using in client-side. Undefined if using in server-side (Node.js).
622622
*/
623623
readonly userConsent?: ConsentStatus;
624+
/**
625+
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
626+
*/
627+
readonly fallbackTreatments?: FallbackTreatmentOptions;
624628
}
625629
/**
626630
* Log levels.
@@ -1228,6 +1232,15 @@ declare namespace SplitIO {
12281232
* User consent status.
12291233
*/
12301234
type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
1235+
/**
1236+
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
1237+
*/
1238+
type FallbackTreatmentOptions = {
1239+
global?: TreatmentWithConfig | Treatment,
1240+
byFlag: {
1241+
[key: string]: TreatmentWithConfig | Treatment
1242+
}
1243+
}
12311244
/**
12321245
* Logger. Its interface details are not part of the public API. It shouldn't be used directly.
12331246
*/

0 commit comments

Comments
 (0)