diff --git a/CHANGES.txt b/CHANGES.txt index 1827849..a2851ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +2.6.0 (October 31, 2025) + - Updated @splitsoftware/splitio package to version 11.8.0 that includes minor updates: + - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. + - Added support for custom loggers: added `logger` configuration option and `factory.Logger.setLogger` method to allow the SDK to use a custom logger. + - Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted. + 2.5.0 (September 18, 2025) - Updated @splitsoftware/splitio package to version 11.6.0 that includes minor updates: - Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`. diff --git a/package-lock.json b/package-lock.json index d2c6e9f..310bfe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.5.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "11.6.0", + "@splitsoftware/splitio": "11.7.2-rc.4", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -1583,12 +1583,12 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.0.tgz", - "integrity": "sha512-48sksG00073Nltma/BxpH6xHVZmoBjank40EU4h+XqrMGm0qM3jGngPO9R/iWAHdSduUWAoMJVJYA68AtvKgeQ==", + "version": "11.7.2-rc.4", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.2-rc.4.tgz", + "integrity": "sha512-1A26oJ82JLmBC4OhRJoNgrESVCN+HErqrduiI3J88oznxIPjRVQGJt/BhfF87FhNXOXvtgx+4dF15Pje6zCq7A==", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.6.0", + "@splitsoftware/splitio-commons": "2.7.9-rc.2", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -1601,9 +1601,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.0.tgz", - "integrity": "sha512-0xODXLciIvHSuMlb8eukIB2epb3ZyGOsrwS0cMuTdxEvCqr7Nuc9pWDdJtRuN1UwL/jIjBnpDYAc8s6mpqLX2g==", + "version": "2.7.9-rc.2", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.2.tgz", + "integrity": "sha512-t8YVwDe4UBvD95w+mvKq7Z2khozZXDrIuOWt3ixxtmyeyoZp5L0L9x9E3DWOcQ0EVxfpQv+tAErHG3bw3LkbNg==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -12095,11 +12095,11 @@ } }, "@splitsoftware/splitio": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.0.tgz", - "integrity": "sha512-48sksG00073Nltma/BxpH6xHVZmoBjank40EU4h+XqrMGm0qM3jGngPO9R/iWAHdSduUWAoMJVJYA68AtvKgeQ==", + "version": "11.7.2-rc.4", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.2-rc.4.tgz", + "integrity": "sha512-1A26oJ82JLmBC4OhRJoNgrESVCN+HErqrduiI3J88oznxIPjRVQGJt/BhfF87FhNXOXvtgx+4dF15Pje6zCq7A==", "requires": { - "@splitsoftware/splitio-commons": "2.6.0", + "@splitsoftware/splitio-commons": "2.7.9-rc.2", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -12109,9 +12109,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.0.tgz", - "integrity": "sha512-0xODXLciIvHSuMlb8eukIB2epb3ZyGOsrwS0cMuTdxEvCqr7Nuc9pWDdJtRuN1UwL/jIjBnpDYAc8s6mpqLX2g==", + "version": "2.7.9-rc.2", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.2.tgz", + "integrity": "sha512-t8YVwDe4UBvD95w+mvKq7Z2khozZXDrIuOWt3ixxtmyeyoZp5L0L9x9E3DWOcQ0EVxfpQv+tAErHG3bw3LkbNg==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index e168d84..4c8e21c 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "11.6.0", + "@splitsoftware/splitio": "11.7.2-rc.4", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx index 05bbbca..e76830c 100644 --- a/src/SplitFactoryProvider.tsx +++ b/src/SplitFactoryProvider.tsx @@ -51,7 +51,7 @@ export function SplitFactoryProvider(props: ISplitFactoryProviderProps) { // Effect to initialize and destroy the factory when config is provided React.useEffect(() => { if (propFactory) { - if (config) console.log(WARN_SF_CONFIG_AND_FACTORY); + if (config) (propFactory.settings as any).log.warn(WARN_SF_CONFIG_AND_FACTORY); return; } diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 2d1bf1b..a94aeb0 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -56,6 +56,7 @@ describe('SplitClient', () => { client: outerFactory.client(), isReady: true, isReadyFromCache: true, + isOperational: true, lastUpdate: getStatus(outerFactory.client()).lastUpdate }); @@ -141,7 +142,7 @@ describe('SplitClient', () => { expect(statusProps).toStrictEqual([false, false, true, true]); break; case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. - expect(statusProps).toStrictEqual([true, false, true, false]); + expect(statusProps).toStrictEqual([true, true, true, false]); break; default: fail('Child must not be rerendered'); @@ -182,7 +183,7 @@ describe('SplitClient', () => { expect(statusProps).toStrictEqual([false, false, false, false]); break; case 1: // Ready - expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state break; default: fail('Child must not be rerendered'); @@ -214,7 +215,7 @@ describe('SplitClient', () => { count++; // side effect in the render phase - if (!(client as any).__getStatus().isReady) { + if (!client!.getStatus().isReady) { (client as any).__emitter__.emit(Event.SDK_READY); } @@ -318,11 +319,11 @@ describe('SplitClient', () => { break; case 4: expect(client).toBe(outerFactory.client('user3')); - expect(statusProps).toStrictEqual([true, false, false, false]); + expect(statusProps).toStrictEqual([true, true, false, false]); break; case 5: expect(client).toBe(outerFactory.client('user3')); - expect(statusProps).toStrictEqual([true, false, false, false]); + expect(statusProps).toStrictEqual([true, true, false, false]); break; default: fail('Child must not be rerendered'); @@ -501,7 +502,7 @@ describe('SplitFactoryProvider + SplitClient', () => { expect(statusProps).toStrictEqual([false, false, true, true]); break; case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. - expect(statusProps).toStrictEqual([true, false, true, false]); + expect(statusProps).toStrictEqual([true, true, true, false]); break; default: fail('Child must not be rerendered'); @@ -542,7 +543,7 @@ describe('SplitFactoryProvider + SplitClient', () => { expect(statusProps).toStrictEqual([false, false, true, true]); break; case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. - expect(statusProps).toStrictEqual([true, false, true, false]); + expect(statusProps).toStrictEqual([true, true, true, false]); break; default: fail('Child must not be rerendered'); @@ -578,7 +579,7 @@ describe('SplitFactoryProvider + SplitClient', () => { expect(statusProps).toStrictEqual([false, false, false, false]); break; case 1: // Ready - expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state break; default: fail('Child must not be rerendered'); @@ -615,7 +616,7 @@ describe('SplitFactoryProvider + SplitClient', () => { expect(statusProps).toStrictEqual([false, false, false, false]); break; case 1: // Ready - expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state break; default: fail('Child must not be rerendered'); diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx index 33f0977..86fd386 100644 --- a/src/__tests__/SplitFactoryProvider.test.tsx +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -70,6 +70,7 @@ describe('SplitFactoryProvider', () => { client: outerFactory.client(), isReady: true, isReadyFromCache: true, + isOperational: true, lastUpdate: getStatus(outerFactory.client()).lastUpdate }); return null; @@ -113,7 +114,7 @@ describe('SplitFactoryProvider', () => { ); - expect(logSpy).toBeCalledWith(WARN_SF_CONFIG_AND_FACTORY); + expect(logSpy).toBeCalledWith('[WARN] splitio => ' + WARN_SF_CONFIG_AND_FACTORY); logSpy.mockRestore(); }); diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index cf21ea4..243ddb6 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -19,15 +19,11 @@ import { SplitClient } from '../SplitClient'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { useSplitTreatments } from '../useSplitTreatments'; -const logSpy = jest.spyOn(console, 'log'); - describe('SplitTreatments', () => { const featureFlagNames = ['split1', 'split2']; const flagSets = ['set1', 'set2']; - afterEach(() => { logSpy.mockClear() }); - it('passes control treatments (empty object if flagSets is provided) if the SDK is not operational.', () => { render( @@ -73,7 +69,7 @@ describe('SplitTreatments', () => { expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); - expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, getStatus(outerFactory.client()).lastUpdate]); + expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, true, false, false, false, getStatus(outerFactory.client()).lastUpdate]); return null; }} @@ -105,10 +101,9 @@ describe('SplitTreatments', () => { }); /** - * Input validation. Passing invalid feature flag names or attributes while the Sdk - * is not ready doesn't emit errors, and logs meaningful messages instead. + * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready. */ - it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => { + it('Input validation: invalid names are sanitized.', () => { render( @@ -130,9 +125,9 @@ describe('SplitTreatments', () => { }} {/* @ts-expect-error Test error handling */} - + {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({}); + expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG }); return null; }} @@ -142,14 +137,9 @@ describe('SplitTreatments', () => { ); - expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.'); - expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.'); - - done(); }); - - test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + test('ignores flagSets if both names and flagSets params are provided.', () => { render( {/* @ts-expect-error flagSets and names are mutually exclusive */} @@ -161,8 +151,6 @@ describe('SplitTreatments', () => { ); - - expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { @@ -190,7 +178,7 @@ describe('SplitTreatments', () => { act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes, undefined); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments!); }); }); diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts index 09dca47..b7ea1e2 100644 --- a/src/__tests__/testUtils/mockSplitFactory.ts +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -12,6 +12,13 @@ export const Event = { SDK_UPDATE: 'state::update', }; +const DEFAULT_LOGGER: SplitIO.Logger = { + error(msg) { console.log('[ERROR] splitio => ' + msg); }, + warn(msg) { console.log('[WARN] splitio => ' + msg); }, + info(msg) { console.log('[INFO] splitio => ' + msg); }, + debug(msg) { console.log('[DEBUG] splitio => ' + msg); }, +}; + function parseKey(key: SplitIO.SplitKey): SplitIO.SplitKey { if (key && typeof key === 'object' && key.constructor === Object) { return { @@ -47,7 +54,7 @@ export function mockSdk() { } const __emitter__ = new EventEmitter(); - __emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_READY, () => { isReady = true; isReadyFromCache = true; syncLastUpdate(); }); __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); }); __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); @@ -89,13 +96,13 @@ export function mockSdk() { else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } }); }); - const __getStatus = () => ({ + const getStatus = () => ({ isReady, isReadyFromCache, isTimedout: hasTimedout && !isReady, hasTimedout, isDestroyed, - isOperational: (isReady || isReadyFromCache) && !isDestroyed, + isOperational: isReadyFromCache && !isDestroyed, lastUpdate, }); const destroy: jest.Mock = jest.fn(() => { @@ -115,10 +122,9 @@ export function mockSdk() { setAttributes, clearAttributes, getAttributes, + getStatus, // EventEmitter exposed to trigger events manually __emitter__, - // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) - __getStatus, // Restore the mock client to its initial NO-READY status. // Useful when you want to reuse the same mock between tests after emitting events or destroying the instance. __restore() { @@ -154,6 +160,7 @@ export function mockSdk() { __clients__, settings: Object.assign({ version: jsSdkVersion, + log: DEFAULT_LOGGER }, config), }; diff --git a/src/__tests__/testUtils/utils.tsx b/src/__tests__/testUtils/utils.tsx index 7571873..9c05ec5 100644 --- a/src/__tests__/testUtils/utils.tsx +++ b/src/__tests__/testUtils/utils.tsx @@ -123,6 +123,7 @@ export const INITIAL_STATUS: ISplitStatus & IUpdateProps = { hasTimedout: false, lastUpdate: 0, isDestroyed: false, + isOperational: false, updateOnSdkReady: true, updateOnSdkReadyFromCache: true, updateOnSdkTimedout: true, diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index ea56ad9..5e4bb03 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -207,7 +207,7 @@ describe('useSplitClient', () => { // side effect in the render phase const client = outerFactory.client('some_user') as any; - if (!client.__getStatus().isReady) client.__emitter__.emit(Event.SDK_READY); + if (!client.getStatus().isReady) client.__emitter__.emit(Event.SDK_READY); return null; })} @@ -256,7 +256,7 @@ describe('useSplitClient', () => { act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render expect(rendersCount).toBe(5); - expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: false, hasTimedout: true }); + expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: true, hasTimedout: true }); act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false expect(rendersCount).toBe(5); diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx index 9707042..f8aff53 100644 --- a/src/__tests__/useSplitManager.test.tsx +++ b/src/__tests__/useSplitManager.test.tsx @@ -48,8 +48,9 @@ describe('useSplitManager', () => { hasTimedout: false, isDestroyed: false, isReady: true, - isReadyFromCache: false, + isReadyFromCache: true, isTimedout: false, + isOperational: true, lastUpdate: getStatus(outerFactory.client()).lastUpdate, }); }); @@ -98,8 +99,9 @@ describe('useSplitManager', () => { hasTimedout: false, isDestroyed: false, isReady: true, - isReadyFromCache: false, + isReadyFromCache: true, isTimedout: false, + isOperational: true, lastUpdate: getStatus(outerFactory.client()).lastUpdate, }); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index c93454d..bf32348 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -16,8 +16,6 @@ import { useSplitTreatments } from '../useSplitTreatments'; import { SplitContext } from '../SplitContext'; import { ISplitTreatmentsChildProps } from '../types'; -const logSpy = jest.spyOn(console, 'log'); - describe('useSplitTreatments', () => { const featureFlagNames = ['split1']; @@ -56,10 +54,10 @@ describe('useSplitTreatments', () => { act(() => client.__emitter__.emit(Event.SDK_READY)); expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes, { properties }); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments!); expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith(flagSets, attributes, { properties }); - expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets); + expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets!); }); test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { @@ -113,10 +111,9 @@ describe('useSplitTreatments', () => { }); /** - * Input validation. Passing invalid feature flag names or attributes while the Sdk - * is not ready doesn't emit errors, and logs meaningful messages instead. + * Input validation: sanitize invalid feature flag names and return control while the SDK is not ready. */ - test('Input validation: invalid "names" and "attributes" params in useSplitTreatments.', () => { + test('Input validation: invalid names are sanitized.', () => { render( { @@ -125,16 +122,14 @@ describe('useSplitTreatments', () => { let treatments = useSplitTreatments('split1').treatments; expect(treatments).toEqual({}); // @ts-expect-error Test error handling - treatments = useSplitTreatments({ names: [true] }).treatments; - expect(treatments).toEqual({}); + treatments = useSplitTreatments({ names: [true, ' flag_1 ', ' '] }).treatments; + expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG }); return null; }) } ); - expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.'); - expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.'); }); test('useSplitTreatments must update on SDK events', async () => { @@ -222,7 +217,7 @@ describe('useSplitTreatments', () => { expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined, undefined); }); - test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + test('ignores flagSets if both names and flagSets params are provided.', () => { render( { @@ -235,8 +230,6 @@ describe('useSplitTreatments', () => { } ); - - expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/withSplitFactory.test.tsx b/src/__tests__/withSplitFactory.test.tsx index 95ad025..bcf1b24 100644 --- a/src/__tests__/withSplitFactory.test.tsx +++ b/src/__tests__/withSplitFactory.test.tsx @@ -40,7 +40,7 @@ describe('withSplitFactory', () => { const Component = withSplitFactory(undefined, outerFactory)( ({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { expect(factory).toBe(outerFactory); - expect([isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, 0]); + expect([isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, true, false, false, false, 0]); return null; } ); diff --git a/src/constants.ts b/src/constants.ts index b6716d0..1db7bd4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,8 +14,6 @@ export const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig = { }; // Warning and error messages -export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactoryProvider. Config prop will be ignored.'; +export const WARN_SF_CONFIG_AND_FACTORY: string = 'Both a config and factory props were provided to SplitFactoryProvider. Config prop will be ignored.'; export const EXCEPTION_NO_SFP: string = 'No SplitContext was set. Please ensure the component is wrapped in a SplitFactoryProvider.'; - -export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/types.ts b/src/types.ts index 9493cd5..8a274b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,45 +1,10 @@ import type { ReactNode } from 'react'; +// @TODO: remove in next major release (it duplicates SplitIO.ReadinessStatus) /** - * Split Status interface. It represents the readiness state of an SDK client. + * Readiness Status interface. It represents the readiness state of an SDK client. */ -export interface ISplitStatus { - - /** - * `isReady` indicates if the Split SDK client has triggered an `SDK_READY` event and thus is ready to be consumed. - */ - isReady: boolean; - - /** - * `isReadyFromCache` indicates if the Split SDK client has triggered an `SDK_READY_FROM_CACHE` event and thus is ready to be consumed, - * although the data in cache might be stale. - */ - isReadyFromCache: boolean; - - /** - * `isTimedout` indicates if the Split SDK client has triggered an `SDK_READY_TIMED_OUT` event and is not ready to be consumed. - * In other words, `isTimedout` is equivalent to `hasTimedout && !isReady`. - */ - isTimedout: boolean; - - /** - * `hasTimedout` indicates if the Split SDK client has ever triggered an `SDK_READY_TIMED_OUT` event. - * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. - */ - hasTimedout: boolean; - - /** - * `isDestroyed` indicates if the Split SDK client has been destroyed. - */ - isDestroyed: boolean; - - /** - * `lastUpdate` indicates the timestamp of the most recent status event. This timestamp is only updated for events that are being listened to, - * configured via the `updateOnSdkReady` option for `SDK_READY` event, `updateOnSdkReadyFromCache` for `SDK_READY_FROM_CACHE` event, - * `updateOnSdkTimedout` for `SDK_READY_TIMED_OUT` event, and `updateOnSdkUpdate` for `SDK_UPDATE` event. - */ - lastUpdate: number; -} +export interface ISplitStatus extends SplitIO.ReadinessStatus {} /** * Update Props interface. It defines the props used to configure what SDK events are listened to update the component. diff --git a/src/utils.ts b/src/utils.ts index c78ec77..a0e80f4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,24 +1,9 @@ import memoizeOne from 'memoize-one'; import shallowEqual from 'shallowequal'; -import { CONTROL_WITH_CONFIG, WARN_NAMES_AND_FLAGSETS } from './constants'; +import { CONTROL_WITH_CONFIG } from './constants'; import { ISplitStatus } from './types'; -// Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. - -/** - * ClientWithContext interface. - */ -interface IClientWithContext extends SplitIO.IBrowserClient { - __getStatus(): { - isReady: boolean; - isReadyFromCache: boolean; - isTimedout: boolean; - hasTimedout: boolean; - isDestroyed: boolean; - isOperational: boolean; - lastUpdate: number; - }; -} +// Utils used to access singleton instances of Split factories and clients export interface IFactoryWithLazyInit extends SplitIO.IBrowserSDK { config: SplitIO.IBrowserSettings; @@ -26,9 +11,9 @@ export interface IFactoryWithLazyInit extends SplitIO.IBrowserSDK { } // idempotent operation -export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey): IClientWithContext { +export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey): SplitIO.IBrowserClient { // factory.client is an idempotent operation - const client = (key !== undefined ? factory.client(key) : factory.client()) as IClientWithContext; + const client = key !== undefined ? factory.client(key) : factory.client(); // Remove EventEmitter warning emitted when using multiple SDK hooks or components. // Unlike JS SDK, users don't need to access the client directly, making the warning irrelevant. @@ -38,18 +23,18 @@ export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.Split } // Util used to get client status. -// It might be removed in the future, if the JS SDK extends its public API with a `getStatus` method export function getStatus(client?: SplitIO.IBrowserClient): ISplitStatus { - const status = client && (client as IClientWithContext).__getStatus(); - - return { - isReady: status ? status.isReady : false, - isReadyFromCache: status ? status.isReadyFromCache : false, - isTimedout: status ? status.isTimedout : false, - hasTimedout: status ? status.hasTimedout : false, - isDestroyed: status ? status.isDestroyed : false, - lastUpdate: status ? status.lastUpdate : 0, - }; + return client ? + client.getStatus() : + { + isReady: false, + isReadyFromCache: false, + isTimedout: false, + hasTimedout: false, + isDestroyed: false, + isOperational: false, + lastUpdate: 0, + }; } /** @@ -60,72 +45,21 @@ export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: Spl if (client && attributes) client.setAttributes(attributes); } -// Input validation utils that will be replaced eventually - -function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature flag names'): false | string[] { - if (Array.isArray(maybeFeatureFlags) && maybeFeatureFlags.length > 0) { - const validatedArray: string[] = []; - // Remove invalid values - maybeFeatureFlags.forEach((maybeFeatureFlag) => { - const featureFlagName = validateFeatureFlag(maybeFeatureFlag); - if (featureFlagName) validatedArray.push(featureFlagName); - }); - - // Strip off duplicated values if we have valid feature flag names then return - if (validatedArray.length) return uniq(validatedArray); - } - - console.log(`[ERROR] ${listName} must be a non-empty array.`); - return false; -} - -const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; - -function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'feature flag name'): false | string { - if (maybeFeatureFlag == undefined) { - console.log(`[ERROR] you passed a null or undefined ${item}, ${item} must be a non-empty string.`); - } else if (!isString(maybeFeatureFlag)) { - console.log(`[ERROR] you passed an invalid ${item}, ${item} must be a non-empty string.`); - } else { - if (TRIMMABLE_SPACES_REGEX.test(maybeFeatureFlag)) { - console.log(`[WARN] ${item} "${maybeFeatureFlag}" has extra whitespace, trimming.`); - maybeFeatureFlag = maybeFeatureFlag.trim(); - } - - if ((maybeFeatureFlag as string).length > 0) { - return maybeFeatureFlag as string; - } else { - console.log(`[ERROR] you passed an empty ${item}, ${item} must be a non-empty string.`); - } - } - - return false; -} - export function getControlTreatmentsWithConfig(featureFlagNames: unknown): SplitIO.TreatmentsWithConfig { - // validate featureFlags Names - const validatedFeatureFlagNames = validateFeatureFlags(featureFlagNames); + if (!Array.isArray(featureFlagNames)) return {}; - // return empty object if the returned value is false - if (!validatedFeatureFlagNames) return {}; + featureFlagNames = featureFlagNames + .filter((featureFlagName) => isString(featureFlagName)) + .map((featureFlagName) => featureFlagName.trim()) + .filter((featureFlagName) => featureFlagName.length > 0); // return control treatments for each validated feature flag name - return validatedFeatureFlagNames.reduce((pValue: SplitIO.TreatmentsWithConfig, cValue: string) => { + return (featureFlagNames as string[]).reduce((pValue: SplitIO.TreatmentsWithConfig, cValue: string) => { pValue[cValue] = CONTROL_WITH_CONFIG; return pValue; }, {}); } -/** - * Removes duplicate items on an array of strings. - */ -function uniq(arr: string[]): string[] { - const seen: Record = {}; - return arr.filter((item) => { - return Object.prototype.hasOwnProperty.call(seen, item) ? false : seen[item] = true; - }); -} - /** * Checks if a given value is a string. */ @@ -151,9 +85,7 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { } function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) { - if (names && flagSets) console.log(WARN_NAMES_AND_FLAGSETS); - - return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? + return client && client.getStatus().isOperational && (names || flagSets) ? names ? client.getTreatmentsWithConfig(names, attributes, options) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) :