2021-07-07 10:46:50 +02:00
|
|
|
import { IUnleashStores } from '../types/stores';
|
|
|
|
import { IUnleashConfig } from '../types/option';
|
|
|
|
import { Logger } from '../logger';
|
|
|
|
import {
|
|
|
|
FeatureToggle,
|
|
|
|
IFeatureOverview,
|
2021-08-19 13:25:36 +02:00
|
|
|
IProject,
|
2021-07-07 10:46:50 +02:00
|
|
|
IProjectHealthReport,
|
|
|
|
IProjectOverview,
|
|
|
|
} from '../types/model';
|
2021-08-12 15:04:37 +02:00
|
|
|
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
|
|
|
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
2021-08-19 13:25:36 +02:00
|
|
|
import { IProjectStore } from '../types/stores/project-store';
|
2021-11-12 13:15:51 +01:00
|
|
|
import FeatureToggleService from './feature-toggle-service';
|
2021-11-02 15:13:46 +01:00
|
|
|
import { hoursToMilliseconds } from 'date-fns';
|
|
|
|
import Timer = NodeJS.Timer;
|
2022-11-30 12:41:53 +01:00
|
|
|
import { FavoritesService } from './favorites-service';
|
2021-07-07 10:46:50 +02:00
|
|
|
|
|
|
|
export default class ProjectHealthService {
|
|
|
|
private logger: Logger;
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
private projectStore: IProjectStore;
|
2021-07-07 10:46:50 +02:00
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
private featureTypeStore: IFeatureTypeStore;
|
2021-07-07 10:46:50 +02:00
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
private featureToggleStore: IFeatureToggleStore;
|
2021-07-07 10:46:50 +02:00
|
|
|
|
|
|
|
private featureTypes: Map<string, number>;
|
|
|
|
|
|
|
|
private healthRatingTimer: Timer;
|
|
|
|
|
2021-11-12 13:15:51 +01:00
|
|
|
private featureToggleService: FeatureToggleService;
|
2021-09-13 10:23:57 +02:00
|
|
|
|
2022-11-30 12:41:53 +01:00
|
|
|
private favoritesService: FavoritesService;
|
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
constructor(
|
|
|
|
{
|
|
|
|
projectStore,
|
|
|
|
featureTypeStore,
|
|
|
|
featureToggleStore,
|
|
|
|
}: Pick<
|
2021-08-12 15:04:37 +02:00
|
|
|
IUnleashStores,
|
|
|
|
'projectStore' | 'featureTypeStore' | 'featureToggleStore'
|
2021-07-07 10:46:50 +02:00
|
|
|
>,
|
|
|
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
2021-11-12 13:15:51 +01:00
|
|
|
featureToggleService: FeatureToggleService,
|
2022-11-30 12:41:53 +01:00
|
|
|
favoritesService: FavoritesService,
|
2021-07-07 10:46:50 +02:00
|
|
|
) {
|
|
|
|
this.logger = getLogger('services/project-health-service.ts');
|
|
|
|
this.projectStore = projectStore;
|
|
|
|
this.featureTypeStore = featureTypeStore;
|
|
|
|
this.featureToggleStore = featureToggleStore;
|
|
|
|
this.featureTypes = new Map();
|
|
|
|
this.healthRatingTimer = setInterval(
|
|
|
|
() => this.setHealthRating(),
|
2021-11-02 15:13:46 +01:00
|
|
|
hoursToMilliseconds(1),
|
2021-07-07 10:46:50 +02:00
|
|
|
).unref();
|
2021-09-13 10:23:57 +02:00
|
|
|
this.featureToggleService = featureToggleService;
|
2022-11-30 12:41:53 +01:00
|
|
|
this.favoritesService = favoritesService;
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
2021-09-13 10:23:57 +02:00
|
|
|
// TODO: duplicate from project-service.
|
2021-07-07 10:46:50 +02:00
|
|
|
async getProjectOverview(
|
|
|
|
projectId: string,
|
|
|
|
archived: boolean = false,
|
2022-11-30 12:41:53 +01:00
|
|
|
userId?: number,
|
2021-07-07 10:46:50 +02:00
|
|
|
): Promise<IProjectOverview> {
|
|
|
|
const project = await this.projectStore.get(projectId);
|
2021-09-29 12:22:04 +02:00
|
|
|
const environments = await this.projectStore.getEnvironmentsForProject(
|
|
|
|
projectId,
|
|
|
|
);
|
2022-11-29 16:06:08 +01:00
|
|
|
const features = await this.featureToggleService.getFeatureOverview({
|
2021-07-07 10:46:50 +02:00
|
|
|
projectId,
|
|
|
|
archived,
|
2022-12-01 08:49:49 +01:00
|
|
|
userId,
|
2022-11-29 16:06:08 +01:00
|
|
|
});
|
2022-08-17 11:05:41 +02:00
|
|
|
const members = await this.projectStore.getMembersCountByProject(
|
|
|
|
projectId,
|
|
|
|
);
|
2022-11-30 12:41:53 +01:00
|
|
|
|
2022-11-30 13:26:17 +01:00
|
|
|
const favorite = await this.favoritesService.isFavoriteProject({
|
|
|
|
project: projectId,
|
2022-11-30 12:41:53 +01:00
|
|
|
userId,
|
2022-11-30 13:26:17 +01:00
|
|
|
});
|
2021-07-07 10:46:50 +02:00
|
|
|
return {
|
|
|
|
name: project.name,
|
|
|
|
description: project.description,
|
|
|
|
health: project.health,
|
2022-11-30 12:41:53 +01:00
|
|
|
favorite: favorite,
|
2021-11-30 15:25:52 +01:00
|
|
|
updatedAt: project.updatedAt,
|
2021-09-29 12:22:04 +02:00
|
|
|
environments,
|
2021-07-07 10:46:50 +02:00
|
|
|
features,
|
|
|
|
members,
|
|
|
|
version: 1,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProjectHealthReport(
|
|
|
|
projectId: string,
|
|
|
|
): Promise<IProjectHealthReport> {
|
2022-11-30 12:41:53 +01:00
|
|
|
const overview = await this.getProjectOverview(
|
|
|
|
projectId,
|
|
|
|
false,
|
|
|
|
undefined,
|
|
|
|
);
|
2021-07-07 10:46:50 +02:00
|
|
|
return {
|
|
|
|
...overview,
|
|
|
|
potentiallyStaleCount: await this.potentiallyStaleCount(
|
|
|
|
overview.features,
|
|
|
|
),
|
|
|
|
activeCount: this.activeCount(overview.features),
|
|
|
|
staleCount: this.staleCount(overview.features),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private async potentiallyStaleCount(
|
|
|
|
features: Pick<FeatureToggle, 'createdAt' | 'stale' | 'type'>[],
|
|
|
|
): Promise<number> {
|
|
|
|
const today = new Date().valueOf();
|
|
|
|
if (this.featureTypes.size === 0) {
|
|
|
|
const types = await this.featureTypeStore.getAll();
|
2021-08-12 15:04:37 +02:00
|
|
|
types.forEach((type) => {
|
2021-07-07 10:46:50 +02:00
|
|
|
this.featureTypes.set(
|
|
|
|
type.name.toLowerCase(),
|
|
|
|
type.lifetimeDays,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
2021-08-12 15:04:37 +02:00
|
|
|
return features.filter((feature) => {
|
2021-07-07 10:46:50 +02:00
|
|
|
const diff = today - feature.createdAt.valueOf();
|
|
|
|
const featureTypeExpectedLifetime = this.featureTypes.get(
|
|
|
|
feature.type,
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
!feature.stale &&
|
2021-11-02 15:13:46 +01:00
|
|
|
diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24)
|
2021-07-07 10:46:50 +02:00
|
|
|
);
|
|
|
|
}).length;
|
|
|
|
}
|
|
|
|
|
|
|
|
private activeCount(features: IFeatureOverview[]): number {
|
2021-08-12 15:04:37 +02:00
|
|
|
return features.filter((f) => !f.stale).length;
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private staleCount(features: IFeatureOverview[]): number {
|
2021-08-12 15:04:37 +02:00
|
|
|
return features.filter((f) => f.stale).length;
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async calculateHealthRating(project: IProject): Promise<number> {
|
2021-09-13 10:23:57 +02:00
|
|
|
const toggles = await this.featureToggleStore.getAll({
|
2021-07-07 10:46:50 +02:00
|
|
|
project: project.id,
|
2021-11-25 10:09:23 +01:00
|
|
|
archived: false,
|
2021-07-07 10:46:50 +02:00
|
|
|
});
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
const activeToggles = toggles.filter((feature) => !feature.stale);
|
2021-07-07 10:46:50 +02:00
|
|
|
const staleToggles = toggles.length - activeToggles.length;
|
|
|
|
const potentiallyStaleToggles = await this.potentiallyStaleCount(
|
|
|
|
activeToggles,
|
|
|
|
);
|
|
|
|
return this.getHealthRating(
|
|
|
|
toggles.length,
|
|
|
|
staleToggles,
|
|
|
|
potentiallyStaleToggles,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getHealthRating(
|
|
|
|
toggleCount: number,
|
|
|
|
staleToggleCount: number,
|
|
|
|
potentiallyStaleCount: number,
|
|
|
|
): number {
|
|
|
|
const startPercentage = 100;
|
|
|
|
const stalePercentage = (staleToggleCount / toggleCount) * 100 || 0;
|
|
|
|
const potentiallyStalePercentage =
|
|
|
|
(potentiallyStaleCount / toggleCount) * 100 || 0;
|
|
|
|
const rating = Math.round(
|
|
|
|
startPercentage - stalePercentage - potentiallyStalePercentage,
|
|
|
|
);
|
|
|
|
return rating;
|
|
|
|
}
|
|
|
|
|
|
|
|
async setHealthRating(): Promise<void> {
|
|
|
|
const projects = await this.projectStore.getAll();
|
|
|
|
|
|
|
|
await Promise.all(
|
2021-08-12 15:04:37 +02:00
|
|
|
projects.map(async (project) => {
|
2021-07-07 10:46:50 +02:00
|
|
|
const newHealth = await this.calculateHealthRating(project);
|
|
|
|
await this.projectStore.updateHealth({
|
|
|
|
id: project.id,
|
|
|
|
health: newHealth,
|
|
|
|
});
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy(): void {
|
|
|
|
clearInterval(this.healthRatingTimer);
|
|
|
|
}
|
|
|
|
}
|