unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
255 lines • 8.76 kB
JavaScript
import ClientMetricsServiceV2 from './metrics-service-v2.js';
import getLogger from '../../../../test/fixtures/no-logger.js';
import createStores from '../../../../test/fixtures/store.js';
import EventEmitter from 'events';
import { LastSeenService } from '../last-seen/last-seen-service.js';
import { endOfDay, startOfHour, subDays, subHours } from 'date-fns';
import { UnknownFlagsService } from '../unknown-flags/unknown-flags-service.js';
import { vi } from 'vitest';
function initClientMetrics() {
const stores = createStores();
const eventBus = new EventEmitter();
eventBus.emit = vi.fn(() => true);
const config = {
eventBus,
getLogger,
flagResolver: {
isEnabled: () => true,
},
};
const lastSeenService = new LastSeenService({
lastSeenStore: stores.lastSeenStore,
}, config);
lastSeenService.updateLastSeen = vi.fn();
const unknownFlagsService = new UnknownFlagsService({
unknownFlagsStore: stores.unknownFlagsStore,
}, config);
const service = new ClientMetricsServiceV2(stores, config, lastSeenService, unknownFlagsService);
return { clientMetricsService: service, eventBus, lastSeenService, stores };
}
test('process metrics properly', async () => {
const { clientMetricsService, eventBus, lastSeenService, stores } = initClientMetrics();
stores.clientMetricsStoreV2.getFeatureFlagNames = vi
.fn()
.mockResolvedValue(['myCoolToggle', 'myOtherToggle']);
await clientMetricsService.registerClientMetrics({
appName: 'test',
bucket: {
start: '1982-07-25T12:00:00.000Z',
stop: '2023-07-25T12:00:00.000Z',
toggles: {
myCoolToggle: {
yes: 25,
no: 42,
variants: {
blue: 6,
green: 15,
red: 46,
},
},
myOtherToggle: {
yes: 0,
no: 100,
},
},
},
environment: 'test',
}, '127.0.0.1');
expect(eventBus.emit).toHaveBeenCalledTimes(1);
expect(lastSeenService.updateLastSeen).toHaveBeenCalledTimes(1);
});
test('process metrics properly even when some names are not url friendly, filtering out invalid names when flag is on', async () => {
const { clientMetricsService, eventBus, lastSeenService } = initClientMetrics();
await clientMetricsService.registerClientMetrics({
appName: 'test',
bucket: {
start: '1982-07-25T12:00:00.000Z',
stop: '2023-07-25T12:00:00.000Z',
toggles: {
'not url friendly ☹': {
yes: 0,
no: 100,
},
},
},
environment: 'test',
}, '127.0.0.1');
// only toggle with a bad name gets filtered out
expect(eventBus.emit).not.toHaveBeenCalled();
expect(lastSeenService.updateLastSeen).not.toHaveBeenCalled();
});
test('get daily client metrics for a toggle', async () => {
const yesterday = subDays(new Date(), 1);
const twoDaysAgo = subDays(new Date(), 2);
const threeDaysAgo = subDays(new Date(), 3);
const baseData = {
featureName: 'feature',
appName: 'test',
environment: 'development',
yes: 0,
no: 0,
};
const clientMetricsStoreV2 = {
getMetricsForFeatureToggleV2(featureName, hoursBack) {
return Promise.resolve([
{
...baseData,
timestamp: endOfDay(yesterday),
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
},
};
const config = {
flagResolver: {
isEnabled() {
return true;
},
},
getLogger() { },
};
const lastSeenService = {};
const unknownFlagsService = {};
const service = new ClientMetricsServiceV2({ clientMetricsStoreV2 }, config, lastSeenService, unknownFlagsService);
const metrics = await service.getClientMetricsForToggle('feature', 3 * 24);
expect(metrics).toMatchObject([
{ ...baseData, timestamp: endOfDay(threeDaysAgo) },
{ ...baseData, timestamp: endOfDay(twoDaysAgo) },
{
...baseData,
timestamp: endOfDay(yesterday),
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
});
test('get hourly client metrics for a toggle', async () => {
const hourAgo = startOfHour(subHours(new Date(), 1));
const thisHour = startOfHour(new Date());
const baseData = {
featureName: 'feature',
appName: 'test',
environment: 'development',
yes: 0,
no: 0,
};
const clientMetricsStoreV2 = {
getMetricsForFeatureToggleV2(featureName, hoursBack) {
return Promise.resolve([
{
...baseData,
timestamp: thisHour,
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
},
};
const config = {
flagResolver: {
isEnabled() {
return true;
},
},
getLogger() { },
};
const lastSeenService = {};
const unknownFlagsService = {};
const service = new ClientMetricsServiceV2({ clientMetricsStoreV2 }, config, lastSeenService, unknownFlagsService);
const metrics = await service.getClientMetricsForToggle('feature', 2);
expect(metrics).toMatchObject([
{ ...baseData, timestamp: hourAgo },
{
...baseData,
timestamp: thisHour,
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
});
const setupMetricsService = ({ enabledCount, variantCount, enabledDailyCount, variantDailyCount, limit, }) => {
let aggregationCalled = false;
let recordedWarning = '';
const clientMetricsStoreV2 = {
aggregateDailyMetrics() {
aggregationCalled = true;
return Promise.resolve();
},
countPreviousDayHourlyMetricsBuckets() {
return { enabledCount, variantCount };
},
countPreviousDayMetricsBuckets() {
return {
enabledCount: enabledDailyCount,
variantCount: variantDailyCount,
};
},
};
const config = {
flagResolver: {
isEnabled() {
return true;
},
getVariant() {
return { payload: { value: limit } };
},
},
getLogger() {
return {
warn(message) {
recordedWarning = message;
},
};
},
};
const lastSeenService = {};
const unknownFlagsService = {};
const service = new ClientMetricsServiceV2({ clientMetricsStoreV2 }, config, lastSeenService, unknownFlagsService);
return {
service,
aggregationCalled: () => aggregationCalled,
recordedWarning: () => recordedWarning,
};
};
test('do not aggregate previous day metrics when metrics already calculated', async () => {
const { service, recordedWarning, aggregationCalled } = setupMetricsService({
enabledCount: 2,
variantCount: 4,
enabledDailyCount: 2,
variantDailyCount: 4,
limit: 6,
});
await service.aggregateDailyMetrics();
expect(recordedWarning()).toBe('');
expect(aggregationCalled()).toBe(false);
});
test('do not aggregate previous day metrics when metrics count is below limit', async () => {
const { service, recordedWarning, aggregationCalled } = setupMetricsService({
enabledCount: 2,
variantCount: 4,
enabledDailyCount: 0,
variantDailyCount: 0,
limit: 5,
});
await service.aggregateDailyMetrics();
expect(recordedWarning()).toBe('Skipping previous day metrics aggregation. Too many results. Expected max value: 5, Actual value: 6');
expect(aggregationCalled()).toBe(false);
});
test('aggregate previous day metrics', async () => {
const { service, recordedWarning, aggregationCalled } = setupMetricsService({
enabledCount: 2,
variantCount: 4,
enabledDailyCount: 0,
variantDailyCount: 0,
limit: 6,
});
await service.aggregateDailyMetrics();
expect(recordedWarning()).toBe('');
expect(aggregationCalled()).toBe(true);
});
//# sourceMappingURL=metrics-service-v2.test.js.map