diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 66fdb2f0e5..2685a259ec 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -23,6 +23,7 @@ import ResetTokenService from './reset-token-service'; import SettingService from './setting-service'; import SessionService from './session-service'; import UserFeedbackService from './user-feedback-service'; +import ProjectHealthService from './project-health-service'; export const createServices = ( stores: IUnleashStores, @@ -54,6 +55,7 @@ export const createServices = ( const healthService = new HealthService(stores, config); const settingService = new SettingService(stores, config); const userFeedbackService = new UserFeedbackService(stores, config); + const projectHealthService = new ProjectHealthService(stores, config); return { accessService, @@ -77,6 +79,7 @@ export const createServices = ( settingService, sessionService, userFeedbackService, + projectHealthService, }; }; diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts new file mode 100644 index 0000000000..687a109aaa --- /dev/null +++ b/src/lib/services/project-health-service.ts @@ -0,0 +1,149 @@ +import { IUnleashStores } from '../types/stores'; +import { IUnleashConfig } from '../types/option'; +import ProjectStore, { IProject } from '../db/project-store'; +import { Logger } from '../logger'; +import { + FeatureToggle, + IFeatureOverview, + IProjectHealthReport, + IProjectOverview, +} from '../types/model'; +import { + MILLISECONDS_IN_DAY, + MILLISECONDS_IN_ONE_HOUR, +} from '../util/constants'; +import FeatureTypeStore from '../db/feature-type-store'; +import Timer = NodeJS.Timer; +import FeatureToggleStore from '../db/feature-toggle-store'; + +export default class ProjectHealthService { + private logger: Logger; + + private projectStore: ProjectStore; + + private featureTypeStore: FeatureTypeStore; + + private featureToggleStore: FeatureToggleStore; + + private featureTypes: Map; + + private healthRatingTimer: Timer; + + constructor( + { + projectStore, + featureTypeStore, + featureToggleStore, + }: Pick< + IUnleashStores, + 'projectStore' | 'featureTypeStore' | 'featureToggleStore' + >, + { getLogger }: Pick, + ) { + 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(), + MILLISECONDS_IN_ONE_HOUR, + ).unref(); + } + + async getProjectHealthReport( + projectId: string, + ): Promise { + //const overview = await this.getProjectOverview(projectId, false); + const features = await this.featureToggleStore.getFeatures(); + return { + // ...overview, + potentiallyStaleCount: await this.potentiallyStaleCount(features), + activeCount: this.activeCount(features), + staleCount: this.staleCount(features), + }; + } + + private async potentiallyStaleCount( + features: Pick[], + ): Promise { + 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 * MILLISECONDS_IN_DAY + ); + }).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 { + const toggles = await this.featureToggleStore.getFeaturesBy({ + project: project.id, + }); + + const activeToggles = toggles.filter(feature => !feature.stale); + 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 { + const projects = await this.projectStore.getAll(); + + await Promise.all( + projects.map(async project => { + const newHealth = await this.calculateHealthRating(project); + await this.projectStore.updateHealth({ + id: project.id, + health: newHealth, + }); + }), + ); + } + + destroy(): void { + clearInterval(this.healthRatingTimer); + } +} diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 1195034759..666672ba78 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -19,6 +19,7 @@ import HealthService from '../services/health-service'; import SettingService from '../services/setting-service'; import SessionService from '../services/session-service'; import UserFeedbackService from '../services/user-feedback-service'; +import ProjectHealthService from '../services/project-health-service'; export interface IUnleashServices { accessService: AccessService; @@ -42,4 +43,5 @@ export interface IUnleashServices { userService: UserService; versionService: VersionService; userFeedbackService: UserFeedbackService; + projectHealthService: ProjectHealthService; }