From ec5a663d849e1e7f342172ce04e236096b1ebd31 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 17:59:23 -0300 Subject: [PATCH 01/13] add flagSets prop to SplitTreatments component and flagSets property to useSplitTreatments hook --- package-lock.json | 30 +++++++++++++++--------------- package.json | 2 +- src/SplitTreatments.tsx | 9 +++++---- src/types.ts | 16 +++++++++++++--- src/useSplitTreatments.ts | 9 +++++---- src/utils.ts | 7 ++++--- types/SplitTreatments.d.ts | 5 +++-- types/types.d.ts | 14 +++++++++++--- types/useSplitTreatments.d.ts | 5 +++-- types/utils.d.ts | 2 +- 10 files changed, 61 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f01255..455ce50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.9.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.23.2-rc.3", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -1547,11 +1547,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.23.2-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", + "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", "dependencies": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -1569,9 +1569,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.3.tgz", + "integrity": "sha512-eqJxAMtqFK7fXFKL8gMGfRsMBdxrYI9tIGUHHpY1NcyeKkn4OWqAOZMhX6z2qLdBArzHi34Li0Lb72o+Bh1Tqg==", "dependencies": { "tslib": "^2.3.1" }, @@ -12089,11 +12089,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.23.2-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", + "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", "requires": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -12105,9 +12105,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.3.tgz", + "integrity": "sha512-eqJxAMtqFK7fXFKL8gMGfRsMBdxrYI9tIGUHHpY1NcyeKkn4OWqAOZMhX6z2qLdBArzHi34Li0Lb72o+Bh1Tqg==", "requires": { "tslib": "^2.3.1" } diff --git a/package.json b/package.json index 206ca9b..cb69ffc 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.23.2-rc.3", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 563b2cf..6049c5e 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -5,8 +5,9 @@ import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from './constants'; import { memoizeGetTreatmentsWithConfig } from './utils'; /** - * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to - * call 'client.getTreatmentsWithConfig()' method, and passes the returned treatments to a child as a function. + * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to + * call the 'client.getTreatmentsWithConfig()' method if a `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method + * if a `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ @@ -18,7 +19,7 @@ export class SplitTreatments extends React.Component { private evaluateFeatureFlags = memoizeGetTreatmentsWithConfig(); render() { - const { names, children, attributes } = this.props; + const { names, flagSets, children, attributes } = this.props; return ( @@ -29,7 +30,7 @@ export class SplitTreatments extends React.Component { if (client && isOperational) { // Cloning `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. // Caveat: same issue happens with `names` and `attributes` props if the user follows the bad practice of mutating the object instead of providing a new one. - treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }); + treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets); } else { treatments = getControlTreatmentsWithConfig(names); if (!client) { this.logWarning = true; } diff --git a/src/types.ts b/src/types.ts index 42d2a85..7654fdf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -172,14 +172,19 @@ export interface ISplitClientProps extends IUseSplitClientOptions { /** * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. + * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** * list of feature flag names */ - names: string[] + names?: string[]; + + /** + * list of feature flag sets + */ + flagSets?: string[]; } /** @@ -207,7 +212,12 @@ export interface ISplitTreatmentsProps { /** * list of feature flag names */ - names: string[]; + names?: string[]; + + /** + * list of feature flag sets + */ + flagSets?: string[]; /** * An object of type Attributes used to evaluate the feature flags. diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index ddc0590..1e95ab2 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -5,8 +5,9 @@ import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types' import { useSplitClient } from './useSplitClient'; /** - * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. + * 'useSplitTreatments' is a hook that returns a SplitContext object extended with a `treatments` property, which contains an object of feature flag evaluations (i.e., treatments). + * It utilizes the 'useSplitClient' hook to access the client from the context and invokes the 'getTreatmentsWithConfig' method + * if `names` property is provided, or the 'getTreatmentsWithConfigByFlagSets' method if `flagSets` property is provided. * * @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} @@ -14,12 +15,12 @@ import { useSplitClient } from './useSplitClient'; export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { const context = useSplitClient({...options, attributes: undefined }); const { client, lastUpdate } = context; - const { names, attributes } = options; + const { names, flagSets, attributes } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }) : + getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets) : getControlTreatmentsWithConfig(names); return { diff --git a/src/utils.ts b/src/utils.ts index d3ca36a..d6a892c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -176,9 +176,10 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { newArgs[1] === lastArgs[1] && // lastUpdate shallowEqual(newArgs[2], lastArgs[2]) && // names shallowEqual(newArgs[3], lastArgs[3]) && // attributes - shallowEqual(newArgs[4], lastArgs[4]); // client attributes + shallowEqual(newArgs[4], lastArgs[4]) && // client attributes + shallowEqual(newArgs[5], lastArgs[5]); // flagSets } -function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes) { - return client.getTreatmentsWithConfig(names, attributes); +function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { + return names ? client.getTreatmentsWithConfig(names, attributes) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes); } diff --git a/types/SplitTreatments.d.ts b/types/SplitTreatments.d.ts index 70e213a..a66ff78 100644 --- a/types/SplitTreatments.d.ts +++ b/types/SplitTreatments.d.ts @@ -1,8 +1,9 @@ import React from 'react'; import { ISplitTreatmentsProps } from './types'; /** - * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to - * call 'client.getTreatmentsWithConfig()' method, and passes the returned treatments to a child as a function. + * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to + * call the 'client.getTreatmentsWithConfig()' method if a `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method + * if a `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ diff --git a/types/types.d.ts b/types/types.d.ts index 0e4c275..6e06366 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -141,13 +141,17 @@ export interface ISplitClientProps extends IUseSplitClientOptions { } /** * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' and retrieve the result together with the Split context. + * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** * list of feature flag names */ - names: string[]; + names?: string[]; + /** + * list of feature flag sets + */ + flagSets?: string[]; } /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -171,7 +175,11 @@ export interface ISplitTreatmentsProps { /** * list of feature flag names */ - names: string[]; + names?: string[]; + /** + * list of feature flag sets + */ + flagSets?: string[]; /** * An object of type Attributes used to evaluate the feature flags. */ diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts index b636632..bf7a5ff 100644 --- a/types/useSplitTreatments.d.ts +++ b/types/useSplitTreatments.d.ts @@ -1,7 +1,8 @@ import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; /** - * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. + * 'useSplitTreatments' is a hook that returns a SplitContext object extended with a `treatments` property, which contains an object of feature flag evaluations (i.e., treatments). + * It utilizes the 'useSplitClient' hook to access the client from the context and invokes the 'getTreatmentsWithConfig' method + * if `names` property is provided, or the 'getTreatmentsWithConfigByFlagSets' method if `flagSets` property is provided. * * @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} diff --git a/types/utils.d.ts b/types/utils.d.ts index 942eb0d..0465872 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -34,5 +34,5 @@ export declare function initAttributes(client: SplitIO.IBrowserClient | null, at * 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 declare function memoizeGetTreatmentsWithConfig(): typeof evaluateFeatureFlags; -declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; +declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; export {}; From 601f7dccfac94356f55bad75119cf728909e7aff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 2 Nov 2023 18:23:41 -0300 Subject: [PATCH 02/13] using union to restrict that either 'names' or 'flagSets' is required but not both --- src/__tests__/SplitTreatments.test.tsx | 8 +++--- src/types.ts | 35 +++++++++++++++----------- types/types.d.ts | 35 +++++++++++++++----------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index f3c2f56..52fba21 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -150,16 +150,16 @@ let renderTimes = 0; * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarily when using SplitTreatments and useSplitTreatments. */ describe.each([ - ({ names, attributes }) => ( - + ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => ( + {() => { renderTimes++; return null; }} ), - ({ names, attributes }) => { - useSplitTreatments({ names, attributes }); + ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => { + useSplitTreatments({ names, attributes, flagSets: undefined }); renderTimes++; return null; } diff --git a/src/types.ts b/src/types.ts index 7654fdf..8f9fe1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -174,18 +174,21 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { +export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ /** * list of feature flag names */ - names?: string[]; + names: string[]; + flagSets?: undefined; +} | { /** * list of feature flag sets */ - flagSets?: string[]; -} + flagSets: string[]; + names?: undefined; +}) /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -207,25 +210,29 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. */ -export interface ISplitTreatmentsProps { +export type ISplitTreatmentsProps = { /** - * list of feature flag names + * An object of type Attributes used to evaluate the feature flags. */ - names?: string[]; + attributes?: SplitIO.Attributes; /** - * list of feature flag sets + * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ - flagSets?: string[]; + children: ((props: ISplitTreatmentsChildProps) => ReactNode); +} & ({ /** - * An object of type Attributes used to evaluate the feature flags. + * list of feature flag names */ - attributes?: SplitIO.Attributes; + names: string[]; + flagSets?: undefined; +} | { /** - * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. + * list of feature flag sets */ - children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} + flagSets: string[]; + names?: undefined; +}) diff --git a/types/types.d.ts b/types/types.d.ts index 6e06366..8f0caf3 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -143,16 +143,19 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { +export declare type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ /** * list of feature flag names */ - names?: string[]; + names: string[]; + flagSets?: undefined; +} | { /** * list of feature flag sets */ - flagSets?: string[]; -} + flagSets: string[]; + names?: undefined; +}); /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -171,15 +174,7 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. */ -export interface ISplitTreatmentsProps { - /** - * list of feature flag names - */ - names?: string[]; - /** - * list of feature flag sets - */ - flagSets?: string[]; +export declare type ISplitTreatmentsProps = { /** * An object of type Attributes used to evaluate the feature flags. */ @@ -188,4 +183,16 @@ export interface ISplitTreatmentsProps { * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} +} & ({ + /** + * list of feature flag names + */ + names: string[]; + flagSets?: undefined; +} | { + /** + * list of feature flag sets + */ + flagSets: string[]; + names?: undefined; +}); From fe02e0595f2a74c26f38ff1e68bf06f3630d783e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 13:52:54 -0300 Subject: [PATCH 03/13] Add tests --- src/SplitTreatments.tsx | 16 +-- src/__tests__/SplitTreatments.test.tsx | 139 ++++++++++++++-------- src/__tests__/testUtils/mockSplitSdk.ts | 7 ++ src/__tests__/useSplitTreatments.test.tsx | 52 +++++--- src/constants.ts | 6 +- src/types.ts | 21 ++-- src/useSplitTreatments.ts | 11 +- src/utils.ts | 14 ++- types/constants.d.ts | 1 + types/types.d.ts | 21 ++-- types/utils.d.ts | 2 +- 11 files changed, 168 insertions(+), 122 deletions(-) diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index 6049c5e..fd89a7a 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitTreatmentsProps, ISplitContextValues } from './types'; -import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from './constants'; +import { WARN_ST_NO_CLIENT } from './constants'; import { memoizeGetTreatmentsWithConfig } from './utils'; /** @@ -24,17 +24,9 @@ export class SplitTreatments extends React.Component { return ( {(splitContext: ISplitContextValues) => { - const { client, isReady, isReadyFromCache, isDestroyed, lastUpdate } = splitContext; - let treatments; - const isOperational = !isDestroyed && (isReady || isReadyFromCache); - if (client && isOperational) { - // Cloning `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. - // Caveat: same issue happens with `names` and `attributes` props if the user follows the bad practice of mutating the object instead of providing a new one. - treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets); - } else { - treatments = getControlTreatmentsWithConfig(names); - if (!client) { this.logWarning = true; } - } + const { client, lastUpdate } = splitContext; + const treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); + if (!client) { this.logWarning = true; } // SplitTreatments only accepts a function as a child, not a React Element (JSX) return children({ ...splitContext, treatments, diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 52fba21..ca20da9 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -8,31 +8,27 @@ jest.mock('@splitsoftware/splitio/client', () => { }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -const logSpy = jest.spyOn(console, 'log'); +import { getStatus } from '../utils'; +import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; +import { CONTROL_WITH_CONFIG, WARN_ST_NO_CLIENT } from '../constants'; /** Test target */ import { ISplitTreatmentsChildProps, ISplitTreatmentsProps, ISplitClientProps } from '../types'; import { SplitTreatments } from '../SplitTreatments'; import { SplitClient } from '../SplitClient'; import { SplitFactory } from '../SplitFactory'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from '../constants'; -import { getStatus } from '../utils'; -import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; 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 as treatments prop the value returned by the function "getControlTreatmentsWithConfig" if the SDK is not ready.', (done) => { - const featureFlagNames = ['split1', 'split2']; + it('passes control treatments (empty object if flagSets is provided) if the SDK is not ready.', () => { render( {({ factory }) => { @@ -41,9 +37,16 @@ describe('SplitTreatments', () => { {({ treatments }: ISplitTreatmentsChildProps) => { const clientMock: any = factory?.client('user1'); - expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(0); - expect(treatments).toEqual(getControlTreatmentsWithConfig(featureFlagNames)); - done(); + expect(clientMock.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client('user1'); + expect(clientMock.getTreatmentsWithConfigByFlagSets).not.toBeCalled(); + expect(treatments).toEqual({}); return null; }} @@ -54,8 +57,7 @@ describe('SplitTreatments', () => { ); }); - it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig" if the SDK is ready.', (done) => { - const featureFlagNames = ['split1', 'split2']; + it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig(ByFlagSets)" if the SDK is ready.', () => { const outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); @@ -65,37 +67,44 @@ describe('SplitTreatments', () => { expect(getStatus(outerFactory.client()).isReady).toBe(isReady); expect(isReady).toBe(true); return ( - - {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { - const clientMock: any = factory?.client(); - 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, 0]); - done(); - return null; - }} - + <> + + {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + 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, 0]); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.results[0].value); + expect(flagSets).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls[0][0]); + return null; + }} + + ); }} ); }); - it('logs error and passes control treatments ("getControlTreatmentsWithConfig") if rendered outside an SplitProvider component.', () => { - const featureFlagNames = ['split1', 'split2']; - let passedTreatments; + it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => { render( {({ treatments }: ISplitTreatmentsChildProps) => { - passedTreatments = treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); return null; }} ); + expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(passedTreatments); }); /** @@ -103,8 +112,6 @@ describe('SplitTreatments', () => { * is not ready doesn't emit errors, and logs meaningful messages instead. */ it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => { - const featureFlagNames = ['split1', 'split2']; - render( {() => { @@ -142,24 +149,37 @@ describe('SplitTreatments', () => { done(); }); + + test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + render( + + {({ treatments }) => { + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + ); + + expect(logSpy).toBeCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + }); }); let renderTimes = 0; /** - * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarily when using SplitTreatments and useSplitTreatments. + * Tests for asserting that client.getTreatmentsWithConfig and client.getTreatmentsWithConfigByFlagSets are not called unnecessarily when using SplitTreatments and useSplitTreatments. */ describe.each([ - ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => ( - + ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => ( + {() => { renderTimes++; return null; }} ), - ({ names, attributes }: { names: string[], attributes: SplitIO.Attributes }) => { - useSplitTreatments({ names, attributes, flagSets: undefined }); + ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => { + useSplitTreatments({ names, flagSets, attributes }); renderTimes++; return null; } @@ -167,8 +187,9 @@ describe.each([ let outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - function Component({ names, attributes, splitKey, clientAttributes }: { - names: ISplitTreatmentsProps['names'] + function Component({ names, flagSets, attributes, splitKey, clientAttributes }: { + names?: ISplitTreatmentsProps['names'] + flagSets?: ISplitTreatmentsProps['flagSets'] attributes: ISplitTreatmentsProps['attributes'] splitKey: ISplitClientProps['splitKey'] clientAttributes?: ISplitClientProps['attributes'] @@ -176,13 +197,14 @@ describe.each([ return ( - + ); } const names = ['split1', 'split2']; + const flagSets = ['set1', 'set2']; const attributes = { att1: 'att1' }; const splitKey = sdkBrowser.core.key; @@ -191,25 +213,27 @@ describe.each([ beforeEach(() => { renderTimes = 0; (outerFactory.client().getTreatmentsWithConfig as jest.Mock).mockClear(); - wrapper = render(); + wrapper = render(); }) afterEach(() => { wrapper.unmount(); // unmount to remove event listener from factory }) - it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names and attributes are the same object.', () => { - wrapper.rerender(); + it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names, flagSets and attributes are the same object.', () => { + wrapper.rerender(); expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); }); - it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names and attributes are equals (shallow comparison).', () => { - wrapper.rerender(); + it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names, flagSets and attributes are equals (shallow comparison).', () => { + wrapper.rerender(); expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); }); it('rerenders and re-evaluates feature flags if names are not equals (shallow array comparison).', () => { @@ -217,6 +241,19 @@ describe.each([ expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); + }); + + it('rerenders and re-evaluates feature flags if flag sets are not equals (shallow array comparison).', () => { + wrapper.rerender(); + wrapper.rerender(); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(1); + + wrapper.rerender(); + + expect(renderTimes).toBe(4); + expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(2); }); it('rerenders and re-evaluates feature flags if attributes are not equals (shallow object comparison).', () => { @@ -265,8 +302,6 @@ describe.each([ it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactory and SplitClient components).', async () => { // changes in SplitContext implies that either the factory, the client (user key), or its status changed, what might imply a change in treatments const outerFactory = SplitSdk(sdkBrowser); - const names = ['split1', 'split2']; - const attributes = { att1: 'att1' }; let renderTimesComp1 = 0; let renderTimesComp2 = 0; diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index 846e104..22d40ae 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -53,6 +53,12 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { return result; }, {}); }); + const getTreatmentsWithConfigByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => { + return flagSets.reduce((result: SplitIO.TreatmentsWithConfig, flagSet: string) => { + result[flagSet + '_feature_flag'] = { treatment: 'on', config: null }; + return result; + }, {}); + }); const setAttributes: jest.Mock = jest.fn((attributes) => { attributesCache = Object.assign(attributesCache, attributes); return true; @@ -87,6 +93,7 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { return Object.assign(Object.create(__emitter__), { getTreatmentsWithConfig, + getTreatmentsWithConfigByFlagSets, track, ready, destroy, diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index f5000d6..6699db3 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -8,15 +8,7 @@ jest.mock('@splitsoftware/splitio/client', () => { }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { CONTROL_WITH_CONFIG, getControlTreatmentsWithConfig } from '../constants'; -const logSpy = jest.spyOn(console, 'log'); +import { CONTROL_WITH_CONFIG } from '../constants'; /** Test target */ import { SplitFactory } from '../SplitFactory'; @@ -26,20 +18,25 @@ import { SplitTreatments } from '../SplitTreatments'; import { SplitContext } from '../SplitContext'; import { ISplitTreatmentsChildProps } from '../types'; +const logSpy = jest.spyOn(console, 'log'); + describe('useSplitTreatments', () => { const featureFlagNames = ['split1']; + const flagSets = ['set1']; const attributes = { att1: 'att1' }; - test('returns the treatments evaluated by the client at Split context updated by SplitFactory, or control if the client is not operational.', () => { + test('returns the treatments evaluated by the client at Split context, or control if the client is not operational.', () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client(); let treatments: SplitIO.TreatmentsWithConfig; + let treatmentsByFlagSets: SplitIO.TreatmentsWithConfig; render( {React.createElement(() => { treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes }).treatments; return null; })} @@ -49,14 +46,21 @@ describe('useSplitTreatments', () => { expect(client.getTreatmentsWithConfig).not.toBeCalled(); expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); + // returns empty treatments object if not operational, without calling `getTreatmentsWithConfigByFlagSets` method + expect(client.getTreatmentsWithConfigByFlagSets).not.toBeCalled(); + expect(treatmentsByFlagSets!).toEqual({}); + // once operational (SDK_READY), it evaluates feature flags act(() => client.__emitter__.emit(Event.SDK_READY)); expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + + expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith(flagSets, attributes); + expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets); }); - test('returns the Treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { + test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client('user2'); let treatments: SplitIO.TreatmentsWithConfig; @@ -83,7 +87,7 @@ describe('useSplitTreatments', () => { expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); }); - test('returns the Treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { + test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { const outerFactory = SplitSdk(sdkBrowser); const client: any = outerFactory.client('user2'); let renderTimes = 0; @@ -122,17 +126,17 @@ describe('useSplitTreatments', () => { }); // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' - test('returns Control Treatments if invoked outside Split context.', () => { - let treatments; - + test('returns control treatments (empty object if flagSets is provided) if invoked outside Split context.', () => { render( React.createElement(() => { - treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + const treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + + const treatmentsByFlagSets = useSplitTreatments({ flagSets: featureFlagNames }).treatments; + expect(treatmentsByFlagSets).toEqual({}); return null; }) ); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(treatments); }); /** @@ -239,4 +243,16 @@ describe('useSplitTreatments', () => { expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); + test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + render( + React.createElement(() => { + const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + return null; + }) + ); + + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + }); + }); diff --git a/src/constants.ts b/src/constants.ts index 978dc58..c5675d0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,12 +30,14 @@ export const getControlTreatmentsWithConfig = (featureFlagNames: unknown): Split }; // Warning and error messages -export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactory. Config prop will be ignored.'; +export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactory. Config prop will be ignored.'; export const ERROR_SF_NO_CONFIG_AND_FACTORY: string = '[ERROR] SplitFactory must receive either a Split config or a Split factory as props.'; export const ERROR_SC_NO_FACTORY: string = '[ERROR] SplitClient does not have access to a Split factory. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; -export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; +export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; + +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'; diff --git a/src/types.ts b/src/types.ts index 8f9fe1e..7746369 100644 --- a/src/types.ts +++ b/src/types.ts @@ -174,21 +174,18 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ +export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & { /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}) + flagSets?: string[]; +} /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -221,18 +218,14 @@ export type ISplitTreatmentsProps = { * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} & ({ /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}) + flagSets?: string[]; +} diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 1e95ab2..1be4757 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,6 +1,5 @@ import React from 'react'; -import { getControlTreatmentsWithConfig } from './constants'; -import { IClientWithContext, memoizeGetTreatmentsWithConfig } from './utils'; +import { memoizeGetTreatmentsWithConfig } from './utils'; import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; @@ -13,15 +12,15 @@ import { useSplitClient } from './useSplitClient'; * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { - const context = useSplitClient({...options, attributes: undefined }); + const context = useSplitClient({ ...options, attributes: undefined }); const { client, lastUpdate } = context; const { names, flagSets, attributes } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); - const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - getTreatmentsWithConfig(client, lastUpdate, names, attributes, { ...client.getAttributes() }, flagSets) : - getControlTreatmentsWithConfig(names); + // Clone `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. + // Note: the same issue occurs with `names` and `attributes` arguments if the user mutates them directly instead of providing a new object. + const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); return { ...context, diff --git a/src/utils.ts b/src/utils.ts index a302f79..3cffbc0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import memoizeOne from 'memoize-one'; import shallowEqual from 'shallowequal'; import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { VERSION } from './constants'; +import { VERSION, WARN_NAMES_AND_FLAGSETS, getControlTreatmentsWithConfig } from './constants'; import { ISplitStatus } from './types'; // Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. @@ -180,6 +180,14 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { shallowEqual(newArgs[5], lastArgs[5]); // flagSets } -function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { - return names ? client.getTreatmentsWithConfig(names, attributes) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes); +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { + if (names && flagSets) console.log(WARN_NAMES_AND_FLAGSETS); + + return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? + names ? + client.getTreatmentsWithConfig(names, attributes) : + client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes) : + names ? + getControlTreatmentsWithConfig(names) : + {} } diff --git a/types/constants.d.ts b/types/constants.d.ts index e382449..e2e823c 100644 --- a/types/constants.d.ts +++ b/types/constants.d.ts @@ -9,3 +9,4 @@ export declare const ERROR_SF_NO_CONFIG_AND_FACTORY: string; export declare const ERROR_SC_NO_FACTORY: string; export declare const WARN_ST_NO_CLIENT: string; export declare const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string; +export declare const WARN_NAMES_AND_FLAGSETS: string; diff --git a/types/types.d.ts b/types/types.d.ts index 8f0caf3..f87f867 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -143,19 +143,16 @@ export interface ISplitClientProps extends IUseSplitClientOptions { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export declare type IUseSplitTreatmentsOptions = IUseSplitClientOptions & ({ +export declare type IUseSplitTreatmentsOptions = IUseSplitClientOptions & { /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}); + flagSets?: string[]; +}; /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -183,16 +180,12 @@ export declare type ISplitTreatmentsProps = { * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); -} & ({ /** * list of feature flag names */ - names: string[]; - flagSets?: undefined; -} | { + names?: string[]; /** * list of feature flag sets */ - flagSets: string[]; - names?: undefined; -}); + flagSets?: string[]; +}; diff --git a/types/utils.d.ts b/types/utils.d.ts index 0465872..f0005cf 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -34,5 +34,5 @@ export declare function initAttributes(client: SplitIO.IBrowserClient | null, at * 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 declare function memoizeGetTreatmentsWithConfig(): typeof evaluateFeatureFlags; -declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient, lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; +declare function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]): import("@splitsoftware/splitio/types/splitio").TreatmentsWithConfig; export {}; From 6fd7c7e0d5578cf790154fcee2de1fce65cf4152 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 17:10:45 -0300 Subject: [PATCH 04/13] Add changelog entry --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 86cafe4..8ca12f3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,9 @@ 1.10.0 (September XX, 2023) + - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` argument to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. + - Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. + - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. + - Updated the following SDK manager methods to expose flag sets on flag views: `manager.splits()` and `manager.split()`. - Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index. For example, `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react';` (Related to issue https://github.com/splitio/react-client/issues/162). - Updated type declarations of the library components to not restrict the type of the `children` prop to ReactElement, allowing to pass any valid ReactNode value (Related to issue https://github.com/splitio/react-client/issues/164). - Updated the `useTreatments` hook to optimize feature flag evaluation. It now uses the `useMemo` hook to memoize calls to the SDK's `getTreatmentsWithConfig` function. This avoids re-evaluating feature flags when the hook is called with the same parameters and the feature flag definitions have not changed. From 3f24e2400cba7e4052fa87f8455e2d41ca3cc212 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Nov 2023 17:11:28 -0300 Subject: [PATCH 05/13] Update JS SDK version --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 455ce50..82f1b0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.9.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.23.2-rc.3", + "@splitsoftware/splitio": "10.24.0-beta", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -1547,9 +1547,9 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.23.2-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", - "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", + "version": "10.24.0-beta", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.0-beta.tgz", + "integrity": "sha512-SpYsWoZKLNXtQjQ5xJLJ2BaLZFZBSH3vRJXuYgf1BpsSv6n0s3Lc1NJ4gDI0zRCvGjWEfLOz6VrdBM0klRao8w==", "dependencies": { "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", @@ -12089,9 +12089,9 @@ } }, "@splitsoftware/splitio": { - "version": "10.23.2-rc.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.3.tgz", - "integrity": "sha512-7fX+smD3lZH4M9WLqLfN9MYA5FxxeQAxwEyaTzFI8h0lrKDk7TNYK7V6bP0WuV503vlH6ZlkesWv/+c+BAJkkw==", + "version": "10.24.0-beta", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.0-beta.tgz", + "integrity": "sha512-SpYsWoZKLNXtQjQ5xJLJ2BaLZFZBSH3vRJXuYgf1BpsSv6n0s3Lc1NJ4gDI0zRCvGjWEfLOz6VrdBM0klRao8w==", "requires": { "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", diff --git a/package.json b/package.json index cb69ffc..0605b11 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.23.2-rc.3", + "@splitsoftware/splitio": "10.24.0-beta", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, From 902c38bc926b84ab3c403f97d8f6fb9d892416d5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 11:57:29 -0300 Subject: [PATCH 06/13] polishing --- CHANGES.txt | 3 +-- src/SplitTreatments.tsx | 4 ++-- src/__tests__/SplitTreatments.test.tsx | 2 +- src/__tests__/useSplitTreatments.test.tsx | 2 +- src/constants.ts | 2 +- src/types.ts | 8 ++++---- src/useSplitTreatments.ts | 8 ++++---- src/utils.ts | 2 +- 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 16cf217..666fbc1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,6 @@ 1.10.0 (November XX, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` argument to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. - - Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. + - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. - Updated the following SDK manager methods to expose flag sets on flag views: `manager.splits()` and `manager.split()`. - Added new `useSplitClient` and `useSplitTreatments` hooks to use instead of `useClient` and `useTreatments` respectively, which are deprecated now. diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index fd89a7a..995f108 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -6,8 +6,8 @@ import { memoizeGetTreatmentsWithConfig } from './utils'; /** * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to - * call the 'client.getTreatmentsWithConfig()' method if a `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method - * if a `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. + * 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. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 87fdacf..605d0a3 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -160,7 +160,7 @@ describe('SplitTreatments', () => { ); - expect(logSpy).toBeCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 6e88c36..47dc3c7 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -252,7 +252,7 @@ describe('useSplitTreatments', () => { }) ); - expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); }); }); diff --git a/src/constants.ts b/src/constants.ts index c5675d0..daa2ba9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,4 +40,4 @@ export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; -export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both "names" and "flagSets" props were provided. "flagSets" will be ignored.'; +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets props were provided. flagSets will be ignored.'; diff --git a/src/types.ts b/src/types.ts index 9bbc96d..cdce977 100644 --- a/src/types.ts +++ b/src/types.ts @@ -172,9 +172,9 @@ export interface ISplitClientProps extends IUseSplitClientOptions { /** * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()' or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. + * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export type IUseSplitTreatmentsOptions = IUseSplitClientOptions & { +export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** * list of feature flag names @@ -205,9 +205,9 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { /** * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, - * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. + * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and pass the result to the child component. */ -export type ISplitTreatmentsProps = { +export interface ISplitTreatmentsProps { /** * An object of type Attributes used to evaluate the feature flags. diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 59fb776..309a6a2 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -5,8 +5,8 @@ import { useSplitClient } from './useSplitClient'; /** * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property object that contains feature flag evaluations. - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method if `names` option is provided, - * or the 'getTreatmentsWithConfigByFlagSets' method if `flagSets` option is provided. + * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'client.getTreatmentsWithConfig()' method if the `names` option is provided, + * or the 'client.getTreatmentsWithConfigByFlagSets()' method if the `flagSets` option is provided. * * @return 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. * @@ -24,8 +24,8 @@ export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitT const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); - // Clone `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. - // Note: the same issue occurs with `names` and `attributes` arguments if the user mutates them directly instead of providing a new object. + // 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); return { diff --git a/src/utils.ts b/src/utils.ts index 82bf7d7..ee42523 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -189,5 +189,5 @@ function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes) : names ? getControlTreatmentsWithConfig(names) : - {} + {} // empty object when evaluating with flag sets and client is not ready } From 9ea53a093d81f8f9c70f8c268a8f66e46325656d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 12:13:51 -0300 Subject: [PATCH 07/13] Update type definition comment for names and flagSets params --- src/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index cdce977..4179e93 100644 --- a/src/types.ts +++ b/src/types.ts @@ -177,12 +177,12 @@ export interface ISplitClientProps extends IUseSplitClientOptions { export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { /** - * list of feature flag names + * list of feature flag names to evaluate. If provided, the `flagSets` option is ignored. */ names?: string[]; /** - * list of feature flag sets + * list of feature flag sets to evaluate. */ flagSets?: string[]; } @@ -220,12 +220,12 @@ export interface ISplitTreatmentsProps { children: ((props: ISplitTreatmentsChildProps) => ReactNode); /** - * list of feature flag names + * list of feature flag names to evaluate. If provided, the `flagSets` prop is ignored. */ names?: string[]; /** - * list of feature flag sets + * list of feature flag sets to evaluate. */ flagSets?: string[]; } From 2a59f3652f9225d053f4c8df622f85a019ecdd55 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 17:45:45 -0300 Subject: [PATCH 08/13] Reuse GetTreatmentsOptions type --- src/types.ts | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/types.ts b/src/types.ts index 4179e93..ae43b2c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,23 +170,30 @@ export interface ISplitClientProps extends IUseSplitClientOptions { children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } -/** - * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, - * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. - */ -export interface IUseSplitTreatmentsOptions extends IUseSplitClientOptions { +export type GetTreatmentsOptions = { /** - * list of feature flag names to evaluate. If provided, the `flagSets` option is ignored. + * 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[]; /** - * list of feature flag sets to evaluate. + * 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[]; + + /** + * An object of type Attributes used to evaluate the feature flags. + */ + attributes?: SplitIO.Attributes; } +/** + * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, + * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. + */ +export interface IUseSplitTreatmentsOptions extends GetTreatmentsOptions, IUseSplitClientOptions { } + /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -207,25 +214,10 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and pass the result to the child component. */ -export interface ISplitTreatmentsProps { - - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; +export interface ISplitTreatmentsProps extends GetTreatmentsOptions { /** * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ children: ((props: ISplitTreatmentsChildProps) => ReactNode); - - /** - * list of feature flag names to evaluate. If provided, the `flagSets` prop is ignored. - */ - names?: string[]; - - /** - * list of feature flag sets to evaluate. - */ - flagSets?: string[]; } From accf9d26b106b68c908c52abacf7b87b32b71093 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 17:53:24 -0300 Subject: [PATCH 09/13] Specialize the GetTreatmentsOptions type to mutually exclude names and flagSets properties --- src/__tests__/SplitTreatments.test.tsx | 3 +++ src/__tests__/useSplitTreatments.test.tsx | 4 ++++ src/types.ts | 14 +++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 605d0a3..69602fd 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -152,6 +152,7 @@ describe('SplitTreatments', () => { test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { render( + // @ts-expect-error flagSets and names are mutually exclusive {({ treatments }) => { expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); @@ -171,6 +172,7 @@ let renderTimes = 0; */ describe.each([ ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => ( + // @ts-expect-error names and flagSets are mutually exclusive {() => { renderTimes++; @@ -179,6 +181,7 @@ describe.each([ ), ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => { + // @ts-expect-error names and flagSets are mutually exclusive useSplitTreatments({ names, flagSets, attributes }); renderTimes++; return null; diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 47dc3c7..3364a87 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -37,6 +37,9 @@ describe('useSplitTreatments', () => { {React.createElement(() => { treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes }).treatments; + + // @ts-expect-error Options object must provide either names or flagSets + expect(useSplitTreatments({}).treatments).toEqual({}); return null; })} @@ -246,6 +249,7 @@ describe('useSplitTreatments', () => { test('ignores flagSets and logs a warning 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; expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); return null; diff --git a/src/types.ts b/src/types.ts index ae43b2c..81b735e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,17 +170,21 @@ export interface ISplitClientProps extends IUseSplitClientOptions { children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } -export type GetTreatmentsOptions = { +export type GetTreatmentsOptions = ({ /** * 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[]; + 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[]; + flagSets: string[]; + names?: undefined; +}) & { /** * An object of type Attributes used to evaluate the feature flags. @@ -192,7 +196,7 @@ export type GetTreatmentsOptions = { * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and retrieve the result together with the Split context. */ -export interface IUseSplitTreatmentsOptions extends GetTreatmentsOptions, IUseSplitClientOptions { } +export type IUseSplitTreatmentsOptions = GetTreatmentsOptions & IUseSplitClientOptions; /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. @@ -214,7 +218,7 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, * used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', and pass the result to the child component. */ -export interface ISplitTreatmentsProps extends GetTreatmentsOptions { +export type ISplitTreatmentsProps = GetTreatmentsOptions & { /** * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. From 44c4b90081b9fa6bd074a1512424559a5271a49f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Nov 2023 17:57:19 -0300 Subject: [PATCH 10/13] prepare rc --- .github/workflows/ci.yml | 4 ++-- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3a8b1..4262bf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} - name: Store assets - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/development') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/flagSets_xor') uses: actions/upload-artifact@v3 with: name: assets @@ -88,7 +88,7 @@ jobs: name: Upload assets runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/development' + if: github.event_name == 'push' && github.ref == 'refs/heads/flagSets_xor' strategy: matrix: environment: diff --git a/CHANGES.txt b/CHANGES.txt index 6e62c68..e3a50d3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.10.0 (November XX, 2023) +1.10.0 (November 16, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. diff --git a/package-lock.json b/package-lock.json index b112dcf..b454bd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.0", + "version": "1.9.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.0", + "version": "1.9.1-rc.1", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.24.0-beta", diff --git a/package.json b/package.json index 59d2a09..3aad27b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.0", + "version": "1.9.1-rc.1", "description": "A React library to easily integrate and use Split JS SDK", "main": "lib/index.js", "module": "es/index.js", From a46a180d9498a5df2a3c7dbc7be073c27b9c93c5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 14:20:05 -0300 Subject: [PATCH 11/13] polishing --- .github/workflows/ci.yml | 4 ++-- src/__tests__/SplitTreatments.test.tsx | 2 +- src/__tests__/useSplitTreatments.test.tsx | 2 +- src/constants.ts | 2 +- src/useClient.ts | 3 ++- src/useManager.ts | 3 ++- src/useSplitClient.ts | 2 +- src/useSplitManager.ts | 2 +- src/useSplitTreatments.ts | 2 +- src/useTrack.ts | 3 ++- src/useTreatments.ts | 3 ++- 11 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4262bf8..bc3a8b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} - name: Store assets - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/flagSets_xor') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/development') uses: actions/upload-artifact@v3 with: name: assets @@ -88,7 +88,7 @@ jobs: name: Upload assets runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/flagSets_xor' + if: github.event_name == 'push' && github.ref == 'refs/heads/development' strategy: matrix: environment: diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 69602fd..13d8990 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -161,7 +161,7 @@ describe('SplitTreatments', () => { ); - expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); + expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 3364a87..cbdc62d 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -256,7 +256,7 @@ describe('useSplitTreatments', () => { }) ); - expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets props were provided. flagSets will be ignored.'); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/constants.ts b/src/constants.ts index daa2ba9..47020ef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,4 +40,4 @@ export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; -export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets props were provided. flagSets will be ignored.'; +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/useClient.ts b/src/useClient.ts index 7f41c58..535eb98 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -5,7 +5,8 @@ import { useSplitClient } from './useSplitClient'; * It uses the 'useContext' hook to access the context, which is updated by * SplitFactory and SplitClient components in the hierarchy of components. * - * @return A Split Client instance, or null if used outside the scope of SplitFactory + * @returns A Split Client instance, or null if used outside the scope of SplitFactory + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} * * @deprecated Replace with the new `useSplitClient` hook. diff --git a/src/useManager.ts b/src/useManager.ts index b5771c2..9ba101b 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -5,7 +5,8 @@ import { useSplitManager } from './useSplitManager'; * It uses the 'useContext' hook to access the factory at Split context, which is updated by * the SplitFactory component. * - * @return A Split Manager instance, or null if used outside the scope of SplitFactory + * @returns A Split Manager instance, or null if used outside the scope of SplitFactory + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} * * @deprecated Replace with the new `useSplitManager` hook. diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 5e2e6a6..a0c696c 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -14,7 +14,7 @@ export const DEFAULT_UPDATE_OPTIONS = { * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. * It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components. * - * @return A Split Context object + * @returns A Split Context object * * @example * ```js diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index d6180b2..1ba6a23 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -6,7 +6,7 @@ import { ISplitContextValues } from './types'; * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. * - * @return An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * @returns An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory * * @example * ```js diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 309a6a2..d5dbf6d 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -8,7 +8,7 @@ import { useSplitClient } from './useSplitClient'; * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'client.getTreatmentsWithConfig()' method if the `names` option is provided, * or the 'client.getTreatmentsWithConfigByFlagSets()' method if the `flagSets` option is provided. * - * @return 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. + * @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 diff --git a/src/useTrack.ts b/src/useTrack.ts index 108885e..b511018 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -7,7 +7,8 @@ const noOpFalse = () => false; * 'useTrack' is a hook that returns the track method from a Split client. * It uses the 'useContext' hook to access the client from the Split context. * - * @return A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * @returns A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { diff --git a/src/useTreatments.ts b/src/useTreatments.ts index a794eb6..bd673fa 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -5,7 +5,8 @@ import { useSplitTreatments } from './useSplitTreatments'; * It uses the 'useContext' hook to access the client from the Split context, * and invokes the 'getTreatmentsWithConfig' method. * - * @return A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. + * @returns A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} * * @deprecated Replace with the new `useSplitTreatments` hook. From fab73a0cd87fccea7a421b79e1c7dc9263184b1d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 14:55:31 -0300 Subject: [PATCH 12/13] prepare stable version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b454bd5..f1c9717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.1", + "version": "1.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.1", + "version": "1.10.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.24.0-beta", diff --git a/package.json b/package.json index 3aad27b..4a497b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.1-rc.1", + "version": "1.10.0", "description": "A React library to easily integrate and use Split JS SDK", "main": "lib/index.js", "module": "es/index.js", From 263ee5cee6f7e2e5d7bd432dd1c46c704dafd30f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Nov 2023 16:05:55 -0300 Subject: [PATCH 13/13] update changes entry --- CHANGES.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8c012a3..053cfd6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,15 +2,14 @@ - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. - - Updated the following SDK manager methods to expose flag sets on flag views: `manager.splits()` and `manager.split()`. + - Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views. - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks as replacements for the now deprecated `useClient`, `useTreatments` and `useManager` hooks. - These new hooks return the Split context object along with the SDK client, treatments and manager respectively, enabling direct access to status properties like `isReady`, eliminating the need for using the `useContext` hook or the client's `ready` promise. - `useSplitClient` and `useSplitTreatments` accept an options object as parameter, which support the same arguments as their predecessors, with additional boolean options for controlling re-rendering: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`. - `useSplitTreatments` optimizes feature flag evaluations by using the `useMemo` hook to memoize `getTreatmentsWithConfig` method calls from the SDK. This avoids re-evaluating feature flags when the hook is called with the same options and the feature flag definitions have not changed. - They fixed a bug in the deprecated `useClient` and `useTreatments` hooks, which caused them to not re-render and re-evaluate feature flags when they access a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event). - - Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index. For example, `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react'` (Related to issue https://github.com/splitio/react-client/issues/162). + - Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index, e.g., `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react'` (Related to issue https://github.com/splitio/react-client/issues/162). - Updated type declarations of the library components to not restrict the type of the `children` prop to ReactElement, allowing to pass any valid ReactNode value (Related to issue https://github.com/splitio/react-client/issues/164). - - Updated the `useTreatments` hook to optimize feature flag evaluations. - Updated linter and other dependencies for vulnerability fixes. - Bugfixing - Removed conditional code within hooks to adhere to the rules of hooks and prevent React warnings. Previously, this code checked for the availability of the hooks API (available in React version 16.8.0 or above) and logged an error message. Now, using hooks with React versions below 16.8.0 will throw an error. - Bugfixing - Updated `useClient` and `useTreatments` hooks to re-render and re-evaluate feature flags when they consume a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event).