Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add selectors that retrieve client readiness status #109

Merged
merged 21 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5143303
Added new version of selectors which returns treatments together with…
EmilianoSanchez May 8, 2024
40d6fe0
Merge branch 'master' into selectors_with_client_status
EmilianoSanchez May 20, 2024
5a41433
README.md update and code comments polishing
EmilianoSanchez May 21, 2024
69bdc3d
Update comment
EmilianoSanchez May 21, 2024
e4601de
Check that initSplitSdk action was dispatched before using new selectors
EmilianoSanchez May 22, 2024
8ab1f2d
Tests
EmilianoSanchez May 22, 2024
7934c65
Reuse const messages
EmilianoSanchez May 22, 2024
05affb4
Merge branch 'selectors_with_client_status' into selectors_with_clien…
EmilianoSanchez May 22, 2024
fbce557
rc
EmilianoSanchez May 22, 2024
d7d522b
Comment updates and Test fixes
EmilianoSanchez May 22, 2024
366683c
Merge branch 'selectors_with_client_status' into selectors_with_clien…
EmilianoSanchez May 23, 2024
c0834fe
Polishing
EmilianoSanchez May 23, 2024
53d41b3
Merge branch 'get_status' into selectors_with_client_status_tests
EmilianoSanchez May 23, 2024
758fc9c
Rename new selectors
EmilianoSanchez May 23, 2024
16b5256
Merge branch 'get_status' into selectors_with_client_status_tests
EmilianoSanchez May 24, 2024
780854f
Fix tests
EmilianoSanchez May 24, 2024
31b805c
rc
EmilianoSanchez May 24, 2024
db4ddd7
Merge branch 'get_status' into selectors_with_client_status_tests
EmilianoSanchez May 24, 2024
d76335c
Rollback selector logs, to avoid noise if they are used before action…
EmilianoSanchez May 24, 2024
37f4d71
rc
EmilianoSanchez May 24, 2024
82c5091
stable version
EmilianoSanchez May 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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/).
Expand Down Expand Up @@ -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.
Expand All @@ -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.

Expand Down Expand Up @@ -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!
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 <div>Loading SDK ...</div>;

// 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 <div>Loading SDK ...</div>;

if (treatment === 'on') {
// return JSX for 'on' treatment
} else if (treatment === 'off') {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
100 changes: 100 additions & 0 deletions src/__tests__/selectorsWithStatus.test.ts
Original file line number Diff line number Diff line change
@@ -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<any>(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<any>(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<any>(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<any>(getTreatments({ splitNames: [SPLIT_1] }));
store.dispatch<any>(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();
});

});
3 changes: 2 additions & 1 deletion src/__tests__/utils/mockStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
2 changes: 1 addition & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
54 changes: 50 additions & 4 deletions src/selectors.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
Expand All @@ -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,
};
}
Loading