From 6241b0f0356fa50f7a8b144a5b8d3ba1a961539c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 9 Mar 2026 11:20:16 -0300 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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;