From f6330dc7c7ea59a3de71d0b894bd05cc10619355 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Jul 2026 22:43:01 -0300 Subject: [PATCH 1/5] Add AI config type/subtype to definitions and evaluations; propagate entityType per impression instead of per-submitter --- src/dtos/types.ts | 7 ++++++ src/evaluator/index.ts | 8 +++++- src/evaluator/types.ts | 4 ++- src/listeners/browser.ts | 4 +-- src/sdkFactory/types.ts | 4 --- src/services/authProvider.ts | 2 +- src/storages/utils.ts | 1 + .../__tests__/impressionsSubmitter.spec.ts | 25 +++---------------- src/sync/submitters/impressionsSubmitter.ts | 12 ++++----- src/sync/submitters/types.ts | 3 ++- 10 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 89951783..30e99dfe 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -215,6 +215,9 @@ export interface IRBSegment extends TargetingEntity { } | null } +export type ConfigType = 'STANDARD' | 'AI'; +export type ConfigSubtype = 'LLM_CALL'; + export interface IDefinition extends TargetingEntity { trafficTypeName: string; sets?: string[] | null; @@ -231,6 +234,10 @@ export interface IDefinition extends TargetingEntity { configurations?: { [treatmentName: string]: string | SplitIO.JsonObject } | null; + /** Definition classification. Absent means a feature flag. */ + type?: ConfigType; + /** Only meaningful when `type === 'AI'`. */ + subtype?: ConfigSubtype; } /** Interface of the parsed JSON response of `/splitChanges` */ diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index e5000527..b0c48dee 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -161,6 +161,8 @@ function getEvaluation( return evaluation.then(result => { result.changeNumber = definition.changeNumber; result.config = definition.configurations && definition.configurations[result.treatment] || null; + result.type = definition.type; + result.subtype = definition.subtype; // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. result.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; @@ -169,6 +171,8 @@ function getEvaluation( } else { evaluation.changeNumber = definition.changeNumber; evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null; + evaluation.type = definition.type; + evaluation.subtype = definition.subtype; // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; } @@ -236,7 +240,9 @@ function getDefaultTreatment( treatment: definition.defaultTreatment, label: NO_CONDITION_MATCH, // "default rule" config: definition.configurations && definition.configurations[definition.defaultTreatment] || null, - changeNumber: definition.changeNumber + changeNumber: definition.changeNumber, + type: definition.type, + subtype: definition.subtype }; } diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index d87e2489..e17f250d 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -1,4 +1,4 @@ -import { IBetweenMatcherData, IBetweenStringMatcherData, IDependencyMatcherData, MaybeThenable } from '../dtos/types'; +import { IBetweenMatcherData, IBetweenStringMatcherData, IDefinition, IDependencyMatcherData, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; import SplitIO from '../../types/splitio'; import { ILogger } from '../logger/types'; @@ -23,6 +23,8 @@ export interface IEvaluation { label: string, changeNumber?: number, config?: string | null | SplitIO.JsonObject + type?: IDefinition['type'] + subtype?: IDefinition['subtype'] } export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean } diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index 2db4bda0..a7bade87 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -28,14 +28,14 @@ export class BrowserSignalListener implements ISignalListener { private serviceApi: IServiceApi; private fromImpressionsCollector: (data: SplitIO.ImpressionDTO[]) => ImpressionsPayload; - constructor({ syncManager, settings, storage, serviceApi, entityType }: ISdkFactoryContextSync) { + constructor({ syncManager, settings, storage, serviceApi }: ISdkFactoryContextSync) { this.syncManager = syncManager; this.settings = settings; this.storage = storage; this.serviceApi = serviceApi; this.flushData = this.flushData.bind(this); this.flushDataIfHidden = this.flushDataIfHidden.bind(this); - this.fromImpressionsCollector = fromImpressionsCollector.bind(undefined, settings.core.labelsEnabled, entityType); + this.fromImpressionsCollector = fromImpressionsCollector.bind(undefined, settings.core.labelsEnabled); } /** diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 14bffc56..aea826e4 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -43,9 +43,6 @@ export interface IPlatform { SignalListener?: new (params: ISdkFactoryContext) => ISignalListener, // Used by BrowserSignalListener } -// Definition type -export type EntityType = 'config' | 'flag'; - export interface ISdkFactoryContext { platform: IPlatform, sdkReadinessManager: ISdkReadinessManager, @@ -59,7 +56,6 @@ export interface ISdkFactoryContext { syncManager?: ISyncManager, clients: Record, fallbackCalculator: IFallbackCalculator, - entityType?: EntityType } export interface ISdkFactoryContextSync extends ISdkFactoryContext { diff --git a/src/services/authProvider.ts b/src/services/authProvider.ts index 23cf4990..1816bbd5 100644 --- a/src/services/authProvider.ts +++ b/src/services/authProvider.ts @@ -28,7 +28,7 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit const { urls, log } = settings; function fetchAuth() { - let url = `${urls.auth}/api/v3/auth?capabilities=config`; + let url = `${urls.auth}/api/v3/auth?capabilities=config,aiconfig`; return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN), false, true); } diff --git a/src/storages/utils.ts b/src/storages/utils.ts index 49b21690..38fac94b 100644 --- a/src/storages/utils.ts +++ b/src/storages/utils.ts @@ -31,6 +31,7 @@ export function impressionsToJSON(impressions: SplitIO.ImpressionDTO[], metadata m: impression.time, pt: impression.pt, properties: impression.properties + // @TODO set entityType } }; diff --git a/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts b/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts index 438b2ea4..e7d50b41 100644 --- a/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts +++ b/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts @@ -1,4 +1,4 @@ -import { fromImpressionsCollector, impressionsSubmitterFactory } from '../impressionsSubmitter'; +import { impressionsSubmitterFactory } from '../impressionsSubmitter'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { ImpressionsCacheInMemory } from '../../../storages/inMemory/ImpressionsCacheInMemory'; @@ -11,7 +11,7 @@ const imp1 = { time: 0 }; const imp2 = { ...imp1, keyName: 'k2' }; -const imp3 = { ...imp1, keyName: 'k3' }; +const imp3 = { ...imp1, keyName: 'k3', entityType: 'config' as const }; describe('Impressions submitter', () => { @@ -41,7 +41,7 @@ describe('Impressions submitter', () => { // POST with imp1 ['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123}]}]'], // POST with imp2 and imp3 - ['[{"f":"someFeature","i":[{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123}]}]']]); + ['[{"f":"someFeature","i":[{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123,"et":"config"}]}]']]); impressionsSubmitter.stop(); done(); @@ -66,7 +66,7 @@ describe('Impressions submitter', () => { // impression for imp1 ['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123}]}]'], // impressions for imp1, imp2 and imp3 - ['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123},{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123}]}]']]); + ['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123},{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123,"et":"config"}]}]']]); impressionsSubmitter.stop(); done(); @@ -96,20 +96,3 @@ describe('Impressions submitter', () => { }); }); - -describe('fromImpressionsCollector', () => { - - test('includes entityType in payload when provided', () => { - const impressions = [imp1, imp2]; - const result = fromImpressionsCollector(false, 'config', impressions); - - expect(result).toEqual([{ - f: 'someFeature', - i: [ - { k: 'k1', t: 'someTreatment', m: 0, c: 123, et: 'config' }, - { k: 'k2', t: 'someTreatment', m: 0, c: 123, et: 'config' }, - ] - }]); - }); - -}); diff --git a/src/sync/submitters/impressionsSubmitter.ts b/src/sync/submitters/impressionsSubmitter.ts index b0e310ac..89f01d86 100644 --- a/src/sync/submitters/impressionsSubmitter.ts +++ b/src/sync/submitters/impressionsSubmitter.ts @@ -3,12 +3,12 @@ import SplitIO from '../../../types/splitio'; import { submitterFactory } from './submitter'; import { ImpressionsPayload } from './types'; import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; -import { EntityType, ISdkFactoryContextSync } from '../../sdkFactory/types'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; /** * Converts `impressions` data from cache into request payload. */ -export function fromImpressionsCollector(sendLabels: boolean, entityType: EntityType | undefined, data: SplitIO.ImpressionDTO[]): ImpressionsPayload { +export function fromImpressionsCollector(sendLabels: boolean, data: SplitIO.ImpressionDTO[]): ImpressionsPayload { let groupedByFeature = groupBy(data, 'feature'); let dto: ImpressionsPayload = []; @@ -25,7 +25,8 @@ export function fromImpressionsCollector(sendLabels: boolean, entityType: Entity b: entry.bucketingKey, // Bucketing Key pt: entry.pt, // Previous time properties: entry.properties, // Properties - et: entityType, // Definition type + // @ts-expect-error - entityType is not yet public. @TODO: add to SplitIO.ImpressionDTO type + et: entry.entityType, // Definition type }; }) }); @@ -42,12 +43,11 @@ export function impressionsSubmitterFactory(params: ISdkFactoryContextSync) { const { settings: { log, scheduler: { impressionsRefreshRate }, core: { labelsEnabled } }, serviceApi: { postTestImpressionsBulk }, - storage: { impressions }, - entityType + storage: { impressions } } = params; // retry impressions only once. - const syncTask = submitterFactory(log, postTestImpressionsBulk, impressions, impressionsRefreshRate, fromImpressionsCollector.bind(undefined, labelsEnabled, entityType), 1); + const syncTask = submitterFactory(log, postTestImpressionsBulk, impressions, impressionsRefreshRate, fromImpressionsCollector.bind(undefined, labelsEnabled), 1); // register impressions submitter to be executed when impressions cache is full impressions.setOnFullQueueCb(() => { diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index c5c44381..d865a857 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -2,7 +2,8 @@ import { IMetadata } from '../../dtos/types'; import SplitIO from '../../../types/splitio'; import { ISyncTask } from '../types'; -import { EntityType } from '../../sdkFactory/types'; + +type EntityType = 'config' | 'flag' | 'ai-config'; type ImpressionPayload = { /** Matching Key */ From ff98dc0113983aafdbf3e853a170a5ace987b423 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Jul 2026 23:07:06 -0300 Subject: [PATCH 2/5] Clean up evaluator/index.ts: consistent ternary style and shared helper for evaluation fields AI-Session-Id: 294a604f-d119-436e-a741-a2fad8e55342 AI-Tool: claude-code AI-Model: unknown --- src/evaluator/index.ts | 102 +++++++++++++---------------------------- 1 file changed, 32 insertions(+), 70 deletions(-) diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index b0c48dee..80f9de95 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -47,29 +47,14 @@ export function evaluateFeature( return EVALUATION_EXCEPTION; } - if (thenable(definition)) { - return definition.then((definition) => getEvaluation( - log, - key, - definition, - attributes, - storage, - options, - )).catch( - // Exception on async 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 - () => EVALUATION_EXCEPTION - ); - } - - return getEvaluation( - log, - key, - definition, - attributes, - storage, - options, - ); + return thenable(definition) ? + definition.then((definition) => getEvaluation(log, key, definition, attributes, storage, options)) + .catch( + // Exception on async 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 + () => EVALUATION_EXCEPTION + ) : + getEvaluation(log, key, definition, attributes, storage, options); } export function evaluateFeatures( @@ -91,11 +76,11 @@ export function evaluateFeatures( return thenable(definitions) ? definitions.then(definitions => getEvaluations(log, key, definitionNames, definitions, attributes, storage, options)) - .catch(() => { + .catch( // Exception on async 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(definitionNames); - }) : + () => treatmentsException(definitionNames) + ) : getEvaluations(log, key, definitionNames, definitions, attributes, storage, options); } @@ -137,12 +122,21 @@ export function evaluateFeaturesByFlagSets( // evaluate related features return thenable(storedFlagNames) ? storedFlagNames.then((storedFlagNames) => evaluate(storedFlagNames)) - .catch(() => { - return {}; - }) : + .catch(() => ({})) : evaluate(storedFlagNames); } +function setEvaluationDataFromDefinition(evaluation: IEvaluationResult, definition: IDefinition, options?: SplitIO.EvaluationOptions): IEvaluationResult { + evaluation.changeNumber = definition.changeNumber; + evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null; + evaluation.type = definition.type; + evaluation.subtype = definition.subtype; + // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. + evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; + + return evaluation; +} + function getEvaluation( log: ILogger, key: SplitIO.SplitKey, @@ -156,28 +150,9 @@ function getEvaluation( const split = engineParser(log, definition, storage); const evaluation = split.getTreatment(key, attributes, evaluateFeature); - // If the storage is async and the evaluated definition uses segments or dependencies, evaluation is thenable - if (thenable(evaluation)) { - return evaluation.then(result => { - result.changeNumber = definition.changeNumber; - result.config = definition.configurations && definition.configurations[result.treatment] || null; - result.type = definition.type; - result.subtype = definition.subtype; - // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. - result.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; - - return result; - }); - } else { - evaluation.changeNumber = definition.changeNumber; - evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null; - evaluation.type = definition.type; - evaluation.subtype = definition.subtype; - // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. - evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; - } - - return evaluation; + return thenable(evaluation) ? + evaluation.then(result => setEvaluationDataFromDefinition(result, definition, options)) : + setEvaluationDataFromDefinition(evaluation, definition, options); } return EVALUATION_DEFINITION_NOT_FOUND; @@ -195,21 +170,12 @@ function getEvaluations( const result: Record = {}; const thenables: Promise[] = []; definitionNames.forEach(definitionName => { - const evaluation = getEvaluation( - log, - key, - definitions[definitionName], - attributes, - storage, - options - ); - if (thenable(evaluation)) { + const evaluation = getEvaluation(log, key, definitions[definitionName], attributes, storage, options); + thenable(evaluation) ? thenables.push(evaluation.then(res => { result[definitionName] = res; - })); - } else { + })) : result[definitionName] = evaluation; - } }); return thenables.length > 0 ? Promise.all(thenables).then(() => result) : result; @@ -236,14 +202,10 @@ function getDefaultTreatment( definition: IDefinition | null, ): MaybeThenable { if (definition) { - return { + return setEvaluationDataFromDefinition({ treatment: definition.defaultTreatment, - label: NO_CONDITION_MATCH, // "default rule" - config: definition.configurations && definition.configurations[definition.defaultTreatment] || null, - changeNumber: definition.changeNumber, - type: definition.type, - subtype: definition.subtype - }; + label: NO_CONDITION_MATCH // "default rule" + }, definition); } return EVALUATION_DEFINITION_NOT_FOUND; From c39f2530adac86a7c6c8a1fba18858f2f177403c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 30 Jul 2026 10:35:14 -0300 Subject: [PATCH 3/5] Move Configs SDK types and modules to Configs SDK repo --- src/dtos/types.ts | 4 +- .../__tests__/fallback-calculator.spec.ts | 50 ----- .../__tests__/fallback-sanitizer.spec.ts | 147 --------------- .../fallbackSanitizer/index.ts | 76 -------- .../fallbackConfigsCalculator/index.ts | 24 --- types/splitio.d.ts | 172 +----------------- 6 files changed, 3 insertions(+), 470 deletions(-) delete mode 100644 src/evaluator/fallbackConfigsCalculator/__tests__/fallback-calculator.spec.ts delete mode 100644 src/evaluator/fallbackConfigsCalculator/__tests__/fallback-sanitizer.spec.ts delete mode 100644 src/evaluator/fallbackConfigsCalculator/fallbackSanitizer/index.ts delete mode 100644 src/evaluator/fallbackConfigsCalculator/index.ts diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 30e99dfe..7d4d7c19 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -215,8 +215,8 @@ export interface IRBSegment extends TargetingEntity { } | null } -export type ConfigType = 'STANDARD' | 'AI'; -export type ConfigSubtype = 'LLM_CALL'; +export type ConfigType = 'standard' | 'ai'; +export type ConfigSubtype = 'llm_call'; export interface IDefinition extends TargetingEntity { trafficTypeName: string; diff --git a/src/evaluator/fallbackConfigsCalculator/__tests__/fallback-calculator.spec.ts b/src/evaluator/fallbackConfigsCalculator/__tests__/fallback-calculator.spec.ts deleted file mode 100644 index 239185fb..00000000 --- a/src/evaluator/fallbackConfigsCalculator/__tests__/fallback-calculator.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { FallbackConfigsCalculator } from '../'; -import SplitIO from '../../../../types/splitio'; -import { CONTROL } from '../../../utils/constants'; - -describe('FallbackConfigsCalculator', () => { - test('returns specific fallback if config name exists', () => { - const fallbacks: SplitIO.FallbackConfigs = { - byName: { - 'configA': { variant: 'VARIANT_A', value: { key: 1 } }, - }, - }; - const calculator = FallbackConfigsCalculator(fallbacks); - const result = calculator('configA', 'label by name'); - - expect(result).toEqual({ - treatment: 'VARIANT_A', - config: { key: 1 }, - label: 'fallback - label by name', - }); - }); - - test('returns global fallback if config name is missing and global exists', () => { - const fallbacks: SplitIO.FallbackConfigs = { - byName: {}, - global: { variant: 'GLOBAL_VARIANT', value: { global: true } }, - }; - const calculator = FallbackConfigsCalculator(fallbacks); - const result = calculator('missingConfig', 'label by global'); - - expect(result).toEqual({ - treatment: 'GLOBAL_VARIANT', - config: { global: true }, - label: 'fallback - label by global', - }); - }); - - test('returns control fallback if config name and global are missing', () => { - const fallbacks: SplitIO.FallbackConfigs = { - byName: {}, - }; - const calculator = FallbackConfigsCalculator(fallbacks); - const result = calculator('missingConfig', 'label by noFallback'); - - expect(result).toEqual({ - treatment: CONTROL, - config: null, - label: 'label by noFallback', - }); - }); -}); diff --git a/src/evaluator/fallbackConfigsCalculator/__tests__/fallback-sanitizer.spec.ts b/src/evaluator/fallbackConfigsCalculator/__tests__/fallback-sanitizer.spec.ts deleted file mode 100644 index 12e5807b..00000000 --- a/src/evaluator/fallbackConfigsCalculator/__tests__/fallback-sanitizer.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { isValidConfigName, isValidConfig, sanitizeFallbacks } from '../fallbackSanitizer'; -import SplitIO from '../../../../types/splitio'; -import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; - -describe('FallbackConfigsSanitizer', () => { - const validConfig: SplitIO.Config = { variant: 'on', value: { color: 'blue' } }; - const invalidVariantConfig: SplitIO.Config = { variant: ' ', value: { color: 'blue' } }; - const invalidValueConfig = { variant: 'on', value: 'not_an_object' } as unknown as SplitIO.Config; - const fallbackMock = { - global: undefined, - byName: {} - }; - - beforeEach(() => { - loggerMock.mockClear(); - }); - - describe('isValidConfigName', () => { - test('returns true for a valid config name', () => { - expect(isValidConfigName('my_config')).toBe(true); - }); - - test('returns false for a name longer than 100 chars', () => { - const longName = 'a'.repeat(101); - expect(isValidConfigName(longName)).toBe(false); - }); - - test('returns false if the name contains spaces', () => { - expect(isValidConfigName('invalid config')).toBe(false); - }); - - test('returns false if the name is not a string', () => { - // @ts-ignore - expect(isValidConfigName(true)).toBe(false); - }); - }); - - describe('isValidConfig', () => { - test('returns true for a valid config', () => { - expect(isValidConfig(validConfig)).toBe(true); - }); - - test('returns false for null or undefined', () => { - expect(isValidConfig()).toBe(false); - expect(isValidConfig(undefined)).toBe(false); - }); - - test('returns false for a variant longer than 100 chars', () => { - const long: SplitIO.Config = { variant: 'a'.repeat(101), value: {} }; - expect(isValidConfig(long)).toBe(false); - }); - - test('returns false if variant does not match regex pattern', () => { - const invalid: SplitIO.Config = { variant: 'invalid variant!', value: {} }; - expect(isValidConfig(invalid)).toBe(false); - }); - - test('returns false if value is not an object', () => { - expect(isValidConfig(invalidValueConfig)).toBe(false); - }); - }); - - describe('sanitizeGlobal', () => { - test('returns the config if valid', () => { - expect(sanitizeFallbacks(loggerMock, { ...fallbackMock, global: validConfig })).toEqual({ ...fallbackMock, global: validConfig }); - expect(loggerMock.error).not.toHaveBeenCalled(); - }); - - test('returns undefined and logs error if variant is invalid', () => { - const result = sanitizeFallbacks(loggerMock, { ...fallbackMock, global: invalidVariantConfig }); - expect(result).toEqual(fallbackMock); - expect(loggerMock.error).toHaveBeenCalledWith( - expect.stringContaining('Fallback configs - Discarded fallback') - ); - }); - - test('returns undefined and logs error if value is invalid', () => { - const result = sanitizeFallbacks(loggerMock, { ...fallbackMock, global: invalidValueConfig }); - expect(result).toEqual(fallbackMock); - expect(loggerMock.error).toHaveBeenCalledWith( - expect.stringContaining('Fallback configs - Discarded fallback') - ); - }); - }); - - describe('sanitizeByName', () => { - test('returns a sanitized map with valid entries only', () => { - const input = { - valid_config: validConfig, - 'invalid config': validConfig, - bad_variant: invalidVariantConfig, - }; - - const result = sanitizeFallbacks(loggerMock, { ...fallbackMock, byName: input }); - - expect(result).toEqual({ ...fallbackMock, byName: { valid_config: validConfig } }); - expect(loggerMock.error).toHaveBeenCalledTimes(2); // invalid config name + bad_variant - }); - - test('returns empty object if all invalid', () => { - const input = { - 'invalid config': invalidVariantConfig, - }; - - const result = sanitizeFallbacks(loggerMock, { ...fallbackMock, byName: input }); - expect(result).toEqual(fallbackMock); - expect(loggerMock.error).toHaveBeenCalled(); - }); - - test('returns same object if all valid', () => { - const input = { - ...fallbackMock, - byName: { - config_one: validConfig, - config_two: { variant: 'valid_2', value: { key: 'val' } }, - } - }; - - const result = sanitizeFallbacks(loggerMock, input); - expect(result).toEqual(input); - expect(loggerMock.error).not.toHaveBeenCalled(); - }); - }); - - describe('sanitizeFallbacks', () => { - test('returns undefined and logs error if fallbacks is not an object', () => { // @ts-expect-error - const result = sanitizeFallbacks(loggerMock, 'invalid_fallbacks'); - expect(result).toBeUndefined(); - expect(loggerMock.error).toHaveBeenCalledWith( - 'Fallback configs - Discarded configuration: it must be an object with optional `global` and `byName` properties' - ); - }); - - test('returns undefined and logs error if fallbacks is not an object', () => { // @ts-expect-error - const result = sanitizeFallbacks(loggerMock, true); - expect(result).toBeUndefined(); - expect(loggerMock.error).toHaveBeenCalledWith( - 'Fallback configs - Discarded configuration: it must be an object with optional `global` and `byName` properties' - ); - }); - - test('sanitizes both global and byName fallbacks for empty object', () => { // @ts-expect-error - const result = sanitizeFallbacks(loggerMock, { global: {} }); - expect(result).toEqual({ global: undefined, byName: {} }); - }); - }); -}); diff --git a/src/evaluator/fallbackConfigsCalculator/fallbackSanitizer/index.ts b/src/evaluator/fallbackConfigsCalculator/fallbackSanitizer/index.ts deleted file mode 100644 index d4ba9754..00000000 --- a/src/evaluator/fallbackConfigsCalculator/fallbackSanitizer/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import SplitIO from '../../../../types/splitio'; -import { ILogger } from '../../../logger/types'; -import { isObject, isString } from '../../../utils/lang'; - -const CONFIG_NAME_DISCARD_REASON = 'Invalid config name (max 100 chars, no spaces)'; -const VARIANT_DISCARD_REASON = 'Invalid variant (max 100 chars and must match pattern)'; -const VALUE_DISCARD_REASON = 'Invalid value (must be an object)'; - -const VARIANT_PATTERN = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/; - -export function isValidConfigName(name: string): boolean { - return name.length <= 100 && !name.includes(' '); -} - -export function isValidConfig(config?: SplitIO.Config): boolean { - if (!isObject(config)) return false; - if (!isString(config!.variant) || config!.variant.length > 100 || !VARIANT_PATTERN.test(config!.variant)) return false; - if (!isObject(config!.value)) return false; - return true; -} - -function sanitizeGlobal(logger: ILogger, config?: SplitIO.Config): SplitIO.Config | undefined { - if (config === undefined) return undefined; - if (!isValidConfig(config)) { - if (!isObject(config) || !isString(config!.variant) || config!.variant.length > 100 || !VARIANT_PATTERN.test(config!.variant)) { - logger.error(`Fallback configs - Discarded fallback: ${VARIANT_DISCARD_REASON}`); - } else { - logger.error(`Fallback configs - Discarded fallback: ${VALUE_DISCARD_REASON}`); - } - return undefined; - } - return config; -} - -function sanitizeByName( - logger: ILogger, - byNameFallbacks?: Record -): Record { - const sanitizedByName: Record = {}; - - if (!isObject(byNameFallbacks)) return sanitizedByName; - - Object.keys(byNameFallbacks!).forEach((configName) => { - const config = byNameFallbacks![configName]; - - if (!isValidConfigName(configName)) { - logger.error(`Fallback configs - Discarded config '${configName}': ${CONFIG_NAME_DISCARD_REASON}`); - return; - } - - if (!isValidConfig(config)) { - if (!isObject(config) || !isString(config.variant) || config.variant.length > 100 || !VARIANT_PATTERN.test(config.variant)) { - logger.error(`Fallback configs - Discarded config '${configName}': ${VARIANT_DISCARD_REASON}`); - } else { - logger.error(`Fallback configs - Discarded config '${configName}': ${VALUE_DISCARD_REASON}`); - } - return; - } - - sanitizedByName[configName] = config; - }); - - return sanitizedByName; -} - -export function sanitizeFallbacks(logger: ILogger, fallbacks: SplitIO.FallbackConfigs): SplitIO.FallbackConfigs | undefined { - if (!isObject(fallbacks)) { - logger.error('Fallback configs - Discarded configuration: it must be an object with optional `global` and `byName` properties'); - return; - } - - return { - global: sanitizeGlobal(logger, fallbacks.global), - byName: sanitizeByName(logger, fallbacks.byName) - }; -} diff --git a/src/evaluator/fallbackConfigsCalculator/index.ts b/src/evaluator/fallbackConfigsCalculator/index.ts deleted file mode 100644 index fa80e9bd..00000000 --- a/src/evaluator/fallbackConfigsCalculator/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IFallbackCalculator } from '../fallbackTreatmentsCalculator/index'; -import { CONTROL } from '../../utils/constants'; -import SplitIO from '../../../types/splitio'; - -export const FALLBACK_PREFIX = 'fallback - '; - -export function FallbackConfigsCalculator(fallbacks: SplitIO.FallbackConfigs = {}): IFallbackCalculator { - - return (configName: string, label = '') => { - const fallback = fallbacks.byName?.[configName] || fallbacks.global; - - return fallback ? - { - treatment: fallback.variant, - config: fallback.value, - label: `${FALLBACK_PREFIX}${label}`, - } : - { - treatment: CONTROL, - config: null, - label, - }; - }; -} diff --git a/types/splitio.d.ts b/types/splitio.d.ts index c5ecda3a..4561b728 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -2290,133 +2290,9 @@ declare namespace SplitIO { split(featureFlagName: string): SplitViewAsync; } - /** - * Fallback configuration objects returned by the `client.getConfig` method when the SDK is not ready or the provided config name is not found. - */ - type FallbackConfigs = { - /** - * Fallback config for all config names. - */ - global?: Config; - /** - * Fallback configs for specific config names. It takes precedence over the global fallback config. - */ - byName?: { - [configName: string]: Config; - }; - } - - /** - * Configs SDK settings. - */ - interface ConfigsClientSettings { - /** - * SDK key used to authenticate with Harness services. - * - * @see {@link https://developer.harness.io/docs/feature-management-experimentation/management-and-administration/account-settings/api-keys/} - */ - sdkKey: string; - /** - * Log level for SDK logging. - * - `'none'`: No logging - * - `'error'`: Log errors only - * - `'warn'`: Log warnings and errors - * - `'info'`: Log info, warnings, and errors - * - `'debug'`: Log debug info and above - * @defaultValue `'none'` - */ - logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug'; - /** - * Synchronization configuration. - */ - sync?: { - /** - * Polling rate for configs and segments refresh, in seconds. Minimum value: 5. - * - * @defaultValue `60` - */ - pollingRate?: number; - /** - * Push rate for events and impressions, in seconds. Minimum value: 60. - * - * @defaultValue `60` - */ - pushRate?: number; - /** - * Maximum queue size for events and impressions. When the queue reaches this size, a flush is triggered. Minimum value: 1000. - * - * @defaultValue `10000` - */ - queueSize?: number; - /** - * Time in seconds before emitting the `SDK_READY_TIMED_OUT` event. - * A value of `-1` disables the timeout and thus the event is never emitted. - * - * @defaultValue `10` - */ - readyTimeout?: number; - /** - * Base URLs used by the SDK for different services. - */ - serviceEndpoints?: { - /** - * String property to override the base URL where the SDK will get JWT authentication credentials. - * - * @defaultValue `'https://auth.split.io'` - */ - auth?: string; - /** - * String property to override the base URL where the SDK will get rollout plan related data, like configs and segments definitions. - * - * @defaultValue `'https://configs.split.io'` - */ - configs?: string; - /** - * String property to override the base URL where the SDK will post event-related information like impressions. - * - * @defaultValue `'https://events.split.io'` - */ - events?: string; - }; - }; - /** - * Fallback configuration objects returned by the `client.getConfig` method when the SDK is not ready or the provided config name is not found. - */ - fallbackConfigs?: FallbackConfigs; - /** - * Custom options object for HTTP(S) requests. - * If provided, this object is merged with the options object passed by the SDK for EventSource and Fetch calls. - */ - requestOptions?: { - /** - * Custom Node.js HTTP(S) Agent used by the SDK for HTTP(S) requests. - * - * You can use it, for example, for certificate pinning or setting a network proxy: - * - * ``` - * const { ConfigsClient } = require('@splitsoftware/configs'); - * const { HttpsProxyAgent } = require('https-proxy-agent'); - * - * const proxyAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY || 'http://10.10.1.10:1080'); - * - * const client = ConfigsClient({ - * ... - * requestOptions: { - * agent: proxyAgent - * } - * }) - * ``` - * - * @see {@link https://nodejs.org/api/https.html#class-httpsagent} - * - * @defaultValue `undefined` - */ - agent?: RequestOptions['agent']; - }; - } /** - * Target for a config evaluation. + * Target for an evaluation. */ interface Target { /** @@ -2434,50 +2310,4 @@ declare namespace SplitIO { type JsonValue = string | number | boolean | null | JsonObject | JsonArray; type JsonArray = JsonValue[]; type JsonObject = { [key: string]: JsonValue; }; - - /** - * Config object returned by getConfig. - */ - type Config = { - /** - * The name of the variant. - */ - variant: string; - /** - * The config value, a raw JSON object. - */ - value: JsonObject; - } - - /** - * Configs SDK client interface. - */ - interface ConfigsClient extends Omit { - /** - * Destroys the client. - * - * @returns A promise that resolves once all clients are destroyed. - */ - destroy(): Promise; - /** - * Gets the config object for a given config name and optional target. If no target is provided, the default variant of the config is returned. - * - * @param configName - The name of the config we want to get. - * @param target - The target of the config evaluation. - * @param options - An object of type EvaluationOptions for advanced evaluation options. - * @returns The config object. - */ - getConfig(configName: string, target?: Target, options?: EvaluationOptions): Config; - /** - * Tracks an event to be fed to the results product on Harness FME user interface. - * - * @param trafficKey - The key that identifies the entity related to this event. - * @param trafficType - The traffic type of the entity related to this event. See {@link https://developer.harness.io/docs/feature-management-experimentation/management-and-administration/fme-settings/traffic-types/} - * @param eventType - The event type corresponding to this event. - * @param value - The value of this event. - * @param properties - The properties of this event. Values can be string, number, boolean or null. - * @returns Whether the event was added to the queue successfully or not. - */ - track(trafficKey: SplitKey, trafficType: string, eventType: string, value?: number, properties?: Properties): boolean; - } } From 3e018d23a71c82ab4e460177bab0e9d2070520c7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 30 Jul 2026 10:53:27 -0300 Subject: [PATCH 4/5] rc --- CHANGES.txt | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c98101c4..3c7ca3a1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +3.2.0 (July 31, 2026) + - Added support for AI configs. + - Removed Configs SDK-related types and modules and moved them to the Configs SDK repository. + 3.1.0 (July 23, 2026) - Added support for rule-based segments in /api/v1/configs endpoint. - Updated polling flow to fetch new referenced segments immediately. diff --git a/package-lock.json b/package-lock.json index 0e1b92eb..2d59fcb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.0", + "version": "3.1.1-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "3.1.0", + "version": "3.1.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 59afccdd..e7aecd0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.0", + "version": "3.1.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 870736cc31284ae250464f6ceadef46d26102bbb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 Aug 2026 11:30:45 -0300 Subject: [PATCH 5/5] Simplify code by removing IEvaluationResult type to use IEvaluation --- CHANGES.txt | 2 +- package-lock.json | 16 ++--- package.json | 2 +- src/evaluator/Engine.ts | 6 +- .../__tests__/evaluate-feature.spec.ts | 33 +++++----- .../__tests__/evaluate-features.spec.ts | 48 +++++++-------- src/evaluator/condition/index.ts | 4 +- src/evaluator/index.ts | 60 +++++++------------ .../__tests__/trafficAllocation.spec.ts | 13 ++-- src/evaluator/types.ts | 9 +-- src/sdkClient/client.ts | 49 ++++++++------- 11 files changed, 107 insertions(+), 135 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3c7ca3a1..5a74ab4e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -3.2.0 (July 31, 2026) +3.2.0 (August 10, 2026) - Added support for AI configs. - Removed Configs SDK-related types and modules and moved them to the Configs SDK repository. diff --git a/package-lock.json b/package-lock.json index 2d59fcb6..c9294091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.0", + "version": "3.1.1-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.0", + "version": "3.1.1-rc.1", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -1747,9 +1747,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2217,9 +2217,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e7aecd0c..52f5da9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "3.1.1-rc.0", + "version": "3.1.1-rc.1", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 3ef683e5..b46aef2b 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -7,12 +7,12 @@ import { CONTROL } from '../utils/constants'; import { IDefinition, MaybeThenable } from '../dtos/types'; import SplitIO from '../../types/splitio'; import { IStorageAsync, IStorageSync } from '../storages/types'; -import { IEvaluation, IEvaluationResult, IDefinitionEvaluator } from './types'; +import { IEvaluation, IDefinitionEvaluator } from './types'; import { ILogger } from '../logger/types'; import { ENGINE_DEFAULT } from '../logger/constants'; import { prerequisitesMatcherContext } from './matchers/prerequisites'; -function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { +function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluation { return { treatment: get(result, 'treatment', defaultTreatment), label: get(result, 'label', NO_CONDITION_MATCH) @@ -29,7 +29,7 @@ export function engineParser(log: ILogger, split: IDefinition, storage: IStorage return { - getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: IDefinitionEvaluator): MaybeThenable { + getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: IDefinitionEvaluator): MaybeThenable { const parsedKey = keyParser(key); diff --git a/src/evaluator/__tests__/evaluate-feature.spec.ts b/src/evaluator/__tests__/evaluate-feature.spec.ts index 65c5e0a4..f09654b6 100644 --- a/src/evaluator/__tests__/evaluate-feature.spec.ts +++ b/src/evaluator/__tests__/evaluate-feature.spec.ts @@ -47,10 +47,9 @@ test('EVALUATOR / should return label exception, treatment control and config nu }); -test('EVALUATOR / should return right label, treatment and config if storage returns without errors.', async () => { +test('EVALUATOR / should return right label, treatment and definition if storage returns without errors.', async () => { const expectedOutput = { - treatment: 'on', label: 'in segment all', - config: '{color:\'black\'}', changeNumber: 1487277320548 + treatment: 'on', label: 'in segment all', definition: splitsMock['config'] }; const expectedOutputControl = { treatment: 'control', label: DEFINITION_NOT_FOUND, config: null @@ -63,7 +62,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - expect(evaluationWithConfig).toEqual(expectedOutput); // If the split is retrieved successfully we should get the right evaluation result, label and config. + expect(evaluationWithConfig).toEqual(expectedOutput); // If the split is retrieved successfully we should get the right evaluation result, label and definition. const evaluationNotFound = evaluateFeature( loggerMock, @@ -81,7 +80,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluation).toEqual({ ...expectedOutput, definition: splitsMock['regular'] }); // If the split is retrieved successfully we should get the right evaluation result, label and definition. const evaluationKilled = evaluateFeature( loggerMock, @@ -90,8 +89,8 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluationKilled).toEqual({ ...expectedOutput, treatment: 'off', label: SPLIT_KILLED, definition: splitsMock['killed'] }); + // If the split is retrieved but is killed, we should get the right evaluation result, label and definition. const evaluationArchived = evaluateFeature( loggerMock, @@ -100,8 +99,8 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluationArchived).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, definition: splitsMock['archived'] }); + // If the split is retrieved but is archived, we should get the right evaluation result, label and definition. const evaluationtrafficAlocation1 = evaluateFeature( loggerMock, @@ -110,8 +109,8 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluationtrafficAlocation1).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, treatment: 'off', definition: splitsMock['trafficAlocation1'] }); + // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and definition. const evaluationKilledWithConfig = evaluateFeature( loggerMock, @@ -120,8 +119,8 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluationKilledWithConfig).toEqual({ ...expectedOutput, treatment: 'off', label: SPLIT_KILLED, definition: splitsMock['killedWithConfig'] }); + // If the split is retrieved but is killed, we should get the right evaluation result, label and definition. const evaluationArchivedWithConfig = evaluateFeature( loggerMock, @@ -130,8 +129,8 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluationArchivedWithConfig).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, definition: splitsMock['archivedWithConfig'] }); + // If the split is retrieved but is archived, we should get the right evaluation result, label and definition. const evaluationtrafficAlocation1WithConfig = evaluateFeature( loggerMock, @@ -140,7 +139,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret undefined, mockStorage, ); - 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. + expect(evaluationtrafficAlocation1WithConfig).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, treatment: 'off', definition: splitsMock['trafficAlocation1WithConfig'] }); + // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and definition. }); diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index 9f807be3..a115714c 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -64,11 +64,10 @@ test('EVALUATOR - Multiple evaluations at once / should return label exception, }); -test('EVALUATOR - Multiple evaluations at once / should return right labels, treatments and configs if storage returns without errors.', async () => { +test('EVALUATOR - Multiple evaluations at once / should return right labels, treatments and definitions if storage returns without errors.', async () => { const expectedOutput = { config: { - treatment: 'on', label: 'in segment all', - config: '{color:\'black\'}', changeNumber: 1487277320548 + treatment: 'on', label: 'in segment all', definition: splitsMock['config'] }, not_existent_split: { treatment: 'control', label: DEFINITION_NOT_FOUND, config: null @@ -83,34 +82,34 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre mockStorage, ); // assert evaluationWithConfig - expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config. + expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and definition. // assert evaluationNotFound expect(multipleEvaluationAtOnce['not_existent_split']).toEqual(expectedOutput['not_existent_split']); // If the split is not retrieved successfully because it does not exist, we should get the right evaluation result, label and config. // assert regular - expect(multipleEvaluationAtOnce['regular']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnce['regular']).toEqual({ ...expectedOutput['config'], definition: splitsMock['regular'] }); // If the split is retrieved successfully we should get the right evaluation result, label and definition. // assert killed - expect(multipleEvaluationAtOnce['killed']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnce['killed']).toEqual({ ...expectedOutput['config'], treatment: 'off', label: SPLIT_KILLED, definition: splitsMock['killed'] }); + // 'If the split is retrieved but is killed, we should get the right evaluation result, label and definition. // assert archived - expect(multipleEvaluationAtOnce['archived']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnce['archived']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: SPLIT_ARCHIVED, definition: splitsMock['archived'] }); + // If the split is retrieved but is archived, we should get the right evaluation result, label and definition. // assert trafficAllocation1 - expect(multipleEvaluationAtOnce['trafficAlocation1']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnce['trafficAlocation1']).toEqual({ ...expectedOutput['config'], label: NOT_IN_SPLIT, treatment: 'off', definition: splitsMock['trafficAlocation1'] }); + // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and definition. // assert killedWithConfig - expect(multipleEvaluationAtOnce['killedWithConfig']).toEqual({ ...expectedOutput['config'], treatment: 'off', label: SPLIT_KILLED }); - // If the split is retrieved but is killed, we should get the right evaluation result, label and config. + expect(multipleEvaluationAtOnce['killedWithConfig']).toEqual({ ...expectedOutput['config'], treatment: 'off', label: SPLIT_KILLED, definition: splitsMock['killedWithConfig'] }); + // If the split is retrieved but is killed, we should get the right evaluation result, label and definition. // assert archivedWithConfig - expect(multipleEvaluationAtOnce['archivedWithConfig']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnce['archivedWithConfig']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: SPLIT_ARCHIVED, definition: splitsMock['archivedWithConfig'] }); + // If the split is retrieved but is archived, we should get the right evaluation result, label and definition. // assert trafficAlocation1WithConfig - expect(multipleEvaluationAtOnce['trafficAlocation1WithConfig']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnce['trafficAlocation1WithConfig']).toEqual({ ...expectedOutput['config'], label: NOT_IN_SPLIT, treatment: 'off', definition: splitsMock['trafficAlocation1WithConfig'] }); + // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and definition. }); @@ -118,8 +117,7 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => { const expectedOutput = { config: { - treatment: 'on', label: 'in segment all', - config: '{color:\'black\'}', changeNumber: 1487277320548 + treatment: 'on', label: 'in segment all', definition: splitsMock['config'] }, not_existent_split: { treatment: 'control', label: DEFINITION_NOT_FOUND, config: null @@ -146,14 +144,14 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => { // @todo assert flag set not found - for input validations // assert regular - expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], definition: splitsMock['regular'] }); // If the split is retrieved successfully we should get the right evaluation result, label and definition. // assert killed - expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual({ ...expectedOutput['config'], treatment: 'off', label: SPLIT_KILLED, definition: splitsMock['killed'] }); + // 'If the split is retrieved but is killed, we should get the right evaluation result, label and definition. // assert archived - expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual({ ...expectedOutput['config'], 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. + expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: SPLIT_ARCHIVED, definition: splitsMock['archived'] }); + // If the split is retrieved but is archived, we should get the right evaluation result, label and definition. // assert not_existent_split not in evaluation if it is not related to defined flag sets expect(multipleEvaluationAtOnceByFlagSets['not_existent_split']).toEqual(undefined); @@ -163,7 +161,7 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => { multipleEvaluationAtOnceByFlagSets = await getResultsByFlagSets(['reg_and_config']); expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']); - expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], config: null }); + expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], definition: splitsMock['regular'] }); expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual(undefined); expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual(undefined); }); diff --git a/src/evaluator/condition/index.ts b/src/evaluator/condition/index.ts index d5b6a96c..37fe7b63 100644 --- a/src/evaluator/condition/index.ts +++ b/src/evaluator/condition/index.ts @@ -29,9 +29,9 @@ export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.S // Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions. if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation!, key.bucketingKey, trafficAllocationSeed!)) { return { - treatment: undefined, // treatment value is assigned later + // treatment value is assigned later, at Engine's evaluationResult label: NOT_IN_SPLIT - }; + } as IEvaluation; } // matcherEvaluator could be Async, this relays on matchers return value, so we need diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 80f9de95..659f2092 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -4,11 +4,12 @@ import { EXCEPTION, NO_CONDITION_MATCH, DEFINITION_NOT_FOUND } from '../utils/la import { CONTROL } from '../utils/constants'; import { IDefinition, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; -import { IEvaluationResult } from './types'; +import { IEvaluation } from './types'; 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 { objectAssign } from '../utils/lang/objectAssign'; const EVALUATION_EXCEPTION = { treatment: CONTROL, @@ -23,7 +24,7 @@ const EVALUATION_DEFINITION_NOT_FOUND = { }; function treatmentsException(definitionNames: string[]) { - const evaluations: Record = {}; + const evaluations: Record = {}; definitionNames.forEach(definitionName => { evaluations[definitionName] = EVALUATION_EXCEPTION; }); @@ -36,8 +37,7 @@ export function evaluateFeature( definitionName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, - options?: SplitIO.EvaluationOptions -): MaybeThenable { +): MaybeThenable { let definition; try { @@ -48,13 +48,13 @@ export function evaluateFeature( } return thenable(definition) ? - definition.then((definition) => getEvaluation(log, key, definition, attributes, storage, options)) + definition.then((definition) => getEvaluation(log, key, definition, attributes, storage)) .catch( // Exception on async 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 () => EVALUATION_EXCEPTION ) : - getEvaluation(log, key, definition, attributes, storage, options); + getEvaluation(log, key, definition, attributes, storage); } export function evaluateFeatures( @@ -63,8 +63,7 @@ export function evaluateFeatures( definitionNames: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, - options?: SplitIO.EvaluationOptions, -): MaybeThenable> { +): MaybeThenable> { let definitions; try { @@ -75,13 +74,13 @@ export function evaluateFeatures( } return thenable(definitions) ? - definitions.then(definitions => getEvaluations(log, key, definitionNames, definitions, attributes, storage, options)) + definitions.then(definitions => getEvaluations(log, key, definitionNames, definitions, attributes, storage)) .catch( // Exception on async 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 () => treatmentsException(definitionNames) ) : - getEvaluations(log, key, definitionNames, definitions, attributes, storage, options); + getEvaluations(log, key, definitionNames, definitions, attributes, storage); } export function evaluateFeaturesByFlagSets( @@ -91,8 +90,7 @@ export function evaluateFeaturesByFlagSets( attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, method: string, - options?: SplitIO.EvaluationOptions, -): MaybeThenable> { +): MaybeThenable> { let storedFlagNames: MaybeThenable[]>; function evaluate(featureFlagsByFlagSets: Set[]) { @@ -107,7 +105,7 @@ export function evaluateFeaturesByFlagSets( } return featureFlags.size ? - evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage, options) : + evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage) : {}; } @@ -126,33 +124,21 @@ export function evaluateFeaturesByFlagSets( evaluate(storedFlagNames); } -function setEvaluationDataFromDefinition(evaluation: IEvaluationResult, definition: IDefinition, options?: SplitIO.EvaluationOptions): IEvaluationResult { - evaluation.changeNumber = definition.changeNumber; - evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null; - evaluation.type = definition.type; - evaluation.subtype = definition.subtype; - // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. - evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled; - - return evaluation; -} - function getEvaluation( log: ILogger, key: SplitIO.SplitKey, definition: IDefinition | null, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, - options?: SplitIO.EvaluationOptions, -): MaybeThenable { +): MaybeThenable { if (definition) { const split = engineParser(log, definition, storage); const evaluation = split.getTreatment(key, attributes, evaluateFeature); return thenable(evaluation) ? - evaluation.then(result => setEvaluationDataFromDefinition(result, definition, options)) : - setEvaluationDataFromDefinition(evaluation, definition, options); + evaluation.then(result => objectAssign(result, { definition })) : + objectAssign(evaluation, { definition }); } return EVALUATION_DEFINITION_NOT_FOUND; @@ -165,12 +151,11 @@ function getEvaluations( definitions: Record, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, - options?: SplitIO.EvaluationOptions, -): MaybeThenable> { - const result: Record = {}; +): MaybeThenable> { + const result: Record = {}; const thenables: Promise[] = []; definitionNames.forEach(definitionName => { - const evaluation = getEvaluation(log, key, definitions[definitionName], attributes, storage, options); + const evaluation = getEvaluation(log, key, definitions[definitionName], attributes, storage); thenable(evaluation) ? thenables.push(evaluation.then(res => { result[definitionName] = res; @@ -184,7 +169,7 @@ function getEvaluations( export function evaluateDefaultTreatment( definitionName: string, storage: IStorageSync | IStorageAsync, -): MaybeThenable { +): MaybeThenable { let definition; try { @@ -200,12 +185,13 @@ export function evaluateDefaultTreatment( function getDefaultTreatment( definition: IDefinition | null, -): MaybeThenable { +): MaybeThenable { if (definition) { - return setEvaluationDataFromDefinition({ + return { treatment: definition.defaultTreatment, - label: NO_CONDITION_MATCH // "default rule" - }, definition); + label: NO_CONDITION_MATCH, // "default rule" + definition, + }; } return EVALUATION_DEFINITION_NOT_FOUND; diff --git a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts index 2debd783..76a499c3 100644 --- a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts +++ b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts @@ -2,7 +2,6 @@ import { parser } from '..'; import { keyParser } from '../../../utils/key'; import { IDefinitionCondition } from '../../../dtos/types'; -import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', async () => { @@ -25,8 +24,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', a label: 'in segment all' }] as IDefinitionCondition[]); - // @ts-ignore - const evaluation = await evaluator(keyParser('a key'), 31, 0, 31) as IEvaluation; + const evaluation = await evaluator(keyParser('a key'), 31, 0, 31); expect(evaluation.treatment).toBe(undefined); // treatment should be undefined expect(evaluation.label).toBe('not in split'); // label should be fixed string @@ -52,8 +50,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% wi label: 'in segment all' }] as IDefinitionCondition[]); - // @ts-ignore - const evaluation = await evaluator(keyParser('a key'), 31, 99, 31) as IEvaluation; + const evaluation = await evaluator(keyParser('a key'), 31, 99, 31); expect(evaluation.treatment).toBe('on'); // on expect(evaluation.label).toBe('in segment all'); // in segment all @@ -79,8 +76,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% an label: 'in segment all' }] as IDefinitionCondition[]); - // @ts-ignore - const evaluation = await evaluator(keyParser('a48'), 31, 99, 14) as IEvaluation; // murmur3.bucket('a48', 14) === 100 + const evaluation = await evaluator(keyParser('a48'), 31, 99, 14); // murmur3.bucket('a48', 14) === 100 expect(evaluation.treatment).toBe(undefined); // treatment should be undefined expect(evaluation.label).toBe('not in split'); // label should be fixed string @@ -126,8 +122,7 @@ test('PARSER / if user is whitelisted and in segment all 100%:off with trafficAl label: 'in segment all' }] as IDefinitionCondition[]); - // @ts-ignore - const evaluation = await evaluator(keyParser('a key'), 31, 0, 31) as IEvaluation; + const evaluation = await evaluator(keyParser('a key'), 31, 0, 31); expect(evaluation.treatment).toBe('on'); // on expect(evaluation.label).toBe('whitelisted'); // whitelisted diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index e17f250d..ed618654 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -19,16 +19,11 @@ export interface IMatcherDto { } export interface IEvaluation { - treatment?: string, + treatment: string, label: string, - changeNumber?: number, - config?: string | null | SplitIO.JsonObject - type?: IDefinition['type'] - subtype?: IDefinition['subtype'] + definition?: IDefinition } -export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean } - export type IDefinitionEvaluator = (log: ILogger, key: SplitIO.SplitKey, definitionName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: IDefinitionEvaluator) => MaybeThenable diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 9eeb4f41..6baca5f0 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -4,7 +4,7 @@ import { getMatching, getBucketing } from '../utils/key'; import { validateDefinitionExistence } from '../utils/inputValidation/definitionExistence'; import { SDK_NOT_READY } from '../utils/labels'; import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, TREATMENTS_BY_FLAGSETS, TREATMENTS_BY_FLAGSET, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENT_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS } from '../utils/constants'; -import { IEvaluationResult } from '../evaluator/types'; +import { IEvaluation } from '../evaluator/types'; import SplitIO from '../../types/splitio'; import { IMPRESSION_QUEUEING } from '../logger/constants'; import { ISdkFactoryContext } from '../sdkFactory/types'; @@ -16,17 +16,17 @@ import { trackMethodFactory } from './trackMethod'; const treatmentNotReady = { treatment: CONTROL, label: SDK_NOT_READY }; function treatmentsNotReady(featureFlagNames: string[]) { - const evaluations: Record = {}; + const evaluations: Record = {}; featureFlagNames.forEach(featureFlagName => { evaluations[featureFlagName] = treatmentNotReady; }); return evaluations; } -export function stringify(options?: SplitIO.EvaluationOptions) { - if (options && options.properties) { +export function stringify(properties?: SplitIO.Properties) { + if (properties) { try { - return JSON.stringify(options.properties); + return JSON.stringify(properties); } catch { /* JSON.stringify should never throw with validated options, but handling just in case */ } } } @@ -42,9 +42,9 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) { const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT); - const wrapUp = (evaluationResult: IEvaluationResult) => { + const wrapUp = (evaluationResult: IEvaluation) => { const queue: ImpressionDecorated[] = []; - const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue); + const treatment = processEvaluation(evaluationResult, featureFlagName, key, withConfig, methodName, queue, options); impressionsTracker.track(queue, attributes); stopTelemetryTracker(queue[0] && queue[0].imp.label); @@ -52,7 +52,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl }; const evaluation = readinessManager.isReadyFromCache() ? - evaluateFeature(log, key, featureFlagName, attributes, storage, options) : + evaluateFeature(log, key, featureFlagName, attributes, storage) : 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; @@ -67,12 +67,11 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) { const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS); - const wrapUp = (evaluationResults: Record) => { + const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDecorated[] = []; const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {}; - const properties = stringify(options); Object.keys(evaluationResults).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue); + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, withConfig, methodName, queue, options); }); impressionsTracker.track(queue, attributes); @@ -81,7 +80,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl }; const evaluations = readinessManager.isReadyFromCache() ? - evaluateFeatures(log, key, featureFlagNames, attributes, storage, options) : + evaluateFeatures(log, key, featureFlagNames, attributes, storage) : 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); @@ -96,12 +95,11 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { const stopTelemetryTracker = telemetryTracker.trackEval(method); - const wrapUp = (evaluationResults: Record) => { + const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDecorated[] = []; const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {}; - const properties = stringify(options); Object.keys(evaluationResults).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue); + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, withConfig, methodName, queue, options); }); impressionsTracker.track(queue, attributes); @@ -110,7 +108,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl }; const evaluations = readinessManager.isReadyFromCache() ? - evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName, options) : + evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName) : isAsync ? Promise.resolve({}) : {}; @@ -132,19 +130,20 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl // Internal function function processEvaluation( - evaluation: IEvaluationResult, + evaluation: IEvaluation, featureFlagName: string, key: SplitIO.SplitKey, - properties: string | undefined, withConfig: boolean, invokingMethodName: string, - queue: ImpressionDecorated[] + queue: ImpressionDecorated[], + options: SplitIO.EvaluationOptions = {} ): SplitIO.Treatment | SplitIO.TreatmentWithConfig { const matchingKey = getMatching(key); const bucketingKey = getBucketing(key); - const { changeNumber, impressionsDisabled } = evaluation; - let { treatment, label, config = null } = evaluation; + const { definition } = evaluation; + let { treatment, label } = evaluation; + let config = definition?.configurations?.[treatment] || null; if (treatment === CONTROL) { const fallbackTreatment = fallbackCalculator(featureFlagName, label); @@ -163,10 +162,10 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl time: Date.now(), bucketingKey, label, - changeNumber: changeNumber as number, - properties - }, - disabled: impressionsDisabled + changeNumber: definition!.changeNumber, + properties: stringify(options.properties) + }, // @ts-expect-error impressionsDisabled is not exposed in the public typings yet. + disabled: options.impressionsDisabled || definition!.impressionsDisabled }); }