From c1644e658c1289d978e9b09e8576a84207ba772b Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 6 Jun 2022 14:42:01 -0300 Subject: [PATCH 01/19] SDKS-5789. Add singleSync config parameter --- package-lock.json | 4 ++-- src/types.ts | 8 +++++++- src/utils/settingsValidation/__tests__/index.spec.ts | 1 + src/utils/settingsValidation/__tests__/settings.mocks.ts | 3 ++- src/utils/settingsValidation/index.ts | 3 ++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2be921ba..be95b5e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4836,7 +4836,7 @@ "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true }, "lodash.flatten": { @@ -4848,7 +4848,7 @@ "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "dev": true }, "lodash.isequal": { diff --git a/src/types.ts b/src/types.ts index dd2cb085..a8d7b45c 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, + singleSync: boolean }, readonly runtime: { ip: string | false @@ -214,6 +215,11 @@ interface ISharedSettings { * @default 'OPTIMIZED' */ impressionsMode?: SplitIO.ImpressionsMode, + /** + * single Sync enables. + * @property {boolean} singleSync + */ + singleSync: boolean } } /** diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index e82d1d99..a9484dcd 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.singleSync).toBe(false); }); test('override with default impressionMode if provided one is invalid', () => { diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 908bfdc4..5e79e487 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: [] } - } + }, + singleSync: false }, version: 'jest', runtime: { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 2269721f..1d05a061 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, + singleSync: false }, // Logger From 19cf490f30fce78b7f922ab3658049d9f671ebb0 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 6 Jun 2022 15:24:08 -0300 Subject: [PATCH 02/19] Add singleSync setting validation and test --- .../__tests__/index.spec.ts | 20 +++++++++++++++++++ src/utils/settingsValidation/index.ts | 5 +++++ 2 files changed, 25 insertions(+) diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index a9484dcd..0ea181e2 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -164,6 +164,26 @@ describe('settingsValidation', () => { expect(settingsWithStreamingEnabled.streamingEnabled).toBe(true); // If streamingEnabled is not provided, it will be true. }); + test('singleSync should be overwritable and false by default', () => { + const settingsWithSingleSyncDisabled = settingsValidation({ + core: { + authorizationKey: 'dummy token', + } + }, minimalSettingsParams); + const settingsWithSingleSyncEnabled = settingsValidation({ + core: { + authorizationKey: 'dummy token' + }, + sync: { + singleSync: true + } + }, minimalSettingsParams); + + expect(settingsWithSingleSyncDisabled.sync.singleSync).toBe(false); // If singleSync is not provided, it will be true. + expect(settingsWithSingleSyncEnabled.sync.singleSync).toBe(true); // When creating a setting instance, it will have the provided value for singleSync + + }); + const storageMock = () => { }; const integrationsMock = [() => { }]; diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 1d05a061..277063e8 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -192,6 +192,11 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase); } + // validate singleSync + if (withDefaults.sync.singleSync !== true) { // @ts-ignore, modify readonly prop + withDefaults.sync.singleSync = false; + } + // validate the `splitFilters` settings and parse splits query const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode); withDefaults.sync.splitFilters = splitFiltersValidation.validFilters; From d401084adccfaf0e32446805e5ec8182ff62964b Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 6 Jun 2022 15:28:10 -0300 Subject: [PATCH 03/19] fix check --- src/utils/settingsValidation/__tests__/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 0ea181e2..8b53f43f 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -165,7 +165,7 @@ describe('settingsValidation', () => { }); test('singleSync should be overwritable and false by default', () => { - const settingsWithSingleSyncDisabled = settingsValidation({ + const settingsWithSingleSyncDisabled = settingsValidation({ core: { authorizationKey: 'dummy token', } From a14ec15bd4d61d633c3e0eec2adee9a400d787f5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 7 Jun 2022 13:34:49 -0300 Subject: [PATCH 04/19] remove syncTaskComposite for code simplicity --- src/sync/submitters/submitterManager.ts | 20 +++++++++++++++++-- src/sync/syncTaskComposite.ts | 26 ------------------------- 2 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 src/sync/syncTaskComposite.ts diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 523e5ab5..7bd99dde 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -1,4 +1,3 @@ -import { syncTaskComposite } from '../syncTaskComposite'; import { eventsSubmitterFactory } from './eventsSubmitter'; import { impressionsSubmitterFactory } from './impressionsSubmitter'; import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter'; @@ -17,5 +16,22 @@ export function submitterManagerFactory(params: ISdkFactoryContextSync) { const telemetrySubmitter = telemetrySubmitterFactory(params); if (telemetrySubmitter) submitters.push(telemetrySubmitter); - return syncTaskComposite(submitters); + + return { + start() { + submitters.forEach(submitter => submitter.start()); + }, + stop() { + submitters.forEach(submitter => submitter.stop()); + }, + isRunning() { + return submitters.some(submitter => submitter.isRunning()); + }, + execute() { + return Promise.all(submitters.map(submitter => submitter.execute())); + }, + isExecuting() { + return submitters.some(submitter => submitter.isExecuting()); + } + }; } diff --git a/src/sync/syncTaskComposite.ts b/src/sync/syncTaskComposite.ts deleted file mode 100644 index b16a1704..00000000 --- a/src/sync/syncTaskComposite.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ISyncTask } from './types'; - -/** - * Composite Sync Task: group of sync tasks that are treated as a single one. - */ -export function syncTaskComposite(syncTasks: ISyncTask[]): ISyncTask { - - return { - start() { - syncTasks.forEach(syncTask => syncTask.start()); - }, - stop() { - syncTasks.forEach(syncTask => syncTask.stop()); - }, - isRunning() { - return syncTasks.some(syncTask => syncTask.isRunning()); - }, - execute() { - return Promise.all(syncTasks.map(syncTask => syncTask.execute())); - }, - isExecuting() { - return syncTasks.some(syncTask => syncTask.isExecuting()); - } - }; - -} From b625f42a8bcdde0c8122fbe5a84471e195b6ca31 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 7 Jun 2022 16:08:56 -0300 Subject: [PATCH 05/19] implementation and test updates --- src/consent/__tests__/sdkUserConsent.spec.ts | 14 ++++----- src/consent/sdkUserConsent.ts | 7 +++-- src/listeners/__tests__/browser.spec.ts | 5 ++-- src/listeners/browser.ts | 14 +++++---- src/sync/__tests__/syncManagerOnline.spec.ts | 31 ++++++++++++++------ src/sync/submitters/submitterManager.ts | 19 +++++++----- src/sync/submitters/types.ts | 7 +++++ src/sync/syncManagerOnline.ts | 11 ++++--- src/sync/types.ts | 3 +- 9 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/consent/__tests__/sdkUserConsent.spec.ts b/src/consent/__tests__/sdkUserConsent.spec.ts index 706b6ca9..e7981871 100644 --- a/src/consent/__tests__/sdkUserConsent.spec.ts +++ b/src/consent/__tests__/sdkUserConsent.spec.ts @@ -4,7 +4,7 @@ import { fullSettings } from '../../utils/settingsValidation/__tests__/settings. test('createUserConsentAPI', () => { const settings = { ...fullSettings, userConsent: 'UNKNOWN' }; - const syncManager = { submitter: syncTaskFactory() }; + const syncManager = { submitterManager: syncTaskFactory() }; const storage = { events: { clear: jest.fn() }, impressions: { clear: jest.fn() } @@ -20,15 +20,15 @@ test('createUserConsentAPI', () => { // setting user consent to 'GRANTED' expect(props.setStatus(true)).toBe(true); expect(props.setStatus(true)).toBe(true); // calling again has no affect - expect(syncManager.submitter.start).toBeCalledTimes(1); // submitter resumed - expect(syncManager.submitter.stop).toBeCalledTimes(0); + expect(syncManager.submitterManager.start).toBeCalledTimes(1); // submitter resumed + expect(syncManager.submitterManager.stop).toBeCalledTimes(0); expect(props.getStatus()).toBe(props.Status.GRANTED); // setting user consent to 'DECLINED' expect(props.setStatus(false)).toBe(true); expect(props.setStatus(false)).toBe(true); // calling again has no affect - expect(syncManager.submitter.start).toBeCalledTimes(1); - expect(syncManager.submitter.stop).toBeCalledTimes(1); // submitter paused + expect(syncManager.submitterManager.start).toBeCalledTimes(1); + expect(syncManager.submitterManager.stop).toBeCalledTimes(1); // submitter paused expect(props.getStatus()).toBe(props.Status.DECLINED); expect(storage.events.clear).toBeCalledTimes(1); // storage tracked data dropped expect(storage.impressions.clear).toBeCalledTimes(1); @@ -39,7 +39,7 @@ test('createUserConsentAPI', () => { expect(props.setStatus(undefined)).toBe(false); expect(props.setStatus({})).toBe(false); - expect(syncManager.submitter.start).toBeCalledTimes(1); - expect(syncManager.submitter.stop).toBeCalledTimes(1); + expect(syncManager.submitterManager.start).toBeCalledTimes(1); + expect(syncManager.submitterManager.stop).toBeCalledTimes(1); expect(props.getStatus()).toBe(props.Status.DECLINED); }); diff --git a/src/consent/sdkUserConsent.ts b/src/consent/sdkUserConsent.ts index ac8af3d8..e8f12156 100644 --- a/src/consent/sdkUserConsent.ts +++ b/src/consent/sdkUserConsent.ts @@ -34,9 +34,10 @@ export function createUserConsentAPI(params: ISdkFactoryContext) { settings.userConsent = newConsentStatus; if (consent) { // resumes submitters if transitioning to GRANTED - syncManager?.submitter?.start(); - } else { // pauses submitters and drops tracked data if transitioning to DECLINED - syncManager?.submitter?.stop(); + syncManager?.submitterManager?.start(); + } else { // pauses submitters (except telemetry), and drops tracked data if transitioning to DECLINED + syncManager?.submitterManager?.stop(true); + // @ts-ignore, clear method is present in storage for standalone and partial consumer mode if (events.clear) events.clear(); // @ts-ignore if (impressions.clear) impressions.clear(); diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 4657d24e..a73194d3 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -258,11 +258,12 @@ test('Browser JS listener / standalone mode / user consent status', () => { settings.userConsent = 'DECLINED'; triggerUnloadEvent(); - // Unload event was triggered when user consent was unknown and declined. Thus sendBeacon and post services should not be called - expect(global.window.navigator.sendBeacon).toBeCalledTimes(0); + // Unload event was triggered when user consent was unknown and declined. Thus sendBeacon and post services should be called only for telemetry + expect(global.window.navigator.sendBeacon).toBeCalledTimes(2); expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); + (global.window.navigator.sendBeacon as jest.Mock).mockClear(); settings.userConsent = 'GRANTED'; triggerUnloadEvent(); diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index faf5c956..134de65e 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -67,7 +67,7 @@ export class BrowserSignalListener implements ISignalListener { flushData() { if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush - // Flush data if there is user consent + // Flush impressions & events data if there is user consent if (isConsentGranted(this.settings)) { const eventsUrl = this.settings.urls.events; const extraMetadata = { @@ -78,11 +78,13 @@ export class BrowserSignalListener implements ISignalListener { this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata); this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk); if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector); - if (this.storage.telemetry) { - const telemetryUrl = this.settings.urls.telemetry; - const telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments); - this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage); - } + } + + // Flush telemetry data + if (this.storage.telemetry) { + const telemetryUrl = this.settings.urls.telemetry; + const telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments); + this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage); } // Close streaming connection diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index c47cad86..08b6bcaa 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -14,29 +14,42 @@ test('syncManagerOnline should start or not the submitter depending on user cons // @ts-ignore const syncManager = syncManagerOnlineFactory()({ settings }); - const submitter = syncManager.submitter!; + const submitterManager = syncManager.submitterManager!; syncManager.start(); - expect(submitter.start).toBeCalledTimes(1); // Submitter should be started if userConsent is undefined + expect(submitterManager.start).toBeCalledTimes(1); + expect(submitterManager.start).lastCalledWith(false); // SubmitterManager should start all submitters, if userConsent is undefined syncManager.stop(); - expect(submitter.stop).toBeCalledTimes(1); + expect(submitterManager.stop).toBeCalledTimes(1); settings.userConsent = 'UNKNOWN'; syncManager.start(); - expect(submitter.start).toBeCalledTimes(1); // Submitter should not be started if userConsent is unknown + expect(submitterManager.start).toBeCalledTimes(2); + expect(submitterManager.start).lastCalledWith(true); // SubmitterManager should start only telemetry submitter, if userConsent is unknown syncManager.stop(); - expect(submitter.stop).toBeCalledTimes(2); + expect(submitterManager.stop).toBeCalledTimes(2); + syncManager.flush(); + expect(submitterManager.execute).toBeCalledTimes(1); + expect(submitterManager.execute).lastCalledWith(true); // SubmitterManager should flush only telemetry, if userConsent is unknown settings.userConsent = 'GRANTED'; syncManager.start(); - expect(submitter.start).toBeCalledTimes(2); // Submitter should be started if userConsent is granted + expect(submitterManager.start).toBeCalledTimes(3); + expect(submitterManager.start).lastCalledWith(false); // SubmitterManager should start all submitters, if userConsent is granted syncManager.stop(); - expect(submitter.stop).toBeCalledTimes(3); + expect(submitterManager.stop).toBeCalledTimes(3); + syncManager.flush(); + expect(submitterManager.execute).toBeCalledTimes(2); + expect(submitterManager.execute).lastCalledWith(false); // SubmitterManager should flush all submitters, if userConsent is granted settings.userConsent = 'DECLINED'; syncManager.start(); - expect(submitter.start).toBeCalledTimes(2); // Submitter should not be started if userConsent is declined + expect(submitterManager.start).toBeCalledTimes(4); + expect(submitterManager.start).lastCalledWith(true); // SubmitterManager should start only telemetry submitter, if userConsent is declined syncManager.stop(); - expect(submitter.stop).toBeCalledTimes(4); + expect(submitterManager.stop).toBeCalledTimes(4); + syncManager.flush(); + expect(submitterManager.execute).toBeCalledTimes(3); + expect(submitterManager.execute).lastCalledWith(true); // SubmitterManager should flush only telemetry, if userConsent is unknown }); diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 7bd99dde..7ec32bdb 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -3,8 +3,9 @@ import { impressionsSubmitterFactory } from './impressionsSubmitter'; import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter'; import { telemetrySubmitterFactory } from './telemetrySubmitter'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; +import { ISubmitterManager } from './types'; -export function submitterManagerFactory(params: ISdkFactoryContextSync) { +export function submitterManagerFactory(params: ISdkFactoryContextSync): ISubmitterManager { const submitters = [ impressionsSubmitterFactory(params), @@ -14,21 +15,23 @@ export function submitterManagerFactory(params: ISdkFactoryContextSync) { const impressionCountsSubmitter = impressionCountsSubmitterFactory(params); if (impressionCountsSubmitter) submitters.push(impressionCountsSubmitter); const telemetrySubmitter = telemetrySubmitterFactory(params); - if (telemetrySubmitter) submitters.push(telemetrySubmitter); - return { - start() { - submitters.forEach(submitter => submitter.start()); + start(onlyTelemetry?: boolean) { + if (!onlyTelemetry) submitters.forEach(submitter => submitter.start()); + if (telemetrySubmitter) telemetrySubmitter.start(); }, - stop() { + stop(allExceptTelemetry?: boolean) { submitters.forEach(submitter => submitter.stop()); + if (!allExceptTelemetry && telemetrySubmitter) telemetrySubmitter.stop(); }, isRunning() { return submitters.some(submitter => submitter.isRunning()); }, - execute() { - return Promise.all(submitters.map(submitter => submitter.execute())); + execute(onlyTelemetry?: boolean) { + const promises = onlyTelemetry ? [] : submitters.map(submitter => submitter.execute()); + if (telemetrySubmitter) promises.push(telemetrySubmitter.execute()); + return Promise.all(promises); }, isExecuting() { return submitters.some(submitter => submitter.isExecuting()); diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index f4eb8c7b..fe9fac26 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -1,5 +1,6 @@ import { IMetadata } from '../../dtos/types'; import { SplitIO } from '../../types'; +import { ISyncTask } from '../types'; export type ImpressionsPayload = { /** Split name */ @@ -191,3 +192,9 @@ export type TelemetryConfigStatsPayload = TelemetryConfigStats & { i?: Array, // integrations uC: number, // userConsent } + +export interface ISubmitterManager extends ISyncTask { + start(onlyTelemetry?: boolean): void, + stop(allExceptTelemetry?: boolean): void, + execute(onlyTelemetry?: boolean): Promise +} diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 61f0603d..c4cd4ee5 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -40,7 +40,7 @@ export function syncManagerOnlineFactory( /** Submitter Manager */ // It is not inyected as push and polling managers, because at the moment it is required - const submitter = submitterManagerFactory(params); + const submitterManager = submitterManagerFactory(params); /** Sync Manager logic */ @@ -79,7 +79,7 @@ export function syncManagerOnlineFactory( // E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline). pollingManager, pushManager, - submitter, + submitterManager, /** * Method used to start the syncManager for the first time, or resume it after being stopped. @@ -102,7 +102,7 @@ export function syncManagerOnlineFactory( } // start periodic data recording (events, impressions, telemetry). - if (isConsentGranted(settings)) submitter.start(); + submitterManager.start(!isConsentGranted(settings)); }, /** @@ -116,7 +116,7 @@ export function syncManagerOnlineFactory( if (pollingManager && pollingManager.isRunning()) pollingManager.stop(); // stop periodic data recording (events, impressions, telemetry). - submitter.stop(); + submitterManager.stop(); }, isRunning() { @@ -124,8 +124,7 @@ export function syncManagerOnlineFactory( }, flush() { - if (isConsentGranted(settings)) return submitter.execute(); - else return Promise.resolve(); + return submitterManager.execute(!isConsentGranted(settings)); }, // [Only used for client-side] diff --git a/src/sync/types.ts b/src/sync/types.ts index 22f51eb9..81727ca9 100644 --- a/src/sync/types.ts +++ b/src/sync/types.ts @@ -2,6 +2,7 @@ import { IReadinessManager } from '../readiness/types'; import { IStorageSync } from '../storages/types'; import { IPollingManager } from './polling/types'; import { IPushManager } from './streaming/types'; +import { ISubmitterManager } from './submitters/types'; export interface ITask { /** @@ -39,7 +40,7 @@ export interface ISyncManager extends ITask { flush(): Promise, pushManager?: IPushManager, pollingManager?: IPollingManager, - submitter?: ISyncTask + submitterManager?: ISubmitterManager } export interface ISyncManagerCS extends ISyncManager { From 48d8b270d9848242b449f5a62c27ae9b8d5fbc5e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 13 Jun 2022 10:53:21 -0300 Subject: [PATCH 06/19] Update SyncTask to run executions secuentially to avoid race conditions on submitters --- src/sync/__tests__/syncTask.spec.ts | 112 +++++++++++------- .../__tests__/eventsSubmitter.spec.ts | 19 +-- .../__tests__/impressionsSubmitter.spec.ts | 22 ++++ src/sync/syncTask.ts | 30 +++-- 4 files changed, 123 insertions(+), 60 deletions(-) diff --git a/src/sync/__tests__/syncTask.spec.ts b/src/sync/__tests__/syncTask.spec.ts index 49ecf67f..fa3145cb 100644 --- a/src/sync/__tests__/syncTask.spec.ts +++ b/src/sync/__tests__/syncTask.spec.ts @@ -5,7 +5,7 @@ const period = 30; const taskResult = 'taskResult'; const asyncTask = jest.fn(() => Promise.resolve(taskResult)); -test('syncTaskFactory', (done) => { +test('syncTaskFactory / periodic execution', async () => { const syncTask = syncTaskFactory(loggerMock, asyncTask, period); @@ -40,45 +40,75 @@ test('syncTaskFactory', (done) => { expect(syncTask.isExecuting()).toBe(true); // Executing - setTimeout(() => { - expect(asyncTask).toHaveBeenLastCalledWith(...startArgs); // Periodic call should be done with the initial `start` arguments - expect(asyncTask).toBeCalledTimes(4); // The task was executed 4 times: twice due to periodic execution and twice due to execute call - - setTimeout(() => { - expect(asyncTask).toHaveBeenLastCalledWith(...startArgs); // Periodic call should be done with the initial `start` arguments - expect(asyncTask).toBeCalledTimes(5); // The task was executed 5 times: 3 due to periodic execution and twice due to execute call - - // Calling `stop` stops the periodic execution of the given task - expect(syncTask.isRunning()).toBe(true); // Running periodically - syncTask.stop(); - expect(syncTask.isRunning()).toBe(false); // Stop running periodically - - setTimeout(() => { - expect(asyncTask).toBeCalledTimes(5); // Stopped task should not be called again - - // Stopping and starting - syncTask.stop(); - syncTask.start(); // Inmediatelly call task - syncTask.stop(); - syncTask.start(); // Inmediatelly call task - syncTask.stop(); - expect(asyncTask).toBeCalledTimes(7); - - // Resume periodic execution - syncTask.start(); // Inmediatelly call task - syncTask.start(); // No effect - expect(asyncTask).toBeCalledTimes(8); - - setTimeout(() => { - expect(asyncTask).toBeCalledTimes(9); // Stopped task should not be called again - expect(syncTask.isRunning()).toBe(true); // Running periodically - syncTask.stop(); // Finally stop to finish the test - expect(syncTask.isRunning()).toBe(false); // Stop running periodically - - done(); - }, period + 10); - }, period + 10); - }, period + 10); - }, period + 10); + await new Promise(res => setTimeout(res, period + 10)); + expect(asyncTask).toHaveBeenLastCalledWith(...startArgs); // Periodic call should be done with the initial `start` arguments + expect(asyncTask).toBeCalledTimes(4); // The task was executed 4 times: twice due to periodic execution and twice due to execute call + + await new Promise(res => setTimeout(res, period + 10)); + expect(asyncTask).toHaveBeenLastCalledWith(...startArgs); // Periodic call should be done with the initial `start` arguments + expect(asyncTask).toBeCalledTimes(5); // The task was executed 5 times: 3 due to periodic execution and twice due to execute call + + // Calling `stop` stops the periodic execution of the given task + expect(syncTask.isRunning()).toBe(true); // Running periodically + syncTask.stop(); + expect(syncTask.isRunning()).toBe(false); // Stop running periodically + + await new Promise(res => setTimeout(res, period + 10)); + expect(asyncTask).toBeCalledTimes(5); // Stopped task should not be called again + + // Stopping and starting + syncTask.stop(); + syncTask.start(); // Inmediatelly call task + syncTask.stop(); + syncTask.start(); // Doesn't call task since previous one has not been resolved + syncTask.stop(); + expect(asyncTask).toBeCalledTimes(6); + + // Resume periodic execution + syncTask.start(); // Doesn't call task since previous one has not been resolved + syncTask.start(); // No effect + expect(asyncTask).toBeCalledTimes(6); + + await new Promise(res => setTimeout(res, period + 10)); + expect(asyncTask).toBeCalledTimes(9); // Stopped task should not be called again + expect(syncTask.isRunning()).toBe(true); // Running periodically + syncTask.stop(); // Finally stop to finish the test + expect(syncTask.isRunning()).toBe(false); // Stop running periodically + +}); + +test('syncTaskFactory / chaining executions', (done) => { + let executeCount = 0; + let resolveOrder = 0; + const asyncTask = jest.fn((toReturn) => { + executeCount++; + return new Promise(res => setTimeout(() => res(toReturn))); + }); + + const syncTask = syncTaskFactory(loggerMock, asyncTask, period); + + syncTask.execute(1).then(result=> { + // console.log('1'); + resolveOrder++; + expect(resolveOrder).toBe(1); // @TODO should be 1 ? + expect(executeCount).toBe(1); + expect(result).toBe(1); // @TODO should be 1 + }); + syncTask.execute(2).then(result=> { + // console.log('2'); + resolveOrder++; + expect(resolveOrder).toBe(2); + expect(executeCount).toBe(3); // @TODO borrar + expect(result).toBe(2); // @TODO should be 2 + }); + syncTask.execute(3).then(result=> { + // console.log('3'); + resolveOrder++; + expect(resolveOrder).toBe(3); // @TODO should be 3 ? + expect(executeCount).toBe(3); + expect(result).toBe(3); + + done(); + }); }); diff --git a/src/sync/submitters/__tests__/eventsSubmitter.spec.ts b/src/sync/submitters/__tests__/eventsSubmitter.spec.ts index 60de0379..d5f95ece 100644 --- a/src/sync/submitters/__tests__/eventsSubmitter.spec.ts +++ b/src/sync/submitters/__tests__/eventsSubmitter.spec.ts @@ -48,7 +48,7 @@ describe('Events submitter', () => { expect(eventsSubmitter.isRunning()).toEqual(false); }); - test('without eventsFirstPushWindow', async () => { + test('without eventsFirstPushWindow', (done) => { const eventsFirstPushWindow = 0; params.settings.startup.eventsFirstPushWindow = eventsFirstPushWindow; // @ts-ignore const eventsSubmitter = eventsSubmitterFactory(params); @@ -58,14 +58,19 @@ describe('Events submitter', () => { expect(eventsSubmitter.isExecuting()).toEqual(true); // and executes immediatelly if there isn't a push window expect(eventsCacheMock.isEmpty).toBeCalledTimes(1); - // If queue is full, submitter should be executed + // If queue is full, submitter is executed again after current execution is resolved __onFullQueueCb(); - expect(eventsSubmitter.isExecuting()).toEqual(true); - expect(eventsCacheMock.isEmpty).toBeCalledTimes(2); + expect(eventsCacheMock.isEmpty).toBeCalledTimes(1); - expect(eventsSubmitter.isRunning()).toEqual(true); - eventsSubmitter.stop(); - expect(eventsSubmitter.isRunning()).toEqual(false); + setTimeout(()=> { + expect(eventsSubmitter.isExecuting()).toEqual(false); + expect(eventsCacheMock.isEmpty).toBeCalledTimes(2); // 2 executions: 1st due to start and 2nd due to full queue + + expect(eventsSubmitter.isRunning()).toEqual(true); + eventsSubmitter.stop(); + expect(eventsSubmitter.isRunning()).toEqual(false); + done(); + }); }); test('doesn\'t drop items from cache when POST is resolved', (done) => { diff --git a/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts b/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts index d5c74144..0cf95cd4 100644 --- a/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts +++ b/src/sync/submitters/__tests__/impressionsSubmitter.spec.ts @@ -72,4 +72,26 @@ describe('Impressions submitter', () => { }, params.settings.scheduler.impressionsPushRate + 10); }); + test('if it is executed while POST is pending, execution is queued until POST is resolved and not same items are submitted', (done) => { + // Make the POST request fail + params.splitApi.postTestImpressionsBulk.mockImplementation(() => Promise.resolve()); + + impressionsCacheInMemory.track([imp1]); + impressionsSubmitter.start(); + + // Tracking impression and executing submitter while POST is pending + impressionsCacheInMemory.track([{ ...imp1, keyName: 'k2' }]); + impressionsSubmitter.execute().then(() => { + expect(params.splitApi.postTestImpressionsBulk.mock.calls).toEqual([ + // impression for k1 + ['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123}]}]'], + // impression for k2 + ['[{"f":"someFeature","i":[{"k":"k2","t":"someTreatment","m":0,"c":123}]}]']]); + impressionsSubmitter.stop(); + + done(); + }); + + }); + }); diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index eb3a2c30..9d3a5a4e 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -3,9 +3,9 @@ import { ILogger } from '../logger/types'; import { ISyncTask } from './types'; /** - * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods). - * The task can be executed once calling the "execute" method. - * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization. + * Creates a syncTask that handles the periodic execution of a given task (`start` and `stop` methods). + * The task can be also executed calling the `execute` method. Multiple calls to `execute` are chained, so the task runs secuentially to avoid race conditions. + * For example, submitters executed due to SDK destroy or full queues, while periodic execution is pending. * * @param log Logger instance. * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections. @@ -15,8 +15,8 @@ import { ISyncTask } from './types'; */ export function syncTaskFactory(log: ILogger, task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { - // Flag that indicates if the task is being executed - let executing = false; + // Task promise while it is pending. Undefined once the promise is resolved + let pendingTask: Promise | undefined; // flag that indicates if the task periodic execution has been started/stopped. let running = false; // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently @@ -26,14 +26,21 @@ export function syncTaskFactory(log: ILogger, // Id of the periodic call timeout let timeoutID: any; - function execute(...args: Input) { - executing = true; + function execute(...args: Input): Promise { + // If task is executing, chain the new execution + if (pendingTask) { + return pendingTask.then(() => { + return execute(...args); + }); + } + + // Execute task log.debug(SYNC_TASK_EXECUTE, [taskName]); - return task(...args).then(result => { - executing = false; + pendingTask = task(...args).then(result => { + pendingTask = undefined; return result; }); - // No need to handle promise rejection because it is a pre-condition that provided task never rejects. + return pendingTask; } function periodicExecute(currentRunningId: number) { @@ -46,11 +53,10 @@ export function syncTaskFactory(log: ILogger, } return { - // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming. execute, isExecuting() { - return executing; + return pendingTask !== undefined; }, start(...args: Input) { From 91aa5e9a6023f6a739441fa60421eedd4e6ed9da Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 13 Jun 2022 20:40:31 -0300 Subject: [PATCH 07/19] Add single sync functionality for main client --- src/sdkClient/clientAttributesDecoration.ts | 18 ++++----- src/sync/__tests__/syncManagerOnline.spec.ts | 18 +++++++++ src/sync/syncManagerOnline.ts | 41 +++++++++++++------- 3 files changed, 54 insertions(+), 23 deletions(-) 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 { + const settings = { ...fullSettings }; + + settings.sync.singleSync = true; + + const pollingManager = { + syncAll: jest.fn(), + start: jest.fn() + }; + // @ts-ignore + const syncManager = syncManagerOnlineFactory(() => pollingManager)({ settings }); + + syncManager.start(); + + expect(pollingManager.start).not.toBeCalled(); + expect(pollingManager.syncAll).toBeCalledTimes(1); +}); diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 61f0603d..73f5658a 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -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 singleSync is enabled pushManager and pollingManager should not start + if (settings.sync.singleSync === true) { if (startFirstTime) { pollingManager.syncAll(); startFirstTime = false; } - pushManager.start(); } else { - pollingManager.start(); + if (pushManager) { + // Doesn't call `syncAll` when the syncManager is resuming + if (startFirstTime) { + pollingManager.syncAll(); + startFirstTime = false; + } + pushManager.start(); + } else { + pollingManager.start(); + } } } @@ -138,18 +147,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 (settings.sync.singleSync === true) { + if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); + } else { + 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(); } }, stop() { From f03ede2ec5ae22a22fe5a16931d2956e70a6d912 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 14 Jun 2022 15:45:52 -0300 Subject: [PATCH 08/19] Add test for pushManager --- src/sync/__tests__/syncManagerOnline.spec.ts | 45 ++++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 389f2d27..93678113 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -1,5 +1,9 @@ import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { syncTaskFactory } from './syncTask.mock'; +import { syncManagerOnlineFactory } from '../syncManagerOnline'; +import { pushManagerFactory } from '../streaming/pushManager'; +import { IPushManager } from '../streaming/types'; +import { EventEmitter } from '../../utils/MinEvents'; jest.mock('../submitters/submitterManager', () => { return { @@ -7,7 +11,16 @@ jest.mock('../submitters/submitterManager', () => { }; }); -import { syncManagerOnlineFactory } from '../syncManagerOnline'; +const paramsMock = { + platform: { + getEventSource: jest.fn(() => { return () => { }; }), + EventEmitter + }, + settings: fullSettings, + storage: {}, + readiness: {}, + start: jest.fn() +}; test('syncManagerOnline should start or not the submitter depending on user consent status', () => { const settings = { ...fullSettings }; @@ -48,13 +61,37 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => const pollingManager = { syncAll: jest.fn(), - start: jest.fn() + start: jest.fn(), + stop: jest.fn(), + isRunning: jest.fn() }; + + const fetchAuthMock = jest.fn(); + // @ts-ignore - const syncManager = syncManagerOnlineFactory(() => pollingManager)({ settings }); + const pollingSyncManager = syncManagerOnlineFactory(() => pollingManager)({ settings }); - syncManager.start(); + pollingSyncManager.start(); expect(pollingManager.start).not.toBeCalled(); expect(pollingManager.syncAll).toBeCalledTimes(1); + + pollingSyncManager.stop(); + + const pushManager = pushManagerFactory({ // @ts-ignore + ...paramsMock, splitApi: { fetchAuth: fetchAuthMock } + }, {}) as IPushManager; + + pushManager.start = jest.fn(); + + // @ts-ignore + const pushingSyncManager = syncManagerOnlineFactory(() => pollingManager, () => pushManager)({ settings }); + + pushingSyncManager.start(); + + expect(pushManager.start).not.toBeCalled(); + expect(pollingManager.start).not.toBeCalled(); + + pushingSyncManager.stop(); + }); From 2f8598c88c622f09d90ae75600df415dd587d498 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 15 Jun 2022 13:23:54 -0300 Subject: [PATCH 09/19] Add singleSync tests for shared client. Avoid pushManager instantiation if singleSync is enabled --- src/sync/__tests__/syncManagerOnline.spec.ts | 134 ++++++++++++++++--- src/sync/syncManagerOnline.ts | 8 +- 2 files changed, 120 insertions(+), 22 deletions(-) diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 93678113..dd531b8f 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -4,6 +4,7 @@ import { syncManagerOnlineFactory } from '../syncManagerOnline'; import { pushManagerFactory } from '../streaming/pushManager'; import { IPushManager } from '../streaming/types'; import { EventEmitter } from '../../utils/MinEvents'; +import { IReadinessManager } from '../../readiness/types'; jest.mock('../submitters/submitterManager', () => { return { @@ -22,6 +23,45 @@ const paramsMock = { start: jest.fn() }; +const ALWAYS_ON_SPLIT = '{"trafficTypeName":"user","name":"always-on","trafficAllocation":100,"trafficAllocationSeed":1012950810,"seed":-725161385,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1494364996459,"algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}'; +const ALWAYS_OFF_SPLIT = '{"trafficTypeName":"user","name":"always-off","trafficAllocation":100,"trafficAllocationSeed":-331690370,"seed":403891040,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1494365020316,"algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all"}]}'; + +const STORED_SPLITS: Record = {}; +STORED_SPLITS['always-on'] = ALWAYS_ON_SPLIT; +STORED_SPLITS['always-off'] = ALWAYS_OFF_SPLIT; + +// Mocked storageManager +const storageManagerMock = { + splits: { + getSplit: (name: string) => STORED_SPLITS[name], + 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() +}; + +// Mocked pushManager +const pushManagerMock = pushManagerFactory({ // @ts-ignore + ...paramsMock, splitApi: { fetchAuth: jest.fn() } +}, {}) as IPushManager; + +pushManagerMock.start = jest.fn(); + test('syncManagerOnline should start or not the submitter depending on user consent status', () => { const settings = { ...fullSettings }; @@ -57,41 +97,99 @@ test('syncManagerOnline should start or not the submitter depending on user cons test('syncManagerOnline should syncAll a single time in singleSync mode', () => { const settings = { ...fullSettings }; + // Enable single sync settings.sync.singleSync = true; - const pollingManager = { - syncAll: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - isRunning: jest.fn() - }; + // @ts-ignore + const pollingSyncManager = syncManagerOnlineFactory(() => pollingManagerMock)({ settings }); - const fetchAuthMock = jest.fn(); + expect(pollingSyncManager.pushManager).toBeUndefined(); - // @ts-ignore - const pollingSyncManager = syncManagerOnlineFactory(() => pollingManager)({ settings }); + // Test pollingManager for Main client + pollingSyncManager.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.syncAll).toBeCalledTimes(1); + + pollingSyncManager.stop(); + pollingSyncManager.start(); + expect(pollingManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.syncAll).toBeCalledTimes(1); + + pollingSyncManager.stop(); pollingSyncManager.start(); - expect(pollingManager.start).not.toBeCalled(); - expect(pollingManager.syncAll).toBeCalledTimes(1); + expect(pollingManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.syncAll).toBeCalledTimes(1); pollingSyncManager.stop(); - const pushManager = pushManagerFactory({ // @ts-ignore - ...paramsMock, splitApi: { fetchAuth: fetchAuthMock } - }, {}) as IPushManager; + // @ts-ignore + // Test pollingManager for shared client + const pollingSyncManagerShared = pollingSyncManager.shared('sharedKey', readinessManagerMock, storageManagerMock); + + if (!pollingSyncManagerShared) throw new Error('pollingSyncManagerShared should exist'); + + pollingSyncManagerShared.start(); - pushManager.start = jest.fn(); + expect(pollingManagerMock.start).not.toBeCalled(); + + pollingSyncManagerShared.stop(); + pollingSyncManagerShared.start(); + + expect(pollingManagerMock.start).not.toBeCalled(); + + pollingSyncManagerShared.stop(); // @ts-ignore - const pushingSyncManager = syncManagerOnlineFactory(() => pollingManager, () => pushManager)({ settings }); + // Test pushManager for main client + const pushingSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, () => pushManagerMock)({ settings }); pushingSyncManager.start(); - expect(pushManager.start).not.toBeCalled(); - expect(pollingManager.start).not.toBeCalled(); + expect(pushManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.start).not.toBeCalled(); pushingSyncManager.stop(); + pushingSyncManager.start(); + + expect(pushManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.start).not.toBeCalled(); + + pushingSyncManager.stop(); + + // @ts-ignore + // Test pollingManager for shared client + const pushingSyncManagerShared = pushingSyncManager.shared('pushingSharedKey', readinessManagerMock, storageManagerMock); + + if (!pushingSyncManagerShared) throw new Error('pushingSyncManagerShared should exist'); + + pushingSyncManagerShared.start(); + + expect(pushManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.start).not.toBeCalled(); + + pushingSyncManagerShared.stop(); + pushingSyncManagerShared.start(); + + expect(pushManagerMock.start).not.toBeCalled(); + expect(pollingManagerMock.start).not.toBeCalled(); + + pushingSyncManagerShared.stop(); + + settings.sync.singleSync = false; + // @ts-ignore + // pushManager instantiation control test + const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, () => pushManagerMock)({ settings }); + + expect(testSyncManager.pushManager).not.toBeUndefined(); + + // 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 73f5658a..f51bcce3 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: { singleSync } }, telemetryTracker } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); /** Push Manager */ - const pushManager = streamingEnabled && pollingManager && pushManagerFactory ? + const pushManager = !singleSync && streamingEnabled && pollingManager && pushManagerFactory ? pushManagerFactory(params, pollingManager) : undefined; @@ -91,7 +91,7 @@ export function syncManagerOnlineFactory( if (pollingManager) { // If singleSync is enabled pushManager and pollingManager should not start - if (settings.sync.singleSync === true) { + if (singleSync === true) { if (startFirstTime) { pollingManager.syncAll(); startFirstTime = false; @@ -147,7 +147,7 @@ export function syncManagerOnlineFactory( return { isRunning: mySegmentsSyncTask.isRunning, start() { - if (settings.sync.singleSync === true) { + if (singleSync === true) { if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); } else { if (pushManager) { From d07638431aeb5a4e14f2b3579c4a32ee9ec33bb2 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 15 Jun 2022 14:54:03 -0300 Subject: [PATCH 10/19] Use one syncManager instance for tests --- src/sync/__tests__/syncManagerOnline.spec.ts | 41 +++++++------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index dd531b8f..434ce24c 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -23,17 +23,9 @@ const paramsMock = { start: jest.fn() }; -const ALWAYS_ON_SPLIT = '{"trafficTypeName":"user","name":"always-on","trafficAllocation":100,"trafficAllocationSeed":1012950810,"seed":-725161385,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1494364996459,"algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}'; -const ALWAYS_OFF_SPLIT = '{"trafficTypeName":"user","name":"always-off","trafficAllocation":100,"trafficAllocationSeed":-331690370,"seed":403891040,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1494365020316,"algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all"}]}'; - -const STORED_SPLITS: Record = {}; -STORED_SPLITS['always-on'] = ALWAYS_ON_SPLIT; -STORED_SPLITS['always-off'] = ALWAYS_OFF_SPLIT; - // Mocked storageManager const storageManagerMock = { splits: { - getSplit: (name: string) => STORED_SPLITS[name], usesSegments: () => false } }; @@ -101,33 +93,34 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => settings.sync.singleSync = true; // @ts-ignore - const pollingSyncManager = syncManagerOnlineFactory(() => pollingManagerMock)({ settings }); + // Test pushManager for main client + const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, () => pushManagerMock)({ settings }); - expect(pollingSyncManager.pushManager).toBeUndefined(); + expect(syncManager.pushManager).toBeUndefined(); // Test pollingManager for Main client - pollingSyncManager.start(); + syncManager.start(); expect(pollingManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.syncAll).toBeCalledTimes(1); - pollingSyncManager.stop(); - pollingSyncManager.start(); + syncManager.stop(); + syncManager.start(); expect(pollingManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.syncAll).toBeCalledTimes(1); - pollingSyncManager.stop(); - pollingSyncManager.start(); + syncManager.stop(); + syncManager.start(); expect(pollingManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.syncAll).toBeCalledTimes(1); - pollingSyncManager.stop(); + syncManager.stop(); // @ts-ignore // Test pollingManager for shared client - const pollingSyncManagerShared = pollingSyncManager.shared('sharedKey', readinessManagerMock, storageManagerMock); + const pollingSyncManagerShared = syncManager.shared('sharedKey', readinessManagerMock, storageManagerMock); if (!pollingSyncManagerShared) throw new Error('pollingSyncManagerShared should exist'); @@ -142,26 +135,22 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => pollingSyncManagerShared.stop(); - // @ts-ignore - // Test pushManager for main client - const pushingSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, () => pushManagerMock)({ settings }); - - pushingSyncManager.start(); + syncManager.start(); expect(pushManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.start).not.toBeCalled(); - pushingSyncManager.stop(); - pushingSyncManager.start(); + syncManager.stop(); + syncManager.start(); expect(pushManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.start).not.toBeCalled(); - pushingSyncManager.stop(); + syncManager.stop(); // @ts-ignore // Test pollingManager for shared client - const pushingSyncManagerShared = pushingSyncManager.shared('pushingSharedKey', readinessManagerMock, storageManagerMock); + const pushingSyncManagerShared = syncManager.shared('pushingSharedKey', readinessManagerMock, storageManagerMock); if (!pushingSyncManagerShared) throw new Error('pushingSyncManagerShared should exist'); From 08f55f7ae6743bd6841692602397b8e0c28940a0 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 15 Jun 2022 16:44:08 -0300 Subject: [PATCH 11/19] Use pushManagerFactoryMock and pushManagerMock --- src/sync/__tests__/syncManagerOnline.spec.ts | 38 ++++++-------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 434ce24c..d60156ab 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -1,9 +1,6 @@ import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { syncTaskFactory } from './syncTask.mock'; import { syncManagerOnlineFactory } from '../syncManagerOnline'; -import { pushManagerFactory } from '../streaming/pushManager'; -import { IPushManager } from '../streaming/types'; -import { EventEmitter } from '../../utils/MinEvents'; import { IReadinessManager } from '../../readiness/types'; jest.mock('../submitters/submitterManager', () => { @@ -12,17 +9,6 @@ jest.mock('../submitters/submitterManager', () => { }; }); -const paramsMock = { - platform: { - getEventSource: jest.fn(() => { return () => { }; }), - EventEmitter - }, - settings: fullSettings, - storage: {}, - readiness: {}, - start: jest.fn() -}; - // Mocked storageManager const storageManagerMock = { splits: { @@ -47,12 +33,14 @@ const pollingManagerMock = { get: jest.fn() }; -// Mocked pushManager -const pushManagerMock = pushManagerFactory({ // @ts-ignore - ...paramsMock, splitApi: { fetchAuth: jest.fn() } -}, {}) as IPushManager; +const pushManagerMock = { + start: jest.fn(), + on: jest.fn(), + stop: jest.fn() +}; -pushManagerMock.start = 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 }; @@ -94,9 +82,9 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => // @ts-ignore // Test pushManager for main client - const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, () => pushManagerMock)({ settings }); + const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); - expect(syncManager.pushManager).toBeUndefined(); + expect(pushManagerFactoryMock).not.toBeCalled(); // Test pollingManager for Main client syncManager.start(); @@ -137,13 +125,11 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => syncManager.start(); - expect(pushManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.start).not.toBeCalled(); syncManager.stop(); syncManager.start(); - expect(pushManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.start).not.toBeCalled(); syncManager.stop(); @@ -156,13 +142,11 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => pushingSyncManagerShared.start(); - expect(pushManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.start).not.toBeCalled(); pushingSyncManagerShared.stop(); pushingSyncManagerShared.start(); - expect(pushManagerMock.start).not.toBeCalled(); expect(pollingManagerMock.start).not.toBeCalled(); pushingSyncManagerShared.stop(); @@ -170,9 +154,9 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => settings.sync.singleSync = false; // @ts-ignore // pushManager instantiation control test - const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, () => pushManagerMock)({ settings }); + const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); - expect(testSyncManager.pushManager).not.toBeUndefined(); + expect(pushManagerFactoryMock).toBeCalled(); // Test pollingManager for Main client testSyncManager.start(); From 164270ae43fa8624233d65f90247b5e9380cc8f7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 15 Jun 2022 17:28:33 -0300 Subject: [PATCH 12/19] updated some comments --- src/sync/submitters/submitterManager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 7ec32bdb..298f61a4 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -17,22 +17,29 @@ export function submitterManagerFactory(params: ISdkFactoryContextSync): ISubmit const telemetrySubmitter = telemetrySubmitterFactory(params); return { + // `onlyTelemetry` true if SDK is created with userConsent not GRANTED start(onlyTelemetry?: boolean) { if (!onlyTelemetry) submitters.forEach(submitter => submitter.start()); if (telemetrySubmitter) telemetrySubmitter.start(); }, + + // `allExceptTelemetry` true if userConsent is changed to DECLINED stop(allExceptTelemetry?: boolean) { submitters.forEach(submitter => submitter.stop()); if (!allExceptTelemetry && telemetrySubmitter) telemetrySubmitter.stop(); }, + isRunning() { return submitters.some(submitter => submitter.isRunning()); }, + + // Flush data. Called with `onlyTelemetry` true if SDK is destroyed with userConsent not GRANTED execute(onlyTelemetry?: boolean) { const promises = onlyTelemetry ? [] : submitters.map(submitter => submitter.execute()); if (telemetrySubmitter) promises.push(telemetrySubmitter.execute()); return Promise.all(promises); }, + isExecuting() { return submitters.some(submitter => submitter.isExecuting()); } From 75a0a151b56dd4cc4764f8bf97449aaf9eb5f7b2 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 22 Jun 2022 10:53:59 -0300 Subject: [PATCH 13/19] Update config name from sync.singleSync to sinc.enabled --- CHANGES.txt | 3 +++ package-lock.json | 4 ++-- package.json | 2 +- src/sync/__tests__/syncManagerOnline.spec.ts | 8 ++++---- src/sync/syncManagerOnline.ts | 10 +++++----- src/types.ts | 8 ++++---- .../settingsValidation/__tests__/index.spec.ts | 15 ++++++++------- .../__tests__/settings.mocks.ts | 2 +- src/utils/settingsValidation/index.ts | 8 ++++---- 9 files changed, 32 insertions(+), 28 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3d681b60..ec986a64 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +1.4.2 + - Added `sync.enabled` property to SDK configuration to allow synchronize splits and MySegments once + 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..9f91c91a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.1", + "version": "1.4.2-rc.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..c28a1923 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.1", + "version": "1.4.2-rc.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index d60156ab..ac0d81bf 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -74,11 +74,11 @@ test('syncManagerOnline should start or not the submitter depending on user cons }); -test('syncManagerOnline should syncAll a single time in singleSync mode', () => { +test('syncManagerOnline should syncAll a single time when sync is disabled', () => { const settings = { ...fullSettings }; - // Enable single sync - settings.sync.singleSync = true; + // disable sync + settings.sync.enabled = false; // @ts-ignore // Test pushManager for main client @@ -151,7 +151,7 @@ test('syncManagerOnline should syncAll a single time in singleSync mode', () => pushingSyncManagerShared.stop(); - settings.sync.singleSync = false; + settings.sync.enabled = true; // @ts-ignore // pushManager instantiation control test const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings }); diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index f51bcce3..5400fad8 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, sync: { singleSync } }, telemetryTracker } = params; + const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); /** Push Manager */ - const pushManager = !singleSync && streamingEnabled && pollingManager && pushManagerFactory ? + const pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ? pushManagerFactory(params, pollingManager) : undefined; @@ -90,8 +90,8 @@ export function syncManagerOnlineFactory( // start syncing splits and segments if (pollingManager) { - // If singleSync is enabled pushManager and pollingManager should not start - if (singleSync === true) { + // If synchronization is disabled pushManager and pollingManager should not start + if (syncEnabled === false) { if (startFirstTime) { pollingManager.syncAll(); startFirstTime = false; @@ -147,7 +147,7 @@ export function syncManagerOnlineFactory( return { isRunning: mySegmentsSyncTask.isRunning, start() { - if (singleSync === true) { + if (syncEnabled === false) { if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); } else { if (pushManager) { diff --git a/src/types.ts b/src/types.ts index a8d7b45c..a45c27a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -118,7 +118,7 @@ export interface ISettings { impressionsMode: SplitIO.ImpressionsMode, __splitFiltersValidation: ISplitFiltersValidation, localhostMode?: SplitIO.LocalhostFactory, - singleSync: boolean + enabled: boolean }, readonly runtime: { ip: string | false @@ -216,10 +216,10 @@ interface ISharedSettings { */ impressionsMode?: SplitIO.ImpressionsMode, /** - * single Sync enables. - * @property {boolean} singleSync + * Enables synchronization. + * @property {boolean} enabled */ - singleSync: boolean + enabled: boolean } } /** diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 8b53f43f..2b3f4918 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -40,7 +40,7 @@ describe('settingsValidation', () => { telemetry: 'https://telemetry.split.io/api', }); expect(settings.sync.impressionsMode).toBe(OPTIMIZED); - expect(settings.sync.singleSync).toBe(false); + expect(settings.sync.enabled).toBe(true); }); test('override with default impressionMode if provided one is invalid', () => { @@ -164,23 +164,24 @@ describe('settingsValidation', () => { expect(settingsWithStreamingEnabled.streamingEnabled).toBe(true); // If streamingEnabled is not provided, it will be true. }); - test('singleSync should be overwritable and false by default', () => { - const settingsWithSingleSyncDisabled = settingsValidation({ + test('sync.enabled should be overwritable and true by default', () => { + const settingsWithSyncEnabled = settingsValidation({ core: { authorizationKey: 'dummy token', } }, minimalSettingsParams); - const settingsWithSingleSyncEnabled = settingsValidation({ + + const settingsWithSyncDisabled = settingsValidation({ core: { authorizationKey: 'dummy token' }, sync: { - singleSync: true + enabled: false } }, minimalSettingsParams); - expect(settingsWithSingleSyncDisabled.sync.singleSync).toBe(false); // If singleSync is not provided, it will be true. - expect(settingsWithSingleSyncEnabled.sync.singleSync).toBe(true); // When creating a setting instance, it will have the provided value for singleSync + 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 }); diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 5e79e487..8d213b03 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -79,7 +79,7 @@ export const fullSettings: ISettings = { queryString: null, groupedFilters: { byName: [], byPrefix: [] } }, - singleSync: false + enabled: true }, version: 'jest', runtime: { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 277063e8..84d97954 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -84,7 +84,7 @@ export const base = { // impressions collection mode impressionsMode: OPTIMIZED, localhostMode: undefined, - singleSync: false + enabled: true }, // Logger @@ -192,9 +192,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase); } - // validate singleSync - if (withDefaults.sync.singleSync !== true) { // @ts-ignore, modify readonly prop - withDefaults.sync.singleSync = false; + // 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 From 25d369af8c7e507c925625126701fdcdcfe86907 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Jun 2022 11:08:11 -0300 Subject: [PATCH 14/19] update changelog --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3d681b60..75c777bf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +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.4.1 (June 13, 2022) - Bugfixing - Updated submitters logic, to avoid dropping impressions and events that are being tracked while POST request is pending. From 5c659128ea37d2c93bd48231b9171a7857f7db89 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 22 Jun 2022 12:41:03 -0300 Subject: [PATCH 15/19] Improve sync enabled condition and update changes file --- CHANGES.txt | 4 ++-- src/sync/syncManagerOnline.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ec986a64..dfd38418 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -1.4.2 - - Added `sync.enabled` property to SDK configuration to allow synchronize splits and MySegments once +1.4.2 (June XX, 2022) + - Added `sync.enabled` property to SDK configuration to allow synchronize splits and segments once 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/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 5400fad8..8ac9fc31 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -91,12 +91,7 @@ export function syncManagerOnlineFactory( if (pollingManager) { // If synchronization is disabled pushManager and pollingManager should not start - if (syncEnabled === false) { - if (startFirstTime) { - pollingManager.syncAll(); - startFirstTime = false; - } - } else { + if (syncEnabled) { if (pushManager) { // Doesn't call `syncAll` when the syncManager is resuming if (startFirstTime) { @@ -107,6 +102,11 @@ export function syncManagerOnlineFactory( } else { pollingManager.start(); } + } else { + if (startFirstTime) { + pollingManager.syncAll(); + startFirstTime = false; + } } } @@ -147,9 +147,7 @@ export function syncManagerOnlineFactory( return { isRunning: mySegmentsSyncTask.isRunning, start() { - if (syncEnabled === false) { - if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); - } else { + if (syncEnabled) { if (pushManager) { if (pollingManager!.isRunning()) { // if doing polling, we must start the periodic fetch of data @@ -163,6 +161,8 @@ export function syncManagerOnlineFactory( } else { if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); } + } else { + if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); } }, stop() { From ebe7bbc0edc8d8e17aeb4b8ff8b485038518d5d0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Jun 2022 13:28:12 -0300 Subject: [PATCH 16/19] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f91c91a..d6aa7e29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.2-rc.0", + "version": "1.4.2-rc.1", "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": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, "lodash.isarguments": { diff --git a/package.json b/package.json index c28a1923..3a036085 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.2-rc.0", + "version": "1.4.2-rc.1", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 4f0946f54f23ef117e90d9252186a0481859b10d Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 22 Jun 2022 19:57:19 -0300 Subject: [PATCH 17/19] Update changelog --- CHANGES.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a24222cf..dda80a0e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,7 @@ -1.4.2 (June XX, 2022) - - Added `sync.enabled` property to SDK configuration to allow synchronize splits and segments only once. - - 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 24, 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. From 3ae815097fcefee9185712f81e8a3d621982d8d3 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 24 Jun 2022 17:44:46 -0300 Subject: [PATCH 18/19] update rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6aa7e29..d46df41e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.2-rc.1", + "version": "1.4.2-rc.3", "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 3a036085..7a184ff3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.2-rc.1", + "version": "1.4.2-rc.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From c56fc7184126c14c82e464744826978edde64eef Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 29 Jun 2022 11:40:08 -0300 Subject: [PATCH 19/19] Prepare stable release v1.5.0 --- CHANGES.txt | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index dda80a0e..aa1f7b44 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.5.0 (June 24, 2022) +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. diff --git a/package-lock.json b/package-lock.json index d46df41e..2c6c6e5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.2-rc.3", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7a184ff3..f7fb2df2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.2-rc.3", + "version": "1.5.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js",