From 5143303e85ef251f537c8192474e9275a67e54d2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 8 May 2024 11:01:58 -0300 Subject: [PATCH 01/15] Added new version of selectors which returns treatments together with the client readiness status --- CHANGES.txt | 4 ++- src/asyncActions.ts | 13 ++++++--- src/index.ts | 2 +- src/selectors.ts | 69 +++++++++++++++++++++++++++++++++++++++++++-- src/types.ts | 7 +++-- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index f0d5cf8..76713cb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,6 @@ -1.11.1 (May XX, 2024) +1.12.0 (May XX, 2024) + - Added new `selectSplitTreatment` and `selectSplitTreatmentWithConfig` selectors as a replacement for the now deprecated `selectTreatmentValue` and `selectTreatmentWithConfig` selectors. The new selectors retrieves more advanced use cases. + The old selectors will be removed in a future major version. - Bugfixing - Fixed error when calling `selectTreatmentValue` and `selectTreatmentWithConfig` selectors with an object as a key, caused by the key not being stringified correctly. 1.11.0 (April 3, 2024) diff --git a/src/asyncActions.ts b/src/asyncActions.ts index 7a3b828..51efbc2 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -206,14 +206,19 @@ interface IClientNotDetached extends SplitIO.IClient { * @param key optional user key * @returns SDK client with `evalOnUpdate`, `evalOnReady` and `evalOnReadyFromCache` action lists. */ -export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientNotDetached { +export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey, doNotCreate?: boolean): IClientNotDetached { const stringKey = matching(key); const isMainClient = !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); // we cannot simply use `stringKey` to get the client, since the main one could have been created with a bucketing key and/or a traffic type. - const client = (isMainClient ? splitSdk.factory.client() : splitSdk.factory.client(stringKey)) as IClientNotDetached; - - if (client._trackingStatus) return client; + const client = (isMainClient ? + splitSdk.factory.client() : + doNotCreate ? + splitSdk.sharedClients[stringKey] : + splitSdk.factory.client(stringKey) + ) as IClientNotDetached; + + if (!client || client._trackingStatus) return client; if (!isMainClient) splitSdk.sharedClients[stringKey] = client; client._trackingStatus = true; diff --git a/src/index.ts b/src/index.ts index dcd8012..d80327f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export { splitReducer } from './reducer'; export { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from './asyncActions'; export { track, getSplitNames, getSplit, getSplits } from './helpers'; -export { selectTreatmentValue, selectTreatmentWithConfig } from './selectors'; +export { selectTreatmentValue, selectTreatmentWithConfig, selectSplitTreatment, selectSplitTreatmentWithConfig } from './selectors'; // For React-redux export { connectSplit } from './react-redux/connectSplit'; diff --git a/src/selectors.ts b/src/selectors.ts index 25ca3d0..616ba58 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -1,6 +1,8 @@ -import { ISplitState } from './types'; +import { ISplitState, ISplitStatus } from './types'; import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; -import { matching } from './utils'; +import { getClient } from './asyncActions'; +import { splitSdk } from './asyncActions'; +import { getStatus, matching } from './utils'; export const getStateSlice = (sliceName: string) => (state: any) => state[sliceName]; @@ -13,6 +15,8 @@ export const defaultGetSplitState = getStateSlice(DEFAULT_SPLIT_STATE_SLICE); * @param {string} featureFlagName * @param {SplitIO.SplitKey} key * @param {string} defaultValue + * + * @deprecated Use selectSplitTreatment instead */ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): string { return selectTreatmentWithConfig(splitState, featureFlagName, key, { treatment: defaultValue, config: null }).treatment; @@ -25,8 +29,11 @@ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: s * @param {string} featureFlagName * @param {SplitIO.SplitKey} key * @param {TreatmentWithConfig} defaultValue + * + * @deprecated Use selectSplitTreatmentWithConfig instead */ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): SplitIO.TreatmentWithConfig { + // @TODO reuse `selectSplitTreatmentWithConfig` const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); const treatment = splitTreatments ? @@ -37,3 +44,61 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa return treatment ? treatment : defaultValue; } + +/** + * Selector function to extract a treatment evaluation from the Split state. It returns the treatment string value. + * + * @param {ISplitState} splitState + * @param {string} featureFlagName + * @param {SplitIO.SplitKey} key + * @param {string} defaultValue + */ +export function selectSplitTreatment(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): { + treatment: string +} & ISplitStatus { + const result: any = selectSplitTreatmentWithConfig(splitState, featureFlagName, key, { treatment: defaultValue, config: null }); + result.treatment = result.treatment.treatment; + return result; +} + +/** + * Selector function to extract a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. + * + * @param {ISplitState} splitState + * @param {string} featureFlagName + * @param {SplitIO.SplitKey} key + * @param {TreatmentWithConfig} defaultValue + */ +export function selectSplitTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { + treatment?: SplitIO.TreatmentWithConfig +} & ISplitStatus { + const client = getClient(splitSdk, key, true); + + // @TODO what should return for user error (wrong key or initSplitSdk action not dispatched yet) + if (!client) return { + treatment: undefined, + isReady: false, + isReadyFromCache: false, + hasTimedout: false, + isDestroyed: false, + isTimedout: false, + lastUpdate: 0 + }; + + const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); + const treatment = + splitTreatments ? + key ? + splitTreatments[matching(key)] : + Object.values(splitTreatments)[0] : + undefined; + + const status = getStatus(client); + + return { + ...status, + treatment: treatment ? treatment : defaultValue, + isTimedout: status.hasTimedout && !status.isReady, + lastUpdate: splitState.lastUpdate + }; +} diff --git a/src/types.ts b/src/types.ts index 499354b..7ce42a3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ -/** Type for Split reducer's slice of state */ -export interface ISplitState { +export interface ISplitStatus { /** * isReady indicates if Split SDK is ready, i.e., if it has emitted an SDK_READY event. @@ -39,6 +38,10 @@ export interface ISplitState { * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes} */ lastUpdate: number; +} + +/** Type for Split reducer's slice of state */ +export interface ISplitState extends ISplitStatus { /** * `treatments` is a nested object property that contains the evaluations of feature flags. From 5a414330262da1fcd648697e7510f90de3ef8002 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 21 May 2024 14:08:40 -0300 Subject: [PATCH 02/15] README.md update and code comments polishing --- README.md | 12 +++++------ src/selectors.ts | 53 +++++++++++++++++++----------------------------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index c0b56a7..2992ad6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ import React from 'react'; import { createStore, applyMiddleware, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import { splitReducer, initSplitSdk, getTreatments, - selectTreatmentValue, connectSplit } from '@splitsoftware/splitio-redux' + selectSplitTreatment, connectSplit } from '@splitsoftware/splitio-redux' // Init Redux store const store = createStore( @@ -47,12 +47,12 @@ store.dispatch(getTreatments({ splitNames: 'FEATURE_FLAG_NAME' })) // Connect your component to splitio's piece of state const MyComponent = connectSplit()(({ splitio }) => { - // Check SDK readiness using isReady property - if (!splitio.isReady) - return
Loading SDK ...
; - // Select a treatment value - const treatment = selectTreatmentValue(splitio, 'FEATURE_FLAG_NAME') + const { treatment, isReady } = selectSplitTreatment(splitio, 'FEATURE_FLAG_NAME') + + // Check SDK client readiness using isReady property + if (!isReady) return
Loading SDK ...
; + if (treatment === 'on') { // return JSX for 'on' treatment } else if (treatment === 'off') { diff --git a/src/selectors.ts b/src/selectors.ts index 616ba58..d7e9c21 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -9,31 +9,27 @@ export const getStateSlice = (sliceName: string) => (state: any) => state[sliceN export const defaultGetSplitState = getStateSlice(DEFAULT_SPLIT_STATE_SLICE); /** - * Selector function to extract a treatment evaluation from the Split state. It returns the treatment string value. + * This function extracts a treatment evaluation from the Split state. It returns the treatment string value. + * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value, which is `'control'` if not provided. * * @param {ISplitState} splitState * @param {string} featureFlagName * @param {SplitIO.SplitKey} key * @param {string} defaultValue - * - * @deprecated Use selectSplitTreatment instead */ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): string { return selectTreatmentWithConfig(splitState, featureFlagName, key, { treatment: defaultValue, config: null }).treatment; } /** - * Selector function to extract a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. + * This function extracts a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. + * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value, which is `{ treatment: 'control', configuration: null }` if not provided. * - * @param {ISplitState} splitState * @param {string} featureFlagName * @param {SplitIO.SplitKey} key * @param {TreatmentWithConfig} defaultValue - * - * @deprecated Use selectSplitTreatmentWithConfig instead */ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): SplitIO.TreatmentWithConfig { - // @TODO reuse `selectSplitTreatmentWithConfig` const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); const treatment = splitTreatments ? @@ -46,7 +42,8 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa } /** - * Selector function to extract a treatment evaluation from the Split state. It returns the treatment string value. + * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment string value and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, `isDestroyed`. + * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value, which is `'control'` if not provided. * * @param {ISplitState} splitState * @param {string} featureFlagName @@ -62,7 +59,8 @@ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: s } /** - * Selector function to extract a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. + * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment object and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, `isDestroyed`. + * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value as treatment, which is `{ treatment: 'control', configuration: null }` if not provided. * * @param {ISplitState} splitState * @param {string} featureFlagName @@ -72,33 +70,24 @@ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: s export function selectSplitTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { treatment?: SplitIO.TreatmentWithConfig } & ISplitStatus { - const client = getClient(splitSdk, key, true); + const treatment = selectTreatmentWithConfig(splitState, featureFlagName, key, defaultValue); - // @TODO what should return for user error (wrong key or initSplitSdk action not dispatched yet) - if (!client) return { - treatment: undefined, - isReady: false, - isReadyFromCache: false, - hasTimedout: false, - isDestroyed: false, - isTimedout: false, - lastUpdate: 0 - }; - - const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); - const treatment = - splitTreatments ? - key ? - splitTreatments[matching(key)] : - Object.values(splitTreatments)[0] : - undefined; + const client = getClient(splitSdk, key, true); - const status = getStatus(client); + const status = client ? + getStatus(client) : + { + isReady: false, + isReadyFromCache: false, + hasTimedout: false, + isDestroyed: false, + } return { ...status, - treatment: treatment ? treatment : defaultValue, + treatment, isTimedout: status.hasTimedout && !status.isReady, - lastUpdate: splitState.lastUpdate + // @TODO using main client lastUpdate for now + lastUpdate: client ? splitState.lastUpdate : 0 }; } From 69bdc3d20fa3d5c5d4a83b390495db00ded04e93 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 21 May 2024 15:39:06 -0300 Subject: [PATCH 03/15] Update comment --- src/selectors.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/selectors.ts b/src/selectors.ts index d7e9c21..af8200d 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -10,7 +10,8 @@ export const defaultGetSplitState = getStateSlice(DEFAULT_SPLIT_STATE_SLICE); /** * This function extracts a treatment evaluation from the Split state. It returns the treatment string value. - * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value, which is `'control'` if not provided. + * If a treatment is not found, it returns the default value, which is `'control'` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * * @param {ISplitState} splitState * @param {string} featureFlagName @@ -23,7 +24,8 @@ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: s /** * This function extracts a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. - * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value, which is `{ treatment: 'control', configuration: null }` if not provided. + * If a treatment is not found, it returns the default value, which is `{ treatment: 'control', configuration: null }` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * * @param {string} featureFlagName * @param {SplitIO.SplitKey} key @@ -43,7 +45,8 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa /** * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment string value and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, `isDestroyed`. - * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value, which is `'control'` if not provided. + * If a treatment is not found, it returns the default value, which is `'control'` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * * @param {ISplitState} splitState * @param {string} featureFlagName @@ -60,7 +63,8 @@ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: s /** * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment object and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, `isDestroyed`. - * If a treatment is not found, for example, due to passing an invalid Split state or a nonexistent feature flag name or key, it returns the default value as treatment, which is `{ treatment: 'control', configuration: null }` if not provided. + * If a treatment is not found, it returns the default value as treatment, which is `{ treatment: 'control', configuration: null }` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * * @param {ISplitState} splitState * @param {string} featureFlagName From e4601de7514592619342b364ba077b5be93ad055 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 May 2024 11:58:49 -0300 Subject: [PATCH 04/15] Check that initSplitSdk action was dispatched before using new selectors --- src/constants.ts | 4 +++- src/selectors.ts | 30 ++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index c62d723..2c9f1f3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -50,7 +50,9 @@ export const ERROR_TRACK_NO_INITSPLITSDK = '[ERROR] To use "track" the SDK must export const ERROR_MANAGER_NO_INITSPLITSDK = '[ERROR] To use the manager, the SDK must be first initialized with an "initSplitSdk" action'; -export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] When using selectors, "splitState" value must be a proper splitio piece of state'; +export const ERROR_SELECTOR_NO_INITSPLITSDK = '[ERROR] To use selectors, the SDK must be first initialized with an "initSplitSdk" action'; + +export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] To use selectors, "splitState" param must be a proper splitio piece of state'; export const ERROR_GETT_NO_PARAM_OBJECT = '[ERROR] "getTreatments" must be called with a param object containing a valid splitNames or flagSets properties'; diff --git a/src/selectors.ts b/src/selectors.ts index af8200d..d6e77e1 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -1,5 +1,5 @@ import { ISplitState, ISplitStatus } from './types'; -import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; +import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_INITSPLITSDK, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; import { getClient } from './asyncActions'; import { splitSdk } from './asyncActions'; import { getStatus, matching } from './utils'; @@ -32,15 +32,25 @@ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: s * @param {TreatmentWithConfig} defaultValue */ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): SplitIO.TreatmentWithConfig { - const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); - const treatment = - splitTreatments ? - key ? - splitTreatments[matching(key)] : - Object.values(splitTreatments)[0] : - undefined; + if (!splitState || !splitState.treatments) { + console.log(ERROR_SELECTOR_NO_SPLITSTATE); + return defaultValue; + } - return treatment ? treatment : defaultValue; + const splitTreatments = splitState.treatments[featureFlagName]; + + const treatment = splitTreatments ? + key ? + splitTreatments[matching(key)] : + Object.values(splitTreatments)[0] : + undefined; + + if (!treatment) { + console.log(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${featureFlagName}" ${key ? `and key "${matching(key)}"` : ''}`); + return defaultValue; + } + + return treatment; } /** @@ -76,7 +86,7 @@ export function selectSplitTreatmentWithConfig(splitState: ISplitState, featureF } & ISplitStatus { const treatment = selectTreatmentWithConfig(splitState, featureFlagName, key, defaultValue); - const client = getClient(splitSdk, key, true); + const client = splitSdk.factory ? getClient(splitSdk, key, true) : console.log(ERROR_SELECTOR_NO_INITSPLITSDK); const status = client ? getStatus(client) : From 8ab1f2d7878867342e80c6137f3dd3f91bf4263d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 May 2024 12:03:14 -0300 Subject: [PATCH 05/15] Tests --- src/__tests__/index.test.ts | 6 +- src/__tests__/selectors.test.ts | 24 ++++-- src/__tests__/selectorsWithStatus.test.ts | 100 ++++++++++++++++++++++ src/__tests__/utils/mockStore.ts | 3 +- src/__tests__/utils/storeState.ts | 30 ++++--- 5 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 src/__tests__/selectorsWithStatus.test.ts diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index a6ff13b..2a4dbb9 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -10,6 +10,8 @@ import { getSplits as exportedGetSplits, selectTreatmentValue as exportedSelectTreatmentValue, selectTreatmentWithConfig as exportedSelectTreatmentWithConfig, + selectSplitTreatment as exportedSelectSplitTreatment, + selectSplitTreatmentWithConfig as exportedSelectSplitTreatmentWithConfig, connectSplit as exportedConnectSplit, connectToggler as exportedConnectToggler, mapTreatmentToProps as exportedMapTreatmentToProps, @@ -21,7 +23,7 @@ import { import { splitReducer } from '../reducer'; import { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from '../asyncActions'; import { track, getSplitNames, getSplit, getSplits } from '../helpers'; -import { selectTreatmentValue, selectTreatmentWithConfig } from '../selectors'; +import { selectTreatmentValue, selectTreatmentWithConfig, selectSplitTreatment, selectSplitTreatmentWithConfig } from '../selectors'; import { connectSplit } from '../react-redux/connectSplit'; import { connectToggler, mapTreatmentToProps, mapIsFeatureOnToProps } from '../react-redux/connectToggler'; @@ -38,6 +40,8 @@ it('index should export modules', () => { expect(exportedGetSplits).toBe(getSplits); expect(exportedSelectTreatmentValue).toBe(selectTreatmentValue); expect(exportedSelectTreatmentWithConfig).toBe(selectTreatmentWithConfig); + expect(exportedSelectSplitTreatment).toBe(selectSplitTreatment); + expect(exportedSelectSplitTreatmentWithConfig).toBe(selectSplitTreatmentWithConfig); expect(exportedConnectSplit).toBe(connectSplit); expect(exportedConnectToggler).toBe(connectToggler); expect(exportedMapTreatmentToProps).toBe(mapTreatmentToProps); diff --git a/src/__tests__/selectors.test.ts b/src/__tests__/selectors.test.ts index f2a1fbf..8b30926 100644 --- a/src/__tests__/selectors.test.ts +++ b/src/__tests__/selectors.test.ts @@ -25,20 +25,26 @@ describe('selectTreatmentValue', () => { expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_2, { matchingKey: USER_1 })).toBe(OFF); }); - it('returns "control" value if the given feature flag name or key are invalid (were not evaluated with getTreatment, or returned "control"', () => { + it('returns "control" value and logs error if the given feature flag name or key are invalid (were not evaluated with getTreatment action)', () => { + const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_1, USER_INVALID)).toBe(CONTROL); + expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1)).toBe(CONTROL); + expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); }); - it('returns the passed default treatment value insteaad of "control" if the given feature flag name or key are invalid', () => { + it('returns the passed default treatment value and logs error if the given feature flag name or key are invalid', () => { + const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_1, USER_INVALID, 'some_value')).toBe('some_value'); + expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1, 'some_value')).toBe('some_value'); + expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); }); - it('returns "control" and log error if the given splitState is invalid', () => { - const errorSpy = jest.spyOn(console, 'error'); + it('returns "control" and logs error if the given splitState is invalid', () => { + const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentValue((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL); - expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(logSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); }); }); @@ -58,16 +64,16 @@ describe('selectTreatmentWithConfig', () => { expect(selectTreatmentWithConfig(STATE_READY.splitio, SPLIT_INVALID, USER_1)).toBe(CONTROL_WITH_CONFIG); }); - it('returns the passed default treatment insteaad of "control" if the given feature flag name or key are invalid', () => { + it('returns the passed default treatment instead of "control" if the given feature flag name or key are invalid', () => { const DEFAULT_TREATMENT = { treatment: 'some_value', config: 'some_config' }; expect(selectTreatmentWithConfig(STATE_READY.splitio, SPLIT_1, USER_INVALID, DEFAULT_TREATMENT)).toBe(DEFAULT_TREATMENT); expect(selectTreatmentWithConfig(STATE_READY.splitio, SPLIT_INVALID, USER_1, DEFAULT_TREATMENT)).toBe(DEFAULT_TREATMENT); }); - it('returns "control" and log error if the given splitState is invalid', () => { - const errorSpy = jest.spyOn(console, 'error'); + it('returns "control" and logs error if the given splitState is invalid', () => { + const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentWithConfig((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL_WITH_CONFIG); - expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(logSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); }); }); diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts new file mode 100644 index 0000000..8e3af88 --- /dev/null +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -0,0 +1,100 @@ +/** Mocks */ +import { SPLIT_1, SPLIT_2, STATE_READY, USER_1 } from './utils/storeState'; +import { mockSdk, Event } from './utils/mockBrowserSplitSdk'; +jest.mock('@splitsoftware/splitio', () => { + return { SplitFactory: mockSdk() }; +}); + +import mockStore from './utils/mockStore'; +import { STATE_INITIAL, STATUS_INITIAL } from './utils/storeState'; +import { sdkBrowserConfig } from './utils/sdkConfigs'; +import { initSplitSdk, getTreatments, splitSdk } from '../asyncActions'; + +/** Constants */ +import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE, ERROR_SELECTOR_NO_INITSPLITSDK } from '../constants'; + +/** Test targets */ +import { + selectSplitTreatment, + selectSplitTreatmentWithConfig +} from '../selectors'; + +describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { + + const logSpy = jest.spyOn(console, 'log'); + + beforeEach(() => { + logSpy.mockClear(); + }); + + it('if Split SDK was not initialized, logs error and returns default treatment and initial status', () => { + const DEFAULT_TREATMENT = { treatment: 'some_value', config: 'some_config' }; + + expect(selectSplitTreatmentWithConfig({} as any, SPLIT_1, USER_1, DEFAULT_TREATMENT)).toEqual({ + treatment: DEFAULT_TREATMENT, + ...STATUS_INITIAL, + }); + expect(logSpy).toHaveBeenCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + + expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'default_value')).toEqual({ + treatment: 'default_value', + ...STATUS_INITIAL, + }); + expect(logSpy).toHaveBeenLastCalledWith(ERROR_SELECTOR_NO_INITSPLITSDK); + }); + + it('if getTreatments action was not dispatched for the provided feature flag and key, logs error and returns default treatment and client status', () => { + const store = mockStore(STATE_INITIAL); + store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + + expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1)).toEqual({ + treatment: CONTROL, + // status of main client: + ...STATUS_INITIAL, isReady: true, isOperational: true, + }); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); + + expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ + treatment: 'some_value', + // USER_1 client has not been initialized yet: + ...STATUS_INITIAL, + }); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" and key "user_1"'); + + store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); + (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); + + expect(selectSplitTreatmentWithConfig(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({ + treatment: CONTROL_WITH_CONFIG, + // status of shared client: + ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, + }); + expect(logSpy).toHaveBeenLastCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_2" and key "user_1"'); + }); + + it('happy path: returns the treatment value and status of the client', async () => { + // The following actions result in STATE_READY state: + const store = mockStore(); + store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); + store.dispatch(getTreatments({ splitNames: [SPLIT_1] })); + store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); + + expect(selectSplitTreatment(STATE_READY.splitio, SPLIT_1)).toEqual({ + treatment: ON, + ...STATUS_INITIAL, isReady: true, isOperational: true, + lastUpdate: STATE_READY.splitio.lastUpdate, + }); + + expect(selectSplitTreatmentWithConfig(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ + treatment: STATE_READY.splitio.treatments[SPLIT_2][USER_1], + ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, + lastUpdate: STATE_READY.splitio.lastUpdate, + }); + + expect(logSpy).not.toHaveBeenCalled(); + }); + +}); diff --git a/src/__tests__/utils/mockStore.ts b/src/__tests__/utils/mockStore.ts index 48c720c..f1b0906 100644 --- a/src/__tests__/utils/mockStore.ts +++ b/src/__tests__/utils/mockStore.ts @@ -4,6 +4,7 @@ import configureMockStore from 'redux-mock-store'; const middlewares: any[] = [thunk]; /** - * Utils to not call requires files every time that we need mock the store + * redux-mock-store is designed to test the action-related logic, not the reducer-related one. In other words, it does not update the Redux store. + * Use storeState.ts for mocks of the Redux store state. */ export default configureMockStore(middlewares); diff --git a/src/__tests__/utils/storeState.ts b/src/__tests__/utils/storeState.ts index e0c6cf1..a749a61 100644 --- a/src/__tests__/utils/storeState.ts +++ b/src/__tests__/utils/storeState.ts @@ -9,6 +9,23 @@ export const USER_1 = 'user_1'; export const USER_2 = 'user_2'; export const USER_INVALID = 'user_invalid'; +export const STATUS_INITIAL = { + isReady: false, + isReadyFromCache: false, + isTimedout: false, + hasTimedout: false, + isDestroyed: false, + lastUpdate: 0, +} + +export const STATE_INITIAL: { splitio: ISplitState } = { + splitio: { + ...STATUS_INITIAL, + treatments: { + }, + }, +}; + export const STATE_READY: { splitio: ISplitState } = { splitio: { isReady: true, @@ -27,16 +44,3 @@ export const STATE_READY: { splitio: ISplitState } = { }, }, }; - -export const STATE_INITIAL: { splitio: ISplitState } = { - splitio: { - isReady: false, - isReadyFromCache: false, - isTimedout: false, - hasTimedout: false, - isDestroyed: false, - lastUpdate: 0, - treatments: { - }, - }, -}; From 7934c65a7d525c9ad1b5726cdbf588e024a4b1e6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 May 2024 12:18:01 -0300 Subject: [PATCH 06/15] Reuse const messages --- CHANGES.txt | 2 +- src/constants.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 28c744f..30fd0f1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.13.0 (May XX, 2024) +1.13.0 (May 23, 2024) - Added new `selectSplitTreatment` and `selectSplitTreatmentWithConfig` selectors as a replacement for the now deprecated `selectTreatmentValue` and `selectTreatmentWithConfig` selectors. The new selectors retrieves more advanced use cases. The old selectors will be removed in a future major version. diff --git a/src/constants.ts b/src/constants.ts index 2c9f1f3..300b5d0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,15 +42,17 @@ export const SPLIT_DESTROY = 'SPLIT_DESTROY'; export const ADD_TREATMENTS = 'ADD_TREATMENTS'; // Warning and error messages -export const ERROR_GETT_NO_INITSPLITSDK = '[ERROR] To use "getTreatments" the SDK must be first initialized with an "initSplitSdk" action'; +const errorNoInitSplitSdk = (action: string) => `[ERROR] To use "${action}" the SDK must be first initialized with an "initSplitSdk" action`; -export const ERROR_DESTROY_NO_INITSPLITSDK = '[ERROR] To use "destroySplitSdk" the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_GETT_NO_INITSPLITSDK = errorNoInitSplitSdk('getTreatments'); -export const ERROR_TRACK_NO_INITSPLITSDK = '[ERROR] To use "track" the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_DESTROY_NO_INITSPLITSDK = errorNoInitSplitSdk('destroySplitSdk'); -export const ERROR_MANAGER_NO_INITSPLITSDK = '[ERROR] To use the manager, the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_TRACK_NO_INITSPLITSDK = errorNoInitSplitSdk('track'); -export const ERROR_SELECTOR_NO_INITSPLITSDK = '[ERROR] To use selectors, the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_MANAGER_NO_INITSPLITSDK = errorNoInitSplitSdk('the manager'); + +export const ERROR_SELECTOR_NO_INITSPLITSDK = errorNoInitSplitSdk('selectors'); export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] To use selectors, "splitState" param must be a proper splitio piece of state'; From fbce557c7536964138df1019cb9fe35f8eaf3016 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 May 2024 12:20:00 -0300 Subject: [PATCH 07/15] 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 ffcd7b5..d01fecf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.0", + "version": "1.12.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.12.0", + "version": "1.12.1-rc.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.26.0", diff --git a/package.json b/package.json index 83030b7..93ab5a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.0", + "version": "1.12.1-rc.0", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", From d7d522b8101fce2f81f91e4231a679d958035913 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 May 2024 18:29:02 -0300 Subject: [PATCH 08/15] Comment updates and Test fixes --- CHANGES.txt | 12 ++++++------ src/__tests__/selectors.test.ts | 8 ++++---- src/selectors.ts | 4 ++-- src/types.ts | 12 ++++++------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 30fd0f1..0f51aa9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,6 @@ 1.13.0 (May 23, 2024) - - Added new `selectSplitTreatment` and `selectSplitTreatmentWithConfig` selectors as a replacement for the now deprecated `selectTreatmentValue` and `selectTreatmentWithConfig` selectors. The new selectors retrieves more advanced use cases. - The old selectors will be removed in a future major version. + - Added new `selectSplitTreatment` and `selectSplitTreatmentWithConfig` selectors as an alternative to `selectTreatmentValue` and `selectTreatmentWithConfig` selectors respectively. + The new selectors retrieve an object with the treatment and the status properties of the client (`isReady`, `isReadyFromCache`, `isTimedout`, `hasTimedout`, `isDestroyed`, and `lastUpdate`) for conditional rendering based on the SDK status. 1.12.0 (May 10, 2024) - Updated @splitsoftware/splitio package to version 10.26.0 that includes minor updates: @@ -80,7 +80,7 @@ - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (issue https://github.com/splitio/react-client/issues/34). - Updated streaming logic to use the newest version of our streaming service, including: - Integration with Auth service V2, connecting to the new channels and applying the received connection delay. - - Implemented handling of the new MySegmentsV2 notification types (SegmentRemoval, KeyList, Bounded and Unbounded) + - Implemented handling of the new MySegmentsV2 notification types (SegmentRemoval, KeyList, Bounded and Unbounded). - New control notification for environment scoped streaming reset. - Updated Enzyme and Jest development dependencies to fix vulnerabilities. @@ -108,13 +108,13 @@ - Added an optional callback parameter to `destroySplitSdk` action creator: `onDestroy`, to listen when the SDK has gracefully shut down. 1.1.0 (May 11, 2020) - - Bugfixing - incorrect evaluation of feature flags on browser when using `getTreatments` with a different user key than the default, caused by not waiting the fetch of segments. + - Bugfixing - Incorrect evaluation of feature flags on browser when using `getTreatments` with a different user key than the default, caused by not waiting the fetch of segments (Related to issue https://github.com/splitio/redux-client/issues/9). - Added `destroySplitSdk` action creator to gracefully shutdown the SDK. - Added two new status properties to split's piece of state: `hasTimedout` and `isDestroyed` to better reflect the current state of the associated factory. 1.0.1 (April 6, 2020) - - Updated dependencies to fix vulnerabilities - - Bugfixing - support numbers as user keys + - Updated dependencies to fix vulnerabilities. + - Bugfixing - Support numbers as user keys. 1.0.0 (January 24, 2020) - Initial public release! diff --git a/src/__tests__/selectors.test.ts b/src/__tests__/selectors.test.ts index f2a1fbf..27d7d4f 100644 --- a/src/__tests__/selectors.test.ts +++ b/src/__tests__/selectors.test.ts @@ -36,9 +36,9 @@ describe('selectTreatmentValue', () => { }); it('returns "control" and log error if the given splitState is invalid', () => { - const errorSpy = jest.spyOn(console, 'error'); + const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentValue((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL); - expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(logSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); }); }); @@ -66,8 +66,8 @@ describe('selectTreatmentWithConfig', () => { }); it('returns "control" and log error if the given splitState is invalid', () => { - const errorSpy = jest.spyOn(console, 'error'); + const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentWithConfig((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL_WITH_CONFIG); - expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(logSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); }); }); diff --git a/src/selectors.ts b/src/selectors.ts index d6e77e1..0cc9364 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -54,7 +54,7 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa } /** - * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment string value and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, `isDestroyed`. + * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment string value and the status properties of the client: `isReady`, `isReadyFromCache`, `isTimedout`, `hasTimedout`, `isDestroyed`, and `lastUpdate`. * If a treatment is not found, it returns the default value, which is `'control'` if not specified. * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * @@ -72,7 +72,7 @@ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: s } /** - * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment object and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, `isDestroyed`. + * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment object and the status properties of the client: `isReady`, `isReadyFromCache`, `isTimedout`, `hasTimedout`, `isDestroyed`, and `lastUpdate`. * If a treatment is not found, it returns the default value as treatment, which is `{ treatment: 'control', configuration: null }` if not specified. * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * diff --git a/src/types.ts b/src/types.ts index 7ce42a3..e73d3e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,13 @@ export interface ISplitStatus { /** - * isReady indicates if Split SDK is ready, i.e., if it has emitted an SDK_READY event. + * isReady indicates if Split SDK client is ready, i.e., if it has emitted an SDK_READY event. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} */ isReady: boolean; /** - * isReadyFromCache indicates if Split SDK has emitted an SDK_READY_FROM_CACHE event, what means that the SDK is ready to + * isReadyFromCache indicates if Split SDK client has emitted an SDK_READY_FROM_CACHE event, what means that the SDK is ready to * evaluate using LocalStorage cached data (which might be stale). * This flag only applies for the Browser if using LOCALSTORAGE as storage type. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} @@ -15,26 +15,26 @@ export interface ISplitStatus { isReadyFromCache: boolean; /** - * isTimedout indicates if the Split SDK has emitted an SDK_READY_TIMED_OUT event and is not ready. + * isTimedout indicates if the Split SDK client has emitted an SDK_READY_TIMED_OUT event and is not ready. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} */ isTimedout: boolean; /** - * hasTimedout indicates if the Split SDK has ever emitted an SDK_READY_TIMED_OUT event. + * hasTimedout indicates if the Split SDK client has ever emitted an SDK_READY_TIMED_OUT event. * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} */ hasTimedout: boolean; /** - * isDestroyed indicates if the Split SDK has been destroyed by dispatching a `destroySplitSdk` action. + * isDestroyed indicates if the Split SDK client has been destroyed by dispatching a `destroySplitSdk` action. * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#shutdown} */ isDestroyed: boolean; /** - * lastUpdate is the timestamp of the last Split SDK event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). + * lastUpdate is the timestamp of the last Split SDK client event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes} */ lastUpdate: number; From c0834fef92acc3d39ec12d6cbb6ed6c49a36b63f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 23 May 2024 17:54:24 -0300 Subject: [PATCH 09/15] Polishing --- src/__tests__/selectorsWithStatus.test.ts | 19 +++++---- src/__tests__/utils/storeState.ts | 4 +- src/asyncActions.ts | 19 ++++----- src/constants.ts | 12 +++--- src/helpers.ts | 50 ++++++++++++++++++++--- src/index.ts | 2 +- src/selectors.ts | 29 ++++--------- src/types.ts | 22 +++++----- src/utils.ts | 2 +- 9 files changed, 92 insertions(+), 67 deletions(-) diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts index 8e3af88..604b72d 100644 --- a/src/__tests__/selectorsWithStatus.test.ts +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -11,7 +11,7 @@ import { sdkBrowserConfig } from './utils/sdkConfigs'; import { initSplitSdk, getTreatments, splitSdk } from '../asyncActions'; /** Constants */ -import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE, ERROR_SELECTOR_NO_INITSPLITSDK } from '../constants'; +import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE, ERROR_GETSTATUS_NO_INITSPLITSDK } from '../constants'; /** Test targets */ import { @@ -22,6 +22,7 @@ import { describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { const logSpy = jest.spyOn(console, 'log'); + const errorSpy = jest.spyOn(console, 'error'); beforeEach(() => { logSpy.mockClear(); @@ -40,7 +41,7 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { treatment: 'default_value', ...STATUS_INITIAL, }); - expect(logSpy).toHaveBeenLastCalledWith(ERROR_SELECTOR_NO_INITSPLITSDK); + expect(errorSpy).toHaveBeenCalledWith(ERROR_GETSTATUS_NO_INITSPLITSDK); }); it('if getTreatments action was not dispatched for the provided feature flag and key, logs error and returns default treatment and client status', () => { @@ -53,14 +54,14 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { // status of main client: ...STATUS_INITIAL, isReady: true, isOperational: true, }); - expect(logSpy).toHaveBeenLastCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); + expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ treatment: 'some_value', // USER_1 client has not been initialized yet: ...STATUS_INITIAL, }); - expect(logSpy).toHaveBeenLastCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" and key "user_1"'); + expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" and key "user_1"'); store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); @@ -70,7 +71,7 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { // status of shared client: ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, }); - expect(logSpy).toHaveBeenLastCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_2" and key "user_1"'); + expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_2" and key "user_1"'); }); it('happy path: returns the treatment value and status of the client', async () => { @@ -84,14 +85,14 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { expect(selectSplitTreatment(STATE_READY.splitio, SPLIT_1)).toEqual({ treatment: ON, - ...STATUS_INITIAL, isReady: true, isOperational: true, - lastUpdate: STATE_READY.splitio.lastUpdate, + ...STATUS_INITIAL, + isReady: true, isOperational: true, }); expect(selectSplitTreatmentWithConfig(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ treatment: STATE_READY.splitio.treatments[SPLIT_2][USER_1], - ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, - lastUpdate: STATE_READY.splitio.lastUpdate, + ...STATUS_INITIAL, + isReadyFromCache: true, isOperational: true, }); expect(logSpy).not.toHaveBeenCalled(); diff --git a/src/__tests__/utils/storeState.ts b/src/__tests__/utils/storeState.ts index a749a61..69eccc3 100644 --- a/src/__tests__/utils/storeState.ts +++ b/src/__tests__/utils/storeState.ts @@ -12,15 +12,15 @@ export const USER_INVALID = 'user_invalid'; export const STATUS_INITIAL = { isReady: false, isReadyFromCache: false, - isTimedout: false, hasTimedout: false, isDestroyed: false, - lastUpdate: 0, } export const STATE_INITIAL: { splitio: ISplitState } = { splitio: { ...STATUS_INITIAL, + isTimedout: false, + lastUpdate: 0, treatments: { }, }, diff --git a/src/asyncActions.ts b/src/asyncActions.ts index 51efbc2..ba6651c 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -4,7 +4,7 @@ import { Dispatch, Action } from 'redux'; import { IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ISplitFactoryBuilder } from './types'; import { splitReady, splitReadyWithEvaluations, splitReadyFromCache, splitReadyFromCacheWithEvaluations, splitTimedout, splitUpdate, splitUpdateWithEvaluations, splitDestroy, addTreatments } from './actions'; import { VERSION, ERROR_GETT_NO_INITSPLITSDK, ERROR_DESTROY_NO_INITSPLITSDK, getControlTreatmentsWithConfig } from './constants'; -import { matching, getStatus, validateGetTreatmentsParams } from './utils'; +import { matching, __getStatus, validateGetTreatmentsParams } from './utils'; /** * Internal object SplitSdk. This object should not be accessed or @@ -64,7 +64,7 @@ export function initSplitSdk(params: IInitSplitSdkParams): (dispatch: Dispatch): Promise => { - const status = getStatus(defaultClient); + const status = __getStatus(defaultClient); if (status.hasTimedout) dispatch(splitTimedout()); // dispatched before `splitReady`, since it overwrites `isTimedout` property if (status.isReady) dispatch(splitReady()); @@ -141,7 +141,7 @@ export function getTreatments(params: IGetTreatmentsParams): Action | (() => voi }); } - const status = getStatus(client); + const status = __getStatus(client); // If the SDK is not ready, it stores the action to execute when ready if (!status.isReady) { @@ -206,19 +206,14 @@ interface IClientNotDetached extends SplitIO.IClient { * @param key optional user key * @returns SDK client with `evalOnUpdate`, `evalOnReady` and `evalOnReadyFromCache` action lists. */ -export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey, doNotCreate?: boolean): IClientNotDetached { +export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientNotDetached { const stringKey = matching(key); const isMainClient = !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); // we cannot simply use `stringKey` to get the client, since the main one could have been created with a bucketing key and/or a traffic type. - const client = (isMainClient ? - splitSdk.factory.client() : - doNotCreate ? - splitSdk.sharedClients[stringKey] : - splitSdk.factory.client(stringKey) - ) as IClientNotDetached; - - if (!client || client._trackingStatus) return client; + const client = (isMainClient ? splitSdk.factory.client() : splitSdk.factory.client(stringKey)) as IClientNotDetached; + + if (client._trackingStatus) return client; if (!isMainClient) splitSdk.sharedClients[stringKey] = client; client._trackingStatus = true; diff --git a/src/constants.ts b/src/constants.ts index 300b5d0..a77c90a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,20 +42,22 @@ export const SPLIT_DESTROY = 'SPLIT_DESTROY'; export const ADD_TREATMENTS = 'ADD_TREATMENTS'; // Warning and error messages -const errorNoInitSplitSdk = (action: string) => `[ERROR] To use "${action}" the SDK must be first initialized with an "initSplitSdk" action`; +const errorNoInitSplitSdk = (action: string) => `[ERROR] To use ${action} the SDK must be first initialized with an "initSplitSdk" action`; -export const ERROR_GETT_NO_INITSPLITSDK = errorNoInitSplitSdk('getTreatments'); +export const ERROR_GETT_NO_INITSPLITSDK = errorNoInitSplitSdk('"getTreatments"'); -export const ERROR_DESTROY_NO_INITSPLITSDK = errorNoInitSplitSdk('destroySplitSdk'); +export const ERROR_DESTROY_NO_INITSPLITSDK = errorNoInitSplitSdk('"destroySplitSdk"'); -export const ERROR_TRACK_NO_INITSPLITSDK = errorNoInitSplitSdk('track'); +export const ERROR_TRACK_NO_INITSPLITSDK = errorNoInitSplitSdk('"track"'); export const ERROR_MANAGER_NO_INITSPLITSDK = errorNoInitSplitSdk('the manager'); -export const ERROR_SELECTOR_NO_INITSPLITSDK = errorNoInitSplitSdk('selectors'); +export const ERROR_GETSTATUS_NO_INITSPLITSDK = errorNoInitSplitSdk('"getStatus"'); export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] To use selectors, "splitState" param must be a proper splitio piece of state'; export const ERROR_GETT_NO_PARAM_OBJECT = '[ERROR] "getTreatments" must be called with a param object containing a valid splitNames or flagSets properties'; export const WARN_FEATUREFLAGS_AND_FLAGSETS = '[WARN] Both splitNames and flagSets properties were provided. flagSets will be ignored'; + +export const WARN_GETSTATUS_NO_CLIENT = '[WARN] No client found for the provided key'; diff --git a/src/helpers.ts b/src/helpers.ts index 0693b3e..94c05b4 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,7 @@ import { splitSdk, getClient } from './asyncActions'; -import { ITrackParams } from './types'; -import { ERROR_TRACK_NO_INITSPLITSDK, ERROR_MANAGER_NO_INITSPLITSDK } from './constants'; +import { IStatus, ITrackParams } from './types'; +import { ERROR_TRACK_NO_INITSPLITSDK, ERROR_MANAGER_NO_INITSPLITSDK, ERROR_GETSTATUS_NO_INITSPLITSDK, WARN_GETSTATUS_NO_CLIENT } from './constants'; +import { __getStatus, matching } from './utils'; /** * This function track events, i.e., it invokes the actual `client.track*` methods. @@ -36,7 +37,7 @@ export function track(params: ITrackParams): boolean { } /** - * Get the array of feature flag names. + * Gets the array of feature flag names. * * @returns {string[]} The list of feature flag names. The list might be empty if the SDK was not initialized or if it's not ready yet. * @@ -52,7 +53,7 @@ export function getSplitNames(): string[] { } /** - * Get the data of a split in SplitView format. + * Gets the data of a split in SplitView format. * * @param {string} featureFlagName The name of the split we wan't to get info of. * @returns {SplitView} The SplitIO.SplitView of the given split, or null if split does not exist or the SDK was not initialized or is not ready. @@ -69,7 +70,7 @@ export function getSplit(featureFlagName: string): SplitIO.SplitView { } /** - * Get the array of feature flags data in SplitView format. + * Gets the array of feature flags data in SplitView format. * * @returns {SplitViews} The list of SplitIO.SplitView. The list might be empty if the SDK was not initialized or if it's not ready yet * @@ -83,3 +84,42 @@ export function getSplits(): SplitIO.SplitViews { return splitSdk.factory.manager().splits(); } + +/** + * Gets an object with the status properties of the SDK client or manager: + * + * - `isReady` indicates if the SDK client has emitted the SDK_READY event. + * - `isReadyFromCache` indicates if the SDK client has emitted the SDK_READY_FROM_CACHE event. + * - `hasTimedout` indicates if the SDK client has emitted the SDK_READY_TIMED_OUT event. + * - `isDestroyed` indicates if the SDK client has been destroyed, i.e., if the `destroySplitSdk` action was dispatched. + * + * @param {SplitIO.SplitKey} key To use only on client-side. Ignored in server-side. If a key is provided and a client associated to that key has been used, the status of that client is returned. + * If no key is provided, the status of the main client and manager is returned (the main client shares the status with the manager). + * + * @returns {IStatus} The status of the SDK client or manager. + * + * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#subscribe-to-events} + */ +export function getStatus(key?: SplitIO.SplitKey): IStatus { + if (splitSdk.factory) { + const stringKey = matching(key); + const isMainClient = splitSdk.isDetached || !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); + const client = isMainClient ? splitSdk.factory.client() : splitSdk.sharedClients[stringKey]; + + if (client) { + return __getStatus(client); + } else { + console.log(WARN_GETSTATUS_NO_CLIENT); + } + } else { + console.error(ERROR_GETSTATUS_NO_INITSPLITSDK); + } + + // Default status if SDK is not initialized or client is not found + return { + isReady: false, + isReadyFromCache: false, + hasTimedout: false, + isDestroyed: false, + }; +} diff --git a/src/index.ts b/src/index.ts index d80327f..80deeca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ // For Redux export { splitReducer } from './reducer'; export { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from './asyncActions'; -export { track, getSplitNames, getSplit, getSplits } from './helpers'; +export { track, getSplitNames, getSplit, getSplits, getStatus } from './helpers'; export { selectTreatmentValue, selectTreatmentWithConfig, selectSplitTreatment, selectSplitTreatmentWithConfig } from './selectors'; // For React-redux diff --git a/src/selectors.ts b/src/selectors.ts index 0cc9364..e990366 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -1,8 +1,7 @@ -import { ISplitState, ISplitStatus } from './types'; -import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_INITSPLITSDK, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; -import { getClient } from './asyncActions'; -import { splitSdk } from './asyncActions'; -import { getStatus, matching } from './utils'; +import { ISplitState, IStatus } from './types'; +import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; +import { matching } from './utils'; +import { getStatus } from './helpers'; export const getStateSlice = (sliceName: string) => (state: any) => state[sliceName]; @@ -65,7 +64,7 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa */ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): { treatment: string -} & ISplitStatus { +} & IStatus { const result: any = selectSplitTreatmentWithConfig(splitState, featureFlagName, key, { treatment: defaultValue, config: null }); result.treatment = result.treatment.treatment; return result; @@ -82,26 +81,14 @@ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: s * @param {TreatmentWithConfig} defaultValue */ export function selectSplitTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { - treatment?: SplitIO.TreatmentWithConfig -} & ISplitStatus { + treatment: SplitIO.TreatmentWithConfig +} & IStatus { const treatment = selectTreatmentWithConfig(splitState, featureFlagName, key, defaultValue); - const client = splitSdk.factory ? getClient(splitSdk, key, true) : console.log(ERROR_SELECTOR_NO_INITSPLITSDK); - - const status = client ? - getStatus(client) : - { - isReady: false, - isReadyFromCache: false, - hasTimedout: false, - isDestroyed: false, - } + const status = getStatus(key); return { ...status, treatment, - isTimedout: status.hasTimedout && !status.isReady, - // @TODO using main client lastUpdate for now - lastUpdate: client ? splitState.lastUpdate : 0 }; } diff --git a/src/types.ts b/src/types.ts index e73d3e3..ca7de89 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export interface ISplitStatus { +export interface IStatus { /** * isReady indicates if Split SDK client is ready, i.e., if it has emitted an SDK_READY event. @@ -14,12 +14,6 @@ export interface ISplitStatus { */ isReadyFromCache: boolean; - /** - * isTimedout indicates if the Split SDK client has emitted an SDK_READY_TIMED_OUT event and is not ready. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} - */ - isTimedout: boolean; - /** * hasTimedout indicates if the Split SDK client has ever emitted an SDK_READY_TIMED_OUT event. * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. @@ -32,16 +26,22 @@ export interface ISplitStatus { * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#shutdown} */ isDestroyed: boolean; +} + +/** Type for Split reducer's slice of state */ +export interface ISplitState extends IStatus { + + /** + * isTimedout indicates if the Split SDK client has emitted an SDK_READY_TIMED_OUT event and is not ready. + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + */ + isTimedout: boolean; /** * lastUpdate is the timestamp of the last Split SDK client event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes} */ lastUpdate: number; -} - -/** Type for Split reducer's slice of state */ -export interface ISplitState extends ISplitStatus { /** * `treatments` is a nested object property that contains the evaluations of feature flags. diff --git a/src/utils.ts b/src/utils.ts index 0a9b1f7..bdf915d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,7 +28,7 @@ export interface IClientStatus { isDestroyed: boolean; } -export function getStatus(client: SplitIO.IClient): IClientStatus { +export function __getStatus(client: SplitIO.IClient): IClientStatus { // @ts-expect-error, function exists but it is not part of JS SDK type definitions return client.__getStatus(); } From 758fc9c61a5180f9e70c5c2d55aa839d65b956ad Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 23 May 2024 18:09:19 -0300 Subject: [PATCH 10/15] Rename new selectors --- CHANGES.txt | 7 ++++--- README.md | 4 ++-- src/__tests__/index.test.ts | 10 +++++----- src/__tests__/selectorsWithStatus.test.ts | 20 ++++++++++---------- src/index.ts | 2 +- src/selectors.ts | 6 +++--- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0f51aa9..0346c5e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ -1.13.0 (May 23, 2024) - - Added new `selectSplitTreatment` and `selectSplitTreatmentWithConfig` selectors as an alternative to `selectTreatmentValue` and `selectTreatmentWithConfig` selectors respectively. - The new selectors retrieve an object with the treatment and the status properties of the client (`isReady`, `isReadyFromCache`, `isTimedout`, `hasTimedout`, `isDestroyed`, and `lastUpdate`) for conditional rendering based on the SDK status. +1.13.0 (May 24, 2024) + - Added a new `getStatus` helper function to retrieve the status properties of the SDK manager and clients: `isReady`, `isReadyFromCache`, `hasTimedout`, and `isDestroyed`. + - Added new `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors as alternatives to the `selectTreatmentValue` and `selectTreatmentWithConfig` selectors, respectively. + The new selectors retrieve an object with the treatment and the status properties of the client associated with the provided key, allowing conditional rendering based on the SDK status. 1.12.0 (May 10, 2024) - Updated @splitsoftware/splitio package to version 10.26.0 that includes minor updates: diff --git a/README.md b/README.md index 2992ad6..9041027 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ import React from 'react'; import { createStore, applyMiddleware, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import { splitReducer, initSplitSdk, getTreatments, - selectSplitTreatment, connectSplit } from '@splitsoftware/splitio-redux' + selectTreatmentAndStatus, connectSplit } from '@splitsoftware/splitio-redux' // Init Redux store const store = createStore( @@ -48,7 +48,7 @@ store.dispatch(getTreatments({ splitNames: 'FEATURE_FLAG_NAME' })) // Connect your component to splitio's piece of state const MyComponent = connectSplit()(({ splitio }) => { // Select a treatment value - const { treatment, isReady } = selectSplitTreatment(splitio, 'FEATURE_FLAG_NAME') + const { treatment, isReady } = selectTreatmentAndStatus(splitio, 'FEATURE_FLAG_NAME') // Check SDK client readiness using isReady property if (!isReady) return
Loading SDK ...
; diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 2a4dbb9..9641701 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -10,8 +10,8 @@ import { getSplits as exportedGetSplits, selectTreatmentValue as exportedSelectTreatmentValue, selectTreatmentWithConfig as exportedSelectTreatmentWithConfig, - selectSplitTreatment as exportedSelectSplitTreatment, - selectSplitTreatmentWithConfig as exportedSelectSplitTreatmentWithConfig, + selectTreatmentAndStatus as exportedSelectTreatmentAndStatus, + selectTreatmentWithConfigAndStatus as exportedSelectTreatmentWithConfigAndStatus, connectSplit as exportedConnectSplit, connectToggler as exportedConnectToggler, mapTreatmentToProps as exportedMapTreatmentToProps, @@ -23,7 +23,7 @@ import { import { splitReducer } from '../reducer'; import { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from '../asyncActions'; import { track, getSplitNames, getSplit, getSplits } from '../helpers'; -import { selectTreatmentValue, selectTreatmentWithConfig, selectSplitTreatment, selectSplitTreatmentWithConfig } from '../selectors'; +import { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } from '../selectors'; import { connectSplit } from '../react-redux/connectSplit'; import { connectToggler, mapTreatmentToProps, mapIsFeatureOnToProps } from '../react-redux/connectToggler'; @@ -40,8 +40,8 @@ it('index should export modules', () => { expect(exportedGetSplits).toBe(getSplits); expect(exportedSelectTreatmentValue).toBe(selectTreatmentValue); expect(exportedSelectTreatmentWithConfig).toBe(selectTreatmentWithConfig); - expect(exportedSelectSplitTreatment).toBe(selectSplitTreatment); - expect(exportedSelectSplitTreatmentWithConfig).toBe(selectSplitTreatmentWithConfig); + expect(exportedSelectTreatmentAndStatus).toBe(selectTreatmentAndStatus); + expect(exportedSelectTreatmentWithConfigAndStatus).toBe(selectTreatmentWithConfigAndStatus); expect(exportedConnectSplit).toBe(connectSplit); expect(exportedConnectToggler).toBe(connectToggler); expect(exportedMapTreatmentToProps).toBe(mapTreatmentToProps); diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts index 604b72d..7bf0090 100644 --- a/src/__tests__/selectorsWithStatus.test.ts +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -15,11 +15,11 @@ import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE, ERROR_G /** Test targets */ import { - selectSplitTreatment, - selectSplitTreatmentWithConfig + selectTreatmentAndStatus, + selectTreatmentWithConfigAndStatus } from '../selectors'; -describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { +describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => { const logSpy = jest.spyOn(console, 'log'); const errorSpy = jest.spyOn(console, 'error'); @@ -31,13 +31,13 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { it('if Split SDK was not initialized, logs error and returns default treatment and initial status', () => { const DEFAULT_TREATMENT = { treatment: 'some_value', config: 'some_config' }; - expect(selectSplitTreatmentWithConfig({} as any, SPLIT_1, USER_1, DEFAULT_TREATMENT)).toEqual({ + expect(selectTreatmentWithConfigAndStatus({} as any, SPLIT_1, USER_1, DEFAULT_TREATMENT)).toEqual({ treatment: DEFAULT_TREATMENT, ...STATUS_INITIAL, }); expect(logSpy).toHaveBeenCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); - expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'default_value')).toEqual({ + expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'default_value')).toEqual({ treatment: 'default_value', ...STATUS_INITIAL, }); @@ -49,14 +49,14 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); - expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1)).toEqual({ + expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1)).toEqual({ treatment: CONTROL, // status of main client: ...STATUS_INITIAL, isReady: true, isOperational: true, }); expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); - expect(selectSplitTreatment(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ + expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ treatment: 'some_value', // USER_1 client has not been initialized yet: ...STATUS_INITIAL, @@ -66,7 +66,7 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); - expect(selectSplitTreatmentWithConfig(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({ + expect(selectTreatmentWithConfigAndStatus(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({ treatment: CONTROL_WITH_CONFIG, // status of shared client: ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, @@ -83,13 +83,13 @@ describe('selectSplitTreatment & selectSplitTreatmentWithConfig', () => { store.dispatch(getTreatments({ splitNames: [SPLIT_1] })); store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); - expect(selectSplitTreatment(STATE_READY.splitio, SPLIT_1)).toEqual({ + expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({ treatment: ON, ...STATUS_INITIAL, isReady: true, isOperational: true, }); - expect(selectSplitTreatmentWithConfig(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ + expect(selectTreatmentWithConfigAndStatus(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ treatment: STATE_READY.splitio.treatments[SPLIT_2][USER_1], ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, diff --git a/src/index.ts b/src/index.ts index 80deeca..c99eda8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export { splitReducer } from './reducer'; export { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from './asyncActions'; export { track, getSplitNames, getSplit, getSplits, getStatus } from './helpers'; -export { selectTreatmentValue, selectTreatmentWithConfig, selectSplitTreatment, selectSplitTreatmentWithConfig } from './selectors'; +export { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } from './selectors'; // For React-redux export { connectSplit } from './react-redux/connectSplit'; diff --git a/src/selectors.ts b/src/selectors.ts index e990366..46ea1fd 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -62,10 +62,10 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa * @param {SplitIO.SplitKey} key * @param {string} defaultValue */ -export function selectSplitTreatment(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): { +export function selectTreatmentAndStatus(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): { treatment: string } & IStatus { - const result: any = selectSplitTreatmentWithConfig(splitState, featureFlagName, key, { treatment: defaultValue, config: null }); + const result: any = selectTreatmentWithConfigAndStatus(splitState, featureFlagName, key, { treatment: defaultValue, config: null }); result.treatment = result.treatment.treatment; return result; } @@ -80,7 +80,7 @@ export function selectSplitTreatment(splitState: ISplitState, featureFlagName: s * @param {SplitIO.SplitKey} key * @param {TreatmentWithConfig} defaultValue */ -export function selectSplitTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { +export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { treatment: SplitIO.TreatmentWithConfig } & IStatus { const treatment = selectTreatmentWithConfig(splitState, featureFlagName, key, defaultValue); From 780854ff8220a3410c51c7330104fd3e4f26bc0e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 24 May 2024 11:10:11 -0300 Subject: [PATCH 11/15] Fix tests --- src/__tests__/selectors.test.ts | 16 ++++++++-------- src/__tests__/selectorsWithStatus.test.ts | 12 +++++++----- src/selectors.ts | 9 +++++---- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/__tests__/selectors.test.ts b/src/__tests__/selectors.test.ts index 8b30926..5c87085 100644 --- a/src/__tests__/selectors.test.ts +++ b/src/__tests__/selectors.test.ts @@ -28,23 +28,23 @@ describe('selectTreatmentValue', () => { it('returns "control" value and logs error if the given feature flag name or key are invalid (were not evaluated with getTreatment action)', () => { const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_1, USER_INVALID)).toBe(CONTROL); - expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); + expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1)).toBe(CONTROL); - expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); + expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); }); it('returns the passed default treatment value and logs error if the given feature flag name or key are invalid', () => { const logSpy = jest.spyOn(console, 'log'); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_1, USER_INVALID, 'some_value')).toBe('some_value'); - expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); + expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1, 'some_value')).toBe('some_value'); - expect(logSpy).toHaveBeenLastCalledWith(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); + expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); }); it('returns "control" and logs error if the given splitState is invalid', () => { - const logSpy = jest.spyOn(console, 'log'); + const errorSpy = jest.spyOn(console, 'error'); expect(selectTreatmentValue((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL); - expect(logSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); }); }); @@ -72,8 +72,8 @@ describe('selectTreatmentWithConfig', () => { }); it('returns "control" and logs error if the given splitState is invalid', () => { - const logSpy = jest.spyOn(console, 'log'); + const errorSpy = jest.spyOn(console, 'error'); expect(selectTreatmentWithConfig((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL_WITH_CONFIG); - expect(logSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); }); }); diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts index 7bf0090..dc40a26 100644 --- a/src/__tests__/selectorsWithStatus.test.ts +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -26,6 +26,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => beforeEach(() => { logSpy.mockClear(); + errorSpy.mockClear(); }); it('if Split SDK was not initialized, logs error and returns default treatment and initial status', () => { @@ -35,7 +36,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => treatment: DEFAULT_TREATMENT, ...STATUS_INITIAL, }); - expect(logSpy).toHaveBeenCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + expect(errorSpy).toHaveBeenCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'default_value')).toEqual({ treatment: 'default_value', @@ -54,14 +55,14 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => // status of main client: ...STATUS_INITIAL, isReady: true, isOperational: true, }); - expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); + expect(logSpy).toHaveBeenCalledWith('[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ treatment: 'some_value', // USER_1 client has not been initialized yet: ...STATUS_INITIAL, }); - expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" and key "user_1"'); + expect(logSpy).toHaveBeenCalledWith('[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" and key "user_1"'); store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); @@ -71,10 +72,10 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => // status of shared client: ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, }); - expect(logSpy).toHaveBeenCalledWith('[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_2" and key "user_1"'); + expect(logSpy).toHaveBeenCalledWith('[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_2" and key "user_1"'); }); - it('happy path: returns the treatment value and status of the client', async () => { + it('happy path: returns the treatment value and status of the client', () => { // The following actions result in STATE_READY state: const store = mockStore(); store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); @@ -96,6 +97,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => }); expect(logSpy).not.toHaveBeenCalled(); + expect(errorSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/selectors.ts b/src/selectors.ts index 46ea1fd..6b4a2c0 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -26,13 +26,14 @@ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: s * If a treatment is not found, it returns the default value, which is `{ treatment: 'control', configuration: null }` if not specified. * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * + * @param {ISplitState} splitState * @param {string} featureFlagName * @param {SplitIO.SplitKey} key - * @param {TreatmentWithConfig} defaultValue + * @param {SplitIO.TreatmentWithConfig} defaultValue */ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): SplitIO.TreatmentWithConfig { if (!splitState || !splitState.treatments) { - console.log(ERROR_SELECTOR_NO_SPLITSTATE); + console.error(ERROR_SELECTOR_NO_SPLITSTATE); return defaultValue; } @@ -45,7 +46,7 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa undefined; if (!treatment) { - console.log(`[ERROR] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${featureFlagName}" ${key ? `and key "${matching(key)}"` : ''}`); + console.log(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${featureFlagName}" ${key ? `and key "${matching(key)}"` : ''}`); return defaultValue; } @@ -78,7 +79,7 @@ export function selectTreatmentAndStatus(splitState: ISplitState, featureFlagNam * @param {ISplitState} splitState * @param {string} featureFlagName * @param {SplitIO.SplitKey} key - * @param {TreatmentWithConfig} defaultValue + * @param {SplitIO.TreatmentWithConfig} defaultValue */ export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { treatment: SplitIO.TreatmentWithConfig From 31b805c1d48bb5681b302443e1bccfc784a7af75 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 24 May 2024 11:10:26 -0300 Subject: [PATCH 12/15] 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 d01fecf..263eacf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.0", + "version": "1.12.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.0", + "version": "1.12.1-rc.1", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.26.0", diff --git a/package.json b/package.json index 93ab5a6..39c7f2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.0", + "version": "1.12.1-rc.1", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", From d76335c211a6ae0240789f22738efae504e249fc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 24 May 2024 12:08:12 -0300 Subject: [PATCH 13/15] Rollback selector logs, to avoid noise if they are used before actions are dispatched --- src/__tests__/selectors.test.ts | 10 ++------- src/__tests__/selectorsWithStatus.test.ts | 17 ++++++--------- src/helpers.ts | 2 +- src/selectors.ts | 26 +++++++---------------- 4 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/__tests__/selectors.test.ts b/src/__tests__/selectors.test.ts index 5c87085..0583e9b 100644 --- a/src/__tests__/selectors.test.ts +++ b/src/__tests__/selectors.test.ts @@ -25,20 +25,14 @@ describe('selectTreatmentValue', () => { expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_2, { matchingKey: USER_1 })).toBe(OFF); }); - it('returns "control" value and logs error if the given feature flag name or key are invalid (were not evaluated with getTreatment action)', () => { - const logSpy = jest.spyOn(console, 'log'); + it('returns "control" value if the given feature flag name or key are invalid (were not evaluated with getTreatment, or returned "control"', () => { expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_1, USER_INVALID)).toBe(CONTROL); - expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1)).toBe(CONTROL); - expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); }); - it('returns the passed default treatment value and logs error if the given feature flag name or key are invalid', () => { - const logSpy = jest.spyOn(console, 'log'); + it('returns the passed default treatment value insteaad of "control" if the given feature flag name or key are invalid', () => { expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_1, USER_INVALID, 'some_value')).toBe('some_value'); - expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_1}" and key "${USER_INVALID}"`); expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1, 'some_value')).toBe('some_value'); - expect(logSpy).toHaveBeenLastCalledWith(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${SPLIT_INVALID}" and key "${USER_1}"`); }); it('returns "control" and logs error if the given splitState is invalid', () => { diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts index dc40a26..bf3a7d6 100644 --- a/src/__tests__/selectorsWithStatus.test.ts +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -11,7 +11,7 @@ import { sdkBrowserConfig } from './utils/sdkConfigs'; import { initSplitSdk, getTreatments, splitSdk } from '../asyncActions'; /** Constants */ -import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE, ERROR_GETSTATUS_NO_INITSPLITSDK } from '../constants'; +import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE } from '../constants'; /** Test targets */ import { @@ -21,15 +21,13 @@ import { describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => { - const logSpy = jest.spyOn(console, 'log'); const errorSpy = jest.spyOn(console, 'error'); beforeEach(() => { - logSpy.mockClear(); errorSpy.mockClear(); }); - it('if Split SDK was not initialized, logs error and returns default treatment and initial status', () => { + it('if Split state is invalid or SDK was not initialized, returns default treatment and initial status', () => { const DEFAULT_TREATMENT = { treatment: 'some_value', config: 'some_config' }; expect(selectTreatmentWithConfigAndStatus({} as any, SPLIT_1, USER_1, DEFAULT_TREATMENT)).toEqual({ @@ -37,15 +35,16 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => ...STATUS_INITIAL, }); expect(errorSpy).toHaveBeenCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + errorSpy.mockClear(); expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'default_value')).toEqual({ treatment: 'default_value', ...STATUS_INITIAL, }); - expect(errorSpy).toHaveBeenCalledWith(ERROR_GETSTATUS_NO_INITSPLITSDK); + expect(errorSpy).not.toHaveBeenCalled(); }); - it('if getTreatments action was not dispatched for the provided feature flag and key, logs error and returns default treatment and client status', () => { + it('if getTreatments action was not dispatched for the provided feature flag and key, returns default treatment and client status', () => { const store = mockStore(STATE_INITIAL); store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); @@ -55,14 +54,12 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => // status of main client: ...STATUS_INITIAL, isReady: true, isOperational: true, }); - expect(logSpy).toHaveBeenCalledWith('[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" '); expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ treatment: 'some_value', // USER_1 client has not been initialized yet: ...STATUS_INITIAL, }); - expect(logSpy).toHaveBeenCalledWith('[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_1" and key "user_1"'); store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); @@ -72,7 +69,8 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => // status of shared client: ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, }); - expect(logSpy).toHaveBeenCalledWith('[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "split_2" and key "user_1"'); + + expect(errorSpy).not.toHaveBeenCalled(); }); it('happy path: returns the treatment value and status of the client', () => { @@ -96,7 +94,6 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => isReadyFromCache: true, isOperational: true, }); - expect(logSpy).not.toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled(); }); diff --git a/src/helpers.ts b/src/helpers.ts index 740afce..1b35fcf 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -109,7 +109,7 @@ export function getStatus(key?: SplitIO.SplitKey): IStatus { if (client) return __getStatus(client); } - // Default status if SDK is not initialized or client is not found + // Default status if SDK is not initialized or client is not found. No warning logs for now, in case the helper is used before actions are dispatched return { isReady: false, isReadyFromCache: false, diff --git a/src/selectors.ts b/src/selectors.ts index 6b4a2c0..65750d9 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -32,25 +32,15 @@ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: s * @param {SplitIO.TreatmentWithConfig} defaultValue */ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): SplitIO.TreatmentWithConfig { - if (!splitState || !splitState.treatments) { - console.error(ERROR_SELECTOR_NO_SPLITSTATE); - return defaultValue; - } + const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); + const treatment = + splitTreatments ? + key ? + splitTreatments[matching(key)] : + Object.values(splitTreatments)[0] : + undefined; - const splitTreatments = splitState.treatments[featureFlagName]; - - const treatment = splitTreatments ? - key ? - splitTreatments[matching(key)] : - Object.values(splitTreatments)[0] : - undefined; - - if (!treatment) { - console.log(`[WARN] Treatment not found by selector. Check you have dispatched a "getTreatments" action for the feature flag "${featureFlagName}" ${key ? `and key "${matching(key)}"` : ''}`); - return defaultValue; - } - - return treatment; + return treatment ? treatment : defaultValue; } /** From 37f4d7188722d19758f066f301e0bfe942051f76 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 24 May 2024 12:12:16 -0300 Subject: [PATCH 14/15] 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 263eacf..5202e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.1", + "version": "1.12.1-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.1", + "version": "1.12.1-rc.2", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.26.0", diff --git a/package.json b/package.json index 39c7f2c..2d97e7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.1", + "version": "1.12.1-rc.2", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", From 82c5091d8229f8844eb25e113284fdc7948f28fb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 24 May 2024 12:16:02 -0300 Subject: [PATCH 15/15] stable version --- 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 5202e4f..76a4c02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.2", + "version": "1.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.2", + "version": "1.13.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.26.0", diff --git a/package.json b/package.json index 2d97e7a..9da3da2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.1-rc.2", + "version": "1.13.0", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js",