From e6064ece1b29033539f524afecdea6b83327d0ff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Jan 2022 12:41:06 -0300 Subject: [PATCH 01/70] remove old telemetry modules --- src/storages/inMemory/CountsCacheInMemory.ts | 37 -------------- .../inMemory/LatenciesCacheInMemory.ts | 45 ----------------- src/storages/inRedis/CountsCacheInRedis.ts | 20 -------- src/storages/inRedis/LatenciesCacheInRedis.ts | 23 --------- src/sync/submitters/metricsSyncTask.ts | 49 ------------------- 5 files changed, 174 deletions(-) delete mode 100644 src/storages/inMemory/CountsCacheInMemory.ts delete mode 100644 src/storages/inMemory/LatenciesCacheInMemory.ts delete mode 100644 src/storages/inRedis/CountsCacheInRedis.ts delete mode 100644 src/storages/inRedis/LatenciesCacheInRedis.ts delete mode 100644 src/sync/submitters/metricsSyncTask.ts diff --git a/src/storages/inMemory/CountsCacheInMemory.ts b/src/storages/inMemory/CountsCacheInMemory.ts deleted file mode 100644 index ff7cd591..00000000 --- a/src/storages/inMemory/CountsCacheInMemory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ICountsCacheSync } from '../types'; - -export class CountsCacheInMemory implements ICountsCacheSync { - - private counters: Record = {}; - - /** - * Add counts. - */ - track(metricName: string) { - if (this.counters[metricName] === undefined) this.counters[metricName] = 1; - else this.counters[metricName]++; - - return true; - } - - /** - * Clear the collector - */ - clear() { - this.counters = {}; - } - - /** - * Get the collected data, used as payload for posting. - */ - state() { - return this.counters; - } - - /** - * Check if the cache is empty. - */ - isEmpty() { - return Object.keys(this.counters).length === 0; - } -} diff --git a/src/storages/inMemory/LatenciesCacheInMemory.ts b/src/storages/inMemory/LatenciesCacheInMemory.ts deleted file mode 100644 index 240b5a8b..00000000 --- a/src/storages/inMemory/LatenciesCacheInMemory.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ILatenciesCacheSync } from '../types'; -import { findLatencyIndex } from '../findLatencyIndex'; - -export class LatenciesCacheInMemory implements ILatenciesCacheSync { - - private counters: Record = {}; - - /** - * Add latencies. - */ - track(metricName: string, latency: number) { - // Initialize if needed - if (this.counters[metricName] === undefined) { - this.counters[metricName] = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; - } - - // +1 based on the latency number - this.counters[metricName][findLatencyIndex(latency)]++; - - return true; - } - - /** - * Clear the collector - */ - clear() { - this.counters = {}; - } - - /** - * Get the collected data, used as payload for posting. - */ - state() { - return this.counters; - } - - /** - * Check if the cache is empty. - */ - isEmpty() { - return Object.keys(this.counters).length === 0; - } -} diff --git a/src/storages/inRedis/CountsCacheInRedis.ts b/src/storages/inRedis/CountsCacheInRedis.ts deleted file mode 100644 index cb551fa2..00000000 --- a/src/storages/inRedis/CountsCacheInRedis.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ICountsCacheAsync } from '../types'; -import { KeyBuilderSS } from '../KeyBuilderSS'; -import { Redis } from 'ioredis'; - -export class CountsCacheInRedis implements ICountsCacheAsync { - - private readonly redis: Redis; - private readonly keys: KeyBuilderSS; - - constructor(keys: KeyBuilderSS, redis: Redis) { - this.keys = keys; - this.redis = redis; - } - - track(metricName: string): Promise { - return this.redis.incr(this.keys.buildCountKey(metricName)).catch(() => { - // noop, for telemetry metrics there's no need to throw. - }).then(() => true); - } -} diff --git a/src/storages/inRedis/LatenciesCacheInRedis.ts b/src/storages/inRedis/LatenciesCacheInRedis.ts deleted file mode 100644 index f69b7244..00000000 --- a/src/storages/inRedis/LatenciesCacheInRedis.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ILatenciesCacheAsync } from '../types'; -import { KeyBuilderSS } from '../KeyBuilderSS'; -import { findLatencyIndex } from '../findLatencyIndex'; -import { Redis } from 'ioredis'; - -export class LatenciesCacheInRedis implements ILatenciesCacheAsync { - - private readonly redis: Redis; - private readonly keys: KeyBuilderSS; - - constructor(keys: KeyBuilderSS, redis: Redis) { - this.keys = keys; - this.redis = redis; - } - - track(metricName: string, latency: number): Promise { - const bucketNumber = findLatencyIndex(latency); - - return this.redis.incr(this.keys.buildLatencyKey(metricName, bucketNumber)).catch(() => { - // noop, for telemetry metrics there's no need to throw. - }).then(() => true); - } -} diff --git a/src/sync/submitters/metricsSyncTask.ts b/src/sync/submitters/metricsSyncTask.ts deleted file mode 100644 index a5264b14..00000000 --- a/src/sync/submitters/metricsSyncTask.ts +++ /dev/null @@ -1,49 +0,0 @@ - -import { forOwn } from '../../utils/lang'; -import { ICountsCacheSync, ILatenciesCacheSync } from '../../storages/types'; -import { IPostMetricsCounters, IPostMetricsTimes } from '../../services/types'; -import { ISyncTask, ITimeTracker } from '../types'; -import { submitterSyncTaskFactory } from './submitterSyncTask'; -import { ILogger } from '../../logger/types'; - -// extract POST payload object from cache -function fromCache(propertyName: 'latencies' | 'delta') { - return (data: Record): any[] => { - const result: any[] = []; - - forOwn(data, (value, key) => { - result.push({ name: key, [propertyName]: value }); - }); - - return result; - }; -} - -/** - * Sync task that periodically posts telemetry counts - */ -export function countsSyncTaskFactory( - log: ILogger, - postMetricsCounters: IPostMetricsCounters, - countsCache: ICountsCacheSync, - metricsRefreshRate: number, - latencyTracker?: ITimeTracker -): ISyncTask { - - return submitterSyncTaskFactory(log, postMetricsCounters, countsCache, metricsRefreshRate, 'count metrics', latencyTracker, fromCache('delta')); -} - -/** - * Sync task that periodically posts telemetry latencies - */ -export function latenciesSyncTaskFactory( - log: ILogger, - postMetricsLatencies: IPostMetricsTimes, - latenciesCache: ILatenciesCacheSync, - metricsRefreshRate: number, - latencyTracker?: ITimeTracker -): ISyncTask { - - // don't retry metrics. - return submitterSyncTaskFactory(log, postMetricsLatencies, latenciesCache, metricsRefreshRate, 'latency metrics', latencyTracker, fromCache('latencies'), 0, true); -} From a8413da67d2908705c2e959f97dfeb78c2c23cda Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Jan 2022 11:21:04 -0300 Subject: [PATCH 02/70] remove old telemetry cache tests --- .../__tests__/CountsCacheInMemory.spec.ts | 14 ------- .../__tests__/LatenciesCacheInMemory.spec.ts | 34 ---------------- .../__tests__/CountsCacheInRedis.spec.ts | 35 ----------------- .../__tests__/LatenciesCacheInRedis.spec.ts | 39 ------------------- src/storages/inRedis/index.ts | 5 +-- 5 files changed, 1 insertion(+), 126 deletions(-) delete mode 100644 src/storages/inMemory/__tests__/CountsCacheInMemory.spec.ts delete mode 100644 src/storages/inMemory/__tests__/LatenciesCacheInMemory.spec.ts delete mode 100644 src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts delete mode 100644 src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts diff --git a/src/storages/inMemory/__tests__/CountsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/CountsCacheInMemory.spec.ts deleted file mode 100644 index bb8ebcc0..00000000 --- a/src/storages/inMemory/__tests__/CountsCacheInMemory.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CountsCacheInMemory } from '../CountsCacheInMemory'; - -test('COUNT CACHE IN MEMORY / should count metric names incrementatly', () => { - const cache = new CountsCacheInMemory(); - - cache.track('counted-metric-one'); - cache.track('counted-metric-one'); - cache.track('counted-metric-two'); - - const state = cache.state(); - - expect(state['counted-metric-one']).toBe(2); - expect(state['counted-metric-two']).toBe(1); -}); diff --git a/src/storages/inMemory/__tests__/LatenciesCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/LatenciesCacheInMemory.spec.ts deleted file mode 100644 index 18ad0a3a..00000000 --- a/src/storages/inMemory/__tests__/LatenciesCacheInMemory.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { LatenciesCacheInMemory } from '../LatenciesCacheInMemory'; - -test('METRICS (LATENCIES) CACHE IN MEMORY / should count based on ranges', () => { - const metricName = 'testing'; - const c1 = new LatenciesCacheInMemory(); - - c1.track(metricName, 1); - c1.track(metricName, 1.2); - c1.track(metricName, 1.4); - - expect(c1.state()[metricName][0] === 3).toBe(true); // the bucket #0 should have 3 - - c1.track(metricName, 1.5); - expect(c1.state()[metricName][1] === 1).toBe(true); // the bucket #1 should have 1 - - c1.track(metricName, 2.25); - c1.track(metricName, 2.26); - c1.track(metricName, 2.265); - expect(c1.state()[metricName][2] === 3).toBe(true); // the bucket #3 should have 1 - - c1.track(metricName, 985251); - expect(c1.state()[metricName][23] === 1).toBe(true); // the bucket #23 should have 1 - -}); - -test('METRICS (LATENCIES) CACHE IN MEMORY / clear', () => { - const metricName = 'testing'; - const c1 = new LatenciesCacheInMemory(); - - c1.track(metricName, 1); - c1.track(metricName, 1000); - c1.clear(); - expect(c1.isEmpty()).toBe(true); // after call clear, the cache should be empty -}); diff --git a/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts deleted file mode 100644 index b8032ac2..00000000 --- a/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Redis from 'ioredis'; -import { KeyBuilderSS } from '../../KeyBuilderSS'; -import { CountsCacheInRedis } from '../CountsCacheInRedis'; - -const prefix = 'counts_cache_ut'; -const metadata = { s: 'version', i: 'ip', n: 'hostname' }; - -test('COUNTS CACHE IN REDIS / cover basic behavior', async () => { - const connection = new Redis({}); - const keys = new KeyBuilderSS(prefix, metadata); - const cache = new CountsCacheInRedis(keys, connection); - - await cache.track('counted-metric-one'); - await cache.track('counted-metric-one'); - - const keyOne = keys.buildCountKey('counted-metric-one'); - const keyTwo = keys.buildCountKey('counted-metric-two'); - - let metricOneValue = await connection.get(keyOne); - expect(metricOneValue).toBe('2'); - - await cache.track('counted-metric-two'); - - metricOneValue = await connection.get(keyOne); - let metricTwoValue = await connection.get(keyTwo); - - expect(metricOneValue).toBe('2'); - expect(metricTwoValue).toBe('1'); - - // Clean up - const keysToClean = await connection.keys(`${prefix}*`); - if (keysToClean.length) await connection.del(keysToClean); - - await connection.quit(); -}); diff --git a/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts deleted file mode 100644 index 7567b969..00000000 --- a/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Redis from 'ioredis'; -import { KeyBuilderSS } from '../../KeyBuilderSS'; -import { LatenciesCacheInRedis } from '../LatenciesCacheInRedis'; - -const prefix = 'latencies_cache_UT'; -const metadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; - -test('METRICS (LATENCIES) CACHE IN REDIS / should count based on ranges', async () => { - const connection = new Redis(); - const keys = new KeyBuilderSS(prefix, metadata); - const cache = new LatenciesCacheInRedis(keys, connection); - const metricName = 'testing'; - - await cache.track(metricName, 1); - await cache.track(metricName, 1.2); - await cache.track(metricName, 1.4); - - expect(await connection.get(keys.buildLatencyKey(metricName, 0))).toBe('3'); // the bucket #0 should have 3 - - await cache.track(metricName, 1.5); - - expect(await connection.get(keys.buildLatencyKey(metricName, 1))).toBe('1'); // the bucket #1 should have 1 - - await cache.track(metricName, 2.25); - await cache.track(metricName, 2.26); - await cache.track(metricName, 2.265); - - expect(await connection.get(keys.buildLatencyKey(metricName, 2))).toBe('3'); // the bucket #2 should have 3 - - await cache.track(metricName, 985251); - - expect(await connection.get(keys.buildLatencyKey(metricName, 23))).toBe('1'); // the bucket #23 should have 1 - - // Clean up post-test - const keysToClean = await connection.keys(`${prefix}*`); - await connection.del(keysToClean); - - await connection.quit(); -}); diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index de72a220..c22ee15d 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -6,8 +6,6 @@ import { SplitsCacheInRedis } from './SplitsCacheInRedis'; import { SegmentsCacheInRedis } from './SegmentsCacheInRedis'; import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis'; import { EventsCacheInRedis } from './EventsCacheInRedis'; -import { LatenciesCacheInRedis } from './LatenciesCacheInRedis'; -import { CountsCacheInRedis } from './CountsCacheInRedis'; import { STORAGE_REDIS } from '../../utils/constants'; export interface InRedisStorageOptions { @@ -38,8 +36,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy segments: new SegmentsCacheInRedis(log, keys, redisClient), impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata), events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata), - latencies: new LatenciesCacheInRedis(keys, redisClient), - counts: new CountsCacheInRedis(keys, redisClient), + // @TODO add new telemetry cache // When using REDIS we should: // 1- Disconnect from the storage From 093c49f282498c6c6a1d14c6ecd7df1e9794261d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 25 Jan 2022 18:41:14 -0300 Subject: [PATCH 03/70] implementation and UTs --- .../inMemory/TelemetryCacheInMemory.ts | 196 +++++++++++++++ .../__tests__/TelemetryCacheInMemory.spec.ts | 225 ++++++++++++++++++ src/storages/types.ts | 90 +++++-- src/sync/submitters/types.ts | 123 ++++++++++ src/utils/constants/index.ts | 27 +++ 5 files changed, 636 insertions(+), 25 deletions(-) create mode 100644 src/storages/inMemory/TelemetryCacheInMemory.ts create mode 100644 src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts diff --git a/src/storages/inMemory/TelemetryCacheInMemory.ts b/src/storages/inMemory/TelemetryCacheInMemory.ts new file mode 100644 index 00000000..845348b7 --- /dev/null +++ b/src/storages/inMemory/TelemetryCacheInMemory.ts @@ -0,0 +1,196 @@ +import { ImpressionDataType, EventDataType, LastSync, HttpErrors, HttpLatencies, StreamingEvent, Method, OperationType, MethodExceptions, MethodLatencies } from '../../sync/submitters/types'; +import { TelemetryCacheSync } from '../types'; + +const MAX_LATENCY_BUCKET_COUNT = 23; +const MAX_STREAMING_EVENTS = 20; +const MAX_TAGS = 10; + +export class TelemetryCacheInMemory implements TelemetryCacheSync { + + private timeUntilReady?: number; + + getTimeUntilReady() { + return this.timeUntilReady; + } + + recordTimeUntilReady(ms: number) { + this.timeUntilReady = ms; + } + + private timeUntilReadyFromCache?: number; + + getTimeUntilReadyFromCache() { + return this.timeUntilReadyFromCache; + } + + recordTimeUntilReadyFromCache(ms: number) { + this.timeUntilReadyFromCache = ms; + } + + private notReadyUsage = 0; + + getNonReadyUsage() { + return this.notReadyUsage; + } + + recordNonReadyUsage() { + this.notReadyUsage++; + } + + private impressionStats = [0, 0, 0]; + + getImpressionStats(type: ImpressionDataType) { + return this.impressionStats[type]; + } + + recordImpressionStats(type: ImpressionDataType, count: number) { + this.impressionStats[type] += count; + } + + private eventStats = [0, 0]; + + getEventStats(type: EventDataType) { + return this.eventStats[type]; + } + + recordEventStats(type: EventDataType, count: number) { + this.eventStats[type] += count; + } + + // @ts-expect-error + private lastSync: LastSync = {}; + + getLastSynchronization() { + return this.lastSync; + } + + recordSuccessfulSync(resource: OperationType, timeMs: number) { + this.lastSync[resource] = timeMs; + } + + // @ts-expect-error + private httpErrors: HttpErrors = {}; + + popHttpErrors() { + const result = this.httpErrors; // @ts-expect-error + this.httpErrors = {}; + return result; + } + + recordSyncError(resource: OperationType, status: number) { + if (!this.httpErrors[resource]) this.httpErrors[resource] = {}; + if (!this.httpErrors[resource][status]) { + this.httpErrors[resource][status] = 1; + } else { + this.httpErrors[resource][status]++; + } + } + + // @ts-expect-error + private httpLatencies: HttpLatencies = {}; + + popHttpLatencies() { + const result = this.httpLatencies; // @ts-expect-error + this.httpLatencies = {}; + return result; + } + + recordSyncLatency(resource: OperationType, latencyMs: number) { + if (!this.httpLatencies[resource]) this.httpLatencies[resource] = []; + if (this.httpLatencies[resource].length < MAX_LATENCY_BUCKET_COUNT) { + this.httpLatencies[resource].push(latencyMs); + } + } + + private authRejections = 0; + + popAuthRejections() { + const result = this.authRejections; + this.authRejections = 0; + return result; + } + + recordAuthRejections() { + this.authRejections++; + } + + private tokenRefreshes = 0; + + popTokenRefreshes() { + const result = this.tokenRefreshes; + this.tokenRefreshes = 0; + return result; + } + + recordTokenRefreshes() { + this.tokenRefreshes++; + } + + private streamingEvents: StreamingEvent[] = [] + + popStreamingEvents() { + return this.streamingEvents.splice(0); + } + + recordStreamingEvents(streamingEvent: StreamingEvent) { + if (this.streamingEvents.length < MAX_STREAMING_EVENTS) { + this.streamingEvents.push(streamingEvent); + } + } + + private tags: string[] = []; + + popTags() { + return this.tags.splice(0); + } + + addTag(tag: string) { + if (this.tags.length < MAX_TAGS) { + this.tags.push(tag); + } + } + + private sessionLength?: number; + + getSessionLength() { + return this.sessionLength; + } + + recordSessionLength(ms: number) { + this.sessionLength = ms; + } + + // @ts-expect-error + private exceptions: MethodExceptions = {}; + + popExceptions() { + const result = this.exceptions; // @ts-expect-error + this.exceptions = {}; + return result; + } + + recordException(method: Method) { + if (!this.exceptions[method]) { + this.exceptions[method] = 1; + } else { + this.exceptions[method]++; + } + } + + // @ts-expect-error + private latencies: MethodLatencies = {}; + + popLatencies() { + const result = this.latencies; // @ts-expect-error + this.latencies = {}; + return result; + } + + recordLatency(method: Method, latencyMs: number) { + if (!this.latencies[method]) this.latencies[method] = []; + if (this.latencies[method].length < MAX_LATENCY_BUCKET_COUNT) { + this.latencies[method].push(latencyMs); + } + } + +} diff --git a/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts new file mode 100644 index 00000000..ab09141f --- /dev/null +++ b/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts @@ -0,0 +1,225 @@ +import { QUEUED, DROPPED, DEDUPED, EVENTS, IMPRESSIONS, IMPRESSIONS_COUNT, MY_SEGMENT, SEGMENT, SPLITS, TELEMETRY, TOKEN, TRACK, TREATMENT, TREATMENTS, TREATMENTS_WITH_CONFIG, TREATMENT_WITH_CONFIG } from '../../../utils/constants'; +import { EventDataType, ImpressionDataType, Method, OperationType, StreamingEvent } from '../../../sync/submitters/types'; +import { TelemetryCacheInMemory } from '../TelemetryCacheInMemory'; + +const impressionDataTypes: ImpressionDataType[] = [QUEUED, DROPPED, DEDUPED]; + +const eventDataTypes: EventDataType[] = [QUEUED, DROPPED]; + +const operationTypes: OperationType[] = [ + SPLITS, + IMPRESSIONS, + IMPRESSIONS_COUNT, + EVENTS, + TELEMETRY, + TOKEN, + SEGMENT, + MY_SEGMENT +]; + +const methods: Method[] = [ + TREATMENT, + TREATMENTS, + TREATMENT_WITH_CONFIG, + TREATMENTS_WITH_CONFIG, + TRACK +]; + +describe('TELEMETRY CACHE', () => { + const cache = new TelemetryCacheInMemory(); + + test('time until ready', () => { + expect(cache.getTimeUntilReady()).toBe(undefined); + cache.recordTimeUntilReady(100); + expect(cache.getTimeUntilReady()).toBe(100); + }); + + test('time until ready from cache', () => { + expect(cache.getTimeUntilReadyFromCache()).toBe(undefined); + cache.recordTimeUntilReadyFromCache(10); + expect(cache.getTimeUntilReadyFromCache()).toBe(10); + }); + + test('session length', () => { + expect(cache.getSessionLength()).toBe(undefined); + cache.recordSessionLength(10); + expect(cache.getSessionLength()).toBe(10); + }); + + test('not ready usage', () => { + expect(cache.getNonReadyUsage()).toBe(0); + cache.recordNonReadyUsage(); + expect(cache.getNonReadyUsage()).toBe(1); + cache.recordNonReadyUsage(); + cache.recordNonReadyUsage(); + expect(cache.getNonReadyUsage()).toBe(3); + }); + + test('impression stats', () => { + impressionDataTypes.forEach((stat: ImpressionDataType) => { + expect(cache.getImpressionStats(stat)).toBe(0); + cache.recordImpressionStats(stat, 1); + expect(cache.getImpressionStats(stat)).toBe(1); + cache.recordImpressionStats(stat, 10); + expect(cache.getImpressionStats(stat)).toBe(11); + }); + }); + + test('event stats', () => { + eventDataTypes.forEach((stat: EventDataType) => { + expect(cache.getEventStats(stat)).toBe(0); + cache.recordEventStats(stat, 1); + expect(cache.getEventStats(stat)).toBe(1); + cache.recordEventStats(stat, 2); + expect(cache.getEventStats(stat)).toBe(3); + }); + }); + + test('last synchronization', () => { + expect(cache.getLastSynchronization()).toEqual({}); + operationTypes.forEach((operation, index) => { + cache.recordSuccessfulSync(operation, index); + }); + + const expectedLastSync = { 'sp': 0, 'im': 1, 'ic': 2, 'ev': 3, 'te': 4, 'to': 5, 'se': 6, 'ms': 7 }; + expect(cache.getLastSynchronization()).toEqual(expectedLastSync); + + // Overwrite a single operation + cache.recordSuccessfulSync(MY_SEGMENT, 100); + expect(cache.getLastSynchronization()).toEqual({ ...expectedLastSync, 'ms': 100 }); + }); + + test('http errors', () => { + expect(cache.popHttpErrors()).toEqual({}); + operationTypes.forEach((operation) => { + cache.recordSyncError(operation, 400); + cache.recordSyncError(operation, 400); + cache.recordSyncError(operation, 500); + }); + + const httpErrors = { '400': 2, '500': 1 }; + const expectedHttpErrors = { 'sp': httpErrors, 'im': httpErrors, 'ic': httpErrors, 'ev': httpErrors, 'te': httpErrors, 'to': httpErrors, 'se': httpErrors, 'ms': httpErrors }; + expect(cache.popHttpErrors()).toEqual(expectedHttpErrors); + expect(cache.popHttpErrors()).toEqual({}); + + // Set a single http error + cache.recordSyncError(MY_SEGMENT, 400); + expect(cache.popHttpErrors()).toEqual({ 'ms': { 400: 1 } }); + }); + + test('http latencies', () => { + expect(cache.popHttpLatencies()).toEqual({}); + operationTypes.forEach((operation) => { + cache.recordSyncLatency(operation, 300); + cache.recordSyncLatency(operation, 400); + cache.recordSyncLatency(operation, 500); + }); + + const latencies = [300, 400, 500]; + const expectedLatencies = { 'sp': latencies, 'im': latencies, 'ic': latencies, 'ev': latencies, 'te': latencies, 'to': latencies, 'se': latencies, 'ms': latencies }; + expect(cache.popHttpLatencies()).toEqual(expectedLatencies); + expect(cache.popHttpLatencies()).toEqual({}); + + // MAX_LATENCY_BUCKET_COUNT === 23 + for (let i = 0; i < 100; i++) { + cache.recordSyncLatency(MY_SEGMENT, i); + } + expect(cache.popHttpLatencies()).toEqual({ 'ms': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }); + expect(cache.popHttpLatencies()).toEqual({}); + }); + + test('auth rejections', () => { + expect(cache.popAuthRejections()).toBe(0); + cache.recordAuthRejections(); + expect(cache.popAuthRejections()).toBe(1); + expect(cache.popAuthRejections()).toBe(0); + cache.recordAuthRejections(); + cache.recordAuthRejections(); + expect(cache.popAuthRejections()).toBe(2); + expect(cache.popAuthRejections()).toBe(0); + }); + + test('token refreshes', () => { + expect(cache.popTokenRefreshes()).toBe(0); + cache.recordTokenRefreshes(); + expect(cache.popTokenRefreshes()).toBe(1); + expect(cache.popTokenRefreshes()).toBe(0); + cache.recordTokenRefreshes(); + cache.recordTokenRefreshes(); + expect(cache.popTokenRefreshes()).toBe(2); + expect(cache.popTokenRefreshes()).toBe(0); + }); + + test('streaming events', () => { + const streamingEvent: StreamingEvent = { e: 10, d: 2, t: 3 }; + + expect(cache.popStreamingEvents()).toEqual([]); + cache.recordStreamingEvents(streamingEvent); + expect(cache.popStreamingEvents()).toEqual([streamingEvent]); + expect(cache.popStreamingEvents()).toEqual([]); + + // MAX_STREAMING_EVENTS === 20 + for (let i = 0; i < 100; i++) { + cache.recordStreamingEvents({ ...streamingEvent, t: i }); + } + const actualStreamingEvents = cache.popStreamingEvents(); + expect(actualStreamingEvents.length).toBe(20); + actualStreamingEvents.forEach((actualStreamingEvent, index) => { + expect(actualStreamingEvent).toEqual({ ...streamingEvent, t: index }); + }); + expect(cache.popStreamingEvents()).toEqual([]); + }); + + test('tags', () => { + expect(cache.popTags()).toEqual([]); + cache.addTag('MY_TAG'); + expect(cache.popTags()).toEqual(['MY_TAG']); + expect(cache.popTags()).toEqual([]); + + // MAX_TAGS === 10 + for (let i = 0; i < 100; i++) { + cache.addTag('TAG_' + i); + } + expect(cache.popTags()).toEqual(['TAG_0', 'TAG_1', 'TAG_2', 'TAG_3', 'TAG_4', 'TAG_5', 'TAG_6', 'TAG_7', 'TAG_8', 'TAG_9']); + expect(cache.popTags()).toEqual([]); + }); + + + test('method exceptions', () => { + expect(cache.popExceptions()).toEqual({}); + methods.forEach((method) => { + cache.recordException(method); + cache.recordException(method); + }); + + const expectedExceptions = { 't': 2, 'ts': 2, 'tc': 2, 'tcs': 2, 'tr': 2 }; + expect(cache.popExceptions()).toEqual(expectedExceptions); + expect(cache.popExceptions()).toEqual({}); + + // Set a single method exception error + cache.recordException(TRACK); + expect(cache.popExceptions()).toEqual({ 'tr': 1 }); + }); + + test('method latencies', () => { + expect(cache.popLatencies()).toEqual({}); + methods.forEach((method) => { + cache.recordLatency(method, 300); + cache.recordLatency(method, 400); + cache.recordLatency(method, 500); + }); + + const latencies = [300, 400, 500]; + const expectedLatencies = { 't': latencies, 'ts': latencies, 'tc': latencies, 'tcs': latencies, 'tr': latencies }; + expect(cache.popLatencies()).toEqual(expectedLatencies); + expect(cache.popLatencies()).toEqual({}); + + // MAX_LATENCY_BUCKET_COUNT === 23 + for (let i = 0; i < 100; i++) { + cache.recordLatency(TRACK, i); + } + expect(cache.popLatencies()).toEqual({ 'tr': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }); + expect(cache.popLatencies()).toEqual({}); + }); + +}); diff --git a/src/storages/types.ts b/src/storages/types.ts index 0e3a6001..7285616f 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,6 +1,6 @@ import { MaybeThenable, IMetadata, ISplitFiltersValidation } from '../dtos/types'; import { ILogger } from '../logger/types'; -import { StoredEventWithMetadata, StoredImpressionWithMetadata } from '../sync/submitters/types'; +import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent } from '../sync/submitters/types'; import { SplitIO, ImpressionDTO, SDKMode } from '../types'; /** @@ -351,29 +351,73 @@ export interface IImpressionCountsCacheSync extends IRecorderCacheProducerSync> { - track(metricName: string, latency: number): boolean - isEmpty(): boolean - clear(): void - state(): Record +export interface TelemetryInitConsumerSync { + getTimeUntilReady(): number | undefined; + getTimeUntilReadyFromCache(): number | undefined; + getNonReadyUsage(): number; + // 'active factories' and 'redundant factories' are not tracked in the storage. They are derived from `usedKeysMap` } -export interface ILatenciesCacheAsync { - track(metricName: string, latency: number): Promise +export interface TelemetryRuntimeConsumerSync { + getImpressionStats(type: ImpressionDataType): number; + getEventStats(type: EventDataType): number; + getLastSynchronization(): LastSync; + popHttpErrors(): HttpErrors; + popHttpLatencies(): HttpLatencies; + popAuthRejections(): number; + popTokenRefreshes(): number; + popStreamingEvents(): Array; + popTags(): Array | undefined; + getSessionLength(): number | undefined; } -export interface ICountsCacheSync extends IRecorderCacheProducerSync> { - track(metricName: string): boolean - isEmpty(): boolean - clear(): void - state(): Record +export interface TelemetryEvaluationConsumerSync { + popExceptions(): MethodExceptions; + popLatencies(): MethodLatencies; +} + +export interface TelemetryStorageConsumerSync extends TelemetryInitConsumerSync, TelemetryRuntimeConsumerSync, TelemetryEvaluationConsumerSync { } + +export interface TelemetryInitProducerSync { + recordTimeUntilReady(ms: number): void; + recordTimeUntilReadyFromCache(ms: number): void; + recordNonReadyUsage(): void; + // 'active factories' and 'redundant factories' are not tracked in the storage. They are derived from `usedKeysMap` } -export interface ICountsCacheAsync { - track(metricName: string): Promise +export interface TelemetryRuntimeProducerSync { + addTag(tag: string): void; + recordImpressionStats(type: ImpressionDataType, count: number): void; + recordEventStats(type: EventDataType, count: number): void; + recordSuccessfulSync(resource: OperationType, timeMs: number): void; + recordSyncError(resource: OperationType, status: number): void; + recordSyncLatency(resource: OperationType, latencyMs: number): void; + recordAuthRejections(): void; + recordTokenRefreshes(): void; + recordStreamingEvents(streamingEvent: StreamingEvent): void; + recordSessionLength(ms: number): void; +} + +export interface TelemetryEvaluationProducerSync { + recordLatency(method: Method, latencyMs: number): void; + recordException(method: Method): void; +} + +export interface TelemetryStorageProducerSync extends TelemetryInitProducerSync, TelemetryRuntimeProducerSync, TelemetryEvaluationProducerSync { } + +export interface TelemetryCacheSync extends TelemetryStorageConsumerSync, TelemetryStorageProducerSync { } + +/** + * Telemetry storage interface for consumer mode. + * Methods are async because data is stored in Redis or a pluggable storage. + */ +export interface TelemetryCacheAsync { + // @TODO } /** @@ -385,16 +429,14 @@ export interface IStorageBase< TSegmentsCache extends ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase, TEventsCache extends IEventsCacheBase, - TLatenciesCache extends ILatenciesCacheSync | ILatenciesCacheAsync, - TCountsCache extends ICountsCacheSync | ICountsCacheAsync, + TTelemetryCache extends TelemetryCacheSync | TelemetryCacheAsync > { splits: TSplitsCache, segments: TSegmentsCache, impressions: TImpressionsCache, impressionCounts?: IImpressionCountsCacheSync, events: TEventsCache, - latencies?: TLatenciesCache, - counts?: TCountsCache, + telemetry?: TTelemetryCache, destroy(): void | Promise, shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this } @@ -404,8 +446,7 @@ export type IStorageSync = IStorageBase< ISegmentsCacheSync, IImpressionsCacheSync, IEventsCacheSync, - ILatenciesCacheSync, - ICountsCacheSync + TelemetryCacheSync > export type IStorageAsync = IStorageBase< @@ -413,8 +454,7 @@ export type IStorageAsync = IStorageBase< ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, IEventsCacheAsync | IEventsCacheSync, - ILatenciesCacheAsync, - ICountsCacheAsync + TelemetryCacheAsync | TelemetryCacheSync > /** StorageFactory */ diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 6ab697c1..695c145d 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -62,3 +62,126 @@ export type StoredEventWithMetadata = { /** Stored event */ e: SplitIO.EventData } + +/** + * Telemetry usage stats + */ + +export type QUEUED = 0; +export type DROPPED = 1; +export type DEDUPED = 2; +export type ImpressionDataType = QUEUED | DROPPED | DEDUPED +export type EventDataType = QUEUED | DROPPED; + +export type SPLITS = 'sp'; +export type IMPRESSIONS = 'im'; +export type IMPRESSIONS_COUNT = 'ic'; +export type EVENTS = 'ev'; +export type TELEMETRY = 'te'; +export type TOKEN = 'to'; +export type SEGMENT = 'se'; +export type MY_SEGMENT = 'ms'; +export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MY_SEGMENT; + +export type LastSync = Record +export type HttpErrors = Record +export type HttpLatencies = Record> + +export type TREATMENT = 't'; +export type TREATMENTS = 'ts'; +export type TREATMENT_WITH_CONFIG = 'tc'; +export type TREATMENTS_WITH_CONFIG = 'tcs'; +export type TRACK = 'tr'; +export type Method = TREATMENT | TREATMENTS | TREATMENT_WITH_CONFIG | TREATMENTS_WITH_CONFIG | TRACK; + +export type MethodLatencies = Record>; + +export type MethodExceptions = Record; + +export type CONNECTION_ESTABLISHED = 0; +export type OCCUPANCY_PRI = 10; +export type OCCUPANCY_SEC = 20; +export type STREAMING_STATUS = 30; +export type SSE_CONNECTION_ERROR = 40; +export type TOKEN_REFRESH = 50; +export type ABLY_ERROR = 60; +export type SYNC_MODE_UPDATE = 70; +export type EventType = CONNECTION_ESTABLISHED | OCCUPANCY_PRI | OCCUPANCY_SEC | STREAMING_STATUS | SSE_CONNECTION_ERROR | TOKEN_REFRESH | ABLY_ERROR | SYNC_MODE_UPDATE; + +export type StreamingEvent = { + e: EventType, // eventType + d: number, // eventData + t: number, // timestamp +} + +// 'metrics/usage' JSON request body +export type TelemetryUsageStatsPayload = { + lS: LastSync, // lastSynchronization + mL: MethodLatencies, // clientMethodLatencies + mE: MethodExceptions, // methodExceptions + hE: HttpErrors, // httpErrors + hL: HttpLatencies, // httpLatencies + tR: number, // tokenRefreshes + aR: number, // authRejections + iQ: number, // impressionsQueued + iDe: number, // impressionsDeduped + iDr: number, // impressionsDropped + spC: number, // splitCount + seC: number, // segmentCount + skC: number, // segmentKeyCount + sL: number, // sessionLengthMs + eQ: number, // eventsQueued + eD: number, // eventsDropped + sE: Array, // streamingEvents + t?: Array, // tags +} + +/** + * Telemetry config stats + */ + +export type STANDALONE_ENUM = 0; +export type CONSUMER_ENUM = 1; +export type CONSUMER_PARTIAL_ENUM = 2; +export type OperationMode = STANDALONE_ENUM | CONSUMER_ENUM | CONSUMER_PARTIAL_ENUM + +export type OPTIMIZED_ENUM = 0; +export type DEBUG_ENUM = 1; +export type ImpressionsMode = OPTIMIZED_ENUM | DEBUG_ENUM; + +export type RefreshRates = { + sp: number, // splits + se: number, // mySegments + im: number, // impressions + ev: number, // events + te: number, // telemetry +} + +export type UrlOverrides = { + s: boolean, // sdkUrl + e: boolean, // events + a: boolean, // auth + st: boolean, // stream + t: boolean, // telemetry +} + +// 'metrics/config' JSON request body +export type TelemetryConfigStatsPayload = { + oM?: OperationMode, // operationMode + st: 'memory' | 'redis' | 'pluggable' | 'localstorage', // storage + sE: boolean, // streamingEnabled + rR: RefreshRates, // refreshRates + uO: UrlOverrides, // urlOverrides + iQ: number, // impressionsQueueSize + eQ: number, // eventsQueueSize + iM: ImpressionsMode, // impressionsMode + iL: boolean, // impressionsListenerEnabled + hP: boolean, // httpProxyDetected + aF: number, // activeFactories + rF: number, // redundantActiveFactories + tR: number, // timeUntilSDKReady + tC: number, // timeUntilSDKReadyFromCache + nR: number, // SDKNotReadyUsage + t?: Array, // tags + i?: Array, // integrations +} diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 4a4d8303..56fdddff 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -32,3 +32,30 @@ export const STORAGE_MEMORY: StorageType = 'MEMORY'; export const STORAGE_LOCALSTORAGE: StorageType = 'LOCALSTORAGE'; export const STORAGE_REDIS: StorageType = 'REDIS'; export const STORAGE_PLUGGABLE: StorageType = 'PLUGGABLE'; + +// Telemetry +export const QUEUED = 0; +export const DROPPED = 1; +export const DEDUPED = 2; + +export const STANDALONE_ENUM = 0; +export const CONSUMER_ENUM = 1; +export const CONSUMER_PARTIAL_ENUM = 2; + +export const OPTIMIZED_ENUM = 0; +export const DEBUG_ENUM = 1; + +export const SPLITS = 'sp'; +export const IMPRESSIONS = 'im'; +export const IMPRESSIONS_COUNT = 'ic'; +export const EVENTS = 'ev'; +export const TELEMETRY = 'te'; +export const TOKEN = 'to'; +export const SEGMENT = 'se'; +export const MY_SEGMENT = 'ms'; + +export const TREATMENT = 't'; +export const TREATMENTS = 'ts'; +export const TREATMENT_WITH_CONFIG = 'tc'; +export const TREATMENTS_WITH_CONFIG = 'tcs'; +export const TRACK = 'tr'; From 6ff0ad899f52e9cc037481d3c94325c6f610ae64 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 25 Jan 2022 18:47:54 -0300 Subject: [PATCH 04/70] fixed ut utils --- src/storages/__tests__/testUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index 4905adb2..520dba1d 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -7,8 +7,7 @@ export function assertStorageInterface(storage: IStorageSync | IStorageAsync) { expect(typeof storage.segments).toBe('object'); expect(typeof storage.impressions).toBe('object'); expect(typeof storage.events).toBe('object'); - expect(!storage.latencies || typeof storage.latencies === 'object').toBeTruthy; - expect(!storage.counts || typeof storage.counts === 'object').toBeTruthy; + expect(!storage.telemetry || typeof storage.telemetry === 'object').toBeTruthy; expect(!storage.impressionCounts || typeof storage.impressionCounts === 'object').toBeTruthy; } From f61e0831ec11b13026b6fad25a8bf807ab2ebe40 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 12 Apr 2022 16:03:47 -0300 Subject: [PATCH 05/70] fixed latency buckets --- .../__tests__/findLatencyIndex.spec.ts | 28 +++++++++++++++++++ src/storages/findLatencyIndex.ts | 15 ++++++++-- .../inMemory/TelemetryCacheInMemory.ts | 23 +++++++++------ .../__tests__/TelemetryCacheInMemory.spec.ts | 16 +++++------ src/storages/types.ts | 4 +-- 5 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 src/storages/__tests__/findLatencyIndex.spec.ts diff --git a/src/storages/__tests__/findLatencyIndex.spec.ts b/src/storages/__tests__/findLatencyIndex.spec.ts new file mode 100644 index 00000000..8331876a --- /dev/null +++ b/src/storages/__tests__/findLatencyIndex.spec.ts @@ -0,0 +1,28 @@ +import { findLatencyIndex } from '../findLatencyIndex'; + +test('findLatencyIndex', () => { + + const latenciesInMsAndBuckets = [ + // First bucket is up to 0.5 ms + [0, 0], + [0.500, 0], + [1.400, 1], + [1.500, 1], + [8.000, 6], + [11.39, 6], + [11.392, 7], + [17.085, 7], + // Last bucket + [7481.827, 22], + [7481.828, 22], + [7999.999, 22], + // Invalid values + [NaN, 0], + ['invalid', 0] + ]; + + latenciesInMsAndBuckets.forEach(([latencyInMs, expectedBucket]) => { // @ts-ignore + expect(findLatencyIndex(latencyInMs)).toBe(expectedBucket); + }); + +}); diff --git a/src/storages/findLatencyIndex.ts b/src/storages/findLatencyIndex.ts index 908752d9..aac73e7a 100644 --- a/src/storages/findLatencyIndex.ts +++ b/src/storages/findLatencyIndex.ts @@ -1,7 +1,16 @@ import { isNaNNumber } from '../utils/lang'; -// @TODO add unit tests -export function findLatencyIndex(latency: number, min = 0, max = 23, base = 1.5): number { - const index = Math.min(max, Math.max(min, Math.floor(Math.log(latency) / Math.log(base)))); +const MIN = 0; +const MAX = 22; +const BASE = 1.5; + +/** + * Calculates buckets from latency in milliseconds + * + * @param latencyInMs + * @returns a bucket index from 0 to 22 inclusive + */ +export function findLatencyIndex(latencyInMs: number): number { + const index = Math.min(MAX, Math.max(MIN, Math.ceil(Math.log(latencyInMs) / Math.log(BASE)))); return isNaNNumber(index) ? 0 : index; // index is NaN if latency is not a positive number } diff --git a/src/storages/inMemory/TelemetryCacheInMemory.ts b/src/storages/inMemory/TelemetryCacheInMemory.ts index 845348b7..5c84a20c 100644 --- a/src/storages/inMemory/TelemetryCacheInMemory.ts +++ b/src/storages/inMemory/TelemetryCacheInMemory.ts @@ -1,10 +1,15 @@ import { ImpressionDataType, EventDataType, LastSync, HttpErrors, HttpLatencies, StreamingEvent, Method, OperationType, MethodExceptions, MethodLatencies } from '../../sync/submitters/types'; +import { findLatencyIndex } from '../findLatencyIndex'; import { TelemetryCacheSync } from '../types'; -const MAX_LATENCY_BUCKET_COUNT = 23; const MAX_STREAMING_EVENTS = 20; const MAX_TAGS = 10; +function newBuckets() { + // MAX_LATENCY_BUCKET_COUNT (length) is 23; + return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +} + export class TelemetryCacheInMemory implements TelemetryCacheSync { private timeUntilReady?: number; @@ -77,7 +82,7 @@ export class TelemetryCacheInMemory implements TelemetryCacheSync { return result; } - recordSyncError(resource: OperationType, status: number) { + recordHttpError(resource: OperationType, status: number) { if (!this.httpErrors[resource]) this.httpErrors[resource] = {}; if (!this.httpErrors[resource][status]) { this.httpErrors[resource][status] = 1; @@ -95,11 +100,11 @@ export class TelemetryCacheInMemory implements TelemetryCacheSync { return result; } - recordSyncLatency(resource: OperationType, latencyMs: number) { - if (!this.httpLatencies[resource]) this.httpLatencies[resource] = []; - if (this.httpLatencies[resource].length < MAX_LATENCY_BUCKET_COUNT) { - this.httpLatencies[resource].push(latencyMs); + recordHttpLatency(resource: OperationType, latencyMs: number) { + if (!this.httpLatencies[resource]) { + this.httpLatencies[resource] = newBuckets(); } + this.httpLatencies[resource][findLatencyIndex(latencyMs)]++; } private authRejections = 0; @@ -187,10 +192,10 @@ export class TelemetryCacheInMemory implements TelemetryCacheSync { } recordLatency(method: Method, latencyMs: number) { - if (!this.latencies[method]) this.latencies[method] = []; - if (this.latencies[method].length < MAX_LATENCY_BUCKET_COUNT) { - this.latencies[method].push(latencyMs); + if (!this.latencies[method]) { + this.latencies[method] = newBuckets(); } + this.latencies[method][findLatencyIndex(latencyMs)]++; } } diff --git a/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts index ab09141f..83be6ba9 100644 --- a/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts @@ -92,9 +92,9 @@ describe('TELEMETRY CACHE', () => { test('http errors', () => { expect(cache.popHttpErrors()).toEqual({}); operationTypes.forEach((operation) => { - cache.recordSyncError(operation, 400); - cache.recordSyncError(operation, 400); - cache.recordSyncError(operation, 500); + cache.recordHttpError(operation, 400); + cache.recordHttpError(operation, 400); + cache.recordHttpError(operation, 500); }); const httpErrors = { '400': 2, '500': 1 }; @@ -103,16 +103,16 @@ describe('TELEMETRY CACHE', () => { expect(cache.popHttpErrors()).toEqual({}); // Set a single http error - cache.recordSyncError(MY_SEGMENT, 400); + cache.recordHttpError(MY_SEGMENT, 400); expect(cache.popHttpErrors()).toEqual({ 'ms': { 400: 1 } }); }); test('http latencies', () => { expect(cache.popHttpLatencies()).toEqual({}); operationTypes.forEach((operation) => { - cache.recordSyncLatency(operation, 300); - cache.recordSyncLatency(operation, 400); - cache.recordSyncLatency(operation, 500); + cache.recordHttpLatency(operation, 300); + cache.recordHttpLatency(operation, 400); + cache.recordHttpLatency(operation, 500); }); const latencies = [300, 400, 500]; @@ -122,7 +122,7 @@ describe('TELEMETRY CACHE', () => { // MAX_LATENCY_BUCKET_COUNT === 23 for (let i = 0; i < 100; i++) { - cache.recordSyncLatency(MY_SEGMENT, i); + cache.recordHttpLatency(MY_SEGMENT, i); } expect(cache.popHttpLatencies()).toEqual({ 'ms': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }); expect(cache.popHttpLatencies()).toEqual({}); diff --git a/src/storages/types.ts b/src/storages/types.ts index 9d1e3035..26174555 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -398,8 +398,8 @@ export interface TelemetryRuntimeProducerSync { recordImpressionStats(type: ImpressionDataType, count: number): void; recordEventStats(type: EventDataType, count: number): void; recordSuccessfulSync(resource: OperationType, timeMs: number): void; - recordSyncError(resource: OperationType, status: number): void; - recordSyncLatency(resource: OperationType, latencyMs: number): void; + recordHttpError(resource: OperationType, status: number): void; + recordHttpLatency(resource: OperationType, latencyMs: number): void; recordAuthRejections(): void; recordTokenRefreshes(): void; recordStreamingEvents(streamingEvent: StreamingEvent): void; From 4f5bd22b87705108832749db2da7a135f2c9102f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 12 Apr 2022 16:35:05 -0300 Subject: [PATCH 06/70] updated unit tests --- .../__tests__/findLatencyIndex.spec.ts | 37 +++++++++--------- .../__tests__/TelemetryCacheInMemory.spec.ts | 39 +++++++------------ 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/src/storages/__tests__/findLatencyIndex.spec.ts b/src/storages/__tests__/findLatencyIndex.spec.ts index 8331876a..b2b6f04c 100644 --- a/src/storages/__tests__/findLatencyIndex.spec.ts +++ b/src/storages/__tests__/findLatencyIndex.spec.ts @@ -1,25 +1,24 @@ import { findLatencyIndex } from '../findLatencyIndex'; -test('findLatencyIndex', () => { +const latenciesInMsAndBuckets = [ + // First bucket is up to 0.5 ms + [0, 0], + [0.500, 0], + [1.400, 1], + [1.500, 1], + [8.000, 6], + [11.39, 6], + [11.392, 7], + [17.085, 7], + // Last bucket + [7481.827, 22], + [7481.828, 22], + [7999.999, 22], + // Invalid values + [NaN, 0] +]; - const latenciesInMsAndBuckets = [ - // First bucket is up to 0.5 ms - [0, 0], - [0.500, 0], - [1.400, 1], - [1.500, 1], - [8.000, 6], - [11.39, 6], - [11.392, 7], - [17.085, 7], - // Last bucket - [7481.827, 22], - [7481.828, 22], - [7999.999, 22], - // Invalid values - [NaN, 0], - ['invalid', 0] - ]; +test('findLatencyIndex', () => { latenciesInMsAndBuckets.forEach(([latencyInMs, expectedBucket]) => { // @ts-ignore expect(findLatencyIndex(latencyInMs)).toBe(expectedBucket); diff --git a/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts index 83be6ba9..a3d6d854 100644 --- a/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/TelemetryCacheInMemory.spec.ts @@ -25,6 +25,9 @@ const methods: Method[] = [ TRACK ]; +const latencies = [0, 0.500, 1.400, 17.085, 7999.999]; +const latencyBuckets = [2, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + describe('TELEMETRY CACHE', () => { const cache = new TelemetryCacheInMemory(); @@ -110,21 +113,13 @@ describe('TELEMETRY CACHE', () => { test('http latencies', () => { expect(cache.popHttpLatencies()).toEqual({}); operationTypes.forEach((operation) => { - cache.recordHttpLatency(operation, 300); - cache.recordHttpLatency(operation, 400); - cache.recordHttpLatency(operation, 500); + latencies.forEach((latency) => { + cache.recordHttpLatency(operation, latency); + }); }); - const latencies = [300, 400, 500]; - const expectedLatencies = { 'sp': latencies, 'im': latencies, 'ic': latencies, 'ev': latencies, 'te': latencies, 'to': latencies, 'se': latencies, 'ms': latencies }; - expect(cache.popHttpLatencies()).toEqual(expectedLatencies); - expect(cache.popHttpLatencies()).toEqual({}); - - // MAX_LATENCY_BUCKET_COUNT === 23 - for (let i = 0; i < 100; i++) { - cache.recordHttpLatency(MY_SEGMENT, i); - } - expect(cache.popHttpLatencies()).toEqual({ 'ms': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }); + const expectedLatencyBuckets = { 'sp': latencyBuckets, 'im': latencyBuckets, 'ic': latencyBuckets, 'ev': latencyBuckets, 'te': latencyBuckets, 'to': latencyBuckets, 'se': latencyBuckets, 'ms': latencyBuckets }; + expect(cache.popHttpLatencies()).toEqual(expectedLatencyBuckets); expect(cache.popHttpLatencies()).toEqual({}); }); @@ -204,21 +199,13 @@ describe('TELEMETRY CACHE', () => { test('method latencies', () => { expect(cache.popLatencies()).toEqual({}); methods.forEach((method) => { - cache.recordLatency(method, 300); - cache.recordLatency(method, 400); - cache.recordLatency(method, 500); + latencies.forEach((latency) => { + cache.recordLatency(method, latency); + }); }); - const latencies = [300, 400, 500]; - const expectedLatencies = { 't': latencies, 'ts': latencies, 'tc': latencies, 'tcs': latencies, 'tr': latencies }; - expect(cache.popLatencies()).toEqual(expectedLatencies); - expect(cache.popLatencies()).toEqual({}); - - // MAX_LATENCY_BUCKET_COUNT === 23 - for (let i = 0; i < 100; i++) { - cache.recordLatency(TRACK, i); - } - expect(cache.popLatencies()).toEqual({ 'tr': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }); + const expectedLatencyBuckets = { 't': latencyBuckets, 'ts': latencyBuckets, 'tc': latencyBuckets, 'tcs': latencyBuckets, 'tr': latencyBuckets }; + expect(cache.popLatencies()).toEqual(expectedLatencyBuckets); expect(cache.popLatencies()).toEqual({}); }); From fe1c54dafec14cabb3774b0f7e9982d6f8c97bfa Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 13 Apr 2022 12:17:20 -0300 Subject: [PATCH 07/70] implement telemetry cache for redis and pluggable storage --- src/storages/KeyBuilderSS.ts | 63 ++++--------------- src/storages/__tests__/KeyBuilder.spec.ts | 17 +++-- src/storages/inRedis/TelemetryCacheInRedis.ts | 27 ++++++++ .../__tests__/TelemetryCacheInRedis.spec.ts | 34 ++++++++++ src/storages/inRedis/index.ts | 3 +- .../pluggable/TelemetryCachePluggable.ts | 24 +++++++ .../__tests__/TelemetryCachePluggable.spec.ts | 30 +++++++++ src/storages/pluggable/index.ts | 3 +- src/storages/types.ts | 15 ++++- 9 files changed, 152 insertions(+), 64 deletions(-) create mode 100644 src/storages/inRedis/TelemetryCacheInRedis.ts create mode 100644 src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts create mode 100644 src/storages/pluggable/TelemetryCachePluggable.ts create mode 100644 src/storages/pluggable/__tests__/TelemetryCachePluggable.spec.ts diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 2f8898c1..d766409d 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -1,9 +1,14 @@ import { KeyBuilder } from './KeyBuilder'; import { IMetadata } from '../dtos/types'; +import { Method } from '../sync/submitters/types'; -// NOT USED -// const everythingAfterCount = /count\.([^/]+)$/; -// const latencyMetricNameAndBucket = /latency\.([^/]+)\.bucket\.([0-9]+)$/; +const methodNames: Record = { + t: 'treatment', + ts: 'treatments', + tc: 'treatmentWithConfig', + tcs: 'treatmentsWithConfig', + tr: 'track' +}; export class KeyBuilderSS extends KeyBuilder { @@ -19,7 +24,7 @@ export class KeyBuilderSS extends KeyBuilder { } private buildVersionablePrefix() { - return `${this.prefix}/${this.metadata.s}/${this.metadata.i}`; + return `${this.metadata.s}/${this.metadata.n}/${this.metadata.i}`; } buildImpressionsKey() { @@ -30,58 +35,16 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.prefix}.events`; } - private buildLatencyKeyPrefix() { - return `${this.buildVersionablePrefix()}/latency`; + buildLatencyKey(method: Method, bucket: number) { + return `${this.prefix}.telemetry.latencies::${this.buildVersionablePrefix()}/${methodNames[method]}/${bucket}`; } - buildLatencyKey(metricName: string, bucketNumber: number | string) { - return `${this.buildLatencyKeyPrefix()}.${metricName}.bucket.${bucketNumber}`; + buildExceptionKey(method: Method) { + return `${this.prefix}.telemetry.exceptions::${this.buildVersionablePrefix()}/${methodNames[method]}`; } - buildCountKey(metricName: string) { - return `${this.buildVersionablePrefix()}/count.${metricName}`; - } - - // NOT USED - // buildGaugeKey(metricName: string) { - // return `${this.buildVersionablePrefix()}/gauge.${metricName}`; - // } - - // NOT USED - // searchPatternForCountKeys() { - // return `${this.buildVersionablePrefix()}/count.*`; - // } - searchPatternForSplitKeys() { return `${this.buildSplitKeyPrefix()}*`; } - // NOT USED - // searchPatternForLatency() { - // return `${this.buildLatencyKeyPrefix()}.*`; - // } - - // NOT USED - // extractCounterName(counterKey: string) { - // const m = counterKey.match(everythingAfterCount); - // if (m && m.length) { - // return m[1]; // everything after count - // } else { - // throw new Error('Invalid counter key provided'); - // } - // } - - // NOT USED - // extractLatencyMetricNameAndBucket(latencyKey: string) { - // const parts = latencyKey.match(latencyMetricNameAndBucket); - - // if (parts && parts.length > 2) { - // return { - // metricName: parts[1], - // bucketNumber: parts[2] - // }; - // } else { - // throw new Error('Invalid counter key provided'); - // } - // } } diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index e0b82ebe..196865d8 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -90,20 +90,17 @@ test('KEYS / events', () => { }); -test('KEYS / latency keys', () => { +test('KEYS / latency and exception keys (telemetry)', () => { const prefix = 'SPLITIO'; const metadata = { s: 'js-1234', i: '10-10-10-10', n: 'UNKNOWN' }; const builder = new KeyBuilderSS(prefix, metadata); - const metricName = 'unit testing metric name'; - const bucketNumber = '10'; + const methodName = 't'; // treatment + const bucketNumber = 10; - const expectedLatencyKey = `${prefix}/${metadata.s}/${metadata.i}/latency.${metricName}.bucket.${bucketNumber}`; + const expectedLatencyKey = `${prefix}.telemetry.latencies::${metadata.s}/${metadata.n}/${metadata.i}/treatment/${bucketNumber}`; + expect(builder.buildLatencyKey(methodName, bucketNumber)).toBe(expectedLatencyKey); - expect(builder.buildLatencyKey(metricName, bucketNumber) === expectedLatencyKey).toBe(true); - - // NOT USED - // const metricNameAndBucket = builder.extractLatencyMetricNameAndBucket(expectedLatencyKey); - // expect(metricName === metricNameAndBucket.metricName).toBe(true); - // expect(bucketNumber === metricNameAndBucket.bucketNumber).toBe(true); + const expectedExceptionKey = `${prefix}.telemetry.exceptions::${metadata.s}/${metadata.n}/${metadata.i}/treatment`; + expect(builder.buildExceptionKey(methodName)).toBe(expectedExceptionKey); }); diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts new file mode 100644 index 00000000..10510583 --- /dev/null +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -0,0 +1,27 @@ +import { ILogger } from '../../logger/types'; +import { Method } from '../../sync/submitters/types'; +import { KeyBuilderSS } from '../KeyBuilderSS'; +import { TelemetryCacheAsync } from '../types'; +import { findLatencyIndex } from '../findLatencyIndex'; +import { Redis } from 'ioredis'; + +export class TelemetryCacheInRedis implements TelemetryCacheAsync { + + /** + * Create a Telemetry cache that uses a storage wrapper. + * @param log Logger instance. + * @param keys Key builder. + * @param redis Redis client. + */ + constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly redis: Redis) { } + + recordLatency(method: Method, latencyMs: number) { + const [key, field] = this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs)).split('::'); + return this.redis.hincrby(key, field, 1); + } + recordException(method: Method) { + const [key, field] = this.keys.buildExceptionKey(method).split('::'); + return this.redis.hincrby(key, field, 1); + } + +} diff --git a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts new file mode 100644 index 00000000..7f2bf391 --- /dev/null +++ b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts @@ -0,0 +1,34 @@ +import Redis from 'ioredis'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { KeyBuilderSS } from '../../KeyBuilderSS'; +import { TelemetryCacheInRedis } from '../TelemetryCacheInRedis'; +import { fakeMetadata } from '../../pluggable/__tests__/ImpressionsCachePluggable.spec'; + +const prefix = 'telemetry_cache_ut'; +const exceptionKey = `${prefix}.telemetry.exceptions`; +const latencyKey = `${prefix}.telemetry.latencies`; +const fieldVersionablePrefix = `${fakeMetadata.s}/${fakeMetadata.n}/${fakeMetadata.i}`; + +test('TELEMETRY CACHE IN REDIS / `recordLatency` and `recordException`', async () => { + + const keysBuilder = new KeyBuilderSS(prefix, fakeMetadata); + const connection = new Redis(); + const cache = new TelemetryCacheInRedis(loggerMock, keysBuilder, connection); + + expect(await cache.recordException('tr')).toBe(1); + expect(await cache.recordException('tr')).toBe(2); + + expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe('2'); + expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatment')).toBe(null); + + expect(await cache.recordLatency('tr', 1.6)).toBe(1); + expect(await cache.recordLatency('tr', 1.6)).toBe(2); + + expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe('2'); + expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatment/2')).toBe(null); + + // Clean up then end. + await connection.hdel(exceptionKey, fieldVersionablePrefix + '/track'); + await connection.hdel(latencyKey, fieldVersionablePrefix + '/track/2'); + await connection.quit(); +}); diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index c22ee15d..84fe4860 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -7,6 +7,7 @@ import { SegmentsCacheInRedis } from './SegmentsCacheInRedis'; import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis'; import { EventsCacheInRedis } from './EventsCacheInRedis'; import { STORAGE_REDIS } from '../../utils/constants'; +import { TelemetryCacheInRedis } from './TelemetryCacheInRedis'; export interface InRedisStorageOptions { prefix?: string @@ -36,7 +37,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy segments: new SegmentsCacheInRedis(log, keys, redisClient), impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata), events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata), - // @TODO add new telemetry cache + telemetry: new TelemetryCacheInRedis(log, keys, redisClient), // When using REDIS we should: // 1- Disconnect from the storage diff --git a/src/storages/pluggable/TelemetryCachePluggable.ts b/src/storages/pluggable/TelemetryCachePluggable.ts new file mode 100644 index 00000000..d40767b3 --- /dev/null +++ b/src/storages/pluggable/TelemetryCachePluggable.ts @@ -0,0 +1,24 @@ +import { ILogger } from '../../logger/types'; +import { Method } from '../../sync/submitters/types'; +import { KeyBuilderSS } from '../KeyBuilderSS'; +import { IPluggableStorageWrapper, TelemetryCacheAsync } from '../types'; +import { findLatencyIndex } from '../findLatencyIndex'; + +export class TelemetryCachePluggable implements TelemetryCacheAsync { + + /** + * Create a Telemetry cache that uses a storage wrapper. + * @param log Logger instance. + * @param keys Key builder. + * @param wrapper Adapted wrapper storage. + */ + constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly wrapper: IPluggableStorageWrapper) { } + + recordLatency(method: Method, latencyMs: number) { + return this.wrapper.incr(this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs))); + } + recordException(method: Method) { + return this.wrapper.incr(this.keys.buildExceptionKey(method)); + } + +} diff --git a/src/storages/pluggable/__tests__/TelemetryCachePluggable.spec.ts b/src/storages/pluggable/__tests__/TelemetryCachePluggable.spec.ts new file mode 100644 index 00000000..dd6d3bbb --- /dev/null +++ b/src/storages/pluggable/__tests__/TelemetryCachePluggable.spec.ts @@ -0,0 +1,30 @@ +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { KeyBuilderSS } from '../../KeyBuilderSS'; +import { wrapperMockFactory } from './wrapper.mock'; +import { TelemetryCachePluggable } from '../TelemetryCachePluggable'; +import { fakeMetadata } from './ImpressionsCachePluggable.spec'; + +const prefix = 'telemetry_cache_ut'; +const exceptionKey = `${prefix}.telemetry.exceptions`; +const latencyKey = `${prefix}.telemetry.latencies`; +const fieldVersionablePrefix = `${fakeMetadata.s}/${fakeMetadata.n}/${fakeMetadata.i}`; + +test('TELEMETRY CACHE PLUGGABLE / `recordLatency` and `recordException`', async () => { + + const keysBuilder = new KeyBuilderSS(prefix, fakeMetadata); + const wrapper = wrapperMockFactory(); + const cache = new TelemetryCachePluggable(loggerMock, keysBuilder, wrapper); + + expect(await cache.recordException('tr')).toBe(1); + expect(await cache.recordException('tr')).toBe(2); + + expect(await wrapper.get(exceptionKey + '::' + fieldVersionablePrefix + '/track')).toBe('2'); + expect(await wrapper.get(exceptionKey + '::' + fieldVersionablePrefix + '/treatment')).toBe(null); + + expect(await cache.recordLatency('tr', 1.6)).toBe(1); + expect(await cache.recordLatency('tr', 1.6)).toBe(2); + + expect(await wrapper.get(latencyKey + '::' + fieldVersionablePrefix + '/track/2')).toBe('2'); + expect(await wrapper.get(latencyKey+ '::' + fieldVersionablePrefix + '/treatment/2')).toBe(null); + +}); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index e4062132..99fcf0e1 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -77,7 +77,8 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined, events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata), - // @TODO add telemetry cache when required + // @TODO not supported by Synchronizer yet + // telemetry: isPartialConsumer ? new TelemetryCacheInMemory() : new TelemetryCachePluggable(log, keys, wrapper), // Disconnect the underlying storage destroy() { diff --git a/src/storages/types.ts b/src/storages/types.ts index 26174555..9d7fa131 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -419,10 +419,21 @@ export interface TelemetryCacheSync extends TelemetryStorageConsumerSync, Teleme * Telemetry storage interface for consumer mode. * Methods are async because data is stored in Redis or a pluggable storage. */ -export interface TelemetryCacheAsync { - // @TODO + +export interface TelemetryEvaluationConsumerAsync { + popExceptions(): Promise; + popLatencies(): Promise; +} + +export interface TelemetryEvaluationProducerAsync { + recordLatency(method: Method, latencyMs: number): Promise; + recordException(method: Method): Promise; } +// ATM it only implements the producer API, used by the SDK. +// @TODO implement consumer API for JS Synchronizer. +export interface TelemetryCacheAsync extends TelemetryEvaluationProducerAsync { } + /** * Storages */ From ba6a24c85bb04d5a538853871236be3845664c51 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 13 Apr 2022 16:26:06 -0300 Subject: [PATCH 08/70] promise rejections must be handled --- src/storages/inRedis/TelemetryCacheInRedis.ts | 6 ++++-- src/storages/pluggable/TelemetryCachePluggable.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts index 10510583..f0068320 100644 --- a/src/storages/inRedis/TelemetryCacheInRedis.ts +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -17,11 +17,13 @@ export class TelemetryCacheInRedis implements TelemetryCacheAsync { recordLatency(method: Method, latencyMs: number) { const [key, field] = this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs)).split('::'); - return this.redis.hincrby(key, field, 1); + return this.redis.hincrby(key, field, 1) + .catch(() => { /* Handle rejections for telemetry */ }); } recordException(method: Method) { const [key, field] = this.keys.buildExceptionKey(method).split('::'); - return this.redis.hincrby(key, field, 1); + return this.redis.hincrby(key, field, 1) + .catch(() => { /* Handle rejections for telemetry */ }); } } diff --git a/src/storages/pluggable/TelemetryCachePluggable.ts b/src/storages/pluggable/TelemetryCachePluggable.ts index d40767b3..02d3be47 100644 --- a/src/storages/pluggable/TelemetryCachePluggable.ts +++ b/src/storages/pluggable/TelemetryCachePluggable.ts @@ -15,10 +15,12 @@ export class TelemetryCachePluggable implements TelemetryCacheAsync { constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly wrapper: IPluggableStorageWrapper) { } recordLatency(method: Method, latencyMs: number) { - return this.wrapper.incr(this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs))); + return this.wrapper.incr(this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs))) + .catch(() => { /* Handle rejections for telemetry */ }); } recordException(method: Method) { - return this.wrapper.incr(this.keys.buildExceptionKey(method)); + return this.wrapper.incr(this.keys.buildExceptionKey(method)) + .catch(() => { /* Handle rejections for telemetry */ }); } } From 8beaf44a028083ad6b6ec7db4134fbda1b0d9cec Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 18 Apr 2022 16:06:18 -0300 Subject: [PATCH 09/70] implementation --- src/sdkClient/client.ts | 13 ++++++++++- src/sdkFactory/index.ts | 9 +++++--- src/sdkFactory/types.ts | 6 +++-- src/trackers/telemetryTracker.ts | 38 ++++++++++++++++++++++++++++++++ src/trackers/types.ts | 10 +++++++++ src/utils/timeTracker/timer.ts | 6 ++--- 6 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 src/trackers/telemetryTracker.ts diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 44501624..1be10ae2 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -15,14 +15,18 @@ import { ISdkFactoryContext } from '../sdkFactory/types'; */ // @TODO missing time tracking to collect telemetry export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker } = params; + const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker, telemetryTracker } = params; const { log, mode } = settings; function getTreatment(key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, withConfig = false) { + const stopTelemetryTracker = telemetryTracker.start(withConfig ? 'tc' : 't'); + const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDTO[] = []; const treatment = processEvaluation(evaluationResult, splitName, key, attributes, withConfig, `getTreatment${withConfig ? 'withConfig' : ''}`, queue); impressionsTracker.track(queue, attributes); + + stopTelemetryTracker(queue[0] && queue[0].label); return treatment; }; @@ -36,6 +40,8 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl } function getTreatments(key: SplitIO.SplitKey, splitNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false) { + const stopTelemetryTracker = telemetryTracker.start(withConfig ? 'tcs' : 'ts'); + const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDTO[] = []; const treatments: Record = {}; @@ -43,6 +49,8 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl treatments[splitName] = processEvaluation(evaluationResults[splitName], splitName, key, attributes, withConfig, `getTreatments${withConfig ? 'withConfig' : ''}`, queue); }); impressionsTracker.track(queue, attributes); + + stopTelemetryTracker(queue[0] && queue[0].label); return treatments; }; @@ -101,6 +109,8 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl } function track(key: SplitIO.SplitKey, trafficTypeName: string, eventTypeId: string, value?: number, properties?: SplitIO.Properties, size = 1024) { + const stopTelemetryTracker = telemetryTracker.start('tr'); + const matchingKey = getMatching(key); const timestamp = Date.now(); const eventData: SplitIO.EventData = { @@ -115,6 +125,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl // This may be async but we only warn, we don't actually care if it is valid or not in terms of queueing the event. validateTrafficTypeExistance(log, readinessManager, storage.splits, mode, trafficTypeName, 'track'); + stopTelemetryTracker(); return eventTracker.track(eventData, size); } diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index e28bd1e2..e5c899b2 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -2,6 +2,7 @@ import { ISdkFactoryParams } from './types'; import { sdkReadinessManagerFactory } from '../readiness/sdkReadinessManager'; import { impressionsTrackerFactory } from '../trackers/impressionsTracker'; import { eventTrackerFactory } from '../trackers/eventTracker'; +import { telemetryTrackerFactory } from '../trackers/telemetryTracker'; import { IStorageFactoryParams, IStorageSync } from '../storages/types'; import { SplitIO } from '../types'; import { ISplitApi } from '../services/types'; @@ -24,11 +25,12 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory } = params; const log = settings.log; - // @TODO handle non-recoverable errors: not start sync, mark the SDK as destroyed, etc. + // @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid API Key, etc. + // On non-recoverable errors, we should mark the SDK as destroyed and not start synchronization. + // We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle. validateAndTrackApiKey(log, settings.core.authorizationKey); - // @TODO handle non-recoverable error, such as, `fetch` api not available, invalid API Key, etc. const sdkReadinessManager = sdkReadinessManagerFactory(log, platform.EventEmitter, settings.startup.readyTimeout); const readinessManager = sdkReadinessManager.readinessManager; @@ -76,12 +78,13 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const observer = impressionsObserverFactory && impressionsObserverFactory(); const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, integrationsManager, observer, storage.impressionCounts); const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager); + const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); // signal listener const signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi); // Sdk client and manager - const ctx = { eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }; + const ctx = { eventTracker, impressionsTracker, telemetryTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }; const clientMethod = sdkClientMethodFactory(ctx); const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 21968c46..8781ae0c 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -6,7 +6,7 @@ import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types'; import { IStorageAsync, IStorageSync, ISplitsCacheSync, ISplitsCacheAsync, IStorageFactoryParams } from '../storages/types'; import { ISyncManager, ISyncManagerFactoryParams } from '../sync/types'; import { IImpressionObserver } from '../trackers/impressionObserver/types'; -import { IImpressionsTracker, IEventTracker } from '../trackers/types'; +import { IImpressionsTracker, IEventTracker, ITelemetryTracker } from '../trackers/types'; import { SplitIO, ISettings, IEventEmitter } from '../types'; export interface ISdkFactoryContext { @@ -15,6 +15,7 @@ export interface ISdkFactoryContext { settings: ISettings impressionsTracker: IImpressionsTracker, eventTracker: IEventTracker, + telemetryTracker: ITelemetryTracker signalListener?: ISignalListener syncManager?: ISyncManager, } @@ -27,7 +28,8 @@ export interface IPlatform { getOptions?: () => object getFetch?: () => (IFetch | undefined) getEventSource?: () => (IEventSourceConstructor | undefined) - EventEmitter: new () => IEventEmitter + EventEmitter: new () => IEventEmitter, + now?: () => number } /** diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts new file mode 100644 index 00000000..ebccbdfa --- /dev/null +++ b/src/trackers/telemetryTracker.ts @@ -0,0 +1,38 @@ +import { TelemetryCacheAsync, TelemetryCacheSync } from '../storages/types'; +import { Method } from '../sync/submitters/types'; +import { EXCEPTION, SDK_NOT_READY } from '../utils/labels'; +import { ITelemetryTracker } from './types'; +import { timer } from '../utils/timeTracker/timer'; + +export function telemetryTrackerFactory( + telemetryCache?: TelemetryCacheAsync | TelemetryCacheSync, + now?: () => number +): ITelemetryTracker { + + if (telemetryCache && now) { + + return { + start(method: Method) { + const timeTracker = timer(now); + + return (label?: string) => { + telemetryCache.recordLatency(method, timeTracker()); + + switch (label) { + case EXCEPTION: + telemetryCache.recordException(method); break; + case SDK_NOT_READY: // @ts-ignore. TelemetryCacheAsync doesn't implement the method + telemetryCache?.recordNonReadyUsage(); + } + }; + } + }; + + } else { // If there is not `telemetryCache` or `now` time tracker, return a mock telemetry tracker + return { + start() { + return () => { }; + } + }; + } +} diff --git a/src/trackers/types.ts b/src/trackers/types.ts index dd00ec13..6f8673f9 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -1,4 +1,5 @@ import { SplitIO, ImpressionDTO } from '../types'; +import { Method } from '../sync/submitters/types'; import { IEventsCacheBase } from '../storages/types'; /** Events tracker */ @@ -18,3 +19,12 @@ export interface IImpressionsHandler { export interface IImpressionsTracker { track(impressions: ImpressionDTO[], attributes?: SplitIO.Attributes): void } + +/** Telemetry tracker */ + +export interface ITelemetryTracker { + /** + * Creates a telemetry evaluator tracker, to record Latencies, Exceptions and NonReadyUsage of client operations (getTreatments and track method calls) + */ + start(method: Method): (label?: string) => void +} diff --git a/src/utils/timeTracker/timer.ts b/src/utils/timeTracker/timer.ts index 425f993a..ee7ca8bb 100644 --- a/src/utils/timeTracker/timer.ts +++ b/src/utils/timeTracker/timer.ts @@ -1,7 +1,7 @@ -export function timer(now?: () => number) { - const st = now ? now() : Date.now(); +export function timer(now: () => number) { + const st = now(); return function stop() { - return Math.round(now ? now() : Date.now() - st); + return Math.round(now() - st); }; } From d990c2b60362935801e5f265d739d443519b5fa6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 18 Apr 2022 16:57:39 -0300 Subject: [PATCH 10/70] unit tests --- .../__tests__/telemetryTracker.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/trackers/__tests__/telemetryTracker.spec.ts diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts new file mode 100644 index 00000000..22c653d7 --- /dev/null +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -0,0 +1,43 @@ +import { EXCEPTION, SDK_NOT_READY } from '../../utils/labels'; +import { telemetryTrackerFactory } from '../telemetryTracker'; + +test('Telemetry Tracker', async () => { + + const fakeNow = jest.fn(() => { return Date.now(); }); + const fakeTelemetryCache = { + recordLatency: jest.fn(), + recordException: jest.fn(), + recordNonReadyUsage: jest.fn() + }; + + const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); + + let stopTracker = tracker.start('t'); + stopTracker(); + + stopTracker = tracker.start('ts'); + stopTracker(EXCEPTION); + + stopTracker = tracker.start('tc'); + stopTracker(SDK_NOT_READY); + + stopTracker = tracker.start('tcs'); + + await new Promise(res => setTimeout(res, 200)); + stopTracker(); + + expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordNonReadyUsage).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); + + const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; + expect(latency > 200 && latency < 250).toBeTruthy(); // last tracked latency is around 200 ms +}); + +test('Telemetry Tracker no-op', () => { + // The instance must implement the TelemetryTracker API even if no cache is provided + const tracker = telemetryTrackerFactory(); + + const stopTracker = tracker.start('tr'); + expect(stopTracker()).toBe(undefined); +}); From aaa58cc71f1aeeac0d558bcc4e24dae900d7da70 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 19 Apr 2022 12:58:40 -0300 Subject: [PATCH 11/70] using constants --- src/sdkClient/client.ts | 8 ++++---- src/trackers/telemetryTracker.ts | 4 ++-- src/trackers/types.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 1be10ae2..3b44e6e6 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -4,7 +4,7 @@ import { getMatching, getBucketing } from '../utils/key'; import { validateSplitExistance } from '../utils/inputValidation/splitExistance'; import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance'; import { SDK_NOT_READY } from '../utils/labels'; -import { CONTROL } from '../utils/constants'; +import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK } from '../utils/constants'; import { IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; @@ -19,7 +19,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl const { log, mode } = settings; function getTreatment(key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, withConfig = false) { - const stopTelemetryTracker = telemetryTracker.start(withConfig ? 'tc' : 't'); + const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT); const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDTO[] = []; @@ -40,7 +40,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl } function getTreatments(key: SplitIO.SplitKey, splitNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false) { - const stopTelemetryTracker = telemetryTracker.start(withConfig ? 'tcs' : 'ts'); + const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS); const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDTO[] = []; @@ -109,7 +109,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl } function track(key: SplitIO.SplitKey, trafficTypeName: string, eventTypeId: string, value?: number, properties?: SplitIO.Properties, size = 1024) { - const stopTelemetryTracker = telemetryTracker.start('tr'); + const stopTelemetryTracker = telemetryTracker.trackEval(TRACK); const matchingKey = getMatching(key); const timestamp = Date.now(); diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index ebccbdfa..a0c394e3 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -12,7 +12,7 @@ export function telemetryTrackerFactory( if (telemetryCache && now) { return { - start(method: Method) { + trackEval(method: Method) { const timeTracker = timer(now); return (label?: string) => { @@ -30,7 +30,7 @@ export function telemetryTrackerFactory( } else { // If there is not `telemetryCache` or `now` time tracker, return a mock telemetry tracker return { - start() { + trackEval() { return () => { }; } }; diff --git a/src/trackers/types.ts b/src/trackers/types.ts index 6f8673f9..4dee11cd 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -26,5 +26,5 @@ export interface ITelemetryTracker { /** * Creates a telemetry evaluator tracker, to record Latencies, Exceptions and NonReadyUsage of client operations (getTreatments and track method calls) */ - start(method: Method): (label?: string) => void + trackEval(method: Method): (label?: string) => void } From 77cfc4df36bb8bbc79ceab7de614c1599443cfb8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 21 Apr 2022 14:34:31 -0300 Subject: [PATCH 12/70] implementation --- src/services/__tests__/splitApi.spec.ts | 15 +++-- src/services/splitApi.ts | 35 +++++----- src/services/splitHttpClient.ts | 9 ++- src/services/types.ts | 12 ++-- .../__tests__/splitChangesUpdater.spec.ts | 3 +- .../AuthClient/__tests__/index.spec.ts | 3 +- .../__tests__/telemetryTracker.spec.ts | 66 +++++++++++++------ src/trackers/telemetryTracker.ts | 25 ++++--- src/trackers/types.ts | 7 +- src/types.ts | 16 ++++- .../__tests__/index.spec.ts | 2 + .../__tests__/settings.mocks.ts | 11 +++- src/utils/settingsValidation/index.ts | 2 + src/utils/settingsValidation/url.ts | 4 ++ src/utils/timeTracker/__tests__/index.spec.ts | 2 +- src/utils/timeTracker/index.ts | 2 +- 16 files changed, 145 insertions(+), 69 deletions(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index c014e678..3aa41d7e 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -5,6 +5,8 @@ import { settingsSplitApi } from '../../utils/settingsValidation/__tests__/setti const settingsWithRuntime = { ...settingsSplitApi, runtime: { ip: 'ip', hostname: 'hostname' } } as ISettings; +const telemetryTrackerMock = { trackHttp() { return () => { }; } }; + function assertHeaders(settings: ISettings, headers: Record) { expect(headers['Accept']).toBe('application/json'); expect(headers['Content-Type']).toBe('application/json'); @@ -20,7 +22,7 @@ describe('splitApi', () => { test.each([settingsSplitApi, settingsWithRuntime])('performs requests with expected headers', (settings) => { const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); - const splitApi = splitApiFactory(settings, { getFetch: () => fetchMock }); + const splitApi = splitApiFactory(settings, { getFetch: () => fetchMock }, telemetryTrackerMock); splitApi.fetchAuth(); assertHeaders(settings, fetchMock.mock.calls[0][1].headers); @@ -44,10 +46,9 @@ describe('splitApi', () => { splitApi.postTestImpressionsCount('fake-body'); assertHeaders(settings, fetchMock.mock.calls[6][1].headers); - // Deprecated - splitApi.postMetricsCounters('fake-body'); + splitApi.postMetricsConfig('fake-body'); assertHeaders(settings, fetchMock.mock.calls[7][1].headers); - splitApi.postMetricsTimes('fake-body'); + splitApi.postMetricsUsage('fake-body'); assertHeaders(settings, fetchMock.mock.calls[8][1].headers); fetchMock.mockClear(); @@ -55,7 +56,7 @@ describe('splitApi', () => { test('rejects requests if fetch Api is not provided', (done) => { - const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => undefined }); + const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => undefined }, telemetryTrackerMock); // Invoking any Service method, returns a rejected promise with Split error splitApi.fetchAuth().catch(error => { @@ -68,7 +69,7 @@ describe('splitApi', () => { test('performs requests with overwritten headers', () => { const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); - const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }); + const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }, telemetryTrackerMock); const newHeaders = { SplitSDKVersion: 'newVersion', SplitSDKMachineIP: 'newIp', SplitSDKMachineName: 'newHostname' }; const expectedHeaders = { ...settingsWithRuntime, version: newHeaders.SplitSDKVersion, runtime: { ip: newHeaders.SplitSDKMachineIP, hostname: newHeaders.SplitSDKMachineName } }; @@ -83,7 +84,7 @@ describe('splitApi', () => { test('performs APIs health service check', (done) => { const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); - const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }); + const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }, telemetryTrackerMock); splitApi.getSdkAPIHealthCheck().then((res) => { expect(res).toEqual(true); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index dbbcb953..e713c4f2 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -3,6 +3,8 @@ import { ISettings } from '../types'; import { splitHttpClientFactory } from './splitHttpClient'; import { ISplitApi } from './types'; import { objectAssign } from '../utils/lang/objectAssign'; +import { ITelemetryTracker } from '../trackers/types'; +import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MY_SEGMENT } from '../utils/constants'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -18,7 +20,8 @@ function userKeyToQueryParam(userKey: string) { */ export function splitApiFactory( settings: Pick, - platform: Pick + platform: Pick, + telemetryTracker: ITelemetryTracker ): ISplitApi { const urls = settings.urls; @@ -29,12 +32,12 @@ export function splitApiFactory( return { getSdkAPIHealthCheck() { const url = `${urls.sdk}/version`; - return splitHttpClient(url).then(() => true).catch(() => false); + return splitHttpClient(() => { }, url).then(() => true).catch(() => false); }, getEventsAPIHealthCheck() { const url = `${urls.events}/version`; - return splitHttpClient(url).then(() => true).catch(() => false); + return splitHttpClient(() => { }, url).then(() => true).catch(() => false); }, fetchAuth(userMatchingKeys?: string[]) { @@ -44,17 +47,17 @@ export function splitApiFactory( if (queryParams) // accounting the possibility that `userKeys` and thus `queryParams` are empty url += '?' + queryParams; } - return splitHttpClient(url); + return splitHttpClient(telemetryTracker.trackHttp(TOKEN), url); }, fetchSplitChanges(since: number, noCache?: boolean) { const url = `${urls.sdk}/splitChanges?since=${since}${filterQueryString || ''}`; - return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined); + return splitHttpClient(telemetryTracker.trackHttp(SPLITS), url, noCache ? noCacheHeaderOptions : undefined); }, fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean) { const url = `${urls.sdk}/segmentChanges/${segmentName}?since=${since}`; - return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined); + return splitHttpClient(telemetryTracker.trackHttp(SEGMENT), url, noCache ? noCacheHeaderOptions : undefined); }, fetchMySegments(userMatchingKey: string, noCache?: boolean) { @@ -65,7 +68,7 @@ export function splitApiFactory( * - match user keys with special characters. E.g.: 'foo%bar', 'foo/bar' */ const url = `${urls.sdk}/mySegments/${encodeURIComponent(userMatchingKey)}`; - return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined); + return splitHttpClient(telemetryTracker.trackHttp(MY_SEGMENT), url, noCache ? noCacheHeaderOptions : undefined); }, /** @@ -76,7 +79,7 @@ export function splitApiFactory( */ postEventsBulk(body: string, headers?: Record) { const url = `${urls.events}/events/bulk`; - return splitHttpClient(url, { method: 'POST', body, headers }); + return splitHttpClient(telemetryTracker.trackHttp(EVENTS), url, { method: 'POST', body, headers }); }, /** @@ -87,7 +90,7 @@ export function splitApiFactory( */ postTestImpressionsBulk(body: string, headers?: Record) { const url = `${urls.events}/testImpressions/bulk`; - return splitHttpClient(url, { + return splitHttpClient(telemetryTracker.trackHttp(IMPRESSIONS), url, { // Adding extra headers to send impressions in OPTIMIZED or DEBUG modes. method: 'POST', body, headers: objectAssign({ SplitSDKImpressionsMode }, headers) }); @@ -101,17 +104,17 @@ export function splitApiFactory( */ postTestImpressionsCount(body: string, headers?: Record) { const url = `${urls.events}/testImpressions/count`; - return splitHttpClient(url, { method: 'POST', body, headers }); + return splitHttpClient(telemetryTracker.trackHttp(IMPRESSIONS_COUNT), url, { method: 'POST', body, headers }); }, - postMetricsCounters(body: string) { - const url = `${urls.events}/metrics/counters`; - return splitHttpClient(url, { method: 'POST', body }, true); + postMetricsConfig(body: string) { + const url = `${urls.telemetry}/metrics/config`; + return splitHttpClient(telemetryTracker.trackHttp(TELEMETRY), url, { method: 'POST', body }, true); }, - postMetricsTimes(body: string) { - const url = `${urls.events}/metrics/times`; - return splitHttpClient(url, { method: 'POST', body }, true); + postMetricsUsage(body: string) { + const url = `${urls.telemetry}/metrics/usage`; + return splitHttpClient(telemetryTracker.trackHttp(TELEMETRY), url, { method: 'POST', body }, true); } }; } diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index d9ec90f7..7ade55b6 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -1,4 +1,4 @@ -import { IFetch, IRequestOptions, IResponse, ISplitHttpClient } from './types'; +import { IFetch, IRequestOptions, IResponse, ISplitHttpClient, NetworkError } from './types'; import { objectAssign } from '../utils/lang/objectAssign'; import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants'; import { ISettings } from '../types'; @@ -31,7 +31,7 @@ export function splitHttpClientFactory(settings: Pick { + return function httpClient(tracker: (error?: NetworkError) => void, url: string, reqOpts: IRequestOptions = {}, logErrorsAsInfo: boolean = false): Promise { const request = objectAssign({ headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers, @@ -46,6 +46,7 @@ export function splitHttpClientFactory(settings: Pick Promise.reject({ response, message })); } + tracker(); return response; }) .catch(error => { @@ -68,9 +69,11 @@ export function splitHttpClientFactory(settings: Pick Promise // IFetch specialization export type IHealthCheckAPI = () => Promise -export type ISplitHttpClient = (url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise +export type ISplitHttpClient = (tracker: (error?: NetworkError) => void, url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise export type IFetchAuth = (userKeys?: string[]) => Promise @@ -45,9 +47,9 @@ export type IPostTestImpressionsBulk = (body: string, headers?: Record) => Promise -export type IPostMetricsCounters = (body: string) => Promise +export type IPostMetricsConfig = (body: string) => Promise -export type IPostMetricsTimes = (body: string) => Promise +export type IPostMetricsUsage = (body: string) => Promise export interface ISplitApi { getSdkAPIHealthCheck: IHealthCheckAPI @@ -59,8 +61,8 @@ export interface ISplitApi { postEventsBulk: IPostEventsBulk postTestImpressionsBulk: IPostTestImpressionsBulk postTestImpressionsCount: IPostTestImpressionsCount - postMetricsCounters: IPostMetricsCounters - postMetricsTimes: IPostMetricsTimes + postMetricsConfig: IPostMetricsConfig + postMetricsUsage: IPostMetricsUsage } // Minimal version of EventSource API used by the SDK diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 60cc9b52..a1cb72b6 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -10,6 +10,7 @@ import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import { EventEmitter } from '../../../../utils/MinEvents'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; +import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; const activeSplitWithSegments = { name: 'Split1', @@ -57,7 +58,7 @@ test('splitChangesUpdater / compute splits mutation', () => { test('splitChangesUpdater / factory', (done) => { fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore - const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock, EventEmitter }); + const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock, EventEmitter }, telemetryTrackerFactory()); const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); const splitsCache = new SplitsCacheInMemory(); diff --git a/src/sync/streaming/AuthClient/__tests__/index.spec.ts b/src/sync/streaming/AuthClient/__tests__/index.spec.ts index 70604400..d27f5b4d 100644 --- a/src/sync/streaming/AuthClient/__tests__/index.spec.ts +++ b/src/sync/streaming/AuthClient/__tests__/index.spec.ts @@ -4,13 +4,14 @@ import { authDataResponseSample, authDataSample, jwtSampleInvalid, jwtSampleNoCh import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import { EventEmitter } from '../../../../utils/MinEvents'; +import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; // module to test import { authenticateFactory, hashUserKey } from '../index'; const authorizationKey = settingsSplitApi.core.authorizationKey; const authUrl = settingsSplitApi.urls.auth; // @ts-ignore -const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock, EventEmitter }); +const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock, EventEmitter }, telemetryTrackerFactory()); const authenticate = authenticateFactory(splitApi.fetchAuth); test('hashUserKey', () => { diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index 22c653d7..5a99446e 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -1,43 +1,71 @@ import { EXCEPTION, SDK_NOT_READY } from '../../utils/labels'; import { telemetryTrackerFactory } from '../telemetryTracker'; -test('Telemetry Tracker', async () => { +describe('Telemetry Tracker', () => { const fakeNow = jest.fn(() => { return Date.now(); }); const fakeTelemetryCache = { recordLatency: jest.fn(), recordException: jest.fn(), - recordNonReadyUsage: jest.fn() + recordNonReadyUsage: jest.fn(), + recordHttpLatency: jest.fn(), + recordHttpError: jest.fn(), }; - + // @ts-ignore const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); - let stopTracker = tracker.start('t'); - stopTracker(); + test('trackEval', async () => { + + let stopTracker = tracker.trackEval('t'); + stopTracker(); + + stopTracker = tracker.trackEval('ts'); + stopTracker(EXCEPTION); + + stopTracker = tracker.trackEval('tc'); + stopTracker(SDK_NOT_READY); + + stopTracker = tracker.trackEval('tcs'); - stopTracker = tracker.start('ts'); - stopTracker(EXCEPTION); + await new Promise(res => setTimeout(res, 50)); + stopTracker(); - stopTracker = tracker.start('tc'); - stopTracker(SDK_NOT_READY); + expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordNonReadyUsage).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); - stopTracker = tracker.start('tcs'); + const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; + expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms + }); - await new Promise(res => setTimeout(res, 200)); - stopTracker(); + test('trackHttp', async () => { - expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); - expect(fakeTelemetryCache.recordNonReadyUsage).toBeCalledTimes(1); - expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); + let stopTracker = tracker.trackHttp('ev'); + stopTracker(); - const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; - expect(latency > 200 && latency < 250).toBeTruthy(); // last tracked latency is around 200 ms + stopTracker = tracker.trackHttp('ic'); // @ts-ignore + stopTracker({ statusCode: 400 }); + + stopTracker = tracker.trackHttp('im'); + + await new Promise(res => setTimeout(res, 50)); + stopTracker(); + + expect(fakeTelemetryCache.recordHttpError).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordHttpLatency).toBeCalledTimes(3); + + const latency = fakeTelemetryCache.recordHttpLatency.mock.calls[2][1]; + expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms + }); }); test('Telemetry Tracker no-op', () => { // The instance must implement the TelemetryTracker API even if no cache is provided const tracker = telemetryTrackerFactory(); - const stopTracker = tracker.start('tr'); - expect(stopTracker()).toBe(undefined); + const stopEvalTracker = tracker.trackEval('tr'); + expect(stopEvalTracker()).toBe(undefined); + + const stopHttpTracker = tracker.trackHttp('ev'); + expect(stopHttpTracker()).toBe(undefined); }); diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index a0c394e3..eddd2fc4 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -1,11 +1,12 @@ -import { TelemetryCacheAsync, TelemetryCacheSync } from '../storages/types'; -import { Method } from '../sync/submitters/types'; +import { TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; +import { Method, OperationType } from '../sync/submitters/types'; import { EXCEPTION, SDK_NOT_READY } from '../utils/labels'; import { ITelemetryTracker } from './types'; import { timer } from '../utils/timeTracker/timer'; +import { NetworkError } from '../services/types'; export function telemetryTrackerFactory( - telemetryCache?: TelemetryCacheAsync | TelemetryCacheSync, + telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync, now?: () => number ): ITelemetryTracker { @@ -25,14 +26,22 @@ export function telemetryTrackerFactory( telemetryCache?.recordNonReadyUsage(); } }; - } + }, + trackHttp(operation: OperationType) { + const timeTracker = timer(now); + + return (error?: NetworkError) => { + (telemetryCache as TelemetryCacheSync).recordHttpLatency(operation, timeTracker()); + if (error && error.statusCode) (telemetryCache as TelemetryCacheSync).recordHttpError(operation, error.statusCode); + }; + }, }; - } else { // If there is not `telemetryCache` or `now` time tracker, return a mock telemetry tracker + } else { // If there is not `telemetryCache` or `now` time tracker, return a no-op telemetry tracker + const noopTrack = () => () => { }; return { - trackEval() { - return () => { }; - } + trackEval: noopTrack, + trackHttp: noopTrack }; } } diff --git a/src/trackers/types.ts b/src/trackers/types.ts index 4dee11cd..57745177 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -1,6 +1,7 @@ import { SplitIO, ImpressionDTO } from '../types'; -import { Method } from '../sync/submitters/types'; +import { Method, OperationType } from '../sync/submitters/types'; import { IEventsCacheBase } from '../storages/types'; +import { NetworkError } from '../services/types'; /** Events tracker */ @@ -27,4 +28,8 @@ export interface ITelemetryTracker { * Creates a telemetry evaluator tracker, to record Latencies, Exceptions and NonReadyUsage of client operations (getTreatments and track method calls) */ trackEval(method: Method): (label?: string) => void + /** + * Creates a telemetry runtime tracker, to record Latencies and Exceptions of HTTP requests + */ + trackHttp(method: OperationType): (error?: NetworkError) => void } diff --git a/src/types.ts b/src/types.ts index 7d523223..6c5ba33e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,12 +93,16 @@ export interface ISettings { eventsFirstPushWindow: number }, readonly storage: IStorageSyncFactory | IStorageAsyncFactory, - readonly integrations?: Array<(params: IIntegrationFactoryParams) => IIntegration | void>, + readonly integrations: Array<{ + readonly type: string, + (params: IIntegrationFactoryParams): IIntegration | void + }>, readonly urls: { events: string, sdk: string, auth: string, - streaming: string + streaming: string, + telemetry: string }, readonly debug: boolean | LogLevel | ILogger, readonly version: string, @@ -664,7 +668,13 @@ export namespace SplitIO { * @property {string} streaming * @default 'https://streaming.split.io' */ - streaming?: string + streaming?: string, + /** + * String property to override the base URL where the SDK will post telemetry data. + * @property {string} telemetry + * @default 'https://telemetry.split.io' + */ + telemetry?: string }; /** * SplitFilter type. diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 3f799970..7804a011 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -37,6 +37,7 @@ describe('settingsValidation', () => { events: 'https://events.split.io/api', auth: 'https://auth.split.io/api', streaming: 'https://streaming.split.io', + telemetry: 'https://telemetry.split.io', }); expect(settings.sync.impressionsMode).toBe(OPTIMIZED); }); @@ -73,6 +74,7 @@ describe('settingsValidation', () => { events: 'events-url', auth: 'auth-url', streaming: 'streaming-url', + telemetry: 'telemetry-url', }; const settings = settingsValidation({ diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index ff160297..bac178e7 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -37,6 +37,9 @@ export const settingsWithKeyObject = { log: loggerMock }; +function NoopIntegration() {} +NoopIntegration.type = 'NoopIntegration'; + export const fullSettings: ISettings = { core: { authorizationKey: 'aaaabbbbcccc1234', @@ -63,7 +66,7 @@ export const fullSettings: ISettings = { }, features: 'path/to/file', storage: InMemoryStorageCSFactory, - integrations: [() => { }], // A no-op integration + integrations: [NoopIntegration], mode: 'standalone', debug: false, streamingEnabled: true, @@ -86,7 +89,8 @@ export const fullSettings: ISettings = { events: 'events', sdk: 'sdk', auth: 'auth', - streaming: 'streaming' + streaming: 'streaming', + telemetry: 'telemetry' }, log: loggerMock, userConsent: undefined @@ -110,7 +114,8 @@ export const settingsSplitApi = { events: 'events', sdk: 'sdk', auth: 'auth', - streaming: 'streaming' + streaming: 'streaming', + telemetry: 'telemetry' }, sync: { impressionsMode: 'DEBUG' diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 5bb1125b..d75f8aa9 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -55,6 +55,8 @@ const base = { auth: 'https://auth.split.io/api', // Streaming Server streaming: 'https://streaming.split.io', + // Telemetry Server + telemetry: 'https://telemetry.split.io', }, // Defines which kind of storage we should instanciate. diff --git a/src/utils/settingsValidation/url.ts b/src/utils/settingsValidation/url.ts index 796f5423..142d2dc4 100644 --- a/src/utils/settingsValidation/url.ts +++ b/src/utils/settingsValidation/url.ts @@ -1,5 +1,6 @@ import { ISettings } from '../../types'; +const telemetryEndpointMatcher = /^\/metrics\/(config|usage)/; const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/; const authEndpointMatcher = /^\/v2\/auth/; const streamingEndpointMatcher = /^\/(sse|event-stream)/; @@ -13,6 +14,9 @@ const streamingEndpointMatcher = /^\/(sse|event-stream)/; * @return complete url */ export function url(settings: ISettings, target: string) { + if (telemetryEndpointMatcher.test(target)) { + return `${settings.urls.telemetry}${target}`; + } if (eventsEndpointMatcher.test(target)) { return `${settings.urls.events}${target}`; } diff --git a/src/utils/timeTracker/__tests__/index.spec.ts b/src/utils/timeTracker/__tests__/index.spec.ts index e4036fd8..3b405264 100644 --- a/src/utils/timeTracker/__tests__/index.spec.ts +++ b/src/utils/timeTracker/__tests__/index.spec.ts @@ -5,7 +5,7 @@ import { IResponse } from '../../../services/types'; test('TIMER / should count the time between two tasks', (done) => { const timerDuration = Math.floor(Math.random() * 1000); // In millis - const stopTimer = timer(); + const stopTimer = timer(Date.now); setTimeout(() => { const elapsedTime = stopTimer(); diff --git a/src/utils/timeTracker/index.ts b/src/utils/timeTracker/index.ts index a09289b8..433a8d52 100644 --- a/src/utils/timeTracker/index.ts +++ b/src/utils/timeTracker/index.ts @@ -148,7 +148,7 @@ export const TrackerAPI = { * @param {Promise} promise - (optional) The promise we are tracking. * @return {Function | Promise} The stop function for this specific task or the promise received with the callbacks registered. */ - start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now?: () => number): Promise | (() => number) { + start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now: () => number = Date.now): Promise | (() => number) { const taskUniqueId = uniqueId(); const taskCollector = getCollectorForTask(task, collectors); let result; From 3e63eef2f00cb7da0018e6afb2c17455f3586946 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 21 Apr 2022 14:46:23 -0300 Subject: [PATCH 13/70] fixes on tests --- .../__tests__/telemetryTracker.spec.ts | 42 ++++++++++--------- src/trackers/telemetryTracker.ts | 11 +++-- src/utils/timeTracker/__tests__/index.spec.ts | 2 +- src/utils/timeTracker/index.ts | 2 +- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index 22c653d7..dab33add 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -1,43 +1,47 @@ import { EXCEPTION, SDK_NOT_READY } from '../../utils/labels'; import { telemetryTrackerFactory } from '../telemetryTracker'; -test('Telemetry Tracker', async () => { +describe('Telemetry Tracker', () => { const fakeNow = jest.fn(() => { return Date.now(); }); const fakeTelemetryCache = { recordLatency: jest.fn(), recordException: jest.fn(), - recordNonReadyUsage: jest.fn() + recordNonReadyUsage: jest.fn(), }; - + // @ts-ignore const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); - let stopTracker = tracker.start('t'); - stopTracker(); + test('trackEval', async () => { + + let stopTracker = tracker.trackEval('t'); + stopTracker(); + + stopTracker = tracker.trackEval('ts'); + stopTracker(EXCEPTION); - stopTracker = tracker.start('ts'); - stopTracker(EXCEPTION); + stopTracker = tracker.trackEval('tc'); + stopTracker(SDK_NOT_READY); - stopTracker = tracker.start('tc'); - stopTracker(SDK_NOT_READY); + stopTracker = tracker.trackEval('tcs'); - stopTracker = tracker.start('tcs'); + await new Promise(res => setTimeout(res, 50)); + stopTracker(); - await new Promise(res => setTimeout(res, 200)); - stopTracker(); + expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordNonReadyUsage).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); - expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); - expect(fakeTelemetryCache.recordNonReadyUsage).toBeCalledTimes(1); - expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); + const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; + expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms + }); - const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; - expect(latency > 200 && latency < 250).toBeTruthy(); // last tracked latency is around 200 ms }); test('Telemetry Tracker no-op', () => { // The instance must implement the TelemetryTracker API even if no cache is provided const tracker = telemetryTrackerFactory(); - const stopTracker = tracker.start('tr'); - expect(stopTracker()).toBe(undefined); + const stopEvalTracker = tracker.trackEval('tr'); + expect(stopEvalTracker()).toBe(undefined); }); diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index a0c394e3..a40a051f 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -1,11 +1,11 @@ -import { TelemetryCacheAsync, TelemetryCacheSync } from '../storages/types'; +import { TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; import { Method } from '../sync/submitters/types'; import { EXCEPTION, SDK_NOT_READY } from '../utils/labels'; import { ITelemetryTracker } from './types'; import { timer } from '../utils/timeTracker/timer'; export function telemetryTrackerFactory( - telemetryCache?: TelemetryCacheAsync | TelemetryCacheSync, + telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync, now?: () => number ): ITelemetryTracker { @@ -28,11 +28,10 @@ export function telemetryTrackerFactory( } }; - } else { // If there is not `telemetryCache` or `now` time tracker, return a mock telemetry tracker + } else { // If there is not `telemetryCache` or `now` time tracker, return a no-op telemetry tracker + const noopTrack = () => () => { }; return { - trackEval() { - return () => { }; - } + trackEval: noopTrack }; } } diff --git a/src/utils/timeTracker/__tests__/index.spec.ts b/src/utils/timeTracker/__tests__/index.spec.ts index e4036fd8..3b405264 100644 --- a/src/utils/timeTracker/__tests__/index.spec.ts +++ b/src/utils/timeTracker/__tests__/index.spec.ts @@ -5,7 +5,7 @@ import { IResponse } from '../../../services/types'; test('TIMER / should count the time between two tasks', (done) => { const timerDuration = Math.floor(Math.random() * 1000); // In millis - const stopTimer = timer(); + const stopTimer = timer(Date.now); setTimeout(() => { const elapsedTime = stopTimer(); diff --git a/src/utils/timeTracker/index.ts b/src/utils/timeTracker/index.ts index a09289b8..433a8d52 100644 --- a/src/utils/timeTracker/index.ts +++ b/src/utils/timeTracker/index.ts @@ -148,7 +148,7 @@ export const TrackerAPI = { * @param {Promise} promise - (optional) The promise we are tracking. * @return {Function | Promise} The stop function for this specific task or the promise received with the callbacks registered. */ - start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now?: () => number): Promise | (() => number) { + start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now: () => number = Date.now): Promise | (() => number) { const taskUniqueId = uniqueId(); const taskCollector = getCollectorForTask(task, collectors); let result; From a088b8cc3a39f684acead6c373ed9f1bfd120436 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 25 Apr 2022 12:04:52 -0300 Subject: [PATCH 14/70] added telemetryRefreshRate config param validation --- src/consent/sdkUserConsent.ts | 2 +- src/logger/messages/info.ts | 4 ++-- src/types.ts | 20 ++++++++++++++++++- .../__tests__/index.spec.ts | 6 ++++-- .../__tests__/settings.mocks.ts | 2 +- src/utils/settingsValidation/index.ts | 10 +++++++--- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/consent/sdkUserConsent.ts b/src/consent/sdkUserConsent.ts index cd7d387f..ac8af3d8 100644 --- a/src/consent/sdkUserConsent.ts +++ b/src/consent/sdkUserConsent.ts @@ -23,7 +23,7 @@ export function createUserConsentAPI(params: ISdkFactoryContext) { setStatus(consent: unknown) { // validate input param if (!isBoolean(consent)) { - log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']); + log.warn(ERROR_NOT_BOOLEAN, ['UserConsent.setStatus']); return false; } diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 96703540..dd633639 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -14,8 +14,8 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.NEW_FACTORY, 'New Split SDK instance created.'], [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'], [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'], - [c.USER_CONSENT_UPDATED, 'setUserConsent: consent status changed from %s to %s.'], - [c.USER_CONSENT_NOT_UPDATED, 'setUserConsent: call had no effect because it was the current consent status (%s).'], + [c.USER_CONSENT_UPDATED, 'UserConsent: consent status changed from %s to %s.'], + [c.USER_CONSENT_NOT_UPDATED, 'UserConsent: call had no effect because it was the current consent status (%s).'], [c.USER_CONSENT_INITIAL, 'Starting the SDK with %s user consent. No data will be sent.'], // synchronizer diff --git a/src/types.ts b/src/types.ts index 6c330eec..102738b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -79,7 +79,11 @@ export interface ISettings { featuresRefreshRate: number, impressionsRefreshRate: number, impressionsQueueSize: number, - metricsRefreshRate: number, + /** + * @deprecated + */ + metricsRefreshRate?: number, + telemetryRefreshRate: number, segmentsRefreshRate: number, offlineRefreshRate: number, eventsPushRate: number, @@ -273,8 +277,15 @@ interface INodeBasicSettings extends ISharedSettings { * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. * @property {number} metricsRefreshRate * @default 120 + * @deprecated This parameter is ignored now. */ metricsRefreshRate?: number, + /** + * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. + * @property {number} telemetryRefreshRate + * @default 3600 + */ + telemetryRefreshRate?: number, /** * The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. * @property {number} segmentsRefreshRate @@ -794,8 +805,15 @@ export namespace SplitIO { * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. * @property {number} metricsRefreshRate * @default 120 + * @deprecated This parameter is ignored now. */ metricsRefreshRate?: number, + /** + * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. + * @property {number} telemetryRefreshRate + * @default 3600 + */ + telemetryRefreshRate?: number, /** * The SDK polls Split servers for changes to segment definitions. This parameter controls this polling period in seconds. * @property {number} segmentsRefreshRate diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 3f799970..01c3740f 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -90,8 +90,9 @@ describe('settingsValidation', () => { const locatorSchedulerFeaturesRefreshRate = _.property('scheduler.featuresRefreshRate'); const locatorSchedulerSegmentsRefreshRate = _.property('scheduler.segmentsRefreshRate'); - const locatorSchedulerMetricsRefreshRate = _.property('scheduler.metricsRefreshRate'); + const locatorSchedulerTelemetryRefreshRate = _.property('scheduler.telemetryRefreshRate'); const locatorSchedulerImpressionsRefreshRate = _.property('scheduler.impressionsRefreshRate'); + const locatorSchedulerEventsPushRate = _.property('scheduler.eventsPushRate'); const locatorUrlsSDK = _.property('urls.sdk'); const locatorUrlsEvents = _.property('urls.events'); @@ -125,8 +126,9 @@ describe('settingsValidation', () => { expect(locatorSchedulerFeaturesRefreshRate(settings) !== undefined).toBe(true); // scheduler.featuresRefreshRate should be present expect(locatorSchedulerSegmentsRefreshRate(settings) !== undefined).toBe(true); // scheduler.segmentsRefreshRate should be present - expect(locatorSchedulerMetricsRefreshRate(settings)).toBe(120 * 1000); // scheduler.metricsRefreshRate should be present + expect(locatorSchedulerTelemetryRefreshRate(settings)).toBe(3600 * 1000); // scheduler.telemetryRefreshRate should be present expect(locatorSchedulerImpressionsRefreshRate(settings) !== undefined).toBe(true); // scheduler.impressionsRefreshRate should be present + expect(locatorSchedulerEventsPushRate(settings) !== undefined).toBe(true); // scheduler.eventsPushRate should be present expect(locatorUrlsSDK(settings) !== undefined).toBe(true); // urls.sdk should be present expect(locatorUrlsEvents(settings) !== undefined).toBe(true); // urls.events should be present diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index ff160297..7f320c86 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -47,7 +47,7 @@ export const fullSettings: ISettings = { scheduler: { featuresRefreshRate: 1, impressionsRefreshRate: 1, - metricsRefreshRate: 1, + telemetryRefreshRate: 1, segmentsRefreshRate: 1, offlineRefreshRate: 1, eventsPushRate: 1, diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 5bb1125b..2b804980 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -30,8 +30,8 @@ const base = { featuresRefreshRate: 30, // fetch segments updates each 60 sec segmentsRefreshRate: 60, - // publish metrics each 120 sec - metricsRefreshRate: 120, + // publish telemetry stats each 3600 secs (1 hour) + telemetryRefreshRate: 3600, // publish evaluations each 60 sec impressionsRefreshRate: 60, // fetch offline changes each 15 sec @@ -113,10 +113,14 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV const { scheduler, startup } = withDefaults; scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate); scheduler.segmentsRefreshRate = fromSecondsToMillis(scheduler.segmentsRefreshRate); - scheduler.metricsRefreshRate = fromSecondsToMillis(scheduler.metricsRefreshRate); scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate); scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate); scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate); + // @TODO handle min 60 secs + scheduler.telemetryRefreshRate = fromSecondsToMillis(scheduler.telemetryRefreshRate); + + // Log deprecation for old telemetry param + if (scheduler.metricsRefreshRate) log.warn('`metricsRefreshRate` will be deprecated soon. For configuring telemetry rates, update `telemetryRefreshRate` value in configs'); // Startup periods startup.requestTimeoutBeforeReady = fromSecondsToMillis(startup.requestTimeoutBeforeReady); From ac6b8e837487a637a7dafe69d4e8a7c6a1087a9e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 25 Apr 2022 15:38:16 -0300 Subject: [PATCH 15/70] update: don't call recordLatency if calling recordException --- src/trackers/telemetryTracker.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index a40a051f..eb3b40dd 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -13,17 +13,17 @@ export function telemetryTrackerFactory( return { trackEval(method: Method) { - const timeTracker = timer(now); + const stopTimer = timer(now); return (label?: string) => { - telemetryCache.recordLatency(method, timeTracker()); - switch (label) { case EXCEPTION: - telemetryCache.recordException(method); break; + telemetryCache.recordException(method); + return; // Don't track latency on exceptions case SDK_NOT_READY: // @ts-ignore. TelemetryCacheAsync doesn't implement the method telemetryCache?.recordNonReadyUsage(); } + telemetryCache.recordLatency(method, stopTimer()); }; } }; From 837101a77bc3141cacbe5e25494af438dcca33ff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 25 Apr 2022 16:09:38 -0300 Subject: [PATCH 16/70] record latency for track method --- src/sdkClient/client.ts | 13 +++++++++++-- src/trackers/__tests__/telemetryTracker.spec.ts | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 3b44e6e6..59226c12 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -125,8 +125,17 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl // This may be async but we only warn, we don't actually care if it is valid or not in terms of queueing the event. validateTrafficTypeExistance(log, readinessManager, storage.splits, mode, trafficTypeName, 'track'); - stopTelemetryTracker(); - return eventTracker.track(eventData, size); + const result = eventTracker.track(eventData, size); + + if (thenable(result)) { + return result.then((result) => { + stopTelemetryTracker(); + return result; + }); + } else { + stopTelemetryTracker(); + return result; + } } return { diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index dab33add..ea1af1bf 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -30,9 +30,9 @@ describe('Telemetry Tracker', () => { expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); expect(fakeTelemetryCache.recordNonReadyUsage).toBeCalledTimes(1); - expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); + expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(3); - const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; + const latency = fakeTelemetryCache.recordLatency.mock.calls[2][1]; expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms }); From 2f4ac69ae171fdc068fa7d06c2319e45a1347a75 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 25 Apr 2022 16:46:51 -0300 Subject: [PATCH 17/70] recordImpressionStats and recordEventStats --- src/sdkFactory/index.ts | 4 ++-- src/trackers/eventTracker.ts | 10 +++++++--- src/trackers/impressionsTracker.ts | 14 +++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index e5c899b2..737db30a 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -76,8 +76,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // trackers const observer = impressionsObserverFactory && impressionsObserverFactory(); - const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, integrationsManager, observer, storage.impressionCounts); - const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager); + const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, integrationsManager, observer, storage.impressionCounts, storage.telemetry); + const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry); const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); // signal listener diff --git a/src/trackers/eventTracker.ts b/src/trackers/eventTracker.ts index 7ac851a9..57a75fda 100644 --- a/src/trackers/eventTracker.ts +++ b/src/trackers/eventTracker.ts @@ -1,10 +1,10 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; -import { IEventsCacheBase } from '../storages/types'; +import { IEventsCacheBase, TelemetryCacheAsync, TelemetryCacheSync } from '../storages/types'; import { IEventsHandler, IEventTracker } from './types'; import { ISettings, SplitIO } from '../types'; import { EVENTS_TRACKER_SUCCESS, ERROR_EVENTS_TRACKER } from '../logger/constants'; -import { CONSENT_DECLINED } from '../utils/constants'; +import { CONSENT_DECLINED, DROPPED, QUEUED } from '../utils/constants'; import { isStorageSync } from './impressionObserver/utils'; /** @@ -16,7 +16,8 @@ import { isStorageSync } from './impressionObserver/utils'; export function eventTrackerFactory( settings: ISettings, eventsCache: IEventsCacheBase, - integrationsManager?: IEventsHandler + integrationsManager?: IEventsHandler, + telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync ): IEventTracker { const log = settings.log; @@ -57,6 +58,9 @@ export function eventTrackerFactory( if (thenable(tracked)) { return tracked.then(queueEventsCallback.bind(null, eventData)); } else { + // Record when eventsCache is sync only (standalone mode) + // @TODO we are not dropping events on full queue yet, so `tracked` is always true ATM + if (telemetryCache) (telemetryCache as TelemetryCacheSync).recordEventStats(tracked ? QUEUED : DROPPED, 1); return queueEventsCallback(eventData, tracked); } } diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index ecd40a5d..847d207d 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -1,12 +1,12 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; import { truncateTimeFrame } from '../utils/time'; -import { IImpressionCountsCacheSync, IImpressionsCacheBase } from '../storages/types'; +import { IImpressionCountsCacheSync, IImpressionsCacheBase, TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; import { IImpressionsHandler, IImpressionsTracker } from './types'; import { SplitIO, ImpressionDTO, ISettings } from '../types'; import { IImpressionObserver } from './impressionObserver/types'; import { IMPRESSIONS_TRACKER_SUCCESS, ERROR_IMPRESSIONS_TRACKER, ERROR_IMPRESSIONS_LISTENER } from '../logger/constants'; -import { CONSENT_DECLINED } from '../utils/constants'; +import { CONSENT_DECLINED, DEDUPED, QUEUED } from '../utils/constants'; /** * Impressions tracker stores impressions in cache and pass them to the listener and integrations manager if provided. @@ -25,7 +25,8 @@ export function impressionsTrackerFactory( // if observer is provided, it implies `shouldAddPreviousTime` flag (i.e., if impressions previous time should be added or not) observer?: IImpressionObserver, // if countsCache is provided, it implies `isOptimized` flag (i.e., if impressions should be deduped or not) - countsCache?: IImpressionCountsCacheSync + countsCache?: IImpressionCountsCacheSync, + telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync ): IImpressionsTracker { const { log, impressionListener, runtime: { ip, hostname }, version } = settings; @@ -65,6 +66,13 @@ export function impressionsTrackerFactory( }).catch(err => { log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsCount, err]); }); + } else { + // Record when impressionsCache is sync only (standalone mode) + // @TODO we are not dropping impressions on full queue yet, so DROPPED stats are not recorded + if (telemetryCache) { + (telemetryCache as TelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStore.length); + (telemetryCache as TelemetryCacheSync).recordImpressionStats(DEDUPED, impressions.length - impressionsToStore.length); + } } // @TODO next block might be handled by the integration manager. In that case, the metadata object doesn't need to be passed in the constructor From 18eee89f1808131e9dccdd03335769b1912dd280 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 25 Apr 2022 18:28:39 -0300 Subject: [PATCH 18/70] recordSuccessfulSync --- src/trackers/__tests__/telemetryTracker.spec.ts | 2 ++ src/trackers/telemetryTracker.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index 5a99446e..bb169d60 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -10,6 +10,7 @@ describe('Telemetry Tracker', () => { recordNonReadyUsage: jest.fn(), recordHttpLatency: jest.fn(), recordHttpError: jest.fn(), + recordSuccessfulSync: jest.fn(), }; // @ts-ignore const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); @@ -52,6 +53,7 @@ describe('Telemetry Tracker', () => { stopTracker(); expect(fakeTelemetryCache.recordHttpError).toBeCalledTimes(1); + expect(fakeTelemetryCache.recordSuccessfulSync).toBeCalledTimes(2); expect(fakeTelemetryCache.recordHttpLatency).toBeCalledTimes(3); const latency = fakeTelemetryCache.recordHttpLatency.mock.calls[2][1]; diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index eddd2fc4..92422a17 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -33,6 +33,7 @@ export function telemetryTrackerFactory( return (error?: NetworkError) => { (telemetryCache as TelemetryCacheSync).recordHttpLatency(operation, timeTracker()); if (error && error.statusCode) (telemetryCache as TelemetryCacheSync).recordHttpError(operation, error.statusCode); + else (telemetryCache as TelemetryCacheSync).recordSuccessfulSync(operation, now()); }; }, }; From 617d5c1d9b6abd0fc86b23cd085b8cb6118d9256 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 26 Apr 2022 14:19:07 -0300 Subject: [PATCH 19/70] recordSessionLength, recordAuthRejections, and recordTokenRefreshes --- src/__tests__/testUtils/index.ts | 14 +++++++++++++ .../__tests__/sdkClientMethod.spec.ts | 7 +++++-- .../__tests__/sdkClientMethodCS.spec.ts | 5 +++-- src/sdkClient/sdkClient.ts | 3 +++ src/sync/streaming/pushManager.ts | 14 ++++++++----- .../__tests__/telemetryTracker.spec.ts | 21 +++++++++++++++---- src/trackers/telemetryTracker.ts | 7 ++++++- src/trackers/types.ts | 4 ++++ 8 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/testUtils/index.ts diff --git a/src/__tests__/testUtils/index.ts b/src/__tests__/testUtils/index.ts new file mode 100644 index 00000000..568f149f --- /dev/null +++ b/src/__tests__/testUtils/index.ts @@ -0,0 +1,14 @@ +const DEFAULT_ERROR_MARGIN = 50; // 0.05 secs if numbers are timestamps in milliseconds + +/** + * Assert if an `actual` and `expected` numeric values are nearly equal. + * + * @param {number} actual actual time lapse in millis + * @param {number} expected expected time lapse in millis + * @param {number} epsilon error margin in millis + * @returns {boolean} whether the absolute difference is minor to epsilon value or not + */ +export function nearlyEqual(actual: number, expected: number, epsilon = DEFAULT_ERROR_MARGIN) { + const diff = Math.abs(actual - expected); + return diff <= epsilon; +} diff --git a/src/sdkClient/__tests__/sdkClientMethod.spec.ts b/src/sdkClient/__tests__/sdkClientMethod.spec.ts index 604f20f6..e35ccab1 100644 --- a/src/sdkClient/__tests__/sdkClientMethod.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethod.spec.ts @@ -2,6 +2,7 @@ import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { CONSUMER_MODE, STANDALONE_MODE } from '../../utils/constants'; import { sdkClientMethodFactory } from '../sdkClientMethod'; import { assertClientApi } from './testUtils'; +import { telemetryTrackerFactory } from '../../trackers/telemetryTracker'; const errorMessage = 'Shared Client not supported by the storage mechanism. Create isolated instances instead.'; @@ -12,7 +13,8 @@ const paramMocks = [ syncManager: undefined, sdkReadinessManager: { sdkStatus: jest.fn(), readinessManager: { destroy: jest.fn() } }, signalListener: undefined, - settings: { mode: CONSUMER_MODE, log: loggerMock, core: { authorizationKey: 'api key '} } + settings: { mode: CONSUMER_MODE, log: loggerMock, core: { authorizationKey: 'api key '} }, + telemetryTracker: telemetryTrackerFactory() }, // SyncManager (i.e., Sync SDK) and Signal listener { @@ -20,7 +22,8 @@ const paramMocks = [ syncManager: { stop: jest.fn(), flush: jest.fn(() => Promise.resolve()) }, sdkReadinessManager: { sdkStatus: jest.fn(), readinessManager: { destroy: jest.fn() } }, signalListener: { stop: jest.fn() }, - settings: { mode: STANDALONE_MODE, log: loggerMock, core: { authorizationKey: 'api key '} } + settings: { mode: STANDALONE_MODE, log: loggerMock, core: { authorizationKey: 'api key '} }, + telemetryTracker: telemetryTrackerFactory() } ]; diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index 7eabeb7c..dbe57ec9 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -1,7 +1,7 @@ import { sdkClientMethodCSFactory as sdkClientMethodCSWithTTFactory } from '../sdkClientMethodCSWithTT'; import { sdkClientMethodCSFactory } from '../sdkClientMethodCS'; import { assertClientApi } from './testUtils'; - +import { telemetryTrackerFactory } from '../../trackers/telemetryTracker'; import { settingsWithKey, settingsWithKeyAndTT, settingsWithKeyObject } from '../../utils/settingsValidation/__tests__/settings.mocks'; const partialStorages: { destroy: jest.Mock }[] = []; @@ -44,7 +44,8 @@ const params = { sdkReadinessManager: sdkReadinessManagerMock, syncManager: syncManagerMock, signalListener: { stop: jest.fn() }, - settings: settingsWithKey + settings: settingsWithKey, + telemetryTracker: telemetryTrackerFactory() }; const invalidAttributes = [ diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index c8ad8dc2..8dd13963 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -25,6 +25,9 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo // Sdk destroy { destroy() { + // record stat before flushing data + if (!isSharedClient) params.telemetryTracker.trackSessionLength(); + // Stop background jobs syncManager && syncManager.stop(); const flush = syncManager ? syncManager.flush() : Promise.resolve(); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index ec5ff916..e16949f7 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -30,7 +30,7 @@ export function pushManagerFactory( pollingManager: IPollingManager, ): IPushManager | undefined { - const { settings, storage, splitApi, readiness, platform } = params; + const { settings, storage: { segments, splits, telemetry }, splitApi, readiness, platform } = params; // `userKey` is the matching key of main client in client-side SDK. // It can be used to check if running on client-side or server-side SDK. @@ -54,9 +54,9 @@ export function pushManagerFactory( // init workers // MySegmentsUpdateWorker (client-side) are initiated in `add` method - const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(pollingManager.segmentsSyncTask, storage.segments); + const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(pollingManager.segmentsSyncTask, segments); // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments - const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); + const splitsUpdateWorker = new SplitsUpdateWorker(splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker const userKeyHashes: Record = {}; @@ -94,7 +94,10 @@ export function pushManagerFactory( log.info(STREAMING_REFRESH_TOKEN, [refreshTokenDelay, connDelay]); - timeoutIdTokenRefresh = setTimeout(connectPush, refreshTokenDelay * 1000); + timeoutIdTokenRefresh = setTimeout(() => { + if (telemetry) telemetry.recordTokenRefreshes(); + connectPush(); + }, refreshTokenDelay * 1000); timeoutIdSseOpen = setTimeout(() => { // halt if disconnected @@ -137,6 +140,7 @@ export function pushManagerFactory( // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) if (error.statusCode >= 400 && error.statusCode < 500) { + if (telemetry) telemetry.recordAuthRejections(); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); return; } @@ -316,7 +320,7 @@ export function pushManagerFactory( }, // true/false if start or stop was called last respectively - isRunning(){ + isRunning() { return disconnected === false; }, diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index 5a99446e..d79b2596 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -1,4 +1,5 @@ import { EXCEPTION, SDK_NOT_READY } from '../../utils/labels'; +import { nearlyEqual } from '../../__tests__/testUtils'; import { telemetryTrackerFactory } from '../telemetryTracker'; describe('Telemetry Tracker', () => { @@ -10,9 +11,11 @@ describe('Telemetry Tracker', () => { recordNonReadyUsage: jest.fn(), recordHttpLatency: jest.fn(), recordHttpError: jest.fn(), + recordSessionLength: jest.fn() }; // @ts-ignore const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); + const startTimestamp = Date.now(); test('trackEval', async () => { @@ -27,7 +30,7 @@ describe('Telemetry Tracker', () => { stopTracker = tracker.trackEval('tcs'); - await new Promise(res => setTimeout(res, 50)); + await new Promise(res => setTimeout(res, 100)); stopTracker(); expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); @@ -35,7 +38,7 @@ describe('Telemetry Tracker', () => { expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(4); const latency = fakeTelemetryCache.recordLatency.mock.calls[3][1]; - expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms + expect(nearlyEqual(latency, 100)).toBeTruthy(); // last tracked latency is around 100 ms }); test('trackHttp', async () => { @@ -48,15 +51,25 @@ describe('Telemetry Tracker', () => { stopTracker = tracker.trackHttp('im'); - await new Promise(res => setTimeout(res, 50)); + await new Promise(res => setTimeout(res, 100)); stopTracker(); expect(fakeTelemetryCache.recordHttpError).toBeCalledTimes(1); expect(fakeTelemetryCache.recordHttpLatency).toBeCalledTimes(3); const latency = fakeTelemetryCache.recordHttpLatency.mock.calls[2][1]; - expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms + expect(nearlyEqual(latency, 100)).toBeTruthy(); // last tracked latency is around 100 ms }); + + test('trackSessionLength', () => { + tracker.trackSessionLength(); + expect(fakeTelemetryCache.recordSessionLength).toBeCalledTimes(1); + + const expectedSessionLength = Date.now() - startTimestamp; + const sessionLength = fakeTelemetryCache.recordSessionLength.mock.calls[0][0]; + expect(nearlyEqual(sessionLength, expectedSessionLength)).toBeTruthy(); + }); + }); test('Telemetry Tracker no-op', () => { diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index eddd2fc4..85991913 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -11,6 +11,7 @@ export function telemetryTrackerFactory( ): ITelemetryTracker { if (telemetryCache && now) { + const startTime = timer(now); return { trackEval(method: Method) { @@ -35,13 +36,17 @@ export function telemetryTrackerFactory( if (error && error.statusCode) (telemetryCache as TelemetryCacheSync).recordHttpError(operation, error.statusCode); }; }, + trackSessionLength() { // @ts-ignore + telemetryCache?.recordSessionLength(startTime()); + } }; } else { // If there is not `telemetryCache` or `now` time tracker, return a no-op telemetry tracker const noopTrack = () => () => { }; return { trackEval: noopTrack, - trackHttp: noopTrack + trackHttp: noopTrack, + trackSessionLength: noopTrack }; } } diff --git a/src/trackers/types.ts b/src/trackers/types.ts index 57745177..535c089e 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -32,4 +32,8 @@ export interface ITelemetryTracker { * Creates a telemetry runtime tracker, to record Latencies and Exceptions of HTTP requests */ trackHttp(method: OperationType): (error?: NetworkError) => void + /** + * Records session length + */ + trackSessionLength(): void } From 504e065ab2bb527062d8056ccaf1786879ab4821 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 26 Apr 2022 16:30:20 -0300 Subject: [PATCH 20/70] recordStreamingEvents --- .../SSEHandler/NotificationKeeper.ts | 30 +++++++++++++- src/sync/streaming/SSEHandler/index.ts | 40 +++++++++++++------ src/sync/streaming/pushManager.ts | 15 +++++-- src/sync/submitters/types.ts | 2 +- src/sync/syncManagerOnline.ts | 17 +++++++- src/utils/constants/index.ts | 17 ++++++++ 6 files changed, 100 insertions(+), 21 deletions(-) diff --git a/src/sync/streaming/SSEHandler/NotificationKeeper.ts b/src/sync/streaming/SSEHandler/NotificationKeeper.ts index 03dae152..633d0ba0 100644 --- a/src/sync/streaming/SSEHandler/NotificationKeeper.ts +++ b/src/sync/streaming/SSEHandler/NotificationKeeper.ts @@ -1,7 +1,11 @@ +import { TelemetryCacheSync } from '../../../storages/types'; +import { CONNECTION_ESTABLISHED, DISABLED, ENABLED, OCCUPANCY_PRI, OCCUPANCY_SEC, PAUSED, STREAMING_STATUS } from '../../../utils/constants'; +import { EventType } from '../../submitters/types'; import { ControlType, PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN } from '../constants'; import { IPushEventEmitter } from '../types'; const CONTROL_CHANNEL_REGEXS = [/control_pri$/, /control_sec$/]; +const STREAMING_EVENT_TYPES: EventType[] = [OCCUPANCY_PRI, OCCUPANCY_SEC]; /** * Factory of notification keeper, which process OCCUPANCY and CONTROL notifications and emits the corresponding push events. @@ -9,7 +13,7 @@ const CONTROL_CHANNEL_REGEXS = [/control_pri$/, /control_sec$/]; * @param pushEmitter emitter for events related to streaming support */ // @TODO update logic to handle OCCUPANCY for any region and rename according to new spec (e.g.: PUSH_SUBSYSTEM_UP --> PUSH_SUBSYSTEM_UP) -export function notificationKeeperFactory(pushEmitter: IPushEventEmitter) { +export function notificationKeeperFactory(pushEmitter: IPushEventEmitter, telemetry?: TelemetryCacheSync) { let channels = CONTROL_CHANNEL_REGEXS.map(regex => ({ regex, @@ -30,6 +34,10 @@ export function notificationKeeperFactory(pushEmitter: IPushEventEmitter) { return { handleOpen() { + if (telemetry) telemetry.recordStreamingEvents({ + e: CONNECTION_ESTABLISHED, + t: Date.now() + }); pushEmitter.emit(PUSH_SUBSYSTEM_UP); }, @@ -41,6 +49,11 @@ export function notificationKeeperFactory(pushEmitter: IPushEventEmitter) { for (let i = 0; i < channels.length; i++) { const c = channels[i]; if (c.regex.test(channel)) { + if (telemetry) telemetry.recordStreamingEvents({ + e: STREAMING_EVENT_TYPES[i], + d: publishers, + t: Date.now() + }); if (timestamp > c.oTime) { c.oTime = timestamp; c.hasPublishers = publishers !== 0; @@ -76,11 +89,26 @@ export function notificationKeeperFactory(pushEmitter: IPushEventEmitter) { if (timestamp > c.cTime) { c.cTime = timestamp; if (controlType === ControlType.STREAMING_DISABLED) { + if (telemetry) telemetry.recordStreamingEvents({ + e: STREAMING_STATUS, + d: DISABLED, + t: Date.now() + }); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); } else if (hasPublishers) { if (controlType === ControlType.STREAMING_PAUSED && hasResumed) { + if (telemetry) telemetry.recordStreamingEvents({ + e: STREAMING_STATUS, + d: PAUSED, + t: Date.now() + }); pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); } else if (controlType === ControlType.STREAMING_RESUMED && !hasResumed) { + if (telemetry) telemetry.recordStreamingEvents({ + e: STREAMING_STATUS, + d: ENABLED, + t: Date.now() + }); pushEmitter.emit(PUSH_SUBSYSTEM_UP); } // nothing to do when hasPublishers === false: diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index b0b46767..755f2ae5 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -6,18 +6,8 @@ import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; import { ILogger } from '../../../logger/types'; import { STREAMING_PARSING_ERROR_FAILS, ERROR_STREAMING_SSE, STREAMING_PARSING_MESSAGE_FAILS, STREAMING_NEW_MESSAGE } from '../../../logger/constants'; - -function isRetryableError(error: INotificationError) { - if (error.parsedData && error.parsedData.code) { - const code = error.parsedData.code; - // 401 errors due to invalid or expired token (e.g., if refresh token coudn't be executed) - if (40140 <= code && code <= 40149) return true; - // Others 4XX errors (e.g., bad request from the SDK) - if (40000 <= code && code <= 49999) return false; - } - // network errors or 5XX HTTP errors - return true; -} +import { TelemetryCacheSync } from '../../../storages/types'; +import { ABLY_ERROR, CONNECTION_ESTABLISHED, NON_REQUESTED, SSE_CONNECTION_ERROR } from '../../../utils/constants'; /** * Factory for SSEHandler, which processes SSEClient messages and emits the corresponding push events. @@ -25,10 +15,34 @@ function isRetryableError(error: INotificationError) { * @param log factory logger * @param pushEmitter emitter for events related to streaming support */ -export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter): ISseEventHandler { +export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, telemetry?: TelemetryCacheSync): ISseEventHandler { const notificationKeeper = notificationKeeperFactory(pushEmitter); + function isRetryableError(error: INotificationError): boolean { + if (error.parsedData && error.parsedData.code) { + // Ably error + const code = error.parsedData.code; + if (telemetry) telemetry.recordStreamingEvents({ + e: ABLY_ERROR, + d: code, + t: Date.now() + }); + // 401 errors due to invalid or expired token (e.g., if refresh token coudn't be executed) + if (40140 <= code && code <= 40149) return true; + // Others 4XX errors (e.g., bad request from the SDK) + if (40000 <= code && code <= 49999) return false; + } else { + // network errors or 5XX HTTP errors + if (telemetry) telemetry.recordStreamingEvents({ + e: SSE_CONNECTION_ERROR, + d: NON_REQUESTED, + t: Date.now() + }); + } + return true; + } + return { handleOpen() { notificationKeeper.handleOpen(); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index e16949f7..7446f7e1 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -19,6 +19,7 @@ import { ISet, _Set } from '../../utils/lang/sets'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; import { IAuthTokenPushEnabled } from './AuthClient/types'; import { ISyncManagerFactoryParams } from '../types'; +import { TOKEN_REFRESH } from '../../utils/constants'; /** * PushManager factory: @@ -94,16 +95,22 @@ export function pushManagerFactory( log.info(STREAMING_REFRESH_TOKEN, [refreshTokenDelay, connDelay]); - timeoutIdTokenRefresh = setTimeout(() => { - if (telemetry) telemetry.recordTokenRefreshes(); - connectPush(); - }, refreshTokenDelay * 1000); + timeoutIdTokenRefresh = setTimeout(connectPush, refreshTokenDelay * 1000); timeoutIdSseOpen = setTimeout(() => { // halt if disconnected if (disconnected) return; sseClient.open(authData); }, connDelay * 1000); + + if (telemetry) { + telemetry.recordTokenRefreshes(); + telemetry.recordStreamingEvents({ + e: TOKEN_REFRESH, + d: decodedToken.exp, + t: Date.now() + }); + } } function connectPush() { diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 695c145d..53fde552 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -110,7 +110,7 @@ export type EventType = CONNECTION_ESTABLISHED | OCCUPANCY_PRI | OCCUPANCY_SEC | export type StreamingEvent = { e: EventType, // eventType - d: number, // eventData + d?: number, // eventData t: number, // timestamp } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 235c18c8..2b36d458 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -7,6 +7,7 @@ import { IPollingManager, IPollingManagerCS } from './polling/types'; import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; import { isConsentGranted } from '../consent'; +import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; /** * Online SyncManager factory. @@ -26,7 +27,7 @@ export function syncManagerOnlineFactory( */ return function (params: ISyncManagerFactoryParams): ISyncManagerCS { - const { settings, settings: { log, streamingEnabled } } = params; + const { settings, settings: { log, streamingEnabled }, storage: { telemetry } } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); @@ -48,13 +49,25 @@ export function syncManagerOnlineFactory( } else { log.info(SYNC_START_POLLING); pollingManager!.start(); + if (telemetry) telemetry.recordStreamingEvents({ + e: SYNC_MODE_UPDATE, + d: POLLING, + t: Date.now() + }); } } function stopPollingAndSyncAll() { log.info(SYNC_STOP_POLLING); // if polling, stop - if (pollingManager!.isRunning()) pollingManager!.stop(); + if (pollingManager!.isRunning()) { + pollingManager!.stop(); + if (telemetry) telemetry.recordStreamingEvents({ + e: SYNC_MODE_UPDATE, + d: STREAMING, + t: Date.now() + }); + } // fetch splits and segments. There is no need to catch this promise (it is always resolved) pollingManager!.syncAll(); diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 6fbcb421..9e6f4043 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -64,3 +64,20 @@ export const TREATMENTS = 'ts'; export const TREATMENT_WITH_CONFIG = 'tc'; export const TREATMENTS_WITH_CONFIG = 'tcs'; export const TRACK = 'tr'; + +export const CONNECTION_ESTABLISHED = 0; +export const OCCUPANCY_PRI = 10; +export const OCCUPANCY_SEC = 20; +export const STREAMING_STATUS = 30; +export const SSE_CONNECTION_ERROR = 40; +export const TOKEN_REFRESH = 50; +export const ABLY_ERROR = 60; +export const SYNC_MODE_UPDATE = 70; + +export const STREAMING = 0; +export const POLLING = 1; +export const REQUESTED = 0; +export const NON_REQUESTED = 1; +export const DISABLED = 0; +export const ENABLED = 1; +export const PAUSED = 2; From 1670fd5953cd3fc9858cd643acdcad869b33f48b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 26 Apr 2022 17:38:46 -0300 Subject: [PATCH 21/70] polishing to reuse code --- src/sdkClient/sdkClient.ts | 2 +- src/sdkFactory/index.ts | 19 +++++----- .../SSEHandler/NotificationKeeper.ts | 38 +++++-------------- src/sync/streaming/SSEHandler/index.ts | 21 ++++------ src/sync/streaming/pushManager.ts | 17 +++------ src/sync/submitters/types.ts | 4 +- src/sync/syncManagerOnline.ts | 14 ++----- src/sync/types.ts | 4 +- .../__tests__/telemetryTracker.spec.ts | 13 ++++++- src/trackers/telemetryTracker.ts | 25 ++++++++---- src/trackers/types.ts | 9 ++++- src/utils/constants/index.ts | 1 + 12 files changed, 78 insertions(+), 89 deletions(-) diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index 8dd13963..e6aa3473 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -26,7 +26,7 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo { destroy() { // record stat before flushing data - if (!isSharedClient) params.telemetryTracker.trackSessionLength(); + if (!isSharedClient) params.telemetryTracker.sessionLength(); // Stop background jobs syncManager && syncManager.stop(); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 737db30a..ce77472a 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -61,6 +61,14 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const storage = storageFactory(storageFactoryParams); // @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);` + const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage }); + + // trackers + const observer = impressionsObserverFactory && impressionsObserverFactory(); + const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, integrationsManager, observer, storage.impressionCounts, storage.telemetry); + const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry); + const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); + // splitApi is used by SyncManager and Browser signal listener const splitApi = splitApiFactory && splitApiFactory(settings, platform); @@ -69,17 +77,10 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. splitApi: splitApi as ISplitApi, storage: storage as IStorageSync, readiness: sdkReadinessManager.readinessManager, - platform + platform, + telemetryTracker }); - const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage }); - - // trackers - const observer = impressionsObserverFactory && impressionsObserverFactory(); - const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, integrationsManager, observer, storage.impressionCounts, storage.telemetry); - const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry); - const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now); - // signal listener const signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi); diff --git a/src/sync/streaming/SSEHandler/NotificationKeeper.ts b/src/sync/streaming/SSEHandler/NotificationKeeper.ts index 633d0ba0..a07c6761 100644 --- a/src/sync/streaming/SSEHandler/NotificationKeeper.ts +++ b/src/sync/streaming/SSEHandler/NotificationKeeper.ts @@ -1,11 +1,11 @@ -import { TelemetryCacheSync } from '../../../storages/types'; +import { ITelemetryTracker } from '../../../trackers/types'; import { CONNECTION_ESTABLISHED, DISABLED, ENABLED, OCCUPANCY_PRI, OCCUPANCY_SEC, PAUSED, STREAMING_STATUS } from '../../../utils/constants'; -import { EventType } from '../../submitters/types'; +import { StreamingEventType } from '../../submitters/types'; import { ControlType, PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN } from '../constants'; import { IPushEventEmitter } from '../types'; const CONTROL_CHANNEL_REGEXS = [/control_pri$/, /control_sec$/]; -const STREAMING_EVENT_TYPES: EventType[] = [OCCUPANCY_PRI, OCCUPANCY_SEC]; +const STREAMING_EVENT_TYPES: StreamingEventType[] = [OCCUPANCY_PRI, OCCUPANCY_SEC]; /** * Factory of notification keeper, which process OCCUPANCY and CONTROL notifications and emits the corresponding push events. @@ -13,7 +13,7 @@ const STREAMING_EVENT_TYPES: EventType[] = [OCCUPANCY_PRI, OCCUPANCY_SEC]; * @param pushEmitter emitter for events related to streaming support */ // @TODO update logic to handle OCCUPANCY for any region and rename according to new spec (e.g.: PUSH_SUBSYSTEM_UP --> PUSH_SUBSYSTEM_UP) -export function notificationKeeperFactory(pushEmitter: IPushEventEmitter, telemetry?: TelemetryCacheSync) { +export function notificationKeeperFactory(pushEmitter: IPushEventEmitter, telemetryTracker: ITelemetryTracker) { let channels = CONTROL_CHANNEL_REGEXS.map(regex => ({ regex, @@ -34,10 +34,7 @@ export function notificationKeeperFactory(pushEmitter: IPushEventEmitter, teleme return { handleOpen() { - if (telemetry) telemetry.recordStreamingEvents({ - e: CONNECTION_ESTABLISHED, - t: Date.now() - }); + telemetryTracker.streamingEvent(CONNECTION_ESTABLISHED); pushEmitter.emit(PUSH_SUBSYSTEM_UP); }, @@ -49,11 +46,8 @@ export function notificationKeeperFactory(pushEmitter: IPushEventEmitter, teleme for (let i = 0; i < channels.length; i++) { const c = channels[i]; if (c.regex.test(channel)) { - if (telemetry) telemetry.recordStreamingEvents({ - e: STREAMING_EVENT_TYPES[i], - d: publishers, - t: Date.now() - }); + telemetryTracker.streamingEvent(STREAMING_EVENT_TYPES[i], publishers); + if (timestamp > c.oTime) { c.oTime = timestamp; c.hasPublishers = publishers !== 0; @@ -89,26 +83,14 @@ export function notificationKeeperFactory(pushEmitter: IPushEventEmitter, teleme if (timestamp > c.cTime) { c.cTime = timestamp; if (controlType === ControlType.STREAMING_DISABLED) { - if (telemetry) telemetry.recordStreamingEvents({ - e: STREAMING_STATUS, - d: DISABLED, - t: Date.now() - }); + telemetryTracker.streamingEvent(STREAMING_STATUS, DISABLED); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); } else if (hasPublishers) { if (controlType === ControlType.STREAMING_PAUSED && hasResumed) { - if (telemetry) telemetry.recordStreamingEvents({ - e: STREAMING_STATUS, - d: PAUSED, - t: Date.now() - }); + telemetryTracker.streamingEvent(STREAMING_STATUS, PAUSED); pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); } else if (controlType === ControlType.STREAMING_RESUMED && !hasResumed) { - if (telemetry) telemetry.recordStreamingEvents({ - e: STREAMING_STATUS, - d: ENABLED, - t: Date.now() - }); + telemetryTracker.streamingEvent(STREAMING_STATUS, ENABLED); pushEmitter.emit(PUSH_SUBSYSTEM_UP); } // nothing to do when hasPublishers === false: diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index 755f2ae5..d8f20b32 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -6,8 +6,8 @@ import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; import { ILogger } from '../../../logger/types'; import { STREAMING_PARSING_ERROR_FAILS, ERROR_STREAMING_SSE, STREAMING_PARSING_MESSAGE_FAILS, STREAMING_NEW_MESSAGE } from '../../../logger/constants'; -import { TelemetryCacheSync } from '../../../storages/types'; -import { ABLY_ERROR, CONNECTION_ESTABLISHED, NON_REQUESTED, SSE_CONNECTION_ERROR } from '../../../utils/constants'; +import { ABLY_ERROR, NON_REQUESTED, SSE_CONNECTION_ERROR } from '../../../utils/constants'; +import { ITelemetryTracker } from '../../../trackers/types'; /** * Factory for SSEHandler, which processes SSEClient messages and emits the corresponding push events. @@ -15,30 +15,23 @@ import { ABLY_ERROR, CONNECTION_ESTABLISHED, NON_REQUESTED, SSE_CONNECTION_ERROR * @param log factory logger * @param pushEmitter emitter for events related to streaming support */ -export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, telemetry?: TelemetryCacheSync): ISseEventHandler { +export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, telemetryTracker: ITelemetryTracker): ISseEventHandler { - const notificationKeeper = notificationKeeperFactory(pushEmitter); + const notificationKeeper = notificationKeeperFactory(pushEmitter, telemetryTracker); function isRetryableError(error: INotificationError): boolean { if (error.parsedData && error.parsedData.code) { // Ably error const code = error.parsedData.code; - if (telemetry) telemetry.recordStreamingEvents({ - e: ABLY_ERROR, - d: code, - t: Date.now() - }); + telemetryTracker.streamingEvent(ABLY_ERROR, code); + // 401 errors due to invalid or expired token (e.g., if refresh token coudn't be executed) if (40140 <= code && code <= 40149) return true; // Others 4XX errors (e.g., bad request from the SDK) if (40000 <= code && code <= 49999) return false; } else { // network errors or 5XX HTTP errors - if (telemetry) telemetry.recordStreamingEvents({ - e: SSE_CONNECTION_ERROR, - d: NON_REQUESTED, - t: Date.now() - }); + telemetryTracker.streamingEvent(SSE_CONNECTION_ERROR, NON_REQUESTED); } return true; } diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 7446f7e1..0f9f9529 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -19,7 +19,7 @@ import { ISet, _Set } from '../../utils/lang/sets'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; import { IAuthTokenPushEnabled } from './AuthClient/types'; import { ISyncManagerFactoryParams } from '../types'; -import { TOKEN_REFRESH } from '../../utils/constants'; +import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants'; /** * PushManager factory: @@ -31,7 +31,7 @@ export function pushManagerFactory( pollingManager: IPollingManager, ): IPushManager | undefined { - const { settings, storage: { segments, splits, telemetry }, splitApi, readiness, platform } = params; + const { settings, storage: { segments, splits }, splitApi, readiness, platform, telemetryTracker } = params; // `userKey` is the matching key of main client in client-side SDK. // It can be used to check if running on client-side or server-side SDK. @@ -50,7 +50,7 @@ export function pushManagerFactory( // init feedback loop const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; - const sseHandler = SSEHandlerFactory(log, pushEmitter); + const sseHandler = SSEHandlerFactory(log, pushEmitter, telemetryTracker); sseClient.setEventHandler(sseHandler); // init workers @@ -103,14 +103,7 @@ export function pushManagerFactory( sseClient.open(authData); }, connDelay * 1000); - if (telemetry) { - telemetry.recordTokenRefreshes(); - telemetry.recordStreamingEvents({ - e: TOKEN_REFRESH, - d: decodedToken.exp, - t: Date.now() - }); - } + telemetryTracker.streamingEvent(TOKEN_REFRESH, decodedToken.exp); } function connectPush() { @@ -147,7 +140,7 @@ export function pushManagerFactory( // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) if (error.statusCode >= 400 && error.statusCode < 500) { - if (telemetry) telemetry.recordAuthRejections(); + telemetryTracker.streamingEvent(AUTH_REJECTION); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); return; } diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 53fde552..395d8375 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -106,10 +106,10 @@ export type SSE_CONNECTION_ERROR = 40; export type TOKEN_REFRESH = 50; export type ABLY_ERROR = 60; export type SYNC_MODE_UPDATE = 70; -export type EventType = CONNECTION_ESTABLISHED | OCCUPANCY_PRI | OCCUPANCY_SEC | STREAMING_STATUS | SSE_CONNECTION_ERROR | TOKEN_REFRESH | ABLY_ERROR | SYNC_MODE_UPDATE; +export type StreamingEventType = CONNECTION_ESTABLISHED | OCCUPANCY_PRI | OCCUPANCY_SEC | STREAMING_STATUS | SSE_CONNECTION_ERROR | TOKEN_REFRESH | ABLY_ERROR | SYNC_MODE_UPDATE; export type StreamingEvent = { - e: EventType, // eventType + e: StreamingEventType, // eventType d?: number, // eventData t: number, // timestamp } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 2b36d458..88c1516e 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -27,7 +27,7 @@ export function syncManagerOnlineFactory( */ return function (params: ISyncManagerFactoryParams): ISyncManagerCS { - const { settings, settings: { log, streamingEnabled }, storage: { telemetry } } = params; + const { settings, settings: { log, streamingEnabled }, telemetryTracker } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); @@ -49,11 +49,7 @@ export function syncManagerOnlineFactory( } else { log.info(SYNC_START_POLLING); pollingManager!.start(); - if (telemetry) telemetry.recordStreamingEvents({ - e: SYNC_MODE_UPDATE, - d: POLLING, - t: Date.now() - }); + telemetryTracker.streamingEvent(SYNC_MODE_UPDATE, POLLING); } } @@ -62,11 +58,7 @@ export function syncManagerOnlineFactory( // if polling, stop if (pollingManager!.isRunning()) { pollingManager!.stop(); - if (telemetry) telemetry.recordStreamingEvents({ - e: SYNC_MODE_UPDATE, - d: STREAMING, - t: Date.now() - }); + telemetryTracker.streamingEvent(SYNC_MODE_UPDATE, STREAMING); } // fetch splits and segments. There is no need to catch this promise (it is always resolved) diff --git a/src/sync/types.ts b/src/sync/types.ts index 1a70a5c6..f2962885 100644 --- a/src/sync/types.ts +++ b/src/sync/types.ts @@ -2,6 +2,7 @@ import { IReadinessManager } from '../readiness/types'; import { IPlatform } from '../sdkFactory/types'; import { ISplitApi } from '../services/types'; import { IStorageSync } from '../storages/types'; +import { ITelemetryTracker } from '../trackers/types'; import { ISettings } from '../types'; import { IPollingManager } from './polling/types'; import { IPushManager } from './streaming/types'; @@ -58,5 +59,6 @@ export interface ISyncManagerFactoryParams { readiness: IReadinessManager, storage: IStorageSync, splitApi: ISplitApi, - platform: IPlatform + platform: IPlatform, + telemetryTracker: ITelemetryTracker } diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index d79b2596..d7a0c8d2 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -11,7 +11,8 @@ describe('Telemetry Tracker', () => { recordNonReadyUsage: jest.fn(), recordHttpLatency: jest.fn(), recordHttpError: jest.fn(), - recordSessionLength: jest.fn() + recordSessionLength: jest.fn(), + recordStreamingEvents: jest.fn(), }; // @ts-ignore const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); @@ -62,7 +63,7 @@ describe('Telemetry Tracker', () => { }); test('trackSessionLength', () => { - tracker.trackSessionLength(); + tracker.sessionLength(); expect(fakeTelemetryCache.recordSessionLength).toBeCalledTimes(1); const expectedSessionLength = Date.now() - startTimestamp; @@ -70,6 +71,11 @@ describe('Telemetry Tracker', () => { expect(nearlyEqual(sessionLength, expectedSessionLength)).toBeTruthy(); }); + test('streamingEvent', () => { + tracker.streamingEvent(10, 1); + expect(fakeTelemetryCache.recordStreamingEvents).toBeCalledTimes(1); + }); + }); test('Telemetry Tracker no-op', () => { @@ -81,4 +87,7 @@ test('Telemetry Tracker no-op', () => { const stopHttpTracker = tracker.trackHttp('ev'); expect(stopHttpTracker()).toBe(undefined); + + expect(tracker.sessionLength()).toBe(undefined); + expect(tracker.streamingEvent(10, 1)).toBe(undefined); }); diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index 85991913..c04c56bb 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -1,9 +1,9 @@ import { TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; -import { Method, OperationType } from '../sync/submitters/types'; import { EXCEPTION, SDK_NOT_READY } from '../utils/labels'; import { ITelemetryTracker } from './types'; import { timer } from '../utils/timeTracker/timer'; import { NetworkError } from '../services/types'; +import { TOKEN_REFRESH, AUTH_REJECTION } from '../utils/constants'; export function telemetryTrackerFactory( telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync, @@ -14,10 +14,10 @@ export function telemetryTrackerFactory( const startTime = timer(now); return { - trackEval(method: Method) { + trackEval(method) { const timeTracker = timer(now); - return (label?: string) => { + return (label) => { telemetryCache.recordLatency(method, timeTracker()); switch (label) { @@ -28,7 +28,7 @@ export function telemetryTrackerFactory( } }; }, - trackHttp(operation: OperationType) { + trackHttp(operation) { const timeTracker = timer(now); return (error?: NetworkError) => { @@ -36,8 +36,18 @@ export function telemetryTrackerFactory( if (error && error.statusCode) (telemetryCache as TelemetryCacheSync).recordHttpError(operation, error.statusCode); }; }, - trackSessionLength() { // @ts-ignore - telemetryCache?.recordSessionLength(startTime()); + sessionLength() { + (telemetryCache as TelemetryCacheSync).recordSessionLength(startTime()); + }, + streamingEvent(e, d) { + if (e === AUTH_REJECTION) { + (telemetryCache as TelemetryCacheSync).recordAuthRejections(); + } else { + (telemetryCache as TelemetryCacheSync).recordStreamingEvents({ + e, d, t: now() + }); + if (e === TOKEN_REFRESH) (telemetryCache as TelemetryCacheSync).recordTokenRefreshes(); + } } }; @@ -46,7 +56,8 @@ export function telemetryTrackerFactory( return { trackEval: noopTrack, trackHttp: noopTrack, - trackSessionLength: noopTrack + sessionLength: () => { }, + streamingEvent: () => { } }; } } diff --git a/src/trackers/types.ts b/src/trackers/types.ts index 535c089e..42dcfa0c 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -1,5 +1,5 @@ import { SplitIO, ImpressionDTO } from '../types'; -import { Method, OperationType } from '../sync/submitters/types'; +import { StreamingEventType, Method, OperationType } from '../sync/submitters/types'; import { IEventsCacheBase } from '../storages/types'; import { NetworkError } from '../services/types'; @@ -22,6 +22,7 @@ export interface IImpressionsTracker { } /** Telemetry tracker */ +export type AUTH_REJECTION = 80; export interface ITelemetryTracker { /** @@ -35,5 +36,9 @@ export interface ITelemetryTracker { /** * Records session length */ - trackSessionLength(): void + sessionLength(): void, + /** + * Records streaming event + */ + streamingEvent(e: StreamingEventType | AUTH_REJECTION, d?: number): void } diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 9e6f4043..4925e71a 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -73,6 +73,7 @@ export const SSE_CONNECTION_ERROR = 40; export const TOKEN_REFRESH = 50; export const ABLY_ERROR = 60; export const SYNC_MODE_UPDATE = 70; +export const AUTH_REJECTION = 80; export const STREAMING = 0; export const POLLING = 1; From c7b9ddbcc5f48ee007ed39b05513b2e8a71f75ac Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 27 Apr 2022 12:12:00 -0300 Subject: [PATCH 22/70] simplify interfaces of factory internal components --- src/sdkFactory/index.ts | 30 +++++++---------- src/sdkFactory/types.ts | 43 +++++++++++++++++-------- src/sync/offline/syncManagerOffline.ts | 7 ++-- src/sync/polling/pollingManagerCS.ts | 4 +-- src/sync/polling/pollingManagerSS.ts | 4 +-- src/sync/streaming/pushManager.ts | 4 +-- src/sync/submitters/submitterManager.ts | 4 +-- src/sync/syncManagerOnline.ts | 11 ++++--- src/sync/types.ts | 13 -------- src/types.ts | 5 +-- 10 files changed, 62 insertions(+), 63 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index ce77472a..bb5070d0 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -1,11 +1,10 @@ -import { ISdkFactoryParams } from './types'; +import { ISdkFactoryContext, ISdkFactoryContextSync, ISdkFactoryParams } from './types'; import { sdkReadinessManagerFactory } from '../readiness/sdkReadinessManager'; import { impressionsTrackerFactory } from '../trackers/impressionsTracker'; import { eventTrackerFactory } from '../trackers/eventTracker'; import { telemetryTrackerFactory } from '../trackers/telemetryTracker'; -import { IStorageFactoryParams, IStorageSync } from '../storages/types'; +import { IStorageFactoryParams } from '../storages/types'; import { SplitIO } from '../types'; -import { ISplitApi } from '../services/types'; import { getMatching } from '../utils/key'; import { shouldBeOptimized } from '../trackers/impressionObserver/utils'; import { validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; @@ -32,7 +31,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. validateAndTrackApiKey(log, settings.core.authorizationKey); const sdkReadinessManager = sdkReadinessManagerFactory(log, platform.EventEmitter, settings.startup.readyTimeout); - const readinessManager = sdkReadinessManager.readinessManager; + const readiness = sdkReadinessManager.readinessManager; // @TODO consider passing the settings object, so that each storage access only what it needs const storageFactoryParams: IStorageFactoryParams = { @@ -51,8 +50,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // or partial consumer mode, where it only has submitters, and therefore it doesn't emit readiness events. onReadyCb: (error) => { if (error) return; // Don't emit SDK_READY if storage failed to connect. Error message is logged by wrapperAdapter - readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + readiness.splits.emit(SDK_SPLITS_ARRIVED); + readiness.segments.emit(SDK_SEGMENTS_ARRIVED); }, metadata: metadataBuilder(settings), log @@ -72,20 +71,15 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // splitApi is used by SyncManager and Browser signal listener const splitApi = splitApiFactory && splitApiFactory(settings, platform); - const syncManager = syncManagerFactory && syncManagerFactory({ - settings, - splitApi: splitApi as ISplitApi, - storage: storage as IStorageSync, - readiness: sdkReadinessManager.readinessManager, - platform, - telemetryTracker - }); - - // signal listener + const ctx: ISdkFactoryContext = { splitApi, eventTracker, impressionsTracker, telemetryTracker, sdkReadinessManager, readiness, settings, storage, platform }; + + const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync); + ctx.syncManager = syncManager; + const signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi); + ctx.signalListener = signalListener; - // Sdk client and manager - const ctx = { eventTracker, impressionsTracker, telemetryTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }; + // SDK client and manager const clientMethod = sdkClientMethodFactory(ctx); const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 8781ae0c..9d0700ef 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -1,25 +1,14 @@ import { IIntegrationManager, IIntegrationFactoryParams } from '../integrations/types'; import { ISignalListener } from '../listeners/types'; import { ILogger } from '../logger/types'; -import { ISdkReadinessManager } from '../readiness/types'; +import { IReadinessManager, ISdkReadinessManager } from '../readiness/types'; import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types'; import { IStorageAsync, IStorageSync, ISplitsCacheSync, ISplitsCacheAsync, IStorageFactoryParams } from '../storages/types'; -import { ISyncManager, ISyncManagerFactoryParams } from '../sync/types'; +import { ISyncManager } from '../sync/types'; import { IImpressionObserver } from '../trackers/impressionObserver/types'; import { IImpressionsTracker, IEventTracker, ITelemetryTracker } from '../trackers/types'; import { SplitIO, ISettings, IEventEmitter } from '../types'; -export interface ISdkFactoryContext { - storage: IStorageSync | IStorageAsync, - sdkReadinessManager: ISdkReadinessManager, - settings: ISettings - impressionsTracker: IImpressionsTracker, - eventTracker: IEventTracker, - telemetryTracker: ITelemetryTracker - signalListener?: ISignalListener - syncManager?: ISyncManager, -} - /** * Environment related dependencies. * These getters are called a fixed number of times per factory instantiation. @@ -32,6 +21,32 @@ export interface IPlatform { now?: () => number } +export interface ISdkFactoryContext { + platform: IPlatform, + sdkReadinessManager: ISdkReadinessManager, + readiness: IReadinessManager, + settings: ISettings + impressionsTracker: IImpressionsTracker, + eventTracker: IEventTracker, + telemetryTracker: ITelemetryTracker, + storage: IStorageSync | IStorageAsync, + signalListener?: ISignalListener + splitApi?: ISplitApi + syncManager?: ISyncManager, +} + +export interface ISdkFactoryContextSync extends ISdkFactoryContext { + storage: IStorageSync, + splitApi: ISplitApi + syncManager: ISyncManager, +} + +export interface ISdkFactoryContextAsync extends ISdkFactoryContext { + storage: IStorageAsync, + splitApi: undefined, + syncManager: undefined +} + /** * Object parameter with the modules required to create an SDK factory instance */ @@ -54,7 +69,7 @@ export interface ISdkFactoryParams { // SyncManager factory. // Not required when providing an asynchronous storage (consumer mode), but required in standalone mode to avoid SDK timeout. // It can create an offline or online sync manager, with or without streaming support. - syncManagerFactory?: (params: ISyncManagerFactoryParams) => ISyncManager, + syncManagerFactory?: (params: ISdkFactoryContextSync) => ISyncManager, // Sdk manager factory sdkManagerFactory: ( diff --git a/src/sync/offline/syncManagerOffline.ts b/src/sync/offline/syncManagerOffline.ts index 253493f2..cd5c435d 100644 --- a/src/sync/offline/syncManagerOffline.ts +++ b/src/sync/offline/syncManagerOffline.ts @@ -1,9 +1,10 @@ -import { ISyncManager, ISyncManagerCS, ISyncManagerFactoryParams } from '../types'; +import { ISyncManager, ISyncManagerCS } from '../types'; import { fromObjectSyncTaskFactory } from './syncTasks/fromObjectSyncTask'; import { objectAssign } from '../../utils/lang/objectAssign'; import { ISplitsParser } from './splitsParser/types'; import { IReadinessManager } from '../../readiness/types'; import { SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; function flush() { return Promise.resolve(); @@ -17,7 +18,7 @@ function flush() { */ export function syncManagerOfflineFactory( splitsParserFactory: () => ISplitsParser -): (params: ISyncManagerFactoryParams) => ISyncManagerCS { +): (params: ISdkFactoryContextSync) => ISyncManagerCS { /** * SyncManager factory for modular SDK @@ -26,7 +27,7 @@ export function syncManagerOfflineFactory( settings, readiness, storage, - }: ISyncManagerFactoryParams): ISyncManagerCS { + }: ISdkFactoryContextSync): ISyncManagerCS { return objectAssign( fromObjectSyncTaskFactory(splitsParserFactory(), storage, readiness, settings), diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 364829a5..af1e82ca 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -7,14 +7,14 @@ import { splitsSyncTaskFactory } from './syncTasks/splitsSyncTask'; import { getMatching } from '../../utils/key'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants'; -import { ISyncManagerFactoryParams } from '../types'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; /** * Expose start / stop mechanism for polling data from services. * For client-side API with multiple clients. */ export function pollingManagerCSFactory( - params: ISyncManagerFactoryParams + params: ISdkFactoryContextSync ): IPollingManagerCS { const { splitApi, storage, readiness, settings } = params; diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index 2c7d3ec9..90f252a4 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -3,13 +3,13 @@ import { segmentsSyncTaskFactory } from './syncTasks/segmentsSyncTask'; import { IPollingManager, ISegmentsSyncTask, ISplitsSyncTask } from './types'; import { thenable } from '../../utils/promise/thenable'; import { POLLING_START, POLLING_STOP, LOG_PREFIX_SYNC_POLLING } from '../../logger/constants'; -import { ISyncManagerFactoryParams } from '../types'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; /** * Expose start / stop mechanism for pulling data from services. */ export function pollingManagerSSFactory( - params: ISyncManagerFactoryParams + params: ISdkFactoryContextSync ): IPollingManager { const { splitApi, storage, readiness, settings } = params; diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 0f9f9529..7f4625e9 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -18,8 +18,8 @@ import { isInBitmap, parseBitmap, parseKeyList } from './mySegmentsV2utils'; import { ISet, _Set } from '../../utils/lang/sets'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; import { IAuthTokenPushEnabled } from './AuthClient/types'; -import { ISyncManagerFactoryParams } from '../types'; import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; /** * PushManager factory: @@ -27,7 +27,7 @@ import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants'; * - for client-side, with support for multiple clients, if key is provided in settings */ export function pushManagerFactory( - params: ISyncManagerFactoryParams, + params: ISdkFactoryContextSync, pollingManager: IPollingManager, ): IPushManager | undefined { diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 51440a25..1eb531c9 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -2,9 +2,9 @@ import { syncTaskComposite } from '../syncTaskComposite'; import { eventsSyncTaskFactory } from './eventsSyncTask'; import { impressionsSyncTaskFactory } from './impressionsSyncTask'; import { impressionCountsSyncTaskFactory } from './impressionCountsSyncTask'; -import { ISyncManagerFactoryParams } from '../types'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; -export function submitterManagerFactory(params: ISyncManagerFactoryParams) { +export function submitterManagerFactory(params: ISdkFactoryContextSync) { const { settings, storage, splitApi } = params; const log = settings.log; diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 88c1516e..61f0603d 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -1,4 +1,4 @@ -import { ISyncManagerCS, ISyncManagerFactoryParams } from './types'; +import { ISyncManagerCS } from './types'; import { submitterManagerFactory } from './submitters/submitterManager'; import { IReadinessManager } from '../readiness/types'; import { IStorageSync } from '../storages/types'; @@ -8,6 +8,7 @@ import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; import { isConsentGranted } from '../consent'; import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; +import { ISdkFactoryContextSync } from '../sdkFactory/types'; /** * Online SyncManager factory. @@ -18,14 +19,14 @@ import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; * @param pushManagerFactory optional to build a SyncManager with or without streaming support */ export function syncManagerOnlineFactory( - pollingManagerFactory?: (params: ISyncManagerFactoryParams) => IPollingManager, - pushManagerFactory?: (params: ISyncManagerFactoryParams, pollingManager: IPollingManager) => IPushManager | undefined, -): (params: ISyncManagerFactoryParams) => ISyncManagerCS { + pollingManagerFactory?: (params: ISdkFactoryContextSync) => IPollingManager, + pushManagerFactory?: (params: ISdkFactoryContextSync, pollingManager: IPollingManager) => IPushManager | undefined, +): (params: ISdkFactoryContextSync) => ISyncManagerCS { /** * SyncManager factory for modular SDK */ - return function (params: ISyncManagerFactoryParams): ISyncManagerCS { + return function (params: ISdkFactoryContextSync): ISyncManagerCS { const { settings, settings: { log, streamingEnabled }, telemetryTracker } = params; diff --git a/src/sync/types.ts b/src/sync/types.ts index f2962885..53a25dae 100644 --- a/src/sync/types.ts +++ b/src/sync/types.ts @@ -1,9 +1,5 @@ import { IReadinessManager } from '../readiness/types'; -import { IPlatform } from '../sdkFactory/types'; -import { ISplitApi } from '../services/types'; import { IStorageSync } from '../storages/types'; -import { ITelemetryTracker } from '../trackers/types'; -import { ISettings } from '../types'; import { IPollingManager } from './polling/types'; import { IPushManager } from './streaming/types'; @@ -53,12 +49,3 @@ export interface ISyncManager extends ITask { export interface ISyncManagerCS extends ISyncManager { shared(matchingKey: string, readinessManager: IReadinessManager, storage: IStorageSync): ISyncManager | undefined } - -export interface ISyncManagerFactoryParams { - settings: ISettings, - readiness: IReadinessManager, - storage: IStorageSync, - splitApi: ISplitApi, - platform: IPlatform, - telemetryTracker: ITelemetryTracker -} diff --git a/src/types.ts b/src/types.ts index 6c5ba33e..1dc35037 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,11 @@ import { ISplitFiltersValidation } from './dtos/types'; import { IIntegration, IIntegrationFactoryParams } from './integrations/types'; import { ILogger } from './logger/types'; +import { ISdkFactoryContext } from './sdkFactory/types'; /* eslint-disable no-use-before-define */ import { IStorageFactoryParams, IStorageSync, IStorageAsync, IStorageSyncFactory, IStorageAsyncFactory } from './storages/types'; -import { ISyncManagerFactoryParams, ISyncManagerCS } from './sync/types'; +import { ISyncManagerCS } from './sync/types'; /** * Reduced version of NodeJS.EventEmitter interface with the minimal methods used by the SDK @@ -611,7 +612,7 @@ export namespace SplitIO { */ export type LocalhostFactory = { type: 'LocalhostFromObject' | 'LocalhostFromFile' - (params: ISyncManagerFactoryParams): ISyncManagerCS + (params: ISdkFactoryContext): ISyncManagerCS } /** * Impression listener interface. This is the interface that needs to be implemented From aefddbff2e05abff3c18b68542e5dad2dedc7f5b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 27 Apr 2022 12:25:11 -0300 Subject: [PATCH 23/70] polishing --- src/__tests__/testUtils/index.ts | 14 ++++++++++++++ src/trackers/__tests__/telemetryTracker.spec.ts | 7 ++++--- src/trackers/telemetryTracker.ts | 13 ++++++------- 3 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 src/__tests__/testUtils/index.ts diff --git a/src/__tests__/testUtils/index.ts b/src/__tests__/testUtils/index.ts new file mode 100644 index 00000000..568f149f --- /dev/null +++ b/src/__tests__/testUtils/index.ts @@ -0,0 +1,14 @@ +const DEFAULT_ERROR_MARGIN = 50; // 0.05 secs if numbers are timestamps in milliseconds + +/** + * Assert if an `actual` and `expected` numeric values are nearly equal. + * + * @param {number} actual actual time lapse in millis + * @param {number} expected expected time lapse in millis + * @param {number} epsilon error margin in millis + * @returns {boolean} whether the absolute difference is minor to epsilon value or not + */ +export function nearlyEqual(actual: number, expected: number, epsilon = DEFAULT_ERROR_MARGIN) { + const diff = Math.abs(actual - expected); + return diff <= epsilon; +} diff --git a/src/trackers/__tests__/telemetryTracker.spec.ts b/src/trackers/__tests__/telemetryTracker.spec.ts index ea1af1bf..babb280d 100644 --- a/src/trackers/__tests__/telemetryTracker.spec.ts +++ b/src/trackers/__tests__/telemetryTracker.spec.ts @@ -1,4 +1,5 @@ import { EXCEPTION, SDK_NOT_READY } from '../../utils/labels'; +import { nearlyEqual } from '../../__tests__/testUtils'; import { telemetryTrackerFactory } from '../telemetryTracker'; describe('Telemetry Tracker', () => { @@ -9,7 +10,7 @@ describe('Telemetry Tracker', () => { recordException: jest.fn(), recordNonReadyUsage: jest.fn(), }; - // @ts-ignore + const tracker = telemetryTrackerFactory(fakeTelemetryCache, fakeNow); test('trackEval', async () => { @@ -25,7 +26,7 @@ describe('Telemetry Tracker', () => { stopTracker = tracker.trackEval('tcs'); - await new Promise(res => setTimeout(res, 50)); + await new Promise(res => setTimeout(res, 100)); stopTracker(); expect(fakeTelemetryCache.recordException).toBeCalledTimes(1); @@ -33,7 +34,7 @@ describe('Telemetry Tracker', () => { expect(fakeTelemetryCache.recordLatency).toBeCalledTimes(3); const latency = fakeTelemetryCache.recordLatency.mock.calls[2][1]; - expect(latency >= 50 && latency < 100).toBeTruthy(); // last tracked latency is around 200 ms + expect(nearlyEqual(latency, 100)).toBeTruthy(); // last tracked latency is around 100 ms }); }); diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index eb3b40dd..0101854b 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -1,5 +1,4 @@ import { TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; -import { Method } from '../sync/submitters/types'; import { EXCEPTION, SDK_NOT_READY } from '../utils/labels'; import { ITelemetryTracker } from './types'; import { timer } from '../utils/timeTracker/timer'; @@ -12,10 +11,10 @@ export function telemetryTrackerFactory( if (telemetryCache && now) { return { - trackEval(method: Method) { - const stopTimer = timer(now); + trackEval(method) { + const evalTime = timer(now); - return (label?: string) => { + return (label) => { switch (label) { case EXCEPTION: telemetryCache.recordException(method); @@ -23,15 +22,15 @@ export function telemetryTrackerFactory( case SDK_NOT_READY: // @ts-ignore. TelemetryCacheAsync doesn't implement the method telemetryCache?.recordNonReadyUsage(); } - telemetryCache.recordLatency(method, stopTimer()); + telemetryCache.recordLatency(method, evalTime()); }; - } + }, }; } else { // If there is not `telemetryCache` or `now` time tracker, return a no-op telemetry tracker const noopTrack = () => () => { }; return { - trackEval: noopTrack + trackEval: noopTrack, }; } } From 6aaef8b912d0ee5e76f6508d2f7cf213e6fb84c3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 27 Apr 2022 13:25:29 -0300 Subject: [PATCH 24/70] updated unit tests --- src/sdkClient/sdkClient.ts | 4 ++-- .../SSEHandler/__tests__/index.spec.ts | 20 +++++++++++++------ src/sync/streaming/pushManager.ts | 6 +++--- src/trackers/__tests__/eventTracker.spec.ts | 11 ++++++++-- .../__tests__/impressionsTracker.spec.ts | 10 ++++++++-- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index e6aa3473..673825cf 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -9,7 +9,7 @@ import { ISdkFactoryContext } from '../sdkFactory/types'; * Creates an Sdk client, i.e., a base client with status and destroy interface */ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: boolean): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager, syncManager, storage, signalListener, settings } = params; + const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker } = params; return objectAssign( // Proto-linkage of the readiness Event Emitter @@ -26,7 +26,7 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo { destroy() { // record stat before flushing data - if (!isSharedClient) params.telemetryTracker.sessionLength(); + if (!isSharedClient) telemetryTracker.sessionLength(); // Stop background jobs syncManager && syncManager.stop(); diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index fea55027..9651d232 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -34,12 +34,14 @@ const controlStreamingDisabledSec = { ...controlStreamingDisabled, data: control // streaming reset message, from `{orgHash}_{envHash}_control` channel import streamingReset from '../../../../__tests__/mocks/message.STREAMING_RESET.json'; +import { ABLY_ERROR, NON_REQUESTED, SSE_CONNECTION_ERROR } from '../../../../utils/constants'; const pushEmitter = { emit: jest.fn() }; +const telemetryTracker = { streamingEvent: jest.fn() }; test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (NotificationKeeper)', () => { pushEmitter.emit.mockClear(); - const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter, telemetryTracker); // handleOpen @@ -86,7 +88,7 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { pushEmitter.emit.mockClear(); - const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter, telemetryTracker); sseHandler.handleOpen(); // CONTROL messages @@ -119,7 +121,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { sseHandler.handleMessage(controlStreamingDisabledSec); // testing STREAMING_DISABLED with second region expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_NONRETRYABLE_ERROR); // must emit PUSH_NONRETRYABLE_ERROR if received a STREAMING_DISABLED control message - const sseHandler2 = SSEHandlerFactory(loggerMock, pushEmitter); + const sseHandler2 = SSEHandlerFactory(loggerMock, pushEmitter, telemetryTracker); sseHandler2.handleOpen(); sseHandler2.handleMessage(controlStreamingPausedSec); // testing STREAMING_PAUSED with second region @@ -131,7 +133,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { }); test('`handlerMessage` for update notifications (NotificationProcessor) and streaming reset', () => { - const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter, telemetryTracker); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); @@ -173,38 +175,44 @@ test('`handlerMessage` for update notifications (NotificationProcessor) and stre }); test('handleError', () => { - const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter, telemetryTracker); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); const error = 'some error'; sseHandler.handleError(error); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_RETRYABLE_ERROR); // A network error must emit PUSH_RETRYABLE_ERROR + expect(telemetryTracker.streamingEvent).toHaveBeenLastCalledWith(SSE_CONNECTION_ERROR, NON_REQUESTED); const errorWithData = { data: '{ "message": "error message"}' }; sseHandler.handleError(errorWithData); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_RETRYABLE_ERROR); // An error without Ably code must emit PUSH_RETRYABLE_ERROR + expect(telemetryTracker.streamingEvent).toHaveBeenLastCalledWith(SSE_CONNECTION_ERROR, NON_REQUESTED); const errorWithBadData = { data: '{"message"error"' }; sseHandler.handleError(errorWithBadData); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_RETRYABLE_ERROR); // An error that cannot be parsed must emit PUSH_RETRYABLE_ERROR + expect(telemetryTracker.streamingEvent).toHaveBeenLastCalledWith(SSE_CONNECTION_ERROR, NON_REQUESTED); const ably4XXRecoverableError = { data: '{"message":"Token expired","code":40142,"statusCode":401}' }; sseHandler.handleError(ably4XXRecoverableError); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_RETRYABLE_ERROR); // An Ably recoverable error must emit PUSH_RETRYABLE_ERROR + expect(telemetryTracker.streamingEvent).toHaveBeenLastCalledWith(ABLY_ERROR, 40142); const ably4XXNonRecoverableError = { data: '{"message":"Token expired","code":42910,"statusCode":429}' }; sseHandler.handleError(ably4XXNonRecoverableError); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_NONRETRYABLE_ERROR); // An Ably non-recoverable error must emit PUSH_NONRETRYABLE_ERROR + expect(telemetryTracker.streamingEvent).toHaveBeenLastCalledWith(ABLY_ERROR, 42910); const ably5XXError = { data: '{"message":"...","code":50000,"statusCode":500}' }; sseHandler.handleError(ably5XXError); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_RETRYABLE_ERROR); // An Ably recoverable error must emit PUSH_RETRYABLE_ERROR + expect(telemetryTracker.streamingEvent).toHaveBeenLastCalledWith(ABLY_ERROR, 50000); }); test('handlerMessage: ignore invalid events', () => { - const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter, telemetryTracker); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 0f9f9529..fc80de04 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -31,7 +31,7 @@ export function pushManagerFactory( pollingManager: IPollingManager, ): IPushManager | undefined { - const { settings, storage: { segments, splits }, splitApi, readiness, platform, telemetryTracker } = params; + const { settings, storage, splitApi, readiness, platform, telemetryTracker } = params; // `userKey` is the matching key of main client in client-side SDK. // It can be used to check if running on client-side or server-side SDK. @@ -55,9 +55,9 @@ export function pushManagerFactory( // init workers // MySegmentsUpdateWorker (client-side) are initiated in `add` method - const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(pollingManager.segmentsSyncTask, segments); + const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(pollingManager.segmentsSyncTask, storage.segments); // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments - const splitsUpdateWorker = new SplitsUpdateWorker(splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); + const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker const userKeyHashes: Record = {}; diff --git a/src/trackers/__tests__/eventTracker.spec.ts b/src/trackers/__tests__/eventTracker.spec.ts index d770d899..18dba632 100644 --- a/src/trackers/__tests__/eventTracker.spec.ts +++ b/src/trackers/__tests__/eventTracker.spec.ts @@ -1,4 +1,5 @@ import { SplitIO } from '../../types'; +import { QUEUED } from '../../utils/constants'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { eventTrackerFactory } from '../eventTracker'; @@ -8,6 +9,10 @@ const fakeEventsCache = { track: jest.fn() }; +const fakeTelemetryCache = { + recordEventStats: jest.fn() +}; + const fakeIntegrationsManager = { handleEvent: jest.fn() }; @@ -45,13 +50,14 @@ describe('Event Tracker', () => { } } }); - - const tracker = eventTrackerFactory(fullSettings, fakeEventsCache, fakeIntegrationsManager); + // @ts-ignore + const tracker = eventTrackerFactory(fullSettings, fakeEventsCache, fakeIntegrationsManager, fakeTelemetryCache); const result1 = tracker.track(fakeEvent, 1); expect(fakeEventsCache.track.mock.calls[0]).toEqual([fakeEvent, 1]); // Should be present in the event cache. expect(fakeIntegrationsManager.handleEvent).not.toBeCalled(); // The integration manager handleEvent method should not be executed synchronously. expect(result1).toBe(true); // Should return the value of the event cache. + expect(fakeTelemetryCache.recordEventStats).toBeCalledWith(QUEUED, 1); await new Promise(res => setTimeout(res)); expect(fakeIntegrationsManager.handleEvent.mock.calls[0]).toEqual([fakeEvent]); // A copy of the tracked event should be sent to integration manager after the timeout wrapping make it to the queue stack. @@ -79,6 +85,7 @@ describe('Event Tracker', () => { expect(fakeIntegrationsManager.handleEvent.mock.calls[1]).toEqual([fakeEvent]); // A copy of tracked event should be sent to integration manager after the timeout wrapping make it to the queue stack. expect(fakeIntegrationsManager.handleEvent.mock.calls[1][0]).not.toBe(fakeEvent); // Should not send the original event. + expect(fakeTelemetryCache.recordEventStats).toBeCalledTimes(1); // Only the first of the 3 track calls is using sync events cache }); test('Should track or not events depending on user consent status', () => { diff --git a/src/trackers/__tests__/impressionsTracker.spec.ts b/src/trackers/__tests__/impressionsTracker.spec.ts index 0a35db12..5ca63895 100644 --- a/src/trackers/__tests__/impressionsTracker.spec.ts +++ b/src/trackers/__tests__/impressionsTracker.spec.ts @@ -4,12 +4,16 @@ import { impressionObserverSSFactory } from '../impressionObserver/impressionObs import { impressionObserverCSFactory } from '../impressionObserver/impressionObserverCS'; import { ImpressionDTO } from '../../types'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; +import { DEDUPED, QUEUED } from '../../utils/constants'; /* Mocks */ const fakeImpressionsCache = { track: jest.fn() }; +const fakeTelemetryCache = { + recordImpressionStats: jest.fn() +}; const fakeListener = { logImpression: jest.fn() }; @@ -168,8 +172,8 @@ describe('Impressions Tracker', () => { impression2.time = Date.now(); impression3.time = Date.now(); - const impressionCountsCache = new ImpressionCountsCacheInMemory(); - const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, undefined, impressionObserverCSFactory(), impressionCountsCache); + const impressionCountsCache = new ImpressionCountsCacheInMemory(); // @ts-ignore + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, undefined, impressionObserverCSFactory(), impressionCountsCache, fakeTelemetryCache); expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker @@ -185,6 +189,8 @@ describe('Impressions Tracker', () => { expect(lastArgs[0][1].feature).toBe('qc_team_2'); expect(Object.keys(impressionCountsCache.state()).length).toBe(2); + expect(fakeTelemetryCache.recordImpressionStats.mock.calls).toEqual([[QUEUED, 2], [DEDUPED, 1]]); + }); test('Should track or not impressions depending on user consent status', () => { From 31f630bb89e01b10368c5f9b90dab2e1084aac7f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 27 Apr 2022 14:14:22 -0300 Subject: [PATCH 25/70] update comment --- src/storages/inRedis/TelemetryCacheInRedis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts index f0068320..e973baef 100644 --- a/src/storages/inRedis/TelemetryCacheInRedis.ts +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -8,7 +8,7 @@ import { Redis } from 'ioredis'; export class TelemetryCacheInRedis implements TelemetryCacheAsync { /** - * Create a Telemetry cache that uses a storage wrapper. + * Create a Telemetry cache that uses Redis as storage. * @param log Logger instance. * @param keys Key builder. * @param redis Redis client. From c12922577348e723abe5964e975438d0ea620073 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 10:55:38 -0300 Subject: [PATCH 26/70] refactor method signature of splitHttpClient --- src/services/__tests__/splitApi.spec.ts | 5 ++++- src/services/splitApi.ts | 24 ++++++++++++------------ src/services/splitHttpClient.ts | 6 +++--- src/services/types.ts | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 3aa41d7e..ca4c172f 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -5,7 +5,7 @@ import { settingsSplitApi } from '../../utils/settingsValidation/__tests__/setti const settingsWithRuntime = { ...settingsSplitApi, runtime: { ip: 'ip', hostname: 'hostname' } } as ISettings; -const telemetryTrackerMock = { trackHttp() { return () => { }; } }; +const telemetryTrackerMock = { trackHttp: jest.fn(() => () => { }) }; function assertHeaders(settings: ISettings, headers: Record) { expect(headers['Accept']).toBe('application/json'); @@ -51,6 +51,9 @@ describe('splitApi', () => { splitApi.postMetricsUsage('fake-body'); assertHeaders(settings, fetchMock.mock.calls[8][1].headers); + expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(9); + + telemetryTrackerMock.trackHttp.mockClear(); fetchMock.mockClear(); }); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index e713c4f2..37d2deab 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -32,12 +32,12 @@ export function splitApiFactory( return { getSdkAPIHealthCheck() { const url = `${urls.sdk}/version`; - return splitHttpClient(() => { }, url).then(() => true).catch(() => false); + return splitHttpClient(url).then(() => true).catch(() => false); }, getEventsAPIHealthCheck() { const url = `${urls.events}/version`; - return splitHttpClient(() => { }, url).then(() => true).catch(() => false); + return splitHttpClient(url).then(() => true).catch(() => false); }, fetchAuth(userMatchingKeys?: string[]) { @@ -47,17 +47,17 @@ export function splitApiFactory( if (queryParams) // accounting the possibility that `userKeys` and thus `queryParams` are empty url += '?' + queryParams; } - return splitHttpClient(telemetryTracker.trackHttp(TOKEN), url); + return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN)); }, fetchSplitChanges(since: number, noCache?: boolean) { const url = `${urls.sdk}/splitChanges?since=${since}${filterQueryString || ''}`; - return splitHttpClient(telemetryTracker.trackHttp(SPLITS), url, noCache ? noCacheHeaderOptions : undefined); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)); }, fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean) { const url = `${urls.sdk}/segmentChanges/${segmentName}?since=${since}`; - return splitHttpClient(telemetryTracker.trackHttp(SEGMENT), url, noCache ? noCacheHeaderOptions : undefined); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SEGMENT)); }, fetchMySegments(userMatchingKey: string, noCache?: boolean) { @@ -68,7 +68,7 @@ export function splitApiFactory( * - match user keys with special characters. E.g.: 'foo%bar', 'foo/bar' */ const url = `${urls.sdk}/mySegments/${encodeURIComponent(userMatchingKey)}`; - return splitHttpClient(telemetryTracker.trackHttp(MY_SEGMENT), url, noCache ? noCacheHeaderOptions : undefined); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(MY_SEGMENT)); }, /** @@ -79,7 +79,7 @@ export function splitApiFactory( */ postEventsBulk(body: string, headers?: Record) { const url = `${urls.events}/events/bulk`; - return splitHttpClient(telemetryTracker.trackHttp(EVENTS), url, { method: 'POST', body, headers }); + return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(EVENTS)); }, /** @@ -90,10 +90,10 @@ export function splitApiFactory( */ postTestImpressionsBulk(body: string, headers?: Record) { const url = `${urls.events}/testImpressions/bulk`; - return splitHttpClient(telemetryTracker.trackHttp(IMPRESSIONS), url, { + return splitHttpClient(url, { // Adding extra headers to send impressions in OPTIMIZED or DEBUG modes. method: 'POST', body, headers: objectAssign({ SplitSDKImpressionsMode }, headers) - }); + }, telemetryTracker.trackHttp(IMPRESSIONS)); }, /** @@ -104,17 +104,17 @@ export function splitApiFactory( */ postTestImpressionsCount(body: string, headers?: Record) { const url = `${urls.events}/testImpressions/count`; - return splitHttpClient(telemetryTracker.trackHttp(IMPRESSIONS_COUNT), url, { method: 'POST', body, headers }); + return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT)); }, postMetricsConfig(body: string) { const url = `${urls.telemetry}/metrics/config`; - return splitHttpClient(telemetryTracker.trackHttp(TELEMETRY), url, { method: 'POST', body }, true); + return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true); }, postMetricsUsage(body: string) { const url = `${urls.telemetry}/metrics/usage`; - return splitHttpClient(telemetryTracker.trackHttp(TELEMETRY), url, { method: 'POST', body }, true); + return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true); } }; } diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index 7ade55b6..25d70f0b 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -31,7 +31,7 @@ export function splitHttpClientFactory(settings: Pick void, url: string, reqOpts: IRequestOptions = {}, logErrorsAsInfo: boolean = false): Promise { + return function httpClient(url: string, reqOpts: IRequestOptions = {}, latencyTracker: (error?: NetworkError) => void = () => { }, logErrorsAsInfo: boolean = false): Promise { const request = objectAssign({ headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers, @@ -46,7 +46,7 @@ export function splitHttpClientFactory(settings: Pick Promise.reject({ response, message })); } - tracker(); + latencyTracker(); return response; }) .catch(error => { @@ -73,7 +73,7 @@ export function splitHttpClientFactory(settings: Pick Promise Promise -export type ISplitHttpClient = (tracker: (error?: NetworkError) => void, url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise +export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean) => Promise export type IFetchAuth = (userKeys?: string[]) => Promise From 7b68cd2c27c642ad1897f95f3a5109a24565abc2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 12:47:16 -0300 Subject: [PATCH 27/70] comments --- src/sdkClient/client.ts | 1 - src/storages/types.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 59226c12..3d3f0fd6 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -13,7 +13,6 @@ import { ISdkFactoryContext } from '../sdkFactory/types'; /** * Creator of base client with getTreatments and track methods. */ -// @TODO missing time tracking to collect telemetry export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient { const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker, telemetryTracker } = params; const { log, mode } = settings; diff --git a/src/storages/types.ts b/src/storages/types.ts index 9d7fa131..47ce9120 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -331,11 +331,13 @@ export interface IRecorderCacheProducerAsync { export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheProducerAsync { // Consumer API method, used by impressions tracker (in standalone and consumer modes) to push data into. + // The result promise can reject. track(data: ImpressionDTO[]): Promise } export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheProducerAsync { // Consumer API method, used by events tracker (in standalone and consumer modes) to push data into. + // The result promise cannot reject. track(data: SplitIO.EventData, size?: number): Promise } From d4a73a5d9a1ead004d6e7943c38dfa520a50ccba Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 14:39:08 -0300 Subject: [PATCH 28/70] update storages and add method for telemetry --- src/storages/AbstractSegmentsCacheSync.ts | 11 +++++--- .../inLocalStorage/MySegmentsCacheInLocal.ts | 14 ++++++++++- .../__tests__/MySegmentsCacheInLocal.spec.ts | 9 +++++-- src/storages/inLocalStorage/index.ts | 3 +++ src/storages/inMemory/InMemoryStorage.ts | 2 ++ src/storages/inMemory/InMemoryStorageCS.ts | 3 +++ .../inMemory/MySegmentsCacheInMemory.ts | 8 ++++++ .../inMemory/SegmentsCacheInMemory.ts | 6 +++++ .../__tests__/MySegmentsCacheInMemory.spec.ts | 9 +++++-- .../__tests__/SegmentsCacheInMemory.spec.ts | 9 ++++++- src/storages/types.ts | 25 +++++++++++-------- .../polling/updaters/mySegmentsUpdater.ts | 4 --- .../polling/updaters/segmentChangesUpdater.ts | 12 ++------- .../polling/updaters/splitChangesUpdater.ts | 6 +---- 14 files changed, 82 insertions(+), 39 deletions(-) diff --git a/src/storages/AbstractSegmentsCacheSync.ts b/src/storages/AbstractSegmentsCacheSync.ts index ad371912..23b3a491 100644 --- a/src/storages/AbstractSegmentsCacheSync.ts +++ b/src/storages/AbstractSegmentsCacheSync.ts @@ -37,10 +37,15 @@ export abstract class AbstractSegmentsCacheSync implements ISegmentsCacheSync { registerSegments(names: string[]): boolean { return false; } /** - * For server-side synchronizer: get the list of segments in the cache. - * For client-side synchronizer: the method is not used. + * For server-side synchronizer: get the list of segments to fetch changes. + * Also used for the `seC` (segment count) telemetry stat. + */ + abstract getRegisteredSegments(): string[] + + /** + * Only used for the `skC`(segment keys count) telemetry stat: 1 for client-side, and total count of keys in server-side. */ - getRegisteredSegments(): string[] { return []; } + abstract getKeysCount(): number /** * For server-side synchronizer: set the change number of `name` segment. diff --git a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts index 627d1fcf..849a344b 100644 --- a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts @@ -72,7 +72,7 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { if (segmentName) { accum.push(segmentName); } else { - // @BREAKING: This is only to clean up "old" keys. Remove this whole else code block. + // @TODO @BREAKING: This is only to clean up "old" keys. Remove this whole else code block and reuse `getRegisteredSegments` method. segmentName = this.keys.extractOldSegmentKey(key); if (segmentName) { // this was an old segment key, let's clean up. @@ -121,4 +121,16 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { return isDiff; } + getRegisteredSegments(): string[] { + return Object.keys(localStorage).reduce((accum, key) => { + const segmentName = this.keys.extractSegmentName(key); + if (segmentName) accum.push(segmentName); + return accum; + }, []); + } + + getKeysCount() { + return 1; + } + } diff --git a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts index 67c9efcc..ae6dac82 100644 --- a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts @@ -9,12 +9,17 @@ test('SEGMENT CACHE / in LocalStorage', () => { cache.clear(); cache.addToSegment('mocked-segment'); + cache.addToSegment('mocked-segment-2'); - expect(cache.isInSegment('mocked-segment') === true).toBe(true); + expect(cache.isInSegment('mocked-segment')).toBe(true); + expect(cache.getRegisteredSegments()).toEqual(['mocked-segment', 'mocked-segment-2']); + expect(cache.getKeysCount()).toBe(1); cache.removeFromSegment('mocked-segment'); - expect(cache.isInSegment('mocked-segment') === false).toBe(true); + expect(cache.isInSegment('mocked-segment')).toBe(false); + expect(cache.getRegisteredSegments()).toEqual(['mocked-segment-2']); + expect(cache.getKeysCount()).toBe(1); }); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 68604e30..174aa650 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -13,6 +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'; export interface InLocalStorageOptions { prefix?: string @@ -43,6 +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(), destroy() { this.splits = new SplitsCacheInMemory(); @@ -62,6 +64,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn impressions: this.impressions, impressionCounts: this.impressionCounts, events: this.events, + telemetry: this.telemetry, destroy() { this.splits = new SplitsCacheInMemory(); diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 6f446f20..ced292fc 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -5,6 +5,7 @@ import { EventsCacheInMemory } from './EventsCacheInMemory'; import { IStorageFactoryParams, IStorageSync } from '../types'; import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; import { STORAGE_MEMORY } from '../../utils/constants'; +import { TelemetryCacheInMemory } from './TelemetryCacheInMemory'; /** * InMemory storage factory for standalone server-side SplitFactory @@ -19,6 +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(), // 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 4fa9b962..15e57fb6 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -5,6 +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'; /** * InMemory storage factory for standalone client-side SplitFactory @@ -19,6 +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(), // When using MEMORY we should clean all the caches to leave them empty destroy() { @@ -37,6 +39,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag impressions: this.impressions, impressionCounts: this.impressionCounts, events: this.events, + telemetry: this.telemetry, // Set a new splits cache to clean it for the client without affecting other clients destroy() { diff --git a/src/storages/inMemory/MySegmentsCacheInMemory.ts b/src/storages/inMemory/MySegmentsCacheInMemory.ts index 02519d18..08a767a1 100644 --- a/src/storages/inMemory/MySegmentsCacheInMemory.ts +++ b/src/storages/inMemory/MySegmentsCacheInMemory.ts @@ -72,4 +72,12 @@ export class MySegmentsCacheInMemory extends AbstractSegmentsCacheSync { return isDiff; } + getRegisteredSegments() { + return Object.keys(this.segmentCache); + } + + getKeysCount() { + return 1; + } + } diff --git a/src/storages/inMemory/SegmentsCacheInMemory.ts b/src/storages/inMemory/SegmentsCacheInMemory.ts index ca86c86d..a7d52b7c 100644 --- a/src/storages/inMemory/SegmentsCacheInMemory.ts +++ b/src/storages/inMemory/SegmentsCacheInMemory.ts @@ -68,6 +68,12 @@ export class SegmentsCacheInMemory extends AbstractSegmentsCacheSync { return Object.keys(this.segmentCache); } + getKeysCount() { + return Object.keys(this.segmentCache).reduce((acum, segmentName) => { + return acum + this.segmentCache[segmentName].size; + }, 0); + } + setChangeNumber(name: string, changeNumber: number) { this.segmentChangeNumber[name] = changeNumber; diff --git a/src/storages/inMemory/__tests__/MySegmentsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/MySegmentsCacheInMemory.spec.ts index 3ac0717c..7236e950 100644 --- a/src/storages/inMemory/__tests__/MySegmentsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/MySegmentsCacheInMemory.spec.ts @@ -4,11 +4,16 @@ test('MY SEGMENTS CACHE / in memory', () => { const cache = new MySegmentsCacheInMemory(); cache.addToSegment('mocked-segment'); + cache.addToSegment('mocked-segment-2'); - expect(cache.isInSegment('mocked-segment') === true).toBe(true); + expect(cache.isInSegment('mocked-segment')).toBe(true); + expect(cache.getRegisteredSegments()).toEqual(['mocked-segment', 'mocked-segment-2']); + expect(cache.getKeysCount()).toBe(1); cache.removeFromSegment('mocked-segment'); - expect(cache.isInSegment('mocked-segment') === false).toBe(true); + expect(cache.isInSegment('mocked-segment')).toBe(false); + expect(cache.getRegisteredSegments()).toEqual(['mocked-segment-2']); + expect(cache.getKeysCount()).toBe(1); }); diff --git a/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts index 4d868849..e6713376 100644 --- a/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts @@ -2,7 +2,7 @@ import { SegmentsCacheInMemory } from '../SegmentsCacheInMemory'; describe('SEGMENTS CACHE IN MEMORY', () => { - test('isInSegment, set/getChangeNumber, add/removeFromSegment', () => { + test('isInSegment, set/getChangeNumber, add/removeFromSegment, getKeysCount', () => { const cache = new SegmentsCacheInMemory(); cache.addToSegment('mocked-segment', [ @@ -26,6 +26,13 @@ describe('SEGMENTS CACHE IN MEMORY', () => { expect(cache.isInSegment('mocked-segment', 'c')).toBe(false); // c expect(cache.isInSegment('mocked-segment', 'd')).toBe(true); // d expect(cache.isInSegment('mocked-segment', 'e')).toBe(true); // e + + // getKeysCount + expect(cache.getKeysCount()).toBe(3); + cache.addToSegment('mocked-segment-2', ['a', 'b', 'c', 'd', 'e']); + expect(cache.getKeysCount()).toBe(8); + cache.clear(); + expect(cache.getKeysCount()).toBe(0); }); test('registerSegment / getRegisteredSegments', async () => { diff --git a/src/storages/types.ts b/src/storages/types.ts index 9d7fa131..4c9739e0 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -262,6 +262,7 @@ export interface ISegmentsCacheSync extends ISegmentsCacheBase { isInSegment(name: string, key?: string): boolean registerSegments(names: string[]): boolean getRegisteredSegments(): string[] + getKeysCount(): number // only used for telemetry setChangeNumber(name: string, changeNumber: number): boolean getChangeNumber(name: string): number resetSegments(names: string[]): boolean // only for Sync Client-Side @@ -331,11 +332,13 @@ export interface IRecorderCacheProducerAsync { export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheProducerAsync { // Consumer API method, used by impressions tracker (in standalone and consumer modes) to push data into. + // The result promise can reject. track(data: ImpressionDTO[]): Promise } export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheProducerAsync { // Consumer API method, used by events tracker (in standalone and consumer modes) to push data into. + // The result promise cannot reject. track(data: SplitIO.EventData, size?: number): Promise } @@ -442,34 +445,34 @@ export interface IStorageBase< TSplitsCache extends ISplitsCacheBase, TSegmentsCache extends ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase, - TEventsCache extends IEventsCacheBase, - TTelemetryCache extends TelemetryCacheSync | TelemetryCacheAsync + TEventsCache extends IEventsCacheBase > { splits: TSplitsCache, segments: TSegmentsCache, impressions: TImpressionsCache, impressionCounts?: IImpressionCountsCacheSync, events: TEventsCache, - telemetry?: TTelemetryCache, destroy(): void | Promise, shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this } -export type IStorageSync = IStorageBase< +export interface IStorageSync extends IStorageBase< ISplitsCacheSync, ISegmentsCacheSync, IImpressionsCacheSync, - IEventsCacheSync, - TelemetryCacheSync -> + IEventsCacheSync + > { + telemetry: TelemetryCacheSync +} -export type IStorageAsync = IStorageBase< +export interface IStorageAsync extends IStorageBase< ISplitsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, - IEventsCacheAsync | IEventsCacheSync, - TelemetryCacheAsync | TelemetryCacheSync -> + IEventsCacheAsync | IEventsCacheSync +> { + telemetry?: TelemetryCacheAsync +} /** StorageFactory */ diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 486cbe00..0028309f 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -33,10 +33,6 @@ export function mySegmentsUpdaterFactory( function _promiseDecorator(promise: Promise) { if (startingUp) promise = timeout(requestTimeoutBeforeReady, promise); return promise; - - // @TODO telemetry - // NOTE: We only collect metrics on startup. - // mySegmentsPromise = tracker.start(tracker.TaskNames.MY_SEGMENTS_FETCH, startingUp ? metricCollectors : false, mySegmentsPromise); } // @TODO if allowing pluggable storages, handle async execution diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 90a1bc06..846bc481 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -1,7 +1,7 @@ import { ISegmentChangesFetcher } from '../fetchers/types'; import { ISegmentsCacheBase } from '../../../storages/types'; import { IReadinessManager } from '../../../readiness/types'; -import { ISegmentChangesResponse, MaybeThenable } from '../../../dtos/types'; +import { MaybeThenable } from '../../../dtos/types'; import { findIndex } from '../../../utils/lang'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; @@ -30,14 +30,6 @@ export function segmentChangesUpdaterFactory( let readyOnAlreadyExistentState = true; - /** telemetry decorator for `segmentChangesFetcher` promise */ - function _promiseDecorator(promise: Promise) { - return promise; - // @TODO handle telemetry? - // const collectMetrics = startingUp || isNode; // If we are on the browser, only collect this metric for first fetch. On node do it always. - // splitsPromise = tracker.start(tracker.TaskNames.SPLITS_FETCH, collectMetrics ? metricCollectors : false, splitsPromise); - } - /** * Segments updater returns a promise that resolves with a `false` boolean value if it fails at least to fetch a segment or synchronize it with the storage. * Thus, a false result doesn't imply that SDK_SEGMENTS_ARRIVED was not emitted. @@ -67,7 +59,7 @@ export function segmentChangesUpdaterFactory( // if fetchOnlyNew flag, avoid processing already fetched segments if (fetchOnlyNew && since !== -1) return -1; - return segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { + return segmentChangesFetcher(since, segmentName, noCache).then(function (changes) { let changeNumber = -1; const results: MaybeThenable[] = []; changes.forEach(x => { diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 822dbf13..12590ed5 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -97,14 +97,10 @@ export function splitChangesUpdaterFactory( let startingUp = true; - /** timeout and telemetry decorator for `splitChangesFetcher` promise */ + /** timeout decorator for `splitChangesFetcher` promise */ function _promiseDecorator(promise: Promise) { if (startingUp && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise); return promise; - - // @TODO telemetry - // const collectMetrics = startingUp || isNode; // If we are on the browser, only collect this metric for first fetch. On node do it always. - // splitsPromise = tracker.start(tracker.TaskNames.SPLITS_FETCH, collectMetrics ? metricCollectors : false, splitsPromise); } /** From bb99a24117a0302391d879c3f602d18303ec0270 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 15:15:29 -0300 Subject: [PATCH 29/70] telemetry submitter --- src/listeners/browser.ts | 4 +- src/logger/messages/info.ts | 2 +- src/logger/messages/warn.ts | 4 +- ...ncTask.spec.ts => eventsSubmitter.spec.ts} | 8 +-- .../{eventsSyncTask.ts => eventsSubmitter.ts} | 13 ++--- ...ncTask.ts => impressionCountsSubmitter.ts} | 12 ++--- ...onsSyncTask.ts => impressionsSubmitter.ts} | 12 ++--- .../{submitterSyncTask.ts => submitter.ts} | 25 ++++----- src/sync/submitters/submitterManager.ts | 15 +++--- src/sync/submitters/telemetrySubmitter.ts | 52 +++++++++++++++++++ src/sync/submitters/types.ts | 4 +- src/sync/types.ts | 4 -- 12 files changed, 96 insertions(+), 59 deletions(-) rename src/sync/submitters/__tests__/{eventsSyncTask.spec.ts => eventsSubmitter.spec.ts} (85%) rename src/sync/submitters/{eventsSyncTask.ts => eventsSubmitter.ts} (74%) rename src/sync/submitters/{impressionCountsSyncTask.ts => impressionCountsSubmitter.ts} (71%) rename src/sync/submitters/{impressionsSyncTask.ts => impressionsSubmitter.ts} (81%) rename src/sync/submitters/{submitterSyncTask.ts => submitter.ts} (60%) create mode 100644 src/sync/submitters/telemetrySubmitter.ts diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index 5bbd3496..a7deef15 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -2,8 +2,8 @@ // @TODO eventually migrate to JS-Browser-SDK package. import { ISignalListener } from './types'; import { IRecorderCacheProducerSync, IStorageSync } from '../storages/types'; -import { fromImpressionsCollector } from '../sync/submitters/impressionsSyncTask'; -import { fromImpressionCountsCollector } from '../sync/submitters/impressionCountsSyncTask'; +import { fromImpressionsCollector } from '../sync/submitters/impressionsSubmitter'; +import { fromImpressionCountsCollector } from '../sync/submitters/impressionCountsSubmitter'; import { IResponse, ISplitApi } from '../services/types'; import { ImpressionDTO, ISettings } from '../types'; import { ImpressionsPayload } from '../sync/submitters/types'; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index dd633639..169e73ea 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -24,7 +24,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.POLLING_STOP, c.LOG_PREFIX_SYNC_POLLING + 'Stopping polling'], [c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying download of splits #%s. Reason: %s'], [c.SUBMITTERS_PUSH_FULL_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full %s queue and reseting timer.'], - [c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s %s.'], + [c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s.'], [c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds, and connecting streaming in %s seconds.'], [c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect streaming in %s seconds.'], [c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Connecting streaming.'], diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index f6a66eec..d820413d 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -11,8 +11,8 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.STREAMING_PARSING_ERROR_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE error notification: %s'], [c.STREAMING_PARSING_MESSAGE_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE message notification: %s'], [c.STREAMING_FALLBACK, c.LOG_PREFIX_SYNC_STREAMING + 'Falling back to polling mode. Reason: %s'], - [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s %s after retry. Reason: %s.'], - [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s %s, keeping data to retry on next iteration. Reason: %s.'], + [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s after retry. Reason: %s.'], + [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'], // client status [c.CLIENT_NOT_READY, '%s: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method.'], [c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'], diff --git a/src/sync/submitters/__tests__/eventsSyncTask.spec.ts b/src/sync/submitters/__tests__/eventsSubmitter.spec.ts similarity index 85% rename from src/sync/submitters/__tests__/eventsSyncTask.spec.ts rename to src/sync/submitters/__tests__/eventsSubmitter.spec.ts index 59b647c1..a17ec656 100644 --- a/src/sync/submitters/__tests__/eventsSyncTask.spec.ts +++ b/src/sync/submitters/__tests__/eventsSubmitter.spec.ts @@ -1,9 +1,9 @@ -import { eventsSyncTaskFactory } from '../eventsSyncTask'; +import { eventsSubmitterFactory } from '../eventsSubmitter'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -describe('Events submitter (eventsSyncTask)', () => { +describe('Events submitter', () => { let __onFullQueueCb: () => void; const postEventsBulkMock = jest.fn(); @@ -18,7 +18,7 @@ describe('Events submitter (eventsSyncTask)', () => { test('with eventsFirstPushWindow', async () => { const eventsFirstPushWindow = 20; // @ts-ignore - const eventsSubmitter = eventsSyncTaskFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000, eventsFirstPushWindow); + const eventsSubmitter = eventsSubmitterFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000, eventsFirstPushWindow); eventsSubmitter.start(); expect(eventsSubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running @@ -41,7 +41,7 @@ describe('Events submitter (eventsSyncTask)', () => { test('without eventsFirstPushWindow', async () => { // @ts-ignore - const eventsSubmitter = eventsSyncTaskFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000); + const eventsSubmitter = eventsSubmitterFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000); eventsSubmitter.start(); expect(eventsSubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running diff --git a/src/sync/submitters/eventsSyncTask.ts b/src/sync/submitters/eventsSubmitter.ts similarity index 74% rename from src/sync/submitters/eventsSyncTask.ts rename to src/sync/submitters/eventsSubmitter.ts index 7c84374b..cbf36492 100644 --- a/src/sync/submitters/eventsSyncTask.ts +++ b/src/sync/submitters/eventsSubmitter.ts @@ -1,29 +1,26 @@ import { IEventsCacheSync } from '../../storages/types'; import { IPostEventsBulk } from '../../services/types'; -import { ISyncTask, ITimeTracker } from '../types'; -import { submitterSyncTaskFactory } from './submitterSyncTask'; +import { submitterFactory } from './submitter'; import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; const DATA_NAME = 'events'; /** - * Sync task that periodically posts tracked events + * Submitter that periodically posts tracked events */ -export function eventsSyncTaskFactory( +export function eventsSubmitterFactory( log: ILogger, postEventsBulk: IPostEventsBulk, eventsCache: IEventsCacheSync, eventsPushRate: number, eventsFirstPushWindow: number, - latencyTracker?: ITimeTracker -): ISyncTask { +) { // don't retry events. - const syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME, latencyTracker); + const syncTask = submitterFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME); // Set a timer for the first push window of events. - // Not implemented in the base submitter or sync task, since this feature is only used by the events submitter. if (eventsFirstPushWindow > 0) { let running = false; let stopEventPublisherTimeout: ReturnType; diff --git a/src/sync/submitters/impressionCountsSyncTask.ts b/src/sync/submitters/impressionCountsSubmitter.ts similarity index 71% rename from src/sync/submitters/impressionCountsSyncTask.ts rename to src/sync/submitters/impressionCountsSubmitter.ts index a24ea41e..78fc6588 100644 --- a/src/sync/submitters/impressionCountsSyncTask.ts +++ b/src/sync/submitters/impressionCountsSubmitter.ts @@ -1,7 +1,6 @@ -import { ISyncTask, ITimeTracker } from '../types'; import { IPostTestImpressionsCount } from '../../services/types'; import { IImpressionCountsCacheSync } from '../../storages/types'; -import { submitterSyncTaskFactory } from './submitterSyncTask'; +import { submitterFactory } from './submitter'; import { ImpressionCountsPayload } from './types'; import { ILogger } from '../../logger/types'; @@ -32,15 +31,14 @@ export function fromImpressionCountsCollector(impressionsCount: Record { diff --git a/src/sync/submitters/submitterSyncTask.ts b/src/sync/submitters/submitter.ts similarity index 60% rename from src/sync/submitters/submitterSyncTask.ts rename to src/sync/submitters/submitter.ts index a17df77d..6c402f61 100644 --- a/src/sync/submitters/submitterSyncTask.ts +++ b/src/sync/submitters/submitter.ts @@ -1,20 +1,19 @@ import { syncTaskFactory } from '../syncTask'; -import { ISyncTask, ITimeTracker } from '../types'; +import { ISyncTask } from '../types'; import { IRecorderCacheProducerSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH, SUBMITTERS_PUSH_FAILS, SUBMITTERS_PUSH_RETRY } from '../../logger/constants'; import { IResponse } from '../../services/types'; /** - * Base function to create submitter sync tasks, such as ImpressionsSyncTask and EventsSyncTask + * Base function to create submitters, such as ImpressionsSubmitter and EventsSubmitter */ -export function submitterSyncTaskFactory( +export function submitterFactory( log: ILogger, postClient: (body: string) => Promise, sourceCache: IRecorderCacheProducerSync, postRate: number, dataName: string, - latencyTracker?: ITimeTracker, fromCacheToPayload?: (cacheData: TState) => any, maxRetries: number = 0, debugLogs?: boolean @@ -26,32 +25,28 @@ export function submitterSyncTaskFactory( if (sourceCache.isEmpty()) return Promise.resolve(); const data = sourceCache.state(); - - const dataCount: number | '' = typeof data.length === 'number' ? data.length : ''; - log[debugLogs ? 'debug' : 'info'](SUBMITTERS_PUSH, [dataCount, dataName]); - const latencyTrackerStop = latencyTracker && latencyTracker.start(); + // @ts-ignore + const dataCountMessage = typeof data.length === 'number' ? `${data.length} ${dataName}` : dataName; + log[debugLogs ? 'debug' : 'info'](SUBMITTERS_PUSH, [dataCountMessage]); const jsonPayload = JSON.stringify(fromCacheToPayload ? fromCacheToPayload(data) : data); if (!maxRetries) sourceCache.clear(); - const postPromise = postClient(jsonPayload).then(() => { + return postClient(jsonPayload).then(() => { retries = 0; sourceCache.clear(); // we clear the queue if request successes. }).catch(err => { if (!maxRetries) { - log.warn(SUBMITTERS_PUSH_FAILS, [dataCount, dataName, err]); + log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]); } else if (retries === maxRetries) { retries = 0; sourceCache.clear(); // we clear the queue if request fails after retries. - log.warn(SUBMITTERS_PUSH_FAILS, [dataCount, dataName, err]); + log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]); } else { retries++; - log.warn(SUBMITTERS_PUSH_RETRY, [dataCount, dataName, err]); + log.warn(SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]); } }); - - // if latencyTracker provided, attach stop callback to postEventsPromise - return latencyTrackerStop ? postPromise.then(latencyTrackerStop).catch(latencyTrackerStop) : postPromise; } return syncTaskFactory(log, postData, postRate, dataName + ' submitter'); diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 51440a25..d7fdce3d 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -1,18 +1,19 @@ import { syncTaskComposite } from '../syncTaskComposite'; -import { eventsSyncTaskFactory } from './eventsSyncTask'; -import { impressionsSyncTaskFactory } from './impressionsSyncTask'; -import { impressionCountsSyncTaskFactory } from './impressionCountsSyncTask'; +import { eventsSubmitterFactory } from './eventsSubmitter'; +import { impressionsSubmitterFactory } from './impressionsSubmitter'; +import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter'; import { ISyncManagerFactoryParams } from '../types'; +import { telemetrySubmitterFactory } from './telemetrySubmitter'; export function submitterManagerFactory(params: ISyncManagerFactoryParams) { const { settings, storage, splitApi } = params; const log = settings.log; const submitters = [ - impressionsSyncTaskFactory(log, splitApi.postTestImpressionsBulk, storage.impressions, settings.scheduler.impressionsRefreshRate, settings.core.labelsEnabled), - eventsSyncTaskFactory(log, splitApi.postEventsBulk, storage.events, settings.scheduler.eventsPushRate, settings.startup.eventsFirstPushWindow) - // @TODO add telemetry submitter + impressionsSubmitterFactory(log, splitApi.postTestImpressionsBulk, storage.impressions, settings.scheduler.impressionsRefreshRate, settings.core.labelsEnabled), + eventsSubmitterFactory(log, splitApi.postEventsBulk, storage.events, settings.scheduler.eventsPushRate, settings.startup.eventsFirstPushWindow) ]; - if (storage.impressionCounts) submitters.push(impressionCountsSyncTaskFactory(log, splitApi.postTestImpressionsCount, storage.impressionCounts)); + if (storage.impressionCounts) submitters.push(impressionCountsSubmitterFactory(log, splitApi.postTestImpressionsCount, storage.impressionCounts)); + if (storage.telemetry) submitters.push(telemetrySubmitterFactory(log, splitApi, storage, settings.scheduler.telemetryRefreshRate)); return syncTaskComposite(submitters); } diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts new file mode 100644 index 00000000..d2c5e7c2 --- /dev/null +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -0,0 +1,52 @@ +import { ISplitApi } from '../../services/types'; +import { IStorageSync } from '../../storages/types'; +import { submitterFactory } from './submitter'; +import { TelemetryUsageStatsPayload } from './types'; +import { QUEUED, DEDUPED, DROPPED } from '../../utils/constants'; +import { ILogger } from '../../logger/types'; + +/** + * Converts data from telemetry cache into request payload. + */ +export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: IStorageSync) { + return { + isEmpty() { return false; }, // There is always data in telemetry cache + clear() { }, // No-op + + // @TODO consider moving inside telemetry cache for code size reduction + state(): TelemetryUsageStatsPayload { + return { + lS: telemetry.getLastSynchronization(), + mL: telemetry.popLatencies(), + mE: telemetry.popExceptions(), + hE: telemetry.popHttpErrors(), + hL: telemetry.popHttpLatencies(), + tR: telemetry.popTokenRefreshes(), + aR: telemetry.popAuthRejections(), + iQ: telemetry.getImpressionStats(QUEUED), + iDe: telemetry.getImpressionStats(DEDUPED), + iDr: telemetry.getImpressionStats(DROPPED), + spC: splits.getSplitNames().length, + seC: segments.getRegisteredSegments().length, + skC: segments.getKeysCount(), + sL: telemetry.getSessionLength(), + eQ: telemetry.getEventStats(QUEUED), + eD: telemetry.getEventStats(DROPPED), + sE: telemetry.popStreamingEvents(), + t: telemetry.popTags(), + }; + } + }; +} + +/** + * Submitter that periodically posts telemetry data + */ +export function telemetrySubmitterFactory( + log: ILogger, + splitApi: ISplitApi, + storage: IStorageSync, + telemetryRefreshRate: number, +) { + return submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(storage), telemetryRefreshRate, 'telemetry stats', undefined, 0, true); +} diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 695c145d..7f297fbd 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -129,7 +129,7 @@ export type TelemetryUsageStatsPayload = { spC: number, // splitCount seC: number, // segmentCount skC: number, // segmentKeyCount - sL: number, // sessionLengthMs + sL?: number, // sessionLengthMs eQ: number, // eventsQueued eD: number, // eventsDropped sE: Array, // streamingEvents @@ -180,7 +180,7 @@ export type TelemetryConfigStatsPayload = { aF: number, // activeFactories rF: number, // redundantActiveFactories tR: number, // timeUntilSDKReady - tC: number, // timeUntilSDKReadyFromCache + tC?: number, // timeUntilSDKReadyFromCache nR: number, // SDKNotReadyUsage t?: Array, // tags i?: Array, // integrations diff --git a/src/sync/types.ts b/src/sync/types.ts index 1a70a5c6..605ade41 100644 --- a/src/sync/types.ts +++ b/src/sync/types.ts @@ -36,10 +36,6 @@ export interface ISyncTask extends ITask isExecuting(): boolean } -export interface ITimeTracker { - start(): () => void // start tracking time and return a function to call for stopping the tracking -} - /** SyncManager */ export interface ISyncManager extends ITask { From 654a3417df10e81b8e4f73a8db15d099ab5cddf0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 15:52:14 -0300 Subject: [PATCH 30/70] removing trailing comma --- src/trackers/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trackers/types.ts b/src/trackers/types.ts index 42dcfa0c..2ba70832 100644 --- a/src/trackers/types.ts +++ b/src/trackers/types.ts @@ -36,7 +36,7 @@ export interface ITelemetryTracker { /** * Records session length */ - sessionLength(): void, + sessionLength(): void /** * Records streaming event */ From 4bf45c61869a7c943c4924771ba3fa612c399089 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 18:50:54 -0300 Subject: [PATCH 31/70] track telemetry init stats, and submit metrics/config --- src/sync/submitters/submitterManager.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 109 +++++++++++++++++++--- src/utils/inputValidation/apiKey.ts | 3 +- src/utils/settingsValidation/index.ts | 3 +- 4 files changed, 102 insertions(+), 15 deletions(-) diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index d7fdce3d..35a4ce1d 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -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(log, splitApi, storage, settings.scheduler.telemetryRefreshRate)); + if (storage.telemetry) submitters.push(telemetrySubmitterFactory(params)); return syncTaskComposite(submitters); } diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index d2c5e7c2..593cbbfc 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -1,12 +1,16 @@ -import { ISplitApi } from '../../services/types'; -import { IStorageSync } from '../../storages/types'; +import { IStorageSync, TelemetryCacheSync } from '../../storages/types'; import { submitterFactory } from './submitter'; -import { TelemetryUsageStatsPayload } from './types'; -import { QUEUED, DEDUPED, DROPPED } from '../../utils/constants'; -import { ILogger } from '../../logger/types'; +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'; +import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants'; +import { ISettings } from '../../types'; +import { base } from '../../utils/settingsValidation'; +import { usedKeysMap } from '../../utils/inputValidation/apiKey'; +import { ISyncManagerFactoryParams } from '../types'; +import { timer } from '../../utils/timeTracker/timer'; /** - * Converts data from telemetry cache into request payload. + * Converts data from telemetry cache into /metrics/usage request payload. */ export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: IStorageSync) { return { @@ -39,14 +43,95 @@ export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: ISto }; } +function mapOperationMode(mode: ISettings['mode']) { + switch (mode) { + case STANDALONE_MODE: return STANDALONE_ENUM; + case CONSUMER_MODE: return CONSUMER_ENUM; + case CONSUMER_PARTIAL_MODE: return CONSUMER_PARTIAL_ENUM; + } +} + +function mapImpressionsMode(mode: ISettings['sync']['impressionsMode']) { + switch (mode) { + case OPTIMIZED: return OPTIMIZED_ENUM; + case DEBUG: return DEBUG_ENUM; + } +} + +function getActiveFactories() { + return Object.keys(usedKeysMap).length; +} + +function getRedundantActiveFactories() { + return Object.keys(usedKeysMap).reduce((acum, apiKey) => { + return acum + usedKeysMap[apiKey] - 1; + }, 0); +} + +/** + * Converts data from telemetry cache and settings into /metrics/config request payload. + */ +export function telemetryCacheConfigAdapter(settings: ISettings, telemetryCache: TelemetryCacheSync) { + return { + isEmpty() { return false; }, + clear() { }, + + state(): TelemetryConfigStatsPayload { + const { urls, scheduler } = settings; + + return { + oM: mapOperationMode(settings.mode), // @ts-ignore lower case of storage type + st: settings.storagetype.toLowerCase(), + sE: settings.streamingEnabled, + rR: { + sp: scheduler.featuresRefreshRate, + se: scheduler.segmentsRefreshRate, + im: scheduler.impressionsRefreshRate, + ev: scheduler.eventsPushRate, + te: scheduler.telemetryRefreshRate, + }, // refreshRates + uO: { + s: urls.sdk !== base.urls.sdk, + e: urls.events !== base.urls.events, + a: urls.auth !== base.urls.auth, + st: urls.streaming !== base.urls.streaming, + t: urls.telemetry !== base.urls.telemetry, + }, // urlOverrides + iQ: scheduler.impressionsQueueSize, + eQ: scheduler.eventsQueueSize, + iM: mapImpressionsMode(settings.sync.impressionsMode), + iL: settings.impressionListener ? true : false, + hP: false, // @TODO proxy not supported + aF: getActiveFactories(), + rF: getRedundantActiveFactories(), + tR: telemetryCache.getTimeUntilReady() as number, + tC: telemetryCache.getTimeUntilReadyFromCache(), + nR: telemetryCache.getNonReadyUsage(), + t: telemetryCache.popTags(), + i: settings.integrations && settings.integrations.map(int => int.type), + }; + } + }; +} + /** * Submitter that periodically posts telemetry data */ -export function telemetrySubmitterFactory( - log: ILogger, - splitApi: ISplitApi, - storage: IStorageSync, - telemetryRefreshRate: number, -) { +export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { + const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, storage, splitApi, platform: { now }, readiness } = params; + const startTime = timer(now || Date.now); + + readiness.gate.once(SDK_READY_FROM_CACHE, () => { + storage.telemetry.recordTimeUntilReadyFromCache(startTime()); + }); + + readiness.gate.once(SDK_READY, () => { + storage.telemetry.recordTimeUntilReady(startTime()); + + // Post telemetry config data once the SDK is ready + const postMetricsConfigTask = submitterFactory(log, splitApi.postMetricsConfig, telemetryCacheConfigAdapter(settings, storage.telemetry), 0, 'telemetry config', undefined, 0, true); + postMetricsConfigTask.execute(); + }); + return submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(storage), telemetryRefreshRate, 'telemetry stats', undefined, 0, true); } diff --git a/src/utils/inputValidation/apiKey.ts b/src/utils/inputValidation/apiKey.ts index 86175189..cef0fa0f 100644 --- a/src/utils/inputValidation/apiKey.ts +++ b/src/utils/inputValidation/apiKey.ts @@ -21,7 +21,8 @@ export function validateApiKey(log: ILogger, maybeApiKey: any): string | false { return apiKey; } -const usedKeysMap: Record = {}; +// Exported for telemetry +export const usedKeysMap: Record = {}; /** validates the given api key and also warns if it is in use */ export function validateAndTrackApiKey(log: ILogger, maybeApiKey: any): string | false { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index baa8dbd9..62c2abb8 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -8,7 +8,8 @@ import { ISettings } from '../../types'; import { validateKey } from '../inputValidation/key'; import { validateTrafficType } from '../inputValidation/trafficType'; -const base = { +// Exported for telemetry +export const base = { // Define which kind of object you want to retrieve from SplitFactory mode: STANDALONE_MODE, From a6193f36dbfc1203ada11a4842a1fee92651f5b6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 28 Apr 2022 19:12:21 -0300 Subject: [PATCH 32/70] refactor submitters --- .../__tests__/eventsSubmitter.spec.ts | 20 ++++++++++++---- src/sync/submitters/eventsSubmitter.ts | 22 ++++++++--------- .../submitters/impressionCountsSubmitter.ts | 24 ++++++++++--------- src/sync/submitters/impressionsSubmitter.ts | 22 ++++++++--------- src/sync/submitters/submitterManager.ts | 13 +++++----- src/sync/submitters/telemetrySubmitter.ts | 4 ++-- 6 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/sync/submitters/__tests__/eventsSubmitter.spec.ts b/src/sync/submitters/__tests__/eventsSubmitter.spec.ts index a17ec656..16b26bc1 100644 --- a/src/sync/submitters/__tests__/eventsSubmitter.spec.ts +++ b/src/sync/submitters/__tests__/eventsSubmitter.spec.ts @@ -6,19 +6,28 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; describe('Events submitter', () => { let __onFullQueueCb: () => void; - const postEventsBulkMock = jest.fn(); const eventsCacheMock = { isEmpty: jest.fn(() => true), setOnFullQueueCb: jest.fn(function (onFullQueueCb) { __onFullQueueCb = onFullQueueCb; }) }; + const params = { + settings: { + log: loggerMock, + scheduler: { eventsPushRate: 30000 }, + startup: { eventsFirstPushWindow: 0 } + }, + splitApi: { postEventsBulkMock: jest.fn() }, + storage: { events: eventsCacheMock } + }; beforeEach(() => { eventsCacheMock.isEmpty.mockClear(); }); test('with eventsFirstPushWindow', async () => { - const eventsFirstPushWindow = 20; // @ts-ignore - const eventsSubmitter = eventsSubmitterFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000, eventsFirstPushWindow); + const eventsFirstPushWindow = 20; + params.settings.startup.eventsFirstPushWindow = eventsFirstPushWindow; // @ts-ignore + const eventsSubmitter = eventsSubmitterFactory(params); eventsSubmitter.start(); expect(eventsSubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running @@ -40,8 +49,9 @@ describe('Events submitter', () => { }); test('without eventsFirstPushWindow', async () => { - // @ts-ignore - const eventsSubmitter = eventsSubmitterFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000); + const eventsFirstPushWindow = 0; + params.settings.startup.eventsFirstPushWindow = eventsFirstPushWindow; // @ts-ignore + const eventsSubmitter = eventsSubmitterFactory(params); eventsSubmitter.start(); expect(eventsSubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running diff --git a/src/sync/submitters/eventsSubmitter.ts b/src/sync/submitters/eventsSubmitter.ts index cbf36492..eb4f5da9 100644 --- a/src/sync/submitters/eventsSubmitter.ts +++ b/src/sync/submitters/eventsSubmitter.ts @@ -1,24 +1,22 @@ -import { IEventsCacheSync } from '../../storages/types'; -import { IPostEventsBulk } from '../../services/types'; import { submitterFactory } from './submitter'; -import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; const DATA_NAME = 'events'; /** * Submitter that periodically posts tracked events */ -export function eventsSubmitterFactory( - log: ILogger, - postEventsBulk: IPostEventsBulk, - eventsCache: IEventsCacheSync, - eventsPushRate: number, - eventsFirstPushWindow: number, -) { +export function eventsSubmitterFactory(params: ISdkFactoryContextSync) { + + const { + settings: { log, scheduler: { eventsPushRate }, startup: { eventsFirstPushWindow } }, + splitApi: { postEventsBulk }, + storage: { events }, + } = params; // don't retry events. - const syncTask = submitterFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME); + const syncTask = submitterFactory(log, postEventsBulk, events, eventsPushRate, DATA_NAME); // Set a timer for the first push window of events. if (eventsFirstPushWindow > 0) { @@ -41,7 +39,7 @@ export function eventsSubmitterFactory( } // register events submitter to be executed when events cache is full - eventsCache.setOnFullQueueCb(() => { + events.setOnFullQueueCb(() => { if (syncTask.isRunning()) { log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); syncTask.execute(); diff --git a/src/sync/submitters/impressionCountsSubmitter.ts b/src/sync/submitters/impressionCountsSubmitter.ts index 78fc6588..48131021 100644 --- a/src/sync/submitters/impressionCountsSubmitter.ts +++ b/src/sync/submitters/impressionCountsSubmitter.ts @@ -1,8 +1,6 @@ -import { IPostTestImpressionsCount } from '../../services/types'; -import { IImpressionCountsCacheSync } from '../../storages/types'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; import { submitterFactory } from './submitter'; import { ImpressionCountsPayload } from './types'; -import { ILogger } from '../../logger/types'; /** * Converts `impressionCounts` data from cache into request payload. @@ -33,12 +31,16 @@ const IMPRESSIONS_COUNT_RATE = 1800000; // 30 minutes /** * Submitter that periodically posts impression counts */ -export function impressionCountsSubmitterFactory( - log: ILogger, - postTestImpressionsCount: IPostTestImpressionsCount, - impressionCountsCache: IImpressionCountsCacheSync, -) { - - // retry impressions counts only once. - return submitterFactory(log, postTestImpressionsCount, impressionCountsCache, IMPRESSIONS_COUNT_RATE, 'impression counts', fromImpressionCountsCollector, 1); +export function impressionCountsSubmitterFactory(params: ISdkFactoryContextSync) { + + const { + settings: { log }, + splitApi: { postTestImpressionsCount }, + storage: { impressionCounts } + } = params; + + if (impressionCounts) { + // retry impressions counts only once. + return submitterFactory(log, postTestImpressionsCount, impressionCounts, IMPRESSIONS_COUNT_RATE, 'impression counts', fromImpressionCountsCollector, 1); + } } diff --git a/src/sync/submitters/impressionsSubmitter.ts b/src/sync/submitters/impressionsSubmitter.ts index 3a01f6f2..e54b5f1f 100644 --- a/src/sync/submitters/impressionsSubmitter.ts +++ b/src/sync/submitters/impressionsSubmitter.ts @@ -1,11 +1,9 @@ import { groupBy, forOwn } from '../../utils/lang'; -import { IPostTestImpressionsBulk } from '../../services/types'; -import { IImpressionsCacheSync } from '../../storages/types'; import { ImpressionDTO } from '../../types'; import { submitterFactory } from './submitter'; import { ImpressionsPayload } from './types'; -import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; const DATA_NAME = 'impressions'; @@ -42,19 +40,19 @@ export function fromImpressionsCollector(sendLabels: boolean, data: ImpressionDT /** * Submitter that periodically posts impressions data */ -export function impressionsSubmitterFactory( - log: ILogger, - postTestImpressionsBulk: IPostTestImpressionsBulk, - impressionsCache: IImpressionsCacheSync, - impressionsRefreshRate: number, - sendLabels = false, -) { +export function impressionsSubmitterFactory(params: ISdkFactoryContextSync) { + + const { + settings: { log, scheduler: { impressionsRefreshRate }, core: { labelsEnabled } }, + splitApi: { postTestImpressionsBulk }, + storage: { impressions } + } = params; // retry impressions only once. - const syncTask = submitterFactory(log, postTestImpressionsBulk, impressionsCache, impressionsRefreshRate, DATA_NAME, fromImpressionsCollector.bind(undefined, sendLabels), 1); + const syncTask = submitterFactory(log, postTestImpressionsBulk, impressions, impressionsRefreshRate, DATA_NAME, fromImpressionsCollector.bind(undefined, labelsEnabled), 1); // register impressions submitter to be executed when impressions cache is full - impressionsCache.setOnFullQueueCb(() => { + impressions.setOnFullQueueCb(() => { if (syncTask.isRunning()) { log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); syncTask.execute(); diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index fc97f11a..4de02ec2 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -7,13 +7,14 @@ import { ISdkFactoryContextSync } from '../../sdkFactory/types'; export function submitterManagerFactory(params: ISdkFactoryContextSync) { - const { settings, storage, splitApi } = params; - const log = settings.log; const submitters = [ - impressionsSubmitterFactory(log, splitApi.postTestImpressionsBulk, storage.impressions, settings.scheduler.impressionsRefreshRate, settings.core.labelsEnabled), - eventsSubmitterFactory(log, splitApi.postEventsBulk, storage.events, settings.scheduler.eventsPushRate, settings.startup.eventsFirstPushWindow) + impressionsSubmitterFactory(params), + eventsSubmitterFactory(params), + telemetrySubmitterFactory(params) ]; - if (storage.impressionCounts) submitters.push(impressionCountsSubmitterFactory(log, splitApi.postTestImpressionsCount, storage.impressionCounts)); - if (storage.telemetry) submitters.push(telemetrySubmitterFactory(params)); + + const impressionCountsSubmitter = impressionCountsSubmitterFactory(params); + if (impressionCountsSubmitter) submitters.push(impressionCountsSubmitter); + return syncTaskComposite(submitters); } diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 593cbbfc..3b052a13 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -6,8 +6,8 @@ import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants'; import { ISettings } from '../../types'; import { base } from '../../utils/settingsValidation'; import { usedKeysMap } from '../../utils/inputValidation/apiKey'; -import { ISyncManagerFactoryParams } from '../types'; import { timer } from '../../utils/timeTracker/timer'; +import { ISdkFactoryContextSync } from '../../sdkFactory/types'; /** * Converts data from telemetry cache into /metrics/usage request payload. @@ -117,7 +117,7 @@ export function telemetryCacheConfigAdapter(settings: ISettings, telemetryCache: /** * Submitter that periodically posts telemetry data */ -export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { +export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) { const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, storage, splitApi, platform: { now }, readiness } = params; const startTime = timer(now || Date.now); From 84a936e9c3212026737a874128f7e9a8628d79d7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 29 Apr 2022 12:58:20 -0300 Subject: [PATCH 33/70] added first push window to telemetry submitter --- .../__tests__/telemetrySubmitter.spec.ts | 41 +++++++++++++++++++ src/sync/submitters/eventsSubmitter.ts | 29 +++---------- src/sync/submitters/submitter.ts | 23 +++++++++++ src/sync/submitters/submitterManager.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 21 +++++----- 5 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/sync/submitters/__tests__/telemetrySubmitter.spec.ts diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts new file mode 100644 index 00000000..6e0373d7 --- /dev/null +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -0,0 +1,41 @@ +import { telemetrySubmitterFactory } from '../telemetrySubmitter'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { InMemoryStorageFactory } from '../../../storages/inMemory/InMemoryStorage'; + +describe('Telemetry submitter', () => { + + const telemetryRefreshRate = 100; // 100 ms + const postMetricsUsage = jest.fn(() => Promise.resolve()); + const params = { + settings: { + log: loggerMock, + scheduler: { telemetryRefreshRate } + }, + splitApi: { postMetricsUsage }, // @ts-ignore + storage: InMemoryStorageFactory({}), + }; + const popLatenciesSpy = jest.spyOn(params.storage.telemetry, 'popLatencies'); + + test('submits metrics/usage periodically', async () => { + // @ts-ignore + const telemetrySubmitter = telemetrySubmitterFactory(params); + + telemetrySubmitter.start(); + expect(telemetrySubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running + expect(telemetrySubmitter.isExecuting()).toEqual(false); // but not executed immediatelly (first push window) + expect(popLatenciesSpy).toBeCalledTimes(0); + + // Await first push + await new Promise(res => setTimeout(res, params.settings.scheduler.telemetryRefreshRate + 10)); + // after the first push, telemetry cache should have been used to create the request payload + expect(popLatenciesSpy).toBeCalledTimes(1); + expect(postMetricsUsage).toBeCalledWith(JSON.stringify({ + lS: {}, mL: {}, mE: {}, hE: {}, hL: {}, tR: 0, aR: 0, iQ: 0, iDe: 0, iDr: 0, spC: 0, seC: 0, skC: 0, eQ: 0, eD: 0, sE: [], t: [] + })); + + expect(telemetrySubmitter.isRunning()).toEqual(true); + telemetrySubmitter.stop(); + expect(telemetrySubmitter.isRunning()).toEqual(false); + }); + +}); diff --git a/src/sync/submitters/eventsSubmitter.ts b/src/sync/submitters/eventsSubmitter.ts index cbf36492..84ce404f 100644 --- a/src/sync/submitters/eventsSubmitter.ts +++ b/src/sync/submitters/eventsSubmitter.ts @@ -1,6 +1,6 @@ import { IEventsCacheSync } from '../../storages/types'; import { IPostEventsBulk } from '../../services/types'; -import { submitterFactory } from './submitter'; +import { submitterFactory, firstPushWindowDecorator } from './submitter'; import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; @@ -18,37 +18,20 @@ export function eventsSubmitterFactory( ) { // don't retry events. - const syncTask = submitterFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME); + let submitter = submitterFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME); // Set a timer for the first push window of events. - if (eventsFirstPushWindow > 0) { - let running = false; - let stopEventPublisherTimeout: ReturnType; - const originalStart = syncTask.start; - syncTask.start = () => { - running = true; - stopEventPublisherTimeout = setTimeout(originalStart, eventsFirstPushWindow); - }; - const originalStop = syncTask.stop; - syncTask.stop = () => { - running = false; - clearTimeout(stopEventPublisherTimeout); - originalStop(); - }; - syncTask.isRunning = () => { - return running; - }; - } + if (eventsFirstPushWindow > 0) submitter = firstPushWindowDecorator(submitter, eventsFirstPushWindow); // register events submitter to be executed when events cache is full eventsCache.setOnFullQueueCb(() => { - if (syncTask.isRunning()) { + if (submitter.isRunning()) { log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); - syncTask.execute(); + submitter.execute(); } // If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data. // Data will be sent when submitter is resumed. }); - return syncTask; + return submitter; } diff --git a/src/sync/submitters/submitter.ts b/src/sync/submitters/submitter.ts index 6c402f61..974bb8e8 100644 --- a/src/sync/submitters/submitter.ts +++ b/src/sync/submitters/submitter.ts @@ -51,3 +51,26 @@ export function submitterFactory( return syncTaskFactory(log, postData, postRate, dataName + ' submitter'); } + +/** + * Decorates a provided submitter with a first execution window + */ +export function firstPushWindowDecorator(submitter: ISyncTask, firstPushWindow: number) { + let running = false; + let stopEventPublisherTimeout: ReturnType; + const originalStart = submitter.start; + submitter.start = () => { + running = true; + stopEventPublisherTimeout = setTimeout(originalStart, firstPushWindow); + }; + const originalStop = submitter.stop; + submitter.stop = () => { + running = false; + clearTimeout(stopEventPublisherTimeout); + originalStop(); + }; + submitter.isRunning = () => { + return running; + }; + return submitter; +} diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index d7fdce3d..35a4ce1d 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -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(log, splitApi, storage, settings.scheduler.telemetryRefreshRate)); + if (storage.telemetry) submitters.push(telemetrySubmitterFactory(params)); return syncTaskComposite(submitters); } diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index d2c5e7c2..64dbbd19 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -1,12 +1,11 @@ -import { ISplitApi } from '../../services/types'; import { IStorageSync } from '../../storages/types'; -import { submitterFactory } from './submitter'; +import { submitterFactory, firstPushWindowDecorator } from './submitter'; import { TelemetryUsageStatsPayload } from './types'; import { QUEUED, DEDUPED, DROPPED } from '../../utils/constants'; -import { ILogger } from '../../logger/types'; +import { ISyncManagerFactoryParams } from '../types'; /** - * Converts data from telemetry cache into request payload. + * Converts data from telemetry cache into /metrics/usage request payload. */ export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: IStorageSync) { return { @@ -42,11 +41,11 @@ export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: ISto /** * Submitter that periodically posts telemetry data */ -export function telemetrySubmitterFactory( - log: ILogger, - splitApi: ISplitApi, - storage: IStorageSync, - telemetryRefreshRate: number, -) { - return submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(storage), telemetryRefreshRate, 'telemetry stats', undefined, 0, true); +export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { + const { settings: { log, scheduler: { telemetryRefreshRate } }, storage, splitApi } = params; + + return firstPushWindowDecorator( + submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(storage), telemetryRefreshRate, 'telemetry stats', undefined, 0, true), + telemetryRefreshRate + ); } From 1bb65ce60b9ce4e4d0da4e4daa6aa36b23528e75 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 29 Apr 2022 15:30:18 -0300 Subject: [PATCH 34/70] unit test --- .../__tests__/telemetrySubmitter.spec.ts | 47 ++++++++++++++----- src/sync/submitters/telemetrySubmitter.ts | 20 ++++---- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 48a11cdb..f551f350 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -1,26 +1,33 @@ import { telemetrySubmitterFactory } from '../telemetrySubmitter'; -import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { InMemoryStorageFactory } from '../../../storages/inMemory/InMemoryStorage'; +import { SDK_READY, SDK_READY_FROM_CACHE } from '../../../readiness/constants'; +import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; describe('Telemetry submitter', () => { const telemetryRefreshRate = 100; // 100 ms const postMetricsUsage = jest.fn(() => Promise.resolve()); + const postMetricsConfig = jest.fn(() => Promise.resolve()); + const readinessGateCallbacks: Record void> = {}; const params = { - settings: { - log: loggerMock, - scheduler: { telemetryRefreshRate } - }, - splitApi: { postMetricsUsage }, // @ts-ignore + settings: { ...fullSettings, scheduler: { ...fullSettings.scheduler, telemetryRefreshRate } }, + splitApi: { postMetricsUsage, postMetricsConfig }, // @ts-ignore storage: InMemoryStorageFactory({}), - platform: {}, - readiness: { gate: { once: jest.fn() } } + platform: { + now: () => 123 // by returning a fixed timestamp, all latencies are equal to 0 + }, + readiness: { + gate: { + once: jest.fn((e: string, cb: () => void) => { + readinessGateCallbacks[e] = cb; + }) + } + } }; - const popLatenciesSpy = jest.spyOn(params.storage.telemetry, 'popLatencies'); - test('submits metrics/usage periodically', async () => { - // @ts-ignore + test('submits metrics/usage periodically', async () => { // @ts-ignore const telemetrySubmitter = telemetrySubmitterFactory(params); + const popLatenciesSpy = jest.spyOn(params.storage.telemetry, 'popLatencies'); telemetrySubmitter.start(); expect(telemetrySubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running @@ -40,4 +47,22 @@ describe('Telemetry submitter', () => { expect(telemetrySubmitter.isRunning()).toEqual(false); }); + test('submits metrics/config when SDK is ready', async () => { // @ts-ignore + const telemetrySubmitter = telemetrySubmitterFactory(params); + const recordTimeUntilReadyFromCacheSpy = jest.spyOn(params.storage.telemetry, 'recordTimeUntilReadyFromCache'); + const recordTimeUntilReadySpy = jest.spyOn(params.storage.telemetry, 'recordTimeUntilReady'); + + telemetrySubmitter.start(); + + readinessGateCallbacks[SDK_READY_FROM_CACHE](); + expect(recordTimeUntilReadyFromCacheSpy).toBeCalledTimes(1); + + readinessGateCallbacks[SDK_READY](); + expect(recordTimeUntilReadySpy).toBeCalledTimes(1); + + expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ + oM: 0, st: 'memory', sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, aF: 0, rF: 0, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'] + })); + }); + }); diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 25f90ef2..b5d7ebca 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -81,7 +81,7 @@ export function telemetryCacheConfigAdapter(settings: ISettings, telemetryCache: return { oM: mapOperationMode(settings.mode), // @ts-ignore lower case of storage type - st: settings.storagetype.toLowerCase(), + st: settings.storage.type.toLowerCase(), sE: settings.streamingEnabled, rR: { sp: scheduler.featuresRefreshRate, @@ -121,6 +121,11 @@ export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, storage, splitApi, platform: { now }, readiness } = params; const startTime = timer(now || Date.now); + const submitter = firstPushWindowDecorator( + submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(storage), telemetryRefreshRate, 'telemetry stats', undefined, 0, true), + telemetryRefreshRate + ); + readiness.gate.once(SDK_READY_FROM_CACHE, () => { storage.telemetry.recordTimeUntilReadyFromCache(startTime()); }); @@ -128,13 +133,12 @@ export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { readiness.gate.once(SDK_READY, () => { storage.telemetry.recordTimeUntilReady(startTime()); - // Post telemetry config data once the SDK is ready - const postMetricsConfigTask = submitterFactory(log, splitApi.postMetricsConfig, telemetryCacheConfigAdapter(settings, storage.telemetry), 0, 'telemetry config', undefined, 0, true); - postMetricsConfigTask.execute(); + // Post config data when the SDK is ready and if the telemetry submitter was started + if (submitter.isRunning()) { + const postMetricsConfigTask = submitterFactory(log, splitApi.postMetricsConfig, telemetryCacheConfigAdapter(settings, storage.telemetry), 0, 'telemetry config', undefined, 0, true); + postMetricsConfigTask.execute(); + } }); - return firstPushWindowDecorator( - submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(storage), telemetryRefreshRate, 'telemetry stats', undefined, 0, true), - telemetryRefreshRate - ); + return submitter; } From 4180a11cc90a9abc81cda9a2fbd432f566c61b79 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 29 Apr 2022 16:16:48 -0300 Subject: [PATCH 35/70] validate min value for telemetryRefreshRate --- src/logger/constants.ts | 1 + src/logger/messages/error.ts | 3 ++- src/utils/settingsValidation/index.ts | 11 +++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 10b45254..698b68c3 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -123,6 +123,7 @@ export const ERROR_HTTP = 322; export const ERROR_LOCALHOST_MODULE_REQUIRED = 323; export const ERROR_STORAGE_INVALID = 324; export const ERROR_NOT_BOOLEAN = 325; +export const ERROR_MIN_CONFIG_PARAM = 326; // Log prefixes (a.k.a. tags or categories) export const LOG_PREFIX_SETTINGS = 'settings'; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 8ef9065b..0d55cb15 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -32,5 +32,6 @@ export const codesError: [number, string][] = [ // initialization / settings validation [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'], [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], - [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid.%s Fallbacking into default MEMORY storage'], + [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Fallbacking into default MEMORY storage'], + [c.ERROR_MIN_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': the provided "%s" config param is lower than allowed. Setting to the minimum value %s seconds'], ]; diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index baa8dbd9..1d83e2b1 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -7,6 +7,7 @@ import { ISettingsValidationParams } from './types'; import { ISettings } from '../../types'; import { validateKey } from '../inputValidation/key'; import { validateTrafficType } from '../inputValidation/trafficType'; +import { ERROR_MIN_CONFIG_PARAM } from '../../logger/constants'; const base = { // Define which kind of object you want to retrieve from SplitFactory @@ -111,6 +112,13 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV const log = logger(withDefaults); // @ts-ignore, modify readonly prop withDefaults.log = log; + function validateMinValue(paramName: string, actualValue: number, minValue: number) { + if (actualValue >= minValue) return actualValue; + // actualValue is not a number or is lower than minValue + log.error(ERROR_MIN_CONFIG_PARAM, [paramName, minValue]); + return minValue; + } + // Scheduler periods const { scheduler, startup } = withDefaults; scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate); @@ -118,8 +126,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate); scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate); scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate); - // @TODO handle min 60 secs - scheduler.telemetryRefreshRate = fromSecondsToMillis(scheduler.telemetryRefreshRate); + scheduler.telemetryRefreshRate = fromSecondsToMillis(validateMinValue('telemetryRefreshRate', scheduler.telemetryRefreshRate, 60)); // Log deprecation for old telemetry param if (scheduler.metricsRefreshRate) log.warn('`metricsRefreshRate` will be deprecated soon. For configuring telemetry rates, update `telemetryRefreshRate` value in configs'); From 77cfe02be4e9fd0e21f6523276330ec4ddbde210 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 29 Apr 2022 16:23:06 -0300 Subject: [PATCH 36/70] unit test --- src/utils/settingsValidation/__tests__/index.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index f1d975df..45e357ce 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -243,6 +243,17 @@ describe('settingsValidation', () => { expect(settings.core.key).toEqual(false); // key is validated expect(settings.core.trafficType).toEqual(true); // traffic type is ignored }); + + test('validate min values', () => { + const settings = settingsValidation({ + scheduler: { + telemetryRefreshRate: 0 + } + }, minimalSettingsParams); + + expect(settings.scheduler.telemetryRefreshRate).toBe(60000); + }); + }); test('SETTINGS / urls should be correctly assigned', () => { From 80469e9185e9fb9c3e1eaf992b7812156f1e7596 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 29 Apr 2022 16:37:44 -0300 Subject: [PATCH 37/70] update defaults and mins of impressionsRefreshRate depending on impressionsMode --- .../__tests__/index.spec.ts | 6 ++++- src/utils/settingsValidation/index.ts | 24 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 45e357ce..f2c2431b 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -53,6 +53,7 @@ describe('settingsValidation', () => { }, minimalSettingsParams); expect(settings.sync.impressionsMode).toBe(OPTIMIZED); + expect(settings.scheduler.impressionsRefreshRate).toBe(300000); }); test('impressionsMode should be configurable', () => { @@ -66,6 +67,7 @@ describe('settingsValidation', () => { }, minimalSettingsParams); expect(settings.sync.impressionsMode).toEqual(DEBUG); + expect(settings.scheduler.impressionsRefreshRate).toBe(60000); // Different default for DEBUG impressionsMode }); test('urls should be configurable', () => { @@ -247,11 +249,13 @@ describe('settingsValidation', () => { test('validate min values', () => { const settings = settingsValidation({ scheduler: { - telemetryRefreshRate: 0 + telemetryRefreshRate: 0, + impressionsRefreshRate: 'invalid', } }, minimalSettingsParams); expect(settings.scheduler.telemetryRefreshRate).toBe(60000); + expect(settings.scheduler.impressionsRefreshRate).toBe(60000); }); }); diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 1d83e2b1..2eca446a 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -1,7 +1,7 @@ import { merge } from '../lang'; import { mode } from './mode'; import { validateSplitFilters } from './splitFilters'; -import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE } from '../constants'; +import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE, DEBUG } from '../constants'; import { validImpressionsMode } from './impressionsMode'; import { ISettingsValidationParams } from './types'; import { ISettings } from '../../types'; @@ -33,8 +33,8 @@ const base = { segmentsRefreshRate: 60, // publish telemetry stats each 3600 secs (1 hour) telemetryRefreshRate: 3600, - // publish evaluations each 60 sec - impressionsRefreshRate: 60, + // publish evaluations each 300 sec (default value for OPTIMIZED impressions mode) + impressionsRefreshRate: 300, // fetch offline changes each 15 sec offlineRefreshRate: 15, // publish events every 60 seconds after the first flush @@ -112,6 +112,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV const log = logger(withDefaults); // @ts-ignore, modify readonly prop withDefaults.log = log; + // ensure a valid impressionsMode + withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode); + function validateMinValue(paramName: string, actualValue: number, minValue: number) { if (actualValue >= minValue) return actualValue; // actualValue is not a number or is lower than minValue @@ -123,11 +126,21 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV const { scheduler, startup } = withDefaults; scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate); scheduler.segmentsRefreshRate = fromSecondsToMillis(scheduler.segmentsRefreshRate); - scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate); scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate); scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate); scheduler.telemetryRefreshRate = fromSecondsToMillis(validateMinValue('telemetryRefreshRate', scheduler.telemetryRefreshRate, 60)); + if (scheduler.impressionsRefreshRate !== base.scheduler.impressionsRefreshRate) { + // Validate impressionsRefreshRate defined by user + scheduler.impressionsRefreshRate = validateMinValue('impressionsRefreshRate', scheduler.impressionsRefreshRate, + withDefaults.sync.impressionsMode === DEBUG ? 1 : 60 // Min is 1 sec for DEBUG and 60 secs for OPTIMIZED + ); + } else { + // Default impressionsRefreshRate for DEBUG mode is 60 secs + if (withDefaults.sync.impressionsMode === DEBUG) scheduler.impressionsRefreshRate = 60; + } + scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate); + // Log deprecation for old telemetry param if (scheduler.metricsRefreshRate) log.warn('`metricsRefreshRate` will be deprecated soon. For configuring telemetry rates, update `telemetryRefreshRate` value in configs'); @@ -189,9 +202,6 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV withDefaults.sync.splitFilters = splitFiltersValidation.validFilters; withDefaults.sync.__splitFiltersValidation = splitFiltersValidation; - // ensure a valid impressionsMode - withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode); - // ensure a valid user consent value // @ts-ignore, modify readonly prop withDefaults.userConsent = consent(withDefaults); From 6210deda62f10300efa66c55796ec06115f656c0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 May 2022 14:45:41 -0300 Subject: [PATCH 38/70] Update src/logger/messages/error.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gastón Thea --- src/logger/messages/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 0d55cb15..1eebbbd7 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -32,6 +32,6 @@ export const codesError: [number, string][] = [ // initialization / settings validation [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'], [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], - [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Fallbacking into default MEMORY storage'], + [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Falling back into default MEMORY storage'], [c.ERROR_MIN_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': the provided "%s" config param is lower than allowed. Setting to the minimum value %s seconds'], ]; From aa8b830f45e8225ab94edcbea9747e4ac6a1c21d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 May 2022 14:50:11 -0300 Subject: [PATCH 39/70] fixing small nits --- src/storages/inLocalStorage/index.ts | 2 +- src/sync/streaming/pushManager.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 68604e30..e03e311c 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -29,7 +29,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn // Fallback to InMemoryStorage if LocalStorage API is not available if (!isLocalStorageAvailable()) { - params.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Fallbacking into default MEMORY storage'); + params.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage'); return InMemoryStorageCSFactory(params); } diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index fc80de04..329aa7bf 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -183,7 +183,7 @@ export function pushManagerFactory( stopWorkers(); }); - /** Fallbacking without retry due to: STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE and Authentication errors */ + /** Fallback to polling without retry due to: STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE and Authentication errors */ pushEmitter.on(PUSH_NONRETRYABLE_ERROR, function handleNonRetryableError() { disabled = true; @@ -192,7 +192,7 @@ export function pushManagerFactory( pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already }); - /** Fallbacking with retry due to recoverable SSE and Authentication errors */ + /** Fallback to polling with retry due to recoverable SSE and Authentication errors */ pushEmitter.on(PUSH_RETRYABLE_ERROR, function handleRetryableError() { // HTTP or network error in SSE connection // SSE connection is closed to avoid repeated errors due to retries From 5591903b8109713f249d78f735e03cee64acf3dc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 May 2022 16:10:17 -0300 Subject: [PATCH 40/70] fixing leak in unit test --- src/sync/submitters/__tests__/telemetrySubmitter.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index f551f350..772d4ced 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -63,6 +63,9 @@ describe('Telemetry submitter', () => { expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ oM: 0, st: 'memory', sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, aF: 0, rF: 0, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'] })); + + // Stop submitter, to not execute the 1st periodic metrics/usage POST + telemetrySubmitter.stop(); }); }); From ae5bc031592924276fa136c0d78b22252faa4a0d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 May 2022 16:38:04 -0300 Subject: [PATCH 41/70] 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 42/70] 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 2abf9bcdccb3c815fa40151c7a482445f7aff640 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 3 May 2022 11:36:59 -0300 Subject: [PATCH 43/70] interface polishing --- .../inMemory/TelemetryCacheInMemory.ts | 4 +- src/storages/inRedis/TelemetryCacheInRedis.ts | 4 +- .../pluggable/TelemetryCachePluggable.ts | 4 +- src/storages/types.ts | 48 +++++++++---------- .../__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/submitterManager.ts | 4 +- src/sync/submitters/telemetrySubmitter.ts | 8 ++-- src/trackers/eventTracker.ts | 6 +-- src/trackers/impressionsTracker.ts | 8 ++-- src/trackers/telemetryTracker.ts | 20 ++++---- 10 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/storages/inMemory/TelemetryCacheInMemory.ts b/src/storages/inMemory/TelemetryCacheInMemory.ts index 5c84a20c..beb684c1 100644 --- a/src/storages/inMemory/TelemetryCacheInMemory.ts +++ b/src/storages/inMemory/TelemetryCacheInMemory.ts @@ -1,6 +1,6 @@ import { ImpressionDataType, EventDataType, LastSync, HttpErrors, HttpLatencies, StreamingEvent, Method, OperationType, MethodExceptions, MethodLatencies } from '../../sync/submitters/types'; import { findLatencyIndex } from '../findLatencyIndex'; -import { TelemetryCacheSync } from '../types'; +import { ITelemetryCacheSync } from '../types'; const MAX_STREAMING_EVENTS = 20; const MAX_TAGS = 10; @@ -10,7 +10,7 @@ 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]; } -export class TelemetryCacheInMemory implements TelemetryCacheSync { +export class TelemetryCacheInMemory implements ITelemetryCacheSync { private timeUntilReady?: number; diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts index e973baef..88ba546b 100644 --- a/src/storages/inRedis/TelemetryCacheInRedis.ts +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -1,11 +1,11 @@ import { ILogger } from '../../logger/types'; import { Method } from '../../sync/submitters/types'; import { KeyBuilderSS } from '../KeyBuilderSS'; -import { TelemetryCacheAsync } from '../types'; +import { ITelemetryCacheAsync } from '../types'; import { findLatencyIndex } from '../findLatencyIndex'; import { Redis } from 'ioredis'; -export class TelemetryCacheInRedis implements TelemetryCacheAsync { +export class TelemetryCacheInRedis implements ITelemetryCacheAsync { /** * Create a Telemetry cache that uses Redis as storage. diff --git a/src/storages/pluggable/TelemetryCachePluggable.ts b/src/storages/pluggable/TelemetryCachePluggable.ts index 02d3be47..24d344f5 100644 --- a/src/storages/pluggable/TelemetryCachePluggable.ts +++ b/src/storages/pluggable/TelemetryCachePluggable.ts @@ -1,10 +1,10 @@ import { ILogger } from '../../logger/types'; import { Method } from '../../sync/submitters/types'; import { KeyBuilderSS } from '../KeyBuilderSS'; -import { IPluggableStorageWrapper, TelemetryCacheAsync } from '../types'; +import { IPluggableStorageWrapper, ITelemetryCacheAsync } from '../types'; import { findLatencyIndex } from '../findLatencyIndex'; -export class TelemetryCachePluggable implements TelemetryCacheAsync { +export class TelemetryCachePluggable implements ITelemetryCacheAsync { /** * Create a Telemetry cache that uses a storage wrapper. diff --git a/src/storages/types.ts b/src/storages/types.ts index 4c9739e0..ea8616fa 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -362,14 +362,14 @@ export interface IImpressionCountsCacheSync extends IRecorderCacheProducerSync; popLatencies(): Promise; } -export interface TelemetryEvaluationProducerAsync { +export interface ITelemetryEvaluationProducerAsync { recordLatency(method: Method, latencyMs: number): Promise; recordException(method: Method): Promise; } -// ATM it only implements the producer API, used by the SDK. +// ATM it only implements the producer API, used by the SDK in consumer mode. // @TODO implement consumer API for JS Synchronizer. -export interface TelemetryCacheAsync extends TelemetryEvaluationProducerAsync { } +export interface ITelemetryCacheAsync extends ITelemetryEvaluationProducerAsync { } /** * Storages @@ -445,13 +445,15 @@ export interface IStorageBase< TSplitsCache extends ISplitsCacheBase, TSegmentsCache extends ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase, - TEventsCache extends IEventsCacheBase + TEventsCache extends IEventsCacheBase, + TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync > { 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, + ITelemetryCacheSync + > { } export interface IStorageAsync extends IStorageBase< ISplitsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, - IEventsCacheAsync | IEventsCacheSync -> { - telemetry?: TelemetryCacheAsync -} + IEventsCacheAsync | IEventsCacheSync, + ITelemetryCacheAsync + > { } /** StorageFactory */ @@ -483,14 +483,12 @@ export interface IStorageFactoryParams { impressionsQueueSize?: number, eventsQueueSize?: number, optimize?: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, + mode: SDKMode, // ATM, only used by InLocalStorage matchingKey?: string, /* undefined on server-side SDKs */ splitFiltersValidation?: ISplitFiltersValidation, - // ATM, only used by PluggableStorage - mode?: SDKMode, - // This callback is invoked when the storage is ready to be used. Error-first callback style: if an error is passed, // it means that the storge fail to connect and shouldn't be used. // It is meant for emitting SDK_READY event in consumer mode, and for synchronizer to wait before using the storage. diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 6e0373d7..6814f7f4 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -14,7 +14,7 @@ describe('Telemetry submitter', () => { splitApi: { postMetricsUsage }, // @ts-ignore storage: InMemoryStorageFactory({}), }; - const popLatenciesSpy = jest.spyOn(params.storage.telemetry, 'popLatencies'); + const popLatenciesSpy = jest.spyOn(params.storage.telemetry!, 'popLatencies'); test('submits metrics/usage periodically', async () => { // @ts-ignore 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 64dbbd19..179a7304 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -1,13 +1,15 @@ -import { IStorageSync } from '../../storages/types'; +import { ITelemetryCacheSync } from '../../storages/types'; import { submitterFactory, firstPushWindowDecorator } from './submitter'; import { TelemetryUsageStatsPayload } from './types'; import { QUEUED, DEDUPED, DROPPED } from '../../utils/constants'; import { ISyncManagerFactoryParams } from '../types'; +export type ISyncManagerFactoryParamsWithTelemetry = ISyncManagerFactoryParams & { storage: { telemetry: ITelemetryCacheSync } } + /** * 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 @@ -41,7 +43,7 @@ export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: ISto /** * Submitter that periodically posts telemetry data */ -export function telemetrySubmitterFactory(params: ISyncManagerFactoryParams) { +export function telemetrySubmitterFactory(params: ISyncManagerFactoryParamsWithTelemetry) { const { settings: { log, scheduler: { telemetryRefreshRate } }, storage, splitApi } = params; return firstPushWindowDecorator( diff --git a/src/trackers/eventTracker.ts b/src/trackers/eventTracker.ts index 57a75fda..878800d3 100644 --- a/src/trackers/eventTracker.ts +++ b/src/trackers/eventTracker.ts @@ -1,6 +1,6 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; -import { IEventsCacheBase, TelemetryCacheAsync, TelemetryCacheSync } from '../storages/types'; +import { IEventsCacheBase, ITelemetryCacheAsync, ITelemetryCacheSync } from '../storages/types'; import { IEventsHandler, IEventTracker } from './types'; import { ISettings, SplitIO } from '../types'; import { EVENTS_TRACKER_SUCCESS, ERROR_EVENTS_TRACKER } from '../logger/constants'; @@ -17,7 +17,7 @@ export function eventTrackerFactory( settings: ISettings, eventsCache: IEventsCacheBase, integrationsManager?: IEventsHandler, - telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync + telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync ): IEventTracker { const log = settings.log; @@ -60,7 +60,7 @@ export function eventTrackerFactory( } else { // Record when eventsCache is sync only (standalone mode) // @TODO we are not dropping events on full queue yet, so `tracked` is always true ATM - if (telemetryCache) (telemetryCache as TelemetryCacheSync).recordEventStats(tracked ? QUEUED : DROPPED, 1); + if (telemetryCache) (telemetryCache as ITelemetryCacheSync).recordEventStats(tracked ? QUEUED : DROPPED, 1); return queueEventsCallback(eventData, tracked); } } diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index fcb8314f..61b8e2cb 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -1,7 +1,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; import { truncateTimeFrame } from '../utils/time'; -import { IImpressionCountsCacheSync, IImpressionsCacheBase, TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; +import { IImpressionCountsCacheSync, IImpressionsCacheBase, ITelemetryCacheSync, ITelemetryCacheAsync } from '../storages/types'; import { IImpressionsHandler, IImpressionsTracker } from './types'; import { SplitIO, ImpressionDTO, ISettings } from '../types'; import { IImpressionObserver } from './impressionObserver/types'; @@ -26,7 +26,7 @@ export function impressionsTrackerFactory( observer?: IImpressionObserver, // if countsCache is provided, it implies `isOptimized` flag (i.e., if impressions should be deduped or not) countsCache?: IImpressionCountsCacheSync, - telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync + telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync ): IImpressionsTracker { const { log, impressionListener, runtime: { ip, hostname }, version } = settings; @@ -70,8 +70,8 @@ export function impressionsTrackerFactory( // Record when impressionsCache is sync only (standalone mode) // @TODO we are not dropping impressions on full queue yet, so DROPPED stats are not recorded if (telemetryCache) { - (telemetryCache as TelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStore.length); - (telemetryCache as TelemetryCacheSync).recordImpressionStats(DEDUPED, impressions.length - impressionsToStore.length); + (telemetryCache as ITelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStore.length); + (telemetryCache as ITelemetryCacheSync).recordImpressionStats(DEDUPED, impressions.length - impressionsToStore.length); } } diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index bf88805c..7cd266cf 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -1,11 +1,11 @@ -import { TelemetryCacheSync, TelemetryCacheAsync } from '../storages/types'; +import { ITelemetryCacheSync, ITelemetryCacheAsync } from '../storages/types'; import { EXCEPTION, SDK_NOT_READY } from '../utils/labels'; import { ITelemetryTracker } from './types'; import { timer } from '../utils/timeTracker/timer'; import { TOKEN_REFRESH, AUTH_REJECTION } from '../utils/constants'; export function telemetryTrackerFactory( - telemetryCache?: TelemetryCacheSync | TelemetryCacheAsync, + telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync, now?: () => number ): ITelemetryTracker { @@ -21,7 +21,7 @@ export function telemetryTrackerFactory( case EXCEPTION: telemetryCache.recordException(method); return; // Don't track latency on exceptions - case SDK_NOT_READY: // @ts-ignore. TelemetryCacheAsync doesn't implement the method + case SDK_NOT_READY: // @ts-ignore ITelemetryCacheAsync doesn't implement the method telemetryCache?.recordNonReadyUsage(); } telemetryCache.recordLatency(method, evalTime()); @@ -31,22 +31,22 @@ export function telemetryTrackerFactory( const httpTime = timer(now); return (error) => { - (telemetryCache as TelemetryCacheSync).recordHttpLatency(operation, httpTime()); - if (error && error.statusCode) (telemetryCache as TelemetryCacheSync).recordHttpError(operation, error.statusCode); - else (telemetryCache as TelemetryCacheSync).recordSuccessfulSync(operation, now()); + (telemetryCache as ITelemetryCacheSync).recordHttpLatency(operation, httpTime()); + if (error && error.statusCode) (telemetryCache as ITelemetryCacheSync).recordHttpError(operation, error.statusCode); + else (telemetryCache as ITelemetryCacheSync).recordSuccessfulSync(operation, now()); }; }, sessionLength() { - (telemetryCache as TelemetryCacheSync).recordSessionLength(startTime()); + (telemetryCache as ITelemetryCacheSync).recordSessionLength(startTime()); }, streamingEvent(e, d) { if (e === AUTH_REJECTION) { - (telemetryCache as TelemetryCacheSync).recordAuthRejections(); + (telemetryCache as ITelemetryCacheSync).recordAuthRejections(); } else { - (telemetryCache as TelemetryCacheSync).recordStreamingEvents({ + (telemetryCache as ITelemetryCacheSync).recordStreamingEvents({ e, d, t: now() }); - if (e === TOKEN_REFRESH) (telemetryCache as TelemetryCacheSync).recordTokenRefreshes(); + if (e === TOKEN_REFRESH) (telemetryCache as ITelemetryCacheSync).recordTokenRefreshes(); } } }; From 6e017bdb254b8468dc7c1d10150fea1b5a318950 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 3 May 2022 18:33:07 -0300 Subject: [PATCH 44/70] fix unit test --- src/sync/submitters/__tests__/telemetrySubmitter.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 772d4ced..9a9175cf 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -27,7 +27,7 @@ describe('Telemetry submitter', () => { test('submits metrics/usage periodically', async () => { // @ts-ignore const telemetrySubmitter = telemetrySubmitterFactory(params); - const popLatenciesSpy = jest.spyOn(params.storage.telemetry, 'popLatencies'); + const popLatenciesSpy = jest.spyOn(params.storage.telemetry!, 'popLatencies'); telemetrySubmitter.start(); expect(telemetrySubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running @@ -49,8 +49,8 @@ describe('Telemetry submitter', () => { test('submits metrics/config when SDK is ready', async () => { // @ts-ignore const telemetrySubmitter = telemetrySubmitterFactory(params); - const recordTimeUntilReadyFromCacheSpy = jest.spyOn(params.storage.telemetry, 'recordTimeUntilReadyFromCache'); - const recordTimeUntilReadySpy = jest.spyOn(params.storage.telemetry, 'recordTimeUntilReady'); + const recordTimeUntilReadyFromCacheSpy = jest.spyOn(params.storage.telemetry!, 'recordTimeUntilReadyFromCache'); + const recordTimeUntilReadySpy = jest.spyOn(params.storage.telemetry!, 'recordTimeUntilReady'); telemetrySubmitter.start(); From 8d8dad94bc5480294cacf233ec7eac5ae36d9136 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 10:56:47 -0300 Subject: [PATCH 45/70] updated some utils functions --- src/sync/submitters/telemetrySubmitter.ts | 26 ++++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 558ab7c4..dbebd972 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -45,20 +45,16 @@ export function telemetryCacheStatsAdapter({ splits, segments, telemetry }: ISyn }; } -function mapOperationMode(mode: ISettings['mode']) { - switch (mode) { - case STANDALONE_MODE: return STANDALONE_ENUM; - case CONSUMER_MODE: return CONSUMER_ENUM; - case CONSUMER_PARTIAL_MODE: return CONSUMER_PARTIAL_ENUM; - } -} +const OPERATION_MODE_MAP = { + [STANDALONE_MODE]: STANDALONE_ENUM, + [CONSUMER_MODE]: CONSUMER_ENUM, + [CONSUMER_PARTIAL_MODE]: CONSUMER_PARTIAL_ENUM +} as Record; -function mapImpressionsMode(mode: ISettings['sync']['impressionsMode']) { - switch (mode) { - case OPTIMIZED: return OPTIMIZED_ENUM; - case DEBUG: return DEBUG_ENUM; - } -} +const IMPRESSIONS_MODE_MAP = { + [OPTIMIZED]: OPTIMIZED_ENUM, + [DEBUG]: DEBUG_ENUM +} as Record; function getActiveFactories() { return Object.keys(usedKeysMap).length; @@ -82,7 +78,7 @@ export function telemetryCacheConfigAdapter(settings: ISettings, telemetryCache: const { urls, scheduler } = settings; return { - oM: mapOperationMode(settings.mode), // @ts-ignore lower case of storage type + oM: OPERATION_MODE_MAP[settings.mode], // @ts-ignore lower case of storage type st: settings.storage.type.toLowerCase(), sE: settings.streamingEnabled, rR: { @@ -101,7 +97,7 @@ export function telemetryCacheConfigAdapter(settings: ISettings, telemetryCache: }, // urlOverrides iQ: scheduler.impressionsQueueSize, eQ: scheduler.eventsQueueSize, - iM: mapImpressionsMode(settings.sync.impressionsMode), + iM: IMPRESSIONS_MODE_MAP[settings.sync.impressionsMode], iL: settings.impressionListener ? true : false, hP: false, // @TODO proxy not supported aF: getActiveFactories(), From efe7af8f7891765c80dd6dcbd66bd1e61bf3abc8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 11:03:11 -0300 Subject: [PATCH 46/70] comments regarding flushing telemetry on browser listener --- src/listeners/browser.ts | 1 + src/storages/pluggable/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index a7deef15..d4be3831 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -77,6 +77,7 @@ 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); + // No beacon endpoint for `/metrics/usage` } // Close streaming connection diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 99fcf0e1..4f2d3a0b 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -77,7 +77,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined, events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata), - // @TODO not supported by Synchronizer yet + // @TODO Not using TelemetryCachePluggable yet, because it is not supported by the Split Synchronizer // telemetry: isPartialConsumer ? new TelemetryCacheInMemory() : new TelemetryCachePluggable(log, keys, wrapper), // Disconnect the underlying storage From 7c12d3c52911aaa4ee4eaf8e8f2b708e9fff4f6c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 14:21:58 -0300 Subject: [PATCH 47/70] fix interface --- package-lock.json | 2 +- package.json | 2 +- src/sdkFactory/index.ts | 2 +- src/sdkFactory/types.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4a208a3..73687c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.0", + "version": "1.3.2-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e9861bed..987ecb67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.0", + "version": "1.3.2-rc.1", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index bb5070d0..9bba6f95 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -69,7 +69,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 ctx: ISdkFactoryContext = { splitApi, eventTracker, impressionsTracker, telemetryTracker, sdkReadinessManager, readiness, settings, storage, platform }; diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 9d0700ef..f041c863 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -64,7 +64,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 381e21cad0b1d5ecb9f90fc4e0c2c12fcf94bcf9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 14:23:19 -0300 Subject: [PATCH 48/70] 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 49/70] 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 50/70] 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 }]]); From 0fc8fee5ec5a8c8b6c0c4c358659773d3c2c0ec1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 4 May 2022 17:44:26 -0300 Subject: [PATCH 51/70] update unit test to avoid flakiness --- src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts index e230fbc0..15d81e6b 100644 --- a/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts @@ -36,6 +36,7 @@ describe('SEGMENTS CACHE IN REDIS', () => { expect(await cache.isInSegment('mocked-segment', 'd')).toBe(true); expect(await cache.isInSegment('mocked-segment', 'e')).toBe(true); + await cache.clear(); await connection.quit(); }); @@ -53,6 +54,7 @@ describe('SEGMENTS CACHE IN REDIS', () => { ['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true)); + await cache.clear(); await connection.quit(); }); From fc3cdc54da5da21b61cc40843c870f12e73b9a7c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 5 May 2022 13:54:39 -0300 Subject: [PATCH 52/70] updated telemetry default URL. using DEBUG log level for telemetry related logs --- package-lock.json | 2 +- package.json | 2 +- src/services/splitApi.ts | 4 ++-- src/sync/submitters/submitter.ts | 8 ++++---- src/types.ts | 2 +- src/utils/settingsValidation/__tests__/index.spec.ts | 2 +- src/utils/settingsValidation/index.ts | 2 +- src/utils/settingsValidation/url.ts | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73687c88..88509e4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.1", + "version": "1.3.2-rc.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 987ecb67..b0fd02a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.1", + "version": "1.3.2-rc.2", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 37d2deab..b1b01bfb 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -108,12 +108,12 @@ export function splitApiFactory( }, postMetricsConfig(body: string) { - const url = `${urls.telemetry}/metrics/config`; + const url = `${urls.telemetry}/v1/metrics/config`; return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true); }, postMetricsUsage(body: string) { - const url = `${urls.telemetry}/metrics/usage`; + const url = `${urls.telemetry}/v1/metrics/usage`; return splitHttpClient(url, { method: 'POST', body }, telemetryTracker.trackHttp(TELEMETRY), true); } }; diff --git a/src/sync/submitters/submitter.ts b/src/sync/submitters/submitter.ts index 974bb8e8..a1312c7d 100644 --- a/src/sync/submitters/submitter.ts +++ b/src/sync/submitters/submitter.ts @@ -16,7 +16,7 @@ export function submitterFactory( dataName: string, fromCacheToPayload?: (cacheData: TState) => any, maxRetries: number = 0, - debugLogs?: boolean + debugLogs?: boolean // true for telemetry submitters ): ISyncTask<[], void> { let retries = 0; @@ -37,14 +37,14 @@ export function submitterFactory( sourceCache.clear(); // we clear the queue if request successes. }).catch(err => { if (!maxRetries) { - log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]); + log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]); } else if (retries === maxRetries) { retries = 0; sourceCache.clear(); // we clear the queue if request fails after retries. - log.warn(SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]); + log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_FAILS, [dataCountMessage, err]); } else { retries++; - log.warn(SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]); + log[debugLogs ? 'debug' : 'warn'](SUBMITTERS_PUSH_RETRY, [dataCountMessage, err]); } }); } diff --git a/src/types.ts b/src/types.ts index 4ea5ce31..dd2cb085 100644 --- a/src/types.ts +++ b/src/types.ts @@ -684,7 +684,7 @@ export namespace SplitIO { /** * String property to override the base URL where the SDK will post telemetry data. * @property {string} telemetry - * @default 'https://telemetry.split.io' + * @default 'https://telemetry.split.io/api' */ telemetry?: string }; diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index f2c2431b..5191d546 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -37,7 +37,7 @@ describe('settingsValidation', () => { events: 'https://events.split.io/api', auth: 'https://auth.split.io/api', streaming: 'https://streaming.split.io', - telemetry: 'https://telemetry.split.io', + telemetry: 'https://telemetry.split.io/api', }); expect(settings.sync.impressionsMode).toBe(OPTIMIZED); }); diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 9a20a56b..faf860b3 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -58,7 +58,7 @@ export const base = { // Streaming Server streaming: 'https://streaming.split.io', // Telemetry Server - telemetry: 'https://telemetry.split.io', + telemetry: 'https://telemetry.split.io/api', }, // Defines which kind of storage we should instanciate. diff --git a/src/utils/settingsValidation/url.ts b/src/utils/settingsValidation/url.ts index 142d2dc4..4dd0179e 100644 --- a/src/utils/settingsValidation/url.ts +++ b/src/utils/settingsValidation/url.ts @@ -1,6 +1,6 @@ import { ISettings } from '../../types'; -const telemetryEndpointMatcher = /^\/metrics\/(config|usage)/; +const telemetryEndpointMatcher = /^\/v1\/metrics\/(config|usage)/; const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/; const authEndpointMatcher = /^\/v2\/auth/; const streamingEndpointMatcher = /^\/(sse|event-stream)/; From 1ee5ca95528c9cdd81b88a84d671157fd26bcc2c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 5 May 2022 16:33:51 -0300 Subject: [PATCH 53/70] updated logic to handle internalReadyCbCount --- package-lock.json | 2 +- package.json | 2 +- src/readiness/__tests__/sdkReadinessManager.spec.ts | 3 ++- src/readiness/sdkReadinessManager.ts | 12 +++++++----- src/readiness/types.ts | 8 +++++++- .../submitters/__tests__/telemetrySubmitter.spec.ts | 5 ++--- src/sync/submitters/telemetrySubmitter.ts | 3 ++- 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88509e4a..9dc16446 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.2", + "version": "1.3.2-rc.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b0fd02a6..b2d0a8c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.2", + "version": "1.3.2-rc.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index b17ea51f..3c93b7a6 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -172,7 +172,8 @@ describe('SDK Readiness Manager - Event emitter', () => { test('The event callbacks should work as expected - SDK_READY emits with expected internal callbacks', () => { // the sdkReadinessManager expects more than one SDK_READY callback to not log the "No listeners" warning - const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock, undefined /* default readyTimeout */, 1 /* internalReadyCbCount */); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); + sdkReadinessManager.incInternalReadyCbCount(); const gateMock = sdkReadinessManager.readinessManager.gate; // Get the callbacks diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index f50f78f9..c137e040 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -15,18 +15,16 @@ const REMOVE_LISTENER_EVENT = 'removeListener'; * It also updates logs related warnings and errors. * * @param readyTimeout time in millis to emit SDK_READY_TIME_OUT event - * @param internalReadyCbCount offset value of SDK_READY listeners that are added/removed internally - * by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected' * @param readinessManager optional readinessManager to use. only used internally for `shared` method */ export function sdkReadinessManagerFactory( log: ILogger, EventEmitter: new () => IEventEmitter, readyTimeout = 0, - internalReadyCbCount = 0, readinessManager = readinessManagerFactory(EventEmitter, readyTimeout)): ISdkReadinessManager { /** Ready callback warning */ + let internalReadyCbCount = 0; let readyCbCount = 0; readinessManager.gate.on(REMOVE_LISTENER_EVENT, (event: any) => { if (event === SDK_READY) readyCbCount--; @@ -74,8 +72,12 @@ export function sdkReadinessManagerFactory( return { readinessManager, - shared(readyTimeout = 0, internalReadyCbCount = 0) { - return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, internalReadyCbCount, readinessManager.shared(readyTimeout)); + shared(readyTimeout = 0) { + return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, readinessManager.shared(readyTimeout)); + }, + + incInternalReadyCbCount() { + internalReadyCbCount++; }, sdkStatus: objectAssign( diff --git a/src/readiness/types.ts b/src/readiness/types.ts index 288a2717..93986f60 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -66,6 +66,12 @@ export interface ISdkReadinessManager { readinessManager: IReadinessManager sdkStatus: IStatusInterface + /** + * Increment internalReadyCbCount, an offset value of SDK_READY listeners that are added/removed internally + * by the SDK. It is required to properly log the warning 'No listeners for SDK Readiness detected' + */ + incInternalReadyCbCount(): void + /** for client-side */ - shared(readyTimeout?: number, internalReadyCbCount?: number): ISdkReadinessManager + shared(readyTimeout?: number): ISdkReadinessManager } diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index cbf65e06..530d4c5f 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -14,9 +14,8 @@ describe('Telemetry submitter', () => { settings: { ...fullSettings, scheduler: { ...fullSettings.scheduler, telemetryRefreshRate } }, splitApi: { postMetricsUsage, postMetricsConfig }, // @ts-ignore storage: InMemoryStorageFactory({}), - platform: { - now: () => 123 // by returning a fixed timestamp, all latencies are equal to 0 - }, + platform: { now: () => 123 }, // by returning a fixed timestamp, all latencies are equal to 0 + sdkReadinessManager: { incInternalReadyCbCount: jest.fn(), }, readiness: { gate: { once: jest.fn((e: string, cb: () => void) => { diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 5d1ab125..8bcdd41f 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -117,7 +117,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) { const { storage: { splits, segments, telemetry } } = params; if (!telemetry) return; // No submitter created if telemetry cache is not defined - const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness } = params; + const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness, sdkReadinessManager } = params; const startTime = timer(now || Date.now); const submitter = firstPushWindowDecorator( @@ -129,6 +129,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) { telemetry.recordTimeUntilReadyFromCache(startTime()); }); + sdkReadinessManager.incInternalReadyCbCount(); readiness.gate.once(SDK_READY, () => { telemetry.recordTimeUntilReady(startTime()); From ca7a3781b92007096b6c9bd34f9c60fe95d19400 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 9 May 2022 13:33:27 -0300 Subject: [PATCH 54/70] updated browser listener to post telemetry stats to beacon endpoint --- src/listeners/__tests__/browser.spec.ts | 29 +++++++++++++++++++------ src/listeners/browser.ts | 7 +++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 5ec91eff..4657d24e 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -3,6 +3,18 @@ import { IEventsCacheSync, IImpressionCountsCacheSync, IImpressionsCacheSync, IS import { ISplitApi } from '../../services/types'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; +jest.mock('../../sync/submitters/telemetrySubmitter', () => { + return { + telemetryCacheStatsAdapter: () => { + return { + isEmpty: () => false, + clear: () => { }, + state: () => ({}), + }; + } + }; +}); + /* Mocks start */ const fakeImpression = { @@ -26,6 +38,7 @@ const fakeImpressionCounts = { 'someFeature::0': 1 }; +// Storage with impressionsCount and telemetry cache const fakeStorageOptimized = { // @ts-expect-error impressions: { isEmpty: jest.fn(), @@ -48,6 +61,7 @@ const fakeStorageOptimized = { // @ts-expect-error return fakeImpressionCounts; } } as IImpressionCountsCacheSync, + telemetry: {} }; const fakeStorageDebug = { @@ -59,7 +73,8 @@ const fakeStorageDebug = { const fakeSplitApi = { postTestImpressionsBulk: jest.fn(() => Promise.resolve()), postEventsBulk: jest.fn(() => Promise.resolve()), - postTestImpressionsCount: jest.fn(() => Promise.resolve()) + postTestImpressionsCount: jest.fn(() => Promise.resolve()), + postMetricsUsage: jest.fn(() => Promise.resolve()), } as ISplitApi; const UNLOAD_DOM_EVENT = 'unload'; @@ -131,7 +146,7 @@ test('Browser JS listener / consumer mode', () => { expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); }); -test('Browser JS listener / standalone mode / Impressions optimized mode', () => { +test('Browser JS listener / standalone mode / Impressions optimized mode with telemetry', () => { const syncManagerMock = {}; // @ts-expect-error @@ -144,8 +159,8 @@ test('Browser JS listener / standalone mode / Impressions optimized mode', () => triggerUnloadEvent(); - // Unload event was triggered. Thus sendBeacon method should have been called three times. - expect(global.window.navigator.sendBeacon).toBeCalledTimes(3); + // Unload event was triggered. Thus sendBeacon method should have been called four times. + expect(global.window.navigator.sendBeacon).toBeCalledTimes(4); // Http post services should have not been called expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); @@ -195,7 +210,7 @@ test('Browser JS listener / standalone mode / Impressions debug mode', () => { }); test('Browser JS listener / standalone mode / Impressions debug mode without sendBeacon API', () => { - // remove sendBeacon API + // remove sendBeacon API temporally const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error global.navigator.sendBeacon = undefined; const syncManagerMockWithoutPushManager = {}; @@ -254,8 +269,8 @@ test('Browser JS listener / standalone mode / user consent status', () => { settings.userConsent = undefined; triggerUnloadEvent(); - // Unload event was triggered when user consent was granted and undefined. Thus sendBeacon should be called 6 times (3 times per event in optimized mode). - expect(global.window.navigator.sendBeacon).toBeCalledTimes(6); + // Unload event was triggered when user consent was granted and undefined. Thus sendBeacon should be called 8 times (4 times per event in optimized mode with telemetry). + expect(global.window.navigator.sendBeacon).toBeCalledTimes(8); listener.stop(); }); diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index d4be3831..faf5c956 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -12,6 +12,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; import { ISyncManager } from '../sync/types'; import { isConsentGranted } from '../consent'; +import { telemetryCacheStatsAdapter } from '../sync/submitters/telemetrySubmitter'; // 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring. const UNLOAD_DOM_EVENT = 'unload'; @@ -77,7 +78,11 @@ 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); - // No beacon endpoint for `/metrics/usage` + 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 From 5b6f2eb14606f1eb64840cfe1192151ec0b8395d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 May 2022 17:35:26 -0300 Subject: [PATCH 55/70] rc --- package-lock.json | 2 +- package.json | 2 +- src/utils/murmur3/utfx.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9dc16446..fcbf68a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.3", + "version": "1.3.2-rc.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b2d0a8c6..78c90af5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.3", + "version": "1.3.2-rc.4", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/utils/murmur3/utfx.ts b/src/utils/murmur3/utfx.ts index 9bc44270..fd9125f4 100644 --- a/src/utils/murmur3/utfx.ts +++ b/src/utils/murmur3/utfx.ts @@ -8,8 +8,7 @@ */ export interface utfx { - encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void, - + encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void } From 15f55b5c3697c4f974181e99400d50117727de4d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 May 2022 10:45:36 -0300 Subject: [PATCH 56/70] add userConsent telemetry property --- .../submitters/__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 11 +++++++++-- src/sync/submitters/types.ts | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 530d4c5f..4b9ff810 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -61,7 +61,7 @@ describe('Telemetry submitter', () => { expect(recordTimeUntilReadySpy).toBeCalledTimes(1); expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ - oM: 0, st: 'memory', sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, aF: 0, rF: 0, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'] + oM: 0, st: 'memory', sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, aF: 0, rF: 0, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 })); // Stop submitter, to not execute the 1st periodic metrics/usage POST diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 8bcdd41f..71f83a81 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -1,9 +1,9 @@ import { ISegmentsCacheSync, ISplitsCacheSync, ITelemetryCacheSync } 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'; +import { QUEUED, DEDUPED, DROPPED, CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, DEBUG_ENUM, OPTIMIZED_ENUM, CONSENT_GRANTED, CONSENT_DECLINED, CONSENT_UNKNOWN } from '../../utils/constants'; import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants'; -import { ISettings } from '../../types'; +import { ConsentStatus, ISettings } from '../../types'; import { base } from '../../utils/settingsValidation'; import { usedKeysMap } from '../../utils/inputValidation/apiKey'; import { timer } from '../../utils/timeTracker/timer'; @@ -54,6 +54,12 @@ const IMPRESSIONS_MODE_MAP = { [DEBUG]: DEBUG_ENUM } as Record; +const USER_CONSENT_MAP = { + [CONSENT_UNKNOWN]: 1, + [CONSENT_GRANTED]: 2, + [CONSENT_DECLINED]: 3 +} as Record; + function getActiveFactories() { return Object.keys(usedKeysMap).length; } @@ -105,6 +111,7 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett nR: telemetry.getNonReadyUsage(), t: telemetry.popTags(), i: settings.integrations && settings.integrations.map(int => int.type), + uC: settings.userConsent ? USER_CONSENT_MAP[settings.userConsent] : 0 }; } }; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index f30c5fd3..188c648c 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -184,4 +184,5 @@ export type TelemetryConfigStatsPayload = { nR: number, // SDKNotReadyUsage t?: Array, // tags i?: Array, // integrations + uC: number, // userConsent } From c3e0f9d3d71d4fba8468dd72645bc99f96416ddf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 12 May 2022 15:59:32 -0300 Subject: [PATCH 57/70] synchronize config for consumer mode --- src/storages/KeyBuilderSS.ts | 18 ++++++++++----- src/storages/inRedis/index.ts | 8 ++++++- src/storages/pluggable/index.ts | 3 ++- .../__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 22 ++++++++++++------- src/sync/submitters/types.ts | 14 +++++++----- 6 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index d766409d..025f8608 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -23,10 +23,6 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.prefix}.segments.registered`; } - private buildVersionablePrefix() { - return `${this.metadata.s}/${this.metadata.n}/${this.metadata.i}`; - } - buildImpressionsKey() { return `${this.prefix}.impressions`; } @@ -35,6 +31,12 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.prefix}.events`; } + searchPatternForSplitKeys() { + return `${this.buildSplitKeyPrefix()}*`; + } + + /* Telemetry keys */ + buildLatencyKey(method: Method, bucket: number) { return `${this.prefix}.telemetry.latencies::${this.buildVersionablePrefix()}/${methodNames[method]}/${bucket}`; } @@ -43,8 +45,12 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.prefix}.telemetry.exceptions::${this.buildVersionablePrefix()}/${methodNames[method]}`; } - searchPatternForSplitKeys() { - return `${this.buildSplitKeyPrefix()}*`; + buildInitKey() { + return `${this.prefix}.telemetry.init::${this.buildVersionablePrefix()}`; + } + + private buildVersionablePrefix() { + return `${this.metadata.s}/${this.metadata.n}/${this.metadata.i}`; } } diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 84fe4860..06ed2114 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -8,6 +8,7 @@ import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis'; import { EventsCacheInRedis } from './EventsCacheInRedis'; import { STORAGE_REDIS } from '../../utils/constants'; import { TelemetryCacheInRedis } from './TelemetryCacheInRedis'; +import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter'; export interface InRedisStorageOptions { prefix?: string @@ -22,7 +23,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy const prefix = validatePrefix(options.prefix); - function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { + function InRedisStorageFactory({ log, metadata, onReadyCb, mode }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); @@ -30,6 +31,11 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy // subscription to Redis connect event in order to emit SDK_READY event on consumer mode redisClient.on('connect', () => { onReadyCb(); + + // Synchronize config + const [key, field] = keys.buildInitKey().split('::'); + const value = JSON.stringify({ t: getTelemetryConfigStats(mode, STORAGE_REDIS) }); + redisClient.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ }); }); return { diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 4f2d3a0b..0a4fa920 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -39,6 +39,7 @@ function validatePluggableStorageOptions(options: any) { function wrapperConnect(wrapper: IPluggableStorageWrapper, onReadyCb: (error?: any) => void) { wrapper.connect().then(() => { onReadyCb(); + // At the moment, we don't synchronize config with pluggable storage }).catch((e) => { onReadyCb(e || new Error('Error connecting wrapper')); }); @@ -77,7 +78,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined, events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata), - // @TODO Not using TelemetryCachePluggable yet, because it is not supported by the Split Synchronizer + // @TODO Not using TelemetryCachePluggable yet, because it's not supported by the Split Synchronizer. It also needs to drop/squeue operations until the wrapper is ready // telemetry: isPartialConsumer ? new TelemetryCacheInMemory() : new TelemetryCachePluggable(log, keys, wrapper), // Disconnect the underlying storage diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 4b9ff810..9b5b5b05 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -61,7 +61,7 @@ describe('Telemetry submitter', () => { expect(recordTimeUntilReadySpy).toBeCalledTimes(1); expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ - oM: 0, st: 'memory', sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, aF: 0, rF: 0, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 + oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 })); // Stop submitter, to not execute the 1st periodic metrics/usage POST diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 71f83a81..b846effd 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -1,13 +1,14 @@ import { ISegmentsCacheSync, ISplitsCacheSync, ITelemetryCacheSync } from '../../storages/types'; import { submitterFactory, firstPushWindowDecorator } from './submitter'; -import { TelemetryUsageStatsPayload, TelemetryConfigStatsPayload } from './types'; +import { TelemetryUsageStatsPayload, TelemetryConfigStatsPayload, TelemetryConfigStats } 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, CONSENT_GRANTED, CONSENT_DECLINED, CONSENT_UNKNOWN } from '../../utils/constants'; import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants'; -import { ConsentStatus, ISettings } from '../../types'; +import { ConsentStatus, ISettings, SDKMode } from '../../types'; import { base } from '../../utils/settingsValidation'; import { usedKeysMap } from '../../utils/inputValidation/apiKey'; import { timer } from '../../utils/timeTracker/timer'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; +import { objectAssign } from '../../utils/lang/objectAssign'; /** * Converts data from telemetry cache into /metrics/usage request payload. @@ -70,6 +71,15 @@ function getRedundantActiveFactories() { }, 0); } +export function getTelemetryConfigStats(mode: SDKMode, storageType: string): TelemetryConfigStats { + return { + oM: OPERATION_MODE_MAP[mode], // @ts-ignore lower case of storage type + st: storageType.toLowerCase(), + aF: getActiveFactories(), + rF: getRedundantActiveFactories(), + }; +} + /** * Converts data from telemetry cache and settings into /metrics/config request payload. */ @@ -81,9 +91,7 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett state(): TelemetryConfigStatsPayload { const { urls, scheduler } = settings; - return { - oM: OPERATION_MODE_MAP[settings.mode], // @ts-ignore lower case of storage type - st: settings.storage.type.toLowerCase(), + return objectAssign(getTelemetryConfigStats(settings.mode, settings.storage.type), { sE: settings.streamingEnabled, rR: { sp: scheduler.featuresRefreshRate, @@ -104,15 +112,13 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett iM: IMPRESSIONS_MODE_MAP[settings.sync.impressionsMode], iL: settings.impressionListener ? true : false, hP: false, // @TODO proxy not supported - aF: getActiveFactories(), - rF: getRedundantActiveFactories(), tR: telemetry.getTimeUntilReady() as number, tC: telemetry.getTimeUntilReadyFromCache(), nR: telemetry.getNonReadyUsage(), t: telemetry.popTags(), i: settings.integrations && settings.integrations.map(int => int.type), uC: settings.userConsent ? USER_CONSENT_MAP[settings.userConsent] : 0 - }; + }); } }; } diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 188c648c..91739337 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -165,10 +165,17 @@ export type UrlOverrides = { t: boolean, // telemetry } -// 'metrics/config' JSON request body -export type TelemetryConfigStatsPayload = { +// 'telemetry.init' Redis/Pluggable key +export type TelemetryConfigStats = { oM?: OperationMode, // operationMode st: 'memory' | 'redis' | 'pluggable' | 'localstorage', // storage + aF: number, // activeFactories + rF: number, // redundantActiveFactories + t?: Array, // tags +} + +// 'metrics/config' JSON request body +export type TelemetryConfigStatsPayload = TelemetryConfigStats & { sE: boolean, // streamingEnabled rR: RefreshRates, // refreshRates uO: UrlOverrides, // urlOverrides @@ -177,12 +184,9 @@ export type TelemetryConfigStatsPayload = { iM: ImpressionsMode, // impressionsMode iL: boolean, // impressionsListenerEnabled hP: boolean, // httpProxyDetected - aF: number, // activeFactories - rF: number, // redundantActiveFactories tR: number, // timeUntilSDKReady tC?: number, // timeUntilSDKReadyFromCache nR: number, // SDKNotReadyUsage - t?: Array, // tags i?: Array, // integrations uC: number, // userConsent } From aea78101d401bcaf6335fb87b7002235ad31e1fc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 16 May 2022 13:00:23 -0300 Subject: [PATCH 58/70] recordCondig method --- src/storages/inRedis/TelemetryCacheInRedis.ts | 7 +++++++ .../__tests__/TelemetryCacheInRedis.spec.ts | 17 ++++++++++++++++- src/storages/inRedis/index.ts | 10 ++++------ src/storages/pluggable/index.ts | 2 +- src/sync/submitters/types.ts | 2 +- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts index 88ba546b..64612ab6 100644 --- a/src/storages/inRedis/TelemetryCacheInRedis.ts +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -4,6 +4,8 @@ import { KeyBuilderSS } from '../KeyBuilderSS'; import { ITelemetryCacheAsync } from '../types'; import { findLatencyIndex } from '../findLatencyIndex'; import { Redis } from 'ioredis'; +import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter'; +import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants'; export class TelemetryCacheInRedis implements ITelemetryCacheAsync { @@ -26,4 +28,9 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync { .catch(() => { /* Handle rejections for telemetry */ }); } + recordConfig() { + const [key, field] = this.keys.buildInitKey().split('::'); + const value = JSON.stringify({ t: getTelemetryConfigStats(CONSUMER_MODE, STORAGE_REDIS) }); + return this.redis.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ }); + } } diff --git a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts index 7f2bf391..35674db0 100644 --- a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts @@ -7,28 +7,43 @@ import { fakeMetadata } from '../../pluggable/__tests__/ImpressionsCachePluggabl const prefix = 'telemetry_cache_ut'; const exceptionKey = `${prefix}.telemetry.exceptions`; const latencyKey = `${prefix}.telemetry.latencies`; +const initKey = `${prefix}.telemetry.init`; const fieldVersionablePrefix = `${fakeMetadata.s}/${fakeMetadata.n}/${fakeMetadata.i}`; -test('TELEMETRY CACHE IN REDIS / `recordLatency` and `recordException`', async () => { +test('TELEMETRY CACHE IN REDIS', async () => { const keysBuilder = new KeyBuilderSS(prefix, fakeMetadata); const connection = new Redis(); const cache = new TelemetryCacheInRedis(loggerMock, keysBuilder, connection); + // recordException expect(await cache.recordException('tr')).toBe(1); expect(await cache.recordException('tr')).toBe(2); expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe('2'); expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatment')).toBe(null); + // recordLatency expect(await cache.recordLatency('tr', 1.6)).toBe(1); expect(await cache.recordLatency('tr', 1.6)).toBe(2); expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe('2'); expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatment/2')).toBe(null); + // recordConfig + expect(await cache.recordConfig()).toBe(1); + expect(JSON.parse(await connection.hget(initKey, fieldVersionablePrefix) as string)).toEqual({ + t: { + oM: 1, + st: 'redis', + aF: 0, + rF: 0 + } + }); + // Clean up then end. await connection.hdel(exceptionKey, fieldVersionablePrefix + '/track'); await connection.hdel(latencyKey, fieldVersionablePrefix + '/track/2'); + await connection.hdel(initKey, fieldVersionablePrefix); await connection.quit(); }); diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 06ed2114..b6022695 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -8,7 +8,6 @@ import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis'; import { EventsCacheInRedis } from './EventsCacheInRedis'; import { STORAGE_REDIS } from '../../utils/constants'; import { TelemetryCacheInRedis } from './TelemetryCacheInRedis'; -import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter'; export interface InRedisStorageOptions { prefix?: string @@ -23,19 +22,18 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy const prefix = validatePrefix(options.prefix); - function InRedisStorageFactory({ log, metadata, onReadyCb, mode }: IStorageFactoryParams): IStorageAsync { + function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); + const telemetry = new TelemetryCacheInRedis(log, keys, redisClient); // subscription to Redis connect event in order to emit SDK_READY event on consumer mode redisClient.on('connect', () => { onReadyCb(); // Synchronize config - const [key, field] = keys.buildInitKey().split('::'); - const value = JSON.stringify({ t: getTelemetryConfigStats(mode, STORAGE_REDIS) }); - redisClient.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ }); + telemetry.recordConfig(); }); return { @@ -43,7 +41,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy segments: new SegmentsCacheInRedis(log, keys, redisClient), impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata), events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata), - telemetry: new TelemetryCacheInRedis(log, keys, redisClient), + telemetry, // When using REDIS we should: // 1- Disconnect from the storage diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 0a4fa920..c0ca4cc3 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -78,7 +78,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined, events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata), - // @TODO Not using TelemetryCachePluggable yet, because it's not supported by the Split Synchronizer. It also needs to drop/squeue operations until the wrapper is ready + // @TODO Not using TelemetryCachePluggable yet because it's not supported by the Split Synchronizer, and needs to drop or queue operations while the wrapper is not ready // telemetry: isPartialConsumer ? new TelemetryCacheInMemory() : new TelemetryCachePluggable(log, keys, wrapper), // Disconnect the underlying storage diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 91739337..aa3dcb59 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -167,7 +167,7 @@ export type UrlOverrides = { // 'telemetry.init' Redis/Pluggable key export type TelemetryConfigStats = { - oM?: OperationMode, // operationMode + oM: OperationMode, // operationMode st: 'memory' | 'redis' | 'pluggable' | 'localstorage', // storage aF: number, // activeFactories rF: number, // redundantActiveFactories From 96ee3d4d439bd566ca90d58439b2ba370a16a853 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 16 May 2022 14:38:32 -0300 Subject: [PATCH 59/70] update telemetry.init value --- src/storages/inRedis/TelemetryCacheInRedis.ts | 2 +- .../inRedis/__tests__/TelemetryCacheInRedis.spec.ts | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/storages/inRedis/TelemetryCacheInRedis.ts b/src/storages/inRedis/TelemetryCacheInRedis.ts index 64612ab6..7f5ccd51 100644 --- a/src/storages/inRedis/TelemetryCacheInRedis.ts +++ b/src/storages/inRedis/TelemetryCacheInRedis.ts @@ -30,7 +30,7 @@ export class TelemetryCacheInRedis implements ITelemetryCacheAsync { recordConfig() { const [key, field] = this.keys.buildInitKey().split('::'); - const value = JSON.stringify({ t: getTelemetryConfigStats(CONSUMER_MODE, STORAGE_REDIS) }); + const value = JSON.stringify(getTelemetryConfigStats(CONSUMER_MODE, STORAGE_REDIS)); return this.redis.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ }); } } diff --git a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts index 35674db0..258fe682 100644 --- a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts @@ -33,12 +33,10 @@ test('TELEMETRY CACHE IN REDIS', async () => { // recordConfig expect(await cache.recordConfig()).toBe(1); expect(JSON.parse(await connection.hget(initKey, fieldVersionablePrefix) as string)).toEqual({ - t: { - oM: 1, - st: 'redis', - aF: 0, - rF: 0 - } + oM: 1, + st: 'redis', + aF: 0, + rF: 0 }); // Clean up then end. From d295dc4ffaad7ca176d53b52fc52c7d72b092c7f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 16 May 2022 16:24:36 -0300 Subject: [PATCH 60/70] fix issue with hset --- src/storages/inRedis/RedisAdapter.ts | 2 +- src/storages/inRedis/__tests__/RedisAdapter.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 0c374f23..45ef7b15 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -8,7 +8,7 @@ import { timeout } from '../../utils/promise/timeout'; const LOG_PREFIX = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. -const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim']; +const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim', 'hset']; // Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere. const DEFAULT_OPTIONS = { diff --git a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts index 1e9fd512..ecdf855a 100644 --- a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts +++ b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts @@ -11,7 +11,7 @@ const LOG_PREFIX = 'storage:redis-adapter: '; // Mocking ioredis // The list of methods we're wrapping on a promise (for timeout) on the adapter. -const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget']; +const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim', 'hset']; const ioredisMock = reduce([...METHODS_TO_PROMISE_WRAP, 'disconnect'], (acc, methodName) => { acc[methodName] = jest.fn(() => Promise.resolve(methodName)); From ab9733bfbce7627f9e14e2c502c386893265f8c4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 16 May 2022 16:29:46 -0300 Subject: [PATCH 61/70] update ci --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 277879c2..55ae4708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,8 @@ on: - '*' pull_request: branches: - - '*' + - main + - development jobs: build: From 030ef82246e2635ef6672e880f026ed9e7d59b6e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 17 May 2022 16:30:31 -0300 Subject: [PATCH 62/70] review feedback --- package-lock.json | 2 +- package.json | 2 +- .../submitters/__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 12 ++++++------ src/trackers/telemetryTracker.ts | 4 ++-- src/utils/timeTracker/now/node.ts | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcbf68a1..3fff5fb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.4", + "version": "1.3.2-rc.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 78c90af5..306d2bbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.4", + "version": "1.3.2-rc.5", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 9b5b5b05..49e5fd5d 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -61,7 +61,7 @@ describe('Telemetry submitter', () => { expect(recordTimeUntilReadySpy).toBeCalledTimes(1); expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ - oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 1, se: 1, im: 1, ev: 1, te: 100 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 + oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, se: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 })); // Stop submitter, to not execute the 1st periodic metrics/usage POST diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index b846effd..b9882d01 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -59,7 +59,7 @@ const USER_CONSENT_MAP = { [CONSENT_UNKNOWN]: 1, [CONSENT_GRANTED]: 2, [CONSENT_DECLINED]: 3 -} as Record; +} as Record; function getActiveFactories() { return Object.keys(usedKeysMap).length; @@ -94,11 +94,11 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett return objectAssign(getTelemetryConfigStats(settings.mode, settings.storage.type), { sE: settings.streamingEnabled, rR: { - sp: scheduler.featuresRefreshRate, - se: scheduler.segmentsRefreshRate, - im: scheduler.impressionsRefreshRate, - ev: scheduler.eventsPushRate, - te: scheduler.telemetryRefreshRate, + sp: scheduler.featuresRefreshRate / 1000, + se: scheduler.segmentsRefreshRate / 1000, + im: scheduler.impressionsRefreshRate / 1000, + ev: scheduler.eventsPushRate / 1000, + te: scheduler.telemetryRefreshRate / 1000, }, // refreshRates uO: { s: urls.sdk !== base.urls.sdk, diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index aa0a6e4c..ab0e1d9e 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -33,7 +33,7 @@ export function telemetryTrackerFactory( return (error) => { (telemetryCache as ITelemetryCacheSync).recordHttpLatency(operation, httpTime()); if (error && error.statusCode) (telemetryCache as ITelemetryCacheSync).recordHttpError(operation, error.statusCode); - else (telemetryCache as ITelemetryCacheSync).recordSuccessfulSync(operation, now()); + else (telemetryCache as ITelemetryCacheSync).recordSuccessfulSync(operation, Date.now()); }; }, sessionLength() { // @ts-ignore ITelemetryCacheAsync doesn't implement the method @@ -44,7 +44,7 @@ export function telemetryTrackerFactory( (telemetryCache as ITelemetryCacheSync).recordAuthRejections(); } else { (telemetryCache as ITelemetryCacheSync).recordStreamingEvents({ - e, d, t: now() + e, d, t: Date.now() }); if (e === TOKEN_REFRESH) (telemetryCache as ITelemetryCacheSync).recordTokenRefreshes(); } diff --git a/src/utils/timeTracker/now/node.ts b/src/utils/timeTracker/now/node.ts index fe5c5f27..27aaa9cd 100644 --- a/src/utils/timeTracker/now/node.ts +++ b/src/utils/timeTracker/now/node.ts @@ -3,5 +3,5 @@ export function now() { // eslint-disable-next-line no-undef let time = process.hrtime(); - return time[0] * 1e3 + time[1] * 1e-6; // convert it to milis + return time[0] * 1e3 + time[1] * 1e-6; // convert it to millis } From f53c5f389040f98ead6f0d83e257fd33f68c798d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 May 2022 19:35:57 -0300 Subject: [PATCH 63/70] remove unused timeTracker --- src/sdkFactory/types.ts | 18 +- src/sync/submitters/telemetrySubmitter.ts | 8 +- src/utils/timeTracker/__tests__/index.spec.ts | 48 ---- src/utils/timeTracker/index.ts | 226 ------------------ src/utils/timeTracker/now/browser.ts | 2 +- src/utils/timeTracker/now/node.ts | 1 - 6 files changed, 21 insertions(+), 282 deletions(-) delete mode 100644 src/utils/timeTracker/index.ts diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index f041c863..de8abe3f 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -11,13 +11,27 @@ import { SplitIO, ISettings, IEventEmitter } from '../types'; /** * Environment related dependencies. - * These getters are called a fixed number of times per factory instantiation. */ export interface IPlatform { - getOptions?: () => object + /** + * If provided, it is used to retrieve the Fetch API for HTTP requests. Otherwise, the global fetch is used. + */ getFetch?: () => (IFetch | undefined) + /** + * If provided, it is used to pass additional options to fetch calls. + */ + getOptions?: () => object + /** + * If provided, it is used to retrieve the EventSource constructor for streaming support. + */ getEventSource?: () => (IEventSourceConstructor | undefined) + /** + * EventEmitter constructor, like NodeJS.EventEmitter or a polyfill. + */ EventEmitter: new () => IEventEmitter, + /** + * Function used to track latencies for telemetry. + */ now?: () => number } diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index b9882d01..54b36a97 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -127,11 +127,11 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett * Submitter that periodically posts telemetry data */ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) { - const { storage: { splits, segments, telemetry } } = params; - if (!telemetry) return; // No submitter created if telemetry cache is not defined + const { storage: { splits, segments, telemetry }, platform: { now } } = params; + if (!telemetry || !now) return; // No submitter created if telemetry cache is not defined - const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, platform: { now }, readiness, sdkReadinessManager } = params; - const startTime = timer(now || Date.now); + const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, readiness, sdkReadinessManager } = params; + const startTime = timer(now); const submitter = firstPushWindowDecorator( submitterFactory(log, splitApi.postMetricsUsage, telemetryCacheStatsAdapter(telemetry, splits, segments), telemetryRefreshRate, 'telemetry stats', undefined, 0, true), diff --git a/src/utils/timeTracker/__tests__/index.spec.ts b/src/utils/timeTracker/__tests__/index.spec.ts index 3b405264..da026034 100644 --- a/src/utils/timeTracker/__tests__/index.spec.ts +++ b/src/utils/timeTracker/__tests__/index.spec.ts @@ -1,7 +1,4 @@ import { timer } from '../timer'; -import { TrackerAPI } from '../index'; -import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { IResponse } from '../../../services/types'; test('TIMER / should count the time between two tasks', (done) => { const timerDuration = Math.floor(Math.random() * 1000); // In millis @@ -15,48 +12,3 @@ test('TIMER / should count the time between two tasks', (done) => { done(); }, timerDuration); }); - -describe('TIME TRACKER', () => { - - test('should have the correct API', () => { - expect(typeof TrackerAPI.start).toBe('function'); // It should have the correct API. - expect(typeof TrackerAPI.stop).toBe('function'); // It should have the correct API. - expect(typeof TrackerAPI.TaskNames).toBe('object'); // It should have the correct API. - }); - - test('start() / should return the correct type', () => { - const promise = new Promise(res => { - setTimeout(res, 1000); - }); - const startNormal = TrackerAPI.start(loggerMock, TrackerAPI.TaskNames.SDK_READY); - const startNormalFake = TrackerAPI.start(loggerMock, 'fakeTask3'); - const startWithPromise = TrackerAPI.start(loggerMock, 'fakeTask4', undefined, promise); - - expect(typeof startNormal).toBe('function'); // If we call start without a promise, it will return the stop function, - // @ts-expect-error - expect(typeof startNormal.setCollectorForTask).toBe('function'); // that has a function as well for setting the collector at a defered time, because it has a registered cb but no collector received. - // @ts-expect-error - expect(typeof startNormalFake.setCollectorForTask).toBe('undefined'); // If no callback is registered for the task, no collectors setup function is attached to returned one. - // @ts-expect-error - expect(typeof startWithPromise.then).toBe('function'); // But if we pass a promise, we will get a promise back, with the necessary callbacks already handled. - }); - - test('stop() / should stop the timer and return the time, if any', () => { - TrackerAPI.start(loggerMock, 'test_task'); - - // creating two tasks with the same task name - const stopFromStart = TrackerAPI.start(loggerMock, 'fakeTask5') as () => number; - const stopFromStart2 = TrackerAPI.start(loggerMock, 'fakeTask5') as () => number; - - const stopNotExistentTask = TrackerAPI.stop(loggerMock, 'not_existent'); - const stopNotExistentTaskAndModifier = TrackerAPI.stop(loggerMock, 'test_task', 'mod'); - - expect(typeof stopNotExistentTask).toBe('undefined'); // If we try to stop a timer that does not exist, we get undefined. - expect(typeof stopNotExistentTaskAndModifier).toBe('undefined'); // If we try to stop a timer that does not exist, we get undefined. - expect(typeof stopFromStart()).toBe('number'); // But if we stop an existing task from the startUnique() returned function, we get a number. - expect(typeof stopFromStart()).toBe('undefined'); // if we stop the same task again, we get undefined. - - expect(typeof stopFromStart2()).toBe('number'); // But if we stop another task created with the same task name, we get a number. - }); - -}); diff --git a/src/utils/timeTracker/index.ts b/src/utils/timeTracker/index.ts deleted file mode 100644 index 433a8d52..00000000 --- a/src/utils/timeTracker/index.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { uniqueId } from '../lang'; -import { timer } from './timer'; -import { thenable } from '../promise/thenable'; -import { ILogger } from '../../logger/types'; -import { IResponse } from '../../services/types'; - -// Based on ProducerMetricsCollector and ClientCollector classes -interface MetricsCollector { - // ProducerMetricsCollector API - countException(): void, - count(status: number): void, - latency(ms: number): void, - - // ClientCollector API - ready(ms: number): void, - getTreatment(ms: number): void, - getTreatments(ms: number): void, - getTreatmentWithConfig(ms: number): void, - getTreatmentsWithConfig(ms: number): void, - - [method: string]: (ms: number) => void, -} - -// Map we will use for storing timers data -const timers: Record void), - timer: () => number -}> = {}; - -// Tasks constants -const CONSTANTS = { - SDK_READY: 'Getting ready - Split SDK', - SDK_GET_TREATMENT: 'SDK - Get Treatment', - SDK_GET_TREATMENTS: 'SDK - Get Treatments', - SDK_GET_TREATMENT_WITH_CONFIG: 'SDK - Get Treatment with config', - SDK_GET_TREATMENTS_WITH_CONFIG: 'SDK - Get Treatments with config', - SPLITS_READY: 'Getting ready - Splits', - SEGMENTS_READY: 'Getting ready - Segments', - METRICS_PUSH: 'Pushing - Metrics', - IMPRESSIONS_PUSH: 'Pushing - Impressions', - EVENTS_PUSH: 'Pushing - Events', - MY_SEGMENTS_FETCH: 'Fetching - My Segments', - SEGMENTS_FETCH: 'Fetching - Segments', - SPLITS_FETCH: 'Fetching - Splits' -}; -// Tasks callbacks, if any -const CALLBACKS = { - [CONSTANTS.SDK_READY]: { - collector: 'client', - method: 'ready' - }, - [CONSTANTS.SDK_GET_TREATMENT]: { - collector: 'client', - method: 'getTreatment' - }, - [CONSTANTS.SDK_GET_TREATMENTS]: { - collector: 'client', - method: 'getTreatments' - }, - [CONSTANTS.SDK_GET_TREATMENT_WITH_CONFIG]: { - collector: 'client', - method: 'getTreatmentWithConfig' - }, - [CONSTANTS.SDK_GET_TREATMENTS_WITH_CONFIG]: { - collector: 'client', - method: 'getTreatmentsWithConfig' - }, - [CONSTANTS.MY_SEGMENTS_FETCH]: { - collector: 'mySegments', - method: 'latency' - }, - [CONSTANTS.SEGMENTS_FETCH]: { - collector: 'segmentChanges', - method: 'latency' - }, - [CONSTANTS.SPLITS_FETCH]: { - collector: 'splitChanges', - method: 'latency' - } -}; -/** - * Generates the timer keys using the task name and a modifier, if any. - * @param {string} task - The task name - * @param {number | string} modifier - (optional) The modifier, if any. - * @return {string} The generated timer key - */ -function generateTimerKey(task: string, modifier?: number | string) { return modifier ? task + modifier : task; } -/** - * Given the collectors map, it returns the specific collector for a given task. - * - * @param {string} task - The task name - * @param {Object} collectors - The collectors map - */ -function getCollectorForTask(task: string, collectors?: Record): false | MetricsCollector { - const callbackData = CALLBACKS[task]; - - if (callbackData && collectors) return collectors[callbackData.collector]; - - return false; -} -/** - * Given a collector and a task, returns the callback function that should be called when we stop the timer. - * - * @param {string} task - The task name - * @param {Object} collector - The collector object for the task - */ -function getCallbackForTask(task: string, collector: MetricsCollector | false): ((ms: number) => void) | false { - const callbackData = CALLBACKS[task]; - - if (callbackData && collector) return collector[callbackData.method]; - - return false; -} - -// Our "time tracker" API -export const TrackerAPI = { - /** - * "Private" method, used to attach count/countException and stop callbacks to a promise. - * - * @param {ILogger} log - Logger. - * @param {Promise} promise - The promise we want to attach the callbacks. - * @param {string} task - The name of the task. - * @param {number | string} modifier - (optional) The modifier for the task, if any. - */ - __attachToPromise(log: ILogger, promise: Promise, task: string, collector: false | MetricsCollector, modifier?: number | string) { - return promise.then(resp => { - this.stop(log, task, modifier); - - if (collector && collector.count) collector.count(resp.status); - - return resp; - }) - .catch(err => { - this.stop(log, task, modifier); - - if (collector && collector.countException) collector.countException(); - - throw err; - }); - }, - /** - * Starts tracking the time for a given task. All tasks tracked are considered "unique" because - * there may be multiple SDK instances tracking a "generic" task, making any task non-generic. - * - * @param {ILogger} log - Logger. - * @param {string} task - The task we are starting. - * @param {Object} collectors - The collectors map. - * @param {Promise} promise - (optional) The promise we are tracking. - * @return {Function | Promise} The stop function for this specific task or the promise received with the callbacks registered. - */ - start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now: () => number = Date.now): Promise | (() => number) { - const taskUniqueId = uniqueId(); - const taskCollector = getCollectorForTask(task, collectors); - let result; - - // If we are registering a promise with this task, we should count the status and the exceptions as well - // as stopping the task when the promise resolves. Then return the promise - if (thenable(promise)) { - result = this.__attachToPromise(log, promise, task, taskCollector, taskUniqueId); - } else { - // If not, we return the stop function, as it will be stopped manually. - result = this.stop.bind(this, log, task, taskUniqueId); - if (CALLBACKS[task] && !taskCollector) { - // and provide a way for a defered setup of the collector, if needed. - // @ts-expect-error - result.setCollectorForTask = this.setCollectorForTask.bind(this, task, taskUniqueId); - } - } - - // We start the timer, with an uniqueId attached to it's name, and save tracking info for this task. - const trackingKey = generateTimerKey(task, taskUniqueId); - const cb = getCallbackForTask(task, taskCollector); - timers[trackingKey] = { - cb, - timer: timer(now) - }; - - return result as () => number; - }, - /** - * Setup the collector for a task that reports metrics. - * - * @param {string} task - The task name - * @param {number | string} taskUniqueId - The unique identifier for this task - * @param {Object} collectors - The collectors map. - */ - setCollectorForTask(task: string, taskUniqueId: number | string, collectors: Record) { - const taskCollector = getCollectorForTask(task, collectors); - - if (taskCollector) { - const trackingKey = generateTimerKey(task, taskUniqueId); - timers[trackingKey].cb = getCallbackForTask(task, taskCollector); - } - }, - /** - * Stops the tracking of a given task. - * - * @param {ILogger} log - Logger. - * @param {string} task - The task we are starting. - * @param {number | string} modifier - (optional) The modifier for that specific task. - */ - stop(log: ILogger, task: string, modifier?: number | string) { - const timerName = generateTimerKey(task, modifier); - const timerData = timers[timerName]; - if (timerData) { - // Stop the timer and round result for readability. - const et = timerData.timer(); - log.debug(`[TIME TRACKER]: [${task}] took ${et}ms to finish.`); - - // Check if we have a tracker callback. - if (timerData.cb) { - // If we have a callback, we call it with the elapsed time of the task and then delete the reference. - timerData.cb(et); - } - - // Remove the task tracking reference. - delete timers[timerName]; - - return et; - } - }, - /** - * The constants shortcut for the task names. - */ - TaskNames: CONSTANTS -}; diff --git a/src/utils/timeTracker/now/browser.ts b/src/utils/timeTracker/now/browser.ts index 5c939606..635157f5 100644 --- a/src/utils/timeTracker/now/browser.ts +++ b/src/utils/timeTracker/now/browser.ts @@ -1,4 +1,4 @@ -// @TODO migrate to Browser SDK package eventually +// Can be used on any runtime, since it fallbacks to `Date.now` if `performance.now` is not available function nowFactory() { // eslint-disable-next-line if (typeof performance === 'object' && typeof performance.now === 'function') { diff --git a/src/utils/timeTracker/now/node.ts b/src/utils/timeTracker/now/node.ts index 27aaa9cd..59d81a0f 100644 --- a/src/utils/timeTracker/now/node.ts +++ b/src/utils/timeTracker/now/node.ts @@ -1,4 +1,3 @@ -// @TODO migrate to Node SDK package eventually export function now() { // eslint-disable-next-line no-undef let time = process.hrtime(); From 1e39aca39664e3df8edded86bc524da8850be417 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 20 May 2022 14:55:21 -0300 Subject: [PATCH 64/70] mySegments refresh rate --- package-lock.json | 2 +- package.json | 2 +- .../__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 4 +++- src/sync/submitters/types.ts | 3 ++- src/utils/timeTracker/now/__tests__/now.spec.ts | 14 ++++++++++---- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3fff5fb8..b55fa9c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.5", + "version": "1.3.2-rc.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 306d2bbd..d16841f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.5", + "version": "1.3.2-rc.6", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index 49e5fd5d..bbe74a59 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -61,7 +61,7 @@ describe('Telemetry submitter', () => { expect(recordTimeUntilReadySpy).toBeCalledTimes(1); expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ - oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, se: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 + oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, ms: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 })); // Stop submitter, to not execute the 1st periodic metrics/usage POST diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 54b36a97..8e208e59 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -90,12 +90,14 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett state(): TelemetryConfigStatsPayload { const { urls, scheduler } = settings; + const isClientSide = settings.core.key !== undefined; return objectAssign(getTelemetryConfigStats(settings.mode, settings.storage.type), { sE: settings.streamingEnabled, rR: { sp: scheduler.featuresRefreshRate / 1000, - se: scheduler.segmentsRefreshRate / 1000, + se: isClientSide ? undefined : scheduler.segmentsRefreshRate / 1000, + ms: isClientSide ? scheduler.segmentsRefreshRate / 1000 : undefined, im: scheduler.impressionsRefreshRate / 1000, ev: scheduler.eventsPushRate / 1000, te: scheduler.telemetryRefreshRate / 1000, diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index aa3dcb59..f4eb8c7b 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -151,7 +151,8 @@ export type ImpressionsMode = OPTIMIZED_ENUM | DEBUG_ENUM; export type RefreshRates = { sp: number, // splits - se: number, // mySegments + se?: number, // segments + ms?: number, // mySegments im: number, // impressions ev: number, // events te: number, // telemetry diff --git a/src/utils/timeTracker/now/__tests__/now.spec.ts b/src/utils/timeTracker/now/__tests__/now.spec.ts index ded2c815..5097d894 100644 --- a/src/utils/timeTracker/now/__tests__/now.spec.ts +++ b/src/utils/timeTracker/now/__tests__/now.spec.ts @@ -16,15 +16,21 @@ limitations under the License. import { now as nowBrowser } from '../browser'; import { now as nowNode } from '../node'; +import { nearlyEqual } from '../../../../__tests__/testUtils/index'; [nowBrowser, nowNode].forEach(now => { - test('NOW / should generate a value each time you call it', () => { - let n1 = now(); - let n2 = now(); - let n3 = now(); + test('NOW / should generate a value each time you call it', async () => { + const n1 = now(); + const n2 = now(); + + const delay = 200; + await new Promise(res => setTimeout(res, delay)); + const n3 = now(); expect(Number.isFinite(n1)).toBe(true); // is a finite value? expect(Number.isFinite(n2)).toBe(true); // is a finite value? expect(Number.isFinite(n3)).toBe(true); // is a finite value? + expect(nearlyEqual(n1, n2)).toBe(true); + expect(nearlyEqual(n1 + delay, n3)).toBe(true); }); }); From c599a8ef80e0d418b3279cb7d8ce80ab778debe5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 20 May 2022 15:15:07 -0300 Subject: [PATCH 65/70] prepare stable release --- CHANGES.txt | 5 +++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 35a4822c..bcc3a808 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +1.4.0 (May 20, 2022) + - Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property. + - Updated SDK telemetry storage, metrics and updater to be more effective and send less often. + - Bugfixing - Updated default values for `scheduler.impressionsRefreshRate` config parameter: 300s for OPTIMIZED impression mode and 60s for DEBUG impression mode. + 1.3.1 (April 19, 2022) - Bugfixing - Added peer dependencies to avoid issues when requiring some third-party dependencies used by modules of the package (Related to issue https://github.com/splitio/javascript-client/issues/662). - Bugfixing - Updated `ready` method to rejects the promise with an Error object instead of a string value (Related to issue https://github.com/splitio/javascript-client/issues/654). diff --git a/package-lock.json b/package-lock.json index b55fa9c8..2be921ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.6", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d16841f8..671e18f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.6", + "version": "1.4.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 6fe85118a105547a4b5437978a459d31abab9718 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 20 May 2022 16:52:48 -0300 Subject: [PATCH 66/70] update release date --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index bcc3a808..cb625471 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.4.0 (May 20, 2022) +1.4.0 (May 23, 2022) - Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property. - Updated SDK telemetry storage, metrics and updater to be more effective and send less often. - Bugfixing - Updated default values for `scheduler.impressionsRefreshRate` config parameter: 300s for OPTIMIZED impression mode and 60s for DEBUG impression mode. From 184f3c18fe8c6dea2cc6098fea3ef49a4391339f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 23 May 2022 17:10:51 -0300 Subject: [PATCH 67/70] removed minimum impressionsRefreshRate --- CHANGES.txt | 2 +- package-lock.json | 2 +- package.json | 2 +- src/utils/lang/index.ts | 2 +- .../__tests__/index.spec.ts | 23 ++++++++++--------- src/utils/settingsValidation/index.ts | 13 +++-------- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index cb625471..00388146 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.4.0 (May 23, 2022) +1.4.0 (May 24, 2022) - Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property. - Updated SDK telemetry storage, metrics and updater to be more effective and send less often. - Bugfixing - Updated default values for `scheduler.impressionsRefreshRate` config parameter: 300s for OPTIMIZED impression mode and 60s for DEBUG impression mode. diff --git a/package-lock.json b/package-lock.json index 2be921ba..445dd36f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.0", + "version": "1.3.2-rc.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 671e18f6..95e66f9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.4.0", + "version": "1.3.2-rc.7", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index bd7c7a51..0a828dda 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -71,7 +71,7 @@ export function forOwn(obj: { [key: string]: T }, iteratee: (value: T, key: s * Safely retrieve the specified prop from obj. If we can't retrieve * that property value, we return the default value. */ -export function get(obj: any, prop: any, val: any): any { +export function get(obj: any, prop: any, val?: any): any { let res = val; try { // No risks nor lots of checks. diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 5191d546..7e765c9d 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -246,17 +246,18 @@ describe('settingsValidation', () => { expect(settings.core.trafficType).toEqual(true); // traffic type is ignored }); - test('validate min values', () => { - const settings = settingsValidation({ - scheduler: { - telemetryRefreshRate: 0, - impressionsRefreshRate: 'invalid', - } - }, minimalSettingsParams); - - expect(settings.scheduler.telemetryRefreshRate).toBe(60000); - expect(settings.scheduler.impressionsRefreshRate).toBe(60000); - }); + // Not implemented yet + // test('validate min values', () => { + // const settings = settingsValidation({ + // scheduler: { + // telemetryRefreshRate: 0, + // impressionsRefreshRate: 'invalid', + // } + // }, minimalSettingsParams); + + // expect(settings.scheduler.telemetryRefreshRate).toBe(60000); + // expect(settings.scheduler.impressionsRefreshRate).toBe(60000); + // }); }); diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index faf860b3..2269721f 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -1,4 +1,4 @@ -import { merge } from '../lang'; +import { merge, get } from '../lang'; import { mode } from './mode'; import { validateSplitFilters } from './splitFilters'; import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE, DEBUG } from '../constants'; @@ -131,15 +131,8 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate); scheduler.telemetryRefreshRate = fromSecondsToMillis(validateMinValue('telemetryRefreshRate', scheduler.telemetryRefreshRate, 60)); - if (scheduler.impressionsRefreshRate !== base.scheduler.impressionsRefreshRate) { - // Validate impressionsRefreshRate defined by user - scheduler.impressionsRefreshRate = validateMinValue('impressionsRefreshRate', scheduler.impressionsRefreshRate, - withDefaults.sync.impressionsMode === DEBUG ? 1 : 60 // Min is 1 sec for DEBUG and 60 secs for OPTIMIZED - ); - } else { - // Default impressionsRefreshRate for DEBUG mode is 60 secs - if (withDefaults.sync.impressionsMode === DEBUG) scheduler.impressionsRefreshRate = 60; - } + // Default impressionsRefreshRate for DEBUG mode is 60 secs + if (get(config, 'scheduler.impressionsRefreshRate') === undefined && withDefaults.sync.impressionsMode === DEBUG) scheduler.impressionsRefreshRate = 60; scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate); // Log deprecation for old telemetry param From 5e5665a41624ae480a8d03d37ae3750c9da30d93 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 24 May 2022 12:40:27 -0300 Subject: [PATCH 68/70] stable version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 445dd36f..2be921ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.7", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 95e66f9f..671e18f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.3.2-rc.7", + "version": "1.4.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 72fdf8402fe313433a76db12781dac03098a6675 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 24 May 2022 12:56:53 -0300 Subject: [PATCH 69/70] additional unit test --- .../__tests__/index.spec.ts | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 7e765c9d..e9b826a8 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -42,32 +42,36 @@ describe('settingsValidation', () => { expect(settings.sync.impressionsMode).toBe(OPTIMIZED); }); - test('override with defaults', () => { - const settings = settingsValidation({ - core: { - authorizationKey: 'dummy token' - }, - sync: { - impressionsMode: 'some', - } - }, minimalSettingsParams); + test('override with default impressionMode if provided one is invalid', () => { + const config = { + core: { authorizationKey: 'dummy token' }, + sync: { impressionsMode: 'some' } + }; + let settings = settingsValidation(config, minimalSettingsParams); + + expect(settings.sync.impressionsMode).toBe(OPTIMIZED); + expect(settings.scheduler.impressionsRefreshRate).toBe(300000); // Default + + settings = settingsValidation({ ...config, scheduler: { impressionsRefreshRate: 10 } }, minimalSettingsParams); expect(settings.sync.impressionsMode).toBe(OPTIMIZED); - expect(settings.scheduler.impressionsRefreshRate).toBe(300000); + expect(settings.scheduler.impressionsRefreshRate).toBe(10000); }); test('impressionsMode should be configurable', () => { - const settings = settingsValidation({ - core: { - authorizationKey: 'dummy token' - }, - sync: { - impressionsMode: DEBUG - } - }, minimalSettingsParams); + const config = { + core: { authorizationKey: 'dummy token' }, + sync: { impressionsMode: DEBUG } + }; + let settings = settingsValidation(config, minimalSettingsParams); expect(settings.sync.impressionsMode).toEqual(DEBUG); expect(settings.scheduler.impressionsRefreshRate).toBe(60000); // Different default for DEBUG impressionsMode + + settings = settingsValidation({ ...config, scheduler: { impressionsRefreshRate: 10 } }, minimalSettingsParams); + + expect(settings.sync.impressionsMode).toBe(OPTIMIZED); + expect(settings.scheduler.impressionsRefreshRate).toBe(10000); }); test('urls should be configurable', () => { From 0b7d4bb52a6bd35b82eba379a293ce95614ae600 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 24 May 2022 13:15:56 -0300 Subject: [PATCH 70/70] fix unit test --- 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 e9b826a8..e82d1d99 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -70,7 +70,7 @@ describe('settingsValidation', () => { settings = settingsValidation({ ...config, scheduler: { impressionsRefreshRate: 10 } }, minimalSettingsParams); - expect(settings.sync.impressionsMode).toBe(OPTIMIZED); + expect(settings.sync.impressionsMode).toBe(DEBUG); expect(settings.scheduler.impressionsRefreshRate).toBe(10000); });