From f6330dc7c7ea59a3de71d0b894bd05cc10619355 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Jul 2026 22:43:01 -0300 Subject: [PATCH 1/2] 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/2] 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;