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
6 changes: 6 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/SplitFactoryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
19 changes: 10 additions & 9 deletions src/__tests__/SplitClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('SplitClient', () => {
client: outerFactory.client(),
isReady: true,
isReadyFromCache: true,
isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate
});

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/SplitFactoryProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('SplitFactoryProvider', () => {
client: outerFactory.client(),
isReady: true,
isReadyFromCache: true,
isOperational: true,
lastUpdate: getStatus(outerFactory.client()).lastUpdate
});
return null;
Expand Down Expand Up @@ -113,7 +114,7 @@ describe('SplitFactoryProvider', () => {
</SplitFactoryProvider>
);

expect(logSpy).toBeCalledWith(WARN_SF_CONFIG_AND_FACTORY);
expect(logSpy).toBeCalledWith('[WARN] splitio => ' + WARN_SF_CONFIG_AND_FACTORY);
logSpy.mockRestore();
});

Expand Down
26 changes: 7 additions & 19 deletions src/__tests__/SplitTreatments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<SplitFactoryProvider config={sdkBrowser} >
Expand Down Expand Up @@ -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;
}}
</SplitTreatments>
Expand Down Expand Up @@ -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(
<SplitFactoryProvider config={sdkBrowser} >
<SplitClient>
Expand All @@ -130,9 +125,9 @@ describe('SplitTreatments', () => {
}}
</SplitTreatments>
{/* @ts-expect-error Test error handling */}
<SplitTreatments names={[true]} attributes={'invalid'} >
<SplitTreatments names={[true, ' flag_1 ', ' ']} >
{({ treatments }: ISplitTreatmentsChildProps) => {
expect(treatments).toEqual({});
expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG });
return null;
}}
</SplitTreatments>
Expand All @@ -142,14 +137,9 @@ describe('SplitTreatments', () => {
</SplitClient>
</SplitFactoryProvider>
);
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(
<SplitFactoryProvider >
{/* @ts-expect-error flagSets and names are mutually exclusive */}
Expand All @@ -161,8 +151,6 @@ describe('SplitTreatments', () => {
</SplitTreatments>
</SplitFactoryProvider>
);

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 () => {
Expand Down Expand Up @@ -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!);
});
});

Expand Down
17 changes: 12 additions & 5 deletions src/__tests__/testUtils/mockSplitFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(); });
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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() {
Expand Down Expand Up @@ -154,6 +160,7 @@ export function mockSdk() {
__clients__,
settings: Object.assign({
version: jsSdkVersion,
log: DEFAULT_LOGGER
}, config),
};

Expand Down
1 change: 1 addition & 0 deletions src/__tests__/testUtils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export const INITIAL_STATUS: ISplitStatus & IUpdateProps = {
hasTimedout: false,
lastUpdate: 0,
isDestroyed: false,
isOperational: false,
updateOnSdkReady: true,
updateOnSdkReadyFromCache: true,
updateOnSdkTimedout: true,
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/useSplitClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
})}
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions src/__tests__/useSplitManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
Expand Down Expand Up @@ -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,
});
});
Expand Down
Loading