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/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/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/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/sdkClient/client.ts b/src/sdkClient/client.ts index 64caaead..fa630dd6 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -5,7 +5,7 @@ import { validateDefinitionExistence } from '../utils/inputValidation/definition 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_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); @@ -153,7 +153,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl config = fallbackTreatment.config; } - if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) { + if (validateDefinitionExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) { log.info(IMPRESSION_QUEUEING, [featureFlagName, matchingKey, treatment, label]); queue.push({ imp: { diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index c50b8f72..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, impressionsTracker } = 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(); - impressionsTracker.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(); - impressionsTracker.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..a8a34988 --- /dev/null +++ b/src/sdkClient/sdkLifecycle.ts @@ -0,0 +1,76 @@ +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): { init(): void; flush(): Promise; destroy(): Promise } { + const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, impressionsTracker } = 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(); + impressionsTracker.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(); + impressionsTracker.stop(); + } + + // Stop background jobs + syncManager && syncManager.stop(); + + return __flush().then(() => { + // Cleanup storage + return storage.destroy(); + }); + } + }; +} diff --git a/src/sdkConfigs/configObject.ts b/src/sdkConfigs/configObject.ts new file mode 100644 index 00000000..267b0879 --- /dev/null +++ b/src/sdkConfigs/configObject.ts @@ -0,0 +1,68 @@ +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 { + return createConfigObject(isString(config) ? JSON.parse(config) : config); + } catch { + return createConfigObject(null); + } +} diff --git a/src/sdkConfigs/index-ff-wrapper.ts b/src/sdkConfigs/index-ff-wrapper.ts new file mode 100644 index 00000000..25222862 --- /dev/null +++ b/src/sdkConfigs/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) 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/sdkConfigs/index.ts b/src/sdkConfigs/index.ts new file mode 100644 index 00000000..8fff3d10 --- /dev/null +++ b/src/sdkConfigs/index.ts @@ -0,0 +1,86 @@ +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 { FallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator'; +import { sdkLifecycleFactory } from '../sdkClient/sdkLifecycle'; + +/** + * Modular SDK factory + */ +export function sdkConfigsFactory(params: ISdkFactoryParams): SplitIO.ConfigsClient { + + const { settings, platform, storageFactory, splitApiFactory, extraProps, + syncManagerFactory, SignalListener, integrationsManagerFactory } = params; + const { log } = 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 = FallbackTreatmentsCalculator(settings.fallbackTreatments); + + const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); + const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker }); + + const impressionsTracker = impressionsTrackerFactory(params, storage, integrationsManager); + 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, 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), + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getConfig(_name: string, _target?: SplitIO.Target): SplitIO.Config { + throw new Error('getConfig not implemented'); + }, + + track() { + return false; + }, + + // Logger wrapper API + Logger: createLoggerAPI(log), + + settings, + }, + extraProps && extraProps(ctx) + ); +} diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 8b51a503..21e2b0e6 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/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/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index 03ce7f22..4a9cefc4 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -1,20 +1,27 @@ 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 { 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. */ export function pollingManagerSSFactory( - params: ISdkFactoryContextSync + params: ISdkFactoryContextSync, ): IPollingManager { const { splitApi, storage, readiness, settings } = params; const log = settings.log; - const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); + 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); return { @@ -24,7 +31,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..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.log, - splitChangesFetcherFactory(fetchSplitChanges, settings, storage), + settings, + fetcher, storage, settings.sync.__splitFiltersValidation, readiness.splits, @@ -29,7 +34,7 @@ export function splitsSyncTaskFactory( settings.startup.retriesOnFailureBeforeReady, isClientSide ), - settings.scheduler.featuresRefreshRate, - 'splitChangesUpdater', + isFetchingConfigs(settings) ? settings.scheduler.configsRefreshRate : settings.scheduler.featuresRefreshRate, + isFetchingConfigs(settings) ? 'configsUpdater' : 'splitChangesUpdater', ); } 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..c613aa56 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -3,14 +3,15 @@ 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'; +import { isFetchingConfigs } from '../pollingManagerSS'; export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise @@ -120,7 +121,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 +130,7 @@ export function splitChangesUpdaterFactory( retriesOnFailureBeforeReady = 0, isClientSide?: boolean ): SplitChangesUpdater { + const { log } = settings; const { splits, rbSegments, segments } = storage; let startingUp = true; @@ -202,7 +204,9 @@ export function splitChangesUpdaterFactory( // emit SDK events if (emitSplitsArrivedEvent) { const metadata: SdkUpdateMetadata = { - type: updatedFlags.length > 0 ? FLAGS_UPDATE : SEGMENTS_UPDATE, + type: updatedFlags.length > 0 ? + isFetchingConfigs(settings) ? CONFIGS_UPDATE : FLAGS_UPDATE : + SEGMENTS_UPDATE, names: updatedFlags.length > 0 ? updatedFlags : [] }; splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, metadata); @@ -220,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; }); 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/sync/submitters/types.ts b/src/sync/submitters/types.ts index 57e8bfd5..a97debb7 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -109,16 +109,17 @@ 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/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/constants/index.ts b/src/utils/constants/index.ts index b9c8edc1..352988e3 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 @@ -85,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; 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 }; +} 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 b8753566..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. @@ -615,6 +615,7 @@ declare namespace SplitIO { readonly mode: SDKMode; readonly scheduler: { featuresRefreshRate: number; + configsRefreshRate: number; impressionsRefreshRate: number; impressionsQueueSize: number; /** @@ -2284,4 +2285,182 @@ declare namespace SplitIO { */ split(featureFlagName: string): SplitViewAsync; } + + // Configs SDK + + interface Target { + 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; + } + + 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 | 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. + // * - 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. + */ + interface ConfigsClient extends IStatusInterface { + /** + * Current settings of the SDK instance. + */ + settings: ISettings; + /** + * Logger API. + */ + Logger: ILoggerAPI; + /** + * 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 The config object. + */ + getConfig(name: string, target?: Target, options?: EvaluationOptions): 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; + } + + /** + * 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; + } }