diff --git a/CHANGES.txt b/CHANGES.txt index 47faaa87..d07f2bc1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,7 +9,7 @@ - Updated the Redis storage to: - Avoid lazy require of the `ioredis` dependency when the SDK is initialized, and - Flag the SDK as ready from cache immediately to allow queueing feature flag evaluations before SDK_READY event is emitted (Reverted in v1.7.0). - - Bugfix - Enhanced HTTP client module to implement timeouts for failing requests that might otherwise remain pending indefinitely on some Fetch API implementations. + - Bugfix - Enhanced HTTP client module to implement timeouts for failing requests that might otherwise remain pending indefinitely on some Fetch API implementations, pausing the SDK synchronization process. 2.2.0 (March 28, 2025) - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. diff --git a/package-lock.json b/package-lock.json index fd6d8782..aa7cf6d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1780,10 +1780,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2218,10 +2219,11 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9496,9 +9498,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -9818,9 +9820,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 123f8eee..70a87eb0 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -14,7 +14,7 @@ export const codesError: [number, string][] = [ [c.ERROR_SYNC_OFFLINE_LOADING, c.LOG_PREFIX_SYNC_OFFLINE + 'There was an issue loading the mock feature flags data. No changes will be applied to the current cache. %s'], [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to connect or error on streaming connection, with error message: %s'], [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: %s.'], - [c.ERROR_HTTP, 'Response status is not OK. Status: %s. URL: %s. Message: %s'], + [c.ERROR_HTTP, 'HTTP request failed with %s. URL: %s. Message: %s'], // client status [c.ERROR_CLIENT_LISTENER, 'A listener was added for %s on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.'], [c.ERROR_CLIENT_DESTROYED, '%s: Client has already been destroyed - no calls possible.'], diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index dcb841c8..89e12533 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -70,7 +70,7 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet } if (!resp || resp.status !== 403) { // 403's log we'll be handled somewhere else. - log[logErrorsAsInfo ? 'info' : 'error'](ERROR_HTTP, [resp ? resp.status : 'NO_STATUS', url, msg]); + log[logErrorsAsInfo ? 'info' : 'error'](ERROR_HTTP, [resp ? 'status code ' + resp.status : 'no status code', url, msg]); } const networkError: NetworkError = new Error(msg); diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 761c5cb9..2a4b9b78 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -1,4 +1,4 @@ -import { ISplitsCacheSync } from './types'; +import { ISplitsCacheSync, IStorageSync } from './types'; import { IRBSegment, ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; @@ -88,3 +88,7 @@ export function usesSegments(ruleEntity: ISplit | IRBSegment) { return false; } + +export function usesSegmentsSync(storage: Pick) { + return storage.splits.usesSegments() || storage.rbSegments.usesSegments(); +} diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 913d6a3b..13ab1b32 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -173,6 +173,9 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => { ], [], -1); cache.addSplit(featureFlagWithEmptyFS); + // Adding an existing FF should not affect the cache + cache.update([featureFlagTwo], [], -1); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 56ca1300..2ed4478b 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -135,6 +135,9 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { ], [], -1); cache.addSplit(featureFlagWithEmptyFS); + // Adding an existing FF should not affect the cache + cache.update([featureFlagTwo], [], -1); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 6a5ba679..5e197e62 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -8,6 +8,7 @@ import { getMatching } from '../../utils/key'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; +import { usesSegmentsSync } from '../../storages/AbstractSplitsCacheSync'; /** * Expose start / stop mechanism for polling data from services. @@ -43,7 +44,7 @@ export function pollingManagerCSFactory( // smart pausing readiness.splits.on(SDK_SPLITS_ARRIVED, () => { if (!splitsSyncTask.isRunning()) return; // noop if not doing polling - const usingSegments = storage.splits.usesSegments() || storage.rbSegments.usesSegments(); + const usingSegments = usesSegmentsSync(storage); if (usingSegments !== mySegmentsSyncTask.isRunning()) { log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']); if (usingSegments) { @@ -59,9 +60,9 @@ export function pollingManagerCSFactory( // smart ready function smartReady() { - if (!readiness.isReady() && !storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); } - if (!storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) setTimeout(smartReady, 0); + if (!usesSegmentsSync(storage)) setTimeout(smartReady, 0); else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady); mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask; @@ -77,7 +78,7 @@ export function pollingManagerCSFactory( log.info(POLLING_START); splitsSyncTask.start(); - if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) startMySegmentsSyncTasks(); + if (usesSegmentsSync(storage)) startMySegmentsSyncTasks(); }, // Stop periodic fetching (polling) diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 501e3b7a..5de512fa 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -8,6 +8,7 @@ import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants'; import { MySegmentsData } from '../types'; import { IMembershipsResponse } from '../../../dtos/types'; import { MEMBERSHIPS_LS_UPDATE } from '../../streaming/constants'; +import { usesSegmentsSync } from '../../../storages/AbstractSplitsCacheSync'; type IMySegmentsUpdater = (segmentsData?: MySegmentsData, noCache?: boolean, till?: number) => Promise @@ -27,7 +28,7 @@ export function mySegmentsUpdaterFactory( matchingKey: string ): IMySegmentsUpdater { - const { splits, rbSegments, segments, largeSegments } = storage; + const { segments, largeSegments } = storage; let readyOnAlreadyExistentState = true; let startingUp = true; @@ -51,7 +52,7 @@ export function mySegmentsUpdaterFactory( } // Notify update if required - if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { + if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { readyOnAlreadyExistentState = false; segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); } diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index ea5e5e44..0331bc43 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -59,7 +59,7 @@ interface ISplitMutations { /** * If there are defined filters and one feature flag doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it - * If there are set filter defined, names filter is ignored + * If there is `bySet` filter, `byName` and `byPrefix` filters are ignored * * @param featureFlag - feature flag to be evaluated * @param filters - splitFiltersValidation bySet | byName diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 21bf81e7..aac6f7e4 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -10,6 +10,7 @@ import { isConsentGranted } from '../consent'; import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; import { ISdkFactoryContextSync } from '../sdkFactory/types'; import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants'; +import { usesSegmentsSync } from '../storages/AbstractSplitsCacheSync'; /** * Online SyncManager factory. @@ -155,14 +156,14 @@ export function syncManagerOnlineFactory( if (pushManager) { if (pollingManager.isRunning()) { // if doing polling, we must start the periodic fetch of data - if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); + if (usesSegmentsSync(storage)) mySegmentsSyncTask.start(); } else { // if not polling, we must execute the sync task for the initial fetch // of segments since `syncAll` was already executed when starting the main client mySegmentsSyncTask.execute(); } } else { - if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); + if (usesSegmentsSync(storage)) mySegmentsSyncTask.start(); } } else { if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index cea3117f..455d3ee1 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -69,12 +69,6 @@ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values /** * Returns a string representing the URL encoded query component of /splitChanges URL. * - * The possible formats of the query string are: - * - null: if all filters are empty - * - '&names=': if only `byPrefix` filter is undefined - * - '&prefixes=': if only `byName` filter is undefined - * - '&names=&prefixes=': if no one is undefined - * * @param groupedFilters - object of filters. Each filter must be a list of valid, unique and ordered string values. * @returns null or string with the `split filter query` component of the URL. */ diff --git a/types/splitio.d.ts b/types/splitio.d.ts index ad8644b2..e85ab01b 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -24,7 +24,11 @@ interface ISharedSettings { sync?: { /** * List of feature flag filters. These filters are used to fetch a subset of the feature flag definitions in your environment, in order to reduce the delay of the SDK to be ready. - * This configuration is only meaningful when the SDK is working in "standalone" mode. + * + * NOTES: + * - This configuration is only meaningful when the SDK is working in `"standalone"` mode. + * - If `bySet` filter is provided, `byName` and `byPrefix` filters are ignored. + * - If both `byName` and `byPrefix` filters are provided, the intersection of the two groups of feature flags is fetched. * * Example: * ``` @@ -66,12 +70,17 @@ interface ISharedSettings { * * @example * ``` - * const getHeaderOverrides = (context) => { - * return { - * 'Authorization': context.headers['Authorization'] + ', other-value', - * 'custom-header': 'custom-value' - * }; - * }; + * const factory = SplitFactory({ + * ... + * sync: { + * getHeaderOverrides: (context) => { + * return { + * 'Authorization': context.headers['Authorization'] + ', other-value', + * 'custom-header': 'custom-value' + * }; + * } + * } + * }); * ``` */ getHeaderOverrides?: (context: { headers: Record }) => Record; @@ -952,7 +961,7 @@ declare namespace SplitIO { */ prefix?: string; /** - * Number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * Number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization. * * @defaultValue `10` */ @@ -1130,7 +1139,7 @@ declare namespace SplitIO { */ type: SplitFilterType; /** - * List of values: feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type. + * List of values: flag set names for 'bySet' filter type, feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type. */ values: string[]; } @@ -1292,7 +1301,7 @@ declare namespace SplitIO { */ prefix?: string; /** - * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization. * * @defaultValue `10` */ @@ -1350,12 +1359,17 @@ declare namespace SplitIO { * * @example * ``` - * const getHeaderOverrides = (context) => { - * return { - * 'Authorization': context.headers['Authorization'] + ', other-value', - * 'custom-header': 'custom-value' - * }; - * }; + * const factory = SplitFactory({ + * ... + * sync: { + * getHeaderOverrides: (context) => { + * return { + * 'Authorization': context.headers['Authorization'] + ', other-value', + * 'custom-header': 'custom-value' + * }; + * } + * } + * }); * ``` */ getHeaderOverrides?: (context: { headers: Record }) => Record;