mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
chore: Convert client metrics controller to typescript (#831)
Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
This commit is contained in:
parent
00f2c7312d
commit
2f013bacbf
@ -1,4 +1,5 @@
|
||||
'use strict';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
||||
const TABLE = 'client_metrics';
|
||||
@ -11,18 +12,27 @@ const mapRow = row => ({
|
||||
metrics: row.metrics,
|
||||
});
|
||||
|
||||
class ClientMetricsDb {
|
||||
constructor(db, getLogger) {
|
||||
this.db = db;
|
||||
export interface IClientMetric {
|
||||
id: number;
|
||||
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');
|
||||
|
||||
// Clear old metrics regulary
|
||||
// Clear old metrics regularly
|
||||
const clearer = () => this.removeMetricsOlderThanOneHour();
|
||||
setTimeout(clearer, 10).unref();
|
||||
this.timer = setInterval(clearer, ONE_MINUTE).unref();
|
||||
}
|
||||
|
||||
async removeMetricsOlderThanOneHour() {
|
||||
async removeMetricsOlderThanOneHour(): Promise<void> {
|
||||
try {
|
||||
const rows = await this.db(TABLE)
|
||||
.whereRaw("created_at < now() - interval '1 hour'")
|
||||
@ -36,12 +46,12 @@ class ClientMetricsDb {
|
||||
}
|
||||
|
||||
// Insert new client metrics
|
||||
async insert(metrics) {
|
||||
async insert(metrics: IClientMetric): Promise<void> {
|
||||
return this.db(TABLE).insert({ metrics });
|
||||
}
|
||||
|
||||
// Used at startup to load all metrics last week into memory!
|
||||
async getMetricsLastHour() {
|
||||
async getMetricsLastHour(): Promise<IClientMetric[]> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.select(METRICS_COLUMNS)
|
||||
@ -57,7 +67,7 @@ class ClientMetricsDb {
|
||||
}
|
||||
|
||||
// Used to poll for new metrics
|
||||
async getNewMetrics(lastKnownId) {
|
||||
async getNewMetrics(lastKnownId: number): Promise<IClientMetric[]> {
|
||||
try {
|
||||
const res = await this.db
|
||||
.select(METRICS_COLUMNS)
|
||||
@ -72,9 +82,7 @@ class ClientMetricsDb {
|
||||
return [];
|
||||
}
|
||||
|
||||
destroy() {
|
||||
destroy(): void {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientMetricsDb;
|
@ -1,8 +1,7 @@
|
||||
'use strict';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const ClientMetricStore = require('./client-metrics-store');
|
||||
const getLogger = require('../../test/fixtures/no-logger');
|
||||
import { ClientMetricsStore } from './client-metrics-store';
|
||||
import getLogger from '../../test/fixtures/no-logger';
|
||||
|
||||
function getMockDb() {
|
||||
const list = [
|
||||
@ -28,7 +27,7 @@ test('should call database on startup', done => {
|
||||
jest.useFakeTimers('modern');
|
||||
const mock = getMockDb();
|
||||
const ee = new EventEmitter();
|
||||
const store = new ClientMetricStore(mock, ee, getLogger);
|
||||
const store = new ClientMetricsStore(mock as any, ee, getLogger);
|
||||
|
||||
jest.runAllTicks();
|
||||
|
||||
@ -49,7 +48,7 @@ test('should start poller even if initial database fetch fails', done => {
|
||||
const mock = getMockDb();
|
||||
mock.getMetricsLastHour = () => Promise.reject(new Error('oops'));
|
||||
const ee = new EventEmitter();
|
||||
const store = new ClientMetricStore(mock, ee, getLogger, 100);
|
||||
const store = new ClientMetricsStore(mock as any, ee, getLogger, 100);
|
||||
jest.runAllTicks();
|
||||
|
||||
const metrics = [];
|
||||
@ -74,7 +73,7 @@ test('should poll for updates', done => {
|
||||
jest.useFakeTimers('modern');
|
||||
const mock = getMockDb();
|
||||
const ee = new EventEmitter();
|
||||
const store = new ClientMetricStore(mock, ee, getLogger, 100);
|
||||
const store = new ClientMetricsStore(mock as any, ee, getLogger, 100);
|
||||
jest.runAllTicks();
|
||||
|
||||
const metrics = [];
|
@ -1,17 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const metricsHelper = require('../util/metrics-helper');
|
||||
const { DB_TIME } = require('../metric-events');
|
||||
import EventEmitter from 'events';
|
||||
import { ClientMetricsDb, IClientMetric } from './client-metrics-db';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import metricsHelper from '../util/metrics-helper';
|
||||
import { DB_TIME } from '../metric-events';
|
||||
|
||||
const TEN_SECONDS = 10 * 1000;
|
||||
|
||||
class ClientMetricsStore extends EventEmitter {
|
||||
constructor(metricsDb, eventBus, getLogger, pollInterval = TEN_SECONDS) {
|
||||
export class ClientMetricsStore extends EventEmitter {
|
||||
private logger: Logger;
|
||||
|
||||
highestIdSeen = 0;
|
||||
|
||||
private startTimer: Function;
|
||||
|
||||
private timer: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private metricsDb: ClientMetricsDb,
|
||||
eventBus: EventEmitter,
|
||||
getLogger: LogProvider,
|
||||
pollInterval = TEN_SECONDS,
|
||||
) {
|
||||
super();
|
||||
this.logger = getLogger('client-metrics-store.js');
|
||||
this.metricsDb = metricsDb;
|
||||
this.eventBus = eventBus;
|
||||
this.highestIdSeen = 0;
|
||||
|
||||
this.startTimer = action =>
|
||||
@ -25,7 +39,7 @@ class ClientMetricsStore extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
async _init(pollInterval) {
|
||||
async _init(pollInterval: number): Promise<void> {
|
||||
try {
|
||||
const metrics = await this.metricsDb.getMetricsLastHour();
|
||||
this._emitMetrics(metrics);
|
||||
@ -36,18 +50,18 @@ class ClientMetricsStore extends EventEmitter {
|
||||
this.emit('ready');
|
||||
}
|
||||
|
||||
_startPoller(pollInterval) {
|
||||
_startPoller(pollInterval: number): void {
|
||||
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
|
||||
this.timer.unref();
|
||||
}
|
||||
|
||||
_fetchNewAndEmit() {
|
||||
_fetchNewAndEmit(): void {
|
||||
this.metricsDb
|
||||
.getNewMetrics(this.highestIdSeen)
|
||||
.then(metrics => this._emitMetrics(metrics));
|
||||
}
|
||||
|
||||
_emitMetrics(metrics) {
|
||||
_emitMetrics(metrics: IClientMetric[]): void {
|
||||
if (metrics && metrics.length > 0) {
|
||||
this.highestIdSeen = metrics[metrics.length - 1].id;
|
||||
metrics.forEach(m => this.emit('metrics', m.metrics));
|
||||
@ -55,7 +69,7 @@ class ClientMetricsStore extends EventEmitter {
|
||||
}
|
||||
|
||||
// Insert new client metrics
|
||||
async insert(metrics) {
|
||||
async insert(metrics: IClientMetric): Promise<void> {
|
||||
const stopTimer = this.startTimer('insert');
|
||||
|
||||
await this.metricsDb.insert(metrics);
|
||||
@ -63,10 +77,8 @@ class ClientMetricsStore extends EventEmitter {
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
destroy(): void {
|
||||
clearInterval(this.timer);
|
||||
this.metricsDb.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientMetricsStore;
|
@ -21,7 +21,7 @@ interface IEventTable {
|
||||
tags: [];
|
||||
}
|
||||
|
||||
interface ICreateEvent {
|
||||
export interface ICreateEvent {
|
||||
type: string;
|
||||
createdBy: string;
|
||||
data?: any;
|
||||
|
@ -11,8 +11,8 @@ import FeatureToggleStore from './feature-toggle-store';
|
||||
import FeatureTypeStore from './feature-type-store';
|
||||
import StrategyStore from './strategy-store';
|
||||
import ClientInstanceStore from './client-instance-store';
|
||||
import ClientMetricsDb from './client-metrics-db';
|
||||
import ClientMetricsStore from './client-metrics-store';
|
||||
import { ClientMetricsDb } from './client-metrics-db';
|
||||
import { ClientMetricsStore } from './client-metrics-store';
|
||||
import ClientApplicationsStore from './client-applications-store';
|
||||
import ContextFieldStore from './context-field-store';
|
||||
import SettingStore from './setting-store';
|
||||
|
@ -106,7 +106,10 @@ class MetricsController extends Controller {
|
||||
|
||||
async getApplications(req: Request, res: Response): Promise<void> {
|
||||
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 });
|
||||
} catch (err) {
|
||||
handleErrors(res, this.logger, err);
|
||||
|
@ -1,6 +1,4 @@
|
||||
'use strict';
|
||||
|
||||
const joi = require('joi');
|
||||
import joi from 'joi';
|
||||
|
||||
const countSchema = joi
|
||||
.object()
|
||||
@ -19,7 +17,7 @@ const countSchema = joi
|
||||
variants: joi.object().pattern(joi.string(), joi.number().min(0)),
|
||||
});
|
||||
|
||||
const clientMetricsSchema = joi
|
||||
export const clientMetricsSchema = joi
|
||||
.object()
|
||||
.options({ stripUnknown: true })
|
||||
.keys({
|
||||
@ -34,5 +32,3 @@ const clientMetricsSchema = joi
|
||||
toggles: joi.object().pattern(/.*/, countSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
module.exports = { clientMetricsSchema };
|
@ -1,21 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const moment = require('moment');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const UnleashClientMetrics = require('./index');
|
||||
import EventEmitter from 'events';
|
||||
import moment from 'moment';
|
||||
import ClientMetricsService, { IClientApp } from './index';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
|
||||
const appName = 'appName';
|
||||
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', () => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
|
||||
expect(metrics.getAppsWithToggles()).toBeTruthy();
|
||||
expect(metrics.getTogglesMetrics()).toBeTruthy();
|
||||
@ -27,10 +33,7 @@ test('data should expire', () => {
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
|
||||
metrics.addPayload({
|
||||
appName,
|
||||
@ -58,22 +61,20 @@ test('data should expire', () => {
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(60 * 1000);
|
||||
expect(lastMinExpires === 1).toBe(true);
|
||||
expect(lastHourExpires === 0).toBe(true);
|
||||
expect(lastMinExpires).toBe(1);
|
||||
expect(lastHourExpires).toBe(0);
|
||||
|
||||
jest.advanceTimersByTime(60 * 60 * 1000);
|
||||
expect(lastMinExpires === 1).toBe(true);
|
||||
expect(lastHourExpires === 1).toBe(true);
|
||||
expect(lastMinExpires).toBe(1);
|
||||
expect(lastHourExpires).toBe(1);
|
||||
|
||||
jest.useRealTimers();
|
||||
metrics.destroy();
|
||||
});
|
||||
|
||||
test('should listen to metrics from store', () => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
clientMetricsStore.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
@ -89,8 +90,8 @@ test('should listen to metrics from store', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(metrics.apps[appName].count === 123).toBeTruthy();
|
||||
expect(metrics.globalCount === 123).toBeTruthy();
|
||||
expect(metrics.apps[appName].count).toBe(123);
|
||||
expect(metrics.globalCount).toBe(123);
|
||||
|
||||
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
||||
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({
|
||||
yes: 133,
|
||||
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', () => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
clientMetricsStore.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
@ -157,12 +155,12 @@ test('should build up list of seen toggles when new metrics arrives', () => {
|
||||
const appToggles = metrics.getAppsWithToggles();
|
||||
const togglesForApp = metrics.getSeenTogglesByAppName(appName);
|
||||
|
||||
expect(appToggles).toHaveLength(1);
|
||||
expect(appToggles[0].seenToggles).toHaveLength(2);
|
||||
expect(appToggles.length).toBe(1);
|
||||
expect(appToggles[0].seenToggles.length).toBe(2);
|
||||
expect(appToggles[0].seenToggles).toContain('toggleX');
|
||||
expect(appToggles[0].seenToggles).toContain('toggleY');
|
||||
|
||||
expect(togglesForApp).toHaveLength(2);
|
||||
expect(togglesForApp.length === 2);
|
||||
expect(togglesForApp).toContain('toggleX');
|
||||
expect(togglesForApp).toContain('toggleY');
|
||||
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', () => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
|
||||
const toggleCounts = {};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
@ -192,7 +187,7 @@ test('should handle a lot of toggles', () => {
|
||||
|
||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||
|
||||
expect(seenToggles).toHaveLength(100);
|
||||
expect(seenToggles.length).toBe(100);
|
||||
metrics.destroy();
|
||||
});
|
||||
|
||||
@ -200,10 +195,7 @@ test('should have correct values for lastMinute', () => {
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
|
||||
const now = new Date();
|
||||
const input = [
|
||||
@ -253,7 +245,7 @@ test('should have correct values for lastMinute', () => {
|
||||
});
|
||||
|
||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||
expect(seenToggles.length === 1).toBeTruthy();
|
||||
expect(seenToggles.length).toBe(1);
|
||||
|
||||
// metrics.se
|
||||
let c = metrics.getTogglesMetrics();
|
||||
@ -275,10 +267,7 @@ test('should have correct values for lastHour', () => {
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
|
||||
const now = new Date();
|
||||
const input = [
|
||||
@ -322,7 +311,7 @@ test('should have correct values for lastHour', () => {
|
||||
|
||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||
|
||||
expect(seenToggles.length === 1).toBeTruthy();
|
||||
expect(seenToggles.length).toBe(1);
|
||||
|
||||
// metrics.se
|
||||
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', () => {
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore },
|
||||
{ getLogger },
|
||||
);
|
||||
const metrics = createMetricsService(clientMetricsStore);
|
||||
clientMetricsStore.emit('metrics', {
|
||||
appName,
|
||||
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 () => {
|
||||
jest.useFakeTimers('modern');
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const clientMetricsStore: any = new EventEmitter();
|
||||
const appStoreSpy = jest.fn();
|
||||
const bulkSpy = jest.fn();
|
||||
const clientApplicationsStore = {
|
||||
const clientApplicationsStore: any = {
|
||||
bulkUpsert: appStoreSpy,
|
||||
};
|
||||
const clientInstanceStore = {
|
||||
const clientInstanceStore: any = {
|
||||
bulkUpsert: bulkSpy,
|
||||
};
|
||||
const clientMetrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
||||
const clientMetrics = new ClientMetricsService(
|
||||
{
|
||||
clientMetricsStore,
|
||||
strategyStore: null,
|
||||
featureToggleStore: null,
|
||||
clientApplicationsStore,
|
||||
clientInstanceStore,
|
||||
eventStore: null,
|
||||
},
|
||||
{ getLogger },
|
||||
);
|
||||
const client1 = {
|
||||
const client1: IClientApp = {
|
||||
appName: 'test_app',
|
||||
instanceId: 'ava',
|
||||
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');
|
||||
jest.advanceTimersByTime(7 * 1000);
|
||||
await jest.advanceTimersByTime(7 * 1000);
|
||||
|
||||
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
||||
expect(bulkSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
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].instanceId).toBe(client1.instanceId);
|
||||
expect(registrations[0].started).toBe(client1.started);
|
||||
expect(registrations[0].interval).toBe(client1.interval);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('Multiple unique clients causes multiple registrations', async () => {
|
||||
jest.useFakeTimers('modern');
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const clientMetricsStore: any = new EventEmitter();
|
||||
const appStoreSpy = jest.fn();
|
||||
const bulkSpy = jest.fn();
|
||||
const clientApplicationsStore = {
|
||||
const clientApplicationsStore: any = {
|
||||
bulkUpsert: appStoreSpy,
|
||||
};
|
||||
const clientInstanceStore = {
|
||||
const clientInstanceStore: any = {
|
||||
bulkUpsert: bulkSpy,
|
||||
};
|
||||
const clientMetrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
||||
const clientMetrics = new ClientMetricsService(
|
||||
{
|
||||
clientMetricsStore,
|
||||
strategyStore: null,
|
||||
featureToggleStore: null,
|
||||
clientApplicationsStore,
|
||||
clientInstanceStore,
|
||||
eventStore: null,
|
||||
},
|
||||
{ getLogger },
|
||||
);
|
||||
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');
|
||||
jest.advanceTimersByTime(7 * 1000);
|
||||
await jest.advanceTimersByTime(7 * 1000);
|
||||
|
||||
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
||||
expect(bulkSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const registrations = appStoreSpy.mock.calls[0][0];
|
||||
expect(registrations).toHaveLength(2);
|
||||
|
||||
expect(registrations.length).toBe(2);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
test('Same client registered outside of dedup interval will be registered twice', async () => {
|
||||
jest.useFakeTimers('modern');
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const clientMetricsStore: any = new EventEmitter();
|
||||
const appStoreSpy = jest.fn();
|
||||
const bulkSpy = jest.fn();
|
||||
const clientApplicationsStore = {
|
||||
const clientApplicationsStore: any = {
|
||||
bulkUpsert: appStoreSpy,
|
||||
};
|
||||
const clientInstanceStore = {
|
||||
const clientInstanceStore: any = {
|
||||
bulkUpsert: bulkSpy,
|
||||
};
|
||||
|
||||
const bulkInterval = 2000;
|
||||
const clientMetrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
||||
{ getLogger, bulkInterval },
|
||||
|
||||
const clientMetrics = new ClientMetricsService(
|
||||
{
|
||||
clientMetricsStore,
|
||||
strategyStore: null,
|
||||
featureToggleStore: null,
|
||||
clientApplicationsStore,
|
||||
clientInstanceStore,
|
||||
eventStore: null,
|
||||
},
|
||||
{ getLogger },
|
||||
bulkInterval,
|
||||
);
|
||||
const client1 = {
|
||||
appName: 'test_app',
|
||||
instanceId: 'client1',
|
||||
strategies: [{ name: 'default' }],
|
||||
strategies: [{ name: 'defaullt' }],
|
||||
started: new Date(),
|
||||
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');
|
||||
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');
|
||||
jest.advanceTimersByTime(3 * 1000);
|
||||
await jest.advanceTimersByTime(3 * 1000);
|
||||
expect(appStoreSpy).toHaveBeenCalledTimes(2);
|
||||
const firstRegistrations = appStoreSpy.mock.calls[0][0];
|
||||
const secondRegistrations = appStoreSpy.mock.calls[1][0];
|
||||
expect(firstRegistrations[0].appName).toBe(secondRegistrations[0].appName);
|
||||
expect(firstRegistrations[0].instanceId).toBe(
|
||||
secondRegistrations[0].instanceId,
|
||||
);
|
||||
expect(bulkSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
const firstRegistrations = appStoreSpy.mock.calls[0][0][0];
|
||||
const secondRegistrations = appStoreSpy.mock.calls[1][0][0];
|
||||
|
||||
expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
|
||||
expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('No registrations during a time period will not call stores', async () => {
|
||||
jest.useFakeTimers('modern');
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const clientMetricsStore: any = new EventEmitter();
|
||||
const appStoreSpy = jest.fn();
|
||||
const bulkSpy = jest.fn();
|
||||
const clientApplicationsStore = {
|
||||
const clientApplicationsStore: any = {
|
||||
bulkUpsert: appStoreSpy,
|
||||
};
|
||||
const clientInstanceStore = {
|
||||
const clientInstanceStore: any = {
|
||||
bulkUpsert: bulkSpy,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const metrics = new UnleashClientMetrics(
|
||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
||||
const clientMetrics = new ClientMetricsService(
|
||||
{
|
||||
clientMetricsStore,
|
||||
strategyStore: null,
|
||||
featureToggleStore: null,
|
||||
clientApplicationsStore,
|
||||
clientInstanceStore,
|
||||
eventStore: null,
|
||||
},
|
||||
{ getLogger },
|
||||
);
|
||||
jest.advanceTimersByTime(6 * 1000);
|
||||
await jest.advanceTimersByTime(6 * 1000);
|
||||
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
||||
expect(bulkSpy).toHaveBeenCalledTimes(0);
|
||||
jest.useRealTimers();
|
@ -1,54 +1,148 @@
|
||||
/* 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 appSchema = require('./metrics-schema');
|
||||
const { clientMetricsSchema } = require('./client-metrics-schema');
|
||||
const { clientRegisterSchema } = require('./register-schema');
|
||||
const { APPLICATION_CREATED } = require('../../types/events');
|
||||
|
||||
const FIVE_SECONDS = 5 * 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(
|
||||
{
|
||||
clientMetricsStore,
|
||||
strategyStore,
|
||||
featureToggleStore,
|
||||
clientApplicationsStore,
|
||||
clientInstanceStore,
|
||||
clientApplicationsStore,
|
||||
eventStore,
|
||||
},
|
||||
{
|
||||
getLogger,
|
||||
bulkInterval = FIVE_SECONDS,
|
||||
announcementInterval: appAnnouncementInterval = FIVE_MINUTES,
|
||||
},
|
||||
}: Pick<IUnleashStores,
|
||||
| 'clientMetricsStore'
|
||||
| 'strategyStore'
|
||||
| 'featureToggleStore'
|
||||
| '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.lastHourProjection = new Projection();
|
||||
this.lastMinuteProjection = new Projection();
|
||||
this.strategyStore = strategyStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.clientApplicationsStore = clientApplicationsStore;
|
||||
this.clientInstanceStore = clientInstanceStore;
|
||||
this.eventStore = eventStore;
|
||||
|
||||
this.lastHourList = new TTLList({
|
||||
interval: 10000,
|
||||
});
|
||||
this.logger = getLogger('services/client-metrics/index.ts');
|
||||
this.logger = getLogger('/services/client-metrics/index.ts');
|
||||
|
||||
this.lastMinuteList = new TTLList({
|
||||
interval: 10000,
|
||||
expireType: 'minutes',
|
||||
expireAmount: 1,
|
||||
});
|
||||
this.bulkInterval = bulkInterval;
|
||||
this.announcementInterval = announcementInterval;
|
||||
|
||||
this.lastHourList.on('expire', toggles => {
|
||||
Object.keys(toggles).forEach(toggleName => {
|
||||
@ -66,21 +160,26 @@ module.exports = class ClientMetricsService {
|
||||
);
|
||||
});
|
||||
});
|
||||
this.seenClients = {};
|
||||
this.bulkAddTimer = setInterval(() => this.bulkAdd(), bulkInterval);
|
||||
this.bulkAddTimer.unref();
|
||||
this.announceTimer = setInterval(
|
||||
() => this.announceUnannounced(),
|
||||
appAnnouncementInterval,
|
||||
|
||||
this.timers.push(
|
||||
setInterval(() => this.bulkAdd(), this.bulkInterval).unref(),
|
||||
);
|
||||
this.timers.push(
|
||||
setInterval(
|
||||
() => this.announceUnannounced(),
|
||||
this.announcementInterval,
|
||||
).unref(),
|
||||
);
|
||||
this.announceTimer.unref();
|
||||
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 toggleNames = Object.keys(value.bucket.toggles);
|
||||
await this.toggleStore.lastSeenToggles(toggleNames);
|
||||
await this.featureToggleStore.lastSeenToggles(toggleNames);
|
||||
await this.clientMetricsStore.insert(value);
|
||||
await this.clientInstanceStore.insert({
|
||||
appName: value.appName,
|
||||
@ -89,40 +188,36 @@ module.exports = class ClientMetricsService {
|
||||
});
|
||||
}
|
||||
|
||||
async announceUnannounced() {
|
||||
if (this.clientAppStore) {
|
||||
try {
|
||||
const appsToAnnounce = await this.clientAppStore.setUnannouncedToAnnounced();
|
||||
if (appsToAnnounce.length > 0) {
|
||||
const events = appsToAnnounce.map(app => ({
|
||||
type: APPLICATION_CREATED,
|
||||
createdBy: app.createdBy || 'unknown',
|
||||
data: app,
|
||||
}));
|
||||
await this.eventStore.batchStore(events);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(e);
|
||||
async announceUnannounced(): Promise<void> {
|
||||
if (this.clientApplicationsStore) {
|
||||
const appsToAnnounce = await this.clientApplicationsStore.setUnannouncedToAnnounced();
|
||||
if (appsToAnnounce.length > 0) {
|
||||
const events = appsToAnnounce.map(app => ({
|
||||
type: APPLICATION_CREATED,
|
||||
createdBy: app.createdBy || 'unknown',
|
||||
data: app,
|
||||
}));
|
||||
await this.eventStore.batchStore(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async registerClient(data, clientIp) {
|
||||
async registerClient(data: IClientApp, clientIp: string): Promise<void> {
|
||||
const value = await clientRegisterSchema.validateAsync(data);
|
||||
value.clientIp = clientIp;
|
||||
value.createdBy = clientIp;
|
||||
this.seenClients[this.clientKey(value)] = value;
|
||||
}
|
||||
|
||||
clientKey(client) {
|
||||
clientKey(client: IClientApp): string {
|
||||
return `${client.appName}_${client.instanceId}`;
|
||||
}
|
||||
|
||||
async bulkAdd() {
|
||||
async bulkAdd(): Promise<void> {
|
||||
if (
|
||||
this &&
|
||||
this.seenClients &&
|
||||
this.clientAppStore &&
|
||||
this.clientApplicationsStore &&
|
||||
this.clientInstanceStore
|
||||
) {
|
||||
const uniqueRegistrations = Object.values(this.seenClients);
|
||||
@ -135,7 +230,7 @@ module.exports = class ClientMetricsService {
|
||||
this.seenClients = {};
|
||||
try {
|
||||
if (uniqueRegistrations.length > 0) {
|
||||
await this.clientAppStore.bulkUpsert(uniqueApps);
|
||||
await this.clientApplicationsStore.bulkUpsert(uniqueApps);
|
||||
await this.clientInstanceStore.bulkUpsert(
|
||||
uniqueRegistrations,
|
||||
);
|
||||
@ -146,7 +241,7 @@ module.exports = class ClientMetricsService {
|
||||
}
|
||||
}
|
||||
|
||||
appToEvent(app) {
|
||||
appToEvent(app: IClientApp): ICreateEvent {
|
||||
return {
|
||||
type: APPLICATION_CREATED,
|
||||
createdBy: app.clientIp,
|
||||
@ -154,7 +249,7 @@ module.exports = class ClientMetricsService {
|
||||
};
|
||||
}
|
||||
|
||||
getAppsWithToggles() {
|
||||
getAppsWithToggles(): IClientApp[] {
|
||||
const apps = [];
|
||||
Object.keys(this.apps).forEach(appName => {
|
||||
const seenToggles = Object.keys(this.apps[appName].seenToggles);
|
||||
@ -164,15 +259,15 @@ module.exports = class ClientMetricsService {
|
||||
return apps;
|
||||
}
|
||||
|
||||
getSeenTogglesByAppName(appName) {
|
||||
getSeenTogglesByAppName(appName: string): string[] {
|
||||
return this.apps[appName]
|
||||
? Object.keys(this.apps[appName].seenToggles)
|
||||
: [];
|
||||
}
|
||||
|
||||
async getSeenApps() {
|
||||
async getSeenApps(): Promise<Record<string, IApplication[]>> {
|
||||
const seenApps = this.getSeenAppsPerToggle();
|
||||
const applications = await this.clientAppStore.getApplications();
|
||||
const applications: IApplication[] = await this.clientApplicationsStore.getApplications();
|
||||
const metaData = applications.reduce((result, entry) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result[entry.appName] = entry;
|
||||
@ -190,11 +285,13 @@ module.exports = class ClientMetricsService {
|
||||
return seenApps;
|
||||
}
|
||||
|
||||
async getApplications(query) {
|
||||
return this.clientAppStore.getApplications(query);
|
||||
async getApplications(
|
||||
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 [
|
||||
application,
|
||||
@ -202,10 +299,10 @@ module.exports = class ClientMetricsService {
|
||||
strategies,
|
||||
features,
|
||||
] = await Promise.all([
|
||||
this.clientAppStore.getApplication(appName),
|
||||
this.clientApplicationsStore.getApplication(appName),
|
||||
this.clientInstanceStore.getByAppName(appName),
|
||||
this.strategyStore.getStrategies(),
|
||||
this.toggleStore.getFeatures(),
|
||||
this.featureToggleStore.getFeatures(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -230,7 +327,7 @@ module.exports = class ClientMetricsService {
|
||||
};
|
||||
}
|
||||
|
||||
getSeenAppsPerToggle() {
|
||||
getSeenAppsPerToggle(): Record<string, IApplication[]> {
|
||||
const toggles = {};
|
||||
Object.keys(this.apps).forEach(appName => {
|
||||
Object.keys(this.apps[appName].seenToggles).forEach(
|
||||
@ -245,20 +342,20 @@ module.exports = class ClientMetricsService {
|
||||
return toggles;
|
||||
}
|
||||
|
||||
getTogglesMetrics() {
|
||||
getTogglesMetrics(): Record<string, Record<string, IYesNoCount>> {
|
||||
return {
|
||||
lastHour: this.lastHourProjection.getProjection(),
|
||||
lastMinute: this.lastMinuteProjection.getProjection(),
|
||||
};
|
||||
}
|
||||
|
||||
addPayload(data) {
|
||||
addPayload(data: IClientApp): void {
|
||||
const { appName, bucket } = data;
|
||||
const app = this.getApp(appName);
|
||||
this.addBucket(app, bucket);
|
||||
}
|
||||
|
||||
getApp(appName) {
|
||||
getApp(appName: string): IClientApp {
|
||||
this.apps[appName] = this.apps[appName] || {
|
||||
seenToggles: {},
|
||||
count: 0,
|
||||
@ -266,7 +363,7 @@ module.exports = class ClientMetricsService {
|
||||
return this.apps[appName];
|
||||
}
|
||||
|
||||
createCountObject(entry) {
|
||||
createCountObject(entry: IMetricCounts): IYesNoCount {
|
||||
let yes = typeof entry.yes === 'number' ? entry.yes : 0;
|
||||
let no = typeof entry.no === 'number' ? entry.no : 0;
|
||||
|
||||
@ -283,7 +380,7 @@ module.exports = class ClientMetricsService {
|
||||
return { yes, no };
|
||||
}
|
||||
|
||||
addBucket(app, bucket) {
|
||||
addBucket(app: IClientApp, bucket: IMetricsBucket): void {
|
||||
let count = 0;
|
||||
// TODO stop should be createdAt
|
||||
const { stop, toggles } = bucket;
|
||||
@ -305,26 +402,25 @@ module.exports = class ClientMetricsService {
|
||||
this.addSeenToggles(app, toggleNames);
|
||||
}
|
||||
|
||||
addSeenToggles(app, toggleNames) {
|
||||
addSeenToggles(app: IClientApp, toggleNames: string[]): void {
|
||||
toggleNames.forEach(t => {
|
||||
app.seenToggles[t] = true;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteApplication(appName) {
|
||||
async deleteApplication(appName: string): Promise<void> {
|
||||
await this.clientInstanceStore.deleteForApplication(appName);
|
||||
await this.clientAppStore.deleteApplication(appName);
|
||||
await this.clientApplicationsStore.deleteApplication(appName);
|
||||
}
|
||||
|
||||
async createApplication(input) {
|
||||
const applicationData = await appSchema.validateAsync(input);
|
||||
await this.clientAppStore.upsert(applicationData);
|
||||
async createApplication(input: IApplication): Promise<void> {
|
||||
const applicationData = await applicationSchema.validateAsync(input);
|
||||
await this.clientApplicationsStore.upsert(applicationData);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
destroy(): void {
|
||||
this.lastHourList.destroy();
|
||||
this.lastMinuteList.destroy();
|
||||
clearInterval(this.announceTimer);
|
||||
clearInterval(this.bulkAddTimer);
|
||||
this.timers.forEach(clearInterval);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
import joi from 'joi';
|
||||
|
||||
const joi = require('joi');
|
||||
|
||||
const applicationSchema = joi
|
||||
export const applicationSchema = joi
|
||||
.object()
|
||||
.options({ stripUnknown: false })
|
||||
.keys({
|
||||
@ -29,5 +27,3 @@ const applicationSchema = joi
|
||||
.allow('')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
module.exports = applicationSchema;
|
27
src/lib/services/client-metrics/models.ts
Normal file
27
src/lib/services/client-metrics/models.ts
Normal 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>;
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
import { Projection } from './projection';
|
||||
|
||||
const Projection = require('./projection');
|
||||
|
||||
test('should return set empty if missing', () => {
|
||||
const projection = new Projection();
|
@ -1,15 +1,13 @@
|
||||
'use strict';
|
||||
import { IYesNoCount } from './models';
|
||||
|
||||
module.exports = class Projection {
|
||||
constructor() {
|
||||
this.store = {};
|
||||
}
|
||||
export class Projection {
|
||||
store: Record<string, IYesNoCount> = {};
|
||||
|
||||
getProjection() {
|
||||
getProjection(): Record<string, IYesNoCount> {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
add(name, countObj) {
|
||||
add(name: string, countObj: IYesNoCount): void {
|
||||
if (this.store[name]) {
|
||||
this.store[name].yes += countObj.yes;
|
||||
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]) {
|
||||
this.store[name].yes -= countObj.yes;
|
||||
this.store[name].no -= countObj.no;
|
||||
@ -32,4 +30,4 @@ module.exports = class Projection {
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -21,7 +21,6 @@ import { IUnleashStores } from '../types/stores';
|
||||
import PasswordUndefinedError from '../error/password-undefined';
|
||||
import EventStore from '../db/event-store';
|
||||
import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
|
||||
import { IRole } from '../db/access-store';
|
||||
|
||||
const systemUser = new User({ id: -1, username: 'system' });
|
||||
|
||||
|
@ -5,7 +5,7 @@ import FeatureTypeStore from '../db/feature-type-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 { ClientMetricsStore } from '../db/client-metrics-store';
|
||||
import FeatureToggleStore from '../db/feature-toggle-store';
|
||||
import ContextFieldStore from '../db/context-field-store';
|
||||
import SettingStore from '../db/setting-store';
|
||||
|
@ -1,7 +1,10 @@
|
||||
import ClientMetricsService, {
|
||||
IClientApp,
|
||||
} from '../../../lib/services/client-metrics';
|
||||
|
||||
const faker = require('faker');
|
||||
const dbInit = require('../helpers/database-init');
|
||||
const getLogger = require('../../fixtures/no-logger');
|
||||
const ClientMetricsService = require('../../../lib/services/client-metrics');
|
||||
const { APPLICATION_CREATED } = require('../../../lib/types/events');
|
||||
|
||||
let stores;
|
||||
@ -11,11 +14,12 @@ let clientMetricsService;
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('client_metrics_service_serial', getLogger);
|
||||
stores = db.stores;
|
||||
clientMetricsService = new ClientMetricsService(stores, {
|
||||
getLogger,
|
||||
bulkInterval: 500,
|
||||
announcementInterval: 2000,
|
||||
});
|
||||
clientMetricsService = new ClientMetricsService(
|
||||
stores,
|
||||
{ getLogger },
|
||||
500,
|
||||
2000,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -24,7 +28,7 @@ afterAll(async () => {
|
||||
});
|
||||
test('Apps registered should be announced', async () => {
|
||||
expect.assertions(3);
|
||||
const clientRegistration = {
|
||||
const clientRegistration: IClientApp = {
|
||||
appName: faker.internet.domainName(),
|
||||
instanceId: faker.datatype.uuid(),
|
||||
strategies: ['default'],
|
Loading…
Reference in New Issue
Block a user