mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
refactor: project health domain (#3024)
This commit is contained in:
parent
50e0c0818a
commit
59e8e2b1ed
@ -0,0 +1,65 @@
|
|||||||
|
import { hoursToMilliseconds } from 'date-fns';
|
||||||
|
import type { IProjectHealthReport } from 'lib/types';
|
||||||
|
import type { IFeatureType } from 'lib/types/stores/feature-type-store';
|
||||||
|
|
||||||
|
const getPotentiallyStaleCount = (
|
||||||
|
features: Array<{
|
||||||
|
stale?: boolean;
|
||||||
|
createdAt?: Date;
|
||||||
|
type?: string;
|
||||||
|
}>,
|
||||||
|
featureTypes: IFeatureType[],
|
||||||
|
) => {
|
||||||
|
const today = new Date().valueOf();
|
||||||
|
|
||||||
|
return features.filter((feature) => {
|
||||||
|
const diff = today - feature.createdAt?.valueOf();
|
||||||
|
const featureTypeExpectedLifetime = featureTypes.find(
|
||||||
|
(t) => t.id === feature.type,
|
||||||
|
)?.lifetimeDays;
|
||||||
|
|
||||||
|
return (
|
||||||
|
!feature.stale &&
|
||||||
|
diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24)
|
||||||
|
);
|
||||||
|
}).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateProjectHealth = (
|
||||||
|
features: Array<{
|
||||||
|
stale?: boolean;
|
||||||
|
createdAt?: Date;
|
||||||
|
type?: string;
|
||||||
|
}>,
|
||||||
|
featureTypes: IFeatureType[],
|
||||||
|
): Pick<
|
||||||
|
IProjectHealthReport,
|
||||||
|
'staleCount' | 'potentiallyStaleCount' | 'activeCount'
|
||||||
|
> => ({
|
||||||
|
potentiallyStaleCount: getPotentiallyStaleCount(features, featureTypes),
|
||||||
|
activeCount: features.filter((f) => !f.stale).length,
|
||||||
|
staleCount: features.filter((f) => f.stale).length,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getHealthRating = (
|
||||||
|
features: Array<{
|
||||||
|
stale?: boolean;
|
||||||
|
createdAt?: Date;
|
||||||
|
type?: string;
|
||||||
|
}>,
|
||||||
|
featureTypes: IFeatureType[],
|
||||||
|
): number => {
|
||||||
|
const { potentiallyStaleCount, activeCount, staleCount } =
|
||||||
|
calculateProjectHealth(features, featureTypes);
|
||||||
|
const toggleCount = activeCount + staleCount;
|
||||||
|
|
||||||
|
const startPercentage = 100;
|
||||||
|
const stalePercentage = (staleCount / toggleCount) * 100 || 0;
|
||||||
|
const potentiallyStalePercentage =
|
||||||
|
(potentiallyStaleCount / toggleCount) * 100 || 0;
|
||||||
|
const rating = Math.round(
|
||||||
|
startPercentage - stalePercentage - potentiallyStalePercentage,
|
||||||
|
);
|
||||||
|
|
||||||
|
return rating;
|
||||||
|
};
|
@ -1,18 +1,20 @@
|
|||||||
import { IUnleashStores } from '../types/stores';
|
import { IUnleashStores } from '../types/stores';
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import {
|
import type { IProject, IProjectHealthReport } from '../types/model';
|
||||||
FeatureToggle,
|
import type { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||||
IFeatureOverview,
|
import type {
|
||||||
IProject,
|
IFeatureType,
|
||||||
IProjectHealthReport,
|
IFeatureTypeStore,
|
||||||
} from '../types/model';
|
} from '../types/stores/feature-type-store';
|
||||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
import type { IProjectStore } from '../types/stores/project-store';
|
||||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
|
||||||
import { IProjectStore } from '../types/stores/project-store';
|
|
||||||
import { hoursToMilliseconds } from 'date-fns';
|
import { hoursToMilliseconds } from 'date-fns';
|
||||||
import Timer = NodeJS.Timer;
|
import Timer = NodeJS.Timer;
|
||||||
import ProjectService from './project-service';
|
import ProjectService from './project-service';
|
||||||
|
import {
|
||||||
|
calculateProjectHealth,
|
||||||
|
getHealthRating,
|
||||||
|
} from '../domain/calculate-project-health/calculate-project-health';
|
||||||
|
|
||||||
export default class ProjectHealthService {
|
export default class ProjectHealthService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -23,7 +25,7 @@ export default class ProjectHealthService {
|
|||||||
|
|
||||||
private featureToggleStore: IFeatureToggleStore;
|
private featureToggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
private featureTypes: Map<string, number>;
|
private featureTypes: IFeatureType[];
|
||||||
|
|
||||||
private healthRatingTimer: Timer;
|
private healthRatingTimer: Timer;
|
||||||
|
|
||||||
@ -45,7 +47,7 @@ export default class ProjectHealthService {
|
|||||||
this.projectStore = projectStore;
|
this.projectStore = projectStore;
|
||||||
this.featureTypeStore = featureTypeStore;
|
this.featureTypeStore = featureTypeStore;
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.featureTypes = new Map();
|
this.featureTypes = [];
|
||||||
this.healthRatingTimer = setInterval(
|
this.healthRatingTimer = setInterval(
|
||||||
() => this.setHealthRating(),
|
() => this.setHealthRating(),
|
||||||
hoursToMilliseconds(1),
|
hoursToMilliseconds(1),
|
||||||
@ -57,85 +59,38 @@ export default class ProjectHealthService {
|
|||||||
async getProjectHealthReport(
|
async getProjectHealthReport(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<IProjectHealthReport> {
|
): Promise<IProjectHealthReport> {
|
||||||
|
if (this.featureTypes.length === 0) {
|
||||||
|
this.featureTypes = await this.featureTypeStore.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
const overview = await this.projectService.getProjectOverview(
|
const overview = await this.projectService.getProjectOverview(
|
||||||
projectId,
|
projectId,
|
||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const healthRating = calculateProjectHealth(
|
||||||
|
overview.features,
|
||||||
|
this.featureTypes,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...overview,
|
...overview,
|
||||||
potentiallyStaleCount: await this.potentiallyStaleCount(
|
...healthRating,
|
||||||
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();
|
|
||||||
types.forEach((type) => {
|
|
||||||
this.featureTypes.set(
|
|
||||||
type.name.toLowerCase(),
|
|
||||||
type.lifetimeDays,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return features.filter((feature) => {
|
|
||||||
const diff = today - feature.createdAt.valueOf();
|
|
||||||
const featureTypeExpectedLifetime = this.featureTypes.get(
|
|
||||||
feature.type,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
!feature.stale &&
|
|
||||||
diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24)
|
|
||||||
);
|
|
||||||
}).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private activeCount(features: IFeatureOverview[]): number {
|
|
||||||
return features.filter((f) => !f.stale).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private staleCount(features: IFeatureOverview[]): number {
|
|
||||||
return features.filter((f) => f.stale).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async calculateHealthRating(project: IProject): Promise<number> {
|
async calculateHealthRating(project: IProject): Promise<number> {
|
||||||
|
if (this.featureTypes.length === 0) {
|
||||||
|
this.featureTypes = await this.featureTypeStore.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
const toggles = await this.featureToggleStore.getAll({
|
const toggles = await this.featureToggleStore.getAll({
|
||||||
project: project.id,
|
project: project.id,
|
||||||
archived: false,
|
archived: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeToggles = toggles.filter((feature) => !feature.stale);
|
return getHealthRating(toggles, this.featureTypes);
|
||||||
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> {
|
async setHealthRating(): Promise<void> {
|
||||||
|
Loading…
Reference in New Issue
Block a user