From 394a55dfffae747f2e67b2e6620eafa1226fe47a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 21 Oct 2025 13:32:27 -0300 Subject: [PATCH 1/4] fix: use Split SDK logger instead of console.log for warnings and errors --- package-lock.json | 30 +++++++------- package.json | 2 +- src/SplitFactoryProvider.tsx | 2 +- src/__tests__/SplitFactoryProvider.test.tsx | 2 +- src/__tests__/SplitTreatments.test.tsx | 6 +-- src/__tests__/testUtils/mockSplitFactory.ts | 2 + src/__tests__/useSplitTreatments.test.tsx | 6 +-- src/__tests__/utils.test.ts | 6 +-- src/__tests__/withSplitTreatments.test.tsx | 4 +- src/constants.ts | 4 +- src/useSplitTreatments.ts | 6 +-- src/utils.ts | 45 ++++++++++++--------- 12 files changed, 62 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2c6e9f..32c9b07 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.6.1-rc.1", "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.6.1-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.1-rc.1.tgz", + "integrity": "sha512-pI8CH7sxOcIL8A5AYLPlgo+GHkDEjxMqFHbbV/wISCB910qWn0TG/48XqCbMUVaanerBxf+L2wCmv7m996kEnA==", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.6.0", + "@splitsoftware/splitio-commons": "2.6.1-rc.1", "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.6.1-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.1-rc.1.tgz", + "integrity": "sha512-cixj1GtAcZTRENyXBCsqCsWXGuzK6xBQrZz34mtMUWIkzCdHb6gXWoEh/UBwqwrJ3cJJmUn7TNcFDjzmfM3Dlg==", "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.6.1-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.1-rc.1.tgz", + "integrity": "sha512-pI8CH7sxOcIL8A5AYLPlgo+GHkDEjxMqFHbbV/wISCB910qWn0TG/48XqCbMUVaanerBxf+L2wCmv7m996kEnA==", "requires": { - "@splitsoftware/splitio-commons": "2.6.0", + "@splitsoftware/splitio-commons": "2.6.1-rc.1", "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.6.1-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.1-rc.1.tgz", + "integrity": "sha512-cixj1GtAcZTRENyXBCsqCsWXGuzK6xBQrZz34mtMUWIkzCdHb6gXWoEh/UBwqwrJ3cJJmUn7TNcFDjzmfM3Dlg==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index e168d84..69e1b37 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.6.1-rc.1", "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__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx index 33f0977..0d79f84 100644 --- a/src/__tests__/SplitFactoryProvider.test.tsx +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -113,7 +113,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..fbc8fef 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -142,8 +142,8 @@ 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.'); + expect(logSpy).toBeCalledWith('[ERROR] splitio => feature flag names must be a non-empty array.'); + expect(logSpy).toBeCalledWith('[ERROR] splitio => you passed an invalid feature flag name, feature flag name must be a non-empty string.'); done(); }); @@ -162,7 +162,7 @@ describe('SplitTreatments', () => { ); - expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); + expect(logSpy).toBeCalledWith('[WARN] splitio => 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 () => { diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts index 09dca47..75b403f 100644 --- a/src/__tests__/testUtils/mockSplitFactory.ts +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; import jsSdkPackageJson from '@splitsoftware/splitio/package.json'; import reactSdkPackageJson from '../../../package.json'; +import { DEFAULT_LOGGER } from '../../utils'; export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`; export const reactSdkVersion = `react-${reactSdkPackageJson.version}`; @@ -154,6 +155,7 @@ export function mockSdk() { __clients__, settings: Object.assign({ version: jsSdkVersion, + log: DEFAULT_LOGGER }, config), }; diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index c93454d..f06818d 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -133,8 +133,8 @@ describe('useSplitTreatments', () => { } ); - 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.'); + expect(logSpy).toBeCalledWith('[ERROR] splitio => feature flag names must be a non-empty array.'); + expect(logSpy).toBeCalledWith('[ERROR] splitio => you passed an invalid feature flag name, feature flag name must be a non-empty string.'); }); test('useSplitTreatments must update on SDK events', async () => { @@ -236,7 +236,7 @@ describe('useSplitTreatments', () => { ); - expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); + expect(logSpy).toHaveBeenLastCalledWith('[WARN] splitio => Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 3d7e32e..1a02718 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,15 +1,15 @@ import { CONTROL_WITH_CONFIG } from '../constants'; -import { getControlTreatmentsWithConfig } from '../utils'; +import { DEFAULT_LOGGER, getControlTreatmentsWithConfig } from '../utils'; describe('getControlTreatmentsWithConfig', () => { it('should return an empty object if an empty array is provided', () => { - expect(Object.values(getControlTreatmentsWithConfig([])).length).toBe(0); + expect(Object.values(getControlTreatmentsWithConfig(DEFAULT_LOGGER, [])).length).toBe(0); }); it('should return an empty object if an empty array is provided', () => { const featureFlagNames = ['split1', 'split2']; - const treatments: SplitIO.TreatmentsWithConfig = getControlTreatmentsWithConfig(featureFlagNames); + const treatments: SplitIO.TreatmentsWithConfig = getControlTreatmentsWithConfig(DEFAULT_LOGGER, featureFlagNames); featureFlagNames.forEach((featureFlagName) => { expect(treatments[featureFlagName]).toBe(CONTROL_WITH_CONFIG); }); diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index af178f2..3bffe7c 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -14,7 +14,7 @@ import { INITIAL_STATUS } from './testUtils/utils'; import { withSplitFactory } from '../withSplitFactory'; import { withSplitClient } from '../withSplitClient'; import { withSplitTreatments } from '../withSplitTreatments'; -import { getControlTreatmentsWithConfig } from '../utils'; +import { DEFAULT_LOGGER, getControlTreatmentsWithConfig } from '../utils'; const featureFlagNames = ['split1', 'split2']; @@ -36,7 +36,7 @@ describe('withSplitTreatments', () => { ...INITIAL_STATUS, factory: factory, client: clientMock, outerProp1: 'outerProp1', outerProp2: 2, - treatments: getControlTreatmentsWithConfig(featureFlagNames), + treatments: getControlTreatmentsWithConfig(DEFAULT_LOGGER, featureFlagNames), }); return null; diff --git a/src/constants.ts b/src/constants.ts index b6716d0..be556a0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,8 +14,8 @@ 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.'; +export const WARN_NAMES_AND_FLAGSETS: string = 'Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 5b672f0..bcc7b11 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { memoizeGetTreatmentsWithConfig } from './utils'; +import { memoizeGetTreatmentsWithConfig, DEFAULT_LOGGER } from './utils'; import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; @@ -20,14 +20,14 @@ import { useSplitClient } from './useSplitClient'; */ export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { const context = useSplitClient({ ...options, attributes: undefined }); - const { client, lastUpdate } = context; + const { factory, client, lastUpdate } = context; const { names, flagSets, attributes, properties } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); // 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, properties && { properties }); + const treatments = getTreatmentsWithConfig(factory ? (factory.settings as any).log : DEFAULT_LOGGER, client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }); return { ...context, diff --git a/src/utils.ts b/src/utils.ts index c78ec77..bb137e4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,13 @@ import shallowEqual from 'shallowequal'; import { CONTROL_WITH_CONFIG, WARN_NAMES_AND_FLAGSETS } from './constants'; import { ISplitStatus } from './types'; +export 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); }, +}; + // Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. /** @@ -62,12 +69,12 @@ export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: Spl // Input validation utils that will be replaced eventually -function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature flag names'): false | string[] { +function validateFeatureFlags(log: SplitIO.Logger, 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); + const featureFlagName = validateFeatureFlag(log, maybeFeatureFlag); if (featureFlagName) validatedArray.push(featureFlagName); }); @@ -75,36 +82,36 @@ function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature fl if (validatedArray.length) return uniq(validatedArray); } - console.log(`[ERROR] ${listName} must be a non-empty array.`); + 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 { +function validateFeatureFlag(log: SplitIO.Logger, 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.`); + 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.`); + 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.`); + 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.`); + log.error(`you passed an empty ${item}, ${item} must be a non-empty string.`); } } return false; } -export function getControlTreatmentsWithConfig(featureFlagNames: unknown): SplitIO.TreatmentsWithConfig { +export function getControlTreatmentsWithConfig(log: SplitIO.Logger, featureFlagNames: unknown): SplitIO.TreatmentsWithConfig { // validate featureFlags Names - const validatedFeatureFlagNames = validateFeatureFlags(featureFlagNames); + const validatedFeatureFlagNames = validateFeatureFlags(log, featureFlagNames); // return empty object if the returned value is false if (!validatedFeatureFlagNames) return {}; @@ -142,22 +149,22 @@ export function memoizeGetTreatmentsWithConfig() { } 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 + return newArgs[1] === lastArgs[1] && // client + newArgs[2] === lastArgs[2] && // lastUpdate + shallowEqual(newArgs[3], lastArgs[3]) && // names + shallowEqual(newArgs[4], lastArgs[4]) && // attributes + shallowEqual(newArgs[5], lastArgs[5]) && // client attributes + shallowEqual(newArgs[6], lastArgs[6]); // flagSets } -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); +function evaluateFeatureFlags(log: SplitIO.Logger, client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) { + if (names && flagSets) log.warn(WARN_NAMES_AND_FLAGSETS); return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? names ? client.getTreatmentsWithConfig(names, attributes, options) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) : names ? - getControlTreatmentsWithConfig(names) : + getControlTreatmentsWithConfig(log, names) : {} // empty object when evaluating with flag sets and client is not ready } From aa039d694a1c67946328351b3491a413c67c9d78 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Oct 2025 13:08:59 -0300 Subject: [PATCH 2/4] refactor: simplify input validation for the case where the SDK is not operational --- CHANGES.txt | 5 ++ package-lock.json | 30 +++---- package.json | 2 +- src/__tests__/SplitTreatments.test.tsx | 24 ++---- src/__tests__/testUtils/mockSplitFactory.ts | 8 +- src/__tests__/useSplitTreatments.test.tsx | 21 ++--- src/__tests__/utils.test.ts | 6 +- src/__tests__/withSplitTreatments.test.tsx | 4 +- src/constants.ts | 2 - src/useSplitTreatments.ts | 6 +- src/utils.ts | 92 ++++----------------- 11 files changed, 65 insertions(+), 135 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1827849..032cf2c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +2.6.0 (October XX, 2025) + - Updated @splitsoftware/splitio package to version 11.7.1 that includes minor updates: + - Added support for custom loggers: added `logger` configuration option and `factory.Logger.setLogger` method to allow the SDK to use a custom logger. + + 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 32c9b07..6925702 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.1-rc.1", + "@splitsoftware/splitio": "11.7.1", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -1583,12 +1583,12 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "11.6.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.1-rc.1.tgz", - "integrity": "sha512-pI8CH7sxOcIL8A5AYLPlgo+GHkDEjxMqFHbbV/wISCB910qWn0TG/48XqCbMUVaanerBxf+L2wCmv7m996kEnA==", + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.1.tgz", + "integrity": "sha512-UczI3c4m7p2r7vmp8mNEuMA3LhySzHbOgvtOwp9PLaL6xyHr6GuFyFrrxjXFzQPq0Um8ensiRtWBks9LSz/uDA==", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.6.1-rc.1", + "@splitsoftware/splitio-commons": "2.7.1", "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.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.1-rc.1.tgz", - "integrity": "sha512-cixj1GtAcZTRENyXBCsqCsWXGuzK6xBQrZz34mtMUWIkzCdHb6gXWoEh/UBwqwrJ3cJJmUn7TNcFDjzmfM3Dlg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.1.tgz", + "integrity": "sha512-7a4VVMczh0YKVRi35EhD0FOAEwzqfJRcCiKqLLhZCxAvrZBpE2khpGn8pOP+y6TefdPVtblW8GIku4O4r0KRdQ==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -12095,11 +12095,11 @@ } }, "@splitsoftware/splitio": { - "version": "11.6.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.6.1-rc.1.tgz", - "integrity": "sha512-pI8CH7sxOcIL8A5AYLPlgo+GHkDEjxMqFHbbV/wISCB910qWn0TG/48XqCbMUVaanerBxf+L2wCmv7m996kEnA==", + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.1.tgz", + "integrity": "sha512-UczI3c4m7p2r7vmp8mNEuMA3LhySzHbOgvtOwp9PLaL6xyHr6GuFyFrrxjXFzQPq0Um8ensiRtWBks9LSz/uDA==", "requires": { - "@splitsoftware/splitio-commons": "2.6.1-rc.1", + "@splitsoftware/splitio-commons": "2.7.1", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -12109,9 +12109,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "2.6.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.6.1-rc.1.tgz", - "integrity": "sha512-cixj1GtAcZTRENyXBCsqCsWXGuzK6xBQrZz34mtMUWIkzCdHb6gXWoEh/UBwqwrJ3cJJmUn7TNcFDjzmfM3Dlg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.1.tgz", + "integrity": "sha512-7a4VVMczh0YKVRi35EhD0FOAEwzqfJRcCiKqLLhZCxAvrZBpE2khpGn8pOP+y6TefdPVtblW8GIku4O4r0KRdQ==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index 69e1b37..3e9a59f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "11.6.1-rc.1", + "@splitsoftware/splitio": "11.7.1", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index fbc8fef..1182ad4 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( @@ -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] splitio => feature flag names must be a non-empty array.'); - expect(logSpy).toBeCalledWith('[ERROR] splitio => 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] splitio => 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 75b403f..2e2808e 100644 --- a/src/__tests__/testUtils/mockSplitFactory.ts +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -1,7 +1,6 @@ import { EventEmitter } from 'events'; import jsSdkPackageJson from '@splitsoftware/splitio/package.json'; import reactSdkPackageJson from '../../../package.json'; -import { DEFAULT_LOGGER } from '../../utils'; export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`; export const reactSdkVersion = `react-${reactSdkPackageJson.version}`; @@ -13,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 { diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index f06818d..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] splitio => feature flag names must be a non-empty array.'); - expect(logSpy).toBeCalledWith('[ERROR] splitio => 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] splitio => Both names and flagSets properties were provided. flagSets will be ignored.'); }); }); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 1a02718..3d7e32e 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,15 +1,15 @@ import { CONTROL_WITH_CONFIG } from '../constants'; -import { DEFAULT_LOGGER, getControlTreatmentsWithConfig } from '../utils'; +import { getControlTreatmentsWithConfig } from '../utils'; describe('getControlTreatmentsWithConfig', () => { it('should return an empty object if an empty array is provided', () => { - expect(Object.values(getControlTreatmentsWithConfig(DEFAULT_LOGGER, [])).length).toBe(0); + expect(Object.values(getControlTreatmentsWithConfig([])).length).toBe(0); }); it('should return an empty object if an empty array is provided', () => { const featureFlagNames = ['split1', 'split2']; - const treatments: SplitIO.TreatmentsWithConfig = getControlTreatmentsWithConfig(DEFAULT_LOGGER, featureFlagNames); + const treatments: SplitIO.TreatmentsWithConfig = getControlTreatmentsWithConfig(featureFlagNames); featureFlagNames.forEach((featureFlagName) => { expect(treatments[featureFlagName]).toBe(CONTROL_WITH_CONFIG); }); diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index 3bffe7c..af178f2 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -14,7 +14,7 @@ import { INITIAL_STATUS } from './testUtils/utils'; import { withSplitFactory } from '../withSplitFactory'; import { withSplitClient } from '../withSplitClient'; import { withSplitTreatments } from '../withSplitTreatments'; -import { DEFAULT_LOGGER, getControlTreatmentsWithConfig } from '../utils'; +import { getControlTreatmentsWithConfig } from '../utils'; const featureFlagNames = ['split1', 'split2']; @@ -36,7 +36,7 @@ describe('withSplitTreatments', () => { ...INITIAL_STATUS, factory: factory, client: clientMock, outerProp1: 'outerProp1', outerProp2: 2, - treatments: getControlTreatmentsWithConfig(DEFAULT_LOGGER, featureFlagNames), + treatments: getControlTreatmentsWithConfig(featureFlagNames), }); return null; diff --git a/src/constants.ts b/src/constants.ts index be556a0..1db7bd4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -17,5 +17,3 @@ export const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig = { 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 = 'Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index bcc7b11..5b672f0 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { memoizeGetTreatmentsWithConfig, DEFAULT_LOGGER } from './utils'; +import { memoizeGetTreatmentsWithConfig } from './utils'; import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; @@ -20,14 +20,14 @@ import { useSplitClient } from './useSplitClient'; */ export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { const context = useSplitClient({ ...options, attributes: undefined }); - const { factory, client, lastUpdate } = context; + const { client, lastUpdate } = context; const { names, flagSets, attributes, properties } = options; const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); // 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(factory ? (factory.settings as any).log : DEFAULT_LOGGER, client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }); + const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets, properties && { properties }); return { ...context, diff --git a/src/utils.ts b/src/utils.ts index bb137e4..e7c5797 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,15 +1,8 @@ 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'; -export 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); }, -}; - // Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. /** @@ -67,72 +60,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(log: SplitIO.Logger, 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(log, 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); - } - - log.error(`${listName} must be a non-empty array.`); - return false; -} - -const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; - -function validateFeatureFlag(log: SplitIO.Logger, maybeFeatureFlag: unknown, item = 'feature flag name'): false | string { - if (maybeFeatureFlag == undefined) { - log.error(`you passed a null or undefined ${item}, ${item} must be a non-empty string.`); - } else if (!isString(maybeFeatureFlag)) { - log.error(`you passed an invalid ${item}, ${item} must be a non-empty string.`); - } else { - if (TRIMMABLE_SPACES_REGEX.test(maybeFeatureFlag)) { - log.warn(`${item} "${maybeFeatureFlag}" has extra whitespace, trimming.`); - maybeFeatureFlag = maybeFeatureFlag.trim(); - } - - if ((maybeFeatureFlag as string).length > 0) { - return maybeFeatureFlag as string; - } else { - log.error(`you passed an empty ${item}, ${item} must be a non-empty string.`); - } - } - - return false; -} +export function getControlTreatmentsWithConfig(featureFlagNames: unknown): SplitIO.TreatmentsWithConfig { + if (!Array.isArray(featureFlagNames)) return {}; -export function getControlTreatmentsWithConfig(log: SplitIO.Logger, featureFlagNames: unknown): SplitIO.TreatmentsWithConfig { - // validate featureFlags Names - const validatedFeatureFlagNames = validateFeatureFlags(log, featureFlagNames); - - // 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. */ @@ -149,22 +91,20 @@ export function memoizeGetTreatmentsWithConfig() { } function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { - return newArgs[1] === lastArgs[1] && // client - newArgs[2] === lastArgs[2] && // lastUpdate - shallowEqual(newArgs[3], lastArgs[3]) && // names - shallowEqual(newArgs[4], lastArgs[4]) && // attributes - shallowEqual(newArgs[5], lastArgs[5]) && // client attributes - shallowEqual(newArgs[6], lastArgs[6]); // flagSets + 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 evaluateFeatureFlags(log: SplitIO.Logger, client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) { - if (names && flagSets) log.warn(WARN_NAMES_AND_FLAGSETS); - +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions) { return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? names ? client.getTreatmentsWithConfig(names, attributes, options) : client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) : names ? - getControlTreatmentsWithConfig(log, names) : + getControlTreatmentsWithConfig(names) : {} // empty object when evaluating with flag sets and client is not ready } From 6c44714bf98585797e5d34436f81b81c9ae3cf3a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 29 Oct 2025 14:03:08 -0300 Subject: [PATCH 3/4] chore: update JS SDK dependency and adapt to new getStatus API --- package-lock.json | 30 +++++++------- package.json | 2 +- src/__tests__/SplitClient.test.tsx | 19 ++++----- src/__tests__/SplitFactoryProvider.test.tsx | 1 + src/__tests__/SplitTreatments.test.tsx | 2 +- src/__tests__/testUtils/mockSplitFactory.ts | 9 ++--- src/__tests__/testUtils/utils.tsx | 1 + src/__tests__/useSplitClient.test.tsx | 4 +- src/__tests__/useSplitManager.test.tsx | 6 ++- src/__tests__/withSplitFactory.test.tsx | 2 +- src/types.ts | 41 ++----------------- src/utils.ts | 45 +++++++-------------- 12 files changed, 58 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6925702..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.7.1", + "@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.7.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.1.tgz", - "integrity": "sha512-UczI3c4m7p2r7vmp8mNEuMA3LhySzHbOgvtOwp9PLaL6xyHr6GuFyFrrxjXFzQPq0Um8ensiRtWBks9LSz/uDA==", + "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.7.1", + "@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.7.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.1.tgz", - "integrity": "sha512-7a4VVMczh0YKVRi35EhD0FOAEwzqfJRcCiKqLLhZCxAvrZBpE2khpGn8pOP+y6TefdPVtblW8GIku4O4r0KRdQ==", + "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.7.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.7.1.tgz", - "integrity": "sha512-UczI3c4m7p2r7vmp8mNEuMA3LhySzHbOgvtOwp9PLaL6xyHr6GuFyFrrxjXFzQPq0Um8ensiRtWBks9LSz/uDA==", + "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.7.1", + "@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.7.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.1.tgz", - "integrity": "sha512-7a4VVMczh0YKVRi35EhD0FOAEwzqfJRcCiKqLLhZCxAvrZBpE2khpGn8pOP+y6TefdPVtblW8GIku4O4r0KRdQ==", + "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 3e9a59f..4c8e21c 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "11.7.1", + "@splitsoftware/splitio": "11.7.2-rc.4", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" 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 0d79f84..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; diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 1182ad4..243ddb6 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -69,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; }} diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts index 2e2808e..b7ea1e2 100644 --- a/src/__tests__/testUtils/mockSplitFactory.ts +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -54,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(); }); @@ -96,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(() => { @@ -122,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() { 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__/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/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 e7c5797..a0e80f4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,22 +3,7 @@ import shallowEqual from 'shallowequal'; 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, + }; } /** @@ -100,7 +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) { - 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) : From 5b1229c9d9c0643799c3b90449492e4144fd126a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 29 Oct 2025 14:07:49 -0300 Subject: [PATCH 4/4] Update changelog entry --- CHANGES.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 032cf2c..a2851ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,8 @@ -2.6.0 (October XX, 2025) - - Updated @splitsoftware/splitio package to version 11.7.1 that includes minor updates: +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: