diff --git a/CHANGES.txt b/CHANGES.txt
index 1827849..3be7e1c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,10 @@
+2.6.0 (November 4, 2025)
+ - Added `useTreatment`, `useTreatments`, `useTreatmentWithConfig` and `useTreatmentsWithConfig` hooks to replace the now deprecated `useSplitTreatments` hook.
+ - Updated @splitsoftware/splitio package to version 11.8.0 that includes minor updates:
+ - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs.
+ - Added support for custom loggers: added `logger` configuration option and `factory.Logger.setLogger` method to allow the SDK to use a custom logger.
+ - Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted.
+
2.5.0 (September 18, 2025)
- Updated @splitsoftware/splitio package to version 11.6.0 that includes minor updates:
- Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`.
diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md
index 1faac7d..2236d48 100644
--- a/MIGRATION-GUIDE.md
+++ b/MIGRATION-GUIDE.md
@@ -3,9 +3,32 @@
React SDK v2.0.0 has a few breaking changes that you should consider when migrating from a previous version. The main changes are:
-### • Deprecated `useClient`, `useTreatments`, and `useManager` hooks have been removed.
-Follow [this section](#migrating-to-get-react-sdk-v1100-improvements-replacing-the-deprecated-useclient-usetreatments-and-usemanager-hooks) to migrate to the new hooks `useSplitClient`, `useSplitTreatments`, and `useSplitManager`.
+### • `useTreatments` hook was removed in v2.0.0, but re-introduced in v2.6.0 with a different API:
+
+Since v2.6.0, there are 4 hooks variants to evaluate feature flags, to better cover the different evaluation methods available in the JavaScript SDK client:
+
+- `useTreatment`: returns a treatment value for a given feature flag name. It calls `client.getTreatment()` method under the hood.
+- `useTreatmentWithConfig`: returns a treatment value and its configuration for a given feature flag name. It calls `client.getTreatmentWithConfig()` method under the hood.
+- `useTreatments`: returns an object with treatment values for multiple feature flag names. It calls `client.getTreatments()` or `client.getTreatmentsByFlagSets()` methods under the hood, depending if the `names` or `flagSets` option is provided.
+- `useTreatmentsWithConfig`: returns an object with treatment values and their configurations for multiple feature flag names. It calls `client.getTreatmentsWithConfig()` or `client.getTreatmentsWithConfigByFlagSets()` methods under the hood, depending if the `names` or `flagSets` option is provided.
+
+The `useTreatments` hook from v1.x.x should be replaced with `useTreatmentsWithConfig`, as follows:
+
+```javascript
+// v1.x.x
+const treatments = useTreatments(featureFlagNames, optionalAttributes, optionalSplitKey);
+
+// v2.6.0+
+const { treatments } = useTreatmentsWithConfig({ names: featureFlagNames, attributes: optionalAttributes, splitKey: optionalSplitKey });
+
+// v2.0.0-v2.5.0
+const { treatments } = useSplitTreatments({ names: featureFlagNames, attributes: optionalAttributes, splitKey: optionalSplitKey });
+```
+
+### • Deprecated `useClient` and `useManager` hooks have been removed.
+
+Follow [this section](#migrating-to-get-react-sdk-v1100-improvements-replacing-the-deprecated-useclient-usetreatments-and-usemanager-hooks) to migrate to the new hooks `useSplitClient` and `useSplitManager`.
### • Updated the default value of `updateOnSdkUpdate` and `updateOnSdkTimedout` options to `true`.
@@ -15,7 +38,7 @@ Consider setting the `updateOnSdkUpdate` option to `false` to revert to the prev
The same applies for the equivalent props in the `[with]SplitClient` and `[with]SplitTreatments` components, although these components are deprecated and we recommend [migrating to their hook alternatives](#-high-order-components-withsplitclient-withsplittreatments-and-components-that-accept-a-render-function-as-child-component-splittreatments-and-splitclient-have-been-deprecated-and-might-be-removed-in-a-future-major-release).
-### • Deprecated `SplitFactory` provider has been removed, `withSplitFactory` is deprecated, and `SplitFactoryProvider` doesn't accept `updateOn` props and a render function as children anymore.
+### • Deprecated `SplitFactory` provider has been removed, `withSplitFactory` is deprecated, and `SplitFactoryProvider` doesn't accept a render function as children anymore.
To migrate your existing code to the new version of `SplitFactoryProvider`, consider the following refactor example:
@@ -53,21 +76,21 @@ should be refactored to:
```tsx
const MyComponent = () => {
- const props: ISplitContextValues = useSplitClient({ updateOnSdkUpdate: false });
+ const props: ISplitContextValues = useSplitClient();
const { factory, client, isReady, isReadyFromCache, ... } = props;
...
};
const App = () => {
return (
-
+
);
};
```
-Notice that `MyComponent` was refactored to use the `useSplitClient` hook and is passed as a React JSX element rather than a render function. The `useSplitClient` hook is called without providing a `splitKey` param. This means that the default client (whose key is set in the `core.key` property of the `mySplitConfig` object) will be used, and the `updateOnSdkUpdate` and `attributes` props are passed as options to the hook.
+Notice that `MyComponent` was refactored to use the `useSplitClient` hook and is passed as a React JSX element rather than a render function. The `useSplitClient` hook is called without providing a `splitKey` param. This means that the default client (whose key is set in the `core.key` property of the `mySplitConfig` object) will be used.
### • High-Order-Components (`withSplitClient`, `withSplitTreatments`) and components that accept a render function as child component (`SplitTreatments`, and `SplitClient`) have been deprecated and might be removed in a future major release.
diff --git a/README.md b/README.md
index 30aec4c..987df64 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Below is a simple example that describes the instantiation and most basic usage
import React from 'react';
// Import SDK functions
-import { SplitFactoryProvider, useSplitTreatments } from '@splitsoftware/splitio-react';
+import { SplitFactoryProvider, useTreatment } from '@splitsoftware/splitio-react';
// Define your config object
const CONFIG = {
@@ -29,18 +29,18 @@ const CONFIG = {
};
function MyComponent() {
- // Evaluate feature flags with useSplitTreatments hook
- const { treatments: { FEATURE_FLAG_NAME }, isReady } = useSplitTreatments({ names: ['FEATURE_FLAG_NAME'] });
+ // Evaluate a feature flag with useTreatment hook
+ const { treatment, isReady } = useTreatment({ name: 'FEATURE_FLAG_NAME' });
// Check SDK readiness using isReady prop
if (!isReady) return Loading SDK ...
;
- if (FEATURE_FLAG_NAME.treatment === 'on') {
- // return JSX for on treatment
- } else if (FEATURE_FLAG_NAME.treatment === 'off') {
- // return JSX for off treatment
+ if (treatment === 'on') {
+ // return JSX for 'on' treatment
+ } else if (treatment === 'off') {
+ // return JSX for 'off' treatment
} else {
- // return JSX for control treatment
+ // return JSX for 'control' treatment
};
}
diff --git a/package-lock.json b/package-lock.json
index d2c6e9f..d27eccf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
{
"name": "@splitsoftware/splitio-react",
- "version": "2.5.0",
+ "version": "2.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@splitsoftware/splitio-react",
- "version": "2.5.0",
+ "version": "2.6.0",
"license": "Apache-2.0",
"dependencies": {
- "@splitsoftware/splitio": "11.6.0",
+ "@splitsoftware/splitio": "11.8.0",
"memoize-one": "^5.1.1",
"shallowequal": "^1.1.0",
"tslib": "^2.3.1"
@@ -778,9 +778,9 @@
"dev": true
},
"node_modules/@ioredis/commands": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
- "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
+ "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
"license": "MIT"
},
"node_modules/@istanbuljs/load-nyc-config": {
@@ -1583,12 +1583,12 @@
}
},
"node_modules/@splitsoftware/splitio": {
- "version": "11.6.0",
- "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.0.tgz",
- "integrity": "sha512-48sksG00073Nltma/BxpH6xHVZmoBjank40EU4h+XqrMGm0qM3jGngPO9R/iWAHdSduUWAoMJVJYA68AtvKgeQ==",
+ "version": "11.8.0",
+ "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.8.0.tgz",
+ "integrity": "sha512-M9ENeH+IEmxwELeCdXgnTbLg+ZP3SRUMM6lImSbv7mD32u1v6ihnUhnhsTJzlQWMDC4H94EAW345q1cO7ovlTQ==",
"license": "Apache-2.0",
"dependencies": {
- "@splitsoftware/splitio-commons": "2.6.0",
+ "@splitsoftware/splitio-commons": "2.8.0",
"bloom-filters": "^3.0.4",
"ioredis": "^4.28.0",
"js-yaml": "^3.13.1",
@@ -1601,9 +1601,9 @@
}
},
"node_modules/@splitsoftware/splitio-commons": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.0.tgz",
- "integrity": "sha512-0xODXLciIvHSuMlb8eukIB2epb3ZyGOsrwS0cMuTdxEvCqr7Nuc9pWDdJtRuN1UwL/jIjBnpDYAc8s6mpqLX2g==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz",
+ "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==",
"license": "Apache-2.0",
"dependencies": {
"@types/ioredis": "^4.28.0",
@@ -11463,9 +11463,9 @@
"dev": true
},
"@ioredis/commands": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
- "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
+ "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow=="
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@@ -12095,11 +12095,11 @@
}
},
"@splitsoftware/splitio": {
- "version": "11.6.0",
- "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.0.tgz",
- "integrity": "sha512-48sksG00073Nltma/BxpH6xHVZmoBjank40EU4h+XqrMGm0qM3jGngPO9R/iWAHdSduUWAoMJVJYA68AtvKgeQ==",
+ "version": "11.8.0",
+ "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.8.0.tgz",
+ "integrity": "sha512-M9ENeH+IEmxwELeCdXgnTbLg+ZP3SRUMM6lImSbv7mD32u1v6ihnUhnhsTJzlQWMDC4H94EAW345q1cO7ovlTQ==",
"requires": {
- "@splitsoftware/splitio-commons": "2.6.0",
+ "@splitsoftware/splitio-commons": "2.8.0",
"bloom-filters": "^3.0.4",
"ioredis": "^4.28.0",
"js-yaml": "^3.13.1",
@@ -12109,9 +12109,9 @@
}
},
"@splitsoftware/splitio-commons": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.0.tgz",
- "integrity": "sha512-0xODXLciIvHSuMlb8eukIB2epb3ZyGOsrwS0cMuTdxEvCqr7Nuc9pWDdJtRuN1UwL/jIjBnpDYAc8s6mpqLX2g==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz",
+ "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==",
"requires": {
"@types/ioredis": "^4.28.0",
"tslib": "^2.3.1"
diff --git a/package.json b/package.json
index e168d84..86da7e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-react",
- "version": "2.5.0",
+ "version": "2.6.0",
"description": "A React library to easily integrate and use Split JS SDK",
"main": "cjs/index.js",
"module": "esm/index.js",
@@ -63,7 +63,7 @@
},
"homepage": "https://github.com/splitio/react-client#readme",
"dependencies": {
- "@splitsoftware/splitio": "11.6.0",
+ "@splitsoftware/splitio": "11.8.0",
"memoize-one": "^5.1.1",
"shallowequal": "^1.1.0",
"tslib": "^2.3.1"
diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx
index fd146ae..e94984d 100644
--- a/src/SplitClient.tsx
+++ b/src/SplitClient.tsx
@@ -9,8 +9,6 @@ import { useSplitClient } from './useSplitClient';
*
* The underlying SDK client can be changed during the component lifecycle
* if the component is updated with a different splitKey prop.
- *
- * @deprecated `SplitClient` will be removed in a future major release. We recommend replacing it with the `useSplitClient` hook.
*/
export function SplitClient(props: ISplitClientProps) {
const { children } = props;
diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx
index 05bbbca..e76830c 100644
--- a/src/SplitFactoryProvider.tsx
+++ b/src/SplitFactoryProvider.tsx
@@ -51,7 +51,7 @@ export function SplitFactoryProvider(props: ISplitFactoryProviderProps) {
// Effect to initialize and destroy the factory when config is provided
React.useEffect(() => {
if (propFactory) {
- if (config) console.log(WARN_SF_CONFIG_AND_FACTORY);
+ if (config) (propFactory.settings as any).log.warn(WARN_SF_CONFIG_AND_FACTORY);
return;
}
diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx
index 7f52776..e895f72 100644
--- a/src/SplitTreatments.tsx
+++ b/src/SplitTreatments.tsx
@@ -9,7 +9,7 @@ import { useSplitTreatments } from './useSplitTreatments';
* call the 'client.getTreatmentsWithConfig()' method if the `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method
* if the `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function.
*
- * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook.
+ * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
*/
export function SplitTreatments(props: ISplitTreatmentsProps) {
const { children } = props;
diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx
index 2d1bf1b..a94aeb0 100644
--- a/src/__tests__/SplitClient.test.tsx
+++ b/src/__tests__/SplitClient.test.tsx
@@ -56,6 +56,7 @@ describe('SplitClient', () => {
client: outerFactory.client(),
isReady: true,
isReadyFromCache: true,
+ isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate
});
@@ -141,7 +142,7 @@ describe('SplitClient', () => {
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
- expect(statusProps).toStrictEqual([true, false, true, false]);
+ expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
@@ -182,7 +183,7 @@ describe('SplitClient', () => {
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Ready
- expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
+ expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
break;
default:
fail('Child must not be rerendered');
@@ -214,7 +215,7 @@ describe('SplitClient', () => {
count++;
// side effect in the render phase
- if (!(client as any).__getStatus().isReady) {
+ if (!client!.getStatus().isReady) {
(client as any).__emitter__.emit(Event.SDK_READY);
}
@@ -318,11 +319,11 @@ describe('SplitClient', () => {
break;
case 4:
expect(client).toBe(outerFactory.client('user3'));
- expect(statusProps).toStrictEqual([true, false, false, false]);
+ expect(statusProps).toStrictEqual([true, true, false, false]);
break;
case 5:
expect(client).toBe(outerFactory.client('user3'));
- expect(statusProps).toStrictEqual([true, false, false, false]);
+ expect(statusProps).toStrictEqual([true, true, false, false]);
break;
default:
fail('Child must not be rerendered');
@@ -501,7 +502,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
- expect(statusProps).toStrictEqual([true, false, true, false]);
+ expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
@@ -542,7 +543,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
expect(statusProps).toStrictEqual([false, false, true, true]);
break;
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
- expect(statusProps).toStrictEqual([true, false, true, false]);
+ expect(statusProps).toStrictEqual([true, true, true, false]);
break;
default:
fail('Child must not be rerendered');
@@ -578,7 +579,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Ready
- expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
+ expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
break;
default:
fail('Child must not be rerendered');
@@ -615,7 +616,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
expect(statusProps).toStrictEqual([false, false, false, false]);
break;
case 1: // Ready
- expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
+ expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
break;
default:
fail('Child must not be rerendered');
diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx
index 33f0977..86fd386 100644
--- a/src/__tests__/SplitFactoryProvider.test.tsx
+++ b/src/__tests__/SplitFactoryProvider.test.tsx
@@ -70,6 +70,7 @@ describe('SplitFactoryProvider', () => {
client: outerFactory.client(),
isReady: true,
isReadyFromCache: true,
+ isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate
});
return null;
@@ -113,7 +114,7 @@ describe('SplitFactoryProvider', () => {
);
- expect(logSpy).toBeCalledWith(WARN_SF_CONFIG_AND_FACTORY);
+ expect(logSpy).toBeCalledWith('[WARN] splitio => ' + WARN_SF_CONFIG_AND_FACTORY);
logSpy.mockRestore();
});
diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx
index cf21ea4..243ddb6 100644
--- a/src/__tests__/SplitTreatments.test.tsx
+++ b/src/__tests__/SplitTreatments.test.tsx
@@ -19,15 +19,11 @@ import { SplitClient } from '../SplitClient';
import { SplitFactoryProvider } from '../SplitFactoryProvider';
import { useSplitTreatments } from '../useSplitTreatments';
-const logSpy = jest.spyOn(console, 'log');
-
describe('SplitTreatments', () => {
const featureFlagNames = ['split1', 'split2'];
const flagSets = ['set1', 'set2'];
- afterEach(() => { logSpy.mockClear() });
-
it('passes control treatments (empty object if flagSets is provided) if the SDK is not operational.', () => {
render(
@@ -73,7 +69,7 @@ describe('SplitTreatments', () => {
expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1);
expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value);
expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]);
- expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, getStatus(outerFactory.client()).lastUpdate]);
+ expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, true, false, false, false, getStatus(outerFactory.client()).lastUpdate]);
return null;
}}
@@ -105,10 +101,9 @@ describe('SplitTreatments', () => {
});
/**
- * Input validation. Passing invalid feature flag names or attributes while the Sdk
- * is not ready doesn't emit errors, and logs meaningful messages instead.
+ * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready.
*/
- it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => {
+ it('Input validation: invalid names are sanitized.', () => {
render(
@@ -130,9 +125,9 @@ describe('SplitTreatments', () => {
}}
{/* @ts-expect-error Test error handling */}
-
+
{({ treatments }: ISplitTreatmentsChildProps) => {
- expect(treatments).toEqual({});
+ expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG });
return null;
}}
@@ -142,14 +137,9 @@ describe('SplitTreatments', () => {
);
- expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.');
- expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.');
-
- done();
});
-
- test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => {
+ test('ignores flagSets if both names and flagSets params are provided.', () => {
render(
{/* @ts-expect-error flagSets and names are mutually exclusive */}
@@ -161,8 +151,6 @@ describe('SplitTreatments', () => {
);
-
- expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.');
});
test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => {
@@ -190,7 +178,7 @@ describe('SplitTreatments', () => {
act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes, undefined);
- expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments);
+ expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments!);
});
});
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
index a455af6..d63b749 100644
--- a/src/__tests__/index.test.ts
+++ b/src/__tests__/index.test.ts
@@ -12,6 +12,10 @@ import {
useSplitClient as exportedUseSplitClient,
useSplitTreatments as exportedUseSplitTreatments,
useSplitManager as exportedUseSplitManager,
+ useTreatment as exportedUseTreatment,
+ useTreatmentWithConfig as exportedUseTreatmentWithConfig,
+ useTreatments as exportedUseTreatments,
+ useTreatmentsWithConfig as exportedUseTreatmentsWithConfig,
// Checks that types are exported. Otherwise, the test would fail with a TS error.
GetTreatmentsOptions,
ISplitClientChildProps,
@@ -39,6 +43,10 @@ import { useTrack } from '../useTrack';
import { useSplitClient } from '../useSplitClient';
import { useSplitTreatments } from '../useSplitTreatments';
import { useSplitManager } from '../useSplitManager';
+import { useTreatment } from '../useTreatment';
+import { useTreatmentWithConfig } from '../useTreatmentWithConfig';
+import { useTreatments } from '../useTreatments';
+import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig';
describe('index', () => {
@@ -59,6 +67,10 @@ describe('index', () => {
expect(exportedUseSplitClient).toBe(useSplitClient);
expect(exportedUseSplitTreatments).toBe(useSplitTreatments);
expect(exportedUseSplitManager).toBe(useSplitManager);
+ expect(exportedUseTreatment).toBe(useTreatment);
+ expect(exportedUseTreatmentWithConfig).toBe(useTreatmentWithConfig);
+ expect(exportedUseTreatments).toBe(useTreatments);
+ expect(exportedUseTreatmentsWithConfig).toBe(useTreatmentsWithConfig);
});
it('should export SplitContext', () => {
diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts
index 09dca47..a6f5ddb 100644
--- a/src/__tests__/testUtils/mockSplitFactory.ts
+++ b/src/__tests__/testUtils/mockSplitFactory.ts
@@ -1,6 +1,7 @@
import { EventEmitter } from 'events';
import jsSdkPackageJson from '@splitsoftware/splitio/package.json';
import reactSdkPackageJson from '../../../package.json';
+import { CONTROL, CONTROL_WITH_CONFIG } from '../../constants';
export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`;
export const reactSdkVersion = `react-${reactSdkPackageJson.version}`;
@@ -12,6 +13,13 @@ export const Event = {
SDK_UPDATE: 'state::update',
};
+const DEFAULT_LOGGER: SplitIO.Logger = {
+ error(msg) { console.log('[ERROR] splitio => ' + msg); },
+ warn(msg) { console.log('[WARN] splitio => ' + msg); },
+ info(msg) { console.log('[INFO] splitio => ' + msg); },
+ debug(msg) { console.log('[DEBUG] splitio => ' + msg); },
+};
+
function parseKey(key: SplitIO.SplitKey): SplitIO.SplitKey {
if (key && typeof key === 'object' && key.constructor === Object) {
return {
@@ -47,7 +55,7 @@ export function mockSdk() {
}
const __emitter__ = new EventEmitter();
- __emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); });
+ __emitter__.on(Event.SDK_READY, () => { isReady = true; isReadyFromCache = true; syncLastUpdate(); });
__emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); });
__emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); });
__emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); });
@@ -58,6 +66,24 @@ export function mockSdk() {
const track: jest.Mock = jest.fn(() => {
return true;
});
+ const getTreatment: jest.Mock = jest.fn((featureFlagName: string) => {
+ return typeof featureFlagName === 'string' ? 'on' : CONTROL;
+ });
+ const getTreatments: jest.Mock = jest.fn((featureFlagNames: string[]) => {
+ return featureFlagNames.reduce((result: SplitIO.Treatments, featureName: string) => {
+ result[featureName] = 'on';
+ return result;
+ }, {});
+ });
+ const getTreatmentsByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => {
+ return flagSets.reduce((result: SplitIO.Treatments, flagSet: string) => {
+ result[flagSet + '_feature_flag'] = 'on';
+ return result;
+ }, {});
+ });
+ const getTreatmentWithConfig: jest.Mock = jest.fn((featureFlagName: string) => {
+ return typeof featureFlagName === 'string' ? { treatment: 'on', config: null } : CONTROL_WITH_CONFIG;
+ });
const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => {
return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => {
result[featureName] = { treatment: 'on', config: null };
@@ -89,13 +115,13 @@ export function mockSdk() {
else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); }
});
});
- const __getStatus = () => ({
+ const getStatus = () => ({
isReady,
isReadyFromCache,
isTimedout: hasTimedout && !isReady,
hasTimedout,
isDestroyed,
- isOperational: (isReady || isReadyFromCache) && !isDestroyed,
+ isOperational: isReadyFromCache && !isDestroyed,
lastUpdate,
});
const destroy: jest.Mock = jest.fn(() => {
@@ -106,6 +132,10 @@ export function mockSdk() {
});
return Object.assign(Object.create(__emitter__), {
+ getTreatment,
+ getTreatments,
+ getTreatmentsByFlagSets,
+ getTreatmentWithConfig,
getTreatmentsWithConfig,
getTreatmentsWithConfigByFlagSets,
track,
@@ -115,10 +145,9 @@ export function mockSdk() {
setAttributes,
clearAttributes,
getAttributes,
+ getStatus,
// EventEmitter exposed to trigger events manually
__emitter__,
- // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed)
- __getStatus,
// Restore the mock client to its initial NO-READY status.
// Useful when you want to reuse the same mock between tests after emitting events or destroying the instance.
__restore() {
@@ -154,6 +183,7 @@ export function mockSdk() {
__clients__,
settings: Object.assign({
version: jsSdkVersion,
+ log: DEFAULT_LOGGER
}, config),
};
diff --git a/src/__tests__/testUtils/sdkConfigs.ts b/src/__tests__/testUtils/sdkConfigs.ts
index 0ec97f5..b3397e6 100644
--- a/src/__tests__/testUtils/sdkConfigs.ts
+++ b/src/__tests__/testUtils/sdkConfigs.ts
@@ -4,3 +4,11 @@ export const sdkBrowser: SplitIO.IBrowserSettings = {
key: 'customer-key',
},
};
+
+export const sdkBrowserWithConfig: SplitIO.IBrowserSettings = {
+ ...sdkBrowser,
+ fallbackTreatments: {
+ global: 'control_global',
+ byFlag: { ff1: { treatment: 'control_ff1', config: 'control_ff1_config' } }
+ }
+};
diff --git a/src/__tests__/testUtils/utils.tsx b/src/__tests__/testUtils/utils.tsx
index 7571873..9c05ec5 100644
--- a/src/__tests__/testUtils/utils.tsx
+++ b/src/__tests__/testUtils/utils.tsx
@@ -123,6 +123,7 @@ export const INITIAL_STATUS: ISplitStatus & IUpdateProps = {
hasTimedout: false,
lastUpdate: 0,
isDestroyed: false,
+ isOperational: false,
updateOnSdkReady: true,
updateOnSdkReadyFromCache: true,
updateOnSdkTimedout: true,
diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx
index ea56ad9..5e4bb03 100644
--- a/src/__tests__/useSplitClient.test.tsx
+++ b/src/__tests__/useSplitClient.test.tsx
@@ -207,7 +207,7 @@ describe('useSplitClient', () => {
// side effect in the render phase
const client = outerFactory.client('some_user') as any;
- if (!client.__getStatus().isReady) client.__emitter__.emit(Event.SDK_READY);
+ if (!client.getStatus().isReady) client.__emitter__.emit(Event.SDK_READY);
return null;
})}
@@ -256,7 +256,7 @@ describe('useSplitClient', () => {
act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render
expect(rendersCount).toBe(5);
- expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: false, hasTimedout: true });
+ expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: true, hasTimedout: true });
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false
expect(rendersCount).toBe(5);
diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx
index 9707042..f8aff53 100644
--- a/src/__tests__/useSplitManager.test.tsx
+++ b/src/__tests__/useSplitManager.test.tsx
@@ -48,8 +48,9 @@ describe('useSplitManager', () => {
hasTimedout: false,
isDestroyed: false,
isReady: true,
- isReadyFromCache: false,
+ isReadyFromCache: true,
isTimedout: false,
+ isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate,
});
});
@@ -98,8 +99,9 @@ describe('useSplitManager', () => {
hasTimedout: false,
isDestroyed: false,
isReady: true,
- isReadyFromCache: false,
+ isReadyFromCache: true,
isTimedout: false,
+ isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate,
});
});
diff --git a/src/__tests__/useTreatment.test.tsx b/src/__tests__/useTreatment.test.tsx
new file mode 100644
index 0000000..69bde09
--- /dev/null
+++ b/src/__tests__/useTreatment.test.tsx
@@ -0,0 +1,186 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+
+/** Mocks */
+import { mockSdk, Event } from './testUtils/mockSplitFactory';
+jest.mock('@splitsoftware/splitio/client', () => {
+ return { SplitFactory: mockSdk() };
+});
+import { SplitFactory } from '@splitsoftware/splitio/client';
+import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
+import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
+
+/** Test target */
+import { SplitFactoryProvider } from '../SplitFactoryProvider';
+import { useTreatment } from '../useTreatment';
+import { SplitContext } from '../SplitContext';
+import { IUseTreatmentResult } from '../types';
+
+describe('useTreatment', () => {
+
+ const featureFlagName = 'split1';
+ const attributes = { att1: 'att1' };
+ const properties = { prop1: 'prop1' };
+
+ test('returns the treatment evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client();
+ let treatment: SplitIO.Treatment;
+
+ render(
+
+ {React.createElement(() => {
+ treatment = useTreatment({ name: featureFlagName, attributes, properties }).treatment;
+ return null;
+ })}
+
+ );
+
+ // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatment` method
+ expect(client.getTreatment).not.toBeCalled();
+ expect(treatment!).toEqual(CONTROL);
+
+ // once operational (SDK_READY), it evaluates feature flags
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+
+ expect(client.getTreatment).toBeCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatment).toHaveReturnedWith(treatment!);
+ });
+
+ test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client('user2');
+ let renderTimes = 0;
+
+ render(
+
+ {React.createElement(() => {
+ const treatment = useTreatment({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment;
+
+ renderTimes++;
+ switch (renderTimes) {
+ case 1:
+ // returns control if not operational (SDK not ready), without calling `getTreatment` method
+ expect(client.getTreatment).not.toBeCalled();
+ expect(treatment).toEqual(CONTROL);
+ break;
+ case 2:
+ case 3:
+ // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
+ expect(client.getTreatment).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatment).toHaveLastReturnedWith(treatment);
+ break;
+ default:
+ throw new Error('Unexpected render');
+ }
+
+ return null;
+ })}
+
+ );
+
+ act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+ act(() => client.__emitter__.emit(Event.SDK_UPDATE));
+ expect(client.getTreatment).toBeCalledTimes(2);
+ });
+
+ test('throws error if invoked outside of SplitFactoryProvider.', () => {
+ expect(() => {
+ render(
+ React.createElement(() => {
+ useTreatment({ name: featureFlagName, attributes }).treatment;
+ return null;
+ })
+ );
+ }).toThrow(EXCEPTION_NO_SFP);
+ });
+
+ test('must update on SDK events', async () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const mainClient = outerFactory.client() as any;
+ const user2Client = outerFactory.client('user_2') as any;
+
+ let countSplitContext = 0, countUseTreatment = 0, countUseTreatmentUser2 = 0, countUseTreatmentUser2WithoutUpdate = 0;
+ const lastUpdateSetUser2 = new Set();
+ const lastUpdateSetUser2WithUpdate = new Set();
+
+ function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentResult) {
+ if (isReady || isReadyFromCache) {
+ expect(treatment).toEqual('on')
+ } else {
+ expect(treatment).toEqual('control')
+ }
+ }
+
+ render(
+
+ <>
+
+ {() => countSplitContext++}
+
+ {React.createElement(() => {
+ const context = useTreatment({ name: 'split_test', attributes: { att1: 'att1' } });
+ expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
+ validateTreatment(context);
+ countUseTreatment++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatment({ name: 'split_test', splitKey: 'user_2' });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2.add(context.lastUpdate);
+ countUseTreatmentUser2++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatment({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
+ countUseTreatmentUser2WithoutUpdate++;
+ return null;
+ })}
+ >
+
+ );
+
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY));
+ act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY));
+ act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
+
+ // SplitFactoryProvider renders once
+ expect(countSplitContext).toEqual(1);
+
+ // If useTreatment evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatment).toEqual(4);
+ expect(mainClient.getTreatment).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
+ expect(mainClient.getTreatment).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined);
+
+ // If useTreatment evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentUser2).toEqual(4);
+ expect(lastUpdateSetUser2.size).toEqual(4);
+ // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
+ expect(countUseTreatmentUser2WithoutUpdate).toEqual(3);
+ expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
+ expect(user2Client.getTreatment).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
+ expect(user2Client.getTreatment).toHaveBeenLastCalledWith('split_test', undefined, undefined);
+ });
+
+ test('returns fallback treatment if the client is not operational', () => {
+ render(
+
+ {React.createElement(() => {
+ expect(useTreatment({ name: featureFlagName, attributes, properties }).treatment).toEqual('control_global');
+ expect(useTreatment({ name: 'ff1', attributes, properties }).treatment).toEqual('control_ff1');
+ return null;
+ })}
+
+ );
+ });
+
+});
diff --git a/src/__tests__/useTreatmentWithConfig.test.tsx b/src/__tests__/useTreatmentWithConfig.test.tsx
new file mode 100644
index 0000000..f74ac42
--- /dev/null
+++ b/src/__tests__/useTreatmentWithConfig.test.tsx
@@ -0,0 +1,186 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+
+/** Mocks */
+import { mockSdk, Event } from './testUtils/mockSplitFactory';
+jest.mock('@splitsoftware/splitio/client', () => {
+ return { SplitFactory: mockSdk() };
+});
+import { SplitFactory } from '@splitsoftware/splitio/client';
+import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
+import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
+
+/** Test target */
+import { SplitFactoryProvider } from '../SplitFactoryProvider';
+import { useTreatmentWithConfig } from '../useTreatmentWithConfig';
+import { SplitContext } from '../SplitContext';
+import { IUseTreatmentWithConfigResult } from '../types';
+
+describe('useTreatmentWithConfig', () => {
+
+ const featureFlagName = 'split1';
+ const attributes = { att1: 'att1' };
+ const properties = { prop1: 'prop1' };
+
+ test('returns the treatment evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client();
+ let treatment: SplitIO.TreatmentWithConfig;
+
+ render(
+
+ {React.createElement(() => {
+ treatment = useTreatmentWithConfig({ name: featureFlagName, attributes, properties }).treatment;
+ return null;
+ })}
+
+ );
+
+ // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentWithConfig` method
+ expect(client.getTreatmentWithConfig).not.toBeCalled();
+ expect(treatment!).toEqual(CONTROL_WITH_CONFIG);
+
+ // once operational (SDK_READY), it evaluates feature flags
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+
+ expect(client.getTreatmentWithConfig).toBeCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatmentWithConfig).toHaveReturnedWith(treatment!);
+ });
+
+ test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client('user2');
+ let renderTimes = 0;
+
+ render(
+
+ {React.createElement(() => {
+ const treatment = useTreatmentWithConfig({ name: featureFlagName, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatment;
+
+ renderTimes++;
+ switch (renderTimes) {
+ case 1:
+ // returns control if not operational (SDK not ready), without calling `getTreatmentWithConfig` method
+ expect(client.getTreatmentWithConfig).not.toBeCalled();
+ expect(treatment).toEqual(CONTROL_WITH_CONFIG);
+ break;
+ case 2:
+ case 3:
+ // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
+ expect(client.getTreatmentWithConfig).toHaveBeenLastCalledWith(featureFlagName, attributes, { properties });
+ expect(client.getTreatmentWithConfig).toHaveLastReturnedWith(treatment);
+ break;
+ default:
+ throw new Error('Unexpected render');
+ }
+
+ return null;
+ })}
+
+ );
+
+ act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+ act(() => client.__emitter__.emit(Event.SDK_UPDATE));
+ expect(client.getTreatmentWithConfig).toBeCalledTimes(2);
+ });
+
+ test('throws error if invoked outside of SplitFactoryProvider.', () => {
+ expect(() => {
+ render(
+ React.createElement(() => {
+ useTreatmentWithConfig({ name: featureFlagName, attributes }).treatment;
+ return null;
+ })
+ );
+ }).toThrow(EXCEPTION_NO_SFP);
+ });
+
+ test('must update on SDK events', async () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const mainClient = outerFactory.client() as any;
+ const user2Client = outerFactory.client('user_2') as any;
+
+ let countSplitContext = 0, countUseTreatmentWithConfig = 0, countUseTreatmentWithConfigUser2 = 0, countUseTreatmentWithConfigUser2WithoutUpdate = 0;
+ const lastUpdateSetUser2 = new Set();
+ const lastUpdateSetUser2WithUpdate = new Set();
+
+ function validateTreatment({ treatment, isReady, isReadyFromCache }: IUseTreatmentWithConfigResult) {
+ if (isReady || isReadyFromCache) {
+ expect(treatment).toEqual({ treatment: 'on', config: null })
+ } else {
+ expect(treatment).toEqual({ treatment: 'control', config: null })
+ }
+ }
+
+ render(
+
+ <>
+
+ {() => countSplitContext++}
+
+ {React.createElement(() => {
+ const context = useTreatmentWithConfig({ name: 'split_test', attributes: { att1: 'att1' } });
+ expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
+ validateTreatment(context);
+ countUseTreatmentWithConfig++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatmentWithConfig({ name: 'split_test', splitKey: 'user_2' });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2.add(context.lastUpdate);
+ countUseTreatmentWithConfigUser2++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatmentWithConfig({ name: 'split_test', splitKey: 'user_2', updateOnSdkUpdate: false });
+ expect(context.client).toBe(user2Client);
+ validateTreatment(context);
+ lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
+ countUseTreatmentWithConfigUser2WithoutUpdate++;
+ return null;
+ })}
+ >
+
+ );
+
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY));
+ act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY));
+ act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
+
+ // SplitFactoryProvider renders once
+ expect(countSplitContext).toEqual(1);
+
+ // If useTreatmentWithConfig evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatmentWithConfig).toEqual(4);
+ expect(mainClient.getTreatmentWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
+ expect(mainClient.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', { att1: 'att1' }, undefined);
+
+ // If useTreatmentWithConfig evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentWithConfigUser2).toEqual(4);
+ expect(lastUpdateSetUser2.size).toEqual(4);
+ // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
+ expect(countUseTreatmentWithConfigUser2WithoutUpdate).toEqual(3);
+ expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
+ expect(user2Client.getTreatmentWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
+ expect(user2Client.getTreatmentWithConfig).toHaveBeenLastCalledWith('split_test', undefined, undefined);
+ });
+
+ test('returns fallback treatment if the client is not operational', () => {
+ render(
+
+ {React.createElement(() => {
+ expect(useTreatmentWithConfig({ name: featureFlagName, attributes, properties }).treatment).toEqual({ treatment: 'control_global', config: null });
+ expect(useTreatmentWithConfig({ name: 'ff1', attributes, properties }).treatment).toEqual({ treatment: 'control_ff1', config: 'control_ff1_config' });
+ return null;
+ })}
+
+ );
+ });
+
+});
diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx
new file mode 100644
index 0000000..98712b4
--- /dev/null
+++ b/src/__tests__/useTreatments.test.tsx
@@ -0,0 +1,241 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+
+/** Mocks */
+import { mockSdk, Event } from './testUtils/mockSplitFactory';
+jest.mock('@splitsoftware/splitio/client', () => {
+ return { SplitFactory: mockSdk() };
+});
+import { SplitFactory } from '@splitsoftware/splitio/client';
+import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
+import { CONTROL, EXCEPTION_NO_SFP } from '../constants';
+
+/** Test target */
+import { SplitFactoryProvider } from '../SplitFactoryProvider';
+import { useTreatments } from '../useTreatments';
+import { SplitContext } from '../SplitContext';
+import { IUseTreatmentsResult } from '../types';
+
+describe('useTreatments', () => {
+
+ const featureFlagNames = ['split1'];
+ const flagSets = ['set1'];
+ const attributes = { att1: 'att1' };
+ const properties = { prop1: 'prop1' };
+
+ test('returns the treatments evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client();
+ let treatments: SplitIO.Treatments;
+ let treatmentsByFlagSets: SplitIO.Treatments;
+
+ render(
+
+ {React.createElement(() => {
+ treatments = useTreatments({ names: featureFlagNames, attributes, properties }).treatments;
+ treatmentsByFlagSets = useTreatments({ flagSets, attributes, properties }).treatments;
+
+ // @ts-expect-error Options object must provide either names or flagSets
+ expect(useTreatments({}).treatments).toEqual({});
+ return null;
+ })}
+
+ );
+
+ // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatments` method
+ expect(client.getTreatments).not.toBeCalled();
+ expect(treatments!).toEqual({ split1: CONTROL });
+
+ // returns empty treatments object if not operational, without calling `getTreatmentsByFlagSets` method
+ expect(client.getTreatmentsByFlagSets).not.toBeCalled();
+ expect(treatmentsByFlagSets!).toEqual({});
+
+ // once operational (SDK_READY), it evaluates feature flags
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+
+ expect(client.getTreatments).toBeCalledWith(featureFlagNames, attributes, { properties });
+ expect(client.getTreatments).toHaveReturnedWith(treatments!);
+
+ expect(client.getTreatmentsByFlagSets).toBeCalledWith(flagSets, attributes, { properties });
+ expect(client.getTreatmentsByFlagSets).toHaveReturnedWith(treatmentsByFlagSets!);
+ });
+
+ test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const client: any = outerFactory.client('user2');
+ let renderTimes = 0;
+
+ render(
+
+ {React.createElement(() => {
+ const treatments = useTreatments({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments;
+
+ renderTimes++;
+ switch (renderTimes) {
+ case 1:
+ // returns control if not operational (SDK not ready), without calling `getTreatments` method
+ expect(client.getTreatments).not.toBeCalled();
+ expect(treatments).toEqual({ split1: CONTROL });
+ break;
+ case 2:
+ case 3:
+ // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags
+ expect(client.getTreatments).toHaveBeenLastCalledWith(featureFlagNames, attributes, { properties });
+ expect(client.getTreatments).toHaveLastReturnedWith(treatments);
+ break;
+ default:
+ throw new Error('Unexpected render');
+ }
+
+ return null;
+ })}
+
+ );
+
+ act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => client.__emitter__.emit(Event.SDK_READY));
+ act(() => client.__emitter__.emit(Event.SDK_UPDATE));
+ expect(client.getTreatments).toBeCalledTimes(2);
+ });
+
+ test('throws error if invoked outside of SplitFactoryProvider.', () => {
+ expect(() => {
+ render(
+ React.createElement(() => {
+ useTreatments({ names: featureFlagNames, attributes }).treatments;
+ useTreatments({ flagSets: featureFlagNames }).treatments;
+ return null;
+ })
+ );
+ }).toThrow(EXCEPTION_NO_SFP);
+ });
+
+ /**
+ * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready.
+ */
+ test('Input validation: invalid names are sanitized.', () => {
+ render(
+
+ {
+ React.createElement(() => {
+ // @ts-expect-error Test error handling
+ let treatments = useTreatments('split1').treatments;
+ expect(treatments).toEqual({});
+ // @ts-expect-error Test error handling
+ treatments = useTreatments({ names: [true, ' flag_1 ', ' '] }).treatments;
+ expect(treatments).toEqual({ flag_1: CONTROL });
+
+ return null;
+ })
+ }
+
+ );
+ });
+
+ test('must update on SDK events', async () => {
+ const outerFactory = SplitFactory(sdkBrowser);
+ const mainClient = outerFactory.client() as any;
+ const user2Client = outerFactory.client('user_2') as any;
+
+ let countSplitContext = 0, countUseTreatments = 0, countUseTreatmentsUser2 = 0, countUseTreatmentsUser2WithoutUpdate = 0;
+ const lastUpdateSetUser2 = new Set();
+ const lastUpdateSetUser2WithUpdate = new Set();
+
+ function validateTreatments({ treatments, isReady, isReadyFromCache }: IUseTreatmentsResult) {
+ if (isReady || isReadyFromCache) {
+ expect(treatments).toEqual({
+ split_test: 'on'
+ })
+ } else {
+ expect(treatments).toEqual({
+ split_test: 'control'
+ })
+ }
+ }
+
+ render(
+
+ <>
+
+ {() => countSplitContext++}
+
+ {React.createElement(() => {
+ const context = useTreatments({ names: ['split_test'], attributes: { att1: 'att1' } });
+ expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
+ validateTreatments(context);
+ countUseTreatments++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatments({ names: ['split_test'], splitKey: 'user_2' });
+ expect(context.client).toBe(user2Client);
+ validateTreatments(context);
+ lastUpdateSetUser2.add(context.lastUpdate);
+ countUseTreatmentsUser2++;
+ return null;
+ })}
+ {React.createElement(() => {
+ const context = useTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false });
+ expect(context.client).toBe(user2Client);
+ validateTreatments(context);
+ lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
+ countUseTreatmentsUser2WithoutUpdate++;
+ return null;
+ })}
+ >
+
+ );
+
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => mainClient.__emitter__.emit(Event.SDK_READY));
+ act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
+ act(() => user2Client.__emitter__.emit(Event.SDK_READY));
+ act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE));
+
+ // SplitFactoryProvider renders once
+ expect(countSplitContext).toEqual(1);
+
+ // If useTreatments evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatments).toEqual(4);
+ expect(mainClient.getTreatments).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
+ expect(mainClient.getTreatments).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }, undefined);
+
+ // If useTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentsUser2).toEqual(4);
+ expect(lastUpdateSetUser2.size).toEqual(4);
+ // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
+ expect(countUseTreatmentsUser2WithoutUpdate).toEqual(3);
+ expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
+ expect(user2Client.getTreatments).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
+ expect(user2Client.getTreatments).toHaveBeenLastCalledWith(['split_test'], undefined, undefined);
+ });
+
+ test('ignores flagSets if both names and flagSets params are provided.', () => {
+ render(
+
+ {
+ React.createElement(() => {
+ // @ts-expect-error names and flagSets are mutually exclusive
+ const treatments = useTreatments({ names: featureFlagNames, flagSets, attributes }).treatments;
+ expect(treatments).toEqual({ split1: CONTROL });
+ return null;
+ })
+ }
+
+ );
+ });
+
+ test('returns fallback treatments if the client is not operational', () => {
+ render(
+
+ {React.createElement(() => {
+ const { treatments } = useTreatments({ names: ['ff1', 'ff2'], attributes, properties });
+ expect(treatments).toEqual({ ff1: 'control_ff1', ff2: 'control_global' });
+ return null;
+ })}
+
+ );
+ });
+
+});
diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useTreatmentsWithConfig.test.tsx
similarity index 69%
rename from src/__tests__/useSplitTreatments.test.tsx
rename to src/__tests__/useTreatmentsWithConfig.test.tsx
index c93454d..a8c5c0b 100644
--- a/src/__tests__/useSplitTreatments.test.tsx
+++ b/src/__tests__/useTreatmentsWithConfig.test.tsx
@@ -7,18 +7,16 @@ jest.mock('@splitsoftware/splitio/client', () => {
return { SplitFactory: mockSdk() };
});
import { SplitFactory } from '@splitsoftware/splitio/client';
-import { sdkBrowser } from './testUtils/sdkConfigs';
+import { sdkBrowser, sdkBrowserWithConfig } from './testUtils/sdkConfigs';
import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants';
/** Test target */
import { SplitFactoryProvider } from '../SplitFactoryProvider';
-import { useSplitTreatments } from '../useSplitTreatments';
+import { useTreatmentsWithConfig } from '../useTreatmentsWithConfig';
import { SplitContext } from '../SplitContext';
-import { ISplitTreatmentsChildProps } from '../types';
+import { IUseTreatmentsWithConfigResult } from '../types';
-const logSpy = jest.spyOn(console, 'log');
-
-describe('useSplitTreatments', () => {
+describe('useTreatmentsWithConfig', () => {
const featureFlagNames = ['split1'];
const flagSets = ['set1'];
@@ -34,11 +32,11 @@ describe('useSplitTreatments', () => {
render(
{React.createElement(() => {
- treatments = useSplitTreatments({ names: featureFlagNames, attributes, properties }).treatments;
- treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes, properties }).treatments;
+ treatments = useTreatmentsWithConfig({ names: featureFlagNames, attributes, properties }).treatments;
+ treatmentsByFlagSets = useTreatmentsWithConfig({ flagSets, attributes, properties }).treatments;
// @ts-expect-error Options object must provide either names or flagSets
- expect(useSplitTreatments({}).treatments).toEqual({});
+ expect(useTreatmentsWithConfig({}).treatments).toEqual({});
return null;
})}
@@ -56,10 +54,10 @@ describe('useSplitTreatments', () => {
act(() => client.__emitter__.emit(Event.SDK_READY));
expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes, { properties });
- expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments);
+ expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments!);
expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith(flagSets, attributes, { properties });
- expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets);
+ expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets!);
});
test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => {
@@ -70,7 +68,7 @@ describe('useSplitTreatments', () => {
render(
{React.createElement(() => {
- const treatments = useSplitTreatments({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments;
+ const treatments = useTreatmentsWithConfig({ names: featureFlagNames, attributes, properties, splitKey: 'user2', updateOnSdkUpdate: false }).treatments;
renderTimes++;
switch (renderTimes) {
@@ -104,8 +102,8 @@ describe('useSplitTreatments', () => {
expect(() => {
render(
React.createElement(() => {
- useSplitTreatments({ names: featureFlagNames, attributes }).treatments;
- useSplitTreatments({ flagSets: featureFlagNames }).treatments;
+ useTreatmentsWithConfig({ names: featureFlagNames, attributes }).treatments;
+ useTreatmentsWithConfig({ flagSets: featureFlagNames }).treatments;
return null;
})
);
@@ -113,40 +111,37 @@ describe('useSplitTreatments', () => {
});
/**
- * Input validation. Passing invalid feature flag names or attributes while the Sdk
- * is not ready doesn't emit errors, and logs meaningful messages instead.
+ * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready.
*/
- test('Input validation: invalid "names" and "attributes" params in useSplitTreatments.', () => {
+ test('Input validation: invalid names are sanitized.', () => {
render(
{
React.createElement(() => {
// @ts-expect-error Test error handling
- let treatments = useSplitTreatments('split1').treatments;
+ let treatments = useTreatmentsWithConfig('split1').treatments;
expect(treatments).toEqual({});
// @ts-expect-error Test error handling
- treatments = useSplitTreatments({ names: [true] }).treatments;
- expect(treatments).toEqual({});
+ treatments = useTreatmentsWithConfig({ names: [true, ' flag_1 ', ' '] }).treatments;
+ expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG });
return null;
})
}
);
- expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.');
- expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.');
});
- test('useSplitTreatments must update on SDK events', async () => {
+ test('must update on SDK events', async () => {
const outerFactory = SplitFactory(sdkBrowser);
const mainClient = outerFactory.client() as any;
const user2Client = outerFactory.client('user_2') as any;
- let countSplitContext = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithoutUpdate = 0;
+ let countSplitContext = 0, countUseTreatmentsWithConfig = 0, countUseTreatmentsWithConfigUser2 = 0, countUseTreatmentsWithConfigUser2WithoutUpdate = 0;
const lastUpdateSetUser2 = new Set();
const lastUpdateSetUser2WithUpdate = new Set();
- function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) {
+ function validateTreatments({ treatments, isReady, isReadyFromCache }: IUseTreatmentsWithConfigResult) {
if (isReady || isReadyFromCache) {
expect(treatments).toEqual({
split_test: {
@@ -171,26 +166,26 @@ describe('useSplitTreatments', () => {
{() => countSplitContext++}
{React.createElement(() => {
- const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } });
+ const context = useTreatmentsWithConfig({ names: ['split_test'], attributes: { att1: 'att1' } });
expect(context.client).toBe(mainClient); // Assert that the main client was retrieved.
validateTreatments(context);
- countUseSplitTreatments++;
+ countUseTreatmentsWithConfig++;
return null;
})}
{React.createElement(() => {
- const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' });
+ const context = useTreatmentsWithConfig({ names: ['split_test'], splitKey: 'user_2' });
expect(context.client).toBe(user2Client);
validateTreatments(context);
lastUpdateSetUser2.add(context.lastUpdate);
- countUseSplitTreatmentsUser2++;
+ countUseTreatmentsWithConfigUser2++;
return null;
})}
{React.createElement(() => {
- const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false });
+ const context = useTreatmentsWithConfig({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false });
expect(context.client).toBe(user2Client);
validateTreatments(context);
lastUpdateSetUser2WithUpdate.add(context.lastUpdate);
- countUseSplitTreatmentsUser2WithoutUpdate++;
+ countUseTreatmentsWithConfigUser2WithoutUpdate++;
return null;
})}
>
@@ -207,36 +202,46 @@ describe('useSplitTreatments', () => {
// SplitFactoryProvider renders once
expect(countSplitContext).toEqual(1);
- // If useSplitTreatments evaluates with the main client and have default update options, it re-renders for each main client event.
- expect(countUseSplitTreatments).toEqual(4);
+ // If useTreatmentsWithConfig evaluates with the main client and have default update options, it re-renders for each main client event.
+ expect(countUseTreatmentsWithConfig).toEqual(4);
expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update
expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }, undefined);
- // If useSplitTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client.
- expect(countUseSplitTreatmentsUser2).toEqual(4);
+ // If useTreatmentsWithConfig evaluates with a different client and have default update options, it re-renders for each event of the new client.
+ expect(countUseTreatmentsWithConfigUser2).toEqual(4);
expect(lastUpdateSetUser2.size).toEqual(4);
// If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event.
- expect(countUseSplitTreatmentsUser2WithoutUpdate).toEqual(3);
+ expect(countUseTreatmentsWithConfigUser2WithoutUpdate).toEqual(3);
expect(lastUpdateSetUser2WithUpdate.size).toEqual(3);
expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1
expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined, undefined);
});
- test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => {
+ test('ignores flagSets if both names and flagSets params are provided.', () => {
render(
{
React.createElement(() => {
// @ts-expect-error names and flagSets are mutually exclusive
- const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments;
+ const treatments = useTreatmentsWithConfig({ names: featureFlagNames, flagSets, attributes }).treatments;
expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG });
return null;
})
}
);
+ });
- expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.');
+ test('returns fallback treatments if the client is not operational', () => {
+ render(
+
+ {React.createElement(() => {
+ const { treatments } = useTreatmentsWithConfig({ names: ['ff1', 'ff2'], attributes, properties });
+ expect(treatments).toEqual({ ff1: { treatment: 'control_ff1', config: 'control_ff1_config' }, ff2: { treatment: 'control_global', config: null } });
+ return null;
+ })}
+
+ );
});
});
diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts
index 3d7e32e..355c88c 100644
--- a/src/__tests__/utils.test.ts
+++ b/src/__tests__/utils.test.ts
@@ -1,19 +1,61 @@
-import { CONTROL_WITH_CONFIG } from '../constants';
-import { getControlTreatmentsWithConfig } from '../utils';
+import { CONTROL, CONTROL_WITH_CONFIG } from '../constants';
+import { getTreatments, getTreatment } from '../utils';
+import { sdkBrowserWithConfig } from './testUtils/sdkConfigs';
-describe('getControlTreatmentsWithConfig', () => {
+const factoryWithoutFallbacks = {
+ settings: {}
+} as SplitIO.IBrowserSDK;
+
+const factoryWithFallbacks = {
+ settings: sdkBrowserWithConfig
+} as SplitIO.IBrowserSDK
+
+describe('getTreatments', () => {
it('should return an empty object if an empty array is provided', () => {
- expect(Object.values(getControlTreatmentsWithConfig([])).length).toBe(0);
+ expect(getTreatments([], true)).toEqual({});
+ expect(getTreatments([], false)).toEqual({});
});
- it('should return an empty object if an empty array is provided', () => {
+ it('should return an object with control treatments if an array of feature flag names is provided', () => {
const featureFlagNames = ['split1', 'split2'];
- const treatments: SplitIO.TreatmentsWithConfig = getControlTreatmentsWithConfig(featureFlagNames);
- featureFlagNames.forEach((featureFlagName) => {
- expect(treatments[featureFlagName]).toBe(CONTROL_WITH_CONFIG);
- });
- expect(Object.keys(treatments).length).toBe(featureFlagNames.length);
+ const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getTreatments(featureFlagNames, true);
+ expect(treatmentsWithConfig).toEqual({ 'split1': CONTROL_WITH_CONFIG, 'split2': CONTROL_WITH_CONFIG });
+
+ const treatments: SplitIO.Treatments = getTreatments(featureFlagNames, false);
+ expect(treatments).toEqual({ 'split1': CONTROL, 'split2': CONTROL });
+
+ expect(getTreatments(featureFlagNames, true, factoryWithoutFallbacks)).toEqual({ 'split1': CONTROL_WITH_CONFIG, 'split2': CONTROL_WITH_CONFIG });
+ expect(getTreatments(featureFlagNames, false, factoryWithoutFallbacks)).toEqual({ 'split1': CONTROL, 'split2': CONTROL });
+ });
+
+ it('should return an object with fallback or control treatments if an array of feature flag names and factory are provided', () => {
+ const featureFlagNames = ['split1', 'ff1'];
+ const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getTreatments(featureFlagNames, true, factoryWithFallbacks);
+ expect(treatmentsWithConfig).toEqual({ 'split1': { treatment: 'control_global', config: null }, 'ff1': { treatment: 'control_ff1', config: 'control_ff1_config' } });
+
+ const treatments: SplitIO.Treatments = getTreatments(featureFlagNames, false, factoryWithFallbacks);
+ expect(treatments).toEqual({ 'split1': 'control_global', 'ff1': 'control_ff1' });
+ });
+
+});
+
+describe('getTreatment', () => {
+
+ it('should return control treatments', () => {
+ expect(getTreatment('any', true)).toEqual(CONTROL_WITH_CONFIG);
+ expect(getTreatment('any', false)).toEqual(CONTROL);
+
+ expect(getTreatment('any', true, factoryWithoutFallbacks)).toEqual(CONTROL_WITH_CONFIG);
+ expect(getTreatment('any', false, factoryWithoutFallbacks)).toEqual(CONTROL);
+ });
+
+ it('should return fallback treatments if a factory with fallback treatments is provided', () => {
+ const treatmentWithConfig: SplitIO.TreatmentWithConfig = getTreatment('split1', true, factoryWithFallbacks);
+ expect(treatmentWithConfig).toEqual({ treatment: 'control_global', config: null });
+
+ const treatment: SplitIO.Treatment = getTreatment('ff1', false, factoryWithFallbacks);
+ expect(treatment).toEqual('control_ff1' );
});
});
diff --git a/src/__tests__/withSplitFactory.test.tsx b/src/__tests__/withSplitFactory.test.tsx
index 95ad025..bcf1b24 100644
--- a/src/__tests__/withSplitFactory.test.tsx
+++ b/src/__tests__/withSplitFactory.test.tsx
@@ -40,7 +40,7 @@ describe('withSplitFactory', () => {
const Component = withSplitFactory(undefined, outerFactory)(
({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => {
expect(factory).toBe(outerFactory);
- expect([isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, 0]);
+ expect([isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, true, false, false, false, 0]);
return null;
}
);
diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx
index af178f2..88e1dd9 100644
--- a/src/__tests__/withSplitTreatments.test.tsx
+++ b/src/__tests__/withSplitTreatments.test.tsx
@@ -14,14 +14,14 @@ import { INITIAL_STATUS } from './testUtils/utils';
import { withSplitFactory } from '../withSplitFactory';
import { withSplitClient } from '../withSplitClient';
import { withSplitTreatments } from '../withSplitTreatments';
-import { getControlTreatmentsWithConfig } from '../utils';
+import { getTreatments } from '../utils';
const featureFlagNames = ['split1', 'split2'];
describe('withSplitTreatments', () => {
it(`passes Split props and outer props to the child.
- In this test, the value of "props.treatments" is obtained by the function "getControlTreatmentsWithConfig",
+ In this test, the value of "props.treatments" is obtained by the function "getTreatments",
and not "client.getTreatmentsWithConfig" since the client is not ready.`, () => {
const Component = withSplitFactory(sdkBrowser)<{ outerProp1: string, outerProp2: number }>(
@@ -36,7 +36,7 @@ describe('withSplitTreatments', () => {
...INITIAL_STATUS,
factory: factory, client: clientMock,
outerProp1: 'outerProp1', outerProp2: 2,
- treatments: getControlTreatmentsWithConfig(featureFlagNames),
+ treatments: getTreatments(featureFlagNames, true),
});
return null;
diff --git a/src/constants.ts b/src/constants.ts
index b6716d0..1db7bd4 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -14,8 +14,6 @@ export const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig = {
};
// Warning and error messages
-export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactoryProvider. Config prop will be ignored.';
+export const WARN_SF_CONFIG_AND_FACTORY: string = 'Both a config and factory props were provided to SplitFactoryProvider. Config prop will be ignored.';
export const EXCEPTION_NO_SFP: string = 'No SplitContext was set. Please ensure the component is wrapped in a SplitFactoryProvider.';
-
-export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets properties were provided. flagSets will be ignored.';
diff --git a/src/index.ts b/src/index.ts
index 431fe6e..cc69202 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,6 +13,10 @@ export { SplitFactoryProvider } from './SplitFactoryProvider';
// Hooks
export { useTrack } from './useTrack';
+export { useTreatment } from './useTreatment';
+export { useTreatments } from './useTreatments';
+export { useTreatmentWithConfig } from './useTreatmentWithConfig';
+export { useTreatmentsWithConfig } from './useTreatmentsWithConfig';
export { useSplitClient } from './useSplitClient';
export { useSplitTreatments } from './useSplitTreatments';
export { useSplitManager } from './useSplitManager';
@@ -34,5 +38,11 @@ export type {
IUpdateProps,
IUseSplitClientOptions,
IUseSplitTreatmentsOptions,
- IUseSplitManagerResult
+ IUseSplitManagerResult,
+ IUseTreatmentOptions,
+ IUseTreatmentsOptions,
+ IUseTreatmentResult,
+ IUseTreatmentWithConfigResult,
+ IUseTreatmentsResult,
+ IUseTreatmentsWithConfigResult
} from './types';
diff --git a/src/types.ts b/src/types.ts
index 9493cd5..04d902b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,45 +1,10 @@
import type { ReactNode } from 'react';
+// @TODO: remove in next major release (it duplicates SplitIO.ReadinessStatus)
/**
- * Split Status interface. It represents the readiness state of an SDK client.
+ * Readiness Status interface. It represents the readiness state of an SDK client.
*/
-export interface ISplitStatus {
-
- /**
- * `isReady` indicates if the Split SDK client has triggered an `SDK_READY` event and thus is ready to be consumed.
- */
- isReady: boolean;
-
- /**
- * `isReadyFromCache` indicates if the Split SDK client has triggered an `SDK_READY_FROM_CACHE` event and thus is ready to be consumed,
- * although the data in cache might be stale.
- */
- isReadyFromCache: boolean;
-
- /**
- * `isTimedout` indicates if the Split SDK client has triggered an `SDK_READY_TIMED_OUT` event and is not ready to be consumed.
- * In other words, `isTimedout` is equivalent to `hasTimedout && !isReady`.
- */
- isTimedout: boolean;
-
- /**
- * `hasTimedout` indicates if the Split SDK client has ever triggered 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.
- */
- hasTimedout: boolean;
-
- /**
- * `isDestroyed` indicates if the Split SDK client has been destroyed.
- */
- isDestroyed: boolean;
-
- /**
- * `lastUpdate` indicates the timestamp of the most recent status event. This timestamp is only updated for events that are being listened to,
- * configured via the `updateOnSdkReady` option for `SDK_READY` event, `updateOnSdkReadyFromCache` for `SDK_READY_FROM_CACHE` event,
- * `updateOnSdkTimedout` for `SDK_READY_TIMED_OUT` event, and `updateOnSdkUpdate` for `SDK_UPDATE` event.
- */
- lastUpdate: number;
-}
+export interface ISplitStatus extends SplitIO.ReadinessStatus {}
/**
* Update Props interface. It defines the props used to configure what SDK events are listened to update the component.
@@ -170,6 +135,9 @@ export interface ISplitClientProps extends IUseSplitClientOptions {
children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode;
}
+/**
+ * Result of the `useSplitManager` hook.
+ */
export interface IUseSplitManagerResult extends ISplitContextValues {
/**
* Split manager instance.
@@ -179,6 +147,17 @@ export interface IUseSplitManagerResult extends ISplitContextValues {
manager?: SplitIO.IManager;
}
+type EvaluationOptions = SplitIO.EvaluationOptions & {
+
+ /**
+ * An object of type Attributes used to evaluate the feature flags.
+ */
+ attributes?: SplitIO.Attributes;
+}
+
+/**
+ * @deprecated `useSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
+ */
export type GetTreatmentsOptions = ({
/**
@@ -193,27 +172,51 @@ export type GetTreatmentsOptions = ({
*/
flagSets: string[];
names?: undefined;
-}) & {
+}) & EvaluationOptions;
- /**
- * An object of type Attributes used to evaluate the feature flags.
- */
- attributes?: SplitIO.Attributes;
+/**
+ * Options object accepted by the `useSplitTreatments` hook, used to call `client.getTreatmentsWithConfig()`, or `client.getTreatmentsWithConfigByFlagSets()`,
+ * depending on whether `names` or `flagSets` options are provided, and to retrieve the result along with the Split context.
+ *
+ * @deprecated `useSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
+ */
+export type IUseSplitTreatmentsOptions = GetTreatmentsOptions & IUseSplitClientOptions;
+
+/**
+ * Options object accepted by the `useTreatment` and `useTreatmentWithConfig` hooks.
+ */
+export type IUseTreatmentOptions = {
/**
- * Optional properties to append to the generated impression object sent to Split backend.
+ * Feature flag name to evaluate.
*/
- properties?: SplitIO.Properties;
-}
+ name: string;
+} & EvaluationOptions & IUseSplitClientOptions;
+
/**
- * Options object accepted by the `useSplitTreatments` hook, used to call `client.getTreatmentsWithConfig()`, or `client.getTreatmentsWithConfigByFlagSets()`,
- * depending on whether `names` or `flagSets` options are provided, and to retrieve the result along with the Split context.
+ * Options object accepted by the `useTreatments` and `useTreatmentsWithConfig` hooks.
*/
-export type IUseSplitTreatmentsOptions = GetTreatmentsOptions & IUseSplitClientOptions;
+export type IUseTreatmentsOptions = ({
+
+ /**
+ * List of feature flag names to evaluate. Either this or the `flagSets` property must be provided. If both are provided, the `flagSets` option is ignored.
+ */
+ names: string[];
+ flagSets?: undefined;
+} | {
+
+ /**
+ * List of feature flag sets to evaluate. Either this or the `names` property must be provided. If both are provided, the `flagSets` option is ignored.
+ */
+ flagSets: string[];
+ names?: undefined;
+}) & EvaluationOptions & IUseSplitClientOptions;
/**
* SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component.
+ *
+ * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatments*` hooks.
*/
export interface ISplitTreatmentsChildProps extends ISplitContextValues {
@@ -231,9 +234,68 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues {
treatments: SplitIO.TreatmentsWithConfig;
}
+/**
+ * Result of the `useTreatment` hook.
+ */
+export interface IUseTreatmentResult extends ISplitContextValues {
+ /**
+ * The treatment string for a feature flag, returned by client.getTreatment().
+ */
+ treatment: SplitIO.Treatment;
+}
+
+/**
+ * Result of the `useTreatmentWithConfig` hook.
+ */
+export interface IUseTreatmentWithConfigResult extends ISplitContextValues {
+ /**
+ * The treatment with config for a feature flag, returned by client.getTreatmentWithConfig().
+ */
+ treatment: SplitIO.TreatmentWithConfig;
+}
+
+/**
+ * Result of the `useTreatments` hook.
+ */
+export interface IUseTreatmentsResult extends ISplitContextValues {
+ /**
+ * An object with the treatment strings for a bulk of feature flags, returned by client.getTreatments() or client.getTreatmentsByFlagSets().
+ * For example:
+ *
+ * ```js
+ * {
+ * feature1: 'on',
+ * feature2: 'off'
+ * }
+ * ```
+ */
+ treatments: SplitIO.Treatments;
+}
+
+/**
+ * Result of the `useTreatmentsWithConfig` hook.
+ */
+export interface IUseTreatmentsWithConfigResult extends ISplitContextValues {
+
+ /**
+ * An object with the treatments with configs for a bulk of feature flags, returned by client.getTreatmentsWithConfig() or client.getTreatmentsWithConfigByFlagSets().
+ * Each existing configuration is a stringified version of the JSON you defined on the Split user interface. For example:
+ *
+ * ```js
+ * {
+ * feature1: { treatment: 'on', config: null },
+ * feature2: { treatment: 'off', config: '{"bannerText":"Click here."}' }
+ * }
+ * ```
+ */
+ treatments: SplitIO.TreatmentsWithConfig;
+}
+
/**
* SplitTreatments Props interface. These are the props accepted by SplitTreatments component, used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()',
* depending on whether `names` or `flagSets` props are provided, and to pass the result to the child component.
+ *
+ * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatments*` hooks.
*/
export type ISplitTreatmentsProps = IUseSplitTreatmentsOptions & {
diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts
index 5b672f0..4d7f595 100644
--- a/src/useSplitTreatments.ts
+++ b/src/useSplitTreatments.ts
@@ -1,7 +1,5 @@
-import * as React from 'react';
-import { memoizeGetTreatmentsWithConfig } from './utils';
import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types';
-import { useSplitClient } from './useSplitClient';
+import { useTreatmentsWithConfig } from '.';
/**
* `useSplitTreatments` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations.
@@ -17,20 +15,9 @@ import { useSplitClient } from './useSplitClient';
* ```
*
* @see {@link https://developer.harness.io/docs/feature-management-experimentation/sdks-and-infrastructure/client-side-sdks/javascript-sdk/#get-treatments-with-configurations}
+ *
+ * @deprecated `useSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
*/
export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps {
- const context = useSplitClient({ ...options, attributes: undefined });
- const { client, lastUpdate } = context;
- const { names, flagSets, attributes, properties } = options;
-
- const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []);
-
- // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked.
- // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object.
- const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties });
-
- return {
- ...context,
- treatments,
- };
+ return useTreatmentsWithConfig(options);
}
diff --git a/src/useTreatment.ts b/src/useTreatment.ts
new file mode 100644
index 0000000..19daa8b
--- /dev/null
+++ b/src/useTreatment.ts
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import memoizeOne from 'memoize-one';
+import { argsAreEqual, getTreatment } from './utils';
+import { IUseTreatmentResult, IUseTreatmentOptions } from './types';
+import { useSplitClient } from './useSplitClient';
+
+function evaluateFeatureFlag(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
+ return client && client.getStatus().isOperational ?
+ client.getTreatment(names[0], attributes, options) :
+ getTreatment(names[0], false, factory);
+}
+
+function memoizeGetTreatment() {
+ return memoizeOne(evaluateFeatureFlag, argsAreEqual);
+}
+
+/**
+ * `useTreatment` is a hook that returns an Split Context object extended with a `treatment` property.
+ * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatment()` method.
+ *
+ * @param options - An options object with a feature flag name to evaluate, and an optional `attributes` and `splitKey` values to configure the client.
+ * @returns A Split Context object extended with a Treatment instance, that might be a control treatment if the client is not available or ready, or if the provided feature flag name does not exist.
+ *
+ * @example
+ * ```js
+ * const { treatment, isReady, isReadyFromCache, hasTimedout, lastUpdate, ... } = useTreatment({ name: 'feature_1'});
+ * ```
+ *
+ * @see {@link https://developer.harness.io/docs/feature-management-experimentation/sdks-and-infrastructure/client-side-sdks/javascript-sdk/#multiple-evaluations-at-once}
+ */
+export function useTreatment(options: IUseTreatmentOptions): IUseTreatmentResult {
+ const context = useSplitClient({ ...options, attributes: undefined });
+ const { factory, client, lastUpdate } = context;
+ const { name, attributes, properties } = options;
+
+ const getTreatment = React.useMemo(memoizeGetTreatment, []);
+
+ // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked.
+ // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object.
+ const treatment = getTreatment(client, lastUpdate, [name], attributes, client ? { ...client.getAttributes() } : {}, undefined, properties && { properties }, factory);
+
+ return {
+ ...context,
+ treatment,
+ };
+}
diff --git a/src/useTreatmentWithConfig.ts b/src/useTreatmentWithConfig.ts
new file mode 100644
index 0000000..0fecfac
--- /dev/null
+++ b/src/useTreatmentWithConfig.ts
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import memoizeOne from 'memoize-one';
+import { argsAreEqual, getTreatment } from './utils';
+import { IUseTreatmentWithConfigResult, IUseTreatmentOptions } from './types';
+import { useSplitClient } from './useSplitClient';
+
+function evaluateFeatureFlagWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
+ return client && client.getStatus().isOperational ?
+ client.getTreatmentWithConfig(names[0], attributes, options) :
+ getTreatment(names[0], true, factory);
+}
+
+function memoizeGetTreatmentWithConfig() {
+ return memoizeOne(evaluateFeatureFlagWithConfig, argsAreEqual);
+}
+
+/**
+ * `useTreatmentWithConfig` is a hook that returns an Split Context object extended with a `treatment` property.
+ * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentWithConfig()` method.
+ *
+ * @param options - An options object with a feature flag name to evaluate, and an optional `attributes` and `splitKey` values to configure the client.
+ * @returns A Split Context object extended with a TreatmentWithConfig instance, that might be a control treatment if the client is not available or ready, or if the provided feature flag name does not exist.
+ *
+ * @example
+ * ```js
+ * const { treatment: { treatment, config }, isReady, isReadyFromCache, hasTimedout, lastUpdate, ... } = useTreatmentWithConfig({ name: 'feature_1'});
+ * ```
+ *
+ * @see {@link https://developer.harness.io/docs/feature-management-experimentation/sdks-and-infrastructure/client-side-sdks/javascript-sdk/#get-treatments-with-configurations}
+ */
+export function useTreatmentWithConfig(options: IUseTreatmentOptions): IUseTreatmentWithConfigResult {
+ const context = useSplitClient({ ...options, attributes: undefined });
+ const { factory, client, lastUpdate } = context;
+ const { name, attributes, properties } = options;
+
+ const getTreatmentWithConfig = React.useMemo(memoizeGetTreatmentWithConfig, []);
+
+ // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked.
+ // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object.
+ const treatment = getTreatmentWithConfig(client, lastUpdate, [name], attributes, client ? { ...client.getAttributes() } : {}, undefined, properties && { properties }, factory);
+
+ return {
+ ...context,
+ treatment,
+ };
+}
diff --git a/src/useTreatments.ts b/src/useTreatments.ts
new file mode 100644
index 0000000..1cb22b3
--- /dev/null
+++ b/src/useTreatments.ts
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import memoizeOne from 'memoize-one';
+import { argsAreEqual, getTreatments } from './utils';
+import { IUseTreatmentsResult, IUseTreatmentsOptions } from './types';
+import { useSplitClient } from './useSplitClient';
+
+function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
+ return client && client.getStatus().isOperational && (names || flagSets) ?
+ names ?
+ client.getTreatments(names, attributes, options) :
+ client.getTreatmentsByFlagSets(flagSets!, attributes, options) :
+ names ?
+ getTreatments(names, false, factory) :
+ {} // empty object when evaluating with flag sets and client is not ready
+}
+
+export function memoizeGetTreatments() {
+ return memoizeOne(evaluateFeatureFlags, argsAreEqual);
+}
+
+/**
+ * `useTreatments` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations.
+ * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatments()` method if the `names` option is provided,
+ * or the `client.getTreatmentsByFlagSets()` method if the `flagSets` option is provided.
+ *
+ * @param options - An options object with a list of feature flag names or flag sets to evaluate, and an optional `attributes` and `splitKey` values to configure the client.
+ * @returns A Split Context object extended with a Treatments instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist.
+ *
+ * @example
+ * ```js
+ * const { treatments: { feature_1, feature_2 }, isReady, isReadyFromCache, hasTimedout, lastUpdate, ... } = useTreatments({ names: ['feature_1', 'feature_2']});
+ * ```
+ *
+ * @see {@link https://developer.harness.io/docs/feature-management-experimentation/sdks-and-infrastructure/client-side-sdks/javascript-sdk/#multiple-evaluations-at-once}
+ */
+export function useTreatments(options: IUseTreatmentsOptions): IUseTreatmentsResult {
+ const context = useSplitClient({ ...options, attributes: undefined });
+ const { factory, client, lastUpdate } = context;
+ const { names, flagSets, attributes, properties } = options;
+
+ const getTreatments = React.useMemo(memoizeGetTreatments, []);
+
+ // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked.
+ // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object.
+ const treatments = getTreatments(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }, factory);
+
+ return {
+ ...context,
+ treatments,
+ };
+}
diff --git a/src/useTreatmentsWithConfig.ts b/src/useTreatmentsWithConfig.ts
new file mode 100644
index 0000000..0cbd7c3
--- /dev/null
+++ b/src/useTreatmentsWithConfig.ts
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import memoizeOne from 'memoize-one';
+import { argsAreEqual, getTreatments } from './utils';
+import { IUseTreatmentsOptions, IUseTreatmentsWithConfigResult } from './types';
+import { useSplitClient } from './useSplitClient';
+
+function evaluateFeatureFlagsWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
+ return client && client.getStatus().isOperational && (names || flagSets) ?
+ names ?
+ client.getTreatmentsWithConfig(names, attributes, options) :
+ client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) :
+ names ?
+ getTreatments(names, true, factory) :
+ {} // empty object when evaluating with flag sets and client is not ready
+}
+
+function memoizeGetTreatmentsWithConfig() {
+ return memoizeOne(evaluateFeatureFlagsWithConfig, argsAreEqual);
+}
+
+/**
+ * `useTreatmentsWithConfig` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations.
+ * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentsWithConfig()` method if the `names` option is provided,
+ * or the `client.getTreatmentsWithConfigByFlagSets()` method if the `flagSets` option is provided.
+ *
+ * @param options - An options object with a list of feature flag names or flag sets to evaluate, and an optional `attributes` and `splitKey` values to configure the client.
+ * @returns A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist.
+ *
+ * @example
+ * ```js
+ * const { treatments: { feature_1, feature_2 }, isReady, isReadyFromCache, hasTimedout, lastUpdate, ... } = useTreatmentsWithConfig({ names: ['feature_1', 'feature_2']});
+ * ```
+ *
+ * @see {@link https://developer.harness.io/docs/feature-management-experimentation/sdks-and-infrastructure/client-side-sdks/javascript-sdk/#get-treatments-with-configurations}
+ */
+export function useTreatmentsWithConfig(options: IUseTreatmentsOptions): IUseTreatmentsWithConfigResult {
+ const context = useSplitClient({ ...options, attributes: undefined });
+ const { factory, client, lastUpdate } = context;
+ const { names, flagSets, attributes, properties } = options;
+
+ const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []);
+
+ // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked.
+ // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object.
+ const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }, factory);
+
+ return {
+ ...context,
+ treatments,
+ };
+}
diff --git a/src/utils.ts b/src/utils.ts
index c78ec77..dddb3f7 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,34 +1,16 @@
-import memoizeOne from 'memoize-one';
import shallowEqual from 'shallowequal';
-import { CONTROL_WITH_CONFIG, WARN_NAMES_AND_FLAGSETS } from './constants';
+import { CONTROL, CONTROL_WITH_CONFIG } from './constants';
import { ISplitStatus } from './types';
-// Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together.
-
-/**
- * ClientWithContext interface.
- */
-interface IClientWithContext extends SplitIO.IBrowserClient {
- __getStatus(): {
- isReady: boolean;
- isReadyFromCache: boolean;
- isTimedout: boolean;
- hasTimedout: boolean;
- isDestroyed: boolean;
- isOperational: boolean;
- lastUpdate: number;
- };
+function isString(val: unknown): val is string {
+ return typeof val === 'string' || val instanceof String;
}
-export interface IFactoryWithLazyInit extends SplitIO.IBrowserSDK {
- config: SplitIO.IBrowserSettings;
- init(): void;
-}
+// Utils used to access singleton instances of Split factories and clients:
-// idempotent operation
-export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey): IClientWithContext {
+export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey): SplitIO.IBrowserClient {
// factory.client is an idempotent operation
- const client = (key !== undefined ? factory.client(key) : factory.client()) as IClientWithContext;
+ const client = key !== undefined ? factory.client(key) : factory.client();
// Remove EventEmitter warning emitted when using multiple SDK hooks or components.
// Unlike JS SDK, users don't need to access the client directly, making the warning irrelevant.
@@ -37,111 +19,71 @@ export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.Split
return client;
}
-// Util used to get client status.
-// It might be removed in the future, if the JS SDK extends its public API with a `getStatus` method
export function getStatus(client?: SplitIO.IBrowserClient): ISplitStatus {
- const status = client && (client as IClientWithContext).__getStatus();
-
- return {
- isReady: status ? status.isReady : false,
- isReadyFromCache: status ? status.isReadyFromCache : false,
- isTimedout: status ? status.isTimedout : false,
- hasTimedout: status ? status.hasTimedout : false,
- isDestroyed: status ? status.isDestroyed : false,
- lastUpdate: status ? status.lastUpdate : 0,
- };
+ return client ?
+ client.getStatus() :
+ {
+ isReady: false,
+ isReadyFromCache: false,
+ isTimedout: false,
+ hasTimedout: false,
+ isDestroyed: false,
+ isOperational: false,
+ lastUpdate: 0,
+ };
}
-/**
- * Manage client attributes binding
- */
+// Manage client attributes binding
// @TODO should reset attributes rather than set/merge them, to keep SFP and hooks pure.
export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: SplitIO.Attributes) {
if (client && attributes) client.setAttributes(attributes);
}
-// Input validation utils that will be replaced eventually
-
-function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature flag names'): false | string[] {
- if (Array.isArray(maybeFeatureFlags) && maybeFeatureFlags.length > 0) {
- const validatedArray: string[] = [];
- // Remove invalid values
- maybeFeatureFlags.forEach((maybeFeatureFlag) => {
- const featureFlagName = validateFeatureFlag(maybeFeatureFlag);
- if (featureFlagName) validatedArray.push(featureFlagName);
- });
+// Utils used to retrieve fallback or control treatments when the client is not operational:
- // Strip off duplicated values if we have valid feature flag names then return
- if (validatedArray.length) return uniq(validatedArray);
- }
+export function getTreatment(flagName: string, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentWithConfig;
+export function getTreatment(flagName: string, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment;
+export function getTreatment(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment | SplitIO.TreatmentWithConfig;
+export function getTreatment(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK) {
+ if (factory && factory.settings.fallbackTreatments) {
+ const fallbacks = factory.settings.fallbackTreatments;
- console.log(`[ERROR] ${listName} must be a non-empty array.`);
- return false;
-}
+ const treatment = fallbacks.byFlag?.[flagName] || fallbacks.global;
-const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/;
-
-function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'feature flag name'): false | string {
- if (maybeFeatureFlag == undefined) {
- console.log(`[ERROR] you passed a null or undefined ${item}, ${item} must be a non-empty string.`);
- } else if (!isString(maybeFeatureFlag)) {
- console.log(`[ERROR] you passed an invalid ${item}, ${item} must be a non-empty string.`);
- } else {
- if (TRIMMABLE_SPACES_REGEX.test(maybeFeatureFlag)) {
- console.log(`[WARN] ${item} "${maybeFeatureFlag}" has extra whitespace, trimming.`);
- maybeFeatureFlag = maybeFeatureFlag.trim();
- }
-
- if ((maybeFeatureFlag as string).length > 0) {
- return maybeFeatureFlag as string;
- } else {
- console.log(`[ERROR] you passed an empty ${item}, ${item} must be a non-empty string.`);
+ if (treatment) {
+ return isString(treatment) ?
+ withConfig ? { treatment, config: null } : treatment :
+ withConfig ? treatment : treatment.treatment;
}
}
- return false;
+ return withConfig ? CONTROL_WITH_CONFIG : CONTROL;
}
-export function getControlTreatmentsWithConfig(featureFlagNames: unknown): SplitIO.TreatmentsWithConfig {
- // validate featureFlags Names
- const validatedFeatureFlagNames = validateFeatureFlags(featureFlagNames);
+export function getTreatments(featureFlagNames: unknown, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentsWithConfig;
+export function getTreatments(featureFlagNames: unknown, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatments;
+export function getTreatments(featureFlagNames: unknown, withConfig: boolean, factory?: SplitIO.IBrowserSDK) {
+ // validate feature flag names
+ if (!Array.isArray(featureFlagNames)) return {};
- // return empty object if the returned value is false
- if (!validatedFeatureFlagNames) return {};
+ featureFlagNames = featureFlagNames
+ .filter((featureFlagName) => isString(featureFlagName))
+ .map((featureFlagName) => featureFlagName.trim())
+ .filter((featureFlagName) => featureFlagName.length > 0);
- // return control treatments for each validated feature flag name
- return validatedFeatureFlagNames.reduce((pValue: SplitIO.TreatmentsWithConfig, cValue: string) => {
- pValue[cValue] = CONTROL_WITH_CONFIG;
+ // return control or fallback treatment for each validated feature flag name
+ return (featureFlagNames as string[]).reduce((pValue: SplitIO.Treatments | SplitIO.TreatmentsWithConfig, featureFlagName: string) => {
+ pValue[featureFlagName] = getTreatment(featureFlagName, withConfig, factory);
return pValue;
}, {});
}
/**
- * Removes duplicate items on an array of strings.
+ * Utils to memoize `client.getTreatments*` method calls to avoid duplicated impressions.
+ * The result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag names and attributes.
*/
-function uniq(arr: string[]): string[] {
- const seen: Record = {};
- return arr.filter((item) => {
- return Object.prototype.hasOwnProperty.call(seen, item) ? false : seen[item] = true;
- });
-}
-/**
- * Checks if a given value is a string.
- */
-function isString(val: unknown): val is string {
- return typeof val === 'string' || val instanceof String;
-}
-
-/**
- * Gets a memoized version of the `client.getTreatmentsWithConfig` method.
- * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`.
- */
-export function memoizeGetTreatmentsWithConfig() {
- return memoizeOne(evaluateFeatureFlags, argsAreEqual);
-}
-
-function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
+export function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
return newArgs[0] === lastArgs[0] && // client
newArgs[1] === lastArgs[1] && // lastUpdate
shallowEqual(newArgs[2], lastArgs[2]) && // names
@@ -149,15 +91,3 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
shallowEqual(newArgs[4], lastArgs[4]) && // client attributes
shallowEqual(newArgs[5], lastArgs[5]); // flagSets
}
-
-function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) {
- if (names && flagSets) console.log(WARN_NAMES_AND_FLAGSETS);
-
- return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ?
- names ?
- client.getTreatmentsWithConfig(names, attributes, options) :
- client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) :
- names ?
- getControlTreatmentsWithConfig(names) :
- {} // empty object when evaluating with flag sets and client is not ready
-}
diff --git a/src/withSplitTreatments.tsx b/src/withSplitTreatments.tsx
index 8119292..54bf4fa 100644
--- a/src/withSplitTreatments.tsx
+++ b/src/withSplitTreatments.tsx
@@ -10,7 +10,7 @@ import { SplitTreatments } from './SplitTreatments';
* @param names - list of feature flag names
* @param attributes - An object of type Attributes used to evaluate the feature flags.
*
- * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook.
+ * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useTreatment*` hooks.
*/
export function withSplitTreatments(names: string[], attributes?: SplitIO.Attributes) {
diff --git a/umd.ts b/umd.ts
index 351c5d3..d9a5dcb 100644
--- a/umd.ts
+++ b/umd.ts
@@ -3,6 +3,7 @@ import {
withSplitFactory, withSplitClient, withSplitTreatments,
SplitFactoryProvider, SplitClient, SplitTreatments,
useSplitClient, useSplitTreatments, useTrack, useSplitManager,
+ useTreatment, useTreatments, useTreatmentWithConfig, useTreatmentsWithConfig,
SplitContext,
} from './src/index';
@@ -11,5 +12,6 @@ export default {
withSplitFactory, withSplitClient, withSplitTreatments,
SplitFactoryProvider, SplitClient, SplitTreatments,
useSplitClient, useSplitTreatments, useTrack, useSplitManager,
+ useTreatment, useTreatments, useTreatmentWithConfig, useTreatmentsWithConfig,
SplitContext,
};