Skip to content

Commit 3ced1c1

Browse files
Merge pull request #248 from splitio/use-sdk-logger
Update JS SDK dependency and use SDK logger instead of `console.log` for logging
2 parents ac974c7 + 5b1229c commit 3ced1c1

16 files changed

+95
-201
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
2.6.0 (October 31, 2025)
2+
- Updated @splitsoftware/splitio package to version 11.8.0 that includes minor updates:
3+
- 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.
4+
- Added support for custom loggers: added `logger` configuration option and `factory.Logger.setLogger` method to allow the SDK to use a custom logger.
5+
- Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted.
6+
17
2.5.0 (September 18, 2025)
28
- Updated @splitsoftware/splitio package to version 11.6.0 that includes minor updates:
39
- 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`.

package-lock.json

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
},
6464
"homepage": "https://github.com/splitio/react-client#readme",
6565
"dependencies": {
66-
"@splitsoftware/splitio": "11.6.0",
66+
"@splitsoftware/splitio": "11.7.2-rc.4",
6767
"memoize-one": "^5.1.1",
6868
"shallowequal": "^1.1.0",
6969
"tslib": "^2.3.1"

src/SplitFactoryProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function SplitFactoryProvider(props: ISplitFactoryProviderProps) {
5151
// Effect to initialize and destroy the factory when config is provided
5252
React.useEffect(() => {
5353
if (propFactory) {
54-
if (config) console.log(WARN_SF_CONFIG_AND_FACTORY);
54+
if (config) (propFactory.settings as any).log.warn(WARN_SF_CONFIG_AND_FACTORY);
5555
return;
5656
}
5757

src/__tests__/SplitClient.test.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe('SplitClient', () => {
5656
client: outerFactory.client(),
5757
isReady: true,
5858
isReadyFromCache: true,
59+
isOperational: true,
5960
lastUpdate: getStatus(outerFactory.client()).lastUpdate
6061
});
6162

@@ -141,7 +142,7 @@ describe('SplitClient', () => {
141142
expect(statusProps).toStrictEqual([false, false, true, true]);
142143
break;
143144
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
144-
expect(statusProps).toStrictEqual([true, false, true, false]);
145+
expect(statusProps).toStrictEqual([true, true, true, false]);
145146
break;
146147
default:
147148
fail('Child must not be rerendered');
@@ -182,7 +183,7 @@ describe('SplitClient', () => {
182183
expect(statusProps).toStrictEqual([false, false, false, false]);
183184
break;
184185
case 1: // Ready
185-
expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
186+
expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
186187
break;
187188
default:
188189
fail('Child must not be rerendered');
@@ -214,7 +215,7 @@ describe('SplitClient', () => {
214215
count++;
215216

216217
// side effect in the render phase
217-
if (!(client as any).__getStatus().isReady) {
218+
if (!client!.getStatus().isReady) {
218219
(client as any).__emitter__.emit(Event.SDK_READY);
219220
}
220221

@@ -318,11 +319,11 @@ describe('SplitClient', () => {
318319
break;
319320
case 4:
320321
expect(client).toBe(outerFactory.client('user3'));
321-
expect(statusProps).toStrictEqual([true, false, false, false]);
322+
expect(statusProps).toStrictEqual([true, true, false, false]);
322323
break;
323324
case 5:
324325
expect(client).toBe(outerFactory.client('user3'));
325-
expect(statusProps).toStrictEqual([true, false, false, false]);
326+
expect(statusProps).toStrictEqual([true, true, false, false]);
326327
break;
327328
default:
328329
fail('Child must not be rerendered');
@@ -501,7 +502,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
501502
expect(statusProps).toStrictEqual([false, false, true, true]);
502503
break;
503504
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
504-
expect(statusProps).toStrictEqual([true, false, true, false]);
505+
expect(statusProps).toStrictEqual([true, true, true, false]);
505506
break;
506507
default:
507508
fail('Child must not be rerendered');
@@ -542,7 +543,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
542543
expect(statusProps).toStrictEqual([false, false, true, true]);
543544
break;
544545
case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client.
545-
expect(statusProps).toStrictEqual([true, false, true, false]);
546+
expect(statusProps).toStrictEqual([true, true, true, false]);
546547
break;
547548
default:
548549
fail('Child must not be rerendered');
@@ -578,7 +579,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
578579
expect(statusProps).toStrictEqual([false, false, false, false]);
579580
break;
580581
case 1: // Ready
581-
expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
582+
expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
582583
break;
583584
default:
584585
fail('Child must not be rerendered');
@@ -615,7 +616,7 @@ describe('SplitFactoryProvider + SplitClient', () => {
615616
expect(statusProps).toStrictEqual([false, false, false, false]);
616617
break;
617618
case 1: // Ready
618-
expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
619+
expect(statusProps).toStrictEqual([true, true, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state
619620
break;
620621
default:
621622
fail('Child must not be rerendered');

src/__tests__/SplitFactoryProvider.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('SplitFactoryProvider', () => {
7070
client: outerFactory.client(),
7171
isReady: true,
7272
isReadyFromCache: true,
73+
isOperational: true,
7374
lastUpdate: getStatus(outerFactory.client()).lastUpdate
7475
});
7576
return null;
@@ -113,7 +114,7 @@ describe('SplitFactoryProvider', () => {
113114
</SplitFactoryProvider>
114115
);
115116

116-
expect(logSpy).toBeCalledWith(WARN_SF_CONFIG_AND_FACTORY);
117+
expect(logSpy).toBeCalledWith('[WARN] splitio => ' + WARN_SF_CONFIG_AND_FACTORY);
117118
logSpy.mockRestore();
118119
});
119120

src/__tests__/SplitTreatments.test.tsx

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@ import { SplitClient } from '../SplitClient';
1919
import { SplitFactoryProvider } from '../SplitFactoryProvider';
2020
import { useSplitTreatments } from '../useSplitTreatments';
2121

22-
const logSpy = jest.spyOn(console, 'log');
23-
2422
describe('SplitTreatments', () => {
2523

2624
const featureFlagNames = ['split1', 'split2'];
2725
const flagSets = ['set1', 'set2'];
2826

29-
afterEach(() => { logSpy.mockClear() });
30-
3127
it('passes control treatments (empty object if flagSets is provided) if the SDK is not operational.', () => {
3228
render(
3329
<SplitFactoryProvider config={sdkBrowser} >
@@ -73,7 +69,7 @@ describe('SplitTreatments', () => {
7369
expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1);
7470
expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value);
7571
expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]);
76-
expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, getStatus(outerFactory.client()).lastUpdate]);
72+
expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, true, false, false, false, getStatus(outerFactory.client()).lastUpdate]);
7773
return null;
7874
}}
7975
</SplitTreatments>
@@ -105,10 +101,9 @@ describe('SplitTreatments', () => {
105101
});
106102

107103
/**
108-
* Input validation. Passing invalid feature flag names or attributes while the Sdk
109-
* is not ready doesn't emit errors, and logs meaningful messages instead.
104+
* Input validation: sanitize invalid feature flag names and return control while the SDK is not ready.
110105
*/
111-
it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => {
106+
it('Input validation: invalid names are sanitized.', () => {
112107
render(
113108
<SplitFactoryProvider config={sdkBrowser} >
114109
<SplitClient>
@@ -130,9 +125,9 @@ describe('SplitTreatments', () => {
130125
}}
131126
</SplitTreatments>
132127
{/* @ts-expect-error Test error handling */}
133-
<SplitTreatments names={[true]} attributes={'invalid'} >
128+
<SplitTreatments names={[true, ' flag_1 ', ' ']} >
134129
{({ treatments }: ISplitTreatmentsChildProps) => {
135-
expect(treatments).toEqual({});
130+
expect(treatments).toEqual({ flag_1: CONTROL_WITH_CONFIG });
136131
return null;
137132
}}
138133
</SplitTreatments>
@@ -142,14 +137,9 @@ describe('SplitTreatments', () => {
142137
</SplitClient>
143138
</SplitFactoryProvider>
144139
);
145-
expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.');
146-
expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.');
147-
148-
done();
149140
});
150141

151-
152-
test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => {
142+
test('ignores flagSets if both names and flagSets params are provided.', () => {
153143
render(
154144
<SplitFactoryProvider >
155145
{/* @ts-expect-error flagSets and names are mutually exclusive */}
@@ -161,8 +151,6 @@ describe('SplitTreatments', () => {
161151
</SplitTreatments>
162152
</SplitFactoryProvider>
163153
);
164-
165-
expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.');
166154
});
167155

168156
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', () => {
190178
act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE));
191179

192180
expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes, undefined);
193-
expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments);
181+
expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments!);
194182
});
195183
});
196184

src/__tests__/testUtils/mockSplitFactory.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ export const Event = {
1212
SDK_UPDATE: 'state::update',
1313
};
1414

15+
const DEFAULT_LOGGER: SplitIO.Logger = {
16+
error(msg) { console.log('[ERROR] splitio => ' + msg); },
17+
warn(msg) { console.log('[WARN] splitio => ' + msg); },
18+
info(msg) { console.log('[INFO] splitio => ' + msg); },
19+
debug(msg) { console.log('[DEBUG] splitio => ' + msg); },
20+
};
21+
1522
function parseKey(key: SplitIO.SplitKey): SplitIO.SplitKey {
1623
if (key && typeof key === 'object' && key.constructor === Object) {
1724
return {
@@ -47,7 +54,7 @@ export function mockSdk() {
4754
}
4855

4956
const __emitter__ = new EventEmitter();
50-
__emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); });
57+
__emitter__.on(Event.SDK_READY, () => { isReady = true; isReadyFromCache = true; syncLastUpdate(); });
5158
__emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); });
5259
__emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); });
5360
__emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); });
@@ -89,13 +96,13 @@ export function mockSdk() {
8996
else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); }
9097
});
9198
});
92-
const __getStatus = () => ({
99+
const getStatus = () => ({
93100
isReady,
94101
isReadyFromCache,
95102
isTimedout: hasTimedout && !isReady,
96103
hasTimedout,
97104
isDestroyed,
98-
isOperational: (isReady || isReadyFromCache) && !isDestroyed,
105+
isOperational: isReadyFromCache && !isDestroyed,
99106
lastUpdate,
100107
});
101108
const destroy: jest.Mock = jest.fn(() => {
@@ -115,10 +122,9 @@ export function mockSdk() {
115122
setAttributes,
116123
clearAttributes,
117124
getAttributes,
125+
getStatus,
118126
// EventEmitter exposed to trigger events manually
119127
__emitter__,
120-
// Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed)
121-
__getStatus,
122128
// Restore the mock client to its initial NO-READY status.
123129
// Useful when you want to reuse the same mock between tests after emitting events or destroying the instance.
124130
__restore() {
@@ -154,6 +160,7 @@ export function mockSdk() {
154160
__clients__,
155161
settings: Object.assign({
156162
version: jsSdkVersion,
163+
log: DEFAULT_LOGGER
157164
}, config),
158165
};
159166

src/__tests__/testUtils/utils.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const INITIAL_STATUS: ISplitStatus & IUpdateProps = {
123123
hasTimedout: false,
124124
lastUpdate: 0,
125125
isDestroyed: false,
126+
isOperational: false,
126127
updateOnSdkReady: true,
127128
updateOnSdkReadyFromCache: true,
128129
updateOnSdkTimedout: true,

src/__tests__/useSplitClient.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('useSplitClient', () => {
207207

208208
// side effect in the render phase
209209
const client = outerFactory.client('some_user') as any;
210-
if (!client.__getStatus().isReady) client.__emitter__.emit(Event.SDK_READY);
210+
if (!client.getStatus().isReady) client.__emitter__.emit(Event.SDK_READY);
211211

212212
return null;
213213
})}
@@ -256,7 +256,7 @@ describe('useSplitClient', () => {
256256

257257
act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render
258258
expect(rendersCount).toBe(5);
259-
expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: false, hasTimedout: true });
259+
expect(currentStatus).toMatchObject({ isReady: true, isReadyFromCache: true, hasTimedout: true });
260260

261261
act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false
262262
expect(rendersCount).toBe(5);

0 commit comments

Comments
 (0)