diff --git a/CHANGES.txt b/CHANGES.txt
index 926feea..0346c5e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,8 @@
+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:
- Added support for targeting rules based on semantic versions (https://semver.org/).
@@ -55,7 +60,7 @@
1.6.0 (Jul 7, 2022)
- Updated @splitsoftware/splitio dependency to version 10.20.0, which includes:
- - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config sync.enabled . Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used.
+ - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config `sync.enabled`. Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used.
- Updated telemetry logic to track the anonymous config for user consent flag set to declined or unknown.
- Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while its periodic post of impressions is running.
- Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property.
@@ -76,7 +81,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.
@@ -104,13 +109,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/README.md b/README.md
index c0b56a7..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,
- selectTreatmentValue, connectSplit } from '@splitsoftware/splitio-redux'
+ selectTreatmentAndStatus, 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 } = selectTreatmentAndStatus(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/package-lock.json b/package-lock.json
index ffcd7b5..76a4c02 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@splitsoftware/splitio-redux",
- "version": "1.12.0",
+ "version": "1.13.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@splitsoftware/splitio-redux",
- "version": "1.12.0",
+ "version": "1.13.0",
"license": "Apache-2.0",
"dependencies": {
"@splitsoftware/splitio": "10.26.0",
diff --git a/package.json b/package.json
index 83030b7..9da3da2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-redux",
- "version": "1.12.0",
+ "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",
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
index a6ff13b..9641701 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,
+ selectTreatmentAndStatus as exportedSelectTreatmentAndStatus,
+ selectTreatmentWithConfigAndStatus as exportedSelectTreatmentWithConfigAndStatus,
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, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } 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(exportedSelectTreatmentAndStatus).toBe(selectTreatmentAndStatus);
+ expect(exportedSelectTreatmentWithConfigAndStatus).toBe(selectTreatmentWithConfigAndStatus);
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..0583e9b 100644
--- a/src/__tests__/selectors.test.ts
+++ b/src/__tests__/selectors.test.ts
@@ -35,7 +35,7 @@ describe('selectTreatmentValue', () => {
expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1, 'some_value')).toBe('some_value');
});
- it('returns "control" and log error if the given splitState is invalid', () => {
+ it('returns "control" and logs error if the given splitState is invalid', () => {
const errorSpy = jest.spyOn(console, 'error');
expect(selectTreatmentValue((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL);
expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE);
@@ -58,14 +58,14 @@ 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', () => {
+ it('returns "control" and logs error if the given splitState is invalid', () => {
const errorSpy = jest.spyOn(console, 'error');
expect(selectTreatmentWithConfig((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL_WITH_CONFIG);
expect(errorSpy).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..bf3a7d6
--- /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 } from '../constants';
+
+/** Test targets */
+import {
+ selectTreatmentAndStatus,
+ selectTreatmentWithConfigAndStatus
+} from '../selectors';
+
+describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => {
+
+ const errorSpy = jest.spyOn(console, 'error');
+
+ beforeEach(() => {
+ errorSpy.mockClear();
+ });
+
+ 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({
+ treatment: DEFAULT_TREATMENT,
+ ...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).not.toHaveBeenCalled();
+ });
+
+ 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);
+
+ expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1)).toEqual({
+ treatment: CONTROL,
+ // status of main client:
+ ...STATUS_INITIAL, isReady: true, isOperational: true,
+ });
+
+ 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,
+ });
+
+ store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] }));
+ (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE);
+
+ expect(selectTreatmentWithConfigAndStatus(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({
+ treatment: CONTROL_WITH_CONFIG,
+ // status of shared client:
+ ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true,
+ });
+
+ expect(errorSpy).not.toHaveBeenCalled();
+ });
+
+ 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 }));
+ (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(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({
+ treatment: ON,
+ ...STATUS_INITIAL,
+ isReady: true, isOperational: true,
+ });
+
+ 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,
+ });
+
+ expect(errorSpy).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/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/index.ts b/src/index.ts
index 9aeeb44..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 } 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 25ca3d0..65750d9 100644
--- a/src/selectors.ts
+++ b/src/selectors.ts
@@ -1,13 +1,16 @@
-import { ISplitState } from './types';
+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];
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, 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
@@ -19,12 +22,14 @@ export function selectTreatmentValue(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 a treatment object containing its value and configuration.
+ * 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 {
const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE);
@@ -37,3 +42,44 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa
return treatment ? treatment : defaultValue;
}
+
+/**
+ * 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.
+ *
+ * @param {ISplitState} splitState
+ * @param {string} featureFlagName
+ * @param {SplitIO.SplitKey} key
+ * @param {string} defaultValue
+ */
+export function selectTreatmentAndStatus(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): {
+ treatment: string
+} & IStatus {
+ const result: any = selectTreatmentWithConfigAndStatus(splitState, featureFlagName, key, { treatment: defaultValue, config: null });
+ result.treatment = result.treatment.treatment;
+ return result;
+}
+
+/**
+ * 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.
+ *
+ * @param {ISplitState} splitState
+ * @param {string} featureFlagName
+ * @param {SplitIO.SplitKey} key
+ * @param {SplitIO.TreatmentWithConfig} defaultValue
+ */
+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);
+
+ const status = getStatus(key);
+
+ return {
+ ...status,
+ treatment,
+ };
+}