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 }]]); 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. diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index eedb14a9..850d495f 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -12,8 +12,8 @@ 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 { TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; +import { LOCALHOST_MODE, STORAGE_LOCALSTORAGE } from '../../utils/constants'; +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: 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 ced292fc..d5acfb6a 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(), + telemetry: params.mode !== LOCALHOST_MODE ? new TelemetryCacheInMemory() : undefined, // Always track telemetry in standalone mode on 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..8f1d7d3b 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -4,8 +4,8 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory'; import { EventsCacheInMemory } from './EventsCacheInMemory'; import { IStorageSync, IStorageFactoryParams } from '../types'; import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; -import { STORAGE_MEMORY } from '../../utils/constants'; -import { TelemetryCacheInMemory } from './TelemetryCacheInMemory'; +import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; +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: params.mode !== LOCALHOST_MODE && 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 beb684c1..6a56d26b 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 ITelemetryCacheSync { private timeUntilReady?: number; 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) {