1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

feat: read change requests for insights (#6651)

This commit is contained in:
Mateusz Kwasniewski 2024-03-21 09:08:19 +01:00 committed by GitHub
parent fb88048acf
commit 8080a1d907
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 130 additions and 75 deletions

View File

@ -12,6 +12,8 @@ import FeatureTypeStore from '../../db/feature-type-store';
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store'; import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
import { ProjectInsightsService } from './project-insights-service'; import { ProjectInsightsService } from './project-insights-service';
import ProjectStore from '../project/project-store'; import ProjectStore from '../project/project-store';
import { ProjectInsightsReadModel } from './project-insights-read-model';
import { FakeProjectInsightsReadModel } from './fake-project-insights-read-model';
export const createProjectInsightsService = ( export const createProjectInsightsService = (
db: Db, db: Db,
@ -34,6 +36,7 @@ export const createProjectInsightsService = (
const featureTypeStore = new FeatureTypeStore(db, getLogger); const featureTypeStore = new FeatureTypeStore(db, getLogger);
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger); const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
const featureToggleService = createFeatureToggleService(db, config); const featureToggleService = createFeatureToggleService(db, config);
const projectInsightsReadModel = new ProjectInsightsReadModel(db);
return new ProjectInsightsService( return new ProjectInsightsService(
{ {
@ -43,6 +46,7 @@ export const createProjectInsightsService = (
projectStatsStore, projectStatsStore,
}, },
featureToggleService, featureToggleService,
projectInsightsReadModel,
); );
}; };
@ -54,6 +58,7 @@ export const createFakeProjectInsightsService = (
const featureTypeStore = new FakeFeatureTypeStore(); const featureTypeStore = new FakeFeatureTypeStore();
const projectStatsStore = new FakeProjectStatsStore(); const projectStatsStore = new FakeProjectStatsStore();
const featureToggleService = createFakeFeatureToggleService(config); const featureToggleService = createFakeFeatureToggleService(config);
const projectInsightsReadModel = new FakeProjectInsightsReadModel();
return new ProjectInsightsService( return new ProjectInsightsService(
{ {
@ -63,5 +68,6 @@ export const createFakeProjectInsightsService = (
projectStatsStore, projectStatsStore,
}, },
featureToggleService, featureToggleService,
projectInsightsReadModel,
); );
}; };

View File

@ -0,0 +1,19 @@
import type {
ChangeRequestCounts,
IProjectInsightsReadModel,
} from './project-insights-read-model-type';
const changeRequestCounts: ChangeRequestCounts = {
total: 0,
approved: 0,
applied: 0,
rejected: 0,
reviewRequired: 10,
scheduled: 0,
};
export class FakeProjectInsightsReadModel implements IProjectInsightsReadModel {
async getChangeRequests(projectId: string): Promise<ChangeRequestCounts> {
return changeRequestCounts;
}
}

View File

@ -0,0 +1,12 @@
export type ChangeRequestCounts = {
total: number;
approved: number;
applied: number;
rejected: number;
reviewRequired: number;
scheduled: number;
};
export interface IProjectInsightsReadModel {
getChangeRequests(projectId: string): Promise<ChangeRequestCounts>;
}

View File

@ -0,0 +1,59 @@
import type {
ChangeRequestCounts,
IProjectInsightsReadModel,
} from './project-insights-read-model-type';
import type { Db } from '../../db/db';
export type ChangeRequestDBState =
| 'Approved'
| 'In review'
| 'Applied'
| 'Scheduled'
| 'Rejected';
export class ProjectInsightsReadModel implements IProjectInsightsReadModel {
private db: Db;
constructor(db: Db) {
this.db = db;
}
async getChangeRequests(projectId: string): Promise<ChangeRequestCounts> {
const changeRequestCounts: ChangeRequestCounts = {
total: 0,
approved: 0,
applied: 0,
rejected: 0,
reviewRequired: 0,
scheduled: 0,
};
const rows: Array<{ state: ChangeRequestDBState; count: string }> =
await this.db('change_requests')
.select('state')
.count('* as count')
.where('project', '=', projectId)
.groupBy('state');
return rows.reduce((acc, current) => {
if (current.state === 'Applied') {
acc.applied = Number(current.count);
acc.total += Number(current.count);
} else if (current.state === 'Approved') {
acc.approved = Number(current.count);
acc.total += Number(current.count);
} else if (current.state === 'Rejected') {
acc.rejected = Number(current.count);
acc.total += Number(current.count);
} else if (current.state === 'In review') {
acc.reviewRequired = Number(current.count);
acc.total += Number(current.count);
} else if (current.state === 'Scheduled') {
acc.scheduled = Number(current.count);
acc.total += Number(current.count);
}
return acc;
}, changeRequestCounts);
}
}

View File

@ -10,6 +10,7 @@ import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type'; import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type';
import type { ProjectDoraMetricsSchema } from '../../openapi'; import type { ProjectDoraMetricsSchema } from '../../openapi';
import { calculateProjectHealth } from '../../domain/project-health/project-health'; import { calculateProjectHealth } from '../../domain/project-health/project-health';
import type { IProjectInsightsReadModel } from './project-insights-read-model-type';
export class ProjectInsightsService { export class ProjectInsightsService {
private projectStore: IProjectStore; private projectStore: IProjectStore;
@ -22,6 +23,8 @@ export class ProjectInsightsService {
private projectStatsStore: IProjectStatsStore; private projectStatsStore: IProjectStatsStore;
private projectInsightsReadModel: IProjectInsightsReadModel;
constructor( constructor(
{ {
projectStore, projectStore,
@ -36,12 +39,14 @@ export class ProjectInsightsService {
| 'featureTypeStore' | 'featureTypeStore'
>, >,
featureToggleService: FeatureToggleService, featureToggleService: FeatureToggleService,
projectInsightsReadModel: IProjectInsightsReadModel,
) { ) {
this.projectStore = projectStore; this.projectStore = projectStore;
this.featureToggleStore = featureToggleStore; this.featureToggleStore = featureToggleStore;
this.featureTypeStore = featureTypeStore; this.featureTypeStore = featureTypeStore;
this.featureToggleService = featureToggleService; this.featureToggleService = featureToggleService;
this.projectStatsStore = projectStatsStore; this.projectStatsStore = projectStatsStore;
this.projectInsightsReadModel = projectInsightsReadModel;
} }
private async getDoraMetrics( private async getDoraMetrics(
@ -103,27 +108,28 @@ export class ProjectInsightsService {
inactive: 3, inactive: 3,
totalPreviousMonth: 15, totalPreviousMonth: 15,
}, },
changeRequests: {
total: 24,
approved: 5,
applied: 2,
rejected: 4,
reviewRequired: 10,
scheduled: 3,
},
}; };
const [stats, featureTypeCounts, health, leadTime] = await Promise.all([ const [stats, featureTypeCounts, health, leadTime, changeRequests] =
this.projectStatsStore.getProjectStats(projectId), await Promise.all([
this.featureToggleService.getFeatureTypeCounts({ this.projectStatsStore.getProjectStats(projectId),
projectId, this.featureToggleService.getFeatureTypeCounts({
archived: false, projectId,
}), archived: false,
this.getHealthInsights(projectId), }),
this.getDoraMetrics(projectId), this.getHealthInsights(projectId),
]); this.getDoraMetrics(projectId),
this.projectInsightsReadModel.getChangeRequests(projectId),
]);
return { ...result, stats, featureTypeCounts, health, leadTime }; return {
...result,
stats,
featureTypeCounts,
health,
leadTime,
changeRequests,
};
} }
private async getProjectHealth( private async getProjectHealth(

View File

@ -34,6 +34,8 @@ test('project insights happy path', async () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200); .expect(200);
console.log(body);
expect(body).toMatchObject({ expect(body).toMatchObject({
stats: { stats: {
avgTimeToProdCurrentWindow: 0, avgTimeToProdCurrentWindow: 0,
@ -53,5 +55,13 @@ test('project insights happy path', async () => {
staleCount: 0, staleCount: 0,
rating: 100, rating: 100,
}, },
changeRequests: {
total: 0,
approved: 0,
applied: 0,
rejected: 0,
reviewRequired: 0,
scheduled: 0,
},
}); });
}); });

View File

@ -76,7 +76,6 @@ import type {
IProjectEnterpriseSettingsUpdate, IProjectEnterpriseSettingsUpdate,
IProjectQuery, IProjectQuery,
} from './project-store-type'; } from './project-store-type';
import { calculateProjectHealth } from '../../domain/project-health/project-health';
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown'; const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
@ -1240,62 +1239,6 @@ export default class ProjectService {
}; };
} }
private async getHealthInsights(projectId: string) {
const [overview, featureTypes] = await Promise.all([
this.getProjectHealth(projectId, false, undefined),
this.featureTypeStore.getAll(),
]);
const { activeCount, potentiallyStaleCount, staleCount } =
calculateProjectHealth(overview.features, featureTypes);
return {
activeCount,
potentiallyStaleCount,
staleCount,
rating: overview.health,
};
}
async getProjectInsights(projectId: string) {
const result = {
leadTime: {
projectAverage: 17.1,
features: [
{ name: 'feature1', timeToProduction: 120 },
{ name: 'feature2', timeToProduction: 0 },
{ name: 'feature3', timeToProduction: 33 },
{ name: 'feature4', timeToProduction: 131 },
{ name: 'feature5', timeToProduction: 2 },
],
},
members: {
active: 20,
inactive: 3,
totalPreviousMonth: 15,
},
changeRequests: {
total: 24,
approved: 5,
applied: 2,
rejected: 4,
reviewRequired: 10,
scheduled: 3,
},
};
const [stats, featureTypeCounts, health] = await Promise.all([
this.projectStatsStore.getProjectStats(projectId),
this.featureToggleService.getFeatureTypeCounts({
projectId,
archived: false,
}),
this.getHealthInsights(projectId),
]);
return { ...result, stats, featureTypeCounts, health };
}
async getProjectHealth( async getProjectHealth(
projectId: string, projectId: string,
archived: boolean = false, archived: boolean = false,