Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { FallbackTreatmentsCalculator } from '../';
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; // adjust path if needed

describe('FallbackTreatmentsCalculator', () => {

test('returns specific fallback if flag exists', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {
'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
},
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('featureA', 'label by flag');

expect(result).toEqual({
treatment: 'TREATMENT_A',
config: '{ value: 1 }',
label: 'fallback - label by flag',
});
});

test('returns global fallback if flag is missing and global exists', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {},
global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' },
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('missingFlag', 'label by global');

expect(result).toEqual({
treatment: 'GLOBAL_TREATMENT',
config: '{ global: true }',
label: 'fallback - label by global',
});
});

test('returns control fallback if flag and global are missing', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {},
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('missingFlag', 'label by noFallback');

expect(result).toEqual({
treatment: 'CONTROL',
config: null,
label: 'fallback - label by noFallback',
});
});

test('returns undefined label if no label provided', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {
'featureB': { treatment: 'TREATMENT_B' },
},
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('featureB');

expect(result).toEqual({
treatment: 'TREATMENT_B',
config: undefined,
label: undefined,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,49 @@ describe('FallbacksSanitizer', () => {
});

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

it('returns false for a name longer than 100 chars', () => {
test('returns false for a name longer than 100 chars', () => {
const longName = 'a'.repeat(101);
expect((FallbacksSanitizer as any).isValidFlagName(longName)).toBe(false);
});

it('returns false if the name contains spaces', () => {
test('returns false if the name contains spaces', () => {
expect((FallbacksSanitizer as any).isValidFlagName('invalid flag')).toBe(false);
});
});

describe('isValidTreatment', () => {
it('returns true for a valid treatment string', () => {
test('returns true for a valid treatment string', () => {
expect((FallbacksSanitizer as any).isValidTreatment(validTreatment)).toBe(true);
});

it('returns false for null or undefined', () => {
test('returns false for null or undefined', () => {
expect((FallbacksSanitizer as any).isValidTreatment(null)).toBe(false);
expect((FallbacksSanitizer as any).isValidTreatment(undefined)).toBe(false);
});

it('returns false for a treatment longer than 100 chars', () => {
test('returns false for a treatment longer than 100 chars', () => {
const long = { treatment: 'a'.repeat(101) };
expect((FallbacksSanitizer as any).isValidTreatment(long)).toBe(false);
});

it('returns false if treatment does not match regex pattern', () => {
test('returns false if treatment does not match regex pattern', () => {
const invalid = { treatment: 'invalid treatment!' };
expect((FallbacksSanitizer as any).isValidTreatment(invalid)).toBe(false);
});
});

describe('sanitizeGlobal', () => {
it('returns the treatment if valid', () => {
test('returns the treatment if valid', () => {
expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment);
expect(console.error).not.toHaveBeenCalled();
});

it('returns undefined and logs error if invalid', () => {
test('returns undefined and logs error if invalid', () => {
const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment);
expect(result).toBeUndefined();
expect(console.error).toHaveBeenCalledWith(
Expand All @@ -66,7 +66,7 @@ describe('FallbacksSanitizer', () => {
});

describe('sanitizeByFlag', () => {
it('returns a sanitized map with valid entries only', () => {
test('returns a sanitized map with valid entries only', () => {
const input = {
valid_flag: validTreatment,
'invalid flag': validTreatment,
Expand All @@ -79,7 +79,7 @@ describe('FallbacksSanitizer', () => {
expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
});

it('returns empty object if all invalid', () => {
test('returns empty object if all invalid', () => {
const input = {
'invalid flag': invalidTreatment,
};
Expand All @@ -89,7 +89,7 @@ describe('FallbacksSanitizer', () => {
expect(console.error).toHaveBeenCalled();
});

it('returns same object if all valid', () => {
test('returns same object if all valid', () => {
const input = {
flag_one: validTreatment,
flag_two: { treatment: 'valid_2', config: null },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TreatmentWithConfig } from '../../../../types/splitio';
import { FallbackTreatment } from '../../../../types/splitio';
import { FallbackDiscardReason } from '../constants';


Expand All @@ -10,21 +10,26 @@ export class FallbacksSanitizer {
return name.length <= 100 && !name.includes(' ');
}

private static isValidTreatment(t?: TreatmentWithConfig): boolean {
if (!t || !t.treatment) {
private static isValidTreatment(t?: FallbackTreatment): boolean {
if (!t) {
return false;
}

const { treatment } = t;
if (typeof t === 'string') {
if (t.length > 100) {
return false;
}
return FallbacksSanitizer.pattern.test(t);
}

if (treatment.length > 100) {
const { treatment } = t;
if (!treatment || treatment.length > 100) {
return false;
}

return FallbacksSanitizer.pattern.test(treatment);
}

static sanitizeGlobal(treatment?: TreatmentWithConfig): TreatmentWithConfig | undefined {
static sanitizeGlobal(treatment?: FallbackTreatment): FallbackTreatment | undefined {
if (!this.isValidTreatment(treatment)) {
console.error(
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
Expand All @@ -35,9 +40,9 @@ export class FallbacksSanitizer {
}

static sanitizeByFlag(
byFlagFallbacks: Record<string, TreatmentWithConfig>
): Record<string, TreatmentWithConfig> {
const sanitizedByFlag: Record<string, TreatmentWithConfig> = {};
byFlagFallbacks: Record<string, FallbackTreatment>
): Record<string, FallbackTreatment> {
const sanitizedByFlag: Record<string, FallbackTreatment> = {};

const entries = Object.entries(byFlagFallbacks);
entries.forEach(([flag, t]) => {
Expand Down
49 changes: 49 additions & 0 deletions src/evaluator/fallbackTreatmentsCalculator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FallbackTreatmentConfiguration, FallbackTreatment, IFallbackTreatmentsCalculator} from '../../../types/splitio';

export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator {
private readonly labelPrefix = 'fallback - ';
private readonly control = 'CONTROL';
private readonly fallbacks: FallbackTreatmentConfiguration;

constructor(fallbacks: FallbackTreatmentConfiguration) {
this.fallbacks = fallbacks;
}

resolve(flagName: string, label?: string | undefined): FallbackTreatment {
const treatment = this.fallbacks.byFlag[flagName];
if (treatment) {
return this.copyWithLabel(treatment, label);
}

if (this.fallbacks.global) {
return this.copyWithLabel(this.fallbacks.global, label);
}

return {
treatment: this.control,
config: null,
label: this.resolveLabel(label),
};
}

private copyWithLabel(fallback: FallbackTreatment, label: string | undefined): FallbackTreatment {
if (typeof fallback === 'string') {
return {
treatment: fallback,
config: null,
label: this.resolveLabel(label),
};
}

return {
treatment: fallback.treatment,
config: fallback.config,
label: this.resolveLabel(label),
};
}

private resolveLabel(label?: string | undefined): string | undefined {
return label ? `${this.labelPrefix}${label}` : undefined;
}

}
19 changes: 15 additions & 4 deletions types/splitio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ declare namespace SplitIO {
/**
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
*/
readonly fallbackTreatments?: FallbackTreatmentOptions;
readonly fallbackTreatments?: FallbackTreatmentConfiguration;
}
/**
* Log levels.
Expand Down Expand Up @@ -1232,15 +1232,26 @@ declare namespace SplitIO {
* User consent status.
*/
type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
/**
* Fallback treatment can be either a string (treatment) or an object with treatment, config and label.
*/
type FallbackTreatment = string | {
treatment: string;
config?: string | null;
label?: string | null;
};
/**
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
*/
type FallbackTreatmentOptions = {
global?: TreatmentWithConfig | Treatment,
type FallbackTreatmentConfiguration = {
global?: FallbackTreatment,
byFlag: {
[key: string]: TreatmentWithConfig | Treatment
[key: string]: FallbackTreatment
}
}
type IFallbackTreatmentsCalculator = {
resolve(flagName: string, label?: string | undefined): FallbackTreatment;
}
/**
* Logger. Its interface details are not part of the public API. It shouldn't be used directly.
*/
Expand Down