From 6bb988ccec2b8290522d029b74355b01085e60b4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 14 Apr 2025 16:10:11 -0300 Subject: [PATCH 1/3] Update splitChangesFetcher to handle proxy error with spec v1.3 --- src/services/splitApi.ts | 5 +- .../polling/fetchers/splitChangesFetcher.ts | 59 +++++++++++++++++-- src/sync/polling/syncTasks/splitsSyncTask.ts | 2 +- .../__tests__/splitChangesUpdater.spec.ts | 11 ++-- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index b7163b93..6860b022 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -29,7 +29,6 @@ export function splitApiFactory( const urls = settings.urls; const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; const SplitSDKImpressionsMode = settings.sync.impressionsMode; - const flagSpecVersion = settings.sync.flagSpecVersion; const splitHttpClient = splitHttpClientFactory(settings, platform); return { @@ -45,7 +44,7 @@ export function splitApiFactory( }, fetchAuth(userMatchingKeys?: string[]) { - let url = `${urls.auth}/v2/auth?s=${flagSpecVersion}`; + let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`; if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&'); if (queryParams) url += '&' + queryParams; @@ -54,7 +53,7 @@ export function splitApiFactory( }, fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) { - const url = `${urls.sdk}/splitChanges?s=${flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; + const url = `${urls.sdk}/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) .catch((err) => { if (err.statusCode === 414) settings.log.error(ERROR_TOO_MANY_SETS); diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index d134601b..be9ad3a4 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,11 +1,26 @@ +import { ISettings } from '../../../../types/splitio'; +import { ISplitChangesResponse } from '../../../dtos/types'; import { IFetchSplitChanges, IResponse } from '../../../services/types'; +import { IStorageBase } from '../../../storages/types'; +import { FLAG_SPEC_VERSION } from '../../../utils/constants'; +import { base } from '../../../utils/settingsValidation'; import { ISplitChangesFetcher } from './types'; +const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side +const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side + +function sdkEndpointOverriden(settings: ISettings) { + return settings.urls.sdk !== base.urls.sdk; +} + /** * Factory of SplitChanges fetcher. * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors. */ -export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges): ISplitChangesFetcher { +export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { + + const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; + let _lastProxyCheckTimestamp: number | undefined; return function splitChangesFetcher( since: number, @@ -14,12 +29,48 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges rbSince?: number, // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise - ) { + ): Promise { + + if (_lastProxyCheckTimestamp && (Date.now() - _lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { + settings.sync.flagSpecVersion = FLAG_SPEC_VERSION; + } + + let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince) + // Handle proxy errors with spec 1.3 + .catch((err) => { + if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { + _lastProxyCheckTimestamp = Date.now(); + settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec + return fetchSplitChanges(since, noCache, till); // retry request without rbSince + } + throw err; + }); - let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince); if (decorator) splitsPromise = decorator(splitsPromise); - return splitsPromise.then(resp => resp.json()); + return splitsPromise + .then(resp => resp.json()) + .then(data => { + // Using flag spec version 1.2 + if (data.splits) { + return { + ff: { + d: data.splits, + s: data.since, + t: data.till + } + }; + } + + // Proxy recovery + if (_lastProxyCheckTimestamp) { + _lastProxyCheckTimestamp = undefined; + return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) + .then(() => splitChangesFetcher(-1, undefined, undefined, -1)); + } + + return data; + }); }; } diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index d6fed5a2..d385bf77 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -21,7 +21,7 @@ export function splitsSyncTaskFactory( settings.log, splitChangesUpdaterFactory( settings.log, - splitChangesFetcherFactory(fetchSplitChanges), + splitChangesFetcherFactory(fetchSplitChanges, settings, storage), storage, settings.sync.__splitFiltersValidation, readiness.splits, diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index a77e9516..750f1c0d 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -156,12 +156,6 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { }); describe('splitChangesUpdater', () => { - - fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore - const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); - const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); - const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); - const splits = new SplitsCacheInMemory(); const updateSplits = jest.spyOn(splits, 'update'); @@ -173,6 +167,11 @@ describe('splitChangesUpdater', () => { const storage = { splits, rbSegments, segments }; + fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore + const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); + const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); + const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges, fullSettings, storage); + const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); From 8429d400ec14109cba96334364c8bf784003b76e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 12:56:04 -0300 Subject: [PATCH 2/3] Implementation fixes --- .../polling/fetchers/splitChangesFetcher.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index be9ad3a4..928ab6e4 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -17,10 +17,11 @@ function sdkEndpointOverriden(settings: ISettings) { * Factory of SplitChanges fetcher. * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors. */ +// @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; - let _lastProxyCheckTimestamp: number | undefined; + let lastProxyCheckTimestamp: number | undefined; return function splitChangesFetcher( since: number, @@ -31,15 +32,16 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges decorator?: (promise: Promise) => Promise ): Promise { - if (_lastProxyCheckTimestamp && (Date.now() - _lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { + // Recheck proxy + if (lastProxyCheckTimestamp && (Date.now() - lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { settings.sync.flagSpecVersion = FLAG_SPEC_VERSION; } - let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince) - // Handle proxy errors with spec 1.3 + let splitsPromise = fetchSplitChanges(since, noCache, till, settings.sync.flagSpecVersion === FLAG_SPEC_VERSION ? rbSince : undefined) + // Handle proxy error with spec 1.3 .catch((err) => { if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { - _lastProxyCheckTimestamp = Date.now(); + lastProxyCheckTimestamp = Date.now(); settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec return fetchSplitChanges(since, noCache, till); // retry request without rbSince } @@ -63,10 +65,10 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges } // Proxy recovery - if (_lastProxyCheckTimestamp) { - _lastProxyCheckTimestamp = undefined; + if (lastProxyCheckTimestamp) { + lastProxyCheckTimestamp = undefined; return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) - .then(() => splitChangesFetcher(-1, undefined, undefined, -1)); + .then(() => splitChangesFetcher(storage.splits.getChangeNumber() as number, undefined, undefined, storage.rbSegments.getChangeNumber() as number)); } return data; From 1118e4949f6bedd0cb7ab4e97aad075e2b78d871 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 13:04:39 -0300 Subject: [PATCH 3/3] Add logs --- src/sync/polling/fetchers/splitChangesFetcher.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 928ab6e4..58f87e9a 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,10 +1,11 @@ -import { ISettings } from '../../../../types/splitio'; +import { ISettings } from '../../../types'; import { ISplitChangesResponse } from '../../../dtos/types'; import { IFetchSplitChanges, IResponse } from '../../../services/types'; import { IStorageBase } from '../../../storages/types'; import { FLAG_SPEC_VERSION } from '../../../utils/constants'; import { base } from '../../../utils/settingsValidation'; import { ISplitChangesFetcher } from './types'; +import { LOG_PREFIX_SYNC_SPLITS } from '../../../logger/constants'; const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side @@ -20,6 +21,7 @@ function sdkEndpointOverriden(settings: ISettings) { // @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { + const log = settings.log; const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; let lastProxyCheckTimestamp: number | undefined; @@ -41,6 +43,7 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges // Handle proxy error with spec 1.3 .catch((err) => { if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { + log.error(LOG_PREFIX_SYNC_SPLITS + 'Proxy error detected. If you are using Split Proxy, please upgrade to latest version'); lastProxyCheckTimestamp = Date.now(); settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec return fetchSplitChanges(since, noCache, till); // retry request without rbSince @@ -66,6 +69,7 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges // Proxy recovery if (lastProxyCheckTimestamp) { + log.info(LOG_PREFIX_SYNC_SPLITS + 'Proxy error recovered'); lastProxyCheckTimestamp = undefined; return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) .then(() => splitChangesFetcher(storage.splits.getChangeNumber() as number, undefined, undefined, storage.rbSegments.getChangeNumber() as number));