mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: DORA metrics lead time to production (#4589)
* Adds initial support for estimating dora metric lead time for changes per feature toggle
This commit is contained in:
parent
ff346adb94
commit
3b2d6a4cbf
@ -41,6 +41,7 @@ import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi
|
||||
import { ImportModal } from './Import/ImportModal';
|
||||
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
||||
import { ProjectDoraMetrics } from './ProjectDoraMetrics/ProjectDoraMetrics';
|
||||
|
||||
export const Project = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
@ -63,34 +64,53 @@ export const Project = () => {
|
||||
title: 'Overview',
|
||||
path: basePath,
|
||||
name: 'overview',
|
||||
flag: undefined,
|
||||
},
|
||||
{
|
||||
title: 'Health',
|
||||
path: `${basePath}/health`,
|
||||
name: 'health',
|
||||
flag: undefined,
|
||||
},
|
||||
{
|
||||
title: 'Archive',
|
||||
path: `${basePath}/archive`,
|
||||
name: 'archive',
|
||||
flag: undefined,
|
||||
},
|
||||
{
|
||||
title: 'Change requests',
|
||||
path: `${basePath}/change-requests`,
|
||||
name: 'change-request',
|
||||
isEnterprise: true,
|
||||
flag: undefined,
|
||||
},
|
||||
{
|
||||
title: 'DORA Metrics',
|
||||
path: `${basePath}/dora`,
|
||||
name: 'dora',
|
||||
flag: 'doraMetrics',
|
||||
},
|
||||
{
|
||||
title: 'Event log',
|
||||
path: `${basePath}/logs`,
|
||||
name: 'logs',
|
||||
flag: undefined,
|
||||
},
|
||||
{
|
||||
title: 'Project settings',
|
||||
path: `${basePath}/settings`,
|
||||
name: 'settings',
|
||||
flag: undefined,
|
||||
},
|
||||
].filter(tab => !(isOss() && tab.isEnterprise));
|
||||
]
|
||||
.filter(tab => {
|
||||
if (tab.flag) {
|
||||
return uiConfig.flags[tab.flag];
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.filter(tab => !(isOss() && tab.isEnterprise));
|
||||
|
||||
const activeTab = [...tabs]
|
||||
.reverse()
|
||||
@ -188,24 +208,26 @@ export const Project = () => {
|
||||
variant="scrollable"
|
||||
allowScrollButtonsMobile
|
||||
>
|
||||
{tabs.map(tab => (
|
||||
<StyledTab
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB_${tab.title}`}
|
||||
iconPosition={
|
||||
tab.isEnterprise ? 'end' : undefined
|
||||
}
|
||||
icon={
|
||||
(tab.isEnterprise &&
|
||||
isPro() &&
|
||||
enterpriseIcon) ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{tabs.map(tab => {
|
||||
return (
|
||||
<StyledTab
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB_${tab.title}`}
|
||||
iconPosition={
|
||||
tab.isEnterprise ? 'end' : undefined
|
||||
}
|
||||
icon={
|
||||
(tab.isEnterprise &&
|
||||
isPro() &&
|
||||
enterpriseIcon) ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</StyledTabContainer>
|
||||
</StyledHeader>
|
||||
@ -242,6 +264,7 @@ export const Project = () => {
|
||||
element={<ChangeRequestOverview />}
|
||||
/>
|
||||
<Route path="settings/*" element={<ProjectSettings />} />
|
||||
<Route path="dora" element={<ProjectDoraMetrics />} />
|
||||
<Route path="*" element={<ProjectOverview />} />
|
||||
</Routes>
|
||||
<ImportModal
|
||||
|
@ -0,0 +1,194 @@
|
||||
import { Box, List, ListItem } from '@mui/material';
|
||||
import { useProjectDoraMetrics } from 'hooks/api/getters/useProjectDoraMetrics/useProjectDoraMetrics';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useMemo } from 'react';
|
||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||
import {
|
||||
Table,
|
||||
SortableTableHeader,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TablePlaceholder,
|
||||
} from 'component/common/Table';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
|
||||
const resolveDoraMetrics = (input: number) => {
|
||||
const ONE_MONTH = 30;
|
||||
const ONE_WEEK = 7;
|
||||
|
||||
if (input >= ONE_MONTH) {
|
||||
return <Badge color="error">Low</Badge>;
|
||||
}
|
||||
|
||||
if (input <= ONE_MONTH && input >= ONE_WEEK + 1) {
|
||||
return <Badge>Medium</Badge>;
|
||||
}
|
||||
|
||||
if (input <= ONE_WEEK) {
|
||||
return <Badge color="success">High</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
export const ProjectDoraMetrics = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
|
||||
const { dora, loading } = useProjectDoraMetrics(projectId);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (loading) {
|
||||
return Array(5).fill({
|
||||
name: 'Featurename',
|
||||
timeToProduction: 'Tag type for production',
|
||||
});
|
||||
}
|
||||
|
||||
return dora.features;
|
||||
}, [dora, loading]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
width: '50%',
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { name, description },
|
||||
},
|
||||
}: any) => {
|
||||
return (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{
|
||||
pl: 2,
|
||||
pr: 1,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
sortType: 'alphanumeric',
|
||||
},
|
||||
{
|
||||
Header: 'Time to production',
|
||||
id: 'Time to production',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original } }: any) => (
|
||||
<Box
|
||||
sx={{ display: 'flex', justifyContent: 'center' }}
|
||||
data-loading
|
||||
>
|
||||
{original.timeToProduction} days
|
||||
</Box>
|
||||
),
|
||||
width: 150,
|
||||
disableGlobalFilter: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Header: 'DORA',
|
||||
id: 'dora',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original } }: any) => (
|
||||
<Box
|
||||
sx={{ display: 'flex', justifyContent: 'center' }}
|
||||
data-loading
|
||||
>
|
||||
{resolveDoraMetrics(original.timeToProduction)}
|
||||
</Box>
|
||||
),
|
||||
width: 200,
|
||||
disableGlobalFilter: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[JSON.stringify(dora.features), loading]
|
||||
);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
sortBy: [{ id: 'name', desc: false }],
|
||||
hiddenColumns: ['description'],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: { globalFilter },
|
||||
setGlobalFilter,
|
||||
} = useTable(
|
||||
{
|
||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||
data,
|
||||
initialState,
|
||||
autoResetGlobalFilter: false,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
header={
|
||||
<PageHeader
|
||||
title={`Lead time for changes (per feature toggle)`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table {...getTableProps()}>
|
||||
<SortableTableHeader headerGroups={headerGroups} />
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map(row => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow hover {...row.getRowProps()}>
|
||||
{row.cells.map(cell => (
|
||||
<TableCell {...cell.getCellProps()}>
|
||||
{cell.render('Cell')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={globalFilter?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No tags found matching “
|
||||
{globalFilter}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No tags available. Get started by adding one.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
export const useProjectDoraMetrics = (
|
||||
projectId: string,
|
||||
options: SWRConfiguration = {}
|
||||
) => {
|
||||
const KEY = `api/admin/projects/${projectId}/dora`;
|
||||
const path = formatApiPath(KEY);
|
||||
|
||||
const fetcher = () => {
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(handleErrorResponses('Dora metrics'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
const { data, error } = useSWR(KEY, fetcher, options);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetchDoraMetrics = () => {
|
||||
mutate(KEY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
dora: data || {},
|
||||
error,
|
||||
loading,
|
||||
refetchDoraMetrics,
|
||||
};
|
||||
};
|
@ -58,6 +58,8 @@ export interface IFlags {
|
||||
newApplicationList?: boolean;
|
||||
integrationsRework?: boolean;
|
||||
multipleRoles?: boolean;
|
||||
doraMetrics?: boolean;
|
||||
[key: string]: boolean | Variant | undefined;
|
||||
}
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -75,6 +75,7 @@ exports[`should create default config 1`] = `
|
||||
"demo": false,
|
||||
"disableBulkToggle": false,
|
||||
"disableNotifications": false,
|
||||
"doraMetrics": false,
|
||||
"embedProxy": true,
|
||||
"embedProxyFrontend": true,
|
||||
"featuresExportImport": true,
|
||||
@ -112,6 +113,7 @@ exports[`should create default config 1`] = `
|
||||
"demo": false,
|
||||
"disableBulkToggle": false,
|
||||
"disableNotifications": false,
|
||||
"doraMetrics": false,
|
||||
"embedProxy": true,
|
||||
"embedProxyFrontend": true,
|
||||
"featuresExportImport": true,
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
IProjectStatsStore,
|
||||
} from 'lib/types/stores/project-stats-store-type';
|
||||
import { Db } from './db';
|
||||
import { DoraFeaturesSchema } from 'lib/openapi';
|
||||
|
||||
const TABLE = 'project_stats';
|
||||
|
||||
@ -144,6 +145,51 @@ class ProjectStatsStore implements IProjectStatsStore {
|
||||
.orderBy('events.created_at', 'asc');
|
||||
return result;
|
||||
}
|
||||
|
||||
async getTimeToProdDatesForFeatureToggles(
|
||||
projectId: string,
|
||||
featureToggleNames: string[],
|
||||
): Promise<DoraFeaturesSchema[]> {
|
||||
const result = await this.db
|
||||
.select('events.feature_name')
|
||||
.distinctOn('events.feature_name')
|
||||
.select(
|
||||
this.db.raw(
|
||||
'events.created_at as enabled, features.created_at as created',
|
||||
),
|
||||
)
|
||||
.from('events')
|
||||
.innerJoin(
|
||||
'environments',
|
||||
'environments.name',
|
||||
'=',
|
||||
'events.environment',
|
||||
)
|
||||
.innerJoin('features', 'features.name', '=', 'events.feature_name')
|
||||
.whereIn('events.feature_name', featureToggleNames)
|
||||
.where('events.type', '=', 'feature-environment-enabled')
|
||||
.where('environments.type', '=', 'production')
|
||||
.where('features.type', '=', 'release')
|
||||
.where(this.db.raw('events.created_at > features.created_at'))
|
||||
.where('features.project', '=', projectId)
|
||||
.orderBy('events.feature_name')
|
||||
.orderBy('events.created_at', 'asc');
|
||||
|
||||
const timeDifferenceData: DoraFeaturesSchema[] = result.map((row) => {
|
||||
const enabledDate = new Date(row.enabled).getTime();
|
||||
const createdDate = new Date(row.created).getTime();
|
||||
const timeDifferenceInDays = Math.floor(
|
||||
(enabledDate - createdDate) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
return {
|
||||
name: row.feature_name,
|
||||
timeToProduction: timeDifferenceInDays,
|
||||
};
|
||||
});
|
||||
|
||||
return timeDifferenceData;
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectStatsStore;
|
||||
|
@ -155,6 +155,8 @@ import {
|
||||
createStrategyVariantSchema,
|
||||
clientSegmentSchema,
|
||||
createGroupSchema,
|
||||
doraFeaturesSchema,
|
||||
projectDoraMetricsSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
@ -367,6 +369,8 @@ export const schemas: UnleashSchemas = {
|
||||
createStrategyVariantSchema,
|
||||
clientSegmentSchema,
|
||||
createGroupSchema,
|
||||
doraFeaturesSchema,
|
||||
projectDoraMetricsSchema,
|
||||
};
|
||||
|
||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||
|
24
src/lib/openapi/spec/dora-features-schema.ts
Normal file
24
src/lib/openapi/spec/dora-features-schema.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const doraFeaturesSchema = {
|
||||
$id: '#/components/schemas/doraFeaturesSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'timeToProduction'],
|
||||
description:
|
||||
'The representation of a dora time to production feature metric',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The name of a feature toggle',
|
||||
},
|
||||
timeToProduction: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The average number of days it takes a feature toggle to get into production',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type DoraFeaturesSchema = FromSchema<typeof doraFeaturesSchema>;
|
@ -154,3 +154,5 @@ export * from './client-segment-schema';
|
||||
export * from './update-feature-type-lifetime-schema';
|
||||
export * from './create-group-schema';
|
||||
export * from './application-usage-schema';
|
||||
export * from './dora-features-schema';
|
||||
export * from './project-dora-metrics-schema';
|
||||
|
27
src/lib/openapi/spec/project-dora-metrics-schema.ts
Normal file
27
src/lib/openapi/spec/project-dora-metrics-schema.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { doraFeaturesSchema } from './dora-features-schema';
|
||||
|
||||
export const projectDoraMetricsSchema = {
|
||||
$id: '#/components/schemas/projectDoraMetricsSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['features'],
|
||||
description: 'A projects dora metrics',
|
||||
properties: {
|
||||
features: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/doraFeaturesSchema' },
|
||||
description:
|
||||
'An array of objects containing feature toggle name and timeToProduction values',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
doraFeaturesSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ProjectDoraMetricsSchema = FromSchema<
|
||||
typeof projectDoraMetricsSchema
|
||||
>;
|
@ -15,6 +15,8 @@ import ProjectService from '../../../services/project-service';
|
||||
import VariantsController from './variants';
|
||||
import {
|
||||
createResponseSchema,
|
||||
ProjectDoraMetricsSchema,
|
||||
projectDoraMetricsSchema,
|
||||
ProjectOverviewSchema,
|
||||
projectOverviewSchema,
|
||||
projectsSchema,
|
||||
@ -27,6 +29,7 @@ import { ProjectApiTokenController } from './api-token';
|
||||
import ProjectArchiveController from './project-archive';
|
||||
import { createKnexTransactionStarter } from '../../../db/transaction';
|
||||
import { Db } from '../../../db/db';
|
||||
import { InvalidOperationError } from '../../../error';
|
||||
|
||||
export default class ProjectApi extends Controller {
|
||||
private projectService: ProjectService;
|
||||
@ -81,6 +84,26 @@ export default class ProjectApi extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/dora',
|
||||
handler: this.getProjectDora,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
services.openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'getProjectDora',
|
||||
summary: 'Get an overview project dora metrics.',
|
||||
description:
|
||||
'This endpoint returns an overview of the specified dora metrics',
|
||||
responses: {
|
||||
200: createResponseSchema('projectDoraMetricsSchema'),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.use(
|
||||
'/',
|
||||
new ProjectFeaturesController(
|
||||
@ -136,4 +159,26 @@ export default class ProjectApi extends Controller {
|
||||
serializeDates(overview),
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectDora(
|
||||
req: IAuthRequest,
|
||||
res: Response<ProjectDoraMetricsSchema>,
|
||||
): Promise<void> {
|
||||
if (this.config.flagResolver.isEnabled('doraMetrics')) {
|
||||
const { projectId } = req.params;
|
||||
|
||||
const dora = await this.projectService.getDoraMetrics(projectId);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
projectDoraMetricsSchema.$id,
|
||||
dora,
|
||||
);
|
||||
} else {
|
||||
throw new InvalidOperationError(
|
||||
'Feature dora metrics is not enabled',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-p
|
||||
import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type';
|
||||
import { uniqueByKey } from '../util/unique';
|
||||
import { PermissionError } from '../error';
|
||||
import { ProjectDoraMetricsSchema } from 'lib/openapi';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
@ -684,6 +685,20 @@ export default class ProjectService {
|
||||
}
|
||||
}
|
||||
|
||||
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
|
||||
const featureToggleNames = (await this.featureToggleStore.getAll()).map(
|
||||
(feature) => feature.name,
|
||||
);
|
||||
|
||||
const avgTimeToProductionPerToggle =
|
||||
await this.projectStatsStore.getTimeToProdDatesForFeatureToggles(
|
||||
projectId,
|
||||
featureToggleNames,
|
||||
);
|
||||
|
||||
return { features: avgTimeToProductionPerToggle };
|
||||
}
|
||||
|
||||
async changeRole(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
@ -959,6 +974,7 @@ export default class ProjectService {
|
||||
: Promise.resolve(false),
|
||||
this.projectStatsStore.getProjectStats(projectId),
|
||||
]);
|
||||
|
||||
return {
|
||||
stats: projectStats,
|
||||
name: project.name,
|
||||
|
@ -29,7 +29,8 @@ export type IFlagKey =
|
||||
| 'customRootRolesKillSwitch'
|
||||
| 'newApplicationList'
|
||||
| 'integrationsRework'
|
||||
| 'multipleRoles';
|
||||
| 'multipleRoles'
|
||||
| 'doraMetrics';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -137,6 +138,7 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_NEW_APPLICATION_LIST,
|
||||
false,
|
||||
),
|
||||
doraMetrics: parseEnvVarBoolean(process.env.UNLEASH_DORA_METRICS, false),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DoraFeaturesSchema } from 'lib/openapi';
|
||||
import { IProjectStats } from 'lib/services/project-service';
|
||||
|
||||
export interface ICreateEnabledDates {
|
||||
@ -9,4 +10,8 @@ export interface IProjectStatsStore {
|
||||
updateProjectStats(projectId: string, status: IProjectStats): Promise<void>;
|
||||
getProjectStats(projectId: string): Promise<IProjectStats>;
|
||||
getTimeToProdDates(projectId: string): Promise<ICreateEnabledDates[]>;
|
||||
getTimeToProdDatesForFeatureToggles(
|
||||
projectId: string,
|
||||
toggleNames: string[],
|
||||
): Promise<DoraFeaturesSchema[]>;
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ process.nextTick(async () => {
|
||||
lastSeenByEnvironment: true,
|
||||
segmentChangeRequests: true,
|
||||
newApplicationList: true,
|
||||
doraMetrics: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
@ -20,4 +20,8 @@ export default class FakeProjectStatsStore implements IProjectStatsStore {
|
||||
getTimeToProdDates(): Promise<ICreateEnabledDates[]> {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
getTimeToProdDatesForFeatureToggles(): Promise<any> {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user