1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

feat/metricsV2

This commit is contained in:
Ivar Conradi Østhus 2021-10-06 12:01:53 +02:00
parent 796f202da3
commit 4cf6258209
No known key found for this signature in database
GPG Key ID: 31AC596886B0BD09
21 changed files with 771 additions and 6 deletions

View File

@ -40,7 +40,7 @@ function safeNumber(envVar, defaultVal): number {
}
}
function safeBoolean(envVar, defaultVal) {
function safeBoolean(envVar: string, defaultVal: boolean): boolean {
if (envVar) {
return envVar === 'true' || envVar === '1' || envVar === 't';
}
@ -224,6 +224,10 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
const experimental = options.experimental || {};
if (safeBoolean(process.env.EXP_METRICS_V2, false)) {
experimental.metricsV2 = { enabled: true };
}
const email: IEmailOption = mergeAll([defaultEmail, options.email]);
let listen: IListeningPipe | IListeningHost;

View File

@ -0,0 +1,109 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import util from 'util';
import { Knex } from 'knex';
import { Logger, LogProvider } from '../logger';
import {
IClientMetricsEnv,
IClientMetricsEnvKey,
IClientMetricsStoreV2,
} from '../types/stores/client-metrics-store-v2';
interface ClientMetricsEnvTable {
feature_name: string;
app_name: string;
environment: string;
timestamp: Date;
yes: number;
no: number;
}
const TABLE = 'client_metrics_env';
function roundDownToHour(date) {
let p = 60 * 60 * 1000; // milliseconds in an hour
return new Date(Math.floor(date.getTime() / p) * p);
}
const fromRow = (row: ClientMetricsEnvTable) => ({
featureName: row.feature_name,
appName: row.app_name,
environment: row.environment,
timestamp: row.timestamp,
yes: row.yes,
no: row.no,
});
const toRow = (metric: IClientMetricsEnv) => ({
feature_name: metric.featureName,
app_name: metric.appName,
environment: metric.environment,
timestamp: roundDownToHour(metric.timestamp),
yes: metric.yes,
no: metric.no,
});
export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
private db: Knex;
private logger: Logger;
constructor(db: Knex, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('client-metrics-store-v2.js');
}
get(key: IClientMetricsEnvKey): Promise<IClientMetricsEnv> {
throw new Error('Method not implemented.');
}
async getAll(query: Object = {}): Promise<IClientMetricsEnv[]> {
const rows = await this.db<ClientMetricsEnvTable>(TABLE)
.select('*')
.where(query);
return rows.map(fromRow);
}
exists(key: IClientMetricsEnvKey): Promise<boolean> {
throw new Error('Method not implemented.');
}
delete(key: IClientMetricsEnvKey): Promise<void> {
throw new Error('Method not implemented.');
}
deleteAll(): Promise<void> {
throw new Error('Method not implemented.');
}
destroy(): void {
// Nothing to do!
}
async batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise<void> {
const rows = metrics.map(toRow);
// Consider rewriting to SQL batch!
for (const row of rows) {
const insert = this.db<ClientMetricsEnvTable>(TABLE)
.insert(row)
.toQuery();
const query = `${insert.toString()} ON CONFLICT (feature_name, app_name, environment, timestamp)
DO UPDATE SET
"yes" = "client_metrics_env"."yes" + ?,
"no" = "client_metrics_env"."no" + ?`;
await this.db.raw(query, [row.yes, row.no]);
}
}
async getMetricsForFeatureToggle(
featureName: string,
hoursBack: number = 24,
): Promise<IClientMetricsEnv[]> {
const rows = await this.db<ClientMetricsEnvTable>(TABLE)
.select('*')
.where({ feature_name: featureName })
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`);
return rows.map(fromRow);
}
}

View File

@ -28,6 +28,7 @@ import FeatureToggleClientStore from './feature-toggle-client-store';
import EnvironmentStore from './environment-store';
import FeatureTagStore from './feature-tag-store';
import { FeatureEnvironmentStore } from './feature-environment-store';
import { ClientMetricsStoreV2 } from './client-metrics-store-v2';
export const createStores = (
config: IUnleashConfig,
@ -54,6 +55,7 @@ export const createStores = (
eventBus,
getLogger,
),
clientMetricsStoreV2: new ClientMetricsStoreV2(db, getLogger),
contextFieldStore: new ContextFieldStore(db, getLogger),
settingStore: new SettingStore(db, getLogger),
userStore: new UserStore(db, getLogger),

View File

@ -0,0 +1,37 @@
import { Request, Response } from 'express';
import Controller from '../controller';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import { Logger } from '../../logger';
import ClientMetricsServiceV2 from '../../services/client-metrics/client-metrics-service-v2';
class ClientMetricsController extends Controller {
private logger: Logger;
private metrics: ClientMetricsServiceV2;
constructor(
config: IUnleashConfig,
{
clientMetricsServiceV2,
}: Pick<IUnleashServices, 'clientMetricsServiceV2'>,
) {
super(config);
this.logger = config.getLogger('/admin-api/client-metrics.ts');
this.metrics = clientMetricsServiceV2;
this.get('/features/:name', this.getFeatureToggleMetrics);
}
async getFeatureToggleMetrics(req: Request, res: Response): Promise<void> {
const { name } = req.params;
const data = await this.metrics.getClientMetricsForToggle(name);
res.json({
version: 1,
maturity: 'experimental',
data,
});
}
}
export default ClientMetricsController;

View File

@ -11,6 +11,7 @@ import MetricsController from './metrics';
import UserController from './user';
import ConfigController from './config';
import ContextController from './context';
import ClientMetricsController from './client-metrics';
import BootstrapController from './bootstrap-controller';
import StateController from './state';
import TagController from './tag';
@ -49,6 +50,10 @@ class AdminApi extends Controller {
'/metrics',
new MetricsController(config, services).router,
);
this.app.use(
'/client-metrics',
new ClientMetricsController(config, services).router,
);
this.app.use('/user', new UserController(config, services).router);
this.app.use(
'/ui-config',

View File

@ -7,21 +7,37 @@ import { Logger } from '../../logger';
import { IAuthRequest } from '../unleash-types';
import ApiUser from '../../types/api-user';
import { ALL } from '../../types/models/api-token';
import ClientMetricsServiceV2 from '../../services/client-metrics/client-metrics-service-v2';
export default class ClientMetricsController extends Controller {
logger: Logger;
metrics: ClientMetricsService;
metricsV2: ClientMetricsServiceV2;
newServiceEnabled: boolean = false;
constructor(
{
clientMetricsService,
}: Pick<IUnleashServices, 'clientMetricsService'>,
clientMetricsServiceV2,
}: Pick<
IUnleashServices,
'clientMetricsService' | 'clientMetricsServiceV2'
>,
config: IUnleashConfig,
) {
super(config);
this.logger = config.getLogger('/api/client/metrics');
const { experimental, getLogger } = config;
if (experimental && experimental.metricsV2) {
//@ts-ignore
this.newServiceEnabled = experimental.metricsV2.enabled;
}
this.logger = getLogger('/api/client/metrics');
this.metrics = clientMetricsService;
this.metricsV2 = clientMetricsServiceV2;
this.post('/', this.registerMetrics);
}
@ -34,6 +50,11 @@ export default class ClientMetricsController extends Controller {
}
}
await this.metrics.registerClientMetrics(data, clientIp);
if (this.newServiceEnabled) {
await this.metricsV2.registerClientMetrics(data, clientIp);
}
return res.status(202).end();
}
}

View File

@ -0,0 +1,70 @@
import { Logger } from '../../logger';
import { IUnleashConfig } from '../../server-impl';
import { IUnleashStores } from '../../types';
import { IClientApp } from '../../types/model';
import { GroupedClientMetrics } from '../../types/models/metrics';
import {
IClientMetricsEnv,
IClientMetricsStoreV2,
} from '../../types/stores/client-metrics-store-v2';
import { clientMetricsSchema } from './client-metrics-schema';
import { groupMetricsOnEnv } from './util';
const FIVE_MINUTES = 5 * 60 * 1000;
export default class ClientMetricsServiceV2 {
private timers: NodeJS.Timeout[] = [];
private clientMetricsStoreV2: IClientMetricsStoreV2;
private logger: Logger;
private bulkInterval: number;
constructor(
{ clientMetricsStoreV2 }: Pick<IUnleashStores, 'clientMetricsStoreV2'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
bulkInterval = FIVE_MINUTES,
) {
this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.logger = getLogger('/services/client-metrics/index.ts');
this.bulkInterval = bulkInterval;
}
async registerClientMetrics(
data: IClientApp,
clientIp: string,
): Promise<void> {
const value = await clientMetricsSchema.validateAsync(data);
const toggleNames = Object.keys(value.bucket.toggles);
this.logger.debug(`got metrics from ${clientIp}`);
const clientMetrics: IClientMetricsEnv[] = toggleNames
.map((name) => ({
featureName: name,
appName: value.appName,
environment: value.environment,
timestamp: value.bucket.start, //we might need to approximate between start/stop...
yes: value.bucket.toggles[name].yes,
no: value.bucket.toggles[name].no,
}))
.filter((item) => !(item.yes === 0 && item.no === 0));
// TODO: should we aggregate for a few minutes (bulkInterval) before pushing to DB?
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
}
async getClientMetricsForToggle(
toggleName: string,
): Promise<GroupedClientMetrics[]> {
const metrics =
await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
toggleName,
);
return groupMetricsOnEnv(metrics);
}
}

View File

@ -1,4 +1,3 @@
import { LogProvider } from '../../logger';
import { applicationSchema } from './metrics-schema';
import { Projection } from './projection';
import { clientMetricsSchema } from './client-metrics-schema';
@ -66,8 +65,6 @@ export default class ClientMetricsService {
private eventStore: IEventStore;
private getLogger: LogProvider;
private bulkInterval: number;
private announcementInterval: number;

View File

@ -0,0 +1,57 @@
import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2';
import { generateLastNHours, groupMetricsOnEnv, roundDownToHour } from './util';
test('should return list of 24 horus', () => {
const hours = generateLastNHours(24, new Date(2021, 10, 10, 15, 30, 1, 0));
expect(hours).toHaveLength(24);
expect(hours[0]).toStrictEqual(new Date(2021, 10, 10, 15, 0, 0));
expect(hours[1]).toStrictEqual(new Date(2021, 10, 10, 14, 0, 0));
expect(hours[2]).toStrictEqual(new Date(2021, 10, 10, 13, 0, 0));
expect(hours[23]).toStrictEqual(new Date(2021, 10, 9, 16, 0, 0));
});
test('should group metrics together', () => {
const date = roundDownToHour(new Date());
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 2,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 3,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'test',
timestamp: date,
yes: 1,
no: 3,
},
];
const grouped = groupMetricsOnEnv(metrics);
expect(grouped[0]).toStrictEqual({
timestamp: date,
environment: 'default',
yes_count: 5,
no_count: 4,
});
expect(grouped[1]).toStrictEqual({
timestamp: date,
environment: 'test',
yes_count: 1,
no_count: 3,
});
});

View File

@ -0,0 +1,48 @@
import { GroupedClientMetrics } from '../../types/models/metrics';
import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2';
//duplicate from client-metrics-store-v2.ts
export function roundDownToHour(date: Date): Date {
let p = 60 * 60 * 1000; // milliseconds in an hour
return new Date(Math.floor(date.getTime() / p) * p);
}
export function generateLastNHours(n: number, start: Date): Date[] {
const nHours: Date[] = [];
nHours.push(roundDownToHour(start));
for (let i = 1; i < n; i++) {
const prev = nHours[i - 1];
const next = new Date(prev);
next.setHours(prev.getHours() - 1);
nHours.push(next);
}
return nHours;
}
export function groupMetricsOnEnv(
metrics: IClientMetricsEnv[],
): GroupedClientMetrics[] {
const hours = generateLastNHours(24, new Date());
const environments = metrics.map((m) => m.environment);
const grouped = {};
hours.forEach((time) => {
environments.forEach((environment) => {
grouped[`${time}:${environment}`] = {
timestamp: time,
environment,
yes_count: 0,
no_count: 0,
};
});
});
metrics.forEach((m) => {
grouped[`${m.timestamp}:${m.environment}`].yes_count += m.yes;
grouped[`${m.timestamp}:${m.environment}`].no_count += m.no;
});
return Object.values(grouped);
}

View File

@ -8,6 +8,7 @@ import HealthService from './health-service';
import ProjectService from './project-service';
import StateService from './state-service';
import ClientMetricsService from './client-metrics';
import ClientMetricsServiceV2 from './client-metrics/client-metrics-service-v2';
import TagTypeService from './tag-type-service';
import TagService from './tag-service';
import StrategyService from './strategy-service';
@ -34,6 +35,7 @@ export const createServices = (
const accessService = new AccessService(stores, config);
const apiTokenService = new ApiTokenService(stores, config);
const clientMetricsService = new ClientMetricsService(stores, config);
const clientMetricsServiceV2 = new ClientMetricsServiceV2(stores, config);
const contextService = new ContextService(stores, config);
const emailService = new EmailService(config.email, config.getLogger);
const eventService = new EventService(stores, config);
@ -82,6 +84,7 @@ export const createServices = (
tagTypeService,
tagService,
clientMetricsService,
clientMetricsServiceV2,
contextService,
versionService,
apiTokenService,

View File

@ -0,0 +1,6 @@
export interface GroupedClientMetrics {
environment: string;
timestamp: Date;
yes_count: number;
no_count: number;
}

View File

@ -22,12 +22,14 @@ import FeatureToggleServiceV2 from '../services/feature-toggle-service-v2';
import EnvironmentService from '../services/environment-service';
import FeatureTagService from '../services/feature-tag-service';
import ProjectHealthService from '../services/project-health-service';
import ClientMetricsServiceV2 from '../services/client-metrics/client-metrics-service-v2';
export interface IUnleashServices {
accessService: AccessService;
addonService: AddonService;
apiTokenService: ApiTokenService;
clientMetricsService: ClientMetricsService;
clientMetricsServiceV2: ClientMetricsServiceV2;
contextService: ContextService;
emailService: EmailService;
environmentService: EnvironmentService;

View File

@ -22,6 +22,7 @@ import { IFeatureEnvironmentStore } from './stores/feature-environment-store';
import { IFeatureStrategiesStore } from './stores/feature-strategies-store';
import { IEnvironmentStore } from './stores/environment-store';
import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store';
import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2';
export interface IUnleashStores {
accessStore: IAccessStore;
@ -30,6 +31,7 @@ export interface IUnleashStores {
clientApplicationsStore: IClientApplicationsStore;
clientInstanceStore: IClientInstanceStore;
clientMetricsStore: IClientMetricsStore;
clientMetricsStoreV2: IClientMetricsStoreV2;
contextFieldStore: IContextFieldStore;
environmentStore: IEnvironmentStore;
eventStore: IEventStore;

View File

@ -0,0 +1,22 @@
import { Store } from './store';
export interface IClientMetricsEnvKey {
featureName: string;
appName: string;
environment: string;
}
export interface IClientMetricsEnv extends IClientMetricsEnvKey {
timestamp: Date;
yes: number;
no: number;
}
export interface IClientMetricsStoreV2
extends Store<IClientMetricsEnv, IClientMetricsEnvKey> {
batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise<void>;
getMetricsForFeatureToggle(
featureName: string,
hoursBack?: number,
): Promise<IClientMetricsEnv[]>;
}

View File

@ -0,0 +1,28 @@
exports.up = function (db, cb) {
// TODO: foreign key on env.
db.runSql(
`
CREATE TABLE client_metrics_env(
feature_name VARCHAR(255),
app_name VARCHAR(255),
environment VARCHAR(100),
timestamp TIMESTAMP WITH TIME ZONE,
yes INTEGER DEFAULT 0,
no INTEGER DEFAULT 0,
PRIMARY KEY (feature_name, app_name, environment, timestamp)
);
CREATE INDEX idx_client_metrics_f_name ON client_metrics_env(feature_name);
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
DROP TABLE client_metrics_env;
`,
cb,
);
};

View File

@ -25,6 +25,11 @@ process.nextTick(async () => {
versionCheck: {
enable: false,
},
experimental: {
metricsV2: {
enabled: true,
},
},
}),
);
} catch (error) {

View File

@ -0,0 +1,95 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { roundDownToHour } from '../../../../lib/services/client-metrics/util';
import { IClientMetricsEnv } from '../../../../lib/types/stores/client-metrics-store-v2';
let app;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('client_metrics_serial', getLogger);
app = await setupAppWithCustomConfig(db.stores, {
experimental: { metricsV2: { enabled: true } },
});
});
afterAll(async () => {
if (db) {
await db.destroy();
}
});
afterEach(async () => {
await db.reset();
});
test('should return grouped metrics', async () => {
const date = roundDownToHour(new Date());
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 2,
no: 2,
},
{
featureName: 't2',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 5,
no: 5,
},
{
featureName: 't2',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 2,
no: 99,
},
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 3,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'test',
timestamp: date,
yes: 1,
no: 3,
},
];
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
const { body: demo } = await app.request
.get('/api/admin/client-metrics/features/demo')
.expect('Content-Type', /json/)
.expect(200);
const { body: t2 } = await app.request
.get('/api/admin/client-metrics/features/t2')
.expect('Content-Type', /json/)
.expect(200);
expect(demo.data).toHaveLength(48);
expect(demo.data[0].environment).toBe('default');
expect(demo.data[0].yes_count).toBe(5);
expect(demo.data[0].no_count).toBe(4);
expect(demo.data[1].environment).toBe('test');
expect(demo.data[1].yes_count).toBe(1);
expect(demo.data[1].no_count).toBe(3);
expect(t2.data).toHaveLength(24);
expect(t2.data[0].environment).toBe('default');
expect(t2.data[0].yes_count).toBe(7);
expect(t2.data[0].no_count).toBe(104);
});

View File

@ -0,0 +1,194 @@
import dbInit from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
import { IUnleashStores } from '../../../lib/types';
import {
IClientMetricsEnv,
IClientMetricsStoreV2,
} from '../../../lib/types/stores/client-metrics-store-v2';
let db;
let stores: IUnleashStores;
let clientMetricsStore: IClientMetricsStoreV2;
beforeEach(async () => {
db = await dbInit('client_metrics_store_v2_e2e_serial', getLogger);
stores = db.stores;
clientMetricsStore = stores.clientMetricsStoreV2;
});
afterEach(async () => {
await db.destroy();
});
test('Should store single list of metrics', async () => {
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: new Date(),
yes: 2,
no: 2,
},
];
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getAll();
expect(savedMetrics).toHaveLength(1);
});
test('Should "increment" metrics within same hour', async () => {
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: new Date(),
yes: 2,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: new Date(),
yes: 1,
no: 3,
},
];
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getAll();
expect(savedMetrics).toHaveLength(1);
expect(savedMetrics[0].yes).toBe(3);
expect(savedMetrics[0].no).toBe(5);
});
test('Should get individual metrics outside same hour', async () => {
const d1 = new Date();
const d2 = new Date();
d1.setHours(10, 10, 11);
d2.setHours(11, 10, 11);
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: d1,
yes: 2,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: d2,
yes: 1,
no: 3,
},
];
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getAll();
expect(savedMetrics).toHaveLength(2);
expect(savedMetrics[0].yes).toBe(2);
expect(savedMetrics[0].no).toBe(2);
});
test('Should insert hundred metrics in a row', async () => {
const metrics: IClientMetricsEnv[] = [];
const date = new Date();
for (let i = 0; i < 100; i++) {
metrics.push({
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: date,
yes: i,
no: i + 1,
});
}
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getAll();
expect(savedMetrics).toHaveLength(1);
expect(savedMetrics[0].yes).toBe(4950);
expect(savedMetrics[0].no).toBe(5050);
});
test('Should insert individual rows for different apps', async () => {
const metrics: IClientMetricsEnv[] = [];
const date = new Date();
for (let i = 0; i < 10; i++) {
metrics.push({
featureName: 'demo',
appName: `web-${i}`,
environment: 'dev',
timestamp: date,
yes: 2,
no: 2,
});
}
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getAll();
expect(savedMetrics).toHaveLength(10);
expect(savedMetrics[0].yes).toBe(2);
expect(savedMetrics[0].no).toBe(2);
});
test('Should insert individual rows for different toggles', async () => {
const metrics: IClientMetricsEnv[] = [];
const date = new Date();
for (let i = 0; i < 10; i++) {
metrics.push({
featureName: `app-${i}`,
appName: `web`,
environment: 'dev',
timestamp: date,
yes: 2,
no: 2,
});
}
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getAll();
expect(savedMetrics).toHaveLength(10);
expect(savedMetrics[0].yes).toBe(2);
expect(savedMetrics[0].no).toBe(2);
});
test('Should get toggle metrics', async () => {
const metrics: IClientMetricsEnv[] = [];
const date = new Date();
for (let i = 0; i < 100; i++) {
metrics.push({
featureName: 'demo',
appName: 'web',
environment: 'dev',
timestamp: date,
yes: i,
no: i + 1,
});
}
await clientMetricsStore.batchInsertMetrics(metrics);
const savedMetrics = await clientMetricsStore.getMetricsForFeatureToggle(
'demo',
);
expect(savedMetrics).toHaveLength(1);
expect(savedMetrics[0].yes).toBe(4950);
expect(savedMetrics[0].no).toBe(5050);
});

View File

@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/lines-between-class-members */
/* eslint-disable @typescript-eslint/no-unused-vars */
import EventEmitter from 'events';
import { IClientMetric } from '../../lib/types/stores/client-metrics-db';
import {
IClientMetricsEnv,
IClientMetricsEnvKey,
IClientMetricsStoreV2,
} from '../../lib/types/stores/client-metrics-store-v2';
export default class FakeClientMetricsStoreV2
extends EventEmitter
implements IClientMetricsStoreV2
{
metrics: IClientMetric[] = [];
constructor() {
super();
this.setMaxListeners(0);
}
getMetricsForFeatureToggle(
featureName: string,
hoursBack?: number,
): Promise<IClientMetricsEnv[]> {
throw new Error('Method not implemented.');
}
batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise<void> {
throw new Error('Method not implemented.');
}
get(key: IClientMetricsEnvKey): Promise<IClientMetricsEnv> {
throw new Error('Method not implemented.');
}
getAll(query?: Object): Promise<IClientMetricsEnv[]> {
throw new Error('Method not implemented.');
}
exists(key: IClientMetricsEnvKey): Promise<boolean> {
throw new Error('Method not implemented.');
}
delete(key: IClientMetricsEnvKey): Promise<void> {
throw new Error('Method not implemented.');
}
async getMetricsLastHour(): Promise<IClientMetric[]> {
return Promise.resolve([]);
}
async insert(): Promise<void> {
return Promise.resolve();
}
async deleteAll(): Promise<void> {
return Promise.resolve(undefined);
}
destroy(): void {}
}

View File

@ -23,6 +23,7 @@ import FakeApiTokenStore from './fake-api-token-store';
import FakeFeatureTypeStore from './fake-feature-type-store';
import FakeResetTokenStore from './fake-reset-token-store';
import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store';
import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2';
const createStores: () => IUnleashStores = () => {
const db = {
@ -35,6 +36,7 @@ const createStores: () => IUnleashStores = () => {
db,
clientApplicationsStore: new FakeClientApplicationsStore(),
clientMetricsStore: new FakeClientMetricsStore(),
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
clientInstanceStore: new FakeClientInstanceStore(),
featureToggleStore: new FakeFeatureToggleStore(),
featureToggleClientStore: new FakeFeatureToggleClientStore(),