From 0cb3a1f51c938c4cb51fb397b4f10364996eb76a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 00:20:10 -0300 Subject: [PATCH 01/44] Move validateCache call from splitChangesUpdater to syncManagerOnline --- src/sync/__tests__/syncManagerOnline.spec.ts | 32 ++++++++++++++++--- .../polling/updaters/splitChangesUpdater.ts | 13 ++------ src/sync/syncManagerOnline.ts | 14 ++++++-- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 7fda853b..11346c98 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -2,6 +2,7 @@ import { fullSettings } from '../../utils/settingsValidation/__tests__/settings. import { syncTaskFactory } from './syncTask.mock'; import { syncManagerOnlineFactory } from '../syncManagerOnline'; import { IReadinessManager } from '../../readiness/types'; +import { SDK_SPLITS_CACHE_LOADED } from '../../readiness/constants'; jest.mock('../submitters/submitterManager', () => { return { @@ -45,8 +46,10 @@ const pushManagerFactoryMock = jest.fn(() => pushManagerMock); test('syncManagerOnline should start or not the submitter depending on user consent status', () => { const settings = { ...fullSettings }; - // @ts-ignore - const syncManager = syncManagerOnlineFactory()({ settings }); + const syncManager = syncManagerOnlineFactory()({ + settings, // @ts-ignore + storage: { splits: { checkCache: () => false } }, + }); const submitterManager = syncManager.submitterManager!; syncManager.start(); @@ -95,7 +98,10 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () // @ts-ignore // Test pushManager for main client - const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); + const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ + settings, // @ts-ignore + storage: { splits: { checkCache: () => false } }, + }); expect(pushManagerFactoryMock).not.toBeCalled(); @@ -161,7 +167,10 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () settings.sync.enabled = true; // @ts-ignore // pushManager instantiation control test - const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); + const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ + settings, // @ts-ignore + storage: { splits: { checkCache: () => false } }, + }); expect(pushManagerFactoryMock).toBeCalled(); @@ -173,3 +182,18 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () testSyncManager.stop(); }); + +test('syncManagerOnline should emit SDK_SPLITS_CACHE_LOADED if validateCache returns true', async () => { + const params = { + settings: fullSettings, + storage: { splits: { checkCache: () => true } }, + readiness: { splits: { emit: jest.fn() } } + }; // @ts-ignore + const syncManager = syncManagerOnlineFactory()(params); + + await syncManager.start(); + + expect(params.readiness.splits.emit).toBeCalledWith(SDK_SPLITS_CACHE_LOADED); + + syncManager.stop(); +}); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index e8153987..e447227d 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -3,7 +3,7 @@ import { ISplitChangesFetcher } from '../fetchers/types'; import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; -import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; +import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; @@ -153,7 +153,7 @@ export function splitChangesUpdaterFactory( */ function _splitChangesUpdater(since: number, retry = 0): Promise { log.debug(SYNC_SPLITS_FETCH, [since]); - const fetcherPromise = Promise.resolve(splitUpdateNotification ? + return Promise.resolve(splitUpdateNotification ? { splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } : splitChangesFetcher(since, noCache, till, _promiseDecorator) ) @@ -200,15 +200,6 @@ export function splitChangesUpdaterFactory( } return false; }); - - // After triggering the requests, if we have cached splits information let's notify that to emit SDK_READY_FROM_CACHE. - // Wrapping in a promise since checkCache can be async. - if (splitsEventEmitter && startingUp) { - Promise.resolve(splits.checkCache()).then(isCacheReady => { - if (isCacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); - }); - } - return fetcherPromise; } let sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 5410c17f..777fe41a 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -9,6 +9,7 @@ import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '.. import { isConsentGranted } from '../consent'; import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; import { ISdkFactoryContextSync } from '../sdkFactory/types'; +import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants'; /** * Online SyncManager factory. @@ -28,7 +29,7 @@ export function syncManagerOnlineFactory( */ return function (params: ISdkFactoryContextSync): ISyncManagerCS { - const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker } = params; + const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker, storage, readiness } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); @@ -87,6 +88,13 @@ export function syncManagerOnlineFactory( start() { running = true; + if (startFirstTime) { + const isCacheLoaded = storage.splits.checkCache(); + Promise.resolve().then(() => { + if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); + }); + } + // start syncing splits and segments if (pollingManager) { @@ -96,7 +104,6 @@ export function syncManagerOnlineFactory( // Doesn't call `syncAll` when the syncManager is resuming if (startFirstTime) { pollingManager.syncAll(); - startFirstTime = false; } pushManager.start(); } else { @@ -105,13 +112,14 @@ export function syncManagerOnlineFactory( } else { if (startFirstTime) { pollingManager.syncAll(); - startFirstTime = false; } } } // start periodic data recording (events, impressions, telemetry). submitterManager.start(!isConsentGranted(settings)); + + startFirstTime = false; }, /** From d392cc88cc4ee1c22aefa20eb77ec85629d744f7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 00:43:02 -0300 Subject: [PATCH 02/44] Replace splits::checkCache method with storage::validateCache method --- src/storages/AbstractSplitsCacheAsync.ts | 8 -------- src/storages/AbstractSplitsCacheSync.ts | 8 -------- src/storages/inLocalStorage/SplitsCacheInLocal.ts | 11 +---------- .../__tests__/SplitsCacheInLocal.spec.ts | 6 ------ src/storages/inLocalStorage/index.ts | 5 +++++ src/storages/types.ts | 5 +---- src/sync/__tests__/syncManagerOnline.spec.ts | 8 ++++---- src/sync/offline/syncTasks/fromObjectSyncTask.ts | 9 +++++---- src/sync/syncManagerOnline.ts | 6 ++---- src/utils/settingsValidation/storage/storageCS.ts | 2 +- 10 files changed, 19 insertions(+), 49 deletions(-) diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index dcf059ed..56664b22 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -27,14 +27,6 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { return Promise.resolve(true); } - /** - * Check if the splits information is already stored in cache. - * Noop, just keeping the interface. This is used by client-side implementations only. - */ - checkCache(): Promise { - return Promise.resolve(false); - } - /** * Kill `name` split and set `defaultTreatment` and `changeNumber`. * Used for SPLIT_KILL push notifications. diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index f82ebbd6..09c131ba 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -47,14 +47,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { abstract clear(): void - /** - * Check if the splits information is already stored in cache. This data can be preloaded. - * It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE. - */ - checkCache(): boolean { - return false; - } - /** * Kill `name` split and set `defaultTreatment` and `changeNumber`. * Used for SPLIT_KILL push notifications. diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 93eb6f32..92057d4a 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -212,15 +212,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - /** - * Check if the splits information is already stored in browser LocalStorage. - * In this function we could add more code to check if the data is valid. - * @override - */ - checkCache(): boolean { - return this.getChangeNumber() > -1; - } - /** * Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`, * @@ -245,7 +236,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this.updateNewFilter = true; // if there is cache, clear it - if (this.checkCache()) this.clear(); + if (this.getChangeNumber() > -1) this.clear(); } catch (e) { this.log.error(LOG_PREFIX + e); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 4d8ec076..17b2584d 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -30,16 +30,10 @@ test('SPLIT CACHE / LocalStorage', () => { expect(cache.getSplit('lol1')).toEqual(null); expect(cache.getSplit('lol2')).toEqual(somethingElse); - expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data. - expect(cache.getChangeNumber() === -1).toBe(true); - expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data. - cache.setChangeNumber(123); - expect(cache.checkCache()).toBe(true); // checkCache should return true once localstorage has data. - expect(cache.getChangeNumber() === 123).toBe(true); }); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 871be592..1d16cd62 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -53,6 +53,11 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined, uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, + // @TODO implement + validateCache() { + return splits.getChangeNumber() > -1; + }, + destroy() { }, // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key). diff --git a/src/storages/types.ts b/src/storages/types.ts index 5d7b40b6..8c63f452 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -191,8 +191,6 @@ export interface ISplitsCacheBase { // only for Client-Side. Returns true if the storage is not synchronized yet (getChangeNumber() === -1) or contains a FF using segments or large segments usesSegments(): MaybeThenable, clear(): MaybeThenable, - // should never reject or throw an exception. Instead return false by default, to avoid emitting SDK_READY_FROM_CACHE. - checkCache(): MaybeThenable, killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable, getNamesByFlagSets(flagSets: string[]): MaybeThenable[]> } @@ -209,7 +207,6 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { trafficTypeExists(trafficType: string): boolean, usesSegments(): boolean, clear(): void, - checkCache(): boolean, killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean, getNamesByFlagSets(flagSets: string[]): Set[] } @@ -226,7 +223,6 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { trafficTypeExists(trafficType: string): Promise, usesSegments(): Promise, clear(): Promise, - checkCache(): Promise, killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise, getNamesByFlagSets(flagSets: string[]): Promise[]> } @@ -457,6 +453,7 @@ export interface IStorageSync extends IStorageBase< IUniqueKeysCacheSync > { // Defined in client-side + validateCache?: () => boolean, // @TODO support async largeSegments?: ISegmentsCacheSync, } diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 11346c98..c7dba96e 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -48,7 +48,7 @@ test('syncManagerOnline should start or not the submitter depending on user cons const syncManager = syncManagerOnlineFactory()({ settings, // @ts-ignore - storage: { splits: { checkCache: () => false } }, + storage: {}, }); const submitterManager = syncManager.submitterManager!; @@ -100,7 +100,7 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () // Test pushManager for main client const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings, // @ts-ignore - storage: { splits: { checkCache: () => false } }, + storage: { validateCache: () => false }, }); expect(pushManagerFactoryMock).not.toBeCalled(); @@ -169,7 +169,7 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () // pushManager instantiation control test const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings, // @ts-ignore - storage: { splits: { checkCache: () => false } }, + storage: { validateCache: () => false }, }); expect(pushManagerFactoryMock).toBeCalled(); @@ -186,7 +186,7 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', () test('syncManagerOnline should emit SDK_SPLITS_CACHE_LOADED if validateCache returns true', async () => { const params = { settings: fullSettings, - storage: { splits: { checkCache: () => true } }, + storage: { validateCache: () => true }, readiness: { splits: { emit: jest.fn() } } }; // @ts-ignore const syncManager = syncManagerOnlineFactory()(params); diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 84805110..1b3f97f8 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -1,6 +1,6 @@ import { forOwn } from '../../../utils/lang'; import { IReadinessManager } from '../../../readiness/types'; -import { ISplitsCacheSync } from '../../../storages/types'; +import { ISplitsCacheSync, IStorageSync } from '../../../storages/types'; import { ISplitsParser } from '../splitsParser/types'; import { ISplit, ISplitPartial } from '../../../dtos/types'; import { syncTaskFactory } from '../../syncTask'; @@ -15,7 +15,7 @@ import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/c */ export function fromObjectUpdaterFactory( splitsParser: ISplitsParser, - storage: { splits: ISplitsCacheSync }, + storage: Pick, readiness: IReadinessManager, settings: ISettings, ): () => Promise { @@ -60,9 +60,10 @@ export function fromObjectUpdaterFactory( if (startingUp) { startingUp = false; - Promise.resolve(splitsCache.checkCache()).then(cacheReady => { + const isCacheLoaded = storage.validateCache ? storage.validateCache() : false; + Promise.resolve().then(() => { // Emits SDK_READY_FROM_CACHE - if (cacheReady) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); + if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); // Emits SDK_READY readiness.segments.emit(SDK_SEGMENTS_ARRIVED); }); diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 777fe41a..aed32493 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -89,10 +89,8 @@ export function syncManagerOnlineFactory( running = true; if (startFirstTime) { - const isCacheLoaded = storage.splits.checkCache(); - Promise.resolve().then(() => { - if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); - }); + const isCacheLoaded = storage.validateCache ? storage.validateCache() : false; + if (isCacheLoaded) Promise.resolve().then(() => { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); }); } // start syncing splits and segments diff --git a/src/utils/settingsValidation/storage/storageCS.ts b/src/utils/settingsValidation/storage/storageCS.ts index f7b531fc..7d58af3d 100644 --- a/src/utils/settingsValidation/storage/storageCS.ts +++ b/src/utils/settingsValidation/storage/storageCS.ts @@ -8,7 +8,7 @@ import { IStorageFactoryParams, IStorageSync } from '../../../storages/types'; export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSync { const result = InMemoryStorageCSFactory(params); - result.splits.checkCache = () => true; // to emit SDK_READY_FROM_CACHE + result.validateCache = () => true; // to emit SDK_READY_FROM_CACHE return result; } __InLocalStorageMockFactory.type = STORAGE_MEMORY; From 893833455985edf25367457e6b9281bc203d5ecb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 13:54:21 -0300 Subject: [PATCH 03/44] Extract validation logic into SplitsCacheInLocal::validateCache method --- .../inLocalStorage/SplitsCacheInLocal.ts | 70 +++++++++---------- .../__tests__/SplitsCacheInLocal.spec.ts | 31 +++++--- src/storages/inLocalStorage/index.ts | 4 +- 3 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 92057d4a..1fa739aa 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -20,16 +20,47 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private hasSync?: boolean; private updateNewFilter?: boolean; - constructor(settings: ISettings, keys: KeyBuilderCS, expirationTimestamp?: number) { + constructor(settings: ISettings, keys: KeyBuilderCS) { super(); this.keys = keys; this.log = settings.log; this.storageHash = getStorageHash(settings); this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet; + } - this._checkExpiration(expirationTimestamp); + /** + * Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`, + * + * @param expirationTimestamp - if the value is not a number, data will not be cleaned + */ + public validateCache(expirationTimestamp?: number) { + // _checkExpiration + let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey()); + if (value !== null) { + value = parseInt(value, 10); + if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear(); + } - this._checkFilterQuery(); + // @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage + // _checkFilterQuery + const storageHashKey = this.keys.buildHashKey(); + const storageHash = localStorage.getItem(storageHashKey); + + if (storageHash !== this.storageHash) { + try { + // mark cache to update the new query filter on first successful splits fetch + this.updateNewFilter = true; + + // if there is cache, clear it + if (this.getChangeNumber() > -1) this.clear(); + + } catch (e) { + this.log.error(LOG_PREFIX + e); + } + } + // if the filter didn't change, nothing is done + + return this.getChangeNumber() > -1; } private _decrementCount(key: string) { @@ -212,39 +243,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - /** - * Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`, - * - * @param expirationTimestamp - if the value is not a number, data will not be cleaned - */ - private _checkExpiration(expirationTimestamp?: number) { - let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear(); - } - } - - // @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage - private _checkFilterQuery() { - const storageHashKey = this.keys.buildHashKey(); - const storageHash = localStorage.getItem(storageHashKey); - - if (storageHash !== this.storageHash) { - try { - // mark cache to update the new query filter on first successful splits fetch - this.updateNewFilter = true; - - // if there is cache, clear it - if (this.getChangeNumber() > -1) this.clear(); - - } catch (e) { - this.log.error(LOG_PREFIX + e); - } - } - // if the filter didn't change, nothing is done - } - getNamesByFlagSets(flagSets: string[]): Set[] { return flagSets.map(flagSet => { const flagSetKey = this.keys.buildFlagSetKey(flagSet); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 17b2584d..26dcbaf1 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -7,6 +7,7 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin test('SPLIT CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); cache.clear(); @@ -40,6 +41,7 @@ test('SPLIT CACHE / LocalStorage', () => { test('SPLIT CACHE / LocalStorage / Get Keys', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); cache.addSplit('lol1', something); cache.addSplit('lol2', somethingElse); @@ -52,6 +54,7 @@ test('SPLIT CACHE / LocalStorage / Get Keys', () => { test('SPLIT CACHE / LocalStorage / Add Splits', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); cache.addSplits([ ['lol1', something], @@ -66,6 +69,7 @@ test('SPLIT CACHE / LocalStorage / Add Splits', () => { test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); cache.addSplits([ // loop of addSplit ['split1', splitWithUserTT], @@ -104,6 +108,8 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { test('SPLIT CACHE / LocalStorage / killLocally', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); + cache.addSplit('lol1', something); cache.addSplit('lol2', somethingElse); const initialChangeNumber = cache.getChangeNumber(); @@ -136,6 +142,7 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { test('SPLIT CACHE / LocalStorage / usesSegments', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized cache.setChangeNumber(1); // to indicate that data has been synced. @@ -167,6 +174,8 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { } } }, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); + const emptySet = new Set([]); cache.addSplits([ @@ -206,25 +215,27 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { // if FlagSets are not defined, it should store all FlagSets in memory. test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { - const cacheWithoutFilters = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + cache.validateCache(); + const emptySet = new Set([]); - cacheWithoutFilters.addSplits([ + cache.addSplits([ [featureFlagOne.name, featureFlagOne], [featureFlagTwo.name, featureFlagTwo], [featureFlagThree.name, featureFlagThree], ]); - cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); - expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]); - expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual([emptySet]); - expect(cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); + expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); + expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); + expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); // Validate that the feature flag cache is cleared when calling `clear` method - cacheWithoutFilters.clear(); + cache.clear(); expect(localStorage.length).toBe(1); // only 'SPLITIO.hash' should remain in localStorage expect(localStorage.key(0)).toBe('SPLITIO.hash'); }); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 1d16cd62..cce6568c 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -39,7 +39,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn const keys = new KeyBuilderCS(prefix, matchingKey); const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; - const splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp); + const splits = new SplitsCacheInLocal(settings, keys); const segments = new MySegmentsCacheInLocal(log, keys); const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)); @@ -55,7 +55,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn // @TODO implement validateCache() { - return splits.getChangeNumber() > -1; + return splits.validateCache(expirationTimestamp); }, destroy() { }, From 1b061eb06b855914cdec2bc05fc94ac13ba80850 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 13:57:33 -0300 Subject: [PATCH 04/44] Simplify SplitsCacheInLocal::setChangeNumber moving clear logic to SplitsCacheInLocal::validateCache --- .../inLocalStorage/SplitsCacheInLocal.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 1fa739aa..42a1cb75 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -18,7 +18,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly storageHash: string; private readonly flagSetsFilter: string[]; private hasSync?: boolean; - private updateNewFilter?: boolean; constructor(settings: ISettings, keys: KeyBuilderCS) { super(); @@ -47,13 +46,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const storageHash = localStorage.getItem(storageHashKey); if (storageHash !== this.storageHash) { + this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); try { - // mark cache to update the new query filter on first successful splits fetch - this.updateNewFilter = true; - // if there is cache, clear it if (this.getChangeNumber() > -1) this.clear(); + localStorage.setItem(storageHashKey, this.storageHash); } catch (e) { this.log.error(LOG_PREFIX + e); } @@ -169,19 +167,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } setChangeNumber(changeNumber: number): boolean { - - // when using a new split query, we must update it at the store - if (this.updateNewFilter) { - this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); - const storageHashKey = this.keys.buildHashKey(); - try { - localStorage.setItem(storageHashKey, this.storageHash); - } catch (e) { - this.log.error(LOG_PREFIX + e); - } - this.updateNewFilter = false; - } - try { localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + ''); // update "last updated" timestamp with current time From ffd724dd3fdfec27695b6d4449420427494d6684 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 14:03:47 -0300 Subject: [PATCH 05/44] Remove SplitsCacheInLocal::storageHash --- .../inLocalStorage/SplitsCacheInLocal.ts | 9 ++++----- .../__tests__/SplitsCacheInLocal.spec.ts | 16 ++++++++-------- src/storages/inLocalStorage/index.ts | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 42a1cb75..dd28bd41 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -15,7 +15,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly keys: KeyBuilderCS; private readonly log: ILogger; - private readonly storageHash: string; private readonly flagSetsFilter: string[]; private hasSync?: boolean; @@ -23,7 +22,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { super(); this.keys = keys; this.log = settings.log; - this.storageHash = getStorageHash(settings); this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet; } @@ -32,7 +30,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * * @param expirationTimestamp - if the value is not a number, data will not be cleaned */ - public validateCache(expirationTimestamp?: number) { + public validateCache(settings: ISettings, expirationTimestamp?: number) { // _checkExpiration let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey()); if (value !== null) { @@ -44,14 +42,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // _checkFilterQuery const storageHashKey = this.keys.buildHashKey(); const storageHash = localStorage.getItem(storageHashKey); + const currentStorageHash = getStorageHash(settings); - if (storageHash !== this.storageHash) { + if (storageHash !== currentStorageHash) { this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); try { // if there is cache, clear it if (this.getChangeNumber() > -1) this.clear(); - localStorage.setItem(storageHashKey, this.storageHash); + localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { this.log.error(LOG_PREFIX + e); } diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 26dcbaf1..62dad318 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -7,7 +7,7 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin test('SPLIT CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); cache.clear(); @@ -41,7 +41,7 @@ test('SPLIT CACHE / LocalStorage', () => { test('SPLIT CACHE / LocalStorage / Get Keys', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); cache.addSplit('lol1', something); cache.addSplit('lol2', somethingElse); @@ -54,7 +54,7 @@ test('SPLIT CACHE / LocalStorage / Get Keys', () => { test('SPLIT CACHE / LocalStorage / Add Splits', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); cache.addSplits([ ['lol1', something], @@ -69,7 +69,7 @@ test('SPLIT CACHE / LocalStorage / Add Splits', () => { test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); cache.addSplits([ // loop of addSplit ['split1', splitWithUserTT], @@ -108,7 +108,7 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { test('SPLIT CACHE / LocalStorage / killLocally', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); cache.addSplit('lol1', something); cache.addSplit('lol2', somethingElse); @@ -142,7 +142,7 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { test('SPLIT CACHE / LocalStorage / usesSegments', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized cache.setChangeNumber(1); // to indicate that data has been synced. @@ -174,7 +174,7 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { } } }, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); const emptySet = new Set([]); @@ -216,7 +216,7 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { // if FlagSets are not defined, it should store all FlagSets in memory. test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(); + cache.validateCache(fullSettings); const emptySet = new Set([]); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index cce6568c..a42e6a12 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -55,7 +55,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn // @TODO implement validateCache() { - return splits.validateCache(expirationTimestamp); + return splits.validateCache(settings, expirationTimestamp); }, destroy() { }, From e13f81980bfcd3945fdb0ec799152d3a5ad7226c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 14:12:22 -0300 Subject: [PATCH 06/44] Move expirationTimestamp logic inside validateCache method --- src/storages/inLocalStorage/SplitsCacheInLocal.ts | 12 +++++++----- src/storages/inLocalStorage/index.ts | 4 +--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index dd28bd41..bd5ea343 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -7,6 +7,7 @@ import { LOG_PREFIX } from './constants'; import { ISettings } from '../../types'; import { getStorageHash } from '../KeyBuilder'; import { setToArray } from '../../utils/lang/sets'; +import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; /** * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage. @@ -26,16 +27,17 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } /** - * Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`, - * - * @param expirationTimestamp - if the value is not a number, data will not be cleaned + * Clean Splits cache if: + * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` + * - hash has changes, i.e., the SDK key, flags filter criteria or flags spec version was modified */ - public validateCache(settings: ISettings, expirationTimestamp?: number) { + public validateCache(settings: ISettings) { // _checkExpiration + const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey()); if (value !== null) { value = parseInt(value, 10); - if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear(); + if (!isNaNNumber(value) && value < expirationTimestamp) this.clear(); } // @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index a42e6a12..58085e17 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -7,7 +7,6 @@ import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; import { SplitsCacheInLocal } from './SplitsCacheInLocal'; import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; import { DEBUG, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants'; @@ -37,7 +36,6 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params; const matchingKey = getMatching(settings.core.key); const keys = new KeyBuilderCS(prefix, matchingKey); - const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; const splits = new SplitsCacheInLocal(settings, keys); const segments = new MySegmentsCacheInLocal(log, keys); @@ -55,7 +53,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn // @TODO implement validateCache() { - return splits.validateCache(settings, expirationTimestamp); + return splits.validateCache(settings); }, destroy() { }, From b32e3eeb3841dd01e2f20beaa99847614a4861e3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 15:00:33 -0300 Subject: [PATCH 07/44] Move validateCache logic outside SplitsCacheInLocal --- .../inLocalStorage/SplitsCacheInLocal.ts | 38 ---------------- .../__tests__/SplitsCacheInLocal.spec.ts | 11 +---- src/storages/inLocalStorage/index.ts | 4 +- src/storages/inLocalStorage/validateCache.ts | 43 +++++++++++++++++++ src/storages/pluggable/index.ts | 3 +- 5 files changed, 48 insertions(+), 51 deletions(-) create mode 100644 src/storages/inLocalStorage/validateCache.ts diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index bd5ea343..14767d72 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -5,9 +5,7 @@ import { KeyBuilderCS } from '../KeyBuilderCS'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISettings } from '../../types'; -import { getStorageHash } from '../KeyBuilder'; import { setToArray } from '../../utils/lang/sets'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; /** * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage. @@ -26,42 +24,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet; } - /** - * Clean Splits cache if: - * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` - * - hash has changes, i.e., the SDK key, flags filter criteria or flags spec version was modified - */ - public validateCache(settings: ISettings) { - // _checkExpiration - const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; - let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && value < expirationTimestamp) this.clear(); - } - - // @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage - // _checkFilterQuery - const storageHashKey = this.keys.buildHashKey(); - const storageHash = localStorage.getItem(storageHashKey); - const currentStorageHash = getStorageHash(settings); - - if (storageHash !== currentStorageHash) { - this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); - try { - // if there is cache, clear it - if (this.getChangeNumber() > -1) this.clear(); - - localStorage.setItem(storageHashKey, currentStorageHash); - } catch (e) { - this.log.error(LOG_PREFIX + e); - } - } - // if the filter didn't change, nothing is done - - return this.getChangeNumber() > -1; - } - private _decrementCount(key: string) { const count = toNumber(localStorage.getItem(key)) - 1; // @ts-expect-error diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 62dad318..1b3c8183 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -7,7 +7,6 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin test('SPLIT CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); cache.clear(); @@ -41,7 +40,6 @@ test('SPLIT CACHE / LocalStorage', () => { test('SPLIT CACHE / LocalStorage / Get Keys', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); cache.addSplit('lol1', something); cache.addSplit('lol2', somethingElse); @@ -54,7 +52,6 @@ test('SPLIT CACHE / LocalStorage / Get Keys', () => { test('SPLIT CACHE / LocalStorage / Add Splits', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); cache.addSplits([ ['lol1', something], @@ -69,7 +66,6 @@ test('SPLIT CACHE / LocalStorage / Add Splits', () => { test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); cache.addSplits([ // loop of addSplit ['split1', splitWithUserTT], @@ -108,7 +104,6 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { test('SPLIT CACHE / LocalStorage / killLocally', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); cache.addSplit('lol1', something); cache.addSplit('lol2', somethingElse); @@ -142,7 +137,6 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { test('SPLIT CACHE / LocalStorage / usesSegments', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized cache.setChangeNumber(1); // to indicate that data has been synced. @@ -174,7 +168,6 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { } } }, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); const emptySet = new Set([]); @@ -216,7 +209,6 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { // if FlagSets are not defined, it should store all FlagSets in memory. test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.validateCache(fullSettings); const emptySet = new Set([]); @@ -236,6 +228,5 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => // Validate that the feature flag cache is cleared when calling `clear` method cache.clear(); - expect(localStorage.length).toBe(1); // only 'SPLITIO.hash' should remain in localStorage - expect(localStorage.key(0)).toBe('SPLITIO.hash'); + expect(localStorage.length).toBe(0); }); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 58085e17..cb14a235 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -13,6 +13,7 @@ import { DEBUG, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; +import { validateCache } from './validateCache'; export interface InLocalStorageOptions { prefix?: string @@ -51,9 +52,8 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined, uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, - // @TODO implement validateCache() { - return splits.validateCache(settings); + return validateCache(settings, keys, splits); }, destroy() { }, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts new file mode 100644 index 00000000..3fa5f5ae --- /dev/null +++ b/src/storages/inLocalStorage/validateCache.ts @@ -0,0 +1,43 @@ +import { ISettings } from '../../types'; +import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; +import { isNaNNumber } from '../../utils/lang'; +import { getStorageHash } from '../KeyBuilder'; +import { LOG_PREFIX } from './constants'; +import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import { KeyBuilderCS } from '../KeyBuilderCS'; + +/** + * Clean cache if: + * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` + * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + */ +export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal): boolean { + const { log } = settings; + + // Check expiration and clear cache if needed + const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; + let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); + if (value !== null) { + value = parseInt(value, 10); + if (!isNaNNumber(value) && value < expirationTimestamp) splits.clear(); + } + + // Check hash and clear cache if needed + const storageHashKey = keys.buildHashKey(); + const storageHash = localStorage.getItem(storageHashKey); + const currentStorageHash = getStorageHash(settings); + + if (storageHash !== currentStorageHash) { + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); + try { + if (splits.getChangeNumber() > -1) splits.clear(); + + localStorage.setItem(storageHashKey, currentStorageHash); + } catch (e) { + log.error(LOG_PREFIX + e); + } + } + + // Check if the cache is ready + return splits.getChangeNumber() > -1; +} diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 372eeeb4..67f92b05 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -92,7 +92,8 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn // Connects to wrapper and emits SDK_READY event on main client const connectPromise = wrapper.connect().then(() => { if (isSyncronizer) { - // In standalone or producer mode, clear storage if SDK key or feature flag filter has changed + // @TODO reuse InLocalStorage::validateCache logic + // In standalone or producer mode, clear storage if SDK key, flags filter criteria or flags spec version was modified return wrapper.get(keys.buildHashKey()).then((hash) => { const currentHash = getStorageHash(settings); if (hash !== currentHash) { From 679f841c0389da146b8ccdeaf094f3b44c35d875 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 15:17:45 -0300 Subject: [PATCH 08/44] Polishing --- src/sync/offline/syncTasks/fromObjectSyncTask.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 1b3f97f8..d7184fcc 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -1,6 +1,6 @@ import { forOwn } from '../../../utils/lang'; import { IReadinessManager } from '../../../readiness/types'; -import { ISplitsCacheSync, IStorageSync } from '../../../storages/types'; +import { IStorageSync } from '../../../storages/types'; import { ISplitsParser } from '../splitsParser/types'; import { ISplit, ISplitPartial } from '../../../dtos/types'; import { syncTaskFactory } from '../../syncTask'; @@ -81,7 +81,7 @@ export function fromObjectUpdaterFactory( */ export function fromObjectSyncTaskFactory( splitsParser: ISplitsParser, - storage: { splits: ISplitsCacheSync }, + storage: Pick, readiness: IReadinessManager, settings: ISettings ): ISyncTask<[], boolean> { From 6605bfc56e5a4ba11378847fff667ba3243cc278 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 15:39:58 -0300 Subject: [PATCH 09/44] Refactor validateCache function --- src/storages/inLocalStorage/validateCache.ts | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 3fa5f5ae..331a8df9 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -6,23 +6,18 @@ import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; -/** - * Clean cache if: - * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` - * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified - */ -export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal): boolean { +function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { const { log } = settings; - // Check expiration and clear cache if needed + // Check expiration const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); if (value !== null) { value = parseInt(value, 10); - if (!isNaNNumber(value) && value < expirationTimestamp) splits.clear(); + if (!isNaNNumber(value) && value < expirationTimestamp) return true; } - // Check hash and clear cache if needed + // Check hash const storageHashKey = keys.buildHashKey(); const storageHash = localStorage.getItem(storageHashKey); const currentStorageHash = getStorageHash(settings); @@ -30,12 +25,23 @@ export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: S if (storageHash !== currentStorageHash) { log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); try { - if (splits.getChangeNumber() > -1) splits.clear(); - localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { log.error(LOG_PREFIX + e); } + return true; + } +} + +/** + * Clean cache if: + * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` + * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + */ +export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal): boolean { + + if (validateExpiration(settings, keys)) { + splits.clear(); } // Check if the cache is ready From 9b8d36af77c55ead5cfafd835d25aae8a3ea4b16 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 15:48:49 -0300 Subject: [PATCH 10/44] Clear segments and largeSegments caches --- src/storages/inLocalStorage/index.ts | 9 +++------ src/storages/inLocalStorage/validateCache.ts | 5 ++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index cb14a235..f32cc014 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -14,15 +14,12 @@ import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/Telem import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; import { validateCache } from './validateCache'; - -export interface InLocalStorageOptions { - prefix?: string -} +import SplitIO from '../../../types/splitio'; /** * InLocal storage factory for standalone client-side SplitFactory */ -export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyncFactory { +export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): IStorageSyncFactory { const prefix = validatePrefix(options.prefix); @@ -53,7 +50,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, validateCache() { - return validateCache(settings, keys, splits); + return validateCache(settings, keys, splits, segments, largeSegments); }, destroy() { }, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 331a8df9..f76b77ca 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -4,6 +4,7 @@ import { isNaNNumber } from '../../utils/lang'; import { getStorageHash } from '../KeyBuilder'; import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { @@ -38,10 +39,12 @@ function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified */ -export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal): boolean { +export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { if (validateExpiration(settings, keys)) { splits.clear(); + segments.clear(); + largeSegments.clear(); } // Check if the cache is ready From 87fbc4f6fc77791e3868152674c80c14667975a4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 16:20:16 -0300 Subject: [PATCH 11/44] expirationDays configuration --- src/storages/dataLoader.ts | 4 ++- src/storages/inLocalStorage/index.ts | 2 +- src/storages/inLocalStorage/validateCache.ts | 23 +++++++++++------ src/utils/constants/browser.ts | 2 -- src/utils/lang/index.ts | 2 +- types/splitio.d.ts | 26 +++++++++++++++++++- 6 files changed, 45 insertions(+), 14 deletions(-) delete mode 100644 src/utils/constants/browser.ts diff --git a/src/storages/dataLoader.ts b/src/storages/dataLoader.ts index ce288868..55535cfd 100644 --- a/src/storages/dataLoader.ts +++ b/src/storages/dataLoader.ts @@ -1,7 +1,9 @@ import { PreloadedData } from '../types'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser'; import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types'; +// This value might be eventually set via a config parameter +const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days + /** * Factory of client-side storage loader * diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index f32cc014..bb01bd7d 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -50,7 +50,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, validateCache() { - return validateCache(settings, keys, splits, segments, largeSegments); + return validateCache(options, settings, keys, splits, segments, largeSegments); }, destroy() { }, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index f76b77ca..d2da969a 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -1,21 +1,28 @@ import { ISettings } from '../../types'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; -import { isNaNNumber } from '../../utils/lang'; +import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import { getStorageHash } from '../KeyBuilder'; import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; +import SplitIO from '../../../types/splitio'; -function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { +// milliseconds in a day +const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; +const MILLIS_IN_A_DAY = 86400000; + +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS) { const { log } = settings; // Check expiration - const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; + const expirationTimestamp = Date.now() - MILLIS_IN_A_DAY * (isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS); let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); if (value !== null) { value = parseInt(value, 10); - if (!isNaNNumber(value) && value < expirationTimestamp) return true; + if (!isNaNNumber(value) && value < expirationTimestamp) { + log.info(LOG_PREFIX + 'Cache expired. Cleaning up cache'); + return true; + } } // Check hash @@ -24,7 +31,7 @@ function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Cleaning up cache'); try { localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { @@ -39,9 +46,9 @@ function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified */ -export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { +export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { - if (validateExpiration(settings, keys)) { + if (validateExpiration(options, settings, keys)) { splits.clear(); segments.clear(); largeSegments.clear(); diff --git a/src/utils/constants/browser.ts b/src/utils/constants/browser.ts deleted file mode 100644 index d627f780..00000000 --- a/src/utils/constants/browser.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This value might be eventually set via a config parameter -export const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 11b6afd0..b1a7e35a 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -120,7 +120,7 @@ export function isBoolean(val: any): boolean { * Unlike `Number.isFinite`, it also tests Number object instances. * Unlike global `isFinite`, it returns false if the value is not a number or Number object instance. */ -export function isFiniteNumber(val: any): boolean { +export function isFiniteNumber(val: any): val is number { if (val instanceof Number) val = val.valueOf(); return typeof val === 'number' ? Number.isFinite ? Number.isFinite(val) : isFinite(val) : diff --git a/types/splitio.d.ts b/types/splitio.d.ts index bb108c1c..4aa59db3 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -906,6 +906,18 @@ declare namespace SplitIO { * @defaultValue `'SPLITIO'` */ prefix?: string; + /** + * Number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * + * @defaultValue `10` + */ + expirationDays?: number; + /** + * Optional settings to clear the cache. If set to `true`, the SDK clears the cached data on initialization, unless the cache was cleared within the last 24 hours. + * + * @defaultValue `false` + */ + clearOnInit?: boolean; } /** * Storage for asynchronous (consumer) SDK. @@ -1229,11 +1241,23 @@ declare namespace SplitIO { */ type?: BrowserStorage; /** - * Optional prefix to prevent any kind of data collision between SDK versions. + * Optional prefix to prevent any kind of data collision between SDK versions when using 'LOCALSTORAGE'. * * @defaultValue `'SPLITIO'` */ prefix?: string; + /** + * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * + * @defaultValue `10` + */ + expirationDays?: number; + /** + * Optional settings for the 'LOCALSTORAGE' storage type. If set to `true`, the SDK clears the cached data on initialization, unless the cache was cleared within the last 24 hours. + * + * @defaultValue `false` + */ + clearOnInit?: boolean; }; } /** From aca35fe3fea2171e532c43b3de79c535cae4731c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 16:37:47 -0300 Subject: [PATCH 12/44] clearOnInit configuration --- src/storages/KeyBuilderCS.ts | 4 ++++ src/storages/inLocalStorage/validateCache.ts | 21 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index a59d7208..20372358 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -43,6 +43,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { buildTillKey() { return `${this.prefix}.${this.matchingKey}.segments.till`; } + + buildLastClear() { + return `${this.prefix}.lastClear`; + } } export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): MySegmentsKeyBuilder { diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index d2da969a..7c1fa31d 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -39,6 +39,18 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS } return true; } + + // Clear on init + if (options.clearOnInit) { + let value: string | number | null = localStorage.getItem(keys.buildLastClear()); + if (value !== null) { + value = parseInt(value, 10); + if (!isNaNNumber(value) && value < Date.now() - MILLIS_IN_A_DAY) { + log.info(LOG_PREFIX + 'Clear on init was set and cache was cleared more than a day ago. Cleaning up cache'); + return true; + } + } + } } /** @@ -52,8 +64,15 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: splits.clear(); segments.clear(); largeSegments.clear(); + + // Update last clear timestamp + try { + localStorage.setItem(keys.buildLastClear(), Date.now() + ''); + } catch (e) { + settings.log.error(LOG_PREFIX + e); + } } - // Check if the cache is ready + // Check if ready from cache return splits.getChangeNumber() > -1; } From 534d6ca8808662c6ad735f323694db49bfc92a1a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 11:47:33 -0300 Subject: [PATCH 13/44] Reuse Date.now() result --- src/storages/inLocalStorage/validateCache.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 7c1fa31d..d7690828 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -11,16 +11,17 @@ import SplitIO from '../../../types/splitio'; const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; -function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS) { +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number) { const { log } = settings; // Check expiration - const expirationTimestamp = Date.now() - MILLIS_IN_A_DAY * (isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS); + const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; + const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); if (value !== null) { value = parseInt(value, 10); if (!isNaNNumber(value) && value < expirationTimestamp) { - log.info(LOG_PREFIX + 'Cache expired. Cleaning up cache'); + log.info(LOG_PREFIX + 'Cache expired more than ' + cacheExpirationInDays + ' days ago. Cleaning up cache'); return true; } } @@ -45,7 +46,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS let value: string | number | null = localStorage.getItem(keys.buildLastClear()); if (value !== null) { value = parseInt(value, 10); - if (!isNaNNumber(value) && value < Date.now() - MILLIS_IN_A_DAY) { + if (!isNaNNumber(value) && value < currentTimestamp - MILLIS_IN_A_DAY) { log.info(LOG_PREFIX + 'Clear on init was set and cache was cleared more than a day ago. Cleaning up cache'); return true; } @@ -60,14 +61,16 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS */ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { - if (validateExpiration(options, settings, keys)) { + const currentTimestamp = Date.now(); + + if (validateExpiration(options, settings, keys, currentTimestamp)) { splits.clear(); segments.clear(); largeSegments.clear(); // Update last clear timestamp try { - localStorage.setItem(keys.buildLastClear(), Date.now() + ''); + localStorage.setItem(keys.buildLastClear(), currentTimestamp + ''); } catch (e) { settings.log.error(LOG_PREFIX + e); } From 6451cda87ee72fe71f506ca0224c46e8ccf5d372 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 12:48:08 -0300 Subject: [PATCH 14/44] Handle clearOnInit case with older version of the SDK where lastClear item is not available --- src/storages/inLocalStorage/validateCache.ts | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index d7690828..b8676cb7 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -15,12 +15,11 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS const { log } = settings; // Check expiration - const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; - const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; - let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && value < expirationTimestamp) { + const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10); + if (!isNaNNumber(lastUpdatedTimestamp)) { + const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; + const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; + if (lastUpdatedTimestamp < expirationTimestamp) { log.info(LOG_PREFIX + 'Cache expired more than ' + cacheExpirationInDays + ' days ago. Cleaning up cache'); return true; } @@ -32,7 +31,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Cleaning up cache'); + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); try { localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { @@ -43,13 +42,11 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS // Clear on init if (options.clearOnInit) { - let value: string | number | null = localStorage.getItem(keys.buildLastClear()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && value < currentTimestamp - MILLIS_IN_A_DAY) { - log.info(LOG_PREFIX + 'Clear on init was set and cache was cleared more than a day ago. Cleaning up cache'); - return true; - } + const lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()) as string, 10); + + if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) { + log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + return true; } } } @@ -58,6 +55,8 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS * Clean cache if: * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + * + * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) */ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { From 956c1df61ebec9f4df14dc040970026faae97884 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 13:22:25 -0300 Subject: [PATCH 15/44] Handle no cache: cache should not be clearer --- src/storages/inLocalStorage/validateCache.ts | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index b8676cb7..7c0c8ae9 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -11,7 +11,12 @@ import SplitIO from '../../../types/splitio'; const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; -function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number) { +/** + * Validates if cache should be cleared and sets the cache `hash` if needed. + * + * @returns `true` if cache should be cleared, `false` otherwise + */ +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) { const { log } = settings; // Check expiration @@ -31,13 +36,16 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); try { localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { log.error(LOG_PREFIX + e); } - return true; + if (isThereCache) { + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + return true; + } + return false; // No cache to clear } // Clear on init @@ -54,15 +62,17 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS /** * Clean cache if: * - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp` - * - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + * - `clearOnInit` was set and cache was not cleared in the last 24 hours * * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) */ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { const currentTimestamp = Date.now(); + const isThereCache = splits.getChangeNumber() > -1; - if (validateExpiration(options, settings, keys, currentTimestamp)) { + if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) { splits.clear(); segments.clear(); largeSegments.clear(); @@ -73,8 +83,10 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: } catch (e) { settings.log.error(LOG_PREFIX + e); } + + return false; } // Check if ready from cache - return splits.getChangeNumber() > -1; + return isThereCache; } From 28b7fb994217f92c55c3819b810d50e159c1b60b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 17:24:30 -0300 Subject: [PATCH 16/44] Add unit test --- .../inLocalStorage/SplitsCacheInLocal.ts | 2 - .../__tests__/validateCache.spec.ts | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/storages/inLocalStorage/__tests__/validateCache.spec.ts diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 14767d72..7d4c002f 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -71,8 +71,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * We cannot simply call `localStorage.clear()` since that implies removing user items from the storage. */ clear() { - this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage'); - // collect item keys const len = localStorage.length; const accum = []; diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts new file mode 100644 index 00000000..a1f1784d --- /dev/null +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -0,0 +1,125 @@ +import { validateCache } from '../validateCache'; + +import { KeyBuilderCS } from '../../KeyBuilderCS'; +import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; +import { nearlyEqual } from '../../../__tests__/testUtils'; +import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; + +const FULL_SETTINGS_HASH = '404832b3'; + +describe('validateCache', () => { + const keys = new KeyBuilderCS('SPLITIO', 'user'); + const logSpy = jest.spyOn(fullSettings.log, 'info'); + const segments = new MySegmentsCacheInLocal(fullSettings.log, keys); + const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys); + const splits = new SplitsCacheInLocal(fullSettings, keys); + + jest.spyOn(splits, 'clear'); + jest.spyOn(splits, 'getChangeNumber'); + jest.spyOn(segments, 'clear'); + jest.spyOn(largeSegments, 'clear'); + + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + }); + + test('if there is no cache, it should return false', () => { + expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).not.toHaveBeenCalled(); + + expect(splits.clear).not.toHaveBeenCalled(); + expect(segments.clear).not.toHaveBeenCalled(); + expect(largeSegments.clear).not.toHaveBeenCalled(); + expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(localStorage.getItem(keys.buildLastClear())).toBeNull(); + }); + + test('if there is cache and it must not be cleared, it should return true', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + + expect(logSpy).not.toHaveBeenCalled(); + + expect(splits.clear).not.toHaveBeenCalled(); + expect(segments.clear).not.toHaveBeenCalled(); + expect(largeSegments.clear).not.toHaveBeenCalled(); + expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(localStorage.getItem(keys.buildLastClear())).toBeNull(); + }); + + test('if there is cache and it has expired, it should clear cache and return false', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago + + expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); + + test('if there is cache and its hash has changed, it should clear cache and return false', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe('aa4877c2'); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); + + test('if there is cache and clearOnInit is true, it should clear cache and return false', () => { + // Older cache version (without last clear) + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + const lastClear = localStorage.getItem(keys.buildLastClear()); + expect(nearlyEqual(parseInt(lastClear as string), Date.now())).toBe(true); + + // If cache is cleared, it should not clear again until a day has passed + logSpy.mockClear(); + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(logSpy).not.toHaveBeenCalled(); + expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed + + // If a day has passed, it should clear again + localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + expect(splits.clear).toHaveBeenCalledTimes(2); + expect(segments.clear).toHaveBeenCalledTimes(2); + expect(largeSegments.clear).toHaveBeenCalledTimes(2); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); +}); From 6dc1e613c74cadf1aef770dac902a22c2a846e3a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 18:16:33 -0300 Subject: [PATCH 17/44] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 971d1486..160c9c26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.2", + "version": "2.1.0-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.0.2", + "version": "2.1.0-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 936c23bd..37317c20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.2", + "version": "2.1.0-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 5ea10ad81efaa55570df61c8f1ac6b89b18b16a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 18:31:35 -0300 Subject: [PATCH 18/44] Add changelog entry --- CHANGES.txt | 5 +++++ src/storages/inLocalStorage/validateCache.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 02a7bd61..6988dc05 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +2.1.0 (January XX, 2025) + - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: + - `storage.expirationDays` to specify the validity period of the rollout cache. + - `storage.clearOnInit` to clear the rollout cache on SDK initialization. + 2.0.2 (December 3, 2024) - Updated the factory `init` and `destroy` methods to support re-initialization after destruction. This update ensures compatibility of the React SDK with React Strict Mode, where the factory's `init` and `destroy` effects are executed an extra time to validate proper resource cleanup. - Bugfixing - Sanitize the `SplitSDKMachineName` header value to avoid exceptions on HTTP/S requests when it contains non ISO-8859-1 characters (Related to issue https://github.com/splitio/javascript-client/issues/847). diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 7c0c8ae9..c11e8d90 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -7,7 +7,6 @@ import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; import SplitIO from '../../../types/splitio'; -// milliseconds in a day const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; From 71007342f9b44180518021f2efc7a379a94f1226 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 20 Dec 2024 13:53:44 -0300 Subject: [PATCH 19/44] Fix typo --- src/storages/inLocalStorage/__tests__/validateCache.spec.ts | 2 +- src/storages/inLocalStorage/validateCache.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index a1f1784d..27050a56 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -79,7 +79,7 @@ describe('validateCache', () => { expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); - expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index c11e8d90..c9bd78d2 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -41,7 +41,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS log.error(LOG_PREFIX + e); } if (isThereCache) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + log.info(LOG_PREFIX + 'SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); return true; } return false; // No cache to clear From 4c7e7812d2798d8601a81eedfdd4a634d44421fe Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Jan 2025 13:50:57 -0300 Subject: [PATCH 20/44] Updated SDK_READY_FROM_CACHE event when using LOCALSTORAGE storage type to be emitted alongside SDK_READY event in case it has not been emitted --- CHANGES.txt | 1 + src/readiness/__tests__/readinessManager.spec.ts | 14 +++++++++++++- src/readiness/readinessManager.ts | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index db363080..0b3a5fec 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,7 @@ - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: - `storage.expirationDays` to specify the validity period of the rollout cache. - `storage.clearOnInit` to clear the rollout cache on SDK initialization. + - Updated SDK_READY_FROM_CACHE event when using `LOCALSTORAGE` storage type to be emitted alongside SDK_READY event in case it has not been emitted. - Bugfixing - Properly handle rejected promises when using targeting rules with segment matchers in consumer modes (e.g., Redis and Pluggable storages). 2.0.2 (December 3, 2024) diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 9e2cf34a..174f1373 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -3,10 +3,14 @@ import { EventEmitter } from '../../utils/MinEvents'; import { IReadinessManager } from '../types'; import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants'; import { ISettings } from '../../types'; +import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; const settings = { startup: { readyTimeout: 0, + }, + storage: { + type: STORAGE_LOCALSTORAGE } } as unknown as ISettings; @@ -67,7 +71,14 @@ test('READINESS MANAGER / Ready event should be fired once', () => { const readinessManager = readinessManagerFactory(EventEmitter, settings); let counter = 0; + readinessManager.gate.on(SDK_READY_FROM_CACHE, () => { + expect(readinessManager.isReadyFromCache()).toBe(true); + expect(readinessManager.isReady()).toBe(true); + counter++; + }); + readinessManager.gate.on(SDK_READY, () => { + expect(readinessManager.isReadyFromCache()).toBe(true); expect(readinessManager.isReady()).toBe(true); counter++; }); @@ -79,7 +90,7 @@ test('READINESS MANAGER / Ready event should be fired once', () => { readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - expect(counter).toBe(1); // should be called once + expect(counter).toBe(2); // should be called once }); test('READINESS MANAGER / Ready from cache event should be fired once', (done) => { @@ -88,6 +99,7 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) = readinessManager.gate.on(SDK_READY_FROM_CACHE, () => { expect(readinessManager.isReadyFromCache()).toBe(true); + expect(readinessManager.isReady()).toBe(false); counter++; }); diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 6f46474d..c69eedce 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -3,6 +3,7 @@ import { ISettings } from '../types'; import SplitIO from '../../types/splitio'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants'; import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types'; +import { STORAGE_LOCALSTORAGE } from '../utils/constants'; function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter { const splitsEventEmitter = objectAssign(new EventEmitter(), { @@ -114,6 +115,10 @@ export function readinessManagerFactory( isReady = true; try { syncLastUpdate(); + if (!isReadyFromCache && settings.storage?.type === STORAGE_LOCALSTORAGE) { + isReadyFromCache = true; + gate.emit(SDK_READY_FROM_CACHE); + } gate.emit(SDK_READY); } catch (e) { // throws user callback exceptions in next tick From d5a5eaa053dd8169aeca66f1ddcc9b61675889a2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Jan 2025 13:52:08 -0300 Subject: [PATCH 21/44] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 160c9c26..7b2773fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0-rc.0", + "version": "2.1.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0-rc.0", + "version": "2.1.0-rc.1", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 37317c20..1e4eb233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0-rc.0", + "version": "2.1.0-rc.1", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From d854156fcf1a9b862bb618710a69484abdd948bb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Jan 2025 14:40:02 -0300 Subject: [PATCH 22/44] Update changelog entry --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0b3a5fec..b20ac98a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,7 +2,7 @@ - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: - `storage.expirationDays` to specify the validity period of the rollout cache. - `storage.clearOnInit` to clear the rollout cache on SDK initialization. - - Updated SDK_READY_FROM_CACHE event when using `LOCALSTORAGE` storage type to be emitted alongside SDK_READY event in case it has not been emitted. + - Updated SDK_READY_FROM_CACHE event when using the `LOCALSTORAGE` storage type to be emitted alongside the SDK_READY event if it has not already been emitted. - Bugfixing - Properly handle rejected promises when using targeting rules with segment matchers in consumer modes (e.g., Redis and Pluggable storages). 2.0.2 (December 3, 2024) From 20d97aa7f155273e9436a0ac2e4287530a80d067 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Jan 2025 14:07:12 -0300 Subject: [PATCH 23/44] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b2773fe..e774db65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0-rc.1", + "version": "2.1.0-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0-rc.1", + "version": "2.1.0-rc.2", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 1e4eb233..a955ce56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0-rc.1", + "version": "2.1.0-rc.2", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 1cc5ad6f7d8b35ce7ab07bc1e85727294ee66348 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 Feb 2025 23:36:59 -0300 Subject: [PATCH 24/44] Remove unnecessary comment --- src/sync/polling/updaters/splitChangesUpdater.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index e447227d..43f471a2 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -169,7 +169,6 @@ export function splitChangesUpdaterFactory( // Write into storage // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency return Promise.all([ - // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed splits.setChangeNumber(splitChanges.till), splits.addSplits(mutation.added), splits.removeSplits(mutation.removed), From 40124f746f855d587672f0640e966892bbfead51 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Feb 2025 11:32:02 -0300 Subject: [PATCH 25/44] Refactor Split cache: add 'update' method --- src/logger/constants.ts | 4 +- src/logger/messages/debug.ts | 4 +- .../__tests__/index.asyncCache.spec.ts | 2 +- .../__tests__/index.syncCache.spec.ts | 2 +- src/storages/AbstractSplitsCacheAsync.ts | 20 ++- src/storages/AbstractSplitsCacheSync.ts | 22 ++- src/storages/__tests__/testUtils.ts | 4 +- src/storages/dataLoader.ts | 3 +- .../inLocalStorage/SplitsCacheInLocal.ts | 3 +- .../__tests__/SplitsCacheInLocal.spec.ts | 118 ++++++++-------- src/storages/inMemory/SplitsCacheInMemory.ts | 3 +- .../__tests__/SplitsCacheInMemory.spec.ts | 111 ++++++++------- src/storages/inRedis/SplitsCacheInRedis.ts | 25 +--- .../__tests__/SplitsCacheInRedis.spec.ts | 103 +++++++------- .../pluggable/SplitsCachePluggable.ts | 21 +-- .../__tests__/SplitsCachePluggable.spec.ts | 127 ++++++++---------- src/storages/types.ts | 12 +- .../offline/syncTasks/fromObjectSyncTask.ts | 29 ++-- .../__tests__/splitChangesUpdater.spec.ts | 50 +++---- .../polling/updaters/splitChangesUpdater.ts | 33 ++--- .../__tests__/SplitsUpdateWorker.spec.ts | 12 +- 21 files changed, 319 insertions(+), 389 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 520a5707..855675ff 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -20,9 +20,7 @@ export const RETRIEVE_CLIENT_EXISTING = 28; export const RETRIEVE_MANAGER = 29; export const SYNC_OFFLINE_DATA = 30; export const SYNC_SPLITS_FETCH = 31; -export const SYNC_SPLITS_NEW = 32; -export const SYNC_SPLITS_REMOVED = 33; -export const SYNC_SPLITS_SEGMENTS = 34; +export const SYNC_SPLITS_UPDATE = 32; export const STREAMING_NEW_MESSAGE = 35; export const SYNC_TASK_START = 36; export const SYNC_TASK_EXECUTE = 37; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index c89694e6..5dfcace3 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -21,9 +21,7 @@ export const codesDebug: [number, string][] = codesInfo.concat([ // synchronizer [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Feature flags data: \n%s'], [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up feature flags update using since = %s'], - [c.SYNC_SPLITS_NEW, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags %s'], - [c.SYNC_SPLITS_REMOVED, c.LOG_PREFIX_SYNC_SPLITS + 'Removed feature flags %s'], - [c.SYNC_SPLITS_SEGMENTS, c.LOG_PREFIX_SYNC_SPLITS + 'Segment names collected %s'], + [c.SYNC_SPLITS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags %s. Removed feature flags %s. Segment names collected %s'], [c.STREAMING_NEW_MESSAGE, c.LOG_PREFIX_SYNC_STREAMING + 'New SSE message received, with data: %s.'], [c.SYNC_TASK_START, c.LOG_PREFIX_SYNC + ': Starting %s. Running each %s millis'], [c.SYNC_TASK_EXECUTE, c.LOG_PREFIX_SYNC + ': Running %s'], diff --git a/src/sdkManager/__tests__/index.asyncCache.spec.ts b/src/sdkManager/__tests__/index.asyncCache.spec.ts index a0277a75..429513d4 100644 --- a/src/sdkManager/__tests__/index.asyncCache.spec.ts +++ b/src/sdkManager/__tests__/index.asyncCache.spec.ts @@ -33,7 +33,7 @@ describe('Manager with async cache', () => { const cache = new SplitsCacheInRedis(loggerMock, keys, connection); const manager = sdkManagerFactory({ mode: 'consumer', log: loggerMock }, cache, sdkReadinessManagerMock); await cache.clear(); - await cache.addSplit(splitObject.name, splitObject as any); + await cache.addSplit(splitObject as any); /** List all splits */ const views = await manager.splits(); diff --git a/src/sdkManager/__tests__/index.syncCache.spec.ts b/src/sdkManager/__tests__/index.syncCache.spec.ts index 3ca1cfa0..391a053c 100644 --- a/src/sdkManager/__tests__/index.syncCache.spec.ts +++ b/src/sdkManager/__tests__/index.syncCache.spec.ts @@ -19,7 +19,7 @@ describe('Manager with sync cache (In Memory)', () => { /** Setup: create manager */ const cache = new SplitsCacheInMemory(); const manager = sdkManagerFactory({ mode: 'standalone', log: loggerMock }, cache, sdkReadinessManagerMock); - cache.addSplit(splitObject.name, splitObject as any); + cache.addSplit(splitObject as any); test('List all splits', () => { diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index dcf059ed..bafc8e2c 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -8,12 +8,22 @@ import { objectAssign } from '../utils/lang/objectAssign'; */ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { - abstract addSplit(name: string, split: ISplit): Promise - abstract addSplits(entries: [string, ISplit][]): Promise - abstract removeSplits(names: string[]): Promise + protected abstract setChangeNumber(changeNumber: number): Promise + protected abstract addSplit(split: ISplit): Promise + protected abstract removeSplit(name: string): Promise + + update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(addedFFs.map(addedFF => this.addSplit(addedFF))), + Promise.all(removedFFs.map(removedFF => this.removeSplit(removedFF.name))) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + abstract getSplit(name: string): Promise abstract getSplits(names: string[]): Promise> - abstract setChangeNumber(changeNumber: number): Promise abstract getChangeNumber(): Promise abstract getAll(): Promise abstract getSplitNames(): Promise @@ -52,7 +62,7 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { newSplit.defaultTreatment = defaultTreatment; newSplit.changeNumber = changeNumber; - return this.addSplit(name, newSplit); + return this.addSplit(newSplit); } return false; }).catch(() => false); diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index f82ebbd6..d479305f 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -9,16 +9,14 @@ import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; */ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { - abstract addSplit(name: string, split: ISplit): boolean - - addSplits(entries: [string, ISplit][]): boolean[] { - return entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1])); - } - - abstract removeSplit(name: string): boolean - - removeSplits(names: string[]): boolean[] { - return names.map(name => this.removeSplit(name)); + protected abstract setChangeNumber(changeNumber: number): boolean | void + protected abstract addSplit(split: ISplit): boolean + protected abstract removeSplit(name: string): boolean + + update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): boolean { + this.setChangeNumber(changeNumber); + const updated = addedFFs.map(addedFF => this.addSplit(addedFF)).some(result => result); + return removedFFs.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; } abstract getSplit(name: string): ISplit | null @@ -31,8 +29,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { return splits; } - abstract setChangeNumber(changeNumber: number): boolean | void - abstract getChangeNumber(): number getAll(): ISplit[] { @@ -71,7 +67,7 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { newSplit.defaultTreatment = defaultTreatment; newSplit.changeNumber = changeNumber; - return this.addSplit(name, newSplit); + return this.addSplit(newSplit); } return false; } diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index 94e11c36..fa38944f 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -23,9 +23,9 @@ export function assertSyncRecorderCacheInterface(cache: IEventsCacheSync | IImpr // Split mocks //@ts-ignore -export const splitWithUserTT: ISplit = { trafficTypeName: 'user_tt', conditions: [] }; +export const splitWithUserTT: ISplit = { name: 'user_ff', trafficTypeName: 'user_tt', conditions: [] }; //@ts-ignore -export const splitWithAccountTT: ISplit = { trafficTypeName: 'account_tt', conditions: [] }; +export const splitWithAccountTT: ISplit = { name: 'account_ff', trafficTypeName: 'account_tt', conditions: [] }; //@ts-ignore export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'account_tt', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; //@ts-ignore diff --git a/src/storages/dataLoader.ts b/src/storages/dataLoader.ts index ce288868..2ab542a9 100644 --- a/src/storages/dataLoader.ts +++ b/src/storages/dataLoader.ts @@ -35,10 +35,9 @@ export function dataLoaderFactory(preloadedData: PreloadedData): DataLoader { // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data storage.splits.clear(); - storage.splits.setChangeNumber(since); // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data - storage.splits.addSplits(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName]))); + storage.splits.update(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName])), [], since); // add mySegments data let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 93eb6f32..1562cd08 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -96,8 +96,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this.hasSync = false; } - addSplit(name: string, split: ISplit) { + addSplit(split: ISplit) { try { + const name = split.name; const splitKey = this.keys.buildSplitKey(name); const splitFromLocalStorage = localStorage.getItem(splitKey); const previousSplit = splitFromLocalStorage ? JSON.parse(splitFromLocalStorage) : null; diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 4d8ec076..db78c741 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -5,34 +5,33 @@ import { ISplit } from '../../../dtos/types'; import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; -test('SPLIT CACHE / LocalStorage', () => { +test('SPLITS CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); cache.clear(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], -1); let values = cache.getAll(); expect(values).toEqual([something, somethingElse]); - cache.removeSplit('lol1'); + cache.removeSplit(something.name); - const splits = cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1']).toEqual(null); - expect(splits['lol2']).toEqual(somethingElse); + const splits = cache.getSplits([something.name, somethingElse.name]); + expect(splits[something.name]).toEqual(null); + expect(splits[somethingElse.name]).toEqual(somethingElse); values = cache.getAll(); expect(values).toEqual([somethingElse]); - expect(cache.getSplit('lol1')).toEqual(null); - expect(cache.getSplit('lol2')).toEqual(somethingElse); + expect(cache.getSplit(something.name)).toEqual(null); + expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse); expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data. - expect(cache.getChangeNumber() === -1).toBe(true); + expect(cache.getChangeNumber()).toBe(-1); expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data. @@ -40,45 +39,41 @@ test('SPLIT CACHE / LocalStorage', () => { expect(cache.checkCache()).toBe(true); // checkCache should return true once localstorage has data. - expect(cache.getChangeNumber() === 123).toBe(true); + expect(cache.getChangeNumber()).toBe(123); }); -test('SPLIT CACHE / LocalStorage / Get Keys', () => { +test('SPLITS CACHE / LocalStorage / Get Keys', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], 1); const keys = cache.getSplitNames(); - expect(keys.indexOf('lol1') !== -1).toBe(true); - expect(keys.indexOf('lol2') !== -1).toBe(true); + expect(keys.indexOf(something.name) !== -1).toBe(true); + expect(keys.indexOf(somethingElse.name) !== -1).toBe(true); }); -test('SPLIT CACHE / LocalStorage / Add Splits', () => { +test('SPLITS CACHE / LocalStorage / Update Splits', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplits([ - ['lol1', something], - ['lol2', somethingElse] - ]); + cache.update([something, somethingElse], [], 1); - cache.removeSplits(['lol1', 'lol2']); + cache.update([], [something, somethingElse], 1); - expect(cache.getSplit('lol1') == null).toBe(true); - expect(cache.getSplit('lol2') == null).toBe(true); + expect(cache.getSplit(something.name)).toBe(null); + expect(cache.getSplit(somethingElse.name)).toBe(null); }); -test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { +test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplits([ // loop of addSplit - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - cache.addSplit('split4', splitWithUserTT); + cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], 1); + cache.addSplit({ ...splitWithUserTT, name: 'split4' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); @@ -89,7 +84,8 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); - cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + cache.removeSplit('split3'); + cache.removeSplit('split2'); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(false); @@ -99,19 +95,19 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(false); expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.addSplit('split1', splitWithUserTT); + cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); - cache.addSplit('split1', splitWithAccountTT); + cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(cache.trafficTypeExists('account_tt')).toBe(true); expect(cache.trafficTypeExists('user_tt')).toBe(false); }); -test('SPLIT CACHE / LocalStorage / killLocally', () => { +test('SPLITS CACHE / LocalStorage / killLocally', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.addSplit(something); + cache.addSplit(somethingElse); const initialChangeNumber = cache.getChangeNumber(); // kill an non-existent split @@ -122,8 +118,8 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment', 100); + let lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed @@ -132,27 +128,27 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment_2', 90); + lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older }); -test('SPLIT CACHE / LocalStorage / usesSegments', () => { +test('SPLITS CACHE / LocalStorage / usesSegments', () => { const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized cache.setChangeNumber(1); // to indicate that data has been synced. - cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); + cache.update([splitWithUserTT, splitWithAccountTT], [], 1); expect(cache.usesSegments()).toBe(false); // 0 splits using segments - cache.addSplit('split3', splitWithAccountTTAndUsesSegments); + cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split3' }); expect(cache.usesSegments()).toBe(true); // 1 split using segments - cache.addSplit('split4', splitWithAccountTTAndUsesSegments); + cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split4' }); expect(cache.usesSegments()).toBe(true); // 2 splits using segments cache.removeSplit('split3'); @@ -162,7 +158,7 @@ test('SPLIT CACHE / LocalStorage / usesSegments', () => { expect(cache.usesSegments()).toBe(false); // 0 splits using segments }); -test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { +test('SPLITS CACHE / LocalStorage / flag set cache tests', () => { // @ts-ignore const cache = new SplitsCacheInLocal({ ...fullSettings, @@ -175,12 +171,12 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { }, new KeyBuilderCS('SPLITIO', 'user')); const emptySet = new Set([]); - cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -188,13 +184,13 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -206,7 +202,7 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(cache.getNamesByFlagSets([])).toEqual([]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + cache.addSplit(featureFlagWithoutFS); expect(cache.getNamesByFlagSets([])).toEqual([]); }); @@ -215,12 +211,12 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => const cacheWithoutFilters = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); const emptySet = new Set([]); - cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 688b6e24..347ee1e1 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -26,7 +26,8 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { this.segmentsCount = 0; } - addSplit(name: string, split: ISplit): boolean { + addSplit(split: ISplit): boolean { + const name = split.name; const previousSplit = this.getSplit(name); if (previousSplit) { // We had this Split already diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 62812586..2f907eca 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -5,54 +5,62 @@ import { splitWithUserTT, splitWithAccountTT, something, somethingElse, featureF test('SPLITS CACHE / In Memory', () => { const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], -1); let values = cache.getAll(); - expect(values.indexOf(something) !== -1).toBe(true); - expect(values.indexOf(somethingElse) !== -1).toBe(true); + expect(values).toEqual([something, somethingElse]); - cache.removeSplit('lol1'); + cache.removeSplit(something.name); - const splits = cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1'] === null).toBe(true); - expect(splits['lol2'] === somethingElse).toBe(true); + const splits = cache.getSplits([something.name, somethingElse.name]); + expect(splits[something.name]).toEqual(null); + expect(splits[somethingElse.name]).toEqual(somethingElse); values = cache.getAll(); - expect(values.indexOf(something) === -1).toBe(true); - expect(values.indexOf(somethingElse) !== -1).toBe(true); + expect(values).toEqual([somethingElse]); - expect(cache.getSplit('lol1') == null).toBe(true); - expect(cache.getSplit('lol2') === somethingElse).toBe(true); + expect(cache.getSplit(something.name)).toEqual(null); + expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse); + expect(cache.getChangeNumber()).toBe(-1); cache.setChangeNumber(123); - expect(cache.getChangeNumber() === 123).toBe(true); + expect(cache.getChangeNumber()).toBe(123); }); test('SPLITS CACHE / In Memory / Get Keys', () => { const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.update([something, somethingElse], [], 1); - let keys = cache.getSplitNames(); + const keys = cache.getSplitNames(); - expect(keys.indexOf('lol1') !== -1).toBe(true); - expect(keys.indexOf('lol2') !== -1).toBe(true); + expect(keys.indexOf(something.name) !== -1).toBe(true); + expect(keys.indexOf(somethingElse.name) !== -1).toBe(true); +}); + +test('SPLITS CACHE / In Memory / Update Splits', () => { + const cache = new SplitsCacheInMemory(); + + cache.update([something, somethingElse], [], 1); + + cache.update([], [something, somethingElse], 1); + + expect(cache.getSplit(something.name)).toBe(null); + expect(cache.getSplit(somethingElse.name)).toBe(null); }); test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInMemory(); - cache.addSplits([ // loop of addSplit - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - cache.addSplit('split4', splitWithUserTT); + cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], 1); + cache.addSplit({ ...splitWithUserTT, name: 'split4' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); @@ -63,7 +71,8 @@ test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); - cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + cache.removeSplit('split3'); + cache.removeSplit('split2'); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(false); @@ -73,10 +82,10 @@ test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(false); expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.addSplit('split1', splitWithUserTT); + cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(cache.trafficTypeExists('user_tt')).toBe(true); - cache.addSplit('split1', splitWithAccountTT); + cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(cache.trafficTypeExists('account_tt')).toBe(true); expect(cache.trafficTypeExists('user_tt')).toBe(false); @@ -84,30 +93,30 @@ test('SPLITS CACHE / In Memory / trafficTypeExists and ttcache tests', () => { test('SPLITS CACHE / In Memory / killLocally', () => { const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', something); - cache.addSplit('lol2', somethingElse); + cache.addSplit(something); + cache.addSplit(somethingElse); const initialChangeNumber = cache.getChangeNumber(); // kill an non-existent split let updated = cache.killLocally('nonexistent_split', 'other_treatment', 101); const nonexistentSplit = cache.getSplit('nonexistent_split'); - expect(updated).toBe(false); // killLocally resolves without update if split doesn\'t exist + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment', 100); + let lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed - expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have the given default treatment + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = cache.getSplit('lol1') as ISplit; + updated = cache.killLocally(something.name, 'some_treatment_2', 90); + lol1Split = cache.getSplit(something.name) as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older @@ -119,12 +128,12 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { const cache = new SplitsCacheInMemory({ groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new Set([]); - cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -132,13 +141,13 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -150,21 +159,21 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(cache.getNamesByFlagSets([])).toEqual([]); - cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + cache.addSplit(featureFlagWithoutFS); expect(cache.getNamesByFlagSets([])).toEqual([]); }); // if FlagSets are not defined, it should store all FlagSets in memory. -test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { +test('SPLITS CACHE / In Memory / flag set cache tests without filters', () => { const cacheWithoutFilters = new SplitsCacheInMemory(); const emptySet = new Set([]); - cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 7ae68e64..ec360b2f 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -82,7 +82,8 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * The returned promise is resolved when the operation success * or rejected if it fails (e.g., redis operation fails) */ - addSplit(name: string, split: ISplit): Promise { + addSplit(split: ISplit): Promise { + const name = split.name; const splitKey = this.keys.buildSplitKey(name); return this.redis.get(splitKey).then(splitFromStorage => { @@ -107,18 +108,9 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { }).then(() => true); } - /** - * Add a list of splits. - * The returned promise is resolved when the operation success - * or rejected if it fails (e.g., redis operation fails) - */ - addSplits(entries: [string, ISplit][]): Promise { - return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); - } - /** * Remove a given split. - * The returned promise is resolved when the operation success, with 1 or 0 indicating if the split existed or not. + * The returned promise is resolved when the operation success, with true or false indicating if the split existed (and was removed) or not. * or rejected if it fails (e.g., redis operation fails). */ removeSplit(name: string) { @@ -127,19 +119,10 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { return this._decrementCounts(split).then(() => this._updateFlagSets(name, split.sets)); } }).then(() => { - return this.redis.del(this.keys.buildSplitKey(name)); + return this.redis.del(this.keys.buildSplitKey(name)).then(status => status === 1); }); } - /** - * Remove a list of splits. - * The returned promise is resolved when the operation success, - * or rejected if it fails (e.g., redis operation fails). - */ - removeSplits(names: string[]): Promise { - return Promise.all(names.map(name => this.removeSplit(name))); - } - /** * Get split definition or null if it's not defined. * Returned promise is rejected if redis operation fails. diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index 3f577254..0cbc8914 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -11,14 +11,11 @@ const keysBuilder = new KeyBuilderSS(prefix, metadata); describe('SPLITS CACHE REDIS', () => { - test('add/remove/get splits & set/get change number', async () => { + test('add/remove/get splits', async () => { const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - await cache.addSplits([ - ['lol1', splitWithUserTT], - ['lol2', splitWithAccountTT] - ]); + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); let values = await cache.getAll(); @@ -27,33 +24,34 @@ describe('SPLITS CACHE REDIS', () => { let splitNames = await cache.getSplitNames(); - expect(splitNames.indexOf('lol1') !== -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(splitNames.length).toBe(2); + expect(splitNames.indexOf('user_ff') !== -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); - await cache.removeSplit('lol1'); + await cache.removeSplit('user_ff'); values = await cache.getAll(); expect(values).toEqual([splitWithAccountTT]); - expect(await cache.getSplit('lol1')).toEqual(null); - expect(await cache.getSplit('lol2')).toEqual(splitWithAccountTT); + expect(await cache.getSplit('user_ff')).toEqual(null); + expect(await cache.getSplit('account_ff')).toEqual(splitWithAccountTT); await cache.setChangeNumber(123); - expect(await cache.getChangeNumber() === 123).toBe(true); + expect(await cache.getChangeNumber()).toBe(123); splitNames = await cache.getSplitNames(); - expect(splitNames.indexOf('lol1') === -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(splitNames.indexOf('user_ff') === -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); - const splits = await cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1']).toEqual(null); - expect(splits['lol2']).toEqual(splitWithAccountTT); + const splits = await cache.getSplits(['user_ff', 'account_ff']); + expect(splits['user_ff']).toEqual(null); + expect(splits['account_ff']).toEqual(splitWithAccountTT); // Teardown. @TODO use cache clear method when implemented await connection.del(keysBuilder.buildTrafficTypeKey('account_tt')); - await connection.del(keysBuilder.buildSplitKey('lol2')); + await connection.del(keysBuilder.buildSplitKey('account_ff')); await connection.del(keysBuilder.buildSplitsTillKey()); await connection.disconnect(); }); @@ -62,13 +60,13 @@ describe('SPLITS CACHE REDIS', () => { const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - await cache.addSplits([ - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - await cache.addSplit('split4', splitWithUserTT); - await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect + await cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], -1); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); // trying to add the same definition for an already added split will not have effect expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(true); @@ -81,7 +79,8 @@ describe('SPLITS CACHE REDIS', () => { expect(await connection.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe('1'); - await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + await cache.removeSplit('split3'); + await cache.removeSplit('split2'); expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(false); @@ -93,10 +92,10 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.trafficTypeExists('user_tt')).toBe(false); expect(await cache.trafficTypeExists('account_tt')).toBe(false); - await cache.addSplit('split1', splitWithUserTT); + await cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(await cache.trafficTypeExists('user_tt')).toBe(true); - await cache.addSplit('split1', splitWithAccountTT); + await cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(await cache.trafficTypeExists('account_tt')).toBe(true); expect(await cache.trafficTypeExists('user_tt')).toBe(false); @@ -111,8 +110,7 @@ describe('SPLITS CACHE REDIS', () => { const connection = new RedisAdapter(loggerMock); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - await cache.addSplit('lol1', splitWithUserTT); - await cache.addSplit('lol2', splitWithAccountTT); + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); const initialChangeNumber = await cache.getChangeNumber(); // kill an non-existent split @@ -123,8 +121,8 @@ describe('SPLITS CACHE REDIS', () => { expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = await cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment', 100); + let lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed @@ -133,14 +131,15 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = await cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment_2', 90); + lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older // Delete splits and TT keys - await cache.removeSplits(['lol1', 'lol2']); + await cache.update([], [splitWithUserTT, splitWithAccountTT], -1); + await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); }); @@ -151,12 +150,12 @@ describe('SPLITS CACHE REDIS', () => { const emptySet = new Set([]); - await cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + await cache.addSplit(featureFlagWithEmptyFS); expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -164,13 +163,13 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(await cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(await cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(await cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(await cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -188,11 +187,12 @@ describe('SPLITS CACHE REDIS', () => { expect(await cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(await cache.getNamesByFlagSets([])).toEqual([]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + await cache.addSplit({ ...featureFlagWithoutFS, name: featureFlagWithEmptyFS.name }); expect(await cache.getNamesByFlagSets([])).toEqual([]); // Delete splits, TT and flag set keys - await cache.removeSplits([featureFlagThree.name, featureFlagTwo.name, featureFlagWithEmptyFS.name]); + await cache.update([], [featureFlagThree, featureFlagTwo, featureFlagWithEmptyFS], -1); + await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); }); @@ -204,12 +204,12 @@ describe('SPLITS CACHE REDIS', () => { const emptySet = new Set([]); - await cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree + ], [], -1); + await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(await cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -219,7 +219,8 @@ describe('SPLITS CACHE REDIS', () => { expect(await cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); // Delete splits, TT and flag set keys - await cacheWithoutFilters.removeSplits([featureFlagThree.name, featureFlagTwo.name, featureFlagOne.name, featureFlagWithEmptyFS.name]); + await cacheWithoutFilters.update([], [featureFlagThree, featureFlagTwo, featureFlagOne, featureFlagWithEmptyFS], -1); + await connection.del(keysBuilder.buildSplitsTillKey()); expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.disconnect(); }); diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index ddb06149..9b53f3a9 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -66,7 +66,8 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { * The returned promise is resolved when the operation success * or rejected if it fails (e.g., wrapper operation fails) */ - addSplit(name: string, split: ISplit): Promise { + addSplit(split: ISplit): Promise { + const name = split.name; const splitKey = this.keys.buildSplitKey(name); return this.wrapper.get(splitKey).then(splitFromStorage => { @@ -91,15 +92,6 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { }).then(() => true); } - /** - * Add a list of splits. - * The returned promise is resolved when the operation success - * or rejected if it fails (e.g., wrapper operation fails) - */ - addSplits(entries: [string, ISplit][]): Promise { - return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); - } - /** * Remove a given split. * The returned promise is resolved when the operation success, with a boolean indicating if the split existed or not. @@ -115,15 +107,6 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { }); } - /** - * Remove a list of splits. - * The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not. - * or rejected if it fails (e.g., wrapper operation fails). - */ - removeSplits(names: string[]): Promise { // @ts-ignore - return Promise.all(names.map(name => this.removeSplit(name))); - } - /** * Get split. * The returned promise is resolved with the split definition or null if it's not defined, diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index 57fc34b3..03d1ee6e 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -12,74 +12,59 @@ describe('SPLITS CACHE PLUGGABLE', () => { test('add/remove/get splits', async () => { const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); - // Assert addSplit and addSplits - await cache.addSplits([ - ['lol1', splitWithUserTT], - ['lol2', splitWithAccountTT] - ]); - await cache.addSplit('lol3', splitWithAccountTT); - - // Assert getAll + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); + let values = await cache.getAll(); - expect(values).toEqual([splitWithUserTT, splitWithAccountTT, splitWithAccountTT]); + expect(values).toEqual([splitWithUserTT, splitWithAccountTT]); // Assert getSplits - let valuesObj = await cache.getSplits(['lol2', 'lol3']); - - expect(Object.keys(valuesObj).length).toBe(2); - expect(valuesObj.lol2).toEqual(splitWithAccountTT); - expect(valuesObj.lol3).toEqual(splitWithAccountTT); + let valuesObj = await cache.getSplits([splitWithUserTT.name, splitWithAccountTT.name]); + expect(valuesObj).toEqual(values.reduce>((acc, split) => { + acc[split.name] = split; + return acc; + }, {})); // Assert getSplitNames let splitNames = await cache.getSplitNames(); - expect(splitNames.length).toBe(3); - expect(splitNames.indexOf('lol1') !== -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); - expect(splitNames.indexOf('lol3') !== -1).toBe(true); - - // Assert removeSplit - await cache.removeSplit('lol1'); + expect(splitNames.length).toBe(2); + expect(splitNames.indexOf('user_ff') !== -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); - values = await cache.getAll(); - expect(values.length).toBe(2); - expect(await cache.getSplit('lol1')).toEqual(null); - expect(await cache.getSplit('lol2')).toEqual(splitWithAccountTT); - - // Assert removeSplits - await cache.addSplit('lol1', splitWithUserTT); - await cache.removeSplits(['lol1', 'lol3']); + await cache.removeSplit('user_ff'); values = await cache.getAll(); - expect(values.length).toBe(1); - splitNames = await cache.getSplitNames(); - expect(splitNames.length).toBe(1); - expect(await cache.getSplit('lol1')).toEqual(null); - expect(await cache.getSplit('lol2')).toEqual(splitWithAccountTT); - }); + expect(values).toEqual([splitWithAccountTT]); - test('set/get change number', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); + expect(await cache.getSplit('user_ff')).toEqual(null); + expect(await cache.getSplit('account_ff')).toEqual(splitWithAccountTT); - expect(await cache.getChangeNumber()).toBe(-1); // if not set yet, changeNumber is -1 await cache.setChangeNumber(123); expect(await cache.getChangeNumber()).toBe(123); + splitNames = await cache.getSplitNames(); + + expect(splitNames.indexOf('user_ff') === -1).toBe(true); + expect(splitNames.indexOf('account_ff') !== -1).toBe(true); + + const splits = await cache.getSplits(['user_ff', 'account_ff']); + expect(splits['user_ff']).toEqual(null); + expect(splits['account_ff']).toEqual(splitWithAccountTT); }); test('trafficTypeExists', async () => { const wrapper = wrapperMockFactory(); const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper); - await cache.addSplits([ - ['split1', splitWithUserTT], - ['split2', splitWithAccountTT], - ['split3', splitWithUserTT], - ]); - await cache.addSplit('split4', splitWithUserTT); - await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect + await cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], -1); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); + await cache.addSplit({ ...splitWithUserTT, name: 'split4' }); // trying to add the same definition for an already added split will not have effect expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(true); @@ -92,7 +77,8 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await wrapper.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe('1'); - await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + await cache.removeSplit('split3'); + await cache.removeSplit('split2'); expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(false); @@ -104,21 +90,19 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.trafficTypeExists('user_tt')).toBe(false); expect(await cache.trafficTypeExists('account_tt')).toBe(false); - await cache.addSplit('split1', splitWithUserTT); + await cache.addSplit({ ...splitWithUserTT, name: 'split1' }); expect(await cache.trafficTypeExists('user_tt')).toBe(true); - await cache.addSplit('split1', splitWithAccountTT); + await cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); expect(await cache.trafficTypeExists('account_tt')).toBe(true); expect(await cache.trafficTypeExists('user_tt')).toBe(false); - }); test('killLocally', async () => { const wrapper = wrapperMockFactory(); const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper); - await cache.addSplit('lol1', splitWithUserTT); - await cache.addSplit('lol2', splitWithAccountTT); + await cache.update([splitWithUserTT, splitWithAccountTT], [], -1); const initialChangeNumber = await cache.getChangeNumber(); // kill an non-existent split @@ -129,8 +113,8 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split - updated = await cache.killLocally('lol1', 'some_treatment', 100); - let lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment', 100); + let lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed @@ -139,14 +123,15 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old - updated = await cache.killLocally('lol1', 'some_treatment_2', 90); - lol1Split = await cache.getSplit('lol1') as ISplit; + updated = await cache.killLocally('user_ff', 'some_treatment_2', 90); + lol1Split = await cache.getSplit('user_ff') as ISplit; expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older // Delete splits and TT keys - await cache.removeSplits(['lol1', 'lol2']); + await cache.update([], [splitWithUserTT, splitWithAccountTT], -1); + await wrapper.del(keysBuilder.buildSplitsTillKey()); expect(await wrapper.getKeysByPrefix('SPLITIO')).toHaveLength(0); }); @@ -155,12 +140,12 @@ describe('SPLITS CACHE PLUGGABLE', () => { const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper, { groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new Set([]); - await cache.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + await cache.addSplit(featureFlagWithEmptyFS); expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); @@ -168,13 +153,13 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter expect(await cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['1'] }); expect(await cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter expect(await cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); expect(await cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - await cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] }); + await cache.addSplit({ ...featureFlagOne, sets: ['x'] }); expect(await cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); expect(await cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); @@ -189,7 +174,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter expect(await cache.getNamesByFlagSets([])).toEqual([]); - await cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + await cache.addSplit({ ...featureFlagWithoutFS, name: featureFlagWithEmptyFS.name }); expect(await cache.getNamesByFlagSets([])).toEqual([]); }); @@ -198,12 +183,12 @@ describe('SPLITS CACHE PLUGGABLE', () => { const cacheWithoutFilters = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); const emptySet = new Set([]); - await cacheWithoutFilters.addSplits([ - [featureFlagOne.name, featureFlagOne], - [featureFlagTwo.name, featureFlagTwo], - [featureFlagThree.name, featureFlagThree], - ]); - await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + await cacheWithoutFilters.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree + ], [], -1); + await cacheWithoutFilters.addSplit(featureFlagWithEmptyFS); expect(await cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(await cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); diff --git a/src/storages/types.ts b/src/storages/types.ts index 638c4606..6378f229 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -177,11 +177,9 @@ export interface IPluggableStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - addSplits(entries: [string, ISplit][]): MaybeThenable, - removeSplits(names: string[]): MaybeThenable, + update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec - setChangeNumber(changeNumber: number): MaybeThenable, // should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage. getChangeNumber(): MaybeThenable, getAll(): MaybeThenable, @@ -198,11 +196,9 @@ export interface ISplitsCacheBase { } export interface ISplitsCacheSync extends ISplitsCacheBase { - addSplits(entries: [string, ISplit][]): boolean[], - removeSplits(names: string[]): boolean[], + update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): boolean, getSplit(name: string): ISplit | null, getSplits(names: string[]): Record, - setChangeNumber(changeNumber: number): boolean | void, getChangeNumber(): number, getAll(): ISplit[], getSplitNames(): string[], @@ -215,11 +211,9 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { } export interface ISplitsCacheAsync extends ISplitsCacheBase { - addSplits(entries: [string, ISplit][]): Promise, - removeSplits(names: string[]): Promise, + update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, - setChangeNumber(changeNumber: number): Promise, getChangeNumber(): Promise, getAll(): Promise, getSplitNames(): Promise, diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 84805110..14daccf2 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -24,7 +24,7 @@ export function fromObjectUpdaterFactory( let startingUp = true; return function objectUpdater() { - const splits: [string, ISplit][] = []; + const splits: ISplit[] = []; let loadError = null; let splitsMock: false | Record = {}; try { @@ -37,24 +37,23 @@ export function fromObjectUpdaterFactory( if (!loadError && splitsMock) { log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(splitsMock)]); - forOwn(splitsMock, function (val, name) { - splits.push([ // @ts-ignore Split changeNumber and seed is undefined in localhost mode - name, { - name, - status: 'ACTIVE', - killed: false, - trafficAllocation: 100, - defaultTreatment: CONTROL, - conditions: val.conditions || [], - configurations: val.configurations, - trafficTypeName: val.trafficTypeName - } - ]); + forOwn(splitsMock, (val, name) => { + // @ts-ignore Split changeNumber and seed is undefined in localhost mode + splits.push({ + name, + status: 'ACTIVE', + killed: false, + trafficAllocation: 100, + defaultTreatment: CONTROL, + conditions: val.conditions || [], + configurations: val.configurations, + trafficTypeName: val.trafficTypeName + }); }); return Promise.all([ splitsCache.clear(), // required to sync removed splits from mock - splitsCache.addSplits(splits) + splitsCache.update(splits, [], Date.now()) ]).then(() => { readiness.splits.emit(SDK_SPLITS_ARRIVED); diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index b4dca3fe..dc0f0ec3 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -96,59 +96,59 @@ test('splitChangesUpdater / compute splits mutation', () => { let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[activeSplitWithSegments.name, activeSplitWithSegments]]); - expect(splitsMutation.removed).toEqual([archivedSplit.name]); + expect(splitsMutation.added).toEqual([activeSplitWithSegments]); + expect(splitsMutation.removed).toEqual([archivedSplit]); expect(splitsMutation.segments).toEqual(['A', 'B']); // SDK initialization without sets // should process all the notifications splitsMutation = computeSplitsMutation([testFFSetsAB, test2FFSetsX] as ISplit[], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[testFFSetsAB.name, testFFSetsAB],[test2FFSetsX.name, test2FFSetsX]]); + expect(splitsMutation.added).toEqual([testFFSetsAB, test2FFSetsX]); expect(splitsMutation.removed).toEqual([]); expect(splitsMutation.segments).toEqual([]); }); test('splitChangesUpdater / compute splits mutation with filters', () => { // SDK initialization with sets: [set_a, set_b] - let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; + let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a', 'set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; // fetching new feature flag in sets A & B let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); // should add it to mutations - expect(splitsMutation.added).toEqual([[testFFSetsAB.name, testFFSetsAB]]); + expect(splitsMutation.added).toEqual([testFFSetsAB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B splitsMutation = computeSplitsMutation([testFFRemoveSetB], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[testFFRemoveSetB.name, testFFRemoveSetB]]); + expect(splitsMutation.added).toEqual([testFFRemoveSetB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B splitsMutation = computeSplitsMutation([testFFRemoveSetA], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFRemoveSetA.name]); + expect(splitsMutation.removed).toEqual([testFFRemoveSetA]); // fetching existing test feature flag removed from set B splitsMutation = computeSplitsMutation([testFFEmptySet], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); + expect(splitsMutation.removed).toEqual([testFFEmptySet]); // SDK initialization with names: ['test2'] splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); - expect(splitsMutation.removed).toEqual([testFFSetsAB.name]); + expect(splitsMutation.removed).toEqual([testFFSetsAB]); splitsMutation = computeSplitsMutation([test2FFSetsX, testFFEmptySet], splitFiltersValidation); - expect(splitsMutation.added).toEqual([[test2FFSetsX.name, test2FFSetsX],]); - expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); + expect(splitsMutation.added).toEqual([test2FFSetsX]); + expect(splitsMutation.removed).toEqual([testFFEmptySet]); }); describe('splitChangesUpdater', () => { @@ -159,9 +159,7 @@ describe('splitChangesUpdater', () => { const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); const splitsCache = new SplitsCacheInMemory(); - const setChangeNumber = jest.spyOn(splitsCache, 'setChangeNumber'); - const addSplits = jest.spyOn(splitsCache, 'addSplits'); - const removeSplits = jest.spyOn(splitsCache, 'removeSplits'); + const updateSplits = jest.spyOn(splitsCache, 'update'); const segmentsCache = new SegmentsCacheInMemory(); const registerSegments = jest.spyOn(segmentsCache, 'registerSegments'); @@ -178,12 +176,8 @@ describe('splitChangesUpdater', () => { test('test without payload', async () => { const result = await splitChangesUpdater(); - expect(setChangeNumber).toBeCalledTimes(1); - expect(setChangeNumber).lastCalledWith(splitChangesMock1.till); - expect(addSplits).toBeCalledTimes(1); - expect(addSplits.mock.calls[0][0].length).toBe(splitChangesMock1.splits.length); - expect(removeSplits).toBeCalledTimes(1); - expect(removeSplits).lastCalledWith([]); + expect(updateSplits).toBeCalledTimes(1); + expect(updateSplits).lastCalledWith(splitChangesMock1.splits, [], splitChangesMock1.till); expect(registerSegments).toBeCalledTimes(1); expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); expect(result).toBe(true); @@ -195,18 +189,16 @@ describe('splitChangesUpdater', () => { const payload = notification.decoded as Pick; const changeNumber = payload.changeNumber; - await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets:[]}, changeNumber: changeNumber })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); // fetch not being called expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(index + 1); // Change number being updated - expect(setChangeNumber).toBeCalledTimes(index + 1); - expect(setChangeNumber.mock.calls[index][0]).toEqual(changeNumber); + expect(updateSplits.mock.calls[index][2]).toEqual(changeNumber); // Add feature flag in notification - expect(addSplits).toBeCalledTimes(index + 1); - expect(addSplits.mock.calls[index][0].length).toBe(payload.status === ARCHIVED_FF ? 0 : 1); + expect(updateSplits.mock.calls[index][0].length).toBe(payload.status === ARCHIVED_FF ? 0 : 1); // Remove feature flag if status is ARCHIVED - expect(removeSplits).toBeCalledTimes(index + 1); - expect(removeSplits.mock.calls[index][0]).toEqual(payload.status === ARCHIVED_FF ? [payload.name] : []); + expect(updateSplits.mock.calls[index][1]).toEqual(payload.status === ARCHIVED_FF ? [payload] : []); // fetch segments after feature flag update expect(registerSegments).toBeCalledTimes(index + 1); expect(registerSegments.mock.calls[index][0]).toEqual(payload.status === ARCHIVED_FF ? [] : ['maur-2']); @@ -231,7 +223,7 @@ describe('splitChangesUpdater', () => { let calls = 0; // emit always if not configured sets for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets: setMock.sets, status: 'ACTIVE'}, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); expect(splitsEmitSpy.mock.calls[index][0]).toBe('state::splits-arrived'); index++; } @@ -242,7 +234,7 @@ describe('splitChangesUpdater', () => { splitsEmitSpy.mockReset(); index = 0; for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets: setMock.sets, status: 'ACTIVE'}, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); if (setMock.shouldEmit) calls++; expect(splitsEmitSpy.mock.calls.length).toBe(calls); index++; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index e8153987..f534c6c7 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -5,7 +5,7 @@ import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; -import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; +import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; import { IN_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; @@ -42,8 +42,8 @@ export function parseSegments({ conditions }: ISplit): Set { } interface ISplitMutations { - added: [string, ISplit][], - removed: string[], + added: ISplit[], + removed: ISplit[], segments: string[] } @@ -77,13 +77,13 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV const segments = new Set(); const computed = entries.reduce((accum, split) => { if (split.status === 'ACTIVE' && matchFilters(split, filters)) { - accum.added.push([split.name, split]); + accum.added.push(split); parseSegments(split).forEach((segmentName: string) => { segments.add(segmentName); }); } else { - accum.removed.push(split.name); + accum.removed.push(split); } return accum; @@ -128,16 +128,6 @@ export function splitChangesUpdaterFactory( return promise; } - /** Returns true if at least one split was updated */ - function isThereUpdate(flagsChange: [boolean | void, void | boolean[], void | boolean[], boolean | void] | [any, any, any]) { - const [, added, removed] = flagsChange; - // There is at least one added or modified feature flag - if (added && added.some((update: boolean) => update)) return true; - // There is at least one removed feature flag - if (removed && removed.some((update: boolean) => update)) return true; - return false; - } - /** * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. * Returned promise will not be rejected. @@ -162,22 +152,17 @@ export function splitChangesUpdaterFactory( const mutation = computeSplitsMutation(splitChanges.splits, splitFiltersValidation); - log.debug(SYNC_SPLITS_NEW, [mutation.added.length]); - log.debug(SYNC_SPLITS_REMOVED, [mutation.removed.length]); - log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); + log.debug(SYNC_SPLITS_UPDATE, [mutation.added.length, mutation.removed.length, mutation.segments.length]); // Write into storage // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency return Promise.all([ - // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed - splits.setChangeNumber(splitChanges.till), - splits.addSplits(mutation.added), - splits.removeSplits(mutation.removed), + splits.update(mutation.added, mutation.removed, splitChanges.till), segments.registerSegments(mutation.segments) - ]).then((flagsChange) => { + ]).then(([isThereUpdate]) => { if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate(flagsChange) && (isClientSide || checkAllSegmentsExist(segments)))) + return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate && (isClientSide || checkAllSegmentsExist(segments)))) .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index d5fd3acd..4de69ca0 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -169,30 +169,30 @@ describe('SplitsUpdateWorker', () => { test('killSplit', async () => { // setup const cache = new SplitsCacheInMemory(); - cache.addSplit('lol1', '{ "name": "something"}'); - cache.addSplit('lol2', '{ "name": "something else"}'); + cache.addSplit({ name: 'something'}); + cache.addSplit({ name: 'something else'}); const splitsSyncTask = splitsSyncTaskMock(cache); const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); // assert killing split locally, emitting SDK_SPLITS_ARRIVED event, and synchronizing splits if changeNumber is new - splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'lol1', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous + splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'something', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous expect(splitsSyncTask.execute).toBeCalledTimes(1); // synchronizes splits if `isExecuting` is false expect(splitsEventEmitterMock.emit.mock.calls).toEqual([[SDK_SPLITS_ARRIVED, true]]); // emits `SDK_SPLITS_ARRIVED` with `isSplitKill` flag in true, if split kill resolves with update - assertKilledSplit(cache, 100, 'lol1', 'off'); + assertKilledSplit(cache, 100, 'something', 'off'); // assert not killing split locally, not emitting SDK_SPLITS_ARRIVED event, and not synchronizes splits, if changeNumber is old splitsSyncTask.__resolveSplitsUpdaterCall(100); await new Promise(res => setTimeout(res)); splitsSyncTask.execute.mockClear(); splitsEventEmitterMock.emit.mockClear(); - splitUpdateWorker.killSplit({ changeNumber: 90, splitName: 'lol1', defaultTreatment: 'on' }); + splitUpdateWorker.killSplit({ changeNumber: 90, splitName: 'something', defaultTreatment: 'on' }); await new Promise(res => setTimeout(res)); expect(splitsSyncTask.execute).toBeCalledTimes(0); // doesn't synchronize splits if killLocally resolved without update expect(splitsEventEmitterMock.emit).toBeCalledTimes(0); // doesn't emit `SDK_SPLITS_ARRIVED` if killLocally resolved without update - assertKilledSplit(cache, 100, 'lol1', 'off'); // calling `killLocally` with an old changeNumber made no effect + assertKilledSplit(cache, 100, 'something', 'off'); // calling `killLocally` with an old changeNumber made no effect }); test('stop', async () => { From 02c10e91ce06d7fd8ab131b849a25019c3424b13 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Feb 2025 14:40:17 -0300 Subject: [PATCH 26/44] Polishing --- src/storages/AbstractSplitsCacheAsync.ts | 8 ++++---- src/storages/AbstractSplitsCacheSync.ts | 8 ++++---- src/storages/types.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index bafc8e2c..a43a57f2 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -8,15 +8,15 @@ import { objectAssign } from '../utils/lang/objectAssign'; */ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { - protected abstract setChangeNumber(changeNumber: number): Promise protected abstract addSplit(split: ISplit): Promise protected abstract removeSplit(name: string): Promise + protected abstract setChangeNumber(changeNumber: number): Promise - update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): Promise { + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): Promise { return Promise.all([ this.setChangeNumber(changeNumber), - Promise.all(addedFFs.map(addedFF => this.addSplit(addedFF))), - Promise.all(removedFFs.map(removedFF => this.removeSplit(removedFF.name))) + Promise.all(toAdd.map(addedFF => this.addSplit(addedFF))), + Promise.all(toRemove.map(removedFF => this.removeSplit(removedFF.name))) ]).then(([, added, removed]) => { return added.some(result => result) || removed.some(result => result); }); diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index d479305f..512d990e 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -9,14 +9,14 @@ import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; */ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { - protected abstract setChangeNumber(changeNumber: number): boolean | void protected abstract addSplit(split: ISplit): boolean protected abstract removeSplit(name: string): boolean + protected abstract setChangeNumber(changeNumber: number): boolean | void - update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): boolean { + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean { this.setChangeNumber(changeNumber); - const updated = addedFFs.map(addedFF => this.addSplit(addedFF)).some(result => result); - return removedFFs.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; + const updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result); + return toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; } abstract getSplit(name: string): ISplit | null diff --git a/src/storages/types.ts b/src/storages/types.ts index 6378f229..f100775c 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -177,7 +177,7 @@ export interface IPluggableStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): MaybeThenable, + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec // should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage. @@ -196,7 +196,7 @@ export interface ISplitsCacheBase { } export interface ISplitsCacheSync extends ISplitsCacheBase { - update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): boolean, + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean, getSplit(name: string): ISplit | null, getSplits(names: string[]): Record, getChangeNumber(): number, @@ -211,7 +211,7 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { } export interface ISplitsCacheAsync extends ISplitsCacheBase { - update(addedFFs: ISplit[], removedFFs: ISplit[], changeNumber: number): Promise, + update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, getChangeNumber(): Promise, From bcc770e40ed98ceaa205099e279df593e08bcb65 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 25 Feb 2025 20:37:18 -0300 Subject: [PATCH 27/44] Replace deprecated String.substr() with String.slice() --- src/storages/KeyBuilderCS.ts | 5 ++--- src/storages/types.ts | 14 ++++++------- src/sync/polling/syncTasks/splitsSyncTask.ts | 3 +-- .../__tests__/splitChangesUpdater.spec.ts | 20 +++++++++++-------- .../polling/updaters/splitChangesUpdater.ts | 6 +++--- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index a59d7208..b3639c33 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -28,8 +28,7 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { extractSegmentName(builtSegmentKeyName: string) { const prefix = `${this.prefix}.${this.matchingKey}.segment.`; - if (startsWith(builtSegmentKeyName, prefix)) - return builtSegmentKeyName.substr(prefix.length); + if (startsWith(builtSegmentKeyName, prefix)) return builtSegmentKeyName.slice(prefix.length); } buildLastUpdatedKey() { @@ -54,7 +53,7 @@ export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): extractSegmentName(builtSegmentKeyName: string) { const p = `${prefix}.${matchingKey}.largeSegment.`; - if (startsWith(builtSegmentKeyName, p)) return builtSegmentKeyName.substr(p.length); + if (startsWith(builtSegmentKeyName, p)) return builtSegmentKeyName.slice(p.length); }, buildTillKey() { diff --git a/src/storages/types.ts b/src/storages/types.ts index f100775c..71962254 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -422,13 +422,13 @@ export interface ITelemetryCacheAsync extends ITelemetryEvaluationProducerAsync, */ export interface IStorageBase< - TSplitsCache extends ISplitsCacheBase, - TSegmentsCache extends ISegmentsCacheBase, - TImpressionsCache extends IImpressionsCacheBase, - TImpressionsCountCache extends IImpressionCountsCacheBase, - TEventsCache extends IEventsCacheBase, - TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync, - TUniqueKeysCache extends IUniqueKeysCacheBase + TSplitsCache extends ISplitsCacheBase = ISplitsCacheBase, + TSegmentsCache extends ISegmentsCacheBase = ISegmentsCacheBase, + TImpressionsCache extends IImpressionsCacheBase = IImpressionsCacheBase, + TImpressionsCountCache extends IImpressionCountsCacheBase = IImpressionCountsCacheBase, + TEventsCache extends IEventsCacheBase = IEventsCacheBase, + TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync = ITelemetryCacheSync | ITelemetryCacheAsync, + TUniqueKeysCache extends IUniqueKeysCacheBase = IUniqueKeysCacheBase > { splits: TSplitsCache, segments: TSegmentsCache, diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index baef383c..d6fed5a2 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -22,8 +22,7 @@ export function splitsSyncTaskFactory( splitChangesUpdaterFactory( settings.log, splitChangesFetcherFactory(fetchSplitChanges), - storage.splits, - storage.segments, + storage, settings.sync.__splitFiltersValidation, readiness.splits, settings.startup.requestTimeoutBeforeReady, diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index dc0f0ec3..d59c7013 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -158,17 +158,20 @@ describe('splitChangesUpdater', () => { const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); - const splitsCache = new SplitsCacheInMemory(); - const updateSplits = jest.spyOn(splitsCache, 'update'); + const splits = new SplitsCacheInMemory(); + const updateSplits = jest.spyOn(splits, 'update'); + + const segments = new SegmentsCacheInMemory(); + const registerSegments = jest.spyOn(segments, 'registerSegments'); + + const storage = { splits, segments }; - const segmentsCache = new SegmentsCacheInMemory(); - const registerSegments = jest.spyOn(segmentsCache, 'registerSegments'); const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); let splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - let splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); + let splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1); afterEach(() => { jest.clearAllMocks(); @@ -206,7 +209,7 @@ describe('splitChangesUpdater', () => { } }); - test('flag sets splits-arrived emition', async () => { + test('flag sets splits-arrived emission', async () => { const payload = splitNotifications[3].decoded as Pick; const setMocks = [ { sets: [], shouldEmit: false }, /* should not emit if flag does not have any set */ @@ -217,7 +220,7 @@ describe('splitChangesUpdater', () => { { sets: ['set_a'], shouldEmit: true }, /* should emit if flag is back in configured sets */ ]; - splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, new SplitsCacheInMemory(), segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); let index = 0; let calls = 0; @@ -230,7 +233,8 @@ describe('splitChangesUpdater', () => { // @ts-ignore splitFiltersValidation = { queryString: null, groupedFilters: { bySet: ['set_a'], byName: [], byPrefix: [] }, validFilters: [] }; - splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, new SplitsCacheInMemory(), segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + storage.splits.clear(); + splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.splits, 1000, 1, true); splitsEmitSpy.mockReset(); index = 0; for (const setMock of setMocks) { diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index f534c6c7..065ecb89 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -1,4 +1,4 @@ -import { ISegmentsCacheBase, ISplitsCacheBase } from '../../../storages/types'; +import { ISegmentsCacheBase, IStorageBase } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; @@ -111,14 +111,14 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - splits: ISplitsCacheBase, - segments: ISegmentsCacheBase, + storage: Pick, splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, isClientSide?: boolean ): ISplitChangesUpdater { + const { splits, segments } = storage; let startingUp = true; From be494754cfb12976b786fa6994fac195cee89fa6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 25 Feb 2025 23:12:55 -0300 Subject: [PATCH 28/44] Move methods from KeyBuilder to KeyBuilderCS --- src/storages/KeyBuilder.ts | 20 -------------------- src/storages/KeyBuilderCS.ts | 8 ++++++++ src/storages/__tests__/KeyBuilder.spec.ts | 16 +++------------- 3 files changed, 11 insertions(+), 33 deletions(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index e70b251b..2f5dc800 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -1,5 +1,4 @@ import { ISettings } from '../types'; -import { startsWith } from '../utils/lang'; import { hash } from '../utils/murmur3/murmur3'; const everythingAtTheEnd = /[^.]+$/; @@ -34,24 +33,10 @@ export class KeyBuilder { return `${this.prefix}.splits.till`; } - // NOT USED - // buildSplitsReady() { - // return `${this.prefix}.splits.ready`; - // } - - isSplitKey(key: string) { - return startsWith(key, `${this.prefix}.split.`); - } - buildSplitKeyPrefix() { return `${this.prefix}.split.`; } - // Only used by InLocalStorage. - buildSplitsWithSegmentCountKey() { - return `${this.prefix}.splits.usingSegments`; - } - buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } @@ -60,11 +45,6 @@ export class KeyBuilder { return `${this.prefix}.segment.${segmentName}.till`; } - // NOT USED - // buildSegmentsReady() { - // return `${this.prefix}.segments.ready`; - // } - extractKey(builtKey: string) { const s = builtKey.match(everythingAtTheEnd); diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index b3639c33..23961f89 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -42,6 +42,14 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { buildTillKey() { return `${this.prefix}.${this.matchingKey}.segments.till`; } + + isSplitKey(key: string) { + return startsWith(key, `${this.prefix}.split.`); + } + + buildSplitsWithSegmentCountKey() { + return `${this.prefix}.splits.usingSegments`; + } } export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): MySegmentsKeyBuilder { diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index e0494ec9..45af194c 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -9,14 +9,9 @@ test('KEYS / splits keys', () => { const expectedKey = `SPLITIO.split.${splitName}`; const expectedTill = 'SPLITIO.splits.till'; - expect(builder.isSplitKey(expectedKey)).toBe(true); - expect(builder.buildSplitKey(splitName) === expectedKey).toBe(true); - expect(builder.buildSplitsTillKey() === expectedTill).toBe(true); - expect(builder.extractKey(builder.buildSplitKey(splitName)) === splitName).toBe(true); - - // NOT USED - // const expectedReady = 'SPLITIO.splits.ready'; - // expect(builder.buildSplitsReady() === expectedReady).toBe(true); + expect(builder.buildSplitKey(splitName)).toBe(expectedKey); + expect(builder.buildSplitsTillKey()).toBe(expectedTill); + expect(builder.extractKey(builder.buildSplitKey(splitName))).toBe(splitName); }); test('KEYS / splits keys with custom prefix', () => { @@ -27,13 +22,8 @@ test('KEYS / splits keys with custom prefix', () => { const expectedKey = `${prefix}.split.${splitName}`; const expectedTill = `${prefix}.splits.till`; - expect(builder.isSplitKey(expectedKey)).toBe(true); expect(builder.buildSplitKey(splitName)).toBe(expectedKey); expect(builder.buildSplitsTillKey() === expectedTill).toBe(true); - - // NOT USED - // const expectedReady = `${prefix}.SPLITIO.splits.ready`; - // expect(builder.buildSplitsReady() === expectedReady).toBe(true); }); const prefix = 'SPLITIO'; From 8377736e3af16787180f1a7114151beeb6bf31e2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 Feb 2025 13:43:20 -0300 Subject: [PATCH 29/44] Polishing in splits cache --- .../inLocalStorage/SplitsCacheInLocal.ts | 4 +- src/storages/inMemory/SplitsCacheInMemory.ts | 50 ++++++++----------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 1562cd08..61988139 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -121,6 +121,8 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { removeSplit(name: string): boolean { try { const split = this.getSplit(name); + if (!split) return false; + localStorage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); @@ -133,7 +135,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - getSplit(name: string) { + getSplit(name: string): ISplit | null { const item = localStorage.getItem(this.keys.buildSplitKey(name)); return item && JSON.parse(item); } diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 347ee1e1..a8be688a 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -41,41 +41,35 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { if (usesSegments(previousSplit)) this.segmentsCount--; } - if (split) { - // Store the Split. - this.splitsCache[name] = split; - // Update TT cache - const ttName = split.trafficTypeName; - this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1; - this.addToFlagSets(split); - - // Add to segments count for the new version of the Split - if (usesSegments(split)) this.segmentsCount++; - - return true; - } else { - return false; - } + // Store the Split. + this.splitsCache[name] = split; + // Update TT cache + const ttName = split.trafficTypeName; + this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1; + this.addToFlagSets(split); + + // Add to segments count for the new version of the Split + if (usesSegments(split)) this.segmentsCount++; + + return true; } removeSplit(name: string): boolean { const split = this.getSplit(name); - if (split) { - // Delete the Split - delete this.splitsCache[name]; + if (!split) return false; - const ttName = split.trafficTypeName; - this.ttCache[ttName]--; // Update tt cache - if (!this.ttCache[ttName]) delete this.ttCache[ttName]; - this.removeFromFlagSets(split.name, split.sets); + // Delete the Split + delete this.splitsCache[name]; - // Update the segments count. - if (usesSegments(split)) this.segmentsCount--; + const ttName = split.trafficTypeName; + this.ttCache[ttName]--; // Update tt cache + if (!this.ttCache[ttName]) delete this.ttCache[ttName]; + this.removeFromFlagSets(split.name, split.sets); - return true; - } else { - return false; - } + // Update the segments count. + if (usesSegments(split)) this.segmentsCount--; + + return true; } getSplit(name: string): ISplit | null { From fad235bd505064d992c059a2e00e90d0203d0a81 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Mar 2025 16:38:53 -0300 Subject: [PATCH 30/44] Add impression properties support --- src/logger/messages/error.ts | 2 +- src/sdkClient/client.ts | 24 +++---- src/sdkClient/clientInputValidation.ts | 29 ++++---- src/sdkFactory/index.ts | 9 ++- src/sync/submitters/impressionsSubmitter.ts | 5 +- src/sync/submitters/types.ts | 2 + .../__tests__/impressionsTracker.spec.ts | 16 +++-- src/trackers/impressionsTracker.ts | 9 ++- src/trackers/types.ts | 2 +- src/utils/inputValidation/eventProperties.ts | 8 +++ src/utils/inputValidation/index.ts | 1 + types/splitio.d.ts | 71 ++++++++++--------- 12 files changed, 103 insertions(+), 75 deletions(-) diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 2c0b0c63..123f8eee 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -21,7 +21,7 @@ export const codesError: [number, string][] = [ // input validation [c.ERROR_EVENT_TYPE_FORMAT, '%s: you passed "%s", event_type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.'], [c.ERROR_NOT_PLAIN_OBJECT, '%s: %s must be a plain object.'], - [c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded. Event not queued.'], + [c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded.'], [c.ERROR_NOT_FINITE, '%s: value must be a finite number.'], [c.ERROR_NULL, '%s: you passed a null or undefined %s. It must be a non-empty string.'], [c.ERROR_TOO_LONG, '%s: %s too long. It must have 250 characters or less.'], diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 1139a272..e61a393e 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -56,7 +56,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return getTreatment(key, featureFlagName, attributes, true, GET_TREATMENT_WITH_CONFIG); } - function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, methodName = GET_TREATMENTS) { + function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) { const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS); const wrapUp = (evaluationResults: Record) => { @@ -65,7 +65,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl Object.keys(evaluationResults).forEach(featureFlagName => { treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue); }); - impressionsTracker.track(queue, attributes); + impressionsTracker.track(queue, attributes, options); stopTelemetryTracker(queue[0] && queue[0].imp.label); return treatments; @@ -80,11 +80,11 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } - function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined) { - return getTreatments(key, featureFlagNames, attributes, true, GET_TREATMENTS_WITH_CONFIG); + function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG); } - function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { + function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, options: SplitIO.EvaluationOptions | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { const stopTelemetryTracker = telemetryTracker.trackEval(method); const wrapUp = (evaluationResults: Record) => { @@ -94,7 +94,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl Object.keys(evaluations).forEach(featureFlagName => { treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue); }); - impressionsTracker.track(queue, attributes); + impressionsTracker.track(queue, attributes, options); stopTelemetryTracker(queue[0] && queue[0].imp.label); return treatments; @@ -109,16 +109,16 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } - function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, flagSetNames, attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); + function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatmentsByFlagSets(key, flagSetNames, attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); } - function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, [flagSetName], attributes, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET); + function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET); } - function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, [flagSetName], attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET); + function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET); } // Internal function diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index e7fb9db1..38207656 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -9,7 +9,8 @@ import { validateSplits, validateTrafficType, validateIfNotDestroyed, - validateIfOperational + validateIfOperational, + validateEvaluationOptions } from '../utils/inputValidation'; import { startsWith } from '../utils/lang'; import { CONTROL, CONTROL_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENT_WITH_CONFIG, TRACK_FN_LABEL } from '../utils/constants'; @@ -32,7 +33,7 @@ export function clientInputValidationDecorator -1 ? @@ -43,6 +44,7 @@ export function clientInputValidationDecorator res[split] = CONTROL); @@ -94,7 +97,7 @@ export function clientInputValidationDecorator false) }; +const fakeDebugStrategy = { + process: jest.fn(() => false) +}; + /* Tests */ describe('Impressions Tracker', () => { @@ -53,7 +57,7 @@ describe('Impressions Tracker', () => { const strategy = strategyDebugFactory(impressionObserverCSFactory()); test('Should be able to track impressions (in DEBUG mode without Previous Time).', () => { - const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit); + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategy, fakeWhenInit); const imp1 = { feature: '10', @@ -73,7 +77,7 @@ describe('Impressions Tracker', () => { }); test('Tracked impressions should be sent to impression listener and integration manager when we invoke .track()', (done) => { - const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit, fakeIntegrationsManager); + const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategy, fakeWhenInit, fakeIntegrationsManager); const fakeImpression = { feature: 'impression' @@ -147,8 +151,8 @@ describe('Impressions Tracker', () => { impression3.time = 1234567891; const trackers = [ - impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined), - impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined) + impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined), + impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined) ]; expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked. @@ -175,7 +179,7 @@ describe('Impressions Tracker', () => { impression3.time = Date.now(); const impressionCountsCache = new ImpressionCountsCacheInMemory(); - const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache), fakeWhenInit, undefined, fakeTelemetryCache as any); + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache), fakeWhenInit, undefined, fakeTelemetryCache as any); expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker @@ -198,7 +202,7 @@ describe('Impressions Tracker', () => { test('Should track or not impressions depending on user consent status', () => { const settings = { ...fullSettings }; - const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit); + const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategy, fakeWhenInit); tracker.track([{ imp: impression }]); expect(fakeImpressionsCache.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index 0d64a8ed..acf5fa3b 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -14,7 +14,8 @@ export function impressionsTrackerFactory( settings: ISettings, impressionsCache: IImpressionsCacheBase, noneStrategy: IStrategy, - strategy: IStrategy, + debugStrategy: IStrategy, + defaultStrategy: IStrategy, whenInit: (cb: () => void) => void, integrationsManager?: IImpressionsHandler, telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync, @@ -23,13 +24,15 @@ export function impressionsTrackerFactory( const { log, impressionListener, runtime: { ip, hostname }, version } = settings; return { - track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes) { + track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { if (settings.userConsent === CONSENT_DECLINED) return; const impressionsToStore = impressions.filter(({ imp, disabled }) => { return disabled ? noneStrategy.process(imp) : - strategy.process(imp); + options && options.properties ? + (imp.properties = options.properties) && debugStrategy.process(imp) : + defaultStrategy.process(imp); }); const impressionsLength = impressions.length; diff --git a/src/trackers/types.ts b/src/trackers/types.ts index a0dd2dd4..7ac928fb 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -29,7 +29,7 @@ export type ImpressionDecorated = { }; export interface IImpressionsTracker { - track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes): void + track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions): void } /** Telemetry tracker */ diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index 310946cc..4a0f4e5c 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -66,3 +66,11 @@ export function validateEventProperties(log: ILogger, maybeProperties: any, meth return output; } + +export function validateEvaluationOptions(log: ILogger, maybeOptions: any, method: string): SplitIO.EvaluationOptions | undefined { + if (isObject(maybeOptions)) { + const properties = validateEventProperties(log, maybeOptions.properties, method).properties; + return properties ? { properties } : undefined; + } + return undefined; +} diff --git a/src/utils/inputValidation/index.ts b/src/utils/inputValidation/index.ts index 93b09ab7..96cf4be6 100644 --- a/src/utils/inputValidation/index.ts +++ b/src/utils/inputValidation/index.ts @@ -11,3 +11,4 @@ export { validateIfNotDestroyed, validateIfOperational } from './isOperational'; export { validateSplitExistence } from './splitExistence'; export { validateTrafficTypeExistence } from './trafficTypeExistence'; export { validatePreloadedData } from './preloadedData'; +export { validateEvaluationOptions } from './eventProperties'; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 0cab4b66..81ca4d99 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -778,6 +778,12 @@ declare namespace SplitIO { type Properties = { [propertyName: string]: string | number | boolean | null; }; + /** + * Evaluation options object for getTreatment methods. + */ + type EvaluationOptions = { + properties?: Properties; + } /** * The SplitKey object format. */ @@ -811,6 +817,7 @@ declare namespace SplitIO { label: string; changeNumber: number; pt?: number; + properties?: Properties; } /** * Object with information about an impression. It contains the generated impression DTO as well as @@ -1516,7 +1523,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The treatment string. */ - getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes): Treatment; + getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): Treatment; /** * Returns a TreatmentWithConfig value, which is an object with both treatment and config string for the given feature. * @@ -1526,7 +1533,7 @@ declare namespace SplitIO { * @returns The TreatmentWithConfig, the object containing the treatment string and the * configuration stringified JSON (or null if there was no config for that treatment). */ - getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes): TreatmentWithConfig; + getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the given features. * @@ -1535,7 +1542,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The treatments object map. */ - getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): Treatments; + getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * @@ -1544,7 +1551,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * @@ -1553,7 +1560,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the Treatment objects */ - getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): Treatments; + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * @@ -1562,7 +1569,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * @@ -1571,7 +1578,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the Treatment objects */ - getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): Treatments; + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * @@ -1580,7 +1587,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface. * @@ -1607,7 +1614,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatment promise that resolves to the treatment string. */ - getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes): AsyncTreatment; + getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatment; /** * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for the given feature. * @@ -1616,7 +1623,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentWithConfig promise that resolves to the TreatmentWithConfig object. */ - getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes): AsyncTreatmentWithConfig; + getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentWithConfig; /** * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the given features. * @@ -1625,7 +1632,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): AsyncTreatments; + getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which will be (or eventually be) an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * @@ -1634,7 +1641,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ - getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * @@ -1643,7 +1650,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * @@ -1652,7 +1659,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ - getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * @@ -1661,7 +1668,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * @@ -1670,7 +1677,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ - getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface, and returns a promise to signal when the event was successfully queued (or not). * @@ -1737,7 +1744,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The treatment string. */ - getTreatment(featureFlagName: string, attributes?: Attributes): Treatment; + getTreatment(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): Treatment; /** * Returns a TreatmentWithConfig value, which is an object with both treatment and config string for the given feature. * @@ -1745,7 +1752,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map containing the treatment and the configuration stringified JSON (or null if there was no config for that treatment). */ - getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes): TreatmentWithConfig; + getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the given features. * @@ -1753,7 +1760,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The treatments object map. */ - getTreatments(featureFlagNames: string[], attributes?: Attributes): Treatments; + getTreatments(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * @@ -1761,7 +1768,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * @@ -1769,7 +1776,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the Treatments objects */ - getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes): Treatments; + getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * @@ -1777,7 +1784,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * @@ -1785,7 +1792,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the Treatments objects */ - getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes): Treatments; + getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * @@ -1793,7 +1800,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns The map with all the TreatmentWithConfig objects */ - getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes): TreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface. * @@ -1816,7 +1823,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatment promise that resolves to the treatment string. */ - getTreatment(featureFlagName: string, attributes?: Attributes): AsyncTreatment; + getTreatment(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatment; /** * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for the given feature. * @@ -1824,7 +1831,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentWithConfig promise that resolves to the TreatmentWithConfig object. */ - getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes): AsyncTreatmentWithConfig; + getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentWithConfig; /** * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the given features. * @@ -1832,7 +1839,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatments(featureFlagNames: string[], attributes?: Attributes): AsyncTreatments; + getTreatments(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which will be (or eventually be) an object map with the TreatmentWithConfig (an object with both treatment and config string) for the given features. * @@ -1840,7 +1847,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ - getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * @@ -1848,7 +1855,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag set. * @@ -1856,7 +1863,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ - getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flag sets. * @@ -1864,7 +1871,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns Treatments promise that resolves to the treatments object map. */ - getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes): AsyncTreatments; + getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; /** * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flag sets. * @@ -1872,7 +1879,7 @@ declare namespace SplitIO { * @param attributes - An object of type Attributes defining the attributes for the given key. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ - getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes): AsyncTreatmentsWithConfig; + getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; /** * Tracks an event to be fed to the results product on Split user interface, and returns a promise to signal when the event was successfully queued (or not). * From a3ab3068264f5f8b4838575706ec139f59d7083a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 23:00:51 -0300 Subject: [PATCH 31/44] Debug strategy for impressions with properties --- src/sdkFactory/index.ts | 9 +++++---- .../__tests__/impressionsTracker.spec.ts | 16 ++++++---------- src/trackers/impressionsTracker.ts | 9 ++++----- src/trackers/strategy/strategyOptimized.ts | 3 +++ 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index ad1e8432..b342a6e0 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -62,12 +62,13 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA const uniqueKeysTracker = uniqueKeysTrackerFactory(log, storage.uniqueKeys, filterAdapterFactory && filterAdapterFactory()); const noneStrategy = strategyNoneFactory(storage.impressionCounts, uniqueKeysTracker); - const debugStrategy = strategyDebugFactory(observer); - const defaultStrategy = impressionsMode === OPTIMIZED ? + const strategy = impressionsMode === OPTIMIZED ? strategyOptimizedFactory(observer, storage.impressionCounts) : - impressionsMode === DEBUG ? debugStrategy : noneStrategy; + impressionsMode === DEBUG ? + strategyDebugFactory(observer) : + noneStrategy; - const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, noneStrategy, debugStrategy, defaultStrategy, whenInit, integrationsManager, storage.telemetry); + const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, noneStrategy, strategy, whenInit, integrationsManager, storage.telemetry); const eventTracker = eventTrackerFactory(settings, storage.events, whenInit, integrationsManager, storage.telemetry); // splitApi is used by SyncManager and Browser signal listener diff --git a/src/trackers/__tests__/impressionsTracker.spec.ts b/src/trackers/__tests__/impressionsTracker.spec.ts index 52376b9f..1db3875f 100644 --- a/src/trackers/__tests__/impressionsTracker.spec.ts +++ b/src/trackers/__tests__/impressionsTracker.spec.ts @@ -40,10 +40,6 @@ const fakeNoneStrategy = { process: jest.fn(() => false) }; -const fakeDebugStrategy = { - process: jest.fn(() => false) -}; - /* Tests */ describe('Impressions Tracker', () => { @@ -57,7 +53,7 @@ describe('Impressions Tracker', () => { const strategy = strategyDebugFactory(impressionObserverCSFactory()); test('Should be able to track impressions (in DEBUG mode without Previous Time).', () => { - const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategy, fakeWhenInit); + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit); const imp1 = { feature: '10', @@ -77,7 +73,7 @@ describe('Impressions Tracker', () => { }); test('Tracked impressions should be sent to impression listener and integration manager when we invoke .track()', (done) => { - const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategy, fakeWhenInit, fakeIntegrationsManager); + const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit, fakeIntegrationsManager); const fakeImpression = { feature: 'impression' @@ -151,8 +147,8 @@ describe('Impressions Tracker', () => { impression3.time = 1234567891; const trackers = [ - impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined), - impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined) + impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined), + impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined) ]; expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked. @@ -179,7 +175,7 @@ describe('Impressions Tracker', () => { impression3.time = Date.now(); const impressionCountsCache = new ImpressionCountsCacheInMemory(); - const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache), fakeWhenInit, undefined, fakeTelemetryCache as any); + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache), fakeWhenInit, undefined, fakeTelemetryCache as any); expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker @@ -202,7 +198,7 @@ describe('Impressions Tracker', () => { test('Should track or not impressions depending on user consent status', () => { const settings = { ...fullSettings }; - const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache, fakeNoneStrategy, fakeDebugStrategy, strategy, fakeWhenInit); + const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit); tracker.track([{ imp: impression }]); expect(fakeImpressionsCache.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index acf5fa3b..2529633a 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -14,8 +14,7 @@ export function impressionsTrackerFactory( settings: ISettings, impressionsCache: IImpressionsCacheBase, noneStrategy: IStrategy, - debugStrategy: IStrategy, - defaultStrategy: IStrategy, + strategy: IStrategy, whenInit: (cb: () => void) => void, integrationsManager?: IImpressionsHandler, telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync, @@ -28,11 +27,11 @@ export function impressionsTrackerFactory( if (settings.userConsent === CONSENT_DECLINED) return; const impressionsToStore = impressions.filter(({ imp, disabled }) => { + if (options && options.properties) imp.properties = options.properties; + return disabled ? noneStrategy.process(imp) : - options && options.properties ? - (imp.properties = options.properties) && debugStrategy.process(imp) : - defaultStrategy.process(imp); + strategy.process(imp); }); const impressionsLength = impressions.length; diff --git a/src/trackers/strategy/strategyOptimized.ts b/src/trackers/strategy/strategyOptimized.ts index 9a9cf883..8220de7e 100644 --- a/src/trackers/strategy/strategyOptimized.ts +++ b/src/trackers/strategy/strategyOptimized.ts @@ -20,6 +20,9 @@ export function strategyOptimizedFactory( process(impression: SplitIO.ImpressionDTO) { impression.pt = impressionsObserver.testAndSet(impression); + // DEBUG mode for impressions with properties + if (impression.properties) return true; + const now = Date.now(); // Increments impression counter per featureName From a976968dda4f436d416ae8045189ba8747ba2939 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Mar 2025 17:52:50 -0300 Subject: [PATCH 32/44] Add options to client methods --- src/logger/messages/warn.ts | 2 +- src/sdkClient/client.ts | 22 +++++----- src/sdkClient/clientAttributesDecoration.ts | 41 +++++++++--------- src/sdkClient/clientInputValidation.ts | 44 ++++++++++---------- src/trackers/impressionsTracker.ts | 4 +- src/trackers/strategy/strategyOptimized.ts | 6 +-- src/trackers/types.ts | 2 +- src/utils/inputValidation/eventProperties.ts | 2 + types/splitio.d.ts | 5 ++- 9 files changed, 66 insertions(+), 62 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 52487f95..568771a8 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -18,7 +18,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'], // input validation [c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'], - [c.WARN_TRIMMING_PROPERTIES, '%s: Event has more than 300 properties. Some of them will be trimmed when processed.'], + [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.'], diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index e61a393e..ab3b39e6 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -31,12 +31,12 @@ 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 | undefined, withConfig = false, methodName = GET_TREATMENT) { + function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) { const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT); const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDecorated[] = []; - const treatment = processEvaluation(evaluationResult, featureFlagName, key, attributes, withConfig, methodName, queue); + const treatment = processEvaluation(evaluationResult, featureFlagName, key, options, withConfig, methodName, queue); impressionsTracker.track(queue, attributes); stopTelemetryTracker(queue[0] && queue[0].imp.label); @@ -52,8 +52,8 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return thenable(evaluation) ? evaluation.then((res) => wrapUp(res)) : wrapUp(evaluation); } - function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatment(key, featureFlagName, attributes, true, GET_TREATMENT_WITH_CONFIG); + function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) { + return getTreatment(key, featureFlagName, attributes, options, true, GET_TREATMENT_WITH_CONFIG); } function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) { @@ -63,9 +63,9 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const queue: ImpressionDecorated[] = []; const treatments: Record = {}; Object.keys(evaluationResults).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue); + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, options, withConfig, methodName, queue); }); - impressionsTracker.track(queue, attributes, options); + impressionsTracker.track(queue, attributes); stopTelemetryTracker(queue[0] && queue[0].imp.label); return treatments; @@ -84,7 +84,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG); } - function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, options: SplitIO.EvaluationOptions | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { + function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) { const stopTelemetryTracker = telemetryTracker.trackEval(method); const wrapUp = (evaluationResults: Record) => { @@ -92,9 +92,9 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const treatments: Record = {}; const evaluations = evaluationResults; Object.keys(evaluations).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue); + treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, options, withConfig, methodName, queue); }); - impressionsTracker.track(queue, attributes, options); + impressionsTracker.track(queue, attributes); stopTelemetryTracker(queue[0] && queue[0].imp.label); return treatments; @@ -126,13 +126,14 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl evaluation: IEvaluationResult, featureFlagName: string, key: SplitIO.SplitKey, - attributes: SplitIO.Attributes | undefined, + options: SplitIO.EvaluationOptions | undefined, withConfig: boolean, invokingMethodName: string, queue: ImpressionDecorated[] ): SplitIO.Treatment | SplitIO.TreatmentWithConfig { const matchingKey = getMatching(key); const bucketingKey = getBucketing(key); + const properties = options && options.properties ? JSON.stringify(options.properties) : undefined; const { treatment, label, changeNumber, config = null, impressionsDisabled } = evaluation; log.info(IMPRESSION, [featureFlagName, matchingKey, treatment, label]); @@ -148,6 +149,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl bucketingKey, label, changeNumber: changeNumber as number, + properties }, disabled: impressionsDisabled }); diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index cf31b5d3..388b3142 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -22,48 +22,47 @@ export function clientAttributesDecoration 0) { - return objectAssign({}, storedAttributes, maybeAttributes); - } - return maybeAttributes; + return Object.keys(storedAttributes).length > 0 ? + objectAssign({}, storedAttributes, maybeAttributes) : + maybeAttributes; } return objectAssign(client, { diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 38207656..40765d41 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -67,27 +67,27 @@ export function clientInputValidationDecorator res[split] = CONTROL); @@ -96,11 +96,11 @@ export function clientInputValidationDecorator res[split] = objectAssign({}, CONTROL_WITH_CONFIG)); @@ -109,41 +109,41 @@ export function clientInputValidationDecorator { - if (options && options.properties) imp.properties = options.properties; - return disabled ? noneStrategy.process(imp) : strategy.process(imp); diff --git a/src/trackers/strategy/strategyOptimized.ts b/src/trackers/strategy/strategyOptimized.ts index 8220de7e..24c82ebd 100644 --- a/src/trackers/strategy/strategyOptimized.ts +++ b/src/trackers/strategy/strategyOptimized.ts @@ -18,11 +18,11 @@ export function strategyOptimizedFactory( return { process(impression: SplitIO.ImpressionDTO) { - impression.pt = impressionsObserver.testAndSet(impression); - - // DEBUG mode for impressions with properties + // DEBUG mode without previous time, for impressions with properties if (impression.properties) return true; + impression.pt = impressionsObserver.testAndSet(impression); + const now = Date.now(); // Increments impression counter per featureName diff --git a/src/trackers/types.ts b/src/trackers/types.ts index 7ac928fb..a0dd2dd4 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -29,7 +29,7 @@ export type ImpressionDecorated = { }; export interface IImpressionsTracker { - track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions): void + track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes): void } /** Telemetry tracker */ diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index 4a0f4e5c..63178419 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -71,6 +71,8 @@ export function validateEvaluationOptions(log: ILogger, maybeOptions: any, metho if (isObject(maybeOptions)) { const properties = validateEventProperties(log, maybeOptions.properties, method).properties; return properties ? { properties } : undefined; + } else if (maybeOptions) { + log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'evaluation options']); } return undefined; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 81ca4d99..5aff3a88 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -817,7 +817,10 @@ declare namespace SplitIO { label: string; changeNumber: number; pt?: number; - properties?: Properties; + /** + * JSON stringified version of the impression properties. + */ + properties?: string; } /** * Object with information about an impression. It contains the generated impression DTO as well as From b24882b3e390a6c5b2e88567cd65b8842d92f297 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 Mar 2025 13:13:00 -0300 Subject: [PATCH 33/44] Unit tests --- .../__tests__/clientInputValidation.spec.ts | 44 ++++++++++++++++++- src/sdkClient/__tests__/testUtils.ts | 15 +++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/sdkClient/__tests__/clientInputValidation.spec.ts b/src/sdkClient/__tests__/clientInputValidation.spec.ts index 0664c179..25b224e6 100644 --- a/src/sdkClient/__tests__/clientInputValidation.spec.ts +++ b/src/sdkClient/__tests__/clientInputValidation.spec.ts @@ -3,13 +3,15 @@ import { clientInputValidationDecorator } from '../clientInputValidation'; // Mocks import { DebugLogger } from '../../logger/browser/DebugLogger'; +import { createClientMock } from './testUtils'; const settings: any = { log: DebugLogger(), sync: { __splitFiltersValidation: { groupedFilters: { bySet: [] } } } }; -const client: any = {}; +const EVALUATION_RESULT = 'on'; +const client: any = createClientMock(EVALUATION_RESULT); const readinessManager: any = { isReady: () => true, @@ -52,4 +54,44 @@ describe('clientInputValidationDecorator', () => { // @TODO should be 8, but there is an additional log from `getTreatmentsByFlagSet` and `getTreatmentsWithConfigByFlagSet` that should be removed expect(logSpy).toBeCalledTimes(10); }); + + test('should evaluate but log an error if the passed 4th argument (evaluation options) is invalid', () => { + expect(clientWithValidation.getTreatment('key', 'ff', undefined, 'invalid')).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatment: evaluation options must be a plain object.'); + expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, undefined); + + expect(clientWithValidation.getTreatmentWithConfig('key', 'ff', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentWithConfig: properties must be a plain object.'); + expect(client.getTreatmentWithConfig).toBeCalledWith('key', 'ff', undefined, undefined); + + expect(clientWithValidation.getTreatments('key', ['ff'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatments: properties must be a plain object.'); + expect(client.getTreatments).toBeCalledWith('key', ['ff'], undefined, undefined); + + expect(clientWithValidation.getTreatmentsWithConfig('key', ['ff'], {}, { properties: true })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfig: properties must be a plain object.'); + expect(client.getTreatmentsWithConfig).toBeCalledWith('key', ['ff'], {}, undefined); + + expect(clientWithValidation.getTreatmentsByFlagSet('key', 'flagSet', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSet: properties must be a plain object.'); + expect(client.getTreatmentsByFlagSet).toBeCalledWith('key', 'flagset', undefined, undefined); + + expect(clientWithValidation.getTreatmentsWithConfigByFlagSet('key', 'flagSet', {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toBeCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSet: properties must be a plain object.'); + expect(client.getTreatmentsWithConfigByFlagSet).toBeCalledWith('key', 'flagset', {}, undefined); + + expect(clientWithValidation.getTreatmentsByFlagSets('key', ['flagSet'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSets: properties must be a plain object.'); + expect(client.getTreatmentsByFlagSets).toBeCalledWith('key', ['flagset'], undefined, undefined); + + expect(clientWithValidation.getTreatmentsWithConfigByFlagSets('key', ['flagSet'], {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSets: properties must be a plain object.'); + expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith('key', ['flagset'], {}, undefined); + }); + + test('should sanitize the properties in the 4th argument', () => { + expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: { toSanitize: /asd/, correct: 100 }})).toBe(EVALUATION_RESULT); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] splitio => getTreatment: Property "toSanitize" is of invalid type. Setting value to null.'); + expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, { properties: { toSanitize: null, correct: 100 }}); + }); }); diff --git a/src/sdkClient/__tests__/testUtils.ts b/src/sdkClient/__tests__/testUtils.ts index 901897e3..dab0085a 100644 --- a/src/sdkClient/__tests__/testUtils.ts +++ b/src/sdkClient/__tests__/testUtils.ts @@ -8,3 +8,18 @@ export function assertClientApi(client: any, sdkStatus?: object) { expect(typeof client[method]).toBe('function'); }); } + +export function createClientMock(returnValue: any) { + + return { + getTreatment: jest.fn(()=> returnValue), + getTreatmentWithConfig: jest.fn(()=> returnValue), + getTreatments: jest.fn(()=> returnValue), + getTreatmentsWithConfig: jest.fn(()=> returnValue), + getTreatmentsByFlagSets: jest.fn(()=> returnValue), + getTreatmentsWithConfigByFlagSets: jest.fn(()=> returnValue), + getTreatmentsByFlagSet: jest.fn(()=> returnValue), + getTreatmentsWithConfigByFlagSet: jest.fn(()=> returnValue), + track: jest.fn(()=> returnValue), + }; +} From 7a7de540e38170b9f42ac67b2a8393c55cf7be77 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 Mar 2025 13:14:55 -0300 Subject: [PATCH 34/44] Remove unnecessary 'track' function overwrite in clientAttributesDecoration --- .../clientAttributesDecoration.spec.ts | 189 ++---------------- src/sdkClient/clientAttributesDecoration.ts | 6 - 2 files changed, 19 insertions(+), 176 deletions(-) diff --git a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts index e007dc54..65aa8ef1 100644 --- a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts +++ b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts @@ -33,7 +33,7 @@ const clientMock = { } }; // @ts-expect-error -const client = clientAttributesDecoration(loggerMock, clientMock); +const client: any = clientAttributesDecoration(loggerMock, clientMock); test('ATTRIBUTES DECORATION / storage', () => { @@ -91,9 +91,7 @@ describe('ATTRIBUTES DECORATION / validation', () => { test('Should return false if it is an invalid attributes map', () => { expect(client.setAttribute('', 'attributeValue')).toEqual(false); // It should be invalid if the attribute key is not a string - // @ts-expect-error expect(client.setAttribute('attributeKey1', new Date())).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. - // @ts-expect-error expect(client.setAttribute('attributeKey2', { 'some': 'object' })).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. expect(client.setAttribute('attributeKey3', Infinity)).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. @@ -153,180 +151,31 @@ describe('ATTRIBUTES DECORATION / validation', () => { describe('ATTRIBUTES DECORATION / evaluation', () => { - test('Evaluation attributes logic and precedence / getTreatment', () => { - + test.each([ + ['getTreatment', 'split'], + ['getTreatments', ['split']], + ['getTreatmentWithConfig', 'split'], + ['getTreatmentsWithConfig', ['split']], + ['getTreatmentsByFlagSet', 'set'], + ['getTreatmentsWithConfigByFlagSet', 'set'], + ['getTreatmentsByFlagSets', ['set']], + ['getTreatmentsWithConfigByFlagSets', ['set']] + ])('Evaluation attributes logic and precedence / %s', (method, param) => { // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatment('key', 'split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client[method]('key', param)).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client[method]('key', param, { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty client.setAttribute('func_attr_bool', false); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatment('key', 'split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatment('key', 'split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + expect(client[method]('key', param, { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + expect(client[method]('key', param, null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client[method]('key', param, { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations client.setAttributes({ func_attr_str: 'false' }); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatment('key', 'split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatment('key', 'split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client[method]('key', param, { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + expect(client[method]('key', param, null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client[method]('key', param)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. expect(client.clearAttributes()).toEqual(true); - - }); - - test('Evaluation attributes logic and precedence / getTreatments', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatments('key', ['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatments('key', ['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatments('key', ['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatments('key', ['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatments('key', ['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.clearAttributes()).toEqual(true); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentWithConfig', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentWithConfig('key', 'split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentWithConfig('key', 'split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentWithConfig('key', 'split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentWithConfig('key', 'split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.clearAttributes()).toEqual(true); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsWithConfig', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfig('key', ['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfig('key', ['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfig('key', ['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfig('key', ['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsByFlagSets', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsByFlagSets('key', ['set'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsByFlagSets('key', ['set'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsWithConfigByFlagSets', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsByFlagSet', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsByFlagSet('key', 'set')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsByFlagSet('key', 'set')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - - }); - - test('Evaluation attributes logic and precedence / getTreatmentsWithConfigByFlagSet', () => { - - // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api - expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty - client.setAttribute('func_attr_bool', false); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations - client.setAttributes({ func_attr_str: 'false' }); - expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones - // @ts-ignore - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfigByFlagSet('key', 'set')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - client.clearAttributes(); - }); }); diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index 388b3142..b9a901db 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -20,7 +20,6 @@ export function clientAttributesDecoration 0 ? @@ -74,7 +69,6 @@ export function clientAttributesDecoration Date: Thu, 20 Mar 2025 16:40:31 -0300 Subject: [PATCH 35/44] Updated CHANGES.txt file and additional polishing --- CHANGES.txt | 3 + package-lock.json | 307 ++++++++++------------------------------ src/sdkClient/client.ts | 22 +-- types/splitio.d.ts | 24 ++++ 4 files changed, 112 insertions(+), 244 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index db1fa81e..4420de06 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.2.0 (March 26, 2025) + - Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split's backend. Read more in our docs. + 2.1.0 (January 17, 2025) - Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs. diff --git a/package-lock.json b/package-lock.json index f15fce99..f559a07a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,13 +67,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -266,18 +267,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -293,38 +294,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.10" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -495,9 +484,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -507,14 +496,14 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -542,14 +531,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1979,18 +1967,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2379,20 +2355,6 @@ } ] }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -2459,21 +2421,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2812,15 +2759,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -3978,15 +3916,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -7465,18 +7394,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -7577,15 +7494,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8164,13 +8072,14 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { @@ -8319,15 +8228,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true }, "@babel/helper-validator-option": { @@ -8337,33 +8246,24 @@ "dev": true }, "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" } }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.10" } }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -8482,23 +8382,23 @@ } }, "@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "requires": { "regenerator-runtime": "^0.14.0" } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" } }, "@babel/traverse": { @@ -8520,14 +8420,13 @@ } }, "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" } }, "@bcoe/v8-coverage": { @@ -9608,15 +9507,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -9898,17 +9788,6 @@ "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -9956,21 +9835,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -10228,12 +10092,6 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -11089,12 +10947,6 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -13670,15 +13522,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -13757,12 +13600,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index ab3b39e6..84411c6d 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -23,6 +23,10 @@ function treatmentsNotReady(featureFlagNames: string[]) { return evaluations; } +function stringify(options?: SplitIO.EvaluationOptions) { + return options && options.properties ? JSON.stringify(options.properties) : undefined; +} + /** * Creator of base client with getTreatments and track methods. */ @@ -36,7 +40,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDecorated[] = []; - const treatment = processEvaluation(evaluationResult, featureFlagName, key, options, withConfig, methodName, queue); + const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue); impressionsTracker.track(queue, attributes); stopTelemetryTracker(queue[0] && queue[0].imp.label); @@ -61,9 +65,10 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDecorated[] = []; - const treatments: Record = {}; + const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {}; + const properties = stringify(options); Object.keys(evaluationResults).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, options, withConfig, methodName, queue); + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue); }); impressionsTracker.track(queue, attributes); @@ -89,10 +94,10 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDecorated[] = []; - const treatments: Record = {}; - const evaluations = evaluationResults; - Object.keys(evaluations).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, options, withConfig, methodName, queue); + 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); }); impressionsTracker.track(queue, attributes); @@ -126,14 +131,13 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl evaluation: IEvaluationResult, featureFlagName: string, key: SplitIO.SplitKey, - options: SplitIO.EvaluationOptions | undefined, + properties: string | undefined, withConfig: boolean, invokingMethodName: string, queue: ImpressionDecorated[] ): SplitIO.Treatment | SplitIO.TreatmentWithConfig { const matchingKey = getMatching(key); const bucketingKey = getBucketing(key); - const properties = options && options.properties ? JSON.stringify(options.properties) : undefined; const { treatment, label, changeNumber, config = null, impressionsDisabled } = evaluation; log.info(IMPRESSION, [featureFlagName, matchingKey, treatment, label]); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 5aff3a88..75232c28 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -809,13 +809,37 @@ declare namespace SplitIO { * Impression DTO generated by the SDK when processing evaluations. */ type ImpressionDTO = { + /** + * Feature flag name. + */ feature: string; + /** + * Key. + */ keyName: string; + /** + * Treatment value. + */ treatment: string; + /** + * Impression timestamp. + */ time: number; + /** + * Bucketing Key + */ bucketingKey?: string; + /** + * Rule label + */ label: string; + /** + * Version of the feature flag + */ changeNumber: number; + /** + * Previous time + */ pt?: number; /** * JSON stringified version of the impression properties. From d66c7fcad0d52233de84c8cdd0e5543b1b497849 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 Mar 2025 16:52:47 -0300 Subject: [PATCH 36/44] Update type definitions --- types/splitio.d.ts | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 75232c28..739fd85a 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -1548,6 +1548,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatment string. */ getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): Treatment; @@ -1557,8 +1558,8 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. - * @returns The TreatmentWithConfig, the object containing the treatment string and the - * configuration stringified JSON (or null if there was no config for that treatment). + * @param options - An object of type EvaluationOptions for advanced evaluation options. + * @returns The TreatmentWithConfig object that contains the treatment string and the configuration stringified JSON (or null if there was no config for that treatment). */ getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentWithConfig; /** @@ -1567,6 +1568,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatments object map. */ getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; @@ -1576,6 +1578,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; @@ -1585,6 +1588,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatment objects */ getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): Treatments; @@ -1594,6 +1598,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; @@ -1603,6 +1608,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatment objects */ getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; @@ -1612,6 +1618,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; @@ -1639,6 +1646,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatment promise that resolves to the treatment string. */ getTreatment(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatment; @@ -1648,6 +1656,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentWithConfig promise that resolves to the TreatmentWithConfig object. */ getTreatmentWithConfig(key: SplitKey, featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentWithConfig; @@ -1657,6 +1666,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ getTreatments(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; @@ -1666,6 +1676,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; @@ -1675,6 +1686,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; @@ -1684,6 +1696,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; @@ -1693,6 +1706,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; @@ -1702,6 +1716,7 @@ declare namespace SplitIO { * @param key - The string key representing the consumer. * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; @@ -1769,6 +1784,7 @@ declare namespace SplitIO { * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatment string. */ getTreatment(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): Treatment; @@ -1777,7 +1793,8 @@ declare namespace SplitIO { * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. - * @returns The map containing the treatment and the configuration stringified JSON (or null if there was no config for that treatment). + * @param options - An object of type EvaluationOptions for advanced evaluation options. + * @returns The TreatmentWithConfig object that contains the treatment string and the configuration stringified JSON (or null if there was no config for that treatment). */ getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentWithConfig; /** @@ -1785,6 +1802,7 @@ declare namespace SplitIO { * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The treatments object map. */ getTreatments(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; @@ -1793,6 +1811,7 @@ declare namespace SplitIO { * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; @@ -1801,6 +1820,7 @@ declare namespace SplitIO { * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatments objects */ getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): Treatments; @@ -1809,6 +1829,7 @@ declare namespace SplitIO { * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; @@ -1817,6 +1838,7 @@ declare namespace SplitIO { * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the Treatments objects */ getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): Treatments; @@ -1825,6 +1847,7 @@ declare namespace SplitIO { * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): TreatmentsWithConfig; @@ -1848,6 +1871,7 @@ declare namespace SplitIO { * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatment promise that resolves to the treatment string. */ getTreatment(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatment; @@ -1856,6 +1880,7 @@ declare namespace SplitIO { * * @param featureFlagName - The string that represents the feature flag we want to get the treatment. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentWithConfig promise that resolves to the TreatmentWithConfig object. */ getTreatmentWithConfig(featureFlagName: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentWithConfig; @@ -1864,6 +1889,7 @@ declare namespace SplitIO { * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ getTreatments(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; @@ -1872,6 +1898,7 @@ declare namespace SplitIO { * * @param featureFlagNames - An array of the feature flag names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; @@ -1880,6 +1907,7 @@ declare namespace SplitIO { * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ getTreatmentsByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; @@ -1888,6 +1916,7 @@ declare namespace SplitIO { * * @param flagSet - The flag set name we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ getTreatmentsWithConfigByFlagSet(flagSet: string, attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; @@ -1896,6 +1925,7 @@ declare namespace SplitIO { * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns Treatments promise that resolves to the treatments object map. */ getTreatmentsByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatments; @@ -1904,6 +1934,7 @@ declare namespace SplitIO { * * @param flagSets - An array of the flag set names we want to get the treatments. * @param attributes - An object of type Attributes defining the attributes for the given key. + * @param options - An object of type EvaluationOptions for advanced evaluation options. * @returns TreatmentsWithConfig promise that resolves to the TreatmentsWithConfig object. */ getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes, options?: EvaluationOptions): AsyncTreatmentsWithConfig; From bffcdaf453db62d39d60b71bca21e234461c44ce Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 25 Mar 2025 12:05:20 -0300 Subject: [PATCH 37/44] Add error handling --- src/sdkClient/client.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 84411c6d..0e526f72 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -24,7 +24,11 @@ function treatmentsNotReady(featureFlagNames: string[]) { } function stringify(options?: SplitIO.EvaluationOptions) { - return options && options.properties ? JSON.stringify(options.properties) : undefined; + if (options && options.properties) { + try { + return JSON.stringify(options.properties); + } catch { /* JSON.stringify should never throw with validated options, but handling just in case */ } + } } /** From 491ed3443226802ceb4bc28c4fdf9ce33f72c189 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 25 Mar 2025 18:08:05 -0300 Subject: [PATCH 38/44] Enhance input validation to ignore empty object properties --- src/sdkClient/__tests__/clientInputValidation.spec.ts | 10 ++++++++++ src/utils/inputValidation/eventProperties.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/sdkClient/__tests__/clientInputValidation.spec.ts b/src/sdkClient/__tests__/clientInputValidation.spec.ts index 25b224e6..f70845f7 100644 --- a/src/sdkClient/__tests__/clientInputValidation.spec.ts +++ b/src/sdkClient/__tests__/clientInputValidation.spec.ts @@ -94,4 +94,14 @@ describe('clientInputValidationDecorator', () => { expect(logSpy).toHaveBeenLastCalledWith('[WARN] splitio => getTreatment: Property "toSanitize" is of invalid type. Setting value to null.'); expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, { properties: { toSanitize: null, correct: 100 }}); }); + + test('should ignore the properties in the 4th argument if an empty object is passed', () => { + expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: {} })).toBe(EVALUATION_RESULT); + expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined); + + expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: undefined })).toBe(EVALUATION_RESULT); + expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined); + + expect(logSpy).not.toBeCalled(); + }); }); diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index 63178419..1306431c 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -70,7 +70,7 @@ export function validateEventProperties(log: ILogger, maybeProperties: any, meth export function validateEvaluationOptions(log: ILogger, maybeOptions: any, method: string): SplitIO.EvaluationOptions | undefined { if (isObject(maybeOptions)) { const properties = validateEventProperties(log, maybeOptions.properties, method).properties; - return properties ? { properties } : undefined; + return properties && Object.keys(properties).length > 0 ? { properties } : undefined; } else if (maybeOptions) { log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'evaluation options']); } From 23f49090ee2b82b42036cb769b07c2683ff64516 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 Mar 2025 23:03:15 -0300 Subject: [PATCH 39/44] Impression properties in consumer mode --- src/storages/utils.ts | 1 + src/sync/submitters/impressionsSubmitter.ts | 2 +- src/sync/submitters/types.ts | 58 ++++++++------------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/storages/utils.ts b/src/storages/utils.ts index 2963bbc5..49b21690 100644 --- a/src/storages/utils.ts +++ b/src/storages/utils.ts @@ -30,6 +30,7 @@ export function impressionsToJSON(impressions: SplitIO.ImpressionDTO[], metadata c: impression.changeNumber, m: impression.time, pt: impression.pt, + properties: impression.properties } }; diff --git a/src/sync/submitters/impressionsSubmitter.ts b/src/sync/submitters/impressionsSubmitter.ts index fed33e0f..bf05a587 100644 --- a/src/sync/submitters/impressionsSubmitter.ts +++ b/src/sync/submitters/impressionsSubmitter.ts @@ -27,7 +27,7 @@ export function fromImpressionsCollector(sendLabels: boolean, data: SplitIO.Impr r: sendLabels ? entry.label : undefined, // Rule b: entry.bucketingKey, // Bucketing Key pt: entry.pt, // Previous time - properties: entry.properties && JSON.stringify(entry.properties) // Properties + properties: entry.properties // Properties }; return keyImpression; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 47e35c07..9bae212e 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -3,28 +3,30 @@ import { IMetadata } from '../../dtos/types'; import SplitIO from '../../../types/splitio'; import { ISyncTask } from '../types'; +type ImpressionPayload = { + /** Matching Key */ + k: string; + /** Bucketing Key */ + b?: string; + /** Treatment */ + t: string; + /** Timestamp */ + m: number; + /** Change number */ + c: number; + /** Rule label */ + r?: string; + /** Previous time */ + pt?: number; + /** Stringified JSON object with properties */ + properties?: string; +}; + export type ImpressionsPayload = { /** Split name */ f: string, /** Key Impressions */ - i: { - /** User Key */ - k: string; - /** Treatment */ - t: string; - /** Timestamp */ - m: number; - /** ChangeNumber */ - c: number; - /** Rule label */ - r?: string; - /** Bucketing Key */ - b?: string; - /** Previous time */ - pt?: number; - /** Stringified JSON object with properties */ - properties?: string; - }[] + i: ImpressionPayload[] }[] export type ImpressionCountsPayload = { @@ -62,23 +64,9 @@ export type StoredImpressionWithMetadata = { /** Metadata */ m: IMetadata, /** Stored impression */ - i: { - /** keyName */ - k: string, - /** bucketingKey */ - b?: string, - /** Split name */ - f: string, - /** treatment */ - t: string, - /** label */ - r: string, - /** changeNumber */ - c: number, - /** time */ - m: number - /** previous time */ - pt?: number + i: ImpressionPayload & { + /** Feature flag name */ + f: string } } From 0abe3b225cd1bb5892f91cdbc7fff0ecaf91f85f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Mar 2025 13:24:10 -0300 Subject: [PATCH 40/44] rc --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4420de06..41924a72 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.2.0 (March 26, 2025) +2.2.0 (March 28, 2025) - Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split's backend. Read more in our docs. 2.1.0 (January 17, 2025) diff --git a/package-lock.json b/package-lock.json index f559a07a..4cccfff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.1.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.1.1-rc.1", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index d51bc14f..5f6a44d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.1.1-rc.1", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 89c14f0020c21c6381aa8b1fa4587ad693e6b468 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Mar 2025 15:36:19 -0300 Subject: [PATCH 41/44] Update type definitions --- CHANGES.txt | 2 +- types/splitio.d.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 41924a72..29391a11 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ 2.2.0 (March 28, 2025) - - Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split's backend. Read more in our docs. + - Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split backend. 2.1.0 (January 17, 2025) - Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs. diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 739fd85a..4102a7fc 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -782,6 +782,9 @@ declare namespace SplitIO { * Evaluation options object for getTreatment methods. */ type EvaluationOptions = { + /** + * Optional properties to append to the generated impression object sent to Split backend. + */ properties?: Properties; } /** From 9aebb370ebeee4431a2cc91fe94d7c5d778d82f2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 28 Mar 2025 12:02:23 -0300 Subject: [PATCH 42/44] Update DEBUG strategy --- src/trackers/strategy/strategyDebug.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/trackers/strategy/strategyDebug.ts b/src/trackers/strategy/strategyDebug.ts index 65bc06b3..ae19973e 100644 --- a/src/trackers/strategy/strategyDebug.ts +++ b/src/trackers/strategy/strategyDebug.ts @@ -14,6 +14,8 @@ export function strategyDebugFactory( return { process(impression: SplitIO.ImpressionDTO) { + if (impression.properties) return true; + impression.pt = impressionsObserver.testAndSet(impression); return true; } From 69f63d1058161338b47200f040ffec1bec636867 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 28 Mar 2025 13:26:55 -0300 Subject: [PATCH 43/44] rc --- package-lock.json | 4 ++-- package.json | 2 +- src/evaluator/parser/__tests__/index.spec.ts | 3 --- .../inLocalStorage/SplitsCacheInLocal.ts | 24 +++++++++---------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4cccfff2..7efd74c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.1-rc.1", + "version": "2.1.1-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.1-rc.1", + "version": "2.1.1-rc.2", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 5f6a44d9..2250ee18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.1-rc.1", + "version": "2.1.1-rc.2", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/evaluator/parser/__tests__/index.spec.ts b/src/evaluator/parser/__tests__/index.spec.ts index 41176fbd..30c10631 100644 --- a/src/evaluator/parser/__tests__/index.spec.ts +++ b/src/evaluator/parser/__tests__/index.spec.ts @@ -2,7 +2,6 @@ import { parser } from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; -import { bucket } from '../../../utils/murmur3/murmur3'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user is in segment all 100%:on', async function () { @@ -662,8 +661,6 @@ test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async fu let evaluation = await evaluator(keyParser('aa'), 31, 100, 31); expect(evaluation.treatment).toBe('A'); // 20%:A // bucket 6 with murmur3 - console.log(bucket('b297', 31)); - evaluation = await evaluator(keyParser('b297'), 31, 100, 31); expect(evaluation.treatment).toBe('B'); // 20%:B // bucket 34 with murmur3 diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 82be56cd..c3cb3142 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -31,16 +31,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { else localStorage.removeItem(key); } - private _decrementCounts(split: ISplit | null) { + private _decrementCounts(split: ISplit) { try { - if (split) { - const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - this._decrementCount(ttKey); + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + this._decrementCount(ttKey); - if (usesSegments(split)) { - const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - this._decrementCount(segmentsCountKey); - } + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + this._decrementCount(segmentsCountKey); } } catch (e) { this.log.error(LOG_PREFIX + e); @@ -93,12 +91,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const splitFromLocalStorage = localStorage.getItem(splitKey); const previousSplit = splitFromLocalStorage ? JSON.parse(splitFromLocalStorage) : null; + if (previousSplit) { + this._decrementCounts(previousSplit); + this.removeFromFlagSets(previousSplit.name, previousSplit.sets); + } + localStorage.setItem(splitKey, JSON.stringify(split)); this._incrementCounts(split); - this._decrementCounts(previousSplit); - - if (previousSplit) this.removeFromFlagSets(previousSplit.name, previousSplit.sets); this.addToFlagSets(split); return true; @@ -116,7 +116,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { localStorage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); - if (split) this.removeFromFlagSets(split.name, split.sets); + this.removeFromFlagSets(split.name, split.sets); return true; } catch (e) { From 752f73630c0b0c40aca8619b3998a878926a914e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 28 Mar 2025 16:05:39 -0300 Subject: [PATCH 44/44] stable version --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 17789c07..3100c540 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ 2.2.0 (March 28, 2025) - - Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split backend. + - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: - `storage.expirationDays` to specify the validity period of the rollout cache. - `storage.clearOnInit` to clear the rollout cache on SDK initialization. diff --git a/package-lock.json b/package-lock.json index 7efd74c1..5a5d556b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.1-rc.2", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.1-rc.2", + "version": "2.2.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 2250ee18..e7912d5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.1-rc.2", + "version": "2.2.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js",