Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,61 @@
import { CONTROL, CONTROL_WITH_CONFIG } from '../constants';
import { getControlTreatments } from '../utils';
import { getTreatments, getTreatment } from '../utils';
import { sdkBrowserWithConfig } from './testUtils/sdkConfigs';

describe('getControlTreatments', () => {
const factoryWithoutFallbacks = {
settings: {}
} as SplitIO.IBrowserSDK;

const factoryWithFallbacks = {
settings: sdkBrowserWithConfig
} as SplitIO.IBrowserSDK

describe('getTreatments', () => {

it('should return an empty object if an empty array is provided', () => {
expect(Object.values(getControlTreatments([], true)).length).toBe(0);
expect(Object.values(getControlTreatments([], false)).length).toBe(0);
expect(getTreatments([], true)).toEqual({});
expect(getTreatments([], false)).toEqual({});
});

it('should return an object with control treatments if an array of feature flag names is provided', () => {
const featureFlagNames = ['split1', 'split2'];
const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getControlTreatments(featureFlagNames, true);
featureFlagNames.forEach((featureFlagName) => {
expect(treatmentsWithConfig[featureFlagName]).toBe(CONTROL_WITH_CONFIG);
});
expect(Object.keys(treatmentsWithConfig).length).toBe(featureFlagNames.length);

const treatments: SplitIO.Treatments = getControlTreatments(featureFlagNames, false);
featureFlagNames.forEach((featureFlagName) => {
expect(treatments[featureFlagName]).toBe(CONTROL);
});
expect(Object.keys(treatments).length).toBe(featureFlagNames.length);
const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getTreatments(featureFlagNames, true);
expect(treatmentsWithConfig).toEqual({ 'split1': CONTROL_WITH_CONFIG, 'split2': CONTROL_WITH_CONFIG });

const treatments: SplitIO.Treatments = getTreatments(featureFlagNames, false);
expect(treatments).toEqual({ 'split1': CONTROL, 'split2': CONTROL });

expect(getTreatments(featureFlagNames, true, factoryWithoutFallbacks)).toEqual({ 'split1': CONTROL_WITH_CONFIG, 'split2': CONTROL_WITH_CONFIG });
expect(getTreatments(featureFlagNames, false, factoryWithoutFallbacks)).toEqual({ 'split1': CONTROL, 'split2': CONTROL });
});

it('should return an object with fallback or control treatments if an array of feature flag names and factory are provided', () => {
const featureFlagNames = ['split1', 'ff1'];
const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getTreatments(featureFlagNames, true, factoryWithFallbacks);
expect(treatmentsWithConfig).toEqual({ 'split1': { treatment: 'control_global', config: null }, 'ff1': { treatment: 'control_ff1', config: 'control_ff1_config' } });

const treatments: SplitIO.Treatments = getTreatments(featureFlagNames, false, factoryWithFallbacks);
expect(treatments).toEqual({ 'split1': 'control_global', 'ff1': 'control_ff1' });
});

});

describe('getTreatment', () => {

it('should return control treatments', () => {
expect(getTreatment('any', true)).toEqual(CONTROL_WITH_CONFIG);
expect(getTreatment('any', false)).toEqual(CONTROL);

expect(getTreatment('any', true, factoryWithoutFallbacks)).toEqual(CONTROL_WITH_CONFIG);
expect(getTreatment('any', false, factoryWithoutFallbacks)).toEqual(CONTROL);
});

it('should return fallback treatments if a factory with fallback treatments is provided', () => {
const treatmentWithConfig: SplitIO.TreatmentWithConfig = getTreatment('split1', true, factoryWithFallbacks);
expect(treatmentWithConfig).toEqual({ treatment: 'control_global', config: null });

const treatment: SplitIO.Treatment = getTreatment('ff1', false, factoryWithFallbacks);
expect(treatment).toEqual('control_ff1' );
});

});
6 changes: 3 additions & 3 deletions src/__tests__/withSplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { INITIAL_STATUS } from './testUtils/utils';
import { withSplitFactory } from '../withSplitFactory';
import { withSplitClient } from '../withSplitClient';
import { withSplitTreatments } from '../withSplitTreatments';
import { getControlTreatments } from '../utils';
import { getTreatments } from '../utils';

const featureFlagNames = ['split1', 'split2'];

describe('withSplitTreatments', () => {

it(`passes Split props and outer props to the child.
In this test, the value of "props.treatments" is obtained by the function "getControlTreatments",
In this test, the value of "props.treatments" is obtained by the function "getTreatments",
and not "client.getTreatmentsWithConfig" since the client is not ready.`, () => {

const Component = withSplitFactory(sdkBrowser)<{ outerProp1: string, outerProp2: number }>(
Expand All @@ -36,7 +36,7 @@ describe('withSplitTreatments', () => {
...INITIAL_STATUS,
factory: factory, client: clientMock,
outerProp1: 'outerProp1', outerProp2: 2,
treatments: getControlTreatments(featureFlagNames, true),
treatments: getTreatments(featureFlagNames, true),
});

return null;
Expand Down
13 changes: 12 additions & 1 deletion src/useTreatment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import * as React from 'react';
import { memoizeGetTreatment } from './utils';
import memoizeOne from 'memoize-one';
import { argsAreEqual, getTreatment } from './utils';
import { IUseTreatmentResult, IUseTreatmentOptions } from './types';
import { useSplitClient } from './useSplitClient';

function evaluateFeatureFlag(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
return client && client.getStatus().isOperational ?
client.getTreatment(names[0], attributes, options) :
getTreatment(names[0], false, factory);
}

function memoizeGetTreatment() {
return memoizeOne(evaluateFeatureFlag, argsAreEqual);
}

/**
* `useTreatment` is a hook that returns an Split Context object extended with a `treatment` property.
* It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatment()` method.
Expand Down
13 changes: 12 additions & 1 deletion src/useTreatmentWithConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import * as React from 'react';
import { memoizeGetTreatmentWithConfig } from './utils';
import memoizeOne from 'memoize-one';
import { argsAreEqual, getTreatment } from './utils';
import { IUseTreatmentWithConfigResult, IUseTreatmentOptions } from './types';
import { useSplitClient } from './useSplitClient';

function evaluateFeatureFlagWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
return client && client.getStatus().isOperational ?
client.getTreatmentWithConfig(names[0], attributes, options) :
getTreatment(names[0], true, factory);
}

function memoizeGetTreatmentWithConfig() {
return memoizeOne(evaluateFeatureFlagWithConfig, argsAreEqual);
}

/**
* `useTreatmentWithConfig` is a hook that returns an Split Context object extended with a `treatment` property.
* It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentWithConfig()` method.
Expand Down
17 changes: 16 additions & 1 deletion src/useTreatments.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import * as React from 'react';
import { memoizeGetTreatments } from './utils';
import memoizeOne from 'memoize-one';
import { argsAreEqual, getTreatments } from './utils';
import { IUseTreatmentsResult, IUseTreatmentsOptions } from './types';
import { useSplitClient } from './useSplitClient';

function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
return client && client.getStatus().isOperational && (names || flagSets) ?
names ?
client.getTreatments(names, attributes, options) :
client.getTreatmentsByFlagSets(flagSets!, attributes, options) :
names ?
getTreatments(names, false, factory) :
{} // empty object when evaluating with flag sets and client is not ready
}

export function memoizeGetTreatments() {
return memoizeOne(evaluateFeatureFlags, argsAreEqual);
}

/**
* `useTreatments` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations.
* It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatments()` method if the `names` option is provided,
Expand Down
17 changes: 16 additions & 1 deletion src/useTreatmentsWithConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import * as React from 'react';
import { memoizeGetTreatmentsWithConfig } from './utils';
import memoizeOne from 'memoize-one';
import { argsAreEqual, getTreatments } from './utils';
import { IUseTreatmentsOptions, IUseTreatmentsWithConfigResult } from './types';
import { useSplitClient } from './useSplitClient';

function evaluateFeatureFlagsWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) {
return client && client.getStatus().isOperational && (names || flagSets) ?
names ?
client.getTreatmentsWithConfig(names, attributes, options) :
client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) :
names ?
getTreatments(names, true, factory) :
{} // empty object when evaluating with flag sets and client is not ready
}

function memoizeGetTreatmentsWithConfig() {
return memoizeOne(evaluateFeatureFlagsWithConfig, argsAreEqual);
}

/**
* `useTreatmentsWithConfig` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations.
* It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentsWithConfig()` method if the `names` option is provided,
Expand Down
69 changes: 10 additions & 59 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import memoizeOne from 'memoize-one';
import shallowEqual from 'shallowequal';
import { CONTROL, CONTROL_WITH_CONFIG } from './constants';
import { ISplitStatus } from './types';
Expand Down Expand Up @@ -40,12 +39,12 @@ export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: Spl
if (client && attributes) client.setAttributes(attributes);
}

// Utils used to retrieve treatments when the client is not operational:
// Utils used to retrieve fallback or control treatments when the client is not operational:

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;
function resolveFallback(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK) {
export function getTreatment(flagName: string, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentWithConfig;
export function getTreatment(flagName: string, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment;
export function getTreatment(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment | SplitIO.TreatmentWithConfig;
export function getTreatment(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK) {
if (factory && factory.settings.fallbackTreatments) {
const fallbacks = factory.settings.fallbackTreatments;

Expand All @@ -61,9 +60,9 @@ function resolveFallback(flagName: string, withConfig: boolean, factory?: SplitI
return withConfig ? CONTROL_WITH_CONFIG : CONTROL;
}

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) {
export function getTreatments(featureFlagNames: unknown, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentsWithConfig;
export function getTreatments(featureFlagNames: unknown, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatments;
export function getTreatments(featureFlagNames: unknown, withConfig: boolean, factory?: SplitIO.IBrowserSDK) {
// validate feature flag names
if (!Array.isArray(featureFlagNames)) return {};

Expand All @@ -74,7 +73,7 @@ export function getControlTreatments(featureFlagNames: unknown, withConfig: bool

// return control or fallback treatment for each validated feature flag name
return (featureFlagNames as string[]).reduce((pValue: SplitIO.Treatments | SplitIO.TreatmentsWithConfig, featureFlagName: string) => {
pValue[featureFlagName] = resolveFallback(featureFlagName, withConfig, factory);
pValue[featureFlagName] = getTreatment(featureFlagName, withConfig, factory);
return pValue;
}, {});
}
Expand All @@ -84,59 +83,11 @@ export function getControlTreatments(featureFlagNames: unknown, withConfig: bool
* The result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag names and attributes.
*/

function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
export function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean {
return newArgs[0] === lastArgs[0] && // client
newArgs[1] === lastArgs[1] && // lastUpdate
shallowEqual(newArgs[2], lastArgs[2]) && // names
shallowEqual(newArgs[3], lastArgs[3]) && // attributes
shallowEqual(newArgs[4], lastArgs[4]) && // client attributes
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, 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, factory) :
{} // empty object when evaluating with flag sets and client is not ready
}

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, 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, factory) :
{} // empty object when evaluating with flag sets and client is not ready
}

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, factory?: SplitIO.IBrowserSDK) {
return client && client.getStatus().isOperational ?
client.getTreatmentWithConfig(names[0], attributes, options) :
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, factory?: SplitIO.IBrowserSDK) {
return client && client.getStatus().isOperational ?
client.getTreatment(names[0], attributes, options) :
resolveFallback(names[0], false, factory);
}

export function memoizeGetTreatment() {
return memoizeOne(evaluateFeatureFlag, argsAreEqual);
}