diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 4228316f..35c68c4c 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -11,6 +11,7 @@ import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; import { ENGINE_DEFAULT } from '../logger/constants'; import { prerequisitesMatcherContext } from './matchers/prerequisites'; +import { FallbackTreatmentsCalculator } from './fallbackTreatmentsCalculator'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { return { @@ -19,13 +20,13 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str }; } -export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) { +export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator) { const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions, prerequisites } = split; const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; const evaluator = parser(log, conditions, storage); - const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log); + const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log, fallbackTreatmentsCalculator); return { diff --git a/src/evaluator/__tests__/evaluate-feature.spec.ts b/src/evaluator/__tests__/evaluate-feature.spec.ts index ffda4687..c9f96590 100644 --- a/src/evaluator/__tests__/evaluate-feature.spec.ts +++ b/src/evaluator/__tests__/evaluate-feature.spec.ts @@ -2,6 +2,7 @@ import { evaluateFeature } from '../index'; import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; +import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator'; const splitsMock = { 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 = { } }; +const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(); + test('EVALUATOR / should return label exception, treatment control and config null on error', async () => { const expectedOutput = { treatment: 'control', @@ -37,6 +40,7 @@ test('EVALUATOR / should return label exception, treatment control and config nu 'throw_exception', null, mockStorage, + fallbackTreatmentsCalculator ); // 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 'config', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationWithConfig).toEqual(expectedOutput); // If the split is retrieved successfully we should get the right evaluation result, label and config. @@ -70,6 +75,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret 'not_existent_split', null, mockStorage, + fallbackTreatmentsCalculator ); 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. @@ -79,6 +85,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret 'regular', null, mockStorage, + fallbackTreatmentsCalculator ); 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. @@ -88,6 +95,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret 'killed', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationKilled).toEqual({ ...expectedOutput, treatment: 'off', config: null, label: SPLIT_KILLED }); // 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 'archived', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationArchived).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, config: null }); // 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 'trafficAlocation1', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationtrafficAlocation1).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, config: null, treatment: 'off' }); // 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 'killedWithConfig', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationKilledWithConfig).toEqual({ ...expectedOutput, treatment: 'off', label: SPLIT_KILLED }); // 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 'archivedWithConfig', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationArchivedWithConfig).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, config: null }); // 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 'trafficAlocation1WithConfig', null, mockStorage, + fallbackTreatmentsCalculator ); expect(evaluationtrafficAlocation1WithConfig).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, treatment: 'off' }); // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config. diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index e42fc6d3..8f33bf37 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -3,6 +3,7 @@ import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index'; import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants'; +import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator'; const splitsMock = { 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 = { } }; +const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator({}); + test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async () => { const expectedOutput = { throw_exception: { @@ -82,6 +85,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre ['config', 'not_existent_split', 'regular', 'killed', 'archived', 'trafficAlocation1', 'killedWithConfig', 'archivedWithConfig', 'trafficAlocation1WithConfig'], null, mockStorage, + fallbackTreatmentsCalculator ); // assert evaluationWithConfig 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', () => { flagSets, null, storage, - 'method-name' + 'method-name', + fallbackTreatmentsCalculator ); }; diff --git a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts index 786ecbe8..4a1e9d93 100644 --- a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts +++ b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-calculator.spec.ts @@ -1,7 +1,91 @@ import { FallbackTreatmentsCalculator } from '../'; -import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; // adjust path if needed +import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { CONTROL } from '../../../utils/constants'; -describe('FallbackTreatmentsCalculator', () => { +describe('FallbackTreatmentsCalculator' , () => { + const longName = 'a'.repeat(101); + + test('logs an error if flag name is invalid - by Flag', () => { + let config: FallbackTreatmentConfiguration = { + byFlag: { + 'feature A': { treatment: 'TREATMENT_A', config: '{ value: 1 }' }, + }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[0][0]).toBe( + 'Fallback treatments - Discarded flag \'feature A\': Invalid flag name (max 100 chars, no spaces)' + ); + config = { + byFlag: { + [longName]: { treatment: 'TREATMENT_A', config: '{ value: 1 }' }, + }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[1][0]).toBe( + `Fallback treatments - Discarded flag '${longName}': Invalid flag name (max 100 chars, no spaces)` + ); + + config = { + byFlag: { + 'featureB': { treatment: longName, config: '{ value: 1 }' }, + }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[2][0]).toBe( + 'Fallback treatments - Discarded treatment for flag \'featureB\': Invalid treatment (max 100 chars and must match pattern)' + ); + + config = { + byFlag: { + // @ts-ignore + 'featureC': { config: '{ global: true }' }, + }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[3][0]).toBe( + 'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)' + ); + + config = { + byFlag: { + // @ts-ignore + 'featureC': { treatment: 'invalid treatment!', config: '{ global: true }' }, + }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[4][0]).toBe( + 'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)' + ); + }); + + test('logs an error if flag name is invalid - global', () => { + let config: FallbackTreatmentConfiguration = { + global: { treatment: longName, config: '{ value: 1 }' }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[2][0]).toBe( + 'Fallback treatments - Discarded treatment for flag \'featureB\': Invalid treatment (max 100 chars and must match pattern)' + ); + + config = { + // @ts-ignore + global: { config: '{ global: true }' }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[3][0]).toBe( + 'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)' + ); + + config = { + // @ts-ignore + global: { treatment: 'invalid treatment!', config: '{ global: true }' }, + }; + new FallbackTreatmentsCalculator(loggerMock, config); + expect(loggerMock.error.mock.calls[4][0]).toBe( + 'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)' + ); + }); test('returns specific fallback if flag exists', () => { const config: FallbackTreatmentConfiguration = { @@ -9,7 +93,7 @@ describe('FallbackTreatmentsCalculator', () => { 'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' }, }, }; - const calculator = new FallbackTreatmentsCalculator(config); + const calculator = new FallbackTreatmentsCalculator(loggerMock, config); const result = calculator.resolve('featureA', 'label by flag'); expect(result).toEqual({ @@ -24,7 +108,7 @@ describe('FallbackTreatmentsCalculator', () => { byFlag: {}, global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' }, }; - const calculator = new FallbackTreatmentsCalculator(config); + const calculator = new FallbackTreatmentsCalculator(loggerMock, config); const result = calculator.resolve('missingFlag', 'label by global'); expect(result).toEqual({ @@ -38,29 +122,29 @@ describe('FallbackTreatmentsCalculator', () => { const config: FallbackTreatmentConfiguration = { byFlag: {}, }; - const calculator = new FallbackTreatmentsCalculator(config); + const calculator = new FallbackTreatmentsCalculator(loggerMock, config); const result = calculator.resolve('missingFlag', 'label by noFallback'); expect(result).toEqual({ - treatment: 'CONTROL', + treatment: CONTROL, config: null, - label: 'fallback - label by noFallback', + label: 'label by noFallback', }); }); test('returns undefined label if no label provided', () => { const config: FallbackTreatmentConfiguration = { byFlag: { - 'featureB': { treatment: 'TREATMENT_B' }, + 'featureB': { treatment: 'TREATMENT_B', config: '{ value: 1 }' }, }, }; - const calculator = new FallbackTreatmentsCalculator(config); + const calculator = new FallbackTreatmentsCalculator(loggerMock, config); const result = calculator.resolve('featureB'); expect(result).toEqual({ treatment: 'TREATMENT_B', - config: undefined, - label: undefined, + config: '{ value: 1 }', + label: '', }); }); }); diff --git a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts index 0f314e2a..aaaf106c 100644 --- a/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts +++ b/src/evaluator/fallbackTreatmentsCalculator/__tests__/fallback-sanitizer.spec.ts @@ -1,5 +1,6 @@ import { FallbacksSanitizer } from '../fallbackSanitizer'; import { TreatmentWithConfig } from '../../../../types/splitio'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; describe('FallbacksSanitizer', () => { const validTreatment: TreatmentWithConfig = { treatment: 'on', config: '{"color":"blue"}' }; @@ -10,7 +11,7 @@ describe('FallbacksSanitizer', () => { }); afterEach(() => { - (console.error as jest.Mock).mockRestore(); + (loggerMock.error as jest.Mock).mockRestore(); }); describe('isValidFlagName', () => { @@ -52,14 +53,14 @@ describe('FallbacksSanitizer', () => { describe('sanitizeGlobal', () => { test('returns the treatment if valid', () => { - expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment); - expect(console.error).not.toHaveBeenCalled(); + expect(FallbacksSanitizer.sanitizeGlobal(loggerMock, validTreatment)).toEqual(validTreatment); + expect(loggerMock.error).not.toHaveBeenCalled(); }); test('returns undefined and logs error if invalid', () => { - const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment); + const result = FallbacksSanitizer.sanitizeGlobal(loggerMock, invalidTreatment); expect(result).toBeUndefined(); - expect(console.error).toHaveBeenCalledWith( + expect(loggerMock.error).toHaveBeenCalledWith( expect.stringContaining('Fallback treatments - Discarded fallback') ); }); @@ -73,10 +74,10 @@ describe('FallbacksSanitizer', () => { bad_treatment: invalidTreatment, }; - const result = FallbacksSanitizer.sanitizeByFlag(input); + const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input); expect(result).toEqual({ valid_flag: validTreatment }); - expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment + expect(loggerMock.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment }); test('returns empty object if all invalid', () => { @@ -84,9 +85,9 @@ describe('FallbacksSanitizer', () => { 'invalid flag': invalidTreatment, }; - const result = FallbacksSanitizer.sanitizeByFlag(input); + const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input); expect(result).toEqual({}); - expect(console.error).toHaveBeenCalled(); + expect(loggerMock.error).toHaveBeenCalled(); }); test('returns same object if all valid', () => { @@ -95,9 +96,9 @@ describe('FallbacksSanitizer', () => { flag_two: { treatment: 'valid_2', config: null }, }; - const result = FallbacksSanitizer.sanitizeByFlag(input); + const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input); expect(result).toEqual(input); - expect(console.error).not.toHaveBeenCalled(); + expect(loggerMock.error).not.toHaveBeenCalled(); }); }); }); diff --git a/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts b/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts index 06c7fe2e..d7996bbb 100644 --- a/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts +++ b/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts @@ -1,4 +1,6 @@ -import { FallbackTreatment } from '../../../../types/splitio'; +import { FallbackTreatment, Treatment, TreatmentWithConfig } from '../../../../types/splitio'; +import { ILogger } from '../../../logger/types'; +import { isObject, isString } from '../../../utils/lang'; import { FallbackDiscardReason } from '../constants'; @@ -10,51 +12,43 @@ export class FallbacksSanitizer { return name.length <= 100 && !name.includes(' '); } - private static isValidTreatment(t?: FallbackTreatment): boolean { - if (!t) { - return false; - } - - if (typeof t === 'string') { - if (t.length > 100) { - return false; - } - return FallbacksSanitizer.pattern.test(t); - } + private static isValidTreatment(t?: Treatment | FallbackTreatment): boolean { + const treatment = isObject(t) ? (t as TreatmentWithConfig).treatment : t; - const { treatment } = t; - if (!treatment || treatment.length > 100) { + if (!isString(treatment) || treatment.length > 100) { return false; } return FallbacksSanitizer.pattern.test(treatment); } - static sanitizeGlobal(treatment?: FallbackTreatment): FallbackTreatment | undefined { + static sanitizeGlobal(logger: ILogger, treatment?: string | FallbackTreatment): string | FallbackTreatment | undefined { if (!this.isValidTreatment(treatment)) { - console.error( + logger.error( `Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}` ); return undefined; } - return treatment!; + return treatment; } static sanitizeByFlag( - byFlagFallbacks: Record - ): Record { - const sanitizedByFlag: Record = {}; - - const entries = Object.entries(byFlagFallbacks); - entries.forEach(([flag, t]) => { + logger: ILogger, + byFlagFallbacks: Record + ): Record { + const sanitizedByFlag: Record = {}; + + const entries = Object.keys(byFlagFallbacks); + entries.forEach((flag) => { + const t = byFlagFallbacks[flag]; if (!this.isValidFlagName(flag)) { - console.error( + logger.error( `Fallback treatments - Discarded flag '${flag}': ${FallbackDiscardReason.FlagName}` ); return; } if (!this.isValidTreatment(t)) { - console.error( + logger.error( `Fallback treatments - Discarded treatment for flag '${flag}': ${FallbackDiscardReason.Treatment}` ); return; diff --git a/src/evaluator/fallbackTreatmentsCalculator/index.ts b/src/evaluator/fallbackTreatmentsCalculator/index.ts index 30ef69e2..5485c7c3 100644 --- a/src/evaluator/fallbackTreatmentsCalculator/index.ts +++ b/src/evaluator/fallbackTreatmentsCalculator/index.ts @@ -1,16 +1,28 @@ -import { FallbackTreatmentConfiguration, FallbackTreatment, IFallbackTreatmentsCalculator} from '../../../types/splitio'; +import { FallbackTreatmentConfiguration, FallbackTreatment } from '../../../types/splitio'; +import { FallbacksSanitizer } from './fallbackSanitizer'; +import { CONTROL } from '../../utils/constants'; +import { isString } from '../../utils/lang'; +import { ILogger } from '../../logger/types'; + +type IFallbackTreatmentsCalculator = { + resolve(flagName: string, label?: string): FallbackTreatment; +} export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator { private readonly labelPrefix = 'fallback - '; - private readonly control = 'CONTROL'; private readonly fallbacks: FallbackTreatmentConfiguration; - constructor(fallbacks: FallbackTreatmentConfiguration) { - this.fallbacks = fallbacks; + constructor(logger: ILogger, fallbacks?: FallbackTreatmentConfiguration) { + const sanitizedGlobal = fallbacks?.global ? FallbacksSanitizer.sanitizeGlobal(logger, fallbacks.global) : undefined; + const sanitizedByFlag = fallbacks?.byFlag ? FallbacksSanitizer.sanitizeByFlag(logger, fallbacks.byFlag) : {}; + this.fallbacks = { + global: sanitizedGlobal, + byFlag: sanitizedByFlag + }; } - resolve(flagName: string, label?: string | undefined): FallbackTreatment { - const treatment = this.fallbacks.byFlag[flagName]; + resolve(flagName: string, label?: string): FallbackTreatment & { label?: string } { + const treatment = this.fallbacks.byFlag?.[flagName]; if (treatment) { return this.copyWithLabel(treatment, label); } @@ -20,14 +32,14 @@ export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculat } return { - treatment: this.control, + treatment: CONTROL, config: null, - label: this.resolveLabel(label), + label, }; } - private copyWithLabel(fallback: FallbackTreatment, label: string | undefined): FallbackTreatment { - if (typeof fallback === 'string') { + private copyWithLabel(fallback: string | FallbackTreatment, label: string | undefined): FallbackTreatment & { label?: string } { + if (isString(fallback)) { return { treatment: fallback, config: null, @@ -42,8 +54,8 @@ export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculat }; } - private resolveLabel(label?: string | undefined): string | undefined { - return label ? `${this.labelPrefix}${label}` : undefined; + private resolveLabel(label?: string): string { + return label ? `${this.labelPrefix}${label}` : ''; } } diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index e465b9bf..3d979965 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -9,6 +9,7 @@ import SplitIO from '../../types/splitio'; import { ILogger } from '../logger/types'; import { returnSetsUnion, setToArray } from '../utils/lang/sets'; import { WARN_FLAGSET_WITHOUT_FLAGS } from '../logger/constants'; +import { FallbackTreatmentsCalculator } from './fallbackTreatmentsCalculator'; const treatmentException = { treatment: CONTROL, @@ -30,6 +31,7 @@ export function evaluateFeature( splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, + fallbackTreatmentsCalculator: FallbackTreatmentsCalculator, ): MaybeThenable { let parsedSplit; @@ -47,6 +49,7 @@ export function evaluateFeature( split, attributes, storage, + fallbackTreatmentsCalculator, )).catch( // Exception on async `getSplit` storage. For example, when the storage is redis or // pluggable and there is a connection issue and we can't retrieve the split to be evaluated @@ -60,6 +63,7 @@ export function evaluateFeature( parsedSplit, attributes, storage, + fallbackTreatmentsCalculator, ); } @@ -69,6 +73,7 @@ export function evaluateFeatures( splitNames: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, + fallbackTreatmentsCalculator: FallbackTreatmentsCalculator ): MaybeThenable> { let parsedSplits; @@ -80,13 +85,13 @@ export function evaluateFeatures( } return thenable(parsedSplits) ? - parsedSplits.then(splits => getEvaluations(log, key, splitNames, splits, attributes, storage)) + parsedSplits.then(splits => getEvaluations(log, key, splitNames, splits, attributes, storage, fallbackTreatmentsCalculator)) .catch(() => { // Exception on async `getSplits` storage. For example, when the storage is redis or // pluggable and there is a connection issue and we can't retrieve the split to be evaluated return treatmentsException(splitNames); }) : - getEvaluations(log, key, splitNames, parsedSplits, attributes, storage); + getEvaluations(log, key, splitNames, parsedSplits, attributes, storage, fallbackTreatmentsCalculator); } export function evaluateFeaturesByFlagSets( @@ -96,6 +101,7 @@ export function evaluateFeaturesByFlagSets( attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, method: string, + fallbackTreatmentsCalculator: FallbackTreatmentsCalculator ): MaybeThenable> { let storedFlagNames: MaybeThenable[]>; @@ -111,7 +117,7 @@ export function evaluateFeaturesByFlagSets( } return featureFlags.size ? - evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage) : + evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage, fallbackTreatmentsCalculator) : {}; } @@ -138,6 +144,7 @@ function getEvaluation( splitJSON: ISplit | null, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, + fallbackTreatmentsCalculator: FallbackTreatmentsCalculator ): MaybeThenable { let evaluation: MaybeThenable = { treatment: CONTROL, @@ -146,26 +153,40 @@ function getEvaluation( }; if (splitJSON) { - const split = engineParser(log, splitJSON, storage); + const split = engineParser(log, splitJSON, storage, fallbackTreatmentsCalculator); evaluation = split.getTreatment(key, attributes, evaluateFeature); // If the storage is async and the evaluated flag uses segments or dependencies, evaluation is thenable if (thenable(evaluation)) { return evaluation.then(result => { - result.changeNumber = splitJSON.changeNumber; - result.config = splitJSON.configurations && splitJSON.configurations[result.treatment] || null; - result.impressionsDisabled = splitJSON.impressionsDisabled; - - return result; + return buildEvaluation(result, splitJSON, fallbackTreatmentsCalculator); }); - } else { - evaluation.changeNumber = splitJSON.changeNumber; - evaluation.config = splitJSON.configurations && splitJSON.configurations[evaluation.treatment] || null; - evaluation.impressionsDisabled = splitJSON.impressionsDisabled; } } - return evaluation; + return buildEvaluation(evaluation, splitJSON, fallbackTreatmentsCalculator); +} + +function buildEvaluation(evaluation: IEvaluationResult, splitJSON: ISplit | null, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator): IEvaluationResult { + + const result: IEvaluationResult = { + treatment: evaluation.treatment, + label: evaluation.label, + config: evaluation.config + }; + + if (!splitJSON) return result; + + result.changeNumber = splitJSON.changeNumber; + result.config = splitJSON.configurations && splitJSON.configurations[evaluation.treatment] || null; + result.impressionsDisabled = splitJSON.impressionsDisabled; + if (evaluation.treatment === CONTROL) { + const fallbackTreatment = fallbackTreatmentsCalculator.resolve(splitJSON.name, evaluation.label); + result.treatment = fallbackTreatment.treatment; + result.label = fallbackTreatment.label ? fallbackTreatment.label : ''; + result.config = fallbackTreatment.config; + } + return result; } function getEvaluations( @@ -175,6 +196,7 @@ function getEvaluations( splits: Record, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, + fallbackTreatmentsCalculator: FallbackTreatmentsCalculator ): MaybeThenable> { const result: Record = {}; const thenables: Promise[] = []; @@ -184,7 +206,8 @@ function getEvaluations( key, splits[splitName], attributes, - storage + storage, + fallbackTreatmentsCalculator ); if (thenable(evaluation)) { thenables.push(evaluation.then(res => { diff --git a/src/evaluator/matchers/__tests__/prerequisites.spec.ts b/src/evaluator/matchers/__tests__/prerequisites.spec.ts index 82059fc9..0bc0234d 100644 --- a/src/evaluator/matchers/__tests__/prerequisites.spec.ts +++ b/src/evaluator/matchers/__tests__/prerequisites.spec.ts @@ -4,6 +4,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { ISplit } from '../../../dtos/types'; import { ALWAYS_ON_SPLIT, ALWAYS_OFF_SPLIT } from '../../../storages/__tests__/testUtils'; import { prerequisitesMatcherContext } from '../prerequisites'; +import { FallbackTreatmentsCalculator } from '../../fallbackTreatmentsCalculator'; const STORED_SPLITS: Record = { 'always-on': ALWAYS_ON_SPLIT, @@ -16,30 +17,32 @@ const mockStorage = { } } as IStorageSync; +const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(loggerMock); + test('MATCHER PREREQUISITES / should return true when all prerequisites are met', () => { // A single prerequisite const matcherTrueAlwaysOn = prerequisitesMatcherContext([{ n: 'always-on', ts: ['not-existing', 'on', 'other'] // We should match from a list of treatments - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true const matcherFalseAlwaysOn = prerequisitesMatcherContext([{ n: 'always-on', ts: ['off', 'v1'] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherFalseAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false const matcherTrueAlwaysOff = prerequisitesMatcherContext([{ n: 'always-off', ts: ['not-existing', 'off'] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherTrueAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true const matcherFalseAlwaysOff = prerequisitesMatcherContext([{ n: 'always-off', ts: ['v1', 'on'] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherFalseAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false // Multiple prerequisites @@ -52,7 +55,7 @@ test('MATCHER PREREQUISITES / should return true when all prerequisites are met' n: 'always-off', ts: ['off'] } - ], mockStorage, loggerMock); + ], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherTrueMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); // All prerequisites are met, so the matcher returns true const matcherFalseMultiplePrerequisites = prerequisitesMatcherContext([ @@ -64,23 +67,23 @@ test('MATCHER PREREQUISITES / should return true when all prerequisites are met' n: 'always-off', ts: ['on'] } - ], mockStorage, loggerMock); + ], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherFalseMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(false); // One of the prerequisites is not met, so the matcher returns false }); test('MATCHER PREREQUISITES / Edge cases', () => { // No prerequisites - const matcherTrueNoPrerequisites = prerequisitesMatcherContext(undefined, mockStorage, loggerMock); + const matcherTrueNoPrerequisites = prerequisitesMatcherContext(undefined, mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherTrueNoPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); - const matcherTrueEmptyPrerequisites = prerequisitesMatcherContext([], mockStorage, loggerMock); + const matcherTrueEmptyPrerequisites = prerequisitesMatcherContext([], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherTrueEmptyPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); // Non existent feature flag const matcherParentNotExist = prerequisitesMatcherContext([{ n: 'not-existent-feature-flag', ts: ['on', 'off'] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherParentNotExist({ key: 'a-key' }, evaluateFeature)).toBe(false); // If the feature flag does not exist, matcher should return false // Empty treatments list @@ -88,19 +91,19 @@ test('MATCHER PREREQUISITES / Edge cases', () => { { n: 'always-on', ts: [] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherNoTreatmentsExpected({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list is empty, matcher should return false (no treatment will match) const matcherExpectedTreatmentWrongTypeMatching = prerequisitesMatcherContext([{ n: 'always-on', // @ts-ignore ts: [null, [1, 2], 3, {}, true, 'on'] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherExpectedTreatmentWrongTypeMatching({ key: 'a-key' }, evaluateFeature)).toBe(true); // If treatments expectation list has elements of the wrong type, those elements are overlooked. const matcherExpectedTreatmentWrongTypeNotMatching = prerequisitesMatcherContext([{ n: 'always-off', // @ts-ignore ts: [null, [1, 2], 3, {}, true, 'on'] - }], mockStorage, loggerMock); + }], mockStorage, loggerMock, fallbackTreatmentsCalculator); expect(matcherExpectedTreatmentWrongTypeNotMatching({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list has elements of the wrong type, those elements are overlooked. }); diff --git a/src/evaluator/matchers/dependency.ts b/src/evaluator/matchers/dependency.ts index 68448a8c..2eb78fa2 100644 --- a/src/evaluator/matchers/dependency.ts +++ b/src/evaluator/matchers/dependency.ts @@ -4,8 +4,9 @@ import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { IDependencyMatcherValue, IEvaluation, ISplitEvaluator } from '../types'; import { ENGINE_MATCHER_DEPENDENCY, ENGINE_MATCHER_DEPENDENCY_PRE } from '../../logger/constants'; +import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator'; -export function dependencyMatcherContext({ split, treatments }: IDependencyMatcherData, storage: IStorageSync | IStorageAsync, log: ILogger) { +export function dependencyMatcherContext({ split, treatments }: IDependencyMatcherData, storage: IStorageSync | IStorageAsync, log: ILogger, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator) { function checkTreatment(evaluation: IEvaluation, acceptableTreatments: string[], parentName: string) { let matches = false; @@ -21,7 +22,7 @@ export function dependencyMatcherContext({ split, treatments }: IDependencyMatch return function dependencyMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { log.debug(ENGINE_MATCHER_DEPENDENCY_PRE, [split, JSON.stringify(key), attributes ? '\n attributes: ' + JSON.stringify(attributes) : '']); - const evaluation = splitEvaluator(log, key, split, attributes, storage); + const evaluation = splitEvaluator(log, key, split, attributes, storage, fallbackTreatmentsCalculator); if (thenable(evaluation)) { return evaluation.then(ev => checkTreatment(ev, treatments, split)); diff --git a/src/evaluator/matchers/prerequisites.ts b/src/evaluator/matchers/prerequisites.ts index 9bee45b3..73da63f9 100644 --- a/src/evaluator/matchers/prerequisites.ts +++ b/src/evaluator/matchers/prerequisites.ts @@ -3,13 +3,14 @@ import { IStorageAsync, IStorageSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; +import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator'; -export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger) { +export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator) { return function prerequisitesMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { function evaluatePrerequisite(prerequisite: { n: string; ts: string[] }): MaybeThenable { - const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); + const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage, fallbackTreatmentsCalculator); return thenable(evaluation) ? evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) !== -1) : prerequisite.ts.indexOf(evaluation.treatment!) !== -1; diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 92806ddf..97725bec 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -2,6 +2,7 @@ import { IBetweenMatcherData, IBetweenStringMatcherData, IDependencyMatcherData, import { IStorageAsync, IStorageSync } from '../storages/types'; import SplitIO from '../../types/splitio'; import { ILogger } from '../logger/types'; +import { FallbackTreatmentsCalculator } from './fallbackTreatmentsCalculator'; export interface IDependencyMatcherValue { key: SplitIO.SplitKey, @@ -27,7 +28,7 @@ export interface IEvaluation { export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean } -export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable +export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator) => MaybeThenable export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/sdkClient/__tests__/sdkClientMethod.spec.ts b/src/sdkClient/__tests__/sdkClientMethod.spec.ts index e2f53f83..31b62356 100644 --- a/src/sdkClient/__tests__/sdkClientMethod.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethod.spec.ts @@ -4,6 +4,7 @@ import { sdkClientMethodFactory } from '../sdkClientMethod'; import { assertClientApi } from './testUtils'; import { telemetryTrackerFactory } from '../../trackers/telemetryTracker'; import { IBasicClient } from '../../types'; +import { FallbackTreatmentsCalculator } from '../../evaluator/fallbackTreatmentsCalculator'; const errorMessage = 'Shared Client not supported by the storage mechanism. Create isolated instances instead.'; @@ -17,7 +18,8 @@ const paramMocks = [ settings: { mode: CONSUMER_MODE, log: loggerMock, core: { authorizationKey: 'sdk key '} }, telemetryTracker: telemetryTrackerFactory(), clients: {}, - uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() } + uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() }, + fallbackTreatmentsCalculator: new FallbackTreatmentsCalculator(loggerMock, {}) }, // SyncManager (i.e., Sync SDK) and Signal listener { @@ -28,7 +30,8 @@ const paramMocks = [ settings: { mode: STANDALONE_MODE, log: loggerMock, core: { authorizationKey: 'sdk key '} }, telemetryTracker: telemetryTrackerFactory(), clients: {}, - uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() } + uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() }, + fallbackTreatmentsCalculator: new FallbackTreatmentsCalculator(loggerMock, {}) } ]; diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 0e526f72..23e07fc6 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -35,7 +35,7 @@ function stringify(options?: SplitIO.EvaluationOptions) { * Creator of base client with getTreatments and track methods. */ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker, telemetryTracker } = params; + const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker, telemetryTracker, fallbackTreatmentsCalculator } = params; const { log, mode } = settings; const isAsync = isConsumerMode(mode); @@ -52,7 +52,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl }; const evaluation = readinessManager.isReady() || readinessManager.isReadyFromCache() ? - evaluateFeature(log, key, featureFlagName, attributes, storage) : + evaluateFeature(log, key, featureFlagName, attributes, storage, fallbackTreatmentsCalculator) : isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected Promise.resolve(treatmentNotReady) : treatmentNotReady; @@ -81,7 +81,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl }; const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? - evaluateFeatures(log, key, featureFlagNames, attributes, storage) : + evaluateFeatures(log, key, featureFlagNames, attributes, storage, fallbackTreatmentsCalculator) : isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected Promise.resolve(treatmentsNotReady(featureFlagNames)) : treatmentsNotReady(featureFlagNames); @@ -110,7 +110,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl }; const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? - evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName) : + evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName, fallbackTreatmentsCalculator) : isAsync ? Promise.resolve({}) : {}; diff --git a/src/sdkFactory/__tests__/index.spec.ts b/src/sdkFactory/__tests__/index.spec.ts index e46296be..2165bcd4 100644 --- a/src/sdkFactory/__tests__/index.spec.ts +++ b/src/sdkFactory/__tests__/index.spec.ts @@ -3,6 +3,7 @@ import { sdkFactory } from '../index'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import SplitIO from '../../../types/splitio'; import { EventEmitter } from '../../utils/MinEvents'; +import { FallbackTreatmentsCalculator } from '../../evaluator/fallbackTreatmentsCalculator'; /** Mocks */ @@ -36,6 +37,7 @@ const paramsForAsyncSDK = { platform: { EventEmitter }, + fallbackTreatmentsCalculator: new FallbackTreatmentsCalculator(fullSettings.log) }; const SignalListenerInstanceMock = { start: jest.fn() }; diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index eba01028..5e38d5a7 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -17,6 +17,7 @@ import { DEBUG, OPTIMIZED } from '../utils/constants'; import { setRolloutPlan } from '../storages/setRolloutPlan'; import { IStorageSync } from '../storages/types'; import { getMatching } from '../utils/key'; +import { FallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator'; /** * Modular SDK factory @@ -60,6 +61,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA } }); + const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.log, settings.fallbackTreatments); + if (initialRolloutPlan) { setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key)); if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); @@ -85,7 +88,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA // splitApi is used by SyncManager and Browser signal listener const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker); - const ctx: ISdkFactoryContext = { clients, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform }; + const ctx: ISdkFactoryContext = { clients, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform, fallbackTreatmentsCalculator }; const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync); ctx.syncManager = syncManager; diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 25882c38..63b7d0fe 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -10,6 +10,7 @@ import { IImpressionObserver } from '../trackers/impressionObserver/types'; import { IImpressionsTracker, IEventTracker, ITelemetryTracker, IFilterAdapter, IUniqueKeysTracker } from '../trackers/types'; import { ISettings } from '../types'; import SplitIO from '../../types/splitio'; +import { FallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator'; /** * Environment related dependencies. @@ -51,6 +52,7 @@ export interface ISdkFactoryContext { splitApi?: ISplitApi syncManager?: ISyncManager, clients: Record, + fallbackTreatmentsCalculator: FallbackTreatmentsCalculator } export interface ISdkFactoryContextSync extends ISdkFactoryContext { diff --git a/types/splitio.d.ts b/types/splitio.d.ts index f1ac014f..cfe069ad 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -1233,25 +1233,18 @@ declare namespace SplitIO { */ type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN'; /** - * Fallback treatment can be either a string (treatment) or an object with treatment, config and label. + * Fallback treatment can be either a string (Treatment) or an object with treatment and config (TreatmentWithConfig). */ - type FallbackTreatment = string | { - treatment: string; - config?: string | null; - label?: string | null; - }; + type FallbackTreatment = TreatmentWithConfig; /** * Fallback treatments to be used when the SDK is not ready or the flag is not found. */ type FallbackTreatmentConfiguration = { - global?: FallbackTreatment, - byFlag: { - [key: string]: FallbackTreatment + global?: string | FallbackTreatment, + byFlag?: { + [key: string]: 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. */