1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: expose stats, health and flag types insights (#6630)

This commit is contained in:
Mateusz Kwasniewski 2024-03-20 13:34:48 +01:00 committed by GitHub
parent 1becfc0202
commit 6dc6e36084
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 104 additions and 63 deletions

View File

@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
import type { FC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const Dot = styled('span', {
shouldForwardProp: (prop) => prop !== 'color',
@ -48,10 +49,16 @@ export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
return (
<Container>
<Typography variant='h3'>Project Health</Typography>
<Alert severity='warning'>
<b>Health alert!</b> Review your flags and delete the stale
flags
</Alert>
<ConditionallyRender
condition={staleCount > 0}
show={
<Alert severity='warning'>
<b>Health alert!</b> Review your flags and delete the
stale flags
</Alert>
}
/>
<Box
data-loading
sx={(theme) => ({

View File

@ -39,6 +39,8 @@ import {
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
import FeatureTypeStore from '../../db/feature-type-store';
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
export const createProjectService = (
db: Db,
@ -66,6 +68,7 @@ export const createProjectService = (
eventBus,
getLogger,
);
const featureTypeStore = new FeatureTypeStore(db, getLogger);
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
const accessService: AccessService = createAccessService(db, config);
const featureToggleService = createFeatureToggleService(db, config);
@ -109,6 +112,7 @@ export const createProjectService = (
featureToggleStore,
environmentStore,
featureEnvironmentStore,
featureTypeStore,
accountStore,
projectStatsStore,
},
@ -133,6 +137,7 @@ export const createFakeProjectService = (
const accountStore = new FakeAccountStore();
const environmentStore = new FakeEnvironmentStore();
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
const featureTypeStore = new FakeFeatureTypeStore();
const projectStatsStore = new FakeProjectStatsStore();
const { accessService } = createFakeAccessService(config);
const featureToggleService = createFakeFeatureToggleService(config);
@ -168,6 +173,7 @@ export const createFakeProjectService = (
featureToggleStore,
environmentStore,
featureEnvironmentStore,
featureTypeStore,
accountStore,
projectStatsStore,
},

View File

@ -248,67 +248,15 @@ export default class ProjectController extends Controller {
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
res: Response<ProjectInsightsSchema>,
): Promise<void> {
const result = {
stats: {
avgTimeToProdCurrentWindow: 17.1,
createdCurrentWindow: 3,
createdPastWindow: 6,
archivedCurrentWindow: 0,
archivedPastWindow: 1,
projectActivityCurrentWindow: 458,
projectActivityPastWindow: 578,
projectMembersAddedCurrentWindow: 0,
},
featureTypeCounts: [
{
type: 'experiment',
count: 4,
},
{
type: 'permission',
count: 1,
},
{
type: 'release',
count: 24,
},
],
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 },
],
},
health: {
rating: 80,
activeCount: 23,
potentiallyStaleCount: 3,
staleCount: 5,
},
members: {
active: 20,
inactive: 3,
totalPreviousMonth: 15,
},
changeRequests: {
total: 24,
approved: 5,
applied: 2,
rejected: 4,
reviewRequired: 10,
scheduled: 3,
},
};
const { projectId } = req.params;
const insights =
await this.projectService.getProjectInsights(projectId);
this.openApiService.respondWithValidation(
200,
res,
projectInsightsSchema.$id,
serializeDates(result),
serializeDates(insights),
);
}

View File

@ -21,6 +21,7 @@ import {
type IFeatureEnvironmentStore,
type IFeatureNaming,
type IFeatureToggleStore,
type IFeatureTypeStore,
type IFlagResolver,
type IProject,
type IProjectApplications,
@ -75,6 +76,7 @@ import type {
IProjectEnterpriseSettingsUpdate,
IProjectQuery,
} from './project-store-type';
import { calculateProjectHealth } from '../../domain/project-health/project-health';
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
@ -119,6 +121,8 @@ export default class ProjectService {
private featureEnvironmentStore: IFeatureEnvironmentStore;
private featureTypeStore: IFeatureTypeStore;
private environmentStore: IEnvironmentStore;
private groupService: GroupService;
@ -148,6 +152,7 @@ export default class ProjectService {
featureToggleStore,
environmentStore,
featureEnvironmentStore,
featureTypeStore,
accountStore,
projectStatsStore,
}: Pick<
@ -159,6 +164,7 @@ export default class ProjectService {
| 'featureEnvironmentStore'
| 'accountStore'
| 'projectStatsStore'
| 'featureTypeStore'
>,
config: IUnleashConfig,
accessService: AccessService,
@ -174,6 +180,7 @@ export default class ProjectService {
this.accessService = accessService;
this.eventStore = eventStore;
this.featureToggleStore = featureToggleStore;
this.featureTypeStore = featureTypeStore;
this.featureToggleService = featureToggleService;
this.favoritesService = favoriteService;
this.privateProjectChecker = privateProjectChecker;
@ -722,6 +729,7 @@ export default class ProjectService {
(r) => r.project === project && r.name === RoleName.OWNER,
);
}
private async isAllowedToAddAccess(
userAddingAccess: number,
projectId: string,
@ -741,6 +749,7 @@ export default class ProjectService {
userRoles.some((userRole) => userRole.id === roleId),
);
}
async addAccess(
projectId: string,
roles: number[],
@ -1231,6 +1240,62 @@ 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(
projectId: string,
archived: boolean = false,

View File

@ -294,8 +294,23 @@ test('project insights happy path', async () => {
.expect('Content-Type', /json/)
.expect(200);
expect(body.leadTime.features[0]).toEqual({
name: 'feature1',
timeToProduction: 120,
expect(body).toMatchObject({
stats: {
avgTimeToProdCurrentWindow: 0,
createdCurrentWindow: 0,
createdPastWindow: 0,
archivedCurrentWindow: 0,
archivedPastWindow: 0,
projectActivityCurrentWindow: 0,
projectActivityPastWindow: 0,
projectMembersAddedCurrentWindow: 0,
},
featureTypeCounts: [],
health: {
activeCount: 0,
potentiallyStaleCount: 0,
staleCount: 0,
rating: 100,
},
});
});