From ae5bc031592924276fa136c0d78b22252faa4a0d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 May 2022 16:38:04 -0300 Subject: [PATCH 1/5] implementation --- src/storages/inLocalStorage/index.ts | 4 ++-- src/storages/inMemory/InMemoryStorage.ts | 2 +- src/storages/inMemory/InMemoryStorageCS.ts | 4 ++-- .../inMemory/TelemetryCacheInMemory.ts | 9 +++++++++ src/storages/types.ts | 18 +++++++++--------- src/sync/submitters/submitterManager.ts | 4 ++-- src/sync/submitters/telemetrySubmitter.ts | 8 +++++--- 7 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index eedb14a9..a9fd3f68 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -13,7 +13,7 @@ import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browse import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; -import { TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; +import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; export interface InLocalStorageOptions { prefix?: string @@ -44,7 +44,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), - telemetry: new TelemetryCacheInMemory(), + telemetry: shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined, destroy() { this.splits = new SplitsCacheInMemory(); diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index ced292fc..55e86870 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -20,7 +20,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), - telemetry: new TelemetryCacheInMemory(), + telemetry: new TelemetryCacheInMemory(), // Always track telemetry in server-side // When using MEMORY we should clean all the caches to leave them empty destroy() { diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index 15e57fb6..9733de7b 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -5,7 +5,7 @@ import { EventsCacheInMemory } from './EventsCacheInMemory'; import { IStorageSync, IStorageFactoryParams } from '../types'; import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; import { STORAGE_MEMORY } from '../../utils/constants'; -import { TelemetryCacheInMemory } from './TelemetryCacheInMemory'; +import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory'; /** * InMemory storage factory for standalone client-side SplitFactory @@ -20,7 +20,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), - telemetry: new TelemetryCacheInMemory(), + telemetry: shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined, // When using MEMORY we should clean all the caches to leave them empty destroy() { diff --git a/src/storages/inMemory/TelemetryCacheInMemory.ts b/src/storages/inMemory/TelemetryCacheInMemory.ts index 5c84a20c..844cd043 100644 --- a/src/storages/inMemory/TelemetryCacheInMemory.ts +++ b/src/storages/inMemory/TelemetryCacheInMemory.ts @@ -10,6 +10,15 @@ function newBuckets() { return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } +const ACCEPTANCE_RANGE = 0.001; + +/** + * Used on client-side. 0.1% of instances will track telemetry + */ +export function shouldRecordTelemetry() { + return Math.random() <= ACCEPTANCE_RANGE; +} + export class TelemetryCacheInMemory implements TelemetryCacheSync { private timeUntilReady?: number; diff --git a/src/storages/types.ts b/src/storages/types.ts index 4c9739e0..4dfdd748 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -445,13 +445,15 @@ export interface IStorageBase< TSplitsCache extends ISplitsCacheBase, TSegmentsCache extends ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase, - TEventsCache extends IEventsCacheBase + TEventsCache extends IEventsCacheBase, + TTelemetryCache extends TelemetryCacheSync | TelemetryCacheAsync > { splits: TSplitsCache, segments: TSegmentsCache, impressions: TImpressionsCache, impressionCounts?: IImpressionCountsCacheSync, events: TEventsCache, + telemetry?: TTelemetryCache destroy(): void | Promise, shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this } @@ -460,19 +462,17 @@ export interface IStorageSync extends IStorageBase< ISplitsCacheSync, ISegmentsCacheSync, IImpressionsCacheSync, - IEventsCacheSync - > { - telemetry: TelemetryCacheSync -} + IEventsCacheSync, + TelemetryCacheSync + > { } export interface IStorageAsync extends IStorageBase< ISplitsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, - IEventsCacheAsync | IEventsCacheSync -> { - telemetry?: TelemetryCacheAsync -} + IEventsCacheAsync | IEventsCacheSync, + TelemetryCacheAsync + > { } /** StorageFactory */ diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 35a4ce1d..fd3cab7b 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -3,7 +3,7 @@ import { eventsSubmitterFactory } from './eventsSubmitter'; import { impressionsSubmitterFactory } from './impressionsSubmitter'; import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter'; import { ISyncManagerFactoryParams } from '../types'; -import { telemetrySubmitterFactory } from './telemetrySubmitter'; +import { ISyncManagerFactoryParamsWithTelemetry, telemetrySubmitterFactory } from './telemetrySubmitter'; export function submitterManagerFactory(params: ISyncManagerFactoryParams) { @@ -14,6 +14,6 @@ export function submitterManagerFactory(params: ISyncManagerFactoryParams) { eventsSubmitterFactory(log, splitApi.postEventsBulk, storage.events, settings.scheduler.eventsPushRate, settings.startup.eventsFirstPushWindow) ]; if (storage.impressionCounts) submitters.push(impressionCountsSubmitterFactory(log, splitApi.postTestImpressionsCount, storage.impressionCounts)); - if (storage.telemetry) submitters.push(telemetrySubmitterFactory(params)); + if (storage.telemetry) submitters.push(telemetrySubmitterFactory(params as ISyncManagerFactoryParamsWithTelemetry)); return syncTaskComposite(submitters); } diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index b5d7ebca..4267b7c1 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -1,4 +1,4 @@ -import { IStorageSync, TelemetryCacheSync } from '../../storages/types'; +import { TelemetryCacheSync } from '../../storages/types'; import { submitterFactory, firstPushWindowDecorator } from './submitter'; import { TelemetryUsageStatsPayload, TelemetryConfigStatsPayload } from './types'; import { QUEUED, DEDUPED, DROPPED, CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, DEBUG_ENUM, OPTIMIZED_ENUM } from '../../utils/constants'; @@ -9,10 +9,12 @@ import { usedKeysMap } from '../../utils/inputValidation/apiKey'; import { ISyncManagerFactoryParams } from '../types'; import { timer } from '../../utils/timeTracker/timer'; +export type ISyncManagerFactoryParamsWithTelemetry = ISyncManagerFactoryParams & { storage: { telemetry: TelemetryCacheSync } } + /** * Converts data from telemetry cache into /metrics/usage request payload. */ -export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: IStorageSync) { +export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: ISyncManagerFactoryParamsWithTelemetry['storage']) { return { isEmpty() { return false; }, // There is always data in telemetry cache clear() { }, // No-op @@ -117,7 +119,7 @@ export function telemetryCacheConfigAdapter(settings: ISettings, telemetryCache: /** * Submitter that periodically posts telemetry data */ -export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { +export function telemetrySubmitterFactory(params: ISyncManagerFactoryParamsWithTelemetry) { const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, storage, splitApi, platform: { now }, readiness } = params; const startTime = timer(now || Date.now); From d9ac8e405aa399b5169958df4afa92b3579312dc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 May 2022 18:04:18 -0300 Subject: [PATCH 2/5] no track telemetry in localhost mode --- src/storages/inLocalStorage/index.ts | 4 ++-- src/storages/inMemory/InMemoryStorage.ts | 4 ++-- src/storages/inMemory/InMemoryStorageCS.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index a9fd3f68..850d495f 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -12,7 +12,7 @@ import { SplitsCacheInMemory } from '../inMemory/SplitsCacheInMemory'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; -import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; +import { LOCALHOST_MODE, STORAGE_LOCALSTORAGE } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; export interface InLocalStorageOptions { @@ -44,7 +44,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), - telemetry: shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined, + telemetry: params.mode !== LOCALHOST_MODE && shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined, destroy() { this.splits = new SplitsCacheInMemory(); diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 55e86870..9064e9fe 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -4,7 +4,7 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory'; import { EventsCacheInMemory } from './EventsCacheInMemory'; import { IStorageFactoryParams, IStorageSync } from '../types'; import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; -import { STORAGE_MEMORY } from '../../utils/constants'; +import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; import { TelemetryCacheInMemory } from './TelemetryCacheInMemory'; /** @@ -20,7 +20,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), - telemetry: new TelemetryCacheInMemory(), // Always track telemetry in server-side + telemetry: params.mode !== LOCALHOST_MODE ? new TelemetryCacheInMemory() : undefined, // Always track telemetry in server-side // When using MEMORY we should clean all the caches to leave them empty destroy() { diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index 9733de7b..8f1d7d3b 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -4,7 +4,7 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory'; import { EventsCacheInMemory } from './EventsCacheInMemory'; import { IStorageSync, IStorageFactoryParams } from '../types'; import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; -import { STORAGE_MEMORY } from '../../utils/constants'; +import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory'; /** @@ -20,7 +20,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), - telemetry: shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined, + telemetry: params.mode !== LOCALHOST_MODE && shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined, // When using MEMORY we should clean all the caches to leave them empty destroy() { From 381e21cad0b1d5ecb9f90fc4e0c2c12fcf94bcf9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 14:23:19 -0300 Subject: [PATCH 3/5] fix interface --- src/sdkFactory/index.ts | 2 +- src/sdkFactory/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index ce77472a..40f5c45e 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -70,7 +70,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); // splitApi is used by SyncManager and Browser signal listener - const splitApi = splitApiFactory && splitApiFactory(settings, platform); + const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker); const syncManager = syncManagerFactory && syncManagerFactory({ settings, diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 8781ae0c..94223532 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -49,7 +49,7 @@ export interface ISdkFactoryParams { // Factory of Split Api (HTTP Client Service). // It is not required when providing an asynchronous storage or offline SyncManager - splitApiFactory?: (settings: ISettings, platform: IPlatform) => ISplitApi, + splitApiFactory?: (settings: ISettings, platform: IPlatform, telemetryTracker: ITelemetryTracker) => ISplitApi, // SyncManager factory. // Not required when providing an asynchronous storage (consumer mode), but required in standalone mode to avoid SDK timeout. From 23ff6072bea583efd969a0ad1fdf8b5beb26c406 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 16:40:57 -0300 Subject: [PATCH 4/5] fix issue with checkAllSegmentsExist in client-side --- src/sync/polling/pollingManagerCS.ts | 2 +- src/sync/polling/syncTasks/splitsSyncTask.ts | 2 ++ src/sync/polling/updaters/splitChangesUpdater.ts | 3 ++- src/trackers/telemetryTracker.ts | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 364829a5..05f7f793 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -20,7 +20,7 @@ export function pollingManagerCSFactory( const { splitApi, storage, readiness, settings } = params; const log = settings.log; - const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); + const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings, true); // Map of matching keys to their corresponding MySegmentsSyncTask. const mySegmentsSyncTasks: Record = {}; diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index 6807a0b4..a4568d6d 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -15,6 +15,7 @@ export function splitsSyncTaskFactory( storage: IStorageSync, readiness: IReadinessManager, settings: ISettings, + isClientSide?: boolean ): ISplitsSyncTask { return syncTaskFactory( settings.log, @@ -26,6 +27,7 @@ export function splitsSyncTaskFactory( readiness.splits, settings.startup.requestTimeoutBeforeReady, settings.startup.retriesOnFailureBeforeReady, + isClientSide ), settings.scheduler.featuresRefreshRate, 'splitChangesUpdater', diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 12590ed5..aa2ecabc 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -93,6 +93,7 @@ export function splitChangesUpdaterFactory( splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, + isClientSide?: boolean ): ISplitChangesUpdater { let startingUp = true; @@ -140,7 +141,7 @@ export function splitChangesUpdaterFactory( if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && checkAllSegmentsExist(segments))) + return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && (isClientSide || checkAllSegmentsExist(segments)))) .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index 7cd266cf..aa0a6e4c 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -22,7 +22,7 @@ export function telemetryTrackerFactory( telemetryCache.recordException(method); return; // Don't track latency on exceptions case SDK_NOT_READY: // @ts-ignore ITelemetryCacheAsync doesn't implement the method - telemetryCache?.recordNonReadyUsage(); + if (telemetryCache.recordNonReadyUsage) telemetryCache.recordNonReadyUsage(); } telemetryCache.recordLatency(method, evalTime()); }; @@ -36,8 +36,8 @@ export function telemetryTrackerFactory( else (telemetryCache as ITelemetryCacheSync).recordSuccessfulSync(operation, now()); }; }, - sessionLength() { - (telemetryCache as ITelemetryCacheSync).recordSessionLength(startTime()); + sessionLength() { // @ts-ignore ITelemetryCacheAsync doesn't implement the method + if (telemetryCache.recordSessionLength) telemetryCache.recordSessionLength(startTime()); }, streamingEvent(e, d) { if (e === AUTH_REJECTION) { From 83b21212edfbc6b5be21c42b9a69af2d73200f21 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 16:52:51 -0300 Subject: [PATCH 5/5] fix unit test --- src/sdkFactory/__tests__/index.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sdkFactory/__tests__/index.spec.ts b/src/sdkFactory/__tests__/index.spec.ts index 58a1144f..185fccb4 100644 --- a/src/sdkFactory/__tests__/index.spec.ts +++ b/src/sdkFactory/__tests__/index.spec.ts @@ -19,6 +19,12 @@ jest.mock('../../logger/sdkLogger', () => { createLoggerAPI: () => loggerApiMock }; }); +const telemetryTrackerMock = 'telemetryTracker'; +jest.mock('../../trackers/telemetryTracker', () => { + return { + telemetryTrackerFactory: () => telemetryTrackerMock + }; +}); // IAsyncSDK, minimal params const paramsForAsyncSDK = { @@ -74,7 +80,7 @@ function assertModulesCalled(params: any) { expect(SignalListenerInstanceMock.start).toBeCalledTimes(1); } if (params.splitApiFactory) { - expect(params.splitApiFactory.mock.calls).toEqual([[params.settings, params.platform]]); + expect(params.splitApiFactory.mock.calls).toEqual([[params.settings, params.platform, telemetryTrackerMock]]); } if (params.integrationsManagerFactory) { expect(params.integrationsManagerFactory.mock.calls).toEqual([[{ settings: params.settings, storage: mockStorage }]]);