mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-08 01:15:49 +02:00
620 lines
17 KiB
TypeScript
620 lines
17 KiB
TypeScript
import EventEmitter from 'events';
|
|
import ClientMetricsService from './index';
|
|
import getLogger from '../../../test/fixtures/no-logger';
|
|
import { IClientApp } from '../../types/model';
|
|
import {
|
|
addHours,
|
|
addMinutes,
|
|
hoursToMilliseconds,
|
|
minutesToMilliseconds,
|
|
secondsToMilliseconds,
|
|
subHours,
|
|
subMinutes,
|
|
subSeconds,
|
|
} from 'date-fns';
|
|
|
|
/**
|
|
* A utility to wait for any pending promises in the test subject code.
|
|
* For instance, if the test needs to wait for a timeout/interval handler,
|
|
* and that handler does something async, advancing the timers is not enough:
|
|
* We have to explicitly wait for the second promise.
|
|
* For more info, see https://stackoverflow.com/a/51045733/2868829
|
|
*
|
|
* Usage in test code after advancing timers, but before making assertions:
|
|
*
|
|
* test('hello', async () => {
|
|
* jest.useFakeTimers('modern');
|
|
*
|
|
* // Schedule a timeout with a callback that does something async
|
|
* // before calling our spy
|
|
* const spy = jest.fn();
|
|
* setTimeout(async () => {
|
|
* await Promise.resolve();
|
|
* spy();
|
|
* }, 1000);
|
|
*
|
|
* expect(spy).not.toHaveBeenCalled();
|
|
*
|
|
* jest.advanceTimersByTime(1500);
|
|
* await flushPromises(); // this is required to make it work!
|
|
*
|
|
* expect(spy).toHaveBeenCalledTimes(1);
|
|
*
|
|
* jest.useRealTimers();
|
|
* });
|
|
*/
|
|
function flushPromises() {
|
|
return Promise.resolve(setImmediate);
|
|
}
|
|
|
|
const appName = 'appName';
|
|
const instanceId = 'instanceId';
|
|
|
|
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 = createMetricsService(clientMetricsStore);
|
|
|
|
expect(metrics.getAppsWithToggles()).toBeTruthy();
|
|
expect(metrics.getTogglesMetrics()).toBeTruthy();
|
|
|
|
metrics.destroy();
|
|
});
|
|
|
|
test('data should expire', () => {
|
|
jest.useFakeTimers('modern');
|
|
|
|
const clientMetricsStore = new EventEmitter();
|
|
const metrics = createMetricsService(clientMetricsStore);
|
|
|
|
metrics.addPayload({
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: subSeconds(Date.now(), 2),
|
|
stop: subSeconds(Date.now(), 1),
|
|
toggles: {
|
|
toggleX: {
|
|
yes: 123,
|
|
no: 0,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
let lastHourExpires = 0;
|
|
metrics.lastHourList.on('expire', () => {
|
|
lastHourExpires++;
|
|
});
|
|
|
|
let lastMinExpires = 0;
|
|
metrics.lastMinuteList.on('expire', () => {
|
|
lastMinExpires++;
|
|
});
|
|
|
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
|
expect(lastMinExpires).toBe(1);
|
|
expect(lastHourExpires).toBe(0);
|
|
|
|
jest.advanceTimersByTime(hoursToMilliseconds(1));
|
|
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 = createMetricsService(clientMetricsStore);
|
|
clientMetricsStore.emit('metrics', {
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: new Date(),
|
|
stop: new Date(),
|
|
toggles: {
|
|
toggleX: {
|
|
yes: 123,
|
|
no: 0,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(metrics.apps[appName].count).toBe(123);
|
|
expect(metrics.globalCount).toBe(123);
|
|
|
|
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
|
yes: 123,
|
|
no: 0,
|
|
});
|
|
expect(metrics.getTogglesMetrics().lastMinute.toggleX).toEqual({
|
|
yes: 123,
|
|
no: 0,
|
|
});
|
|
|
|
metrics.addPayload({
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: new Date(),
|
|
stop: new Date(),
|
|
toggles: {
|
|
toggleX: {
|
|
yes: 10,
|
|
no: 10,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(metrics.globalCount).toBe(143);
|
|
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
|
yes: 133,
|
|
no: 10,
|
|
});
|
|
expect(metrics.getTogglesMetrics().lastMinute.toggleX).toEqual({
|
|
yes: 133,
|
|
no: 10,
|
|
});
|
|
|
|
metrics.destroy();
|
|
});
|
|
|
|
test('should build up list of seen toggles when new metrics arrives', () => {
|
|
const clientMetricsStore = new EventEmitter();
|
|
const metrics = createMetricsService(clientMetricsStore);
|
|
clientMetricsStore.emit('metrics', {
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: new Date(),
|
|
stop: new Date(),
|
|
toggles: {
|
|
toggleX: {
|
|
yes: 123,
|
|
no: 0,
|
|
},
|
|
toggleY: {
|
|
yes: 50,
|
|
no: 50,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const appToggles = metrics.getAppsWithToggles();
|
|
const togglesForApp = metrics.getSeenTogglesByAppName(appName);
|
|
|
|
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.length === 2);
|
|
expect(togglesForApp).toContain('toggleX');
|
|
expect(togglesForApp).toContain('toggleY');
|
|
metrics.destroy();
|
|
});
|
|
|
|
test('should handle a lot of toggles', () => {
|
|
const clientMetricsStore = new EventEmitter();
|
|
const metrics = createMetricsService(clientMetricsStore);
|
|
|
|
const toggleCounts = {};
|
|
for (let i = 0; i < 100; i++) {
|
|
toggleCounts[`toggle${i}`] = { yes: i, no: i };
|
|
}
|
|
|
|
clientMetricsStore.emit('metrics', {
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: new Date(),
|
|
stop: new Date(),
|
|
toggles: toggleCounts,
|
|
},
|
|
});
|
|
|
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
|
|
|
expect(seenToggles.length).toBe(100);
|
|
metrics.destroy();
|
|
});
|
|
|
|
test('should have correct values for lastMinute', () => {
|
|
jest.useFakeTimers('modern');
|
|
|
|
const clientMetricsStore = new EventEmitter();
|
|
const metrics = createMetricsService(clientMetricsStore);
|
|
|
|
const now = new Date();
|
|
const input = [
|
|
{
|
|
start: subHours(now, 1),
|
|
stop: subMinutes(now, 59),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: subMinutes(now, 30),
|
|
stop: subMinutes(now, 29),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: subMinutes(now, 2),
|
|
stop: subMinutes(now, 1),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: subMinutes(now, 2),
|
|
stop: subSeconds(now, 59),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: now,
|
|
stop: subSeconds(now, 30),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
];
|
|
|
|
input.forEach((bucket) => {
|
|
clientMetricsStore.emit('metrics', {
|
|
appName,
|
|
instanceId,
|
|
bucket,
|
|
});
|
|
});
|
|
|
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
|
expect(seenToggles.length).toBe(1);
|
|
|
|
// metrics.se
|
|
let c = metrics.getTogglesMetrics();
|
|
expect(c.lastMinute.toggle).toEqual({ yes: 20, no: 20 });
|
|
|
|
jest.advanceTimersByTime(10_000);
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastMinute.toggle).toEqual({ yes: 10, no: 10 });
|
|
|
|
jest.advanceTimersByTime(20_000);
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastMinute.toggle).toEqual({ yes: 0, no: 0 });
|
|
|
|
metrics.destroy();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
test('should have correct values for lastHour', () => {
|
|
jest.useFakeTimers('modern');
|
|
|
|
const clientMetricsStore = new EventEmitter();
|
|
const metrics = createMetricsService(clientMetricsStore);
|
|
|
|
const now = Date.now();
|
|
const input = [
|
|
{
|
|
start: subHours(now, 1),
|
|
stop: subMinutes(now, 59),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: subMinutes(now, 30),
|
|
stop: subMinutes(now, 29),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: subMinutes(now, 15),
|
|
stop: subMinutes(now, 14),
|
|
toggles: {
|
|
toggle: { yes: 10, no: 10 },
|
|
},
|
|
},
|
|
{
|
|
start: addMinutes(now, 59),
|
|
stop: addHours(now, 1),
|
|
toggles: {
|
|
toggle: { yes: 11, no: 11 },
|
|
},
|
|
},
|
|
];
|
|
|
|
input.forEach((bucket) => {
|
|
clientMetricsStore.emit('metrics', {
|
|
appName,
|
|
instanceId,
|
|
bucket,
|
|
});
|
|
});
|
|
|
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
|
|
|
expect(seenToggles.length).toBe(1);
|
|
|
|
// metrics.se
|
|
let c = metrics.getTogglesMetrics();
|
|
expect(c.lastHour.toggle).toEqual({ yes: 41, no: 41 });
|
|
|
|
jest.advanceTimersByTime(10_000);
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastHour.toggle).toEqual({ yes: 41, no: 41 });
|
|
|
|
// at 30
|
|
jest.advanceTimersByTime(minutesToMilliseconds(30));
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastHour.toggle).toEqual({ yes: 31, no: 31 });
|
|
|
|
// at 45
|
|
jest.advanceTimersByTime(minutesToMilliseconds(15));
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastHour.toggle).toEqual({ yes: 21, no: 21 });
|
|
|
|
// at 1:15
|
|
jest.advanceTimersByTime(minutesToMilliseconds(30));
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastHour.toggle).toEqual({ yes: 11, no: 11 });
|
|
|
|
// at 2:00
|
|
jest.advanceTimersByTime(minutesToMilliseconds(45));
|
|
c = metrics.getTogglesMetrics();
|
|
expect(c.lastHour.toggle).toEqual({ yes: 0, no: 0 });
|
|
|
|
metrics.destroy();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
test('should not fail when toggle metrics is missing yes/no field', () => {
|
|
const clientMetricsStore = new EventEmitter();
|
|
const metrics = createMetricsService(clientMetricsStore);
|
|
clientMetricsStore.emit('metrics', {
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: new Date(),
|
|
stop: new Date(),
|
|
toggles: {
|
|
toggleX: {
|
|
yes: 123,
|
|
no: 0,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
metrics.addPayload({
|
|
appName,
|
|
instanceId,
|
|
bucket: {
|
|
start: new Date(),
|
|
stop: new Date(),
|
|
toggles: {
|
|
toggleX: {
|
|
blue: 10,
|
|
green: 10,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(metrics.globalCount).toBe(123);
|
|
expect(metrics.getTogglesMetrics().lastMinute.toggleX).toEqual({
|
|
yes: 123,
|
|
no: 0,
|
|
});
|
|
|
|
metrics.destroy();
|
|
});
|
|
|
|
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
|
|
jest.useFakeTimers('modern');
|
|
const clientMetricsStore: any = new EventEmitter();
|
|
const appStoreSpy = jest.fn();
|
|
const bulkSpy = jest.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
const clientMetrics = new ClientMetricsService(
|
|
{
|
|
clientMetricsStore,
|
|
strategyStore: null,
|
|
featureToggleStore: null,
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: null,
|
|
},
|
|
{ getLogger },
|
|
);
|
|
const client1: IClientApp = {
|
|
appName: 'test_app',
|
|
instanceId: 'ava',
|
|
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');
|
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
|
jest.advanceTimersByTime(7000);
|
|
await flushPromises();
|
|
|
|
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
|
expect(bulkSpy).toHaveBeenCalledTimes(1);
|
|
|
|
const registrations = appStoreSpy.mock.calls[0][0];
|
|
|
|
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: any = new EventEmitter();
|
|
const appStoreSpy = jest.fn();
|
|
const bulkSpy = jest.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
|
|
const clientMetrics = new ClientMetricsService(
|
|
{
|
|
clientMetricsStore,
|
|
strategyStore: null,
|
|
featureToggleStore: null,
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: null,
|
|
},
|
|
{ getLogger },
|
|
);
|
|
const client1 = {
|
|
appName: 'test_app',
|
|
instanceId: 'client1',
|
|
strategies: [{ name: 'defaullt' }],
|
|
started: new Date(),
|
|
interval: 10,
|
|
};
|
|
const client2 = {
|
|
appName: 'test_app_2',
|
|
instanceId: 'client2',
|
|
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');
|
|
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(7000);
|
|
await flushPromises();
|
|
|
|
const registrations = appStoreSpy.mock.calls[0][0];
|
|
|
|
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: any = new EventEmitter();
|
|
const appStoreSpy = jest.fn();
|
|
const bulkSpy = jest.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
|
|
const bulkInterval = secondsToMilliseconds(2);
|
|
|
|
const clientMetrics = new ClientMetricsService(
|
|
{
|
|
clientMetricsStore,
|
|
strategyStore: null,
|
|
featureToggleStore: null,
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: null,
|
|
},
|
|
{ getLogger },
|
|
bulkInterval,
|
|
);
|
|
const client1 = {
|
|
appName: 'test_app',
|
|
instanceId: 'client1',
|
|
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(3000);
|
|
|
|
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(3000);
|
|
await flushPromises();
|
|
|
|
expect(appStoreSpy).toHaveBeenCalledTimes(2);
|
|
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: any = new EventEmitter();
|
|
const appStoreSpy = jest.fn();
|
|
const bulkSpy = jest.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
new ClientMetricsService(
|
|
{
|
|
clientMetricsStore,
|
|
strategyStore: null,
|
|
featureToggleStore: null,
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: null,
|
|
},
|
|
{ getLogger },
|
|
);
|
|
jest.advanceTimersByTime(6000);
|
|
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
|
expect(bulkSpy).toHaveBeenCalledTimes(0);
|
|
jest.useRealTimers();
|
|
});
|