From 6241b0f0356fa50f7a8b144a5b8d3ba1a961539c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 9 Mar 2026 11:20:16 -0300 Subject: [PATCH 01/10] Extract SDK lifecycle methods (init, flush, destroy) into separate module, to reuse by Configs SDK and FF SDK main client. --- src/sdkClient/sdkClient.ts | 70 +-------------------- src/sdkClient/sdkLifecycle.ts | 77 +++++++++++++++++++++++ src/sdkConfig/index.ts | 112 ++++++++++++++++++++++++++++++++++ types/splitio.d.ts | 70 +++++++++++++++++++++ 4 files changed, 262 insertions(+), 67 deletions(-) create mode 100644 src/sdkClient/sdkLifecycle.ts create mode 100644 src/sdkConfig/index.ts diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index f72cb4ea..56ebc8f1 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -1,40 +1,16 @@ import { objectAssign } from '../utils/lang/objectAssign'; import SplitIO from '../../types/splitio'; -import { releaseApiKey, validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; import { clientFactory } from './client'; import { clientInputValidationDecorator } from './clientInputValidation'; import { ISdkFactoryContext } from '../sdkFactory/types'; - -const COOLDOWN_TIME_IN_MILLIS = 1000; +import { sdkLifecycleFactory } from './sdkLifecycle'; /** * Creates an Sdk client, i.e., a base client with status, init, flush and destroy interface */ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: boolean): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, uniqueKeysTracker } = params; - - let hasInit = false; - let lastActionTime = 0; - - function __cooldown(func: Function, time: number) { - const now = Date.now(); - //get the actual time elapsed in ms - const timeElapsed = now - lastActionTime; - //check if the time elapsed is less than desired cooldown - if (timeElapsed < time) { - //if yes, return message with remaining time in seconds - settings.log.warn(`Flush cooldown, remaining time ${(time - timeElapsed) / 1000} seconds`); - return Promise.resolve(); - } else { - //Do the requested action and re-assign the lastActionTime - lastActionTime = now; - return func(); - } - } + const { sdkReadinessManager, settings } = params; - function __flush() { - return syncManager ? syncManager.flush() : Promise.resolve(); - } return objectAssign( // Proto-linkage of the readiness Event Emitter @@ -48,46 +24,6 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo params.fallbackTreatmentsCalculator ), - { - init() { - if (hasInit) return; - hasInit = true; - - if (!isSharedClient) { - validateAndTrackApiKey(settings.log, settings.core.authorizationKey); - sdkReadinessManager.readinessManager.init(); - uniqueKeysTracker.start(); - syncManager && syncManager.start(); - signalListener && signalListener.start(); - } - }, - - flush() { - // @TODO define cooldown time - return __cooldown(__flush, COOLDOWN_TIME_IN_MILLIS); - }, - - destroy() { - hasInit = false; - // Mark the SDK as destroyed immediately - sdkReadinessManager.readinessManager.destroy(); - - // For main client, cleanup the SDK Key, listeners and scheduled jobs, and record stat before flushing data - if (!isSharedClient) { - releaseApiKey(settings.core.authorizationKey); - telemetryTracker.sessionLength(); - signalListener && signalListener.stop(); - uniqueKeysTracker.stop(); - } - - // Stop background jobs - syncManager && syncManager.stop(); - - return __flush().then(() => { - // Cleanup storage - return storage.destroy(); - }); - } - } + sdkLifecycleFactory(params, isSharedClient) ); } diff --git a/src/sdkClient/sdkLifecycle.ts b/src/sdkClient/sdkLifecycle.ts new file mode 100644 index 00000000..51b859be --- /dev/null +++ b/src/sdkClient/sdkLifecycle.ts @@ -0,0 +1,77 @@ +import SplitIO from '../../types/splitio'; +import { releaseApiKey, validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; +import { ISdkFactoryContext } from '../sdkFactory/types'; + +const COOLDOWN_TIME_IN_MILLIS = 1000; + +/** + * Creates an Sdk client, i.e., a base client with status, init, flush and destroy interface + */ +export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?: boolean): Pick { + const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, uniqueKeysTracker } = params; + + let hasInit = false; + let lastActionTime = 0; + + function __cooldown(func: Function, time: number) { + const now = Date.now(); + //get the actual time elapsed in ms + const timeElapsed = now - lastActionTime; + //check if the time elapsed is less than desired cooldown + if (timeElapsed < time) { + //if yes, return message with remaining time in seconds + settings.log.warn(`Flush cooldown, remaining time ${(time - timeElapsed) / 1000} seconds`); + return Promise.resolve(); + } else { + //Do the requested action and re-assign the lastActionTime + lastActionTime = now; + return func(); + } + } + + function __flush() { + return syncManager ? syncManager.flush() : Promise.resolve(); + } + + return { + init() { + if (hasInit) return; + hasInit = true; + + if (!isSharedClient) { + validateAndTrackApiKey(settings.log, settings.core.authorizationKey); + sdkReadinessManager.readinessManager.init(); + uniqueKeysTracker.start(); + syncManager && syncManager.start(); + signalListener && signalListener.start(); + } + }, + + flush() { + // @TODO define cooldown time + return __cooldown(__flush, COOLDOWN_TIME_IN_MILLIS); + }, + + destroy() { + hasInit = false; + // Mark the SDK as destroyed immediately + sdkReadinessManager.readinessManager.destroy(); + + // For main client, cleanup the SDK Key, listeners and scheduled jobs, and record stat before flushing data + if (!isSharedClient) { + releaseApiKey(settings.core.authorizationKey); + telemetryTracker.sessionLength(); + signalListener && signalListener.stop(); + uniqueKeysTracker.stop(); + } + + // Stop background jobs + syncManager && syncManager.stop(); + + return __flush().then(() => { + // Cleanup storage + return storage.destroy(); + }); + } + }; +} diff --git a/src/sdkConfig/index.ts b/src/sdkConfig/index.ts new file mode 100644 index 00000000..077e6272 --- /dev/null +++ b/src/sdkConfig/index.ts @@ -0,0 +1,112 @@ +import { ISdkFactoryContext, ISdkFactoryContextSync, ISdkFactoryParams } from '../sdkFactory/types'; +import { sdkReadinessManagerFactory } from '../readiness/sdkReadinessManager'; +import { impressionsTrackerFactory } from '../trackers/impressionsTracker'; +import { eventTrackerFactory } from '../trackers/eventTracker'; +import { telemetryTrackerFactory } from '../trackers/telemetryTracker'; +import SplitIO from '../../types/splitio'; +import { createLoggerAPI } from '../logger/sdkLogger'; +import { NEW_FACTORY } from '../logger/constants'; +import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../readiness/constants'; +import { objectAssign } from '../utils/lang/objectAssign'; +import { strategyDebugFactory } from '../trackers/strategy/strategyDebug'; +import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized'; +import { strategyNoneFactory } from '../trackers/strategy/strategyNone'; +import { uniqueKeysTrackerFactory } from '../trackers/uniqueKeysTracker'; +import { DEBUG, OPTIMIZED } from '../utils/constants'; +import { setRolloutPlan } from '../storages/setRolloutPlan'; +import { IStorageSync } from '../storages/types'; +import { getMatching } from '../utils/key'; +import { FallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator'; +import { sdkLifecycleFactory } from '../sdkClient/sdkLifecycle'; + +/** + * Modular SDK factory + */ +export function sdkConfigFactory(params: ISdkFactoryParams): SplitIO.ConfigSDKClient { + + const { settings, platform, storageFactory, splitApiFactory, extraProps, + syncManagerFactory, SignalListener, impressionsObserverFactory, + integrationsManagerFactory, + filterAdapterFactory } = params; + const { log, sync: { impressionsMode }, initialRolloutPlan, core: { key } } = settings; + + // @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid SDK Key, etc. + // On non-recoverable errors, we should mark the SDK as destroyed and not start synchronization. + + const sdkReadinessManager = sdkReadinessManagerFactory(platform.EventEmitter, settings); + const readiness = sdkReadinessManager.readinessManager; + + const storage = storageFactory({ + settings, + onReadyCb(error) { + if (error) { + // If storage fails to connect, SDK_READY_TIMED_OUT event is emitted immediately. Review when timeout and non-recoverable errors are reworked + readiness.timeout(); + return; + } + readiness.splits.emit(SDK_SPLITS_ARRIVED); + readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + }, + onReadyFromCacheCb() { + readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); + } + }); + + const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.fallbackTreatments); + + if (initialRolloutPlan) { + setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key)); + if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false /* Not an initial load, cache exists */ }); + } + + const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); + const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker }); + + const observer = impressionsObserverFactory(); + const uniqueKeysTracker = uniqueKeysTrackerFactory(log, storage.uniqueKeys, filterAdapterFactory && filterAdapterFactory()); + + const noneStrategy = strategyNoneFactory(storage.impressionCounts, uniqueKeysTracker); + const strategy = impressionsMode === OPTIMIZED ? + strategyOptimizedFactory(observer, storage.impressionCounts) : + impressionsMode === DEBUG ? + strategyDebugFactory(observer) : + noneStrategy; + + const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, noneStrategy, strategy, integrationsManager, storage.telemetry); + const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry); + + // splitApi is used by SyncManager and Browser signal listener + const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker); + + const ctx: ISdkFactoryContext = { clients: {}, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform, fallbackTreatmentsCalculator }; + + const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync); + ctx.syncManager = syncManager; + + const signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi); + ctx.signalListener = signalListener; + + log.info(NEW_FACTORY, [settings.version]); + + return objectAssign( + Object.create(sdkReadinessManager.sdkStatus) as SplitIO.IStatusInterface, + sdkLifecycleFactory(ctx), + { + getConfig(name: string, target?: SplitIO.Target): SplitIO.Config { + return { + value: name + target, + } as SplitIO.Config; + }, + + track() { + return false; + }, + + // Logger wrapper API + Logger: createLoggerAPI(log), + + settings, + }, + extraProps && extraProps(ctx) + ); +} diff --git a/types/splitio.d.ts b/types/splitio.d.ts index b8753566..76219ec5 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -2284,4 +2284,74 @@ declare namespace SplitIO { */ split(featureFlagName: string): SplitViewAsync; } + + // Configs SDK + + interface Target extends EvaluationOptions { + key: SplitKey; + attributes?: Attributes; + } + + interface Config { + value: any; + getString(propertyName: string, propertyDefaultValue?: string): string; + getNumber(propertyName: string, propertyDefaultValue?: number): number; + getBoolean(propertyName: string, propertyDefaultValue?: boolean): boolean; + getArray(propertyName: string): ConfigArray; + getObject(propertyName: string): Config; + } + interface ConfigArray { + value: any; + getString(index: number, propertyDefaultValue?: string): string; + getNumber(index: number, propertyDefaultValue?: number): number; + getBoolean(index: number, propertyDefaultValue?: boolean): boolean; + getArray(index: number): ConfigArray; + getObject(index: number): Config; + } + + /** + * Common definitions between SDK instances for different environments interface. + */ + interface ConfigSDKClient extends IStatusInterface { + /** + * Current settings of the SDK instance. + */ + settings: ISettings; + /** + * Logger API. + */ + Logger: ILoggerAPI; + /** + * Initializes the client. + */ + init(): void; + /** + * Flushes the client. + */ + flush(): Promise; + /** + * Destroys the client. + * + * @returns A promise that resolves once all clients are destroyed. + */ + destroy(): Promise; + /** + * + * @param name + * @param target + * @returns + */ + getConfig(name: string, target?: Target): Config; + /** + * Tracks an event to be fed to the results product on Split user interface. + * + * @param key - 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(key: SplitKey, trafficType: string, eventType: string, value?: number, properties?: Properties): boolean; + } } From 767324de865799b9e08856b741b708562b66c202 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 Mar 2026 11:44:36 -0300 Subject: [PATCH 02/10] Add IConfig DTO and Configs SDK client wrapper --- src/dtos/types.ts | 47 +++++++++++++- src/evaluator/types.ts | 2 +- src/sdkClient/client.ts | 8 +-- src/sdkConfig/configObject.ts | 69 +++++++++++++++++++++ src/sdkConfig/index-ff-wrapper.ts | 68 ++++++++++++++++++++ src/sdkManager/index.ts | 2 +- src/services/__tests__/splitApi.spec.ts | 24 ++++--- src/services/splitApi.ts | 7 ++- src/services/types.ts | 1 + src/storages/KeyBuilderSS.ts | 1 + src/sync/polling/fetchers/configsFetcher.ts | 53 ++++++++++++++++ src/sync/polling/pollingManagerSS.ts | 7 +-- src/sync/submitters/types.ts | 22 ++++--- src/utils/constants/index.ts | 2 + src/utils/inputValidation/target.ts | 21 +++++++ 15 files changed, 305 insertions(+), 29 deletions(-) create mode 100644 src/sdkConfig/configObject.ts create mode 100644 src/sdkConfig/index-ff-wrapper.ts create mode 100644 src/sync/polling/fetchers/configsFetcher.ts create mode 100644 src/utils/inputValidation/target.ts diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 6a252c8c..2c81b222 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -215,6 +215,37 @@ export interface IRBSegment { } | null } +// Similar to ISplit +// - with optional fields related to targeting information and +// - an optional link fields that binds configurations to other entities +export interface IConfig { + name: string, + changeNumber: number, + status?: 'ACTIVE' | 'ARCHIVED', + conditions?: ISplitCondition[] | null, + prerequisites?: null | { + n: string, + ts: string[] + }[] + killed?: boolean, + defaultTreatment: string, + trafficTypeName?: string, + seed?: number, + trafficAllocation?: number, + trafficAllocationSeed?: number + configurations: { + [variantName: string]: string | object | null + }, + sets?: string[], + impressionsDisabled?: boolean, + // a map of entities (e.g., pipeline, feature-flag, etc) to configuration variants + links?: { + [entityType: string]: { + [entityName: string]: string + } + } +} + export interface ISplit { name: string, changeNumber: number, @@ -231,7 +262,7 @@ export interface ISplit { trafficAllocation?: number, trafficAllocationSeed?: number configurations?: { - [treatmentName: string]: string + [treatmentName: string]: string | object | null }, sets?: string[], impressionsDisabled?: boolean @@ -254,6 +285,20 @@ export interface ISplitChangesResponse { } } +/** Interface of the parsed JSON response of `/configs` */ +export interface IConfigsResponse { + configs?: { + t: number, + s?: number, + d: IConfig[] + }, + rbs?: { + t: number, + s?: number, + d: IRBSegment[] + } +} + /** Interface of the parsed JSON response of `/segmentChanges/{segmentName}` */ export interface ISegmentChangesResponse { name: string, diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 42900f06..5b1e5b3a 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -22,7 +22,7 @@ export interface IEvaluation { treatment?: string, label: string, changeNumber?: number, - config?: string | null + config?: string | object | null } export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean } diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 8828a557..451e7005 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -5,7 +5,7 @@ import { validateSplitExistence } from '../utils/inputValidation/splitExistence' import { validateTrafficTypeExistence } from '../utils/inputValidation/trafficTypeExistence'; import { SDK_NOT_READY } from '../utils/labels'; import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, 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, TRACK_FN_LABEL } from '../utils/constants'; -import { IEvaluationResult } from '../evaluator/types'; +import { IEvaluation, IEvaluationResult } from '../evaluator/types'; import SplitIO from '../../types/splitio'; import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; import { ISdkFactoryContext } from '../sdkFactory/types'; @@ -72,7 +72,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl 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, properties, withConfig, methodName, queue) as SplitIO.Treatment | SplitIO.TreatmentWithConfig; }); impressionsTracker.track(queue, attributes); @@ -101,7 +101,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl 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, properties, withConfig, methodName, queue) as SplitIO.Treatment | SplitIO.TreatmentWithConfig; }); impressionsTracker.track(queue, attributes); @@ -139,7 +139,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl withConfig: boolean, invokingMethodName: string, queue: ImpressionDecorated[] - ): SplitIO.Treatment | SplitIO.TreatmentWithConfig { + ): SplitIO.Treatment | Pick { const matchingKey = getMatching(key); const bucketingKey = getBucketing(key); diff --git a/src/sdkConfig/configObject.ts b/src/sdkConfig/configObject.ts new file mode 100644 index 00000000..a8769a47 --- /dev/null +++ b/src/sdkConfig/configObject.ts @@ -0,0 +1,69 @@ +import SplitIO from '../../types/splitio'; +import { isString } from '../utils/lang'; + +function createConfigObject(value: any): SplitIO.Config { + return { + value, + getString(propertyName: string, propertyDefaultValue?: string): string { + const val = value != null ? value[propertyName] : undefined; + if (typeof val === 'string') return val; + return propertyDefaultValue !== undefined ? propertyDefaultValue : ''; + }, + getNumber(propertyName: string, propertyDefaultValue?: number): number { + const val = value != null ? value[propertyName] : undefined; + if (typeof val === 'number') return val; + return propertyDefaultValue !== undefined ? propertyDefaultValue : 0; + }, + getBoolean(propertyName: string, propertyDefaultValue?: boolean): boolean { + const val = value != null ? value[propertyName] : undefined; + if (typeof val === 'boolean') return val; + return propertyDefaultValue !== undefined ? propertyDefaultValue : false; + }, + getArray(propertyName: string): SplitIO.ConfigArray { + const val = value != null ? value[propertyName] : undefined; + return createConfigArrayObject(Array.isArray(val) ? val : []); + }, + getObject(propertyName: string): SplitIO.Config { + const val = value != null ? value[propertyName] : undefined; + return createConfigObject(val != null && typeof val === 'object' && !Array.isArray(val) ? val : null); + } + }; +} + +function createConfigArrayObject(arr: any[]): SplitIO.ConfigArray { + return { + value: arr, + getString(index: number, propertyDefaultValue?: string): string { + const val = arr[index]; + if (typeof val === 'string') return val; + return propertyDefaultValue !== undefined ? propertyDefaultValue : ''; + }, + getNumber(index: number, propertyDefaultValue?: number): number { + const val = arr[index]; + if (typeof val === 'number') return val; + return propertyDefaultValue !== undefined ? propertyDefaultValue : 0; + }, + getBoolean(index: number, propertyDefaultValue?: boolean): boolean { + const val = arr[index]; + if (typeof val === 'boolean') return val; + return propertyDefaultValue !== undefined ? propertyDefaultValue : false; + }, + getArray(index: number): SplitIO.ConfigArray { + const val = arr[index]; + return createConfigArrayObject(Array.isArray(val) ? val : []); + }, + getObject(index: number): SplitIO.Config { + const val = arr[index]; + return createConfigObject(val != null && typeof val === 'object' && !Array.isArray(val) ? val : null); + } + }; +} + +export function parseConfig(config: string | object | null): SplitIO.Config { + try { + // @ts-ignore + return createConfigObject(isString(config) ? JSON.parse(config) : config); + } catch { + return createConfigObject(null); + } +} diff --git a/src/sdkConfig/index-ff-wrapper.ts b/src/sdkConfig/index-ff-wrapper.ts new file mode 100644 index 00000000..6a2c516b --- /dev/null +++ b/src/sdkConfig/index-ff-wrapper.ts @@ -0,0 +1,68 @@ +import { ISdkFactoryParams } from '../sdkFactory/types'; +import { sdkFactory } from '../sdkFactory/index'; +import SplitIO from '../../types/splitio'; +import { objectAssign } from '../utils/lang/objectAssign'; +import { parseConfig } from './configObject'; +import { validateTarget } from '../utils/inputValidation/target'; +import { GET_CONFIG } from '../utils/constants'; +import { ISettings } from '../types'; + +/** + * Configs SDK Client factory implemented as a wrapper over the FF SDK. + * Exposes getConfig and track at the root level instead of requiring a client() call. + * getConfig delegates to getTreatmentWithConfig and wraps the parsed JSON config in a Config object. + */ +export function configsClientFactory(params: ISdkFactoryParams): SplitIO.ConfigsClient { + const ffSdk = sdkFactory({ ...params, lazyInit: true }) as (SplitIO.ISDK | SplitIO.IAsyncSDK) & { init(): void }; + const ffClient = ffSdk.client() as SplitIO.IClient & { init(): void; flush(): Promise }; + const ffManager = ffSdk.manager(); + const log = (ffSdk.settings as ISettings).log; + + return objectAssign( + // Inherit status interface (EventEmitter, Event, getStatus, ready, whenReady, whenReadyFromCache) from ffClient + Object.create(ffClient) as SplitIO.IStatusInterface, + { + settings: ffSdk.settings, + Logger: ffSdk.Logger, + + init() { + ffSdk.init(); + }, + + flush(): Promise { + return ffClient.flush(); + }, + + destroy(): Promise { + return ffSdk.destroy(); + }, + + getConfig(name: string, target?: SplitIO.Target): SplitIO.Config { + if (target) { + // Serve config with target + if (validateTarget(log, target, GET_CONFIG)) { + const result = ffClient.getTreatmentWithConfig(target.key, name, target.attributes, target) as SplitIO.TreatmentWithConfig; + return parseConfig(result.config); + } else { + log.error('Invalid target for getConfig.'); + } + } + + // Serve config without target + const config = ffManager.split(name) as SplitIO.SplitView; + if (!config) { + log.error('Provided config name does not exist. Serving empty config object.'); + return parseConfig({}); + } + + log.info('Serving default config variant, ' + config.defaultTreatment + ' for config ' + name); + const defaultConfigVariant = config.configs[config.defaultTreatment]; + return parseConfig(defaultConfigVariant); + }, + + track(key: SplitIO.SplitKey, trafficType: string, eventType: string, value?: number, properties?: SplitIO.Properties): boolean { + return ffClient.track(key, trafficType, eventType, value, properties) as boolean; + } + } + ); +} diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 5260170c..0d792265 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -29,7 +29,7 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { killed: splitObject.killed, changeNumber: splitObject.changeNumber || 0, treatments: collectTreatments(splitObject), - configs: splitObject.configurations || {}, + configs: splitObject.configurations as Record || {}, sets: splitObject.sets || [], defaultTreatment: splitObject.defaultTreatment, impressionsDisabled: splitObject.impressionsDisabled === true, diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 196266a3..c2f63500 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -45,22 +45,27 @@ describe('splitApi', () => { assertHeaders(settings, headers); expect(url).toBe(expectedFlagsUrl(-1, 100, settings.validateFilters || false, settings, -1)); + splitApi.fetchConfigs(-1, false, 100, -1); + [url, { headers }] = fetchMock.mock.calls[4]; + assertHeaders(settings, headers); + expect(url).toBe(expectedConfigsUrl(-1, 100, settings.validateFilters || false, settings, -1)); + splitApi.postEventsBulk('fake-body'); - assertHeaders(settings, fetchMock.mock.calls[4][1].headers); + assertHeaders(settings, fetchMock.mock.calls[5][1].headers); splitApi.postTestImpressionsBulk('fake-body'); - assertHeaders(settings, fetchMock.mock.calls[5][1].headers); - expect(fetchMock.mock.calls[5][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode); + assertHeaders(settings, fetchMock.mock.calls[6][1].headers); + expect(fetchMock.mock.calls[6][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode); splitApi.postTestImpressionsCount('fake-body'); - assertHeaders(settings, fetchMock.mock.calls[6][1].headers); + assertHeaders(settings, fetchMock.mock.calls[7][1].headers); splitApi.postMetricsConfig('fake-body'); - assertHeaders(settings, fetchMock.mock.calls[7][1].headers); - splitApi.postMetricsUsage('fake-body'); assertHeaders(settings, fetchMock.mock.calls[8][1].headers); + splitApi.postMetricsUsage('fake-body'); + assertHeaders(settings, fetchMock.mock.calls[9][1].headers); - expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(9); + expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(10); telemetryTrackerMock.trackHttp.mockClear(); fetchMock.mockClear(); @@ -70,6 +75,11 @@ describe('splitApi', () => { const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; return `sdk/splitChanges?s=1.1&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`; } + + function expectedConfigsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings, rbSince?: number) { + const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; + return `sdk/configs?${settings.sync.flagSpecVersion ? `s=${settings.sync.flagSpecVersion}&` : ''}since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`; + } }); test('rejects requests if fetch Api is not provided', (done) => { diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 6860b022..67d7834f 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -4,7 +4,7 @@ import { splitHttpClientFactory } from './splitHttpClient'; import { ISplitApi } from './types'; import { objectAssign } from '../utils/lang/objectAssign'; import { ITelemetryTracker } from '../trackers/types'; -import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants'; +import { SPLITS, CONFIGS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants'; import { ERROR_TOO_MANY_SETS } from '../logger/constants'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -61,6 +61,11 @@ export function splitApiFactory( }); }, + fetchConfigs(since: number, noCache?: boolean, till?: number, rbSince?: number) { + const url = `${urls.sdk}/configs?${settings.sync.flagSpecVersion ? `s=${settings.sync.flagSpecVersion}&` : ''}since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(CONFIGS)); + }, + fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean, till?: number) { const url = `${urls.sdk}/segmentChanges/${segmentName}?since=${since}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SEGMENT)); diff --git a/src/services/types.ts b/src/services/types.ts index b747dbb5..fa2261fb 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -60,6 +60,7 @@ export interface ISplitApi { getEventsAPIHealthCheck: IHealthCheckAPI fetchAuth: IFetchAuth fetchSplitChanges: IFetchSplitChanges + fetchConfigs: IFetchSplitChanges fetchSegmentChanges: IFetchSegmentChanges fetchMemberships: IFetchMemberships postEventsBulk: IPostEventsBulk diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index cf8d2156..238abcad 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -11,6 +11,7 @@ export const METHOD_NAMES: Record = { tfs: 'treatmentsByFlagSets', tcf: 'treatmentsWithConfigByFlagSet', tcfs: 'treatmentsWithConfigByFlagSets', + c: 'config', tr: 'track' }; diff --git a/src/sync/polling/fetchers/configsFetcher.ts b/src/sync/polling/fetchers/configsFetcher.ts new file mode 100644 index 00000000..0daeee7e --- /dev/null +++ b/src/sync/polling/fetchers/configsFetcher.ts @@ -0,0 +1,53 @@ +import { IConfig, IConfigsResponse, ISplitChangesResponse } from '../../../dtos/types'; +import { IFetchSplitChanges, IResponse } from '../../../services/types'; +import { ISplitChangesFetcher } from './types'; + +/** + * Factory of Configs fetcher. + * Configs fetcher is a wrapper around `configs` API service that parses the response and handle errors. + */ +export function configsFetcherFactory(fetchConfigs: IFetchSplitChanges): ISplitChangesFetcher { + + return function configsFetcher( + since: number, + noCache?: boolean, + till?: number, + rbSince?: number, + // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker + decorator?: (promise: Promise) => Promise + ): Promise { + + let configsPromise = fetchConfigs(since, noCache, till, rbSince); + if (decorator) configsPromise = decorator(configsPromise); + + return configsPromise + .then((resp: IResponse) => resp.json()) + .then((configs: IConfigsResponse) => { + return convertConfigsToSplits(configs); + }); + }; + +} + +function convertConfigsToSplits(configs: IConfigsResponse): ISplitChangesResponse { + return { + ...configs, + ff: configs.configs ? { + ...configs.configs, + d: configs.configs.d?.map((config: IConfig) => { + // @TODO: review defaults + return { + ...config, + defaultTreatment: config.defaultTreatment, + conditions: config.conditions || [], + killed: config.killed || false, + trafficTypeName: config.trafficTypeName || 'user', + seed: config.seed || 0, + trafficAllocation: config.trafficAllocation || 0, + trafficAllocationSeed: config.trafficAllocationSeed || 0, + }; + }) + } : undefined, + rbs: configs.rbs + }; +} diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index cea57dfe..2adf2ca8 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -1,14 +1,15 @@ import { splitsSyncTaskFactory } from './syncTasks/splitsSyncTask'; import { segmentsSyncTaskFactory } from './syncTasks/segmentsSyncTask'; import { IPollingManager, ISegmentsSyncTask, ISplitsSyncTask } from './types'; -import { POLLING_START, POLLING_STOP, LOG_PREFIX_SYNC_POLLING } from '../../logger/constants'; +import { POLLING_START, POLLING_STOP } from '../../logger/constants'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; /** * Expose start / stop mechanism for pulling data from services. */ export function pollingManagerSSFactory( - params: ISdkFactoryContextSync + params: ISdkFactoryContextSync, + // @TODO ): IPollingManager { const { splitApi, storage, readiness, settings } = params; @@ -24,8 +25,6 @@ export function pollingManagerSSFactory( // Start periodic fetching (polling) start() { log.info(POLLING_START); - log.debug(LOG_PREFIX_SYNC_POLLING + `Splits will be refreshed each ${settings.scheduler.featuresRefreshRate} millis`); - log.debug(LOG_PREFIX_SYNC_POLLING + `Segments will be refreshed each ${settings.scheduler.segmentsRefreshRate} millis`); const startingUp = splitsSyncTask.start(); if (startingUp) { diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 36a76c9b..a97debb7 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -102,22 +102,24 @@ export type TELEMETRY = 'te'; export type TOKEN = 'to'; export type SEGMENT = 'se'; export type MEMBERSHIPS = 'ms'; -export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MEMBERSHIPS; +export type CONFIGS = 'cf'; +export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MEMBERSHIPS | CONFIGS; export type LastSync = Partial> export type HttpErrors = Partial> export type HttpLatencies = Partial>> -export type TREATMENT = 't'; -export type TREATMENTS = 'ts'; -export type TREATMENT_WITH_CONFIG = 'tc'; -export type TREATMENTS_WITH_CONFIG = 'tcs'; +export type GET_TREATMENT = 't'; +export type GET_TREATMENTS = 'ts'; +export type GET_TREATMENT_WITH_CONFIG = 'tc'; +export type GET_TREATMENTS_WITH_CONFIG = 'tcs'; export type TRACK = 'tr'; -export type TREATMENTS_BY_FLAGSET = 'tf' -export type TREATMENTS_BY_FLAGSETS = 'tfs' -export type TREATMENTS_WITH_CONFIG_BY_FLAGSET = 'tcf' -export type TREATMENTS_WITH_CONFIG_BY_FLAGSETS = 'tcfs' -export type Method = TREATMENT | TREATMENTS | TREATMENT_WITH_CONFIG | TREATMENTS_WITH_CONFIG | TRACK | TREATMENTS_BY_FLAGSET | TREATMENTS_BY_FLAGSETS | TREATMENTS_WITH_CONFIG_BY_FLAGSET | TREATMENTS_WITH_CONFIG_BY_FLAGSETS; +export type GET_TREATMENTS_BY_FLAGSET = 'tf' +export type GET_TREATMENTS_BY_FLAGSETS = 'tfs' +export type GET_TREATMENTS_WITH_CONFIG_BY_FLAGSET = 'tcf' +export type GET_TREATMENTS_WITH_CONFIG_BY_FLAGSETS = 'tcfs' +export type GET_CONFIG = 'c'; +export type Method = GET_TREATMENT | GET_TREATMENTS | GET_TREATMENT_WITH_CONFIG | GET_TREATMENTS_WITH_CONFIG | TRACK | GET_TREATMENTS_BY_FLAGSET | GET_TREATMENTS_BY_FLAGSETS | GET_TREATMENTS_WITH_CONFIG_BY_FLAGSET | GET_TREATMENTS_WITH_CONFIG_BY_FLAGSETS | GET_CONFIG; export type MethodLatencies = Partial>>; diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 6686c68e..27c0050e 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -47,6 +47,7 @@ export const GET_TREATMENTS_BY_FLAG_SET = 'getTreatmentsByFlagSet'; export const GET_TREATMENTS_BY_FLAG_SETS = 'getTreatmentsByFlagSets'; export const GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET = 'getTreatmentsWithConfigByFlagSet'; export const GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'getTreatmentsWithConfigByFlagSets'; +export const GET_CONFIG = 'getConfig'; export const TRACK_FN_LABEL = 'track'; // Manager method names @@ -75,6 +76,7 @@ export const TELEMETRY = 'te'; export const TOKEN = 'to'; export const SEGMENT = 'se'; export const MEMBERSHIPS = 'ms'; +export const CONFIGS = 'cf'; export const TREATMENT = 't'; export const TREATMENTS = 'ts'; diff --git a/src/utils/inputValidation/target.ts b/src/utils/inputValidation/target.ts new file mode 100644 index 00000000..393e65d7 --- /dev/null +++ b/src/utils/inputValidation/target.ts @@ -0,0 +1,21 @@ +import { isObject } from '../lang'; +import SplitIO from '../../../types/splitio'; +import { ILogger } from '../../logger/types'; +import { validateKey } from './key'; +import { validateAttributes } from './attributes'; +import { ERROR_NOT_PLAIN_OBJECT } from '../../logger/constants'; + +export function validateTarget(log: ILogger, maybeTarget: any, method: string): SplitIO.Target | false { + if (!isObject(maybeTarget)) { + log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'target']); + return false; + } + + const key = validateKey(log, maybeTarget.key, method); + if (key === false) return false; + + const attributes = validateAttributes(log, maybeTarget.attributes, method); + if (attributes === false) return false; + + return { ...maybeTarget, key, attributes }; +} From c3965550bdbbdfee42218c88ee080ceddb1b480e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 13 Mar 2026 03:08:30 -0300 Subject: [PATCH 03/10] Consolidate impression logging into single message when queueing --- src/logger/constants.ts | 1 - src/logger/messages/info.ts | 3 +-- src/sdkClient/client.ts | 17 ++++++++--------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index ca331f82..0a541f95 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -32,7 +32,6 @@ export const ENGINE_DEFAULT = 41; export const CLIENT_READY_FROM_CACHE = 100; export const CLIENT_READY = 101; -export const IMPRESSION = 102; export const IMPRESSION_QUEUEING = 103; export const NEW_SHARED_CLIENT = 104; export const NEW_FACTORY = 105; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 1e9b5f0d..17f539c3 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -8,8 +8,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.CLIENT_READY_FROM_CACHE, READY_MSG + ' from cache'], [c.CLIENT_READY, READY_MSG], // SDK - [c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Feature flag: %s. Key: %s. Evaluation: %s. Label: %s'], - [c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression.'], + [c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression. Feature flag: %s. Key: %s. Evaluation: %s. Label: %s'], [c.NEW_SHARED_CLIENT, 'New shared client instance created.'], [c.NEW_FACTORY, 'New Split SDK instance created. %s'], [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'], diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 6721b12f..ca3835dc 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -7,7 +7,7 @@ import { SDK_NOT_READY } from '../utils/labels'; import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, 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, TRACK_FN_LABEL } from '../utils/constants'; import { IEvaluationResult } from '../evaluator/types'; import SplitIO from '../../types/splitio'; -import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; +import { IMPRESSION_QUEUEING } from '../logger/constants'; import { ISdkFactoryContext } from '../sdkFactory/types'; import { isConsumerMode } from '../utils/settingsValidation/mode'; import { Method } from '../sync/submitters/types'; @@ -39,7 +39,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const { log, mode } = settings; const isAsync = isConsumerMode(mode); - function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) { + function getTreatment(key: SplitIO.SplitKey | undefined, 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) => { @@ -134,15 +134,12 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl function processEvaluation( evaluation: IEvaluationResult, featureFlagName: string, - key: SplitIO.SplitKey, + key: SplitIO.SplitKey | undefined, properties: string | undefined, withConfig: boolean, invokingMethodName: string, queue: ImpressionDecorated[] ): SplitIO.Treatment | SplitIO.TreatmentWithConfig { - const matchingKey = getMatching(key); - const bucketingKey = getBucketing(key); - const { changeNumber, impressionsDisabled } = evaluation; let { treatment, label, config = null } = evaluation; @@ -153,10 +150,12 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl config = fallbackTreatment.config; } - log.info(IMPRESSION, [featureFlagName, matchingKey, treatment, label]); + // If no target/key, no impression is tracked + if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName) && key) { + const matchingKey = getMatching(key); + const bucketingKey = getBucketing(key); - if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) { - log.info(IMPRESSION_QUEUEING); + log.info(IMPRESSION_QUEUEING, [featureFlagName, matchingKey, treatment, label]); queue.push({ imp: { feature: featureFlagName, From c3d97309caec22b2574f81ee60783f353b2e95c1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 13 Mar 2026 03:30:03 -0300 Subject: [PATCH 04/10] Rename SPLIT_NOT_FOUND to DEFINITION_NOT_FOUND and update related references --- .gitignore | 8 ++++++ .../__tests__/evaluate-feature.spec.ts | 4 +-- .../__tests__/evaluate-features.spec.ts | 6 ++--- src/evaluator/index.ts | 4 +-- src/logger/constants.ts | 2 +- src/logger/messages/warn.ts | 2 +- src/sdkClient/client.ts | 4 +-- src/sdkConfig/index.ts | 2 +- src/sdkManager/index.ts | 6 ++--- .../__tests__/splitExistence.spec.ts | 26 +++++++++---------- .../inputValidation/definitionExistence.ts | 19 ++++++++++++++ src/utils/inputValidation/index.ts | 2 +- src/utils/inputValidation/splitExistence.ts | 19 -------------- src/utils/labels/index.ts | 4 +-- 14 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 src/utils/inputValidation/definitionExistence.ts delete mode 100644 src/utils/inputValidation/splitExistence.ts diff --git a/.gitignore b/.gitignore index 34d8005c..f294b4f1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,11 @@ ## coverage info /coverage + +## worktrees +/.worktrees + +## agents files +/AGENTS.md +/CLAUDE.md +/.claude diff --git a/src/evaluator/__tests__/evaluate-feature.spec.ts b/src/evaluator/__tests__/evaluate-feature.spec.ts index 85db31e7..825c0f10 100644 --- a/src/evaluator/__tests__/evaluate-feature.spec.ts +++ b/src/evaluator/__tests__/evaluate-feature.spec.ts @@ -1,5 +1,5 @@ import { evaluateFeature } from '../index'; -import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels'; +import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, DEFINITION_NOT_FOUND } from '../../utils/labels'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { ISplit } from '../../dtos/types'; import { IStorageSync } from '../../storages/types'; @@ -53,7 +53,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret config: '{color:\'black\'}', changeNumber: 1487277320548 }; const expectedOutputControl = { - treatment: 'control', label: SPLIT_NOT_FOUND, config: null + treatment: 'control', label: DEFINITION_NOT_FOUND, config: null }; const evaluationWithConfig = evaluateFeature( diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index 45832bd0..b6c28262 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -1,5 +1,5 @@ import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index'; -import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels'; +import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, DEFINITION_NOT_FOUND } from '../../utils/labels'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants'; import { ISplit } from '../../dtos/types'; @@ -71,7 +71,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre config: '{color:\'black\'}', changeNumber: 1487277320548 }, not_existent_split: { - treatment: 'control', label: SPLIT_NOT_FOUND, config: null + treatment: 'control', label: DEFINITION_NOT_FOUND, config: null }, }; @@ -122,7 +122,7 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => { config: '{color:\'black\'}', changeNumber: 1487277320548 }, not_existent_split: { - treatment: 'control', label: SPLIT_NOT_FOUND, config: null + treatment: 'control', label: DEFINITION_NOT_FOUND, config: null }, }; diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index b6cb3e97..9d217a31 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,6 +1,6 @@ import { engineParser } from './Engine'; import { thenable } from '../utils/promise/thenable'; -import { EXCEPTION, SPLIT_NOT_FOUND } from '../utils/labels'; +import { EXCEPTION, DEFINITION_NOT_FOUND } from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { ISplit, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; @@ -148,7 +148,7 @@ function getEvaluation( ): MaybeThenable { let evaluation: MaybeThenable = { treatment: CONTROL, - label: SPLIT_NOT_FOUND, + label: DEFINITION_NOT_FOUND, config: null }; diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 0a541f95..1005e5b8 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -71,7 +71,7 @@ export const WARN_SETTING_NULL = 211; export const WARN_TRIMMING_PROPERTIES = 212; export const WARN_CONVERTING = 213; export const WARN_TRIMMING = 214; -export const WARN_NOT_EXISTENT_SPLIT = 215; +export const WARN_NOT_EXISTENT_DEFINITION = 215; export const WARN_LOWERCASE_TRAFFIC_TYPE = 216; export const WARN_NOT_EXISTENT_TT = 217; export const WARN_INTEGRATION_INVALID = 218; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 4bd74dd6..6771fceb 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -21,7 +21,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'], [c.WARN_CONVERTING, '%s: %s "%s" is not of type string, converting.'], [c.WARN_TRIMMING, '%s: %s "%s" has extra whitespace, trimming.'], - [c.WARN_NOT_EXISTENT_SPLIT, '%s: feature flag "%s" does not exist in this environment. Please double check what feature flags exist in the Split user interface.'], + [c.WARN_NOT_EXISTENT_DEFINITION, '%s: definition "%s" does not exist in this environment. Please double check what definitions exist in the Split user interface.'], [c.WARN_LOWERCASE_TRAFFIC_TYPE, '%s: traffic_type_name should be all lowercase - converting string to lowercase.'], [c.WARN_NOT_EXISTENT_TT, '%s: traffic type "%s" does not have any corresponding feature flag in this environment, make sure you\'re tracking your events to a valid traffic type defined in the Split user interface.'], [c.WARN_FLAGSET_NOT_CONFIGURED, '%s: you passed %s which is not part of the configured FlagSetsFilter, ignoring Flag Set.'], diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index ec2ffc5e..f5f46878 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -1,7 +1,7 @@ import { evaluateFeature, evaluateFeatures, evaluateFeaturesByFlagSets } from '../evaluator'; import { thenable } from '../utils/promise/thenable'; import { getMatching, getBucketing } from '../utils/key'; -import { validateSplitExistence } from '../utils/inputValidation/splitExistence'; +import { validateDefinitionExistence } from '../utils/inputValidation/definitionExistence'; import { validateTrafficTypeExistence } from '../utils/inputValidation/trafficTypeExistence'; import { SDK_NOT_READY } from '../utils/labels'; import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, 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, TRACK_FN_LABEL } from '../utils/constants'; @@ -151,7 +151,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl } // If no target/key, no impression is tracked - if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName) && key) { + if (validateDefinitionExistence(log, readinessManager, featureFlagName, label, invokingMethodName) && key) { const matchingKey = getMatching(key); const bucketingKey = getBucketing(key); diff --git a/src/sdkConfig/index.ts b/src/sdkConfig/index.ts index 3005e558..93fad40a 100644 --- a/src/sdkConfig/index.ts +++ b/src/sdkConfig/index.ts @@ -42,7 +42,7 @@ export function sdkConfigFactory(params: ISdkFactoryParams): SplitIO.ConfigsClie } }); - const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.fallbackTreatments); + const fallbackTreatmentsCalculator = FallbackTreatmentsCalculator(settings.fallbackTreatments); const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker }); diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 0d792265..21e2b0e6 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -1,7 +1,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; import { find } from '../utils/lang'; -import { validateSplit, validateSplitExistence, validateIfOperational } from '../utils/inputValidation'; +import { validateSplit, validateDefinitionExistence, validateIfOperational } from '../utils/inputValidation'; import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types'; import { ISdkReadinessManager } from '../readiness/types'; import { ISplit } from '../dtos/types'; @@ -74,12 +74,12 @@ export function sdkManagerFactory null).then(result => { // handle possible rejections when using pluggable storage - validateSplitExistence(log, readinessManager, splitName, result, SPLIT_FN_LABEL); + validateDefinitionExistence(log, readinessManager, splitName, result, SPLIT_FN_LABEL); return objectToView(result); }); } - validateSplitExistence(log, readinessManager, splitName, split, SPLIT_FN_LABEL); + validateDefinitionExistence(log, readinessManager, splitName, split, SPLIT_FN_LABEL); return objectToView(split); }, diff --git a/src/utils/inputValidation/__tests__/splitExistence.spec.ts b/src/utils/inputValidation/__tests__/splitExistence.spec.ts index 9d78df9e..2a320e43 100644 --- a/src/utils/inputValidation/__tests__/splitExistence.spec.ts +++ b/src/utils/inputValidation/__tests__/splitExistence.spec.ts @@ -3,9 +3,9 @@ import * as LabelConstants from '../../labels'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { validateSplitExistence } from '../splitExistence'; +import { validateDefinitionExistence } from '../definitionExistence'; import { IReadinessManager } from '../../../readiness/types'; -import { WARN_NOT_EXISTENT_SPLIT } from '../../../logger/constants'; +import { WARN_NOT_EXISTENT_DEFINITION } from '../../../logger/constants'; describe('Split existence (special case)', () => { @@ -17,11 +17,11 @@ describe('Split existence (special case)', () => { isReady: jest.fn(() => false) // Fake the signal for the non ready SDK } as IReadinessManager; - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'some_split', {}, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'some_split', null, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'some_split', undefined, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'some_split', 'a label', 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'some_split', LabelConstants.SPLIT_NOT_FOUND, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'some_split', {}, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'some_split', null, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'some_split', undefined, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'some_split', 'a label', 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'some_split', LabelConstants.DEFINITION_NOT_FOUND, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. expect(loggerMock.warn).not.toBeCalled(); // There should have been no warning logs since the SDK was not ready yet. expect(loggerMock.error).not.toBeCalled(); // There should have been no error logs since the SDK was not ready yet. @@ -29,18 +29,18 @@ describe('Split existence (special case)', () => { // Prepare the mock to fake that the SDK is ready now. (readinessManagerMock.isReady as jest.Mock).mockImplementation(() => true); - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'other_split', {}, 'other_method')).toBe(true); // Should return true if it receives a Split Object instead of null (when the object is not found, for manager). - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'other_split', 'a label', 'other_method')).toBe(true); // Should return true if it receives a Label and it is not split not found (when the Split was not found on the storage, for client). + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'other_split', {}, 'other_method')).toBe(true); // Should return true if it receives a Split Object instead of null (when the object is not found, for manager). + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'other_split', 'a label', 'other_method')).toBe(true); // Should return true if it receives a Label and it is not split not found (when the Split was not found on the storage, for client). expect(loggerMock.warn).not.toBeCalled(); // There should have been no warning logs since the values we used so far were considered valid. expect(loggerMock.error).not.toBeCalled(); // There should have been no error logs since the values we used so far were considered valid. - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'other_split', null, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'other_split', undefined, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label - expect(validateSplitExistence(loggerMock, readinessManagerMock, 'other_split', LabelConstants.SPLIT_NOT_FOUND, 'other_method')).toBe(false); // Should return false if it receives a label but it is the split not found one. + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'other_split', null, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'other_split', undefined, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label + expect(validateDefinitionExistence(loggerMock, readinessManagerMock, 'other_split', LabelConstants.DEFINITION_NOT_FOUND, 'other_method')).toBe(false); // Should return false if it receives a label but it is the split not found one. expect(loggerMock.warn).toBeCalledTimes(3); // It should have logged 3 warnings, one per each time we called it - loggerMock.warn.mock.calls.forEach(call => expect(call).toEqual([WARN_NOT_EXISTENT_SPLIT, ['other_method', 'other_split']])); // Warning logs should have the correct message. + loggerMock.warn.mock.calls.forEach(call => expect(call).toEqual([WARN_NOT_EXISTENT_DEFINITION, ['other_method', 'other_split']])); // Warning logs should have the correct message. expect(loggerMock.error).not.toBeCalled(); // We log warnings, not errors. }); diff --git a/src/utils/inputValidation/definitionExistence.ts b/src/utils/inputValidation/definitionExistence.ts new file mode 100644 index 00000000..290d8b59 --- /dev/null +++ b/src/utils/inputValidation/definitionExistence.ts @@ -0,0 +1,19 @@ +import { FALLBACK_DEFINITION_NOT_FOUND, DEFINITION_NOT_FOUND } from '../labels'; +import { IReadinessManager } from '../../readiness/types'; +import { ILogger } from '../../logger/types'; +import { WARN_NOT_EXISTENT_DEFINITION } from '../../logger/constants'; + +/** + * This is defined here and in this format mostly because of the logger and the fact that it's considered a validation at product level. + * But it's not going to run on the input validation layer. In any case, the most compelling reason to use it as we do is to avoid going to Redis and get a definition twice. + */ +export function validateDefinitionExistence(log: ILogger, readinessManager: IReadinessManager, definitionName: string, labelOrDefinitionObj: any, method: string): boolean { + if (readinessManager.isReady()) { // Only if it's ready (synced with BE) we validate this, otherwise it may just be that the SDK is still syncing + if (labelOrDefinitionObj === DEFINITION_NOT_FOUND || labelOrDefinitionObj === FALLBACK_DEFINITION_NOT_FOUND || labelOrDefinitionObj == null) { + log.warn(WARN_NOT_EXISTENT_DEFINITION, [method, definitionName]); + return false; + } + } + + return true; +} diff --git a/src/utils/inputValidation/index.ts b/src/utils/inputValidation/index.ts index f6e06c5e..5e892ce8 100644 --- a/src/utils/inputValidation/index.ts +++ b/src/utils/inputValidation/index.ts @@ -8,6 +8,6 @@ export { validateSplit } from './split'; export { validateSplits } from './splits'; export { validateTrafficType } from './trafficType'; export { validateIfNotDestroyed, validateIfReadyFromCache, validateIfOperational } from './isOperational'; -export { validateSplitExistence } from './splitExistence'; +export { validateDefinitionExistence } from './definitionExistence'; export { validateTrafficTypeExistence } from './trafficTypeExistence'; export { validateEvaluationOptions } from './eventProperties'; diff --git a/src/utils/inputValidation/splitExistence.ts b/src/utils/inputValidation/splitExistence.ts deleted file mode 100644 index c8559b2a..00000000 --- a/src/utils/inputValidation/splitExistence.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FALLBACK_SPLIT_NOT_FOUND, SPLIT_NOT_FOUND } from '../labels'; -import { IReadinessManager } from '../../readiness/types'; -import { ILogger } from '../../logger/types'; -import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants'; - -/** - * This is defined here and in this format mostly because of the logger and the fact that it's considered a validation at product level. - * But it's not going to run on the input validation layer. In any case, the most compelling reason to use it as we do is to avoid going to Redis and get a split twice. - */ -export function validateSplitExistence(log: ILogger, readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean { - if (readinessManager.isReady()) { // Only if it's ready (synced with BE) we validate this, otherwise it may just be that the SDK is still syncing - if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj === FALLBACK_SPLIT_NOT_FOUND || labelOrSplitObj == null) { - log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]); - return false; - } - } - - return true; -} diff --git a/src/utils/labels/index.ts b/src/utils/labels/index.ts index 78117a1d..5eccf3a6 100644 --- a/src/utils/labels/index.ts +++ b/src/utils/labels/index.ts @@ -2,11 +2,11 @@ import { FALLBACK_PREFIX } from '../../evaluator/fallbackTreatmentsCalculator'; export const SPLIT_KILLED = 'killed'; export const NO_CONDITION_MATCH = 'default rule'; -export const SPLIT_NOT_FOUND = 'definition not found'; +export const DEFINITION_NOT_FOUND = 'definition not found'; export const SDK_NOT_READY = 'not ready'; export const EXCEPTION = 'exception'; export const SPLIT_ARCHIVED = 'archived'; export const NOT_IN_SPLIT = 'not in split'; export const UNSUPPORTED_MATCHER_TYPE = 'targeting rule type unsupported by sdk'; export const PREREQUISITES_NOT_MET = 'prerequisites not met'; -export const FALLBACK_SPLIT_NOT_FOUND = FALLBACK_PREFIX + SPLIT_NOT_FOUND; +export const FALLBACK_DEFINITION_NOT_FOUND = FALLBACK_PREFIX + DEFINITION_NOT_FOUND; From 30d102feac88046db98fbd04f29fa937c02d96dc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 13 Mar 2026 03:37:27 -0300 Subject: [PATCH 05/10] Remove feature flag name from SDK not ready warning message and simplify validation function signatures --- src/logger/messages/warn.ts | 2 +- src/sdkClient/clientInputValidation.ts | 2 +- src/sdkConfig/index-ff-wrapper.ts | 2 +- .../__tests__/isOperational.spec.ts | 2 +- src/utils/inputValidation/isOperational.ts | 8 +-- types/splitio.d.ts | 54 +++++++++++++++++-- 6 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 6771fceb..58f2ed72 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -14,7 +14,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Dropping %s after retry. Reason: %s.'], [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'], // client status - [c.CLIENT_NOT_READY_FROM_CACHE, '%s: the SDK is not ready to evaluate. Results may be incorrect%s. Make sure to wait for SDK readiness before using this method.'], + [c.CLIENT_NOT_READY_FROM_CACHE, '%s: the SDK is not ready to evaluate. Results may be incorrect. Make sure to wait for SDK readiness before using this method.'], [c.CLIENT_NO_LISTENER, 'No listeners for SDK_READY event detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet synchronized with the backend.'], // input validation [c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'], diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 9ed2a722..4f3f71ac 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -46,7 +46,7 @@ export function clientInputValidationDecorator { // @ts-ignore expect(validateIfReadyFromCache(loggerMock, readinessManagerMock, 'test_method')).toBe(false); // It should return true if SDK was ready. expect(readinessManagerMock.isReadyFromCache).toBeCalledTimes(1); // It checks for SDK_READY_FROM_CACHE status. - expect(loggerMock.warn).toBeCalledWith(CLIENT_NOT_READY_FROM_CACHE, ['test_method', '']); // It should log the expected warning. + expect(loggerMock.warn).toBeCalledWith(CLIENT_NOT_READY_FROM_CACHE, ['test_method']); // It should log the expected warning. expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. }); }); diff --git a/src/utils/inputValidation/isOperational.ts b/src/utils/inputValidation/isOperational.ts index 5f122926..5fbec731 100644 --- a/src/utils/inputValidation/isOperational.ts +++ b/src/utils/inputValidation/isOperational.ts @@ -9,14 +9,14 @@ export function validateIfNotDestroyed(log: ILogger, readinessManager: IReadines return false; } -export function validateIfReadyFromCache(log: ILogger, readinessManager: IReadinessManager, method: string, featureFlagNameOrNames?: string | string[] | false) { +export function validateIfReadyFromCache(log: ILogger, readinessManager: IReadinessManager, method: string) { if (readinessManager.isReadyFromCache()) return true; - log.warn(CLIENT_NOT_READY_FROM_CACHE, [method, featureFlagNameOrNames ? ` for feature flag ${featureFlagNameOrNames.toString()}` : '']); + log.warn(CLIENT_NOT_READY_FROM_CACHE, [method]); return false; } // Operational means that the SDK is ready to evaluate (not destroyed and ready from cache) -export function validateIfOperational(log: ILogger, readinessManager: IReadinessManager, method: string, featureFlagNameOrNames?: string | string[] | false) { - return validateIfNotDestroyed(log, readinessManager, method) && validateIfReadyFromCache(log, readinessManager, method, featureFlagNameOrNames); +export function validateIfOperational(log: ILogger, readinessManager: IReadinessManager, method: string) { + return validateIfNotDestroyed(log, readinessManager, method) && validateIfReadyFromCache(log, readinessManager, method); } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 9f1c4560..27577054 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -2287,7 +2287,7 @@ declare namespace SplitIO { // Configs SDK - interface Target extends EvaluationOptions { + interface Target { key: SplitKey; attributes?: Attributes; } @@ -2300,6 +2300,7 @@ declare namespace SplitIO { getArray(propertyName: string): ConfigArray; getObject(propertyName: string): Config; } + interface ConfigArray { value: any; getString(index: number, propertyDefaultValue?: string): string; @@ -2310,7 +2311,7 @@ declare namespace SplitIO { } /** - * Common definitions between SDK instances for different environments interface. + * Configs SDK client interface. */ interface ConfigsClient extends IStatusInterface { /** @@ -2342,7 +2343,7 @@ declare namespace SplitIO { * @param target - The target of the config we want to get. * @returns The config object. */ - getConfig(name: string, target?: Target): Config; + getConfig(name: string, target?: Target, options?: EvaluationOptions): Config; /** * Tracks an event to be fed to the results product on Split user interface. * @@ -2355,4 +2356,51 @@ declare namespace SplitIO { */ track(key: SplitKey, trafficType: string, eventType: string, value?: number, properties?: Properties): boolean; } + + /** + * Configs SDK client interface with async methods. + */ + interface AsyncConfigsClient extends IStatusInterface { + /** + * Current settings of the SDK instance. + */ + settings: ISettings; + /** + * Logger API. + */ + Logger: ILoggerAPI; + /** + * Initializes the client. + */ + init(): void; + /** + * Flushes the client. + */ + flush(): Promise; + /** + * 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 name - The name of the config we want to get. + * @param target - The target of the config we want to get. + * @returns A promise that resolves with the config object. + */ + getConfig(name: string, target?: Target, options?: EvaluationOptions): Promise; + /** + * Tracks an event to be fed to the results product on Split user interface. + * + * @param key - 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 A promise that resolves with a boolean indicating whether the event was added to the queue successfully or not. + */ + track(key: SplitKey, trafficType: string, eventType: string, value?: number, properties?: Properties): Promise; + } } From 858bb9616070a9ac5092e0e88b7450f2f445aeac Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 13 Mar 2026 04:30:46 -0300 Subject: [PATCH 06/10] Polishing --- src/sdkConfig/configObject.ts | 3 +-- src/utils/constants/index.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdkConfig/configObject.ts b/src/sdkConfig/configObject.ts index a8769a47..267b0879 100644 --- a/src/sdkConfig/configObject.ts +++ b/src/sdkConfig/configObject.ts @@ -59,9 +59,8 @@ function createConfigArrayObject(arr: any[]): SplitIO.ConfigArray { }; } -export function parseConfig(config: string | object | null): SplitIO.Config { +export function parseConfig(config?: string | object | null): SplitIO.Config { try { - // @ts-ignore return createConfigObject(isString(config) ? JSON.parse(config) : config); } catch { return createConfigObject(null); diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 27c0050e..352988e3 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -86,6 +86,7 @@ export const TREATMENTS_BY_FLAGSET = 'tf'; export const TREATMENTS_BY_FLAGSETS = 'tfs'; export const TREATMENTS_WITH_CONFIG_BY_FLAGSET = 'tcf'; export const TREATMENTS_WITH_CONFIG_BY_FLAGSETS = 'tcfs'; +export const CONFIG = 'c'; export const TRACK = 'tr'; export const CONNECTION_ESTABLISHED = 0; From 92170bc14b67069d415dc6d3fa3f5ecafe386ec1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Mar 2026 13:32:23 -0300 Subject: [PATCH 07/10] support configs fetcher --- .eslintrc | 1 + src/sdkClient/sdkLifecycle.ts | 3 +- src/sync/polling/pollingManagerSS.ts | 7 +- src/sync/polling/syncTasks/splitsSyncTask.ts | 4 +- .../__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 1 + src/types.ts | 1 + .../__tests__/settings.mocks.ts | 1 + src/utils/settingsValidation/index.ts | 3 + types/splitio.d.ts | 64 +++++++++++++++++-- 10 files changed, 75 insertions(+), 12 deletions(-) diff --git a/.eslintrc b/.eslintrc index cc505a94..e5d023ab 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,5 @@ { + "root": true, "extends": [ "eslint:recommended" ], diff --git a/src/sdkClient/sdkLifecycle.ts b/src/sdkClient/sdkLifecycle.ts index e101c851..a8a34988 100644 --- a/src/sdkClient/sdkLifecycle.ts +++ b/src/sdkClient/sdkLifecycle.ts @@ -1,4 +1,3 @@ -import SplitIO from '../../types/splitio'; import { releaseApiKey, validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; import { ISdkFactoryContext } from '../sdkFactory/types'; @@ -7,7 +6,7 @@ const COOLDOWN_TIME_IN_MILLIS = 1000; /** * Creates an Sdk client, i.e., a base client with status, init, flush and destroy interface */ -export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?: boolean): Pick { +export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?: boolean): { init(): void; flush(): Promise; destroy(): Promise } { const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, impressionsTracker } = params; let hasInit = false; diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index a31d654f..8a3f3c04 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -9,13 +9,14 @@ import { ISdkFactoryContextSync } from '../../sdkFactory/types'; */ export function pollingManagerSSFactory( params: ISdkFactoryContextSync, - // @TODO ): IPollingManager { const { splitApi, storage, readiness, settings } = params; const log = settings.log; - const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); + const fetchingConfigs = settings.definitionsType === 'configs'; + + const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(fetchingConfigs ? splitApi.fetchConfigs : splitApi.fetchSplitChanges, storage, readiness, settings); const segmentsSyncTask: ISegmentsSyncTask = segmentsSyncTaskFactory(splitApi.fetchSegmentChanges, storage, readiness, settings); return { @@ -25,7 +26,7 @@ export function pollingManagerSSFactory( // Start periodic fetching (polling) start() { log.info(POLLING_START); - log.debug(LOG_PREFIX_SYNC_POLLING + `Definitions will be refreshed each ${settings.scheduler.featuresRefreshRate} millis`); + log.debug(LOG_PREFIX_SYNC_POLLING + `${fetchingConfigs ? 'configs' : 'feature flags'} will be refreshed each ${fetchingConfigs ? settings.scheduler.configsRefreshRate : settings.scheduler.featuresRefreshRate} millis`); log.debug(LOG_PREFIX_SYNC_POLLING + `Segments will be refreshed each ${settings.scheduler.segmentsRefreshRate} millis`); const startingUp = splitsSyncTask.start(); diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index d385bf77..d66541da 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -29,7 +29,7 @@ export function splitsSyncTaskFactory( settings.startup.retriesOnFailureBeforeReady, isClientSide ), - settings.scheduler.featuresRefreshRate, - 'splitChangesUpdater', + settings.definitionsType === 'configs' ? settings.scheduler.configsRefreshRate : settings.scheduler.featuresRefreshRate, + settings.definitionsType === 'configs' ? 'configsUpdater' : 'splitChangesUpdater', ); } diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 57a368c5..caffa93a 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -77,7 +77,7 @@ describe('Telemetry submitter', () => { expect(recordTimeUntilReadySpy).toBeCalledTimes(1); expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ - oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, se: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0, fsT: 0, fsI: 0 + oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, cf: 0.001, se: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0, fsT: 0, fsI: 0 })); // Stop submitter, to not execute the 1st periodic metrics/usage POST diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 3cc19d31..ac39c5b1 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -81,6 +81,7 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett sE: settings.streamingEnabled, rR: { sp: scheduler.featuresRefreshRate / 1000, + cf: scheduler.configsRefreshRate / 1000, se: isServerSide ? scheduler.segmentsRefreshRate / 1000 : undefined, ms: isServerSide ? undefined : scheduler.segmentsRefreshRate / 1000, im: scheduler.impressionsRefreshRate / 1000, diff --git a/src/types.ts b/src/types.ts index 5f6c7e39..43554a5f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,7 @@ export interface ISettings extends SplitIO.ISettings { }; readonly log: ILogger; readonly initialRolloutPlan?: RolloutPlan; + readonly definitionsType?: 'ff' | 'configs'; // default is 'ff' } /** diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index f850f0bf..c561a082 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -38,6 +38,7 @@ export const fullSettings: ISettings = { }, scheduler: { featuresRefreshRate: 1, + configsRefreshRate: 1, impressionsRefreshRate: 1, telemetryRefreshRate: 1, segmentsRefreshRate: 1, diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 8f070082..05d3bfe3 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -29,6 +29,8 @@ export const base = { scheduler: { // fetch feature updates each 60 sec featuresRefreshRate: 60, + // fetch configs updates each 60 sec + configsRefreshRate: 60, // fetch segments updates each 60 sec segmentsRefreshRate: 60, // publish telemetry stats each 3600 secs (1 hour) @@ -129,6 +131,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // Scheduler periods const { scheduler, startup } = withDefaults; scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate); + scheduler.configsRefreshRate = fromSecondsToMillis(scheduler.configsRefreshRate); scheduler.segmentsRefreshRate = fromSecondsToMillis(scheduler.segmentsRefreshRate); scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate); scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 27577054..67f13750 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -615,6 +615,7 @@ declare namespace SplitIO { readonly mode: SDKMode; readonly scheduler: { featuresRefreshRate: number; + configsRefreshRate: number; impressionsRefreshRate: number; impressionsQueueSize: number; /** @@ -2310,6 +2311,65 @@ declare namespace SplitIO { getObject(index: number): Config; } + interface ConfigsOptions { + /** + * Your SDK key. + * + * @see {@link https://developer.harness.io/docs/feature-management-experimentation/management-and-administration/account-settings/api-keys/} + */ + authorizationKey: string; + /** + * Configs definitions refresh rate for polling, in seconds. + * + * @defaultValue `60` + */ + configsRefreshRate?: number; + /** + * Logging level. + * + * @defaultValue `'NONE'` + */ + logLevel?: LogLevel; + /** + * Time in seconds until SDK ready timeout is emitted. + * + * @defaultValue `10` + */ + timeout?: number; + /** + * Custom endpoints to replace the default ones used by the SDK. + */ + urls?: UrlSettings; + // /** + // * Defines what impressions are sent to Split servers. + // * - DEBUG: all impressions are sent. + // * - OPTIMIZED: will send unique impressions to Split servers, avoiding a considerable amount of traffic that duplicated impressions could generate. + // * - NONE: will send unique keys evaluated per config to Split servers instead of full blown impressions. + // * + // * @defaultValue `'OPTIMIZED'` + // */ + // impressionsMode?: ImpressionsMode; + // /** + // * The SDK posts the queued events data in bulks. This parameter controls the posting rate in seconds. + // * + // * @defaultValue `1800` + // */ + // eventsPushRate?: number; + // /** + // * The SDK sends impressions back to Split servers. This parameter controls how often this data is sent, in seconds. + // * + // * @defaultValue `1800` + // */ + // impressionsRefreshRate?: number; + // /** + // * Boolean flag to enable the streaming service as default synchronization mechanism. In the event of any issue with streaming, + // * the SDK would fallback to the polling mechanism. If false, the SDK would poll for changes as usual without attempting to use streaming. + // * + // * @defaultValue `true` + // */ + // streamingEnabled?: boolean; + } + /** * Configs SDK client interface. */ @@ -2322,10 +2382,6 @@ declare namespace SplitIO { * Logger API. */ Logger: ILoggerAPI; - /** - * Initializes the client. - */ - init(): void; /** * Flushes the client. */ From aa9ff602792eee937afada0eb59b30c799366d63 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Mar 2026 16:07:12 -0300 Subject: [PATCH 08/10] rename --- src/{sdkConfig => sdkConfigs}/configObject.ts | 0 src/{sdkConfig => sdkConfigs}/index-ff-wrapper.ts | 0 src/{sdkConfig => sdkConfigs}/index.ts | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename src/{sdkConfig => sdkConfigs}/configObject.ts (100%) rename src/{sdkConfig => sdkConfigs}/index-ff-wrapper.ts (100%) rename src/{sdkConfig => sdkConfigs}/index.ts (97%) diff --git a/src/sdkConfig/configObject.ts b/src/sdkConfigs/configObject.ts similarity index 100% rename from src/sdkConfig/configObject.ts rename to src/sdkConfigs/configObject.ts diff --git a/src/sdkConfig/index-ff-wrapper.ts b/src/sdkConfigs/index-ff-wrapper.ts similarity index 100% rename from src/sdkConfig/index-ff-wrapper.ts rename to src/sdkConfigs/index-ff-wrapper.ts diff --git a/src/sdkConfig/index.ts b/src/sdkConfigs/index.ts similarity index 97% rename from src/sdkConfig/index.ts rename to src/sdkConfigs/index.ts index 93fad40a..2a83db52 100644 --- a/src/sdkConfig/index.ts +++ b/src/sdkConfigs/index.ts @@ -14,7 +14,7 @@ import { sdkLifecycleFactory } from '../sdkClient/sdkLifecycle'; /** * Modular SDK factory */ -export function sdkConfigFactory(params: ISdkFactoryParams): SplitIO.ConfigsClient { +export function sdkConfigsFactory(params: ISdkFactoryParams): SplitIO.ConfigsClient { const { settings, platform, storageFactory, splitApiFactory, extraProps, syncManagerFactory, SignalListener, integrationsManagerFactory } = params; From 485d43265abedf7fc92b28a2c0682e56a4070646 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 23 Mar 2026 11:04:15 -0300 Subject: [PATCH 09/10] Add CONFIGS_UPDATE constant and update splitChangesUpdater for configs support --- src/readiness/constants.ts | 1 + src/sdkConfigs/index.ts | 7 +++---- src/sync/polling/syncTasks/splitsSyncTask.ts | 2 +- .../updaters/__tests__/splitChangesUpdater.spec.ts | 8 ++++---- src/sync/polling/updaters/splitChangesUpdater.ts | 11 +++++++---- types/splitio.d.ts | 8 ++++++-- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/readiness/constants.ts b/src/readiness/constants.ts index f08cf546..022100cc 100644 --- a/src/readiness/constants.ts +++ b/src/readiness/constants.ts @@ -14,3 +14,4 @@ export const SDK_UPDATE = 'state::update'; // SdkUpdateMetadata types: export const FLAGS_UPDATE = 'FLAGS_UPDATE'; export const SEGMENTS_UPDATE = 'SEGMENTS_UPDATE'; +export const CONFIGS_UPDATE = 'CONFIGS_UPDATE'; diff --git a/src/sdkConfigs/index.ts b/src/sdkConfigs/index.ts index 2a83db52..8fff3d10 100644 --- a/src/sdkConfigs/index.ts +++ b/src/sdkConfigs/index.ts @@ -67,10 +67,9 @@ export function sdkConfigsFactory(params: ISdkFactoryParams): SplitIO.ConfigsCli Object.create(sdkReadinessManager.sdkStatus) as SplitIO.IStatusInterface, sdkLifecycleFactory(ctx), { - getConfig(name: string, target?: SplitIO.Target): SplitIO.Config { - return { - value: name + target, - } as SplitIO.Config; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getConfig(_name: string, _target?: SplitIO.Target): SplitIO.Config { + throw new Error('getConfig not implemented'); }, track() { diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index d66541da..c0f1e92b 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -20,7 +20,7 @@ export function splitsSyncTaskFactory( return syncTaskFactory( settings.log, splitChangesUpdaterFactory( - settings.log, + settings, splitChangesFetcherFactory(fetchSplitChanges, settings, storage), storage, settings.sync.__splitFiltersValidation, diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 5398e06b..4bdb462e 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -205,7 +205,7 @@ describe('splitChangesUpdater', () => { let splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - let splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1); + let splitChangesUpdater = splitChangesUpdaterFactory({ log: loggerMock }, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1); afterEach(() => { jest.clearAllMocks(); @@ -279,7 +279,7 @@ describe('splitChangesUpdater', () => { { sets: ['set_a'], shouldEmit: true }, /* should emit if flag is back in configured sets */ ]; - splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + splitChangesUpdater = splitChangesUpdaterFactory({ log: loggerMock }, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); let index = 0; let calls = 0; @@ -294,7 +294,7 @@ describe('splitChangesUpdater', () => { // @ts-ignore splitFiltersValidation = { queryString: null, groupedFilters: { bySet: ['set_a'], byName: [], byPrefix: [] }, validFilters: [] }; storage.splits.clear(); - splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + splitChangesUpdater = splitChangesUpdaterFactory({ log: loggerMock }, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); splitsEmitSpy.mockReset(); index = 0; for (const setMock of setMocks) { @@ -414,7 +414,7 @@ describe('splitChangesUpdater', () => { readinessManager.segments.segmentsArrived = false; // Segments not ready - client-side should still emit // Create client-side updater (isClientSide = true) - const clientSideUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + const clientSideUpdater = splitChangesUpdaterFactory({ log: loggerMock }, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); const flag1 = { name: 'client-flag', status: 'ACTIVE', changeNumber: 300, conditions: [] } as unknown as ISplit; fetchMock.once('*', { status: 200, body: { ff: { d: [flag1], t: 300 } } }); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 0510a485..e89be415 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -3,14 +3,14 @@ import { ISplitChangesFetcher } from '../fetchers/types'; import { IRBSegment, ISplit, ISplitChangesResponse, ISplitFiltersValidation, MaybeThenable } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; -import { SDK_SPLITS_ARRIVED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants'; -import { ILogger } from '../../../logger/types'; +import { SDK_SPLITS_ARRIVED, FLAGS_UPDATE, SEGMENTS_UPDATE, CONFIGS_UPDATE } from '../../../readiness/constants'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; import { SPLIT_UPDATE } from '../../streaming/constants'; import { SdkUpdateMetadata } from '../../../../types/splitio'; +import { ISettings } from '../../../types'; export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise @@ -120,7 +120,7 @@ export function computeMutation(rules: Array, * @param retriesOnFailureBeforeReady - How many retries on `/splitChanges` we the updater do in case of failure or timeout. Default 0, i.e., no retries. */ export function splitChangesUpdaterFactory( - log: ILogger, + settings: Pick, splitChangesFetcher: ISplitChangesFetcher, storage: Pick, splitFiltersValidation: ISplitFiltersValidation, @@ -129,6 +129,7 @@ export function splitChangesUpdaterFactory( retriesOnFailureBeforeReady = 0, isClientSide?: boolean ): SplitChangesUpdater { + const { log, definitionsType } = settings; const { splits, rbSegments, segments } = storage; let startingUp = true; @@ -202,7 +203,9 @@ export function splitChangesUpdaterFactory( // emit SDK events if (emitSplitsArrivedEvent) { const metadata: SdkUpdateMetadata = { - type: updatedFlags.length > 0 ? FLAGS_UPDATE : SEGMENTS_UPDATE, + type: updatedFlags.length > 0 ? + definitionsType === 'configs' ? CONFIGS_UPDATE : FLAGS_UPDATE : + SEGMENTS_UPDATE, names: updatedFlags.length > 0 ? updatedFlags : [] }; splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, metadata); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 67f13750..38718e59 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -509,7 +509,7 @@ declare namespace SplitIO { /** * Metadata type for SDK update events. */ - type SdkUpdateMetadataType = 'FLAGS_UPDATE' | 'SEGMENTS_UPDATE'; + type SdkUpdateMetadataType = 'CONFIGS_UPDATE' | 'FLAGS_UPDATE' | 'SEGMENTS_UPDATE'; /** * Metadata for the ready events emitted when the SDK is ready to evaluate feature flags. @@ -2339,7 +2339,11 @@ declare namespace SplitIO { /** * Custom endpoints to replace the default ones used by the SDK. */ - urls?: UrlSettings; + urls?: UrlSettings | string; + /** + * Fallback configuration objects to use when the SDK is unable to fetch the configurations from the server. + */ + fallbacks?: FallbackTreatmentConfiguration; // /** // * Defines what impressions are sent to Split servers. // * - DEBUG: all impressions are sent. From e0db405dcad8a31d808886cb83d6c42b0b7dd1f5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 23 Mar 2026 11:16:46 -0300 Subject: [PATCH 10/10] Polishing --- src/logger/messages/warn.ts | 2 +- src/sync/polling/pollingManagerSS.ts | 7 ++++++- src/sync/polling/syncTasks/splitsSyncTask.ts | 11 ++++++++--- src/sync/polling/updaters/splitChangesUpdater.ts | 7 ++++--- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 58f2ed72..9e63e45c 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -7,7 +7,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.ENGINE_VALUE_NO_ATTRIBUTES, c.LOG_PREFIX_ENGINE_VALUE + 'Defined attribute `%s`. No attributes received.'], // synchronizer [c.SYNC_MYSEGMENTS_FETCH_RETRY, c.LOG_PREFIX_SYNC_MYSEGMENTS + 'Retrying fetch of memberships (attempt #%s). Reason: %s'], - [c.SYNC_SPLITS_FETCH_FAILS, c.LOG_PREFIX_SYNC_SPLITS + 'Error while doing fetch of feature flags. %s'], + [c.SYNC_SPLITS_FETCH_FAILS, c.LOG_PREFIX_SYNC_SPLITS + 'Error while doing fetch of %s'], [c.STREAMING_PARSING_ERROR_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE error notification: %s'], [c.STREAMING_PARSING_MESSAGE_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE message notification: %s'], [c.STREAMING_FALLBACK, c.LOG_PREFIX_SYNC_STREAMING + 'Falling back to polling mode. Reason: %s'], diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index 8a3f3c04..4a9cefc4 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -3,6 +3,11 @@ import { segmentsSyncTaskFactory } from './syncTasks/segmentsSyncTask'; import { IPollingManager, ISegmentsSyncTask, ISplitsSyncTask } from './types'; import { LOG_PREFIX_SYNC_POLLING, POLLING_START, POLLING_STOP } from '../../logger/constants'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; +import { ISettings } from '../../types'; + +export function isFetchingConfigs(settings: Pick) { + return settings.definitionsType === 'configs'; +} /** * Expose start / stop mechanism for pulling data from services. @@ -14,7 +19,7 @@ export function pollingManagerSSFactory( const { splitApi, storage, readiness, settings } = params; const log = settings.log; - const fetchingConfigs = settings.definitionsType === 'configs'; + const fetchingConfigs = isFetchingConfigs(settings); const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(fetchingConfigs ? splitApi.fetchConfigs : splitApi.fetchSplitChanges, storage, readiness, settings); const segmentsSyncTask: ISegmentsSyncTask = segmentsSyncTaskFactory(splitApi.fetchSegmentChanges, storage, readiness, settings); diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index c0f1e92b..0089bbc3 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -3,9 +3,11 @@ import { IReadinessManager } from '../../../readiness/types'; import { syncTaskFactory } from '../../syncTask'; import { ISplitsSyncTask } from '../types'; import { splitChangesFetcherFactory } from '../fetchers/splitChangesFetcher'; +import { configsFetcherFactory } from '../fetchers/configsFetcher'; import { IFetchSplitChanges } from '../../../services/types'; import { ISettings } from '../../../types'; import { splitChangesUpdaterFactory } from '../updaters/splitChangesUpdater'; +import { isFetchingConfigs } from '../pollingManagerSS'; /** * Creates a sync task that periodically executes a `splitChangesUpdater` task @@ -17,11 +19,14 @@ export function splitsSyncTaskFactory( settings: ISettings, isClientSide?: boolean ): ISplitsSyncTask { + const fetcher = isFetchingConfigs(settings) + ? configsFetcherFactory(fetchSplitChanges) + : splitChangesFetcherFactory(fetchSplitChanges, settings, storage); return syncTaskFactory( settings.log, splitChangesUpdaterFactory( settings, - splitChangesFetcherFactory(fetchSplitChanges, settings, storage), + fetcher, storage, settings.sync.__splitFiltersValidation, readiness.splits, @@ -29,7 +34,7 @@ export function splitsSyncTaskFactory( settings.startup.retriesOnFailureBeforeReady, isClientSide ), - settings.definitionsType === 'configs' ? settings.scheduler.configsRefreshRate : settings.scheduler.featuresRefreshRate, - settings.definitionsType === 'configs' ? 'configsUpdater' : 'splitChangesUpdater', + isFetchingConfigs(settings) ? settings.scheduler.configsRefreshRate : settings.scheduler.featuresRefreshRate, + isFetchingConfigs(settings) ? 'configsUpdater' : 'splitChangesUpdater', ); } diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index e89be415..c613aa56 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -11,6 +11,7 @@ import { setToArray } from '../../../utils/lang/sets'; import { SPLIT_UPDATE } from '../../streaming/constants'; import { SdkUpdateMetadata } from '../../../../types/splitio'; import { ISettings } from '../../../types'; +import { isFetchingConfigs } from '../pollingManagerSS'; export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise @@ -129,7 +130,7 @@ export function splitChangesUpdaterFactory( retriesOnFailureBeforeReady = 0, isClientSide?: boolean ): SplitChangesUpdater { - const { log, definitionsType } = settings; + const { log } = settings; const { splits, rbSegments, segments } = storage; let startingUp = true; @@ -204,7 +205,7 @@ export function splitChangesUpdaterFactory( if (emitSplitsArrivedEvent) { const metadata: SdkUpdateMetadata = { type: updatedFlags.length > 0 ? - definitionsType === 'configs' ? CONFIGS_UPDATE : FLAGS_UPDATE : + isFetchingConfigs(settings) ? CONFIGS_UPDATE : FLAGS_UPDATE : SEGMENTS_UPDATE, names: updatedFlags.length > 0 ? updatedFlags : [] }; @@ -223,7 +224,7 @@ export function splitChangesUpdaterFactory( return _splitChangesUpdater(sinces, retry); } else { startingUp = false; - log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); + log.warn(SYNC_SPLITS_FETCH_FAILS, [(isFetchingConfigs(settings) ? 'configs. ' : 'feature flags. ') + error]); } return false; });