diff --git a/CHANGES.txt b/CHANGES.txt index 75c777bf..aa1f7b44 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ -1.4.2 (June XX, 2022) - - Updated telemetry to submit data even if user consent is not granted. - - Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while performing its periodic post of impressions. +1.5.0 (June 29, 2022) +- Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config sync.enabled . Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used. +- Updated telemetry logic to track the anonymous config for user consent flag set to declined or unknown. +- Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while its periodic post of impressions is running. 1.4.1 (June 13, 2022) - Bugfixing - Updated submitters logic, to avoid dropping impressions and events that are being tracked while POST request is pending. diff --git a/package-lock.json b/package-lock.json index c959e174..2c6c6e5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.1", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4842,7 +4842,7 @@ "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", "dev": true }, "lodash.isarguments": { diff --git a/package.json b/package.json index d9951490..f7fb2df2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.1", + "version": "1.5.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index 5d55c7df..8d160108 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -5,7 +5,7 @@ import { ILogger } from '../logger/types'; import { objectAssign } from '../utils/lang/objectAssign'; /** - * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call + * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call */ export function clientAttributesDecoration(log: ILogger, client: TClient) { @@ -52,10 +52,10 @@ export function clientAttributesDecoration { @@ -101,8 +101,8 @@ export function clientAttributesDecoration { return { @@ -7,7 +9,38 @@ jest.mock('../submitters/submitterManager', () => { }; }); -import { syncManagerOnlineFactory } from '../syncManagerOnline'; +// Mocked storageManager +const storageManagerMock = { + splits: { + usesSegments: () => false + } +}; + +// @ts-expect-error +// Mocked readinessManager +let readinessManagerMock = { + isReady: jest.fn(() => true) // Fake the signal for the non ready SDK +} as IReadinessManager; + + +// Mocked pollingManager +const pollingManagerMock = { + syncAll: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + isRunning: jest.fn(), + add: jest.fn(()=>{return {isrunning: () => true};}), + get: jest.fn() +}; + +const pushManagerMock = { + start: jest.fn(), + on: jest.fn(), + stop: jest.fn() +}; + +// Mocked pushManager +const pushManagerFactoryMock = jest.fn(() => pushManagerMock); test('syncManagerOnline should start or not the submitter depending on user consent status', () => { const settings = { ...fullSettings }; @@ -53,3 +86,96 @@ test('syncManagerOnline should start or not the submitter depending on user cons expect(submitterManager.execute).lastCalledWith(true); // SubmitterManager should flush only telemetry, if userConsent is unknown }); + +test('syncManagerOnline should syncAll a single time when sync is disabled', () => { + const settings = { ...fullSettings }; + + // disable sync + settings.sync.enabled = false; + + // @ts-ignore + // Test pushManager for main client + const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); + + expect(pushManagerFactoryMock).not.toBeCalled(); + + // Test pollingManager for Main client + syncManager.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.syncAll).toBeCalledTimes(1); + + syncManager.stop(); + syncManager.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.syncAll).toBeCalledTimes(1); + + syncManager.stop(); + syncManager.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.syncAll).toBeCalledTimes(1); + + syncManager.stop(); + + // @ts-ignore + // Test pollingManager for shared client + const pollingSyncManagerShared = syncManager.shared('sharedKey', readinessManagerMock, storageManagerMock); + + if (!pollingSyncManagerShared) throw new Error('pollingSyncManagerShared should exist'); + + pollingSyncManagerShared.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + pollingSyncManagerShared.stop(); + pollingSyncManagerShared.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + pollingSyncManagerShared.stop(); + + syncManager.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + syncManager.stop(); + syncManager.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + syncManager.stop(); + + // @ts-ignore + // Test pollingManager for shared client + const pushingSyncManagerShared = syncManager.shared('pushingSharedKey', readinessManagerMock, storageManagerMock); + + if (!pushingSyncManagerShared) throw new Error('pushingSyncManagerShared should exist'); + + pushingSyncManagerShared.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + pushingSyncManagerShared.stop(); + pushingSyncManagerShared.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + pushingSyncManagerShared.stop(); + + settings.sync.enabled = true; + // @ts-ignore + // pushManager instantiation control test + const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); + + expect(pushManagerFactoryMock).toBeCalled(); + + // Test pollingManager for Main client + testSyncManager.start(); + + expect(pushManagerMock.start).toBeCalled(); + + testSyncManager.stop(); + +}); diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index c4cd4ee5..b6407630 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -28,13 +28,13 @@ export function syncManagerOnlineFactory( */ return function (params: ISdkFactoryContextSync): ISyncManagerCS { - const { settings, settings: { log, streamingEnabled }, telemetryTracker } = params; + const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); /** Push Manager */ - const pushManager = streamingEnabled && pollingManager && pushManagerFactory ? + const pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ? pushManagerFactory(params, pollingManager) : undefined; @@ -89,15 +89,24 @@ export function syncManagerOnlineFactory( // start syncing splits and segments if (pollingManager) { - if (pushManager) { - // Doesn't call `syncAll` when the syncManager is resuming + + // If synchronization is disabled pushManager and pollingManager should not start + if (syncEnabled) { + if (pushManager) { + // Doesn't call `syncAll` when the syncManager is resuming + if (startFirstTime) { + pollingManager.syncAll(); + startFirstTime = false; + } + pushManager.start(); + } else { + pollingManager.start(); + } + } else { if (startFirstTime) { pollingManager.syncAll(); startFirstTime = false; } - pushManager.start(); - } else { - pollingManager.start(); } } @@ -137,18 +146,22 @@ export function syncManagerOnlineFactory( return { isRunning: mySegmentsSyncTask.isRunning, start() { - if (pushManager) { - if (pollingManager!.isRunning()) { - // if doing polling, we must start the periodic fetch of data - if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); + if (syncEnabled) { + if (pushManager) { + if (pollingManager!.isRunning()) { + // if doing polling, we must start the periodic fetch of data + if (storage.splits.usesSegments()) 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(); + } + pushManager.add(matchingKey, mySegmentsSyncTask); } 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(); + if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); } - pushManager.add(matchingKey, mySegmentsSyncTask); } else { - if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); + if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); } }, stop() { diff --git a/src/types.ts b/src/types.ts index dd2cb085..a45c27a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -117,7 +117,8 @@ export interface ISettings { splitFilters: SplitIO.SplitFilter[], impressionsMode: SplitIO.ImpressionsMode, __splitFiltersValidation: ISplitFiltersValidation, - localhostMode?: SplitIO.LocalhostFactory + localhostMode?: SplitIO.LocalhostFactory, + enabled: boolean }, readonly runtime: { ip: string | false @@ -214,6 +215,11 @@ interface ISharedSettings { * @default 'OPTIMIZED' */ impressionsMode?: SplitIO.ImpressionsMode, + /** + * Enables synchronization. + * @property {boolean} enabled + */ + enabled: boolean } } /** diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index e82d1d99..2b3f4918 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -40,6 +40,7 @@ describe('settingsValidation', () => { telemetry: 'https://telemetry.split.io/api', }); expect(settings.sync.impressionsMode).toBe(OPTIMIZED); + expect(settings.sync.enabled).toBe(true); }); test('override with default impressionMode if provided one is invalid', () => { @@ -163,6 +164,27 @@ describe('settingsValidation', () => { expect(settingsWithStreamingEnabled.streamingEnabled).toBe(true); // If streamingEnabled is not provided, it will be true. }); + test('sync.enabled should be overwritable and true by default', () => { + const settingsWithSyncEnabled = settingsValidation({ + core: { + authorizationKey: 'dummy token', + } + }, minimalSettingsParams); + + const settingsWithSyncDisabled = settingsValidation({ + core: { + authorizationKey: 'dummy token' + }, + sync: { + enabled: false + } + }, minimalSettingsParams); + + expect(settingsWithSyncDisabled.sync.enabled).toBe(false); // If sync.enabled is not provided, it will be true. + expect(settingsWithSyncEnabled.sync.enabled).toBe(true); // When creating a setting instance, it will have the provided value for sync.enabled + + }); + const storageMock = () => { }; const integrationsMock = [() => { }]; diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 908bfdc4..8d213b03 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -78,7 +78,8 @@ export const fullSettings: ISettings = { validFilters: [], queryString: null, groupedFilters: { byName: [], byPrefix: [] } - } + }, + enabled: true }, version: 'jest', runtime: { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 2269721f..84d97954 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -83,7 +83,8 @@ export const base = { splitFilters: undefined, // impressions collection mode impressionsMode: OPTIMIZED, - localhostMode: undefined + localhostMode: undefined, + enabled: true }, // Logger @@ -191,6 +192,11 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase); } + // validate sync enabled + if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop + withDefaults.sync.enabled = true; + } + // validate the `splitFilters` settings and parse splits query const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode); withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;