unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
222 lines • 10.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("../../../types");
const schema_1 = require("../shared/schema");
const date_fns_1 = require("date-fns");
const events_1 = require("../../../types/events");
const api_user_1 = __importDefault(require("../../../types/api-user"));
const api_token_1 = require("../../../types/models/api-token");
const collapseHourlyMetrics_1 = require("./collapseHourlyMetrics");
const time_utils_1 = require("../../../util/time-utils");
const feature_schema_1 = require("../../../schema/feature-schema");
const memoizee_1 = __importDefault(require("memoizee"));
class ClientMetricsServiceV2 {
constructor({ clientMetricsStoreV2 }, config, lastSeenService) {
this.unsavedMetrics = [];
this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.lastSeenService = lastSeenService;
this.config = config;
this.logger = config.getLogger('/services/client-metrics/client-metrics-service-v2.ts');
this.flagResolver = config.flagResolver;
this.cachedFeatureNames = (0, memoizee_1.default)(async () => this.clientMetricsStoreV2.getFeatureFlagNames(), {
promise: true,
maxAge: (0, date_fns_1.secondsToMilliseconds)(10),
});
}
async clearMetrics(hoursAgo) {
return this.clientMetricsStoreV2.clearMetrics(hoursAgo);
}
async clearDailyMetrics(daysAgo) {
if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
return this.clientMetricsStoreV2.clearDailyMetrics(daysAgo);
}
}
async aggregateDailyMetrics() {
if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
const { enabledCount: hourlyEnabledCount, variantCount: hourlyVariantCount, } = await this.clientMetricsStoreV2.countPreviousDayHourlyMetricsBuckets();
const { enabledCount: dailyEnabledCount, variantCount: dailyVariantCount, } = await this.clientMetricsStoreV2.countPreviousDayMetricsBuckets();
const { payload } = this.flagResolver.getVariant('extendedUsageMetrics');
const limit = payload?.value &&
Number.isInteger(Number.parseInt(payload?.value))
? Number.parseInt(payload?.value)
: 3600000;
const totalHourlyCount = hourlyEnabledCount + hourlyVariantCount;
const totalDailyCount = dailyEnabledCount + dailyVariantCount;
const previousDayDailyCountCalculated = totalDailyCount > totalHourlyCount / 24; // heuristic
if (previousDayDailyCountCalculated) {
return;
}
if (totalHourlyCount > limit) {
this.logger.warn(`Skipping previous day metrics aggregation. Too many results. Expected max value: ${limit}, Actual value: ${totalHourlyCount}`);
return;
}
await this.clientMetricsStoreV2.aggregateDailyMetrics();
}
}
async filterExistingToggleNames(toggleNames) {
if (this.flagResolver.isEnabled('filterExistingFlagNames')) {
try {
const validNames = await this.cachedFeatureNames();
const existingNames = toggleNames.filter((name) => validNames.includes(name));
if (existingNames.length !== toggleNames.length) {
this.logger.info(`Filtered out ${toggleNames.length - existingNames.length} toggles with non-existing names`);
}
return this.filterValidToggleNames(existingNames);
}
catch (e) {
this.logger.error(e);
}
}
return this.filterValidToggleNames(toggleNames);
}
async filterValidToggleNames(toggleNames) {
const nameValidations = toggleNames.map((toggleName) => feature_schema_1.nameSchema.validateAsync({ name: toggleName }));
const badNames = (await Promise.allSettled(nameValidations)).filter((r) => r.status === 'rejected');
if (badNames.length > 0) {
this.logger.warn(`Got a few toggles with invalid names: ${JSON.stringify(badNames)}`);
if (this.flagResolver.isEnabled('filterInvalidClientMetrics')) {
const justNames = badNames.map((r) => r.reason._original.name);
return toggleNames.filter((name) => !justNames.includes(name));
}
}
return toggleNames;
}
async registerBulkMetrics(metrics) {
this.unsavedMetrics = (0, collapseHourlyMetrics_1.collapseHourlyMetrics)([
...this.unsavedMetrics,
...metrics,
]);
this.lastSeenService.updateLastSeen(metrics);
}
async registerClientMetrics(data, clientIp) {
const value = await schema_1.clientMetricsSchema.validateAsync(data);
const toggleNames = Object.keys(value.bucket.toggles).filter((name) => !(value.bucket.toggles[name].yes === 0 &&
value.bucket.toggles[name].no === 0));
const validatedToggleNames = await this.filterExistingToggleNames(toggleNames);
this.logger.debug(`Got ${toggleNames.length} (${validatedToggleNames.length} valid) metrics from ${clientIp}`);
if (data.sdkVersion) {
const [sdkName, sdkVersion] = data.sdkVersion.split(':');
const heartbeatEvent = {
sdkName,
sdkVersion,
metadata: {
platformName: data.platformName,
platformVersion: data.platformVersion,
yggdrasilVersion: data.yggdrasilVersion,
specVersion: data.specVersion,
},
};
this.config.eventBus.emit(events_1.CLIENT_REGISTER, heartbeatEvent);
}
if (validatedToggleNames.length > 0) {
const clientMetrics = validatedToggleNames.map((name) => ({
featureName: name,
appName: value.appName,
environment: value.environment ?? 'default',
timestamp: value.bucket.stop, //we might need to approximate between start/stop...
yes: value.bucket.toggles[name].yes ?? 0,
no: value.bucket.toggles[name].no ?? 0,
variants: value.bucket.toggles[name].variants,
}));
await this.registerBulkMetrics(clientMetrics);
this.config.eventBus.emit(events_1.CLIENT_METRICS, clientMetrics);
}
}
async bulkAdd() {
if (this.unsavedMetrics.length > 0) {
// Make a copy of `unsavedMetrics` in case new metrics
// arrive while awaiting `batchInsertMetrics`.
const copy = [...this.unsavedMetrics];
this.unsavedMetrics = [];
await this.clientMetricsStoreV2.batchInsertMetrics(copy);
this.config.eventBus.emit(types_1.CLIENT_METRICS_ADDED, copy);
}
}
// Overview over usage last "hour" bucket and all applications using the toggle
async getFeatureToggleMetricsSummary(featureName) {
const metrics = await this.clientMetricsStoreV2.getMetricsForFeatureToggle(featureName, 1);
const seenApplications = await this.clientMetricsStoreV2.getSeenAppsForFeatureToggle(featureName);
const groupedMetrics = metrics.reduce((prev, curr) => {
if (prev[curr.environment]) {
prev[curr.environment].yes += curr.yes;
prev[curr.environment].no += curr.no;
}
else {
prev[curr.environment] = {
environment: curr.environment,
timestamp: curr.timestamp,
yes: curr.yes,
no: curr.no,
};
}
return prev;
}, {});
return {
featureName,
lastHourUsage: Object.values(groupedMetrics),
seenApplications,
};
}
async getClientMetricsForToggle(featureName, hoursBack = 24) {
let hours;
let metrics;
if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
// if we're in the daily range we need to add one more day
const normalizedHoursBack = hoursBack > 48 ? hoursBack + 24 : hoursBack;
metrics =
await this.clientMetricsStoreV2.getMetricsForFeatureToggleV2(featureName, normalizedHoursBack);
hours =
hoursBack > 48
? (0, time_utils_1.generateDayBuckets)(Math.floor(hoursBack / 24))
: (0, time_utils_1.generateHourBuckets)(hoursBack);
}
else {
metrics =
await this.clientMetricsStoreV2.getMetricsForFeatureToggle(featureName, hoursBack);
hours = (0, time_utils_1.generateHourBuckets)(hoursBack);
}
const environments = [...new Set(metrics.map((x) => x.environment))];
const applications = [...new Set(metrics.map((x) => x.appName))].slice(0, 100);
const result = environments.flatMap((environment) => {
const environmentMetrics = metrics.filter((metric) => metric.environment === environment);
return applications.flatMap((appName) => {
const applicationMetrics = environmentMetrics.filter((metric) => metric.appName === appName);
return hours.flatMap((hourBucket) => {
const metric = applicationMetrics.find((item) => (0, date_fns_1.compareAsc)(hourBucket.timestamp, item.timestamp) ===
0);
return (metric || {
timestamp: hourBucket.timestamp,
no: 0,
yes: 0,
appName,
environment,
featureName,
});
});
});
});
return result.sort((a, b) => (0, date_fns_1.compareAsc)(a.timestamp, b.timestamp));
}
resolveMetricsEnvironment(user, data) {
if (user instanceof api_user_1.default) {
if (user.environment !== api_token_1.ALL) {
return user.environment;
}
else if (user.environment === api_token_1.ALL && data.environment) {
return data.environment;
}
}
return 'default';
}
resolveUserEnvironment(user) {
if (user instanceof api_user_1.default && user.environment !== api_token_1.ALL) {
return user.environment;
}
return 'default';
}
}
exports.default = ClientMetricsServiceV2;
//# sourceMappingURL=metrics-service-v2.js.map