diff --git a/CHANGES.txt b/CHANGES.txt index 83b116e..3be7e1c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.6.0 (October 31, 2025) +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. diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md index 1faac7d..d867951 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 single 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/package-lock.json b/package-lock.json index 310bfe0..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.7.2-rc.4", + "@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.7.2-rc.4", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.2-rc.4.tgz", - "integrity": "sha512-1A26oJ82JLmBC4OhRJoNgrESVCN+HErqrduiI3J88oznxIPjRVQGJt/BhfF87FhNXOXvtgx+4dF15Pje6zCq7A==", + "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.7.9-rc.2", + "@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.7.9-rc.2", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.2.tgz", - "integrity": "sha512-t8YVwDe4UBvD95w+mvKq7Z2khozZXDrIuOWt3ixxtmyeyoZp5L0L9x9E3DWOcQ0EVxfpQv+tAErHG3bw3LkbNg==", + "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.7.2-rc.4", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.2-rc.4.tgz", - "integrity": "sha512-1A26oJ82JLmBC4OhRJoNgrESVCN+HErqrduiI3J88oznxIPjRVQGJt/BhfF87FhNXOXvtgx+4dF15Pje6zCq7A==", + "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.7.9-rc.2", + "@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.7.9-rc.2", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.2.tgz", - "integrity": "sha512-t8YVwDe4UBvD95w+mvKq7Z2khozZXDrIuOWt3ixxtmyeyoZp5L0L9x9E3DWOcQ0EVxfpQv+tAErHG3bw3LkbNg==", + "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 4c8e21c..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.7.2-rc.4", + "@splitsoftware/splitio": "11.8.0", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" 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__/useTreatment.test.tsx b/src/__tests__/useTreatment.test.tsx index 106131e..69bde09 100644 --- a/src/__tests__/useTreatment.test.tsx +++ b/src/__tests__/useTreatment.test.tsx @@ -7,7 +7,7 @@ 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, EXCEPTION_NO_SFP } from '../constants'; /** Test target */ @@ -96,7 +96,7 @@ describe('useTreatment', () => { }).toThrow(EXCEPTION_NO_SFP); }); - test('useTreatment 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; @@ -171,4 +171,16 @@ describe('useTreatment', () => { 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 index a296126..f74ac42 100644 --- a/src/__tests__/useTreatmentWithConfig.test.tsx +++ b/src/__tests__/useTreatmentWithConfig.test.tsx @@ -7,7 +7,7 @@ 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 */ @@ -96,7 +96,7 @@ describe('useTreatmentWithConfig', () => { }).toThrow(EXCEPTION_NO_SFP); }); - test('useTreatmentWithConfig 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; @@ -171,4 +171,16 @@ describe('useTreatmentWithConfig', () => { 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 index 9b5bb57..98712b4 100644 --- a/src/__tests__/useTreatments.test.tsx +++ b/src/__tests__/useTreatments.test.tsx @@ -7,7 +7,7 @@ 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, EXCEPTION_NO_SFP } from '../constants'; /** Test target */ @@ -132,7 +132,7 @@ describe('useTreatments', () => { ); }); - test('useTreatments 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; @@ -226,4 +226,16 @@ describe('useTreatments', () => { ); }); + 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__/useTreatmentsWithConfig.test.tsx b/src/__tests__/useTreatmentsWithConfig.test.tsx index 84b9981..a8c5c0b 100644 --- a/src/__tests__/useTreatmentsWithConfig.test.tsx +++ b/src/__tests__/useTreatmentsWithConfig.test.tsx @@ -7,7 +7,7 @@ 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 */ @@ -132,7 +132,7 @@ describe('useTreatmentsWithConfig', () => { ); }); - test('useTreatmentsWithConfig 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; @@ -232,4 +232,16 @@ describe('useTreatmentsWithConfig', () => { ); }); + 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/useTreatment.ts b/src/useTreatment.ts index d41438d..1e743e0 100644 --- a/src/useTreatment.ts +++ b/src/useTreatment.ts @@ -19,14 +19,14 @@ import { useSplitClient } from './useSplitClient'; */ export function useTreatment(options: IUseTreatmentOptions): IUseTreatmentResult { const context = useSplitClient({ ...options, attributes: undefined }); - const { client, lastUpdate } = context; + 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 }); + const treatment = getTreatment(client, lastUpdate, [name], attributes, client ? { ...client.getAttributes() } : {}, undefined, properties && { properties }, factory); return { ...context, diff --git a/src/useTreatmentWithConfig.ts b/src/useTreatmentWithConfig.ts index 819396b..b7790d3 100644 --- a/src/useTreatmentWithConfig.ts +++ b/src/useTreatmentWithConfig.ts @@ -19,14 +19,14 @@ import { useSplitClient } from './useSplitClient'; */ export function useTreatmentWithConfig(options: IUseTreatmentOptions): IUseTreatmentWithConfigResult { const context = useSplitClient({ ...options, attributes: undefined }); - const { client, lastUpdate } = context; + 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 }); + const treatment = getTreatmentWithConfig(client, lastUpdate, [name], attributes, client ? { ...client.getAttributes() } : {}, undefined, properties && { properties }, factory); return { ...context, diff --git a/src/useTreatments.ts b/src/useTreatments.ts index e90cb4e..45c29a9 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -20,14 +20,14 @@ import { useSplitClient } from './useSplitClient'; */ export function useTreatments(options: IUseTreatmentsOptions): IUseTreatmentsResult { const context = useSplitClient({ ...options, attributes: undefined }); - const { client, lastUpdate } = context; + 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 }); + const treatments = getTreatments(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }, factory); return { ...context, diff --git a/src/useTreatmentsWithConfig.ts b/src/useTreatmentsWithConfig.ts index f59fe8d..0efb094 100644 --- a/src/useTreatmentsWithConfig.ts +++ b/src/useTreatmentsWithConfig.ts @@ -20,14 +20,14 @@ import { useSplitClient } from './useSplitClient'; */ export function useTreatmentsWithConfig(options: IUseTreatmentsOptions): IUseTreatmentsWithConfigResult { const context = useSplitClient({ ...options, attributes: undefined }); - const { client, lastUpdate } = context; + 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 }); + const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }, factory); return { ...context, diff --git a/src/utils.ts b/src/utils.ts index 025599b..8db4e79 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,6 +37,24 @@ export function getStatus(client?: SplitIO.IBrowserClient): ISplitStatus { }; } +function resolveFallback(flagName: string, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentWithConfig; +function resolveFallback(flagName: string, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment; +function resolveFallback(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment | SplitIO.TreatmentWithConfig { + if (factory && factory.settings.fallbackTreatments) { + const fallbacks = factory.settings.fallbackTreatments; + + const treatment = fallbacks.byFlag?.[flagName] || fallbacks.global; + + if (treatment) { + return isString(treatment) ? + withConfig ? { treatment, config: null } : treatment : + withConfig ? treatment : treatment.treatment; + } + } + + return withConfig ? CONTROL_WITH_CONFIG : CONTROL; +} + /** * Manage client attributes binding */ @@ -45,9 +63,9 @@ export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: Spl if (client && attributes) client.setAttributes(attributes); } -export function getControlTreatments(featureFlagNames: unknown, withConfig: true): SplitIO.TreatmentsWithConfig; -export function getControlTreatments(featureFlagNames: unknown, withConfig: false): SplitIO.Treatments; -export function getControlTreatments(featureFlagNames: unknown, withConfig: boolean): SplitIO.Treatments | SplitIO.TreatmentsWithConfig { +export function getControlTreatments(featureFlagNames: unknown, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentsWithConfig; +export function getControlTreatments(featureFlagNames: unknown, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatments; +export function getControlTreatments(featureFlagNames: unknown, withConfig: boolean, factory?: SplitIO.IBrowserSDK): SplitIO.Treatments | SplitIO.TreatmentsWithConfig { // validate feature flag names if (!Array.isArray(featureFlagNames)) return {}; @@ -56,9 +74,10 @@ export function getControlTreatments(featureFlagNames: unknown, withConfig: bool .map((featureFlagName) => featureFlagName.trim()) .filter((featureFlagName) => featureFlagName.length > 0); - // return control treatments for each validated feature flag name - return (featureFlagNames as string[]).reduce((pValue: SplitIO.Treatments | SplitIO.TreatmentsWithConfig, cValue: string) => { - pValue[cValue] = withConfig ? CONTROL_WITH_CONFIG : CONTROL; + // return control or fallback treatment for each validated feature flag name + return (featureFlagNames as string[]).reduce((pValue: SplitIO.Treatments | SplitIO.TreatmentsWithConfig, featureFlagName: string) => { + // @ts-expect-error asd + pValue[featureFlagName] = resolveFallback(featureFlagName, withConfig, factory); return pValue; }, {}); } @@ -79,13 +98,13 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { shallowEqual(newArgs[5], lastArgs[5]); // flagSets } -function evaluateFeatureFlagsWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) { +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 ? - getControlTreatments(names, true) : + getControlTreatments(names, true, factory) : {} // empty object when evaluating with flag sets and client is not ready } @@ -97,13 +116,13 @@ export function memoizeGetTreatmentsWithConfig() { return memoizeOne(evaluateFeatureFlagsWithConfig, argsAreEqual); } -function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) { +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 ? - getControlTreatments(names, false) : + getControlTreatments(names, false, factory) : {} // empty object when evaluating with flag sets and client is not ready } @@ -111,20 +130,20 @@ export function memoizeGetTreatments() { return memoizeOne(evaluateFeatureFlags, argsAreEqual); } -function evaluateFeatureFlagWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions) { +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) : - CONTROL_WITH_CONFIG + resolveFallback(names[0], true, factory); } export function memoizeGetTreatmentWithConfig() { return memoizeOne(evaluateFeatureFlagWithConfig, argsAreEqual); } -function evaluateFeatureFlag(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions) { +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) : - CONTROL; + resolveFallback(names[0], false, factory); } export function memoizeGetTreatment() {