1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

chore: Convert client metrics controller to typescript (#831)

Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
This commit is contained in:
checketts 2021-06-24 11:22:12 -06:00 committed by GitHub
parent 00f2c7312d
commit 2f013bacbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 406 additions and 242 deletions

View File

@ -1,4 +1,5 @@
'use strict'; import { Knex } from 'knex';
import { Logger, LogProvider } from '../logger';
const METRICS_COLUMNS = ['id', 'created_at', 'metrics']; const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
const TABLE = 'client_metrics'; const TABLE = 'client_metrics';
@ -11,18 +12,27 @@ const mapRow = row => ({
metrics: row.metrics, metrics: row.metrics,
}); });
class ClientMetricsDb { export interface IClientMetric {
constructor(db, getLogger) { id: number;
this.db = db; createdAt: Date;
metrics: any;
}
export class ClientMetricsDb {
private readonly logger: Logger;
private readonly timer: NodeJS.Timeout;
constructor(private db: Knex, getLogger: LogProvider) {
this.logger = getLogger('client-metrics-db.js'); this.logger = getLogger('client-metrics-db.js');
// Clear old metrics regulary // Clear old metrics regularly
const clearer = () => this.removeMetricsOlderThanOneHour(); const clearer = () => this.removeMetricsOlderThanOneHour();
setTimeout(clearer, 10).unref(); setTimeout(clearer, 10).unref();
this.timer = setInterval(clearer, ONE_MINUTE).unref(); this.timer = setInterval(clearer, ONE_MINUTE).unref();
} }
async removeMetricsOlderThanOneHour() { async removeMetricsOlderThanOneHour(): Promise<void> {
try { try {
const rows = await this.db(TABLE) const rows = await this.db(TABLE)
.whereRaw("created_at < now() - interval '1 hour'") .whereRaw("created_at < now() - interval '1 hour'")
@ -36,12 +46,12 @@ class ClientMetricsDb {
} }
// Insert new client metrics // Insert new client metrics
async insert(metrics) { async insert(metrics: IClientMetric): Promise<void> {
return this.db(TABLE).insert({ metrics }); return this.db(TABLE).insert({ metrics });
} }
// Used at startup to load all metrics last week into memory! // Used at startup to load all metrics last week into memory!
async getMetricsLastHour() { async getMetricsLastHour(): Promise<IClientMetric[]> {
try { try {
const result = await this.db const result = await this.db
.select(METRICS_COLUMNS) .select(METRICS_COLUMNS)
@ -57,7 +67,7 @@ class ClientMetricsDb {
} }
// Used to poll for new metrics // Used to poll for new metrics
async getNewMetrics(lastKnownId) { async getNewMetrics(lastKnownId: number): Promise<IClientMetric[]> {
try { try {
const res = await this.db const res = await this.db
.select(METRICS_COLUMNS) .select(METRICS_COLUMNS)
@ -72,9 +82,7 @@ class ClientMetricsDb {
return []; return [];
} }
destroy() { destroy(): void {
clearInterval(this.timer); clearInterval(this.timer);
} }
} }
module.exports = ClientMetricsDb;

View File

@ -1,8 +1,7 @@
'use strict'; import EventEmitter from 'events';
const { EventEmitter } = require('events'); import { ClientMetricsStore } from './client-metrics-store';
const ClientMetricStore = require('./client-metrics-store'); import getLogger from '../../test/fixtures/no-logger';
const getLogger = require('../../test/fixtures/no-logger');
function getMockDb() { function getMockDb() {
const list = [ const list = [
@ -28,7 +27,7 @@ test('should call database on startup', done => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const mock = getMockDb(); const mock = getMockDb();
const ee = new EventEmitter(); const ee = new EventEmitter();
const store = new ClientMetricStore(mock, ee, getLogger); const store = new ClientMetricsStore(mock as any, ee, getLogger);
jest.runAllTicks(); jest.runAllTicks();
@ -49,7 +48,7 @@ test('should start poller even if initial database fetch fails', done => {
const mock = getMockDb(); const mock = getMockDb();
mock.getMetricsLastHour = () => Promise.reject(new Error('oops')); mock.getMetricsLastHour = () => Promise.reject(new Error('oops'));
const ee = new EventEmitter(); const ee = new EventEmitter();
const store = new ClientMetricStore(mock, ee, getLogger, 100); const store = new ClientMetricsStore(mock as any, ee, getLogger, 100);
jest.runAllTicks(); jest.runAllTicks();
const metrics = []; const metrics = [];
@ -74,7 +73,7 @@ test('should poll for updates', done => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const mock = getMockDb(); const mock = getMockDb();
const ee = new EventEmitter(); const ee = new EventEmitter();
const store = new ClientMetricStore(mock, ee, getLogger, 100); const store = new ClientMetricsStore(mock as any, ee, getLogger, 100);
jest.runAllTicks(); jest.runAllTicks();
const metrics = []; const metrics = [];

View File

@ -1,17 +1,31 @@
'use strict'; 'use strict';
const { EventEmitter } = require('events'); import EventEmitter from 'events';
const metricsHelper = require('../util/metrics-helper'); import { ClientMetricsDb, IClientMetric } from './client-metrics-db';
const { DB_TIME } = require('../metric-events'); import { Logger, LogProvider } from '../logger';
import metricsHelper from '../util/metrics-helper';
import { DB_TIME } from '../metric-events';
const TEN_SECONDS = 10 * 1000; const TEN_SECONDS = 10 * 1000;
class ClientMetricsStore extends EventEmitter { export class ClientMetricsStore extends EventEmitter {
constructor(metricsDb, eventBus, getLogger, pollInterval = TEN_SECONDS) { private logger: Logger;
highestIdSeen = 0;
private startTimer: Function;
private timer: NodeJS.Timeout;
constructor(
private metricsDb: ClientMetricsDb,
eventBus: EventEmitter,
getLogger: LogProvider,
pollInterval = TEN_SECONDS,
) {
super(); super();
this.logger = getLogger('client-metrics-store.js'); this.logger = getLogger('client-metrics-store.js');
this.metricsDb = metricsDb; this.metricsDb = metricsDb;
this.eventBus = eventBus;
this.highestIdSeen = 0; this.highestIdSeen = 0;
this.startTimer = action => this.startTimer = action =>
@ -25,7 +39,7 @@ class ClientMetricsStore extends EventEmitter {
}); });
} }
async _init(pollInterval) { async _init(pollInterval: number): Promise<void> {
try { try {
const metrics = await this.metricsDb.getMetricsLastHour(); const metrics = await this.metricsDb.getMetricsLastHour();
this._emitMetrics(metrics); this._emitMetrics(metrics);
@ -36,18 +50,18 @@ class ClientMetricsStore extends EventEmitter {
this.emit('ready'); this.emit('ready');
} }
_startPoller(pollInterval) { _startPoller(pollInterval: number): void {
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval); this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
this.timer.unref(); this.timer.unref();
} }
_fetchNewAndEmit() { _fetchNewAndEmit(): void {
this.metricsDb this.metricsDb
.getNewMetrics(this.highestIdSeen) .getNewMetrics(this.highestIdSeen)
.then(metrics => this._emitMetrics(metrics)); .then(metrics => this._emitMetrics(metrics));
} }
_emitMetrics(metrics) { _emitMetrics(metrics: IClientMetric[]): void {
if (metrics && metrics.length > 0) { if (metrics && metrics.length > 0) {
this.highestIdSeen = metrics[metrics.length - 1].id; this.highestIdSeen = metrics[metrics.length - 1].id;
metrics.forEach(m => this.emit('metrics', m.metrics)); metrics.forEach(m => this.emit('metrics', m.metrics));
@ -55,7 +69,7 @@ class ClientMetricsStore extends EventEmitter {
} }
// Insert new client metrics // Insert new client metrics
async insert(metrics) { async insert(metrics: IClientMetric): Promise<void> {
const stopTimer = this.startTimer('insert'); const stopTimer = this.startTimer('insert');
await this.metricsDb.insert(metrics); await this.metricsDb.insert(metrics);
@ -63,10 +77,8 @@ class ClientMetricsStore extends EventEmitter {
stopTimer(); stopTimer();
} }
destroy() { destroy(): void {
clearInterval(this.timer); clearInterval(this.timer);
this.metricsDb.destroy(); this.metricsDb.destroy();
} }
} }
module.exports = ClientMetricsStore;

View File

@ -21,7 +21,7 @@ interface IEventTable {
tags: []; tags: [];
} }
interface ICreateEvent { export interface ICreateEvent {
type: string; type: string;
createdBy: string; createdBy: string;
data?: any; data?: any;

View File

@ -11,8 +11,8 @@ import FeatureToggleStore from './feature-toggle-store';
import FeatureTypeStore from './feature-type-store'; import FeatureTypeStore from './feature-type-store';
import StrategyStore from './strategy-store'; import StrategyStore from './strategy-store';
import ClientInstanceStore from './client-instance-store'; import ClientInstanceStore from './client-instance-store';
import ClientMetricsDb from './client-metrics-db'; import { ClientMetricsDb } from './client-metrics-db';
import ClientMetricsStore from './client-metrics-store'; import { ClientMetricsStore } from './client-metrics-store';
import ClientApplicationsStore from './client-applications-store'; import ClientApplicationsStore from './client-applications-store';
import ContextFieldStore from './context-field-store'; import ContextFieldStore from './context-field-store';
import SettingStore from './setting-store'; import SettingStore from './setting-store';

View File

@ -106,7 +106,10 @@ class MetricsController extends Controller {
async getApplications(req: Request, res: Response): Promise<void> { async getApplications(req: Request, res: Response): Promise<void> {
try { try {
const applications = await this.metrics.getApplications(req.query); const query = req.query.strategyName
? { strategyName: req.query.strategyName as string }
: {};
const applications = await this.metrics.getApplications(query);
res.json({ applications }); res.json({ applications });
} catch (err) { } catch (err) {
handleErrors(res, this.logger, err); handleErrors(res, this.logger, err);

View File

@ -1,6 +1,4 @@
'use strict'; import joi from 'joi';
const joi = require('joi');
const countSchema = joi const countSchema = joi
.object() .object()
@ -19,7 +17,7 @@ const countSchema = joi
variants: joi.object().pattern(joi.string(), joi.number().min(0)), variants: joi.object().pattern(joi.string(), joi.number().min(0)),
}); });
const clientMetricsSchema = joi export const clientMetricsSchema = joi
.object() .object()
.options({ stripUnknown: true }) .options({ stripUnknown: true })
.keys({ .keys({
@ -34,5 +32,3 @@ const clientMetricsSchema = joi
toggles: joi.object().pattern(/.*/, countSchema), toggles: joi.object().pattern(/.*/, countSchema),
}), }),
}); });
module.exports = { clientMetricsSchema };

View File

@ -1,21 +1,27 @@
'use strict'; import EventEmitter from 'events';
import moment from 'moment';
const moment = require('moment'); import ClientMetricsService, { IClientApp } from './index';
import getLogger from '../../../test/fixtures/no-logger';
const { EventEmitter } = require('events');
const UnleashClientMetrics = require('./index');
const appName = 'appName'; const appName = 'appName';
const instanceId = 'instanceId'; const instanceId = 'instanceId';
const getLogger = require('../../../test/fixtures/no-logger'); const createMetricsService = cms =>
new ClientMetricsService(
{
clientMetricsStore: cms,
strategyStore: null,
featureToggleStore: null,
clientApplicationsStore: null,
clientInstanceStore: null,
eventStore: null,
},
{ getLogger },
);
test('should work without state', () => { test('should work without state', () => {
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
expect(metrics.getAppsWithToggles()).toBeTruthy(); expect(metrics.getAppsWithToggles()).toBeTruthy();
expect(metrics.getTogglesMetrics()).toBeTruthy(); expect(metrics.getTogglesMetrics()).toBeTruthy();
@ -27,10 +33,7 @@ test('data should expire', () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
metrics.addPayload({ metrics.addPayload({
appName, appName,
@ -58,22 +61,20 @@ test('data should expire', () => {
}); });
jest.advanceTimersByTime(60 * 1000); jest.advanceTimersByTime(60 * 1000);
expect(lastMinExpires === 1).toBe(true); expect(lastMinExpires).toBe(1);
expect(lastHourExpires === 0).toBe(true); expect(lastHourExpires).toBe(0);
jest.advanceTimersByTime(60 * 60 * 1000); jest.advanceTimersByTime(60 * 60 * 1000);
expect(lastMinExpires === 1).toBe(true); expect(lastMinExpires).toBe(1);
expect(lastHourExpires === 1).toBe(true); expect(lastHourExpires).toBe(1);
jest.useRealTimers(); jest.useRealTimers();
metrics.destroy();
}); });
test('should listen to metrics from store', () => { test('should listen to metrics from store', () => {
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
clientMetricsStore.emit('metrics', { clientMetricsStore.emit('metrics', {
appName, appName,
instanceId, instanceId,
@ -89,8 +90,8 @@ test('should listen to metrics from store', () => {
}, },
}); });
expect(metrics.apps[appName].count === 123).toBeTruthy(); expect(metrics.apps[appName].count).toBe(123);
expect(metrics.globalCount === 123).toBeTruthy(); expect(metrics.globalCount).toBe(123);
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({ expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
yes: 123, yes: 123,
@ -116,7 +117,7 @@ test('should listen to metrics from store', () => {
}, },
}); });
expect(metrics.globalCount === 143).toBeTruthy(); expect(metrics.globalCount).toBe(143);
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({ expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
yes: 133, yes: 133,
no: 10, no: 10,
@ -131,10 +132,7 @@ test('should listen to metrics from store', () => {
test('should build up list of seen toggles when new metrics arrives', () => { test('should build up list of seen toggles when new metrics arrives', () => {
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
clientMetricsStore.emit('metrics', { clientMetricsStore.emit('metrics', {
appName, appName,
instanceId, instanceId,
@ -157,12 +155,12 @@ test('should build up list of seen toggles when new metrics arrives', () => {
const appToggles = metrics.getAppsWithToggles(); const appToggles = metrics.getAppsWithToggles();
const togglesForApp = metrics.getSeenTogglesByAppName(appName); const togglesForApp = metrics.getSeenTogglesByAppName(appName);
expect(appToggles).toHaveLength(1); expect(appToggles.length).toBe(1);
expect(appToggles[0].seenToggles).toHaveLength(2); expect(appToggles[0].seenToggles.length).toBe(2);
expect(appToggles[0].seenToggles).toContain('toggleX'); expect(appToggles[0].seenToggles).toContain('toggleX');
expect(appToggles[0].seenToggles).toContain('toggleY'); expect(appToggles[0].seenToggles).toContain('toggleY');
expect(togglesForApp).toHaveLength(2); expect(togglesForApp.length === 2);
expect(togglesForApp).toContain('toggleX'); expect(togglesForApp).toContain('toggleX');
expect(togglesForApp).toContain('toggleY'); expect(togglesForApp).toContain('toggleY');
metrics.destroy(); metrics.destroy();
@ -170,10 +168,7 @@ test('should build up list of seen toggles when new metrics arrives', () => {
test('should handle a lot of toggles', () => { test('should handle a lot of toggles', () => {
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
const toggleCounts = {}; const toggleCounts = {};
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
@ -192,7 +187,7 @@ test('should handle a lot of toggles', () => {
const seenToggles = metrics.getSeenTogglesByAppName(appName); const seenToggles = metrics.getSeenTogglesByAppName(appName);
expect(seenToggles).toHaveLength(100); expect(seenToggles.length).toBe(100);
metrics.destroy(); metrics.destroy();
}); });
@ -200,10 +195,7 @@ test('should have correct values for lastMinute', () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
const now = new Date(); const now = new Date();
const input = [ const input = [
@ -253,7 +245,7 @@ test('should have correct values for lastMinute', () => {
}); });
const seenToggles = metrics.getSeenTogglesByAppName(appName); const seenToggles = metrics.getSeenTogglesByAppName(appName);
expect(seenToggles.length === 1).toBeTruthy(); expect(seenToggles.length).toBe(1);
// metrics.se // metrics.se
let c = metrics.getTogglesMetrics(); let c = metrics.getTogglesMetrics();
@ -275,10 +267,7 @@ test('should have correct values for lastHour', () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
const now = new Date(); const now = new Date();
const input = [ const input = [
@ -322,7 +311,7 @@ test('should have correct values for lastHour', () => {
const seenToggles = metrics.getSeenTogglesByAppName(appName); const seenToggles = metrics.getSeenTogglesByAppName(appName);
expect(seenToggles.length === 1).toBeTruthy(); expect(seenToggles.length).toBe(1);
// metrics.se // metrics.se
let c = metrics.getTogglesMetrics(); let c = metrics.getTogglesMetrics();
@ -358,10 +347,7 @@ test('should have correct values for lastHour', () => {
test('should not fail when toggle metrics is missing yes/no field', () => { test('should not fail when toggle metrics is missing yes/no field', () => {
const clientMetricsStore = new EventEmitter(); const clientMetricsStore = new EventEmitter();
const metrics = new UnleashClientMetrics( const metrics = createMetricsService(clientMetricsStore);
{ clientMetricsStore },
{ getLogger },
);
clientMetricsStore.emit('metrics', { clientMetricsStore.emit('metrics', {
appName, appName,
instanceId, instanceId,
@ -403,20 +389,27 @@ test('should not fail when toggle metrics is missing yes/no field', () => {
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => { test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore: any = new EventEmitter();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
const clientInstanceStore = { const clientInstanceStore: any = {
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
const clientMetrics = new UnleashClientMetrics( const clientMetrics = new ClientMetricsService(
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore }, {
clientMetricsStore,
strategyStore: null,
featureToggleStore: null,
clientApplicationsStore,
clientInstanceStore,
eventStore: null,
},
{ getLogger }, { getLogger },
); );
const client1 = { const client1: IClientApp = {
appName: 'test_app', appName: 'test_app',
instanceId: 'ava', instanceId: 'ava',
strategies: [{ name: 'defaullt' }], strategies: [{ name: 'defaullt' }],
@ -427,30 +420,42 @@ test('Multiple registrations of same appname and instanceid within same time per
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(7 * 1000); await jest.advanceTimersByTime(7 * 1000);
expect(appStoreSpy).toHaveBeenCalledTimes(1); expect(appStoreSpy).toHaveBeenCalledTimes(1);
expect(bulkSpy).toHaveBeenCalledTimes(1);
const registrations = appStoreSpy.mock.calls[0][0]; const registrations = appStoreSpy.mock.calls[0][0];
expect(registrations).toHaveLength(1);
expect(registrations.length).toBe(1);
expect(registrations[0].appName).toBe(client1.appName); expect(registrations[0].appName).toBe(client1.appName);
expect(registrations[0].instanceId).toBe(client1.instanceId); expect(registrations[0].instanceId).toBe(client1.instanceId);
expect(registrations[0].started).toBe(client1.started); expect(registrations[0].started).toBe(client1.started);
expect(registrations[0].interval).toBe(client1.interval); expect(registrations[0].interval).toBe(client1.interval);
jest.useRealTimers(); jest.useRealTimers();
}); });
test('Multiple unique clients causes multiple registrations', async () => { test('Multiple unique clients causes multiple registrations', async () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore: any = new EventEmitter();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
const clientInstanceStore = { const clientInstanceStore: any = {
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
const clientMetrics = new UnleashClientMetrics( const clientMetrics = new ClientMetricsService(
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore }, {
clientMetricsStore,
strategyStore: null,
featureToggleStore: null,
clientApplicationsStore,
clientInstanceStore,
eventStore: null,
},
{ getLogger }, { getLogger },
); );
const client1 = { const client1 = {
@ -473,70 +478,92 @@ test('Multiple unique clients causes multiple registrations', async () => {
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1');
jest.advanceTimersByTime(7 * 1000); await jest.advanceTimersByTime(7 * 1000);
expect(appStoreSpy).toHaveBeenCalledTimes(1); expect(appStoreSpy).toHaveBeenCalledTimes(1);
expect(bulkSpy).toHaveBeenCalledTimes(1);
const registrations = appStoreSpy.mock.calls[0][0]; const registrations = appStoreSpy.mock.calls[0][0];
expect(registrations).toHaveLength(2);
expect(registrations.length).toBe(2);
jest.useRealTimers(); jest.useRealTimers();
}); });
test('Same client registered outside of dedup interval will be registered twice', async () => { test('Same client registered outside of dedup interval will be registered twice', async () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore: any = new EventEmitter();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
const clientInstanceStore = { const clientInstanceStore: any = {
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
const bulkInterval = 2000; const bulkInterval = 2000;
const clientMetrics = new UnleashClientMetrics(
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore }, const clientMetrics = new ClientMetricsService(
{ getLogger, bulkInterval }, {
clientMetricsStore,
strategyStore: null,
featureToggleStore: null,
clientApplicationsStore,
clientInstanceStore,
eventStore: null,
},
{ getLogger },
bulkInterval,
); );
const client1 = { const client1 = {
appName: 'test_app', appName: 'test_app',
instanceId: 'client1', instanceId: 'client1',
strategies: [{ name: 'default' }], strategies: [{ name: 'defaullt' }],
started: new Date(), started: new Date(),
interval: 10, interval: 10,
}; };
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(3 * 1000); await jest.advanceTimersByTime(3 * 1000);
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1');
jest.advanceTimersByTime(3 * 1000); await jest.advanceTimersByTime(3 * 1000);
expect(appStoreSpy).toHaveBeenCalledTimes(2); expect(appStoreSpy).toHaveBeenCalledTimes(2);
const firstRegistrations = appStoreSpy.mock.calls[0][0]; expect(bulkSpy).toHaveBeenCalledTimes(2);
const secondRegistrations = appStoreSpy.mock.calls[1][0];
expect(firstRegistrations[0].appName).toBe(secondRegistrations[0].appName); const firstRegistrations = appStoreSpy.mock.calls[0][0][0];
expect(firstRegistrations[0].instanceId).toBe( const secondRegistrations = appStoreSpy.mock.calls[1][0][0];
secondRegistrations[0].instanceId,
); expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId);
jest.useRealTimers(); jest.useRealTimers();
}); });
test('No registrations during a time period will not call stores', async () => { test('No registrations during a time period will not call stores', async () => {
jest.useFakeTimers('modern'); jest.useFakeTimers('modern');
const clientMetricsStore = new EventEmitter(); const clientMetricsStore: any = new EventEmitter();
const appStoreSpy = jest.fn(); const appStoreSpy = jest.fn();
const bulkSpy = jest.fn(); const bulkSpy = jest.fn();
const clientApplicationsStore = { const clientApplicationsStore: any = {
bulkUpsert: appStoreSpy, bulkUpsert: appStoreSpy,
}; };
const clientInstanceStore = { const clientInstanceStore: any = {
bulkUpsert: bulkSpy, bulkUpsert: bulkSpy,
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const metrics = new UnleashClientMetrics( const clientMetrics = new ClientMetricsService(
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore }, {
clientMetricsStore,
strategyStore: null,
featureToggleStore: null,
clientApplicationsStore,
clientInstanceStore,
eventStore: null,
},
{ getLogger }, { getLogger },
); );
jest.advanceTimersByTime(6 * 1000); await jest.advanceTimersByTime(6 * 1000);
expect(appStoreSpy).toHaveBeenCalledTimes(0); expect(appStoreSpy).toHaveBeenCalledTimes(0);
expect(bulkSpy).toHaveBeenCalledTimes(0); expect(bulkSpy).toHaveBeenCalledTimes(0);
jest.useRealTimers(); jest.useRealTimers();

View File

@ -1,54 +1,148 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import EventStore, { ICreateEvent } from '../../db/event-store';
import StrategyStore from '../../db/strategy-store';
import ClientApplicationsDb from '../../db/client-applications-store';
import ClientInstanceStore from '../../db/client-instance-store';
import { ClientMetricsStore } from '../../db/client-metrics-store';
import FeatureToggleStore from '../../db/feature-toggle-store';
import { LogProvider } from '../../logger';
import { applicationSchema } from './metrics-schema';
import { Projection } from './projection';
import { clientMetricsSchema } from './client-metrics-schema';
import { APPLICATION_CREATED } from '../../types/events';
import { IApplication, IYesNoCount } from './models';
import { IUnleashStores } from '../../types/stores';
import { IUnleashConfig } from '../../types/option';
'use strict';
const Projection = require('./projection');
const TTLList = require('./ttl-list'); const TTLList = require('./ttl-list');
const appSchema = require('./metrics-schema');
const { clientMetricsSchema } = require('./client-metrics-schema');
const { clientRegisterSchema } = require('./register-schema'); const { clientRegisterSchema } = require('./register-schema');
const { APPLICATION_CREATED } = require('../../types/events');
const FIVE_SECONDS = 5 * 1000; const FIVE_SECONDS = 5 * 1000;
const FIVE_MINUTES = 5 * 60 * 1000; const FIVE_MINUTES = 5 * 60 * 1000;
module.exports = class ClientMetricsService { export interface IClientApp {
appName: string;
instanceId: string;
clientIp?: string;
seenToggles?: string[];
metricsCount?: number;
strategies?: string[] | Record<string, string>[];
bucket?: any;
count?: number;
started?: number | Date;
interval?: number;
icon?: string;
description?: string;
color?: string;
}
export interface IAppFeature {
name: string;
description: string;
type: string;
project: string;
enabled: boolean;
stale: boolean;
strategies: any;
variants: any[];
createdAt: Date;
lastSeenAt: Date;
}
export interface IApplicationQuery {
strategyName?: string;
}
export interface IAppName {
appName: string;
}
export interface IMetricCounts {
yes?: number;
no?: number;
variants?: Record<string, number>;
}
export interface IMetricsBucket {
start: Date;
stop: Date;
toggles: IMetricCounts;
}
export default class ClientMetricsService {
globalCount = 0;
apps = {};
lastHourProjection = new Projection();
lastMinuteProjection = new Projection();
lastHourList = new TTLList({
interval: 10000,
});
logger = null;
lastMinuteList = new TTLList({
interval: 10000,
expireType: 'minutes',
expireAmount: 1,
});
seenClients: Record<string, IClientApp> = {};
private timers: NodeJS.Timeout[] = [];
private clientMetricsStore: ClientMetricsStore;
private strategyStore: StrategyStore;
private featureToggleStore: FeatureToggleStore;
private clientApplicationsStore: ClientApplicationsDb;
private clientInstanceStore: ClientInstanceStore;
private eventStore: EventStore;
private getLogger: LogProvider;
private bulkInterval: number;
private announcementInterval: number;
constructor( constructor(
{ {
clientMetricsStore, clientMetricsStore,
strategyStore, strategyStore,
featureToggleStore, featureToggleStore,
clientApplicationsStore,
clientInstanceStore, clientInstanceStore,
clientApplicationsStore,
eventStore, eventStore,
}, }: Pick<IUnleashStores,
{ | 'clientMetricsStore'
getLogger, | 'strategyStore'
bulkInterval = FIVE_SECONDS, | 'featureToggleStore'
announcementInterval: appAnnouncementInterval = FIVE_MINUTES, | 'clientApplicationsStore'
}, | 'clientInstanceStore'
| 'eventStore'
>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
bulkInterval = FIVE_SECONDS,
announcementInterval = FIVE_MINUTES,
) { ) {
this.globalCount = 0;
this.apps = {};
this.strategyStore = strategyStore;
this.toggleStore = featureToggleStore;
this.clientAppStore = clientApplicationsStore;
this.clientInstanceStore = clientInstanceStore;
this.clientMetricsStore = clientMetricsStore; this.clientMetricsStore = clientMetricsStore;
this.lastHourProjection = new Projection(); this.strategyStore = strategyStore;
this.lastMinuteProjection = new Projection(); this.featureToggleStore = featureToggleStore;
this.clientApplicationsStore = clientApplicationsStore;
this.clientInstanceStore = clientInstanceStore;
this.eventStore = eventStore; this.eventStore = eventStore;
this.lastHourList = new TTLList({ this.logger = getLogger('/services/client-metrics/index.ts');
interval: 10000,
});
this.logger = getLogger('services/client-metrics/index.ts');
this.lastMinuteList = new TTLList({ this.bulkInterval = bulkInterval;
interval: 10000, this.announcementInterval = announcementInterval;
expireType: 'minutes',
expireAmount: 1,
});
this.lastHourList.on('expire', toggles => { this.lastHourList.on('expire', toggles => {
Object.keys(toggles).forEach(toggleName => { Object.keys(toggles).forEach(toggleName => {
@ -66,21 +160,26 @@ module.exports = class ClientMetricsService {
); );
}); });
}); });
this.seenClients = {};
this.bulkAddTimer = setInterval(() => this.bulkAdd(), bulkInterval); this.timers.push(
this.bulkAddTimer.unref(); setInterval(() => this.bulkAdd(), this.bulkInterval).unref(),
this.announceTimer = setInterval( );
() => this.announceUnannounced(), this.timers.push(
appAnnouncementInterval, setInterval(
() => this.announceUnannounced(),
this.announcementInterval,
).unref(),
); );
this.announceTimer.unref();
clientMetricsStore.on('metrics', m => this.addPayload(m)); clientMetricsStore.on('metrics', m => this.addPayload(m));
} }
async registerClientMetrics(data, clientIp) { async registerClientMetrics(
data: IClientApp,
clientIp: string,
): Promise<void> {
const value = await clientMetricsSchema.validateAsync(data); const value = await clientMetricsSchema.validateAsync(data);
const toggleNames = Object.keys(value.bucket.toggles); const toggleNames = Object.keys(value.bucket.toggles);
await this.toggleStore.lastSeenToggles(toggleNames); await this.featureToggleStore.lastSeenToggles(toggleNames);
await this.clientMetricsStore.insert(value); await this.clientMetricsStore.insert(value);
await this.clientInstanceStore.insert({ await this.clientInstanceStore.insert({
appName: value.appName, appName: value.appName,
@ -89,40 +188,36 @@ module.exports = class ClientMetricsService {
}); });
} }
async announceUnannounced() { async announceUnannounced(): Promise<void> {
if (this.clientAppStore) { if (this.clientApplicationsStore) {
try { const appsToAnnounce = await this.clientApplicationsStore.setUnannouncedToAnnounced();
const appsToAnnounce = await this.clientAppStore.setUnannouncedToAnnounced(); if (appsToAnnounce.length > 0) {
if (appsToAnnounce.length > 0) { const events = appsToAnnounce.map(app => ({
const events = appsToAnnounce.map(app => ({ type: APPLICATION_CREATED,
type: APPLICATION_CREATED, createdBy: app.createdBy || 'unknown',
createdBy: app.createdBy || 'unknown', data: app,
data: app, }));
})); await this.eventStore.batchStore(events);
await this.eventStore.batchStore(events);
}
} catch (e) {
this.logger.warn(e);
} }
} }
} }
async registerClient(data, clientIp) { async registerClient(data: IClientApp, clientIp: string): Promise<void> {
const value = await clientRegisterSchema.validateAsync(data); const value = await clientRegisterSchema.validateAsync(data);
value.clientIp = clientIp; value.clientIp = clientIp;
value.createdBy = clientIp; value.createdBy = clientIp;
this.seenClients[this.clientKey(value)] = value; this.seenClients[this.clientKey(value)] = value;
} }
clientKey(client) { clientKey(client: IClientApp): string {
return `${client.appName}_${client.instanceId}`; return `${client.appName}_${client.instanceId}`;
} }
async bulkAdd() { async bulkAdd(): Promise<void> {
if ( if (
this && this &&
this.seenClients && this.seenClients &&
this.clientAppStore && this.clientApplicationsStore &&
this.clientInstanceStore this.clientInstanceStore
) { ) {
const uniqueRegistrations = Object.values(this.seenClients); const uniqueRegistrations = Object.values(this.seenClients);
@ -135,7 +230,7 @@ module.exports = class ClientMetricsService {
this.seenClients = {}; this.seenClients = {};
try { try {
if (uniqueRegistrations.length > 0) { if (uniqueRegistrations.length > 0) {
await this.clientAppStore.bulkUpsert(uniqueApps); await this.clientApplicationsStore.bulkUpsert(uniqueApps);
await this.clientInstanceStore.bulkUpsert( await this.clientInstanceStore.bulkUpsert(
uniqueRegistrations, uniqueRegistrations,
); );
@ -146,7 +241,7 @@ module.exports = class ClientMetricsService {
} }
} }
appToEvent(app) { appToEvent(app: IClientApp): ICreateEvent {
return { return {
type: APPLICATION_CREATED, type: APPLICATION_CREATED,
createdBy: app.clientIp, createdBy: app.clientIp,
@ -154,7 +249,7 @@ module.exports = class ClientMetricsService {
}; };
} }
getAppsWithToggles() { getAppsWithToggles(): IClientApp[] {
const apps = []; const apps = [];
Object.keys(this.apps).forEach(appName => { Object.keys(this.apps).forEach(appName => {
const seenToggles = Object.keys(this.apps[appName].seenToggles); const seenToggles = Object.keys(this.apps[appName].seenToggles);
@ -164,15 +259,15 @@ module.exports = class ClientMetricsService {
return apps; return apps;
} }
getSeenTogglesByAppName(appName) { getSeenTogglesByAppName(appName: string): string[] {
return this.apps[appName] return this.apps[appName]
? Object.keys(this.apps[appName].seenToggles) ? Object.keys(this.apps[appName].seenToggles)
: []; : [];
} }
async getSeenApps() { async getSeenApps(): Promise<Record<string, IApplication[]>> {
const seenApps = this.getSeenAppsPerToggle(); const seenApps = this.getSeenAppsPerToggle();
const applications = await this.clientAppStore.getApplications(); const applications: IApplication[] = await this.clientApplicationsStore.getApplications();
const metaData = applications.reduce((result, entry) => { const metaData = applications.reduce((result, entry) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
result[entry.appName] = entry; result[entry.appName] = entry;
@ -190,11 +285,13 @@ module.exports = class ClientMetricsService {
return seenApps; return seenApps;
} }
async getApplications(query) { async getApplications(
return this.clientAppStore.getApplications(query); query: IApplicationQuery,
): Promise<Record<string, IApplication>> {
return this.clientApplicationsStore.getApplications(query);
} }
async getApplication(appName) { async getApplication(appName: string): Promise<IApplication> {
const seenToggles = this.getSeenTogglesByAppName(appName); const seenToggles = this.getSeenTogglesByAppName(appName);
const [ const [
application, application,
@ -202,10 +299,10 @@ module.exports = class ClientMetricsService {
strategies, strategies,
features, features,
] = await Promise.all([ ] = await Promise.all([
this.clientAppStore.getApplication(appName), this.clientApplicationsStore.getApplication(appName),
this.clientInstanceStore.getByAppName(appName), this.clientInstanceStore.getByAppName(appName),
this.strategyStore.getStrategies(), this.strategyStore.getStrategies(),
this.toggleStore.getFeatures(), this.featureToggleStore.getFeatures(),
]); ]);
return { return {
@ -230,7 +327,7 @@ module.exports = class ClientMetricsService {
}; };
} }
getSeenAppsPerToggle() { getSeenAppsPerToggle(): Record<string, IApplication[]> {
const toggles = {}; const toggles = {};
Object.keys(this.apps).forEach(appName => { Object.keys(this.apps).forEach(appName => {
Object.keys(this.apps[appName].seenToggles).forEach( Object.keys(this.apps[appName].seenToggles).forEach(
@ -245,20 +342,20 @@ module.exports = class ClientMetricsService {
return toggles; return toggles;
} }
getTogglesMetrics() { getTogglesMetrics(): Record<string, Record<string, IYesNoCount>> {
return { return {
lastHour: this.lastHourProjection.getProjection(), lastHour: this.lastHourProjection.getProjection(),
lastMinute: this.lastMinuteProjection.getProjection(), lastMinute: this.lastMinuteProjection.getProjection(),
}; };
} }
addPayload(data) { addPayload(data: IClientApp): void {
const { appName, bucket } = data; const { appName, bucket } = data;
const app = this.getApp(appName); const app = this.getApp(appName);
this.addBucket(app, bucket); this.addBucket(app, bucket);
} }
getApp(appName) { getApp(appName: string): IClientApp {
this.apps[appName] = this.apps[appName] || { this.apps[appName] = this.apps[appName] || {
seenToggles: {}, seenToggles: {},
count: 0, count: 0,
@ -266,7 +363,7 @@ module.exports = class ClientMetricsService {
return this.apps[appName]; return this.apps[appName];
} }
createCountObject(entry) { createCountObject(entry: IMetricCounts): IYesNoCount {
let yes = typeof entry.yes === 'number' ? entry.yes : 0; let yes = typeof entry.yes === 'number' ? entry.yes : 0;
let no = typeof entry.no === 'number' ? entry.no : 0; let no = typeof entry.no === 'number' ? entry.no : 0;
@ -283,7 +380,7 @@ module.exports = class ClientMetricsService {
return { yes, no }; return { yes, no };
} }
addBucket(app, bucket) { addBucket(app: IClientApp, bucket: IMetricsBucket): void {
let count = 0; let count = 0;
// TODO stop should be createdAt // TODO stop should be createdAt
const { stop, toggles } = bucket; const { stop, toggles } = bucket;
@ -305,26 +402,25 @@ module.exports = class ClientMetricsService {
this.addSeenToggles(app, toggleNames); this.addSeenToggles(app, toggleNames);
} }
addSeenToggles(app, toggleNames) { addSeenToggles(app: IClientApp, toggleNames: string[]): void {
toggleNames.forEach(t => { toggleNames.forEach(t => {
app.seenToggles[t] = true; app.seenToggles[t] = true;
}); });
} }
async deleteApplication(appName) { async deleteApplication(appName: string): Promise<void> {
await this.clientInstanceStore.deleteForApplication(appName); await this.clientInstanceStore.deleteForApplication(appName);
await this.clientAppStore.deleteApplication(appName); await this.clientApplicationsStore.deleteApplication(appName);
} }
async createApplication(input) { async createApplication(input: IApplication): Promise<void> {
const applicationData = await appSchema.validateAsync(input); const applicationData = await applicationSchema.validateAsync(input);
await this.clientAppStore.upsert(applicationData); await this.clientApplicationsStore.upsert(applicationData);
} }
destroy() { destroy(): void {
this.lastHourList.destroy(); this.lastHourList.destroy();
this.lastMinuteList.destroy(); this.lastMinuteList.destroy();
clearInterval(this.announceTimer); this.timers.forEach(clearInterval);
clearInterval(this.bulkAddTimer);
} }
}; }

View File

@ -1,8 +1,6 @@
'use strict'; import joi from 'joi';
const joi = require('joi'); export const applicationSchema = joi
const applicationSchema = joi
.object() .object()
.options({ stripUnknown: false }) .options({ stripUnknown: false })
.keys({ .keys({
@ -29,5 +27,3 @@ const applicationSchema = joi
.allow('') .allow('')
.optional(), .optional(),
}); });
module.exports = applicationSchema;

View File

@ -0,0 +1,27 @@
export interface IYesNoCount {
yes: number;
no: number;
}
export interface IAppInstance {
appName: string;
instanceId: string;
sdkVersion: string;
clientIp: string;
lastSeen: Date;
createdAt: Date;
}
export interface IApplication {
appName: string;
sdkVersion?: string;
strategies?: string[] | any[];
description?: string;
url?: string;
color?: string;
icon?: string;
createdAt: Date;
instances?: IAppInstance;
seenToggles: Record<string, any>;
links: Record<string, string>;
}

View File

@ -1,6 +1,5 @@
'use strict'; import { Projection } from './projection';
const Projection = require('./projection');
test('should return set empty if missing', () => { test('should return set empty if missing', () => {
const projection = new Projection(); const projection = new Projection();

View File

@ -1,15 +1,13 @@
'use strict'; import { IYesNoCount } from './models';
module.exports = class Projection { export class Projection {
constructor() { store: Record<string, IYesNoCount> = {};
this.store = {};
}
getProjection() { getProjection(): Record<string, IYesNoCount> {
return this.store; return this.store;
} }
add(name, countObj) { add(name: string, countObj: IYesNoCount): void {
if (this.store[name]) { if (this.store[name]) {
this.store[name].yes += countObj.yes; this.store[name].yes += countObj.yes;
this.store[name].no += countObj.no; this.store[name].no += countObj.no;
@ -21,7 +19,7 @@ module.exports = class Projection {
} }
} }
substract(name, countObj) { substract(name: string, countObj: IYesNoCount): void {
if (this.store[name]) { if (this.store[name]) {
this.store[name].yes -= countObj.yes; this.store[name].yes -= countObj.yes;
this.store[name].no -= countObj.no; this.store[name].no -= countObj.no;
@ -32,4 +30,4 @@ module.exports = class Projection {
}; };
} }
} }
}; }

View File

@ -21,7 +21,6 @@ import { IUnleashStores } from '../types/stores';
import PasswordUndefinedError from '../error/password-undefined'; import PasswordUndefinedError from '../error/password-undefined';
import EventStore from '../db/event-store'; import EventStore from '../db/event-store';
import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events'; import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
import { IRole } from '../db/access-store';
const systemUser = new User({ id: -1, username: 'system' }); const systemUser = new User({ id: -1, username: 'system' });

View File

@ -5,7 +5,7 @@ import FeatureTypeStore from '../db/feature-type-store';
import StrategyStore from '../db/strategy-store'; import StrategyStore from '../db/strategy-store';
import ClientApplicationsDb from '../db/client-applications-store'; import ClientApplicationsDb from '../db/client-applications-store';
import ClientInstanceStore from '../db/client-instance-store'; import ClientInstanceStore from '../db/client-instance-store';
import ClientMetricsStore from '../db/client-metrics-store'; import { ClientMetricsStore } from '../db/client-metrics-store';
import FeatureToggleStore from '../db/feature-toggle-store'; import FeatureToggleStore from '../db/feature-toggle-store';
import ContextFieldStore from '../db/context-field-store'; import ContextFieldStore from '../db/context-field-store';
import SettingStore from '../db/setting-store'; import SettingStore from '../db/setting-store';

View File

@ -1,7 +1,10 @@
import ClientMetricsService, {
IClientApp,
} from '../../../lib/services/client-metrics';
const faker = require('faker'); const faker = require('faker');
const dbInit = require('../helpers/database-init'); const dbInit = require('../helpers/database-init');
const getLogger = require('../../fixtures/no-logger'); const getLogger = require('../../fixtures/no-logger');
const ClientMetricsService = require('../../../lib/services/client-metrics');
const { APPLICATION_CREATED } = require('../../../lib/types/events'); const { APPLICATION_CREATED } = require('../../../lib/types/events');
let stores; let stores;
@ -11,11 +14,12 @@ let clientMetricsService;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('client_metrics_service_serial', getLogger); db = await dbInit('client_metrics_service_serial', getLogger);
stores = db.stores; stores = db.stores;
clientMetricsService = new ClientMetricsService(stores, { clientMetricsService = new ClientMetricsService(
getLogger, stores,
bulkInterval: 500, { getLogger },
announcementInterval: 2000, 500,
}); 2000,
);
}); });
afterAll(async () => { afterAll(async () => {
@ -24,7 +28,7 @@ afterAll(async () => {
}); });
test('Apps registered should be announced', async () => { test('Apps registered should be announced', async () => {
expect.assertions(3); expect.assertions(3);
const clientRegistration = { const clientRegistration: IClientApp = {
appName: faker.internet.domainName(), appName: faker.internet.domainName(),
instanceId: faker.datatype.uuid(), instanceId: faker.datatype.uuid(),
strategies: ['default'], strategies: ['default'],