mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-12 13:48:35 +02:00
Fix/dora polish (#4645)
This PR includes: * Tests for retrieving lead time per feature toggle and project average * Feedback component
This commit is contained in:
parent
0b5a7b7d36
commit
26ade79d66
@ -37,6 +37,7 @@ import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi
|
|||||||
import { ImportModal } from './Import/ImportModal';
|
import { ImportModal } from './Import/ImportModal';
|
||||||
import { IMPORT_BUTTON } from 'utils/testIds';
|
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||||
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { ProjectDoraMetrics } from './ProjectDoraMetrics/ProjectDoraMetrics';
|
import { ProjectDoraMetrics } from './ProjectDoraMetrics/ProjectDoraMetrics';
|
||||||
|
|
||||||
export const Project = () => {
|
export const Project = () => {
|
||||||
@ -61,18 +62,21 @@ export const Project = () => {
|
|||||||
path: basePath,
|
path: basePath,
|
||||||
name: 'overview',
|
name: 'overview',
|
||||||
flag: undefined,
|
flag: undefined,
|
||||||
|
new: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Health',
|
title: 'Health',
|
||||||
path: `${basePath}/health`,
|
path: `${basePath}/health`,
|
||||||
name: 'health',
|
name: 'health',
|
||||||
flag: undefined,
|
flag: undefined,
|
||||||
|
new: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Archive',
|
title: 'Archive',
|
||||||
path: `${basePath}/archive`,
|
path: `${basePath}/archive`,
|
||||||
name: 'archive',
|
name: 'archive',
|
||||||
flag: undefined,
|
flag: undefined,
|
||||||
|
new: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Change requests',
|
title: 'Change requests',
|
||||||
@ -80,24 +84,28 @@ export const Project = () => {
|
|||||||
name: 'change-request',
|
name: 'change-request',
|
||||||
isEnterprise: true,
|
isEnterprise: true,
|
||||||
flag: undefined,
|
flag: undefined,
|
||||||
|
new: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'DORA Metrics',
|
title: 'Metrics',
|
||||||
path: `${basePath}/dora`,
|
path: `${basePath}/metrics`,
|
||||||
name: 'dora',
|
name: 'dora',
|
||||||
flag: 'doraMetrics',
|
flag: 'doraMetrics',
|
||||||
|
new: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Event log',
|
title: 'Event log',
|
||||||
path: `${basePath}/logs`,
|
path: `${basePath}/logs`,
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
flag: undefined,
|
flag: undefined,
|
||||||
|
new: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Project settings',
|
title: 'Project settings',
|
||||||
path: `${basePath}/settings`,
|
path: `${basePath}/settings`,
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
flag: undefined,
|
flag: undefined,
|
||||||
|
new: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
.filter(tab => {
|
.filter(tab => {
|
||||||
@ -216,10 +224,28 @@ export const Project = () => {
|
|||||||
tab.isEnterprise ? 'end' : undefined
|
tab.isEnterprise ? 'end' : undefined
|
||||||
}
|
}
|
||||||
icon={
|
icon={
|
||||||
(tab.isEnterprise &&
|
<>
|
||||||
isPro() &&
|
<ConditionallyRender
|
||||||
enterpriseIcon) ||
|
condition={tab.new}
|
||||||
undefined
|
show={
|
||||||
|
<Badge
|
||||||
|
sx={{
|
||||||
|
position:
|
||||||
|
'absolute',
|
||||||
|
top: 10,
|
||||||
|
right: 20,
|
||||||
|
}}
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{(tab.isEnterprise &&
|
||||||
|
isPro() &&
|
||||||
|
enterpriseIcon) ||
|
||||||
|
undefined}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -260,7 +286,7 @@ export const Project = () => {
|
|||||||
element={<ChangeRequestOverview />}
|
element={<ChangeRequestOverview />}
|
||||||
/>
|
/>
|
||||||
<Route path="settings/*" element={<ProjectSettings />} />
|
<Route path="settings/*" element={<ProjectSettings />} />
|
||||||
<Route path="dora" element={<ProjectDoraMetrics />} />
|
<Route path="metrics" element={<ProjectDoraMetrics />} />
|
||||||
<Route path="*" element={<ProjectOverview />} />
|
<Route path="*" element={<ProjectOverview />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<ImportModal
|
<ImportModal
|
||||||
|
@ -0,0 +1,161 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Box, Button, Divider, Typography, styled } from '@mui/material';
|
||||||
|
import { PermMedia, Send } from '@mui/icons-material';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const StyledOuterContainer = styled(Box)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
padding: theme.spacing(2, 4),
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledBtnContainer = styled(Box)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledFlexBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginRight: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIconWrapper = styled(Box)(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHeader = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledLink = styled('a')(({ theme }) => ({
|
||||||
|
textDecoration: 'none',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ProjectDoraFeedback = () => {
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const { value, setValue } = createLocalStorage(
|
||||||
|
`project:metrics:plausible`,
|
||||||
|
{ sent: false }
|
||||||
|
);
|
||||||
|
const [metrics, setMetrics] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(metrics);
|
||||||
|
}, [metrics]);
|
||||||
|
|
||||||
|
const onBtnClick = (type: string) => {
|
||||||
|
try {
|
||||||
|
trackEvent('project-metrics', {
|
||||||
|
props: {
|
||||||
|
eventType: type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Boolean(metrics.sent)) {
|
||||||
|
setMetrics({ sent: true });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error sending metrics');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipientEmail = 'recipient@example.com';
|
||||||
|
const emailSubject = "I'd like to get involved";
|
||||||
|
const emailBody = `Hello Unleash,\n\nI just saw the new metrics page you are experimenting with in Unleash. I'd like to be involved in user tests and give my feedback on this feature.\n\nRegards,\n`;
|
||||||
|
|
||||||
|
const mailtoURL = `mailto:${recipientEmail}?subject=${encodeURIComponent(
|
||||||
|
emailSubject
|
||||||
|
)}&body=${encodeURIComponent(emailBody)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledOuterContainer>
|
||||||
|
<StyledHeader variant="h1">
|
||||||
|
We are trying something experimental!
|
||||||
|
</StyledHeader>
|
||||||
|
<Typography>
|
||||||
|
We are considering adding project metrics to see how a project
|
||||||
|
performs. As a first step, we have added a{' '}
|
||||||
|
<i>lead time for changes</i> indicator that is calculated per
|
||||||
|
feature toggle based on the creation of the feature toggle and
|
||||||
|
when it was first turned on in an environment of type
|
||||||
|
production.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!metrics.sent}
|
||||||
|
show={
|
||||||
|
<StyledBtnContainer>
|
||||||
|
{' '}
|
||||||
|
<Typography>Is this useful to you?</Typography>
|
||||||
|
<StyledBtnContainer>
|
||||||
|
<Button
|
||||||
|
sx={theme => ({
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
})}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => onBtnClick('useful')}
|
||||||
|
>
|
||||||
|
Yes, I like the direction
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => onBtnClick('not useful')}
|
||||||
|
>
|
||||||
|
No, I don't see value in this
|
||||||
|
</Button>
|
||||||
|
</StyledBtnContainer>
|
||||||
|
</StyledBtnContainer>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Typography sx={theme => ({ marginTop: theme.spacing(3) })}>
|
||||||
|
Thank you for the feedback. Feel free to check out the
|
||||||
|
sketches and leave comments, or get in touch with our UX
|
||||||
|
team if you'd like to be involved in usertests and the
|
||||||
|
development of this feature.
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledDivider />
|
||||||
|
<StyledFlexBox>
|
||||||
|
<StyledFlexBox>
|
||||||
|
<StyledIconWrapper>
|
||||||
|
<PermMedia />
|
||||||
|
</StyledIconWrapper>
|
||||||
|
<StyledLink
|
||||||
|
href="https://app.mural.co/t/unleash2757/m/unleash2757/1694006366166/fae4aa4f796de214bdb3ae2d5ce9de934b68fdfb?sender=u777a1f5633477c329eae3448"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferer"
|
||||||
|
>
|
||||||
|
View sketches
|
||||||
|
</StyledLink>
|
||||||
|
</StyledFlexBox>
|
||||||
|
|
||||||
|
<StyledFlexBox>
|
||||||
|
<StyledIconWrapper>
|
||||||
|
<Send />
|
||||||
|
</StyledIconWrapper>
|
||||||
|
<StyledLink href={mailtoURL}>
|
||||||
|
Get involved with our UX team
|
||||||
|
</StyledLink>
|
||||||
|
</StyledFlexBox>
|
||||||
|
</StyledFlexBox>
|
||||||
|
</StyledOuterContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -15,6 +15,7 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { ProjectDoraFeedback } from './ProjectDoraFeedback/ProjectDoraFeedback';
|
||||||
|
|
||||||
const resolveDoraMetrics = (input: number) => {
|
const resolveDoraMetrics = (input: number) => {
|
||||||
const ONE_MONTH = 30;
|
const ONE_MONTH = 30;
|
||||||
@ -42,7 +43,7 @@ export const ProjectDoraMetrics = () => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return Array(5).fill({
|
return Array(5).fill({
|
||||||
name: 'Featurename',
|
name: 'Featurename',
|
||||||
timeToProduction: 'Tag type for production',
|
timeToProduction: 'Data for production',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ export const ProjectDoraMetrics = () => {
|
|||||||
{
|
{
|
||||||
Header: 'Name',
|
Header: 'Name',
|
||||||
accessor: 'name',
|
accessor: 'name',
|
||||||
width: '50%',
|
width: '40%',
|
||||||
Cell: ({
|
Cell: ({
|
||||||
row: {
|
row: {
|
||||||
original: { name },
|
original: { name },
|
||||||
@ -90,7 +91,23 @@ export const ProjectDoraMetrics = () => {
|
|||||||
{original.timeToProduction} days
|
{original.timeToProduction} days
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
width: 150,
|
width: 200,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: `Deviation`,
|
||||||
|
id: 'Deviation from average',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', justifyContent: 'center' }}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
{dora.projectAverage - original.timeToProduction} days
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
width: 300,
|
||||||
disableGlobalFilter: true,
|
disableGlobalFilter: true,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
@ -143,51 +160,49 @@ export const ProjectDoraMetrics = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<>
|
||||||
isLoading={loading}
|
<ProjectDoraFeedback />
|
||||||
header={
|
<PageContent
|
||||||
<PageHeader
|
isLoading={loading}
|
||||||
title={`Lead time for changes (per feature toggle)`}
|
header={
|
||||||
/>
|
<PageHeader
|
||||||
}
|
title={`Lead time for changes (per release 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>
|
<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 features with data found “
|
||||||
|
{globalFilter}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -45,6 +45,7 @@ export type CustomEvents =
|
|||||||
| 'feature-type-edit'
|
| 'feature-type-edit'
|
||||||
| 'strategy-variants'
|
| 'strategy-variants'
|
||||||
| 'search-filter-suggestions'
|
| 'search-filter-suggestions'
|
||||||
|
| 'project-metrics'
|
||||||
| 'open-integration';
|
| 'open-integration';
|
||||||
|
|
||||||
export const usePlausibleTracker = () => {
|
export const usePlausibleTracker = () => {
|
||||||
|
@ -8,11 +8,16 @@ export const projectDoraMetricsSchema = {
|
|||||||
required: ['features'],
|
required: ['features'],
|
||||||
description: 'A projects dora metrics',
|
description: 'A projects dora metrics',
|
||||||
properties: {
|
properties: {
|
||||||
|
projectAverage: {
|
||||||
|
type: 'number',
|
||||||
|
description:
|
||||||
|
'The average time it takes a feature toggle to be enabled in production. The measurement unit is days.',
|
||||||
|
},
|
||||||
features: {
|
features: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: { $ref: '#/components/schemas/doraFeaturesSchema' },
|
items: { $ref: '#/components/schemas/doraFeaturesSchema' },
|
||||||
description:
|
description:
|
||||||
'An array of objects containing feature toggle name and timeToProduction values',
|
'An array of objects containing feature toggle name and timeToProduction values. The measurement unit of timeToProduction is days.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -719,17 +719,33 @@ export default class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
|
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
|
||||||
const featureToggleNames = (await this.featureToggleStore.getAll()).map(
|
const activeFeatureToggles = (
|
||||||
(feature) => feature.name,
|
await this.featureToggleStore.getAll({ project: projectId })
|
||||||
|
).map((feature) => feature.name);
|
||||||
|
|
||||||
|
const archivedFeatureToggles = (
|
||||||
|
await this.featureToggleStore.getAll({
|
||||||
|
project: projectId,
|
||||||
|
archived: true,
|
||||||
|
})
|
||||||
|
).map((feature) => feature.name);
|
||||||
|
|
||||||
|
const featureToggleNames = [
|
||||||
|
...activeFeatureToggles,
|
||||||
|
...archivedFeatureToggles,
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectAverage = calculateAverageTimeToProd(
|
||||||
|
await this.projectStatsStore.getTimeToProdDates(projectId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const avgTimeToProductionPerToggle =
|
const toggleAverage =
|
||||||
await this.projectStatsStore.getTimeToProdDatesForFeatureToggles(
|
await this.projectStatsStore.getTimeToProdDatesForFeatureToggles(
|
||||||
projectId,
|
projectId,
|
||||||
featureToggleNames,
|
featureToggleNames,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { features: avgTimeToProductionPerToggle };
|
return { features: toggleAverage, projectAverage: projectAverage };
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeRole(
|
async changeRole(
|
||||||
|
@ -1603,3 +1603,219 @@ test('should get correct amount of project members for current and past window',
|
|||||||
expect(result.updates.projectActivityCurrentWindow).toBe(6);
|
expect(result.updates.projectActivityCurrentWindow).toBe(6);
|
||||||
expect(result.updates.projectActivityPastWindow).toBe(0);
|
expect(result.updates.projectActivityPastWindow).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return average time to production per toggle', async () => {
|
||||||
|
const project = {
|
||||||
|
id: 'average-time-to-prod-per-toggle',
|
||||||
|
name: 'average-time-to-prod-per-toggle',
|
||||||
|
mode: 'open' as const,
|
||||||
|
defaultStickiness: 'clientId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectService.createProject(project, user.id);
|
||||||
|
|
||||||
|
const toggles = [
|
||||||
|
{ name: 'average-prod-time-pt', subdays: 7 },
|
||||||
|
{ name: 'average-prod-time-pt-2', subdays: 14 },
|
||||||
|
{ name: 'average-prod-time-pt-3', subdays: 40 },
|
||||||
|
{ name: 'average-prod-time-pt-4', subdays: 15 },
|
||||||
|
{ name: 'average-prod-time-pt-5', subdays: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const featureToggles = await Promise.all(
|
||||||
|
toggles.map((toggle) => {
|
||||||
|
return featureToggleService.createFeatureToggle(
|
||||||
|
project.id,
|
||||||
|
toggle,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
featureToggles.map((toggle) => {
|
||||||
|
return stores.eventStore.store(
|
||||||
|
new FeatureEnvironmentEvent({
|
||||||
|
enabled: true,
|
||||||
|
project: project.id,
|
||||||
|
featureName: toggle.name,
|
||||||
|
environment: 'default',
|
||||||
|
createdBy: 'Fredrik',
|
||||||
|
tags: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
toggles.map((toggle) =>
|
||||||
|
updateFeature(toggle.name, {
|
||||||
|
created_at: subDays(new Date(), toggle.subdays),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await projectService.getDoraMetrics(project.id);
|
||||||
|
|
||||||
|
expect(result.features).toHaveLength(5);
|
||||||
|
expect(result.features[0].timeToProduction).toBeTruthy();
|
||||||
|
expect(result.projectAverage).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return average time to production per toggle for a specific project', async () => {
|
||||||
|
const project1 = {
|
||||||
|
id: 'average-time-to-prod-per-toggle-1',
|
||||||
|
name: 'Project 1',
|
||||||
|
mode: 'open' as const,
|
||||||
|
defaultStickiness: 'clientId',
|
||||||
|
};
|
||||||
|
|
||||||
|
const project2 = {
|
||||||
|
id: 'average-time-to-prod-per-toggle-2',
|
||||||
|
name: 'Project 2',
|
||||||
|
mode: 'open' as const,
|
||||||
|
defaultStickiness: 'clientId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectService.createProject(project1, user.id);
|
||||||
|
await projectService.createProject(project2, user.id);
|
||||||
|
|
||||||
|
const togglesProject1 = [
|
||||||
|
{ name: 'average-prod-time-pt-10', subdays: 7 },
|
||||||
|
{ name: 'average-prod-time-pt-11', subdays: 14 },
|
||||||
|
{ name: 'average-prod-time-pt-12', subdays: 40 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const togglesProject2 = [
|
||||||
|
{ name: 'average-prod-time-pt-13', subdays: 15 },
|
||||||
|
{ name: 'average-prod-time-pt-14', subdays: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const featureTogglesProject1 = await Promise.all(
|
||||||
|
togglesProject1.map((toggle) => {
|
||||||
|
return featureToggleService.createFeatureToggle(
|
||||||
|
project1.id,
|
||||||
|
toggle,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const featureTogglesProject2 = await Promise.all(
|
||||||
|
togglesProject2.map((toggle) => {
|
||||||
|
return featureToggleService.createFeatureToggle(
|
||||||
|
project2.id,
|
||||||
|
toggle,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
featureTogglesProject1.map((toggle) => {
|
||||||
|
return stores.eventStore.store(
|
||||||
|
new FeatureEnvironmentEvent({
|
||||||
|
enabled: true,
|
||||||
|
project: project1.id,
|
||||||
|
featureName: toggle.name,
|
||||||
|
environment: 'default',
|
||||||
|
createdBy: 'Fredrik',
|
||||||
|
tags: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
featureTogglesProject2.map((toggle) => {
|
||||||
|
return stores.eventStore.store(
|
||||||
|
new FeatureEnvironmentEvent({
|
||||||
|
enabled: true,
|
||||||
|
project: project2.id,
|
||||||
|
featureName: toggle.name,
|
||||||
|
environment: 'default',
|
||||||
|
createdBy: 'Fredrik',
|
||||||
|
tags: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
togglesProject1.map((toggle) =>
|
||||||
|
updateFeature(toggle.name, {
|
||||||
|
created_at: subDays(new Date(), toggle.subdays),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
togglesProject2.map((toggle) =>
|
||||||
|
updateFeature(toggle.name, {
|
||||||
|
created_at: subDays(new Date(), toggle.subdays),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultProject1 = await projectService.getDoraMetrics(project1.id);
|
||||||
|
const resultProject2 = await projectService.getDoraMetrics(project2.id);
|
||||||
|
|
||||||
|
expect(resultProject1.features).toHaveLength(3);
|
||||||
|
expect(resultProject2.features).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return average time to production per toggle and include archived toggles', async () => {
|
||||||
|
const project1 = {
|
||||||
|
id: 'average-time-to-prod-per-toggle-12',
|
||||||
|
name: 'Project 1',
|
||||||
|
mode: 'open' as const,
|
||||||
|
defaultStickiness: 'clientId',
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectService.createProject(project1, user.id);
|
||||||
|
|
||||||
|
const togglesProject1 = [
|
||||||
|
{ name: 'average-prod-time-pta-10', subdays: 7 },
|
||||||
|
{ name: 'average-prod-time-pta-11', subdays: 14 },
|
||||||
|
{ name: 'average-prod-time-pta-12', subdays: 40 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const featureTogglesProject1 = await Promise.all(
|
||||||
|
togglesProject1.map((toggle) => {
|
||||||
|
return featureToggleService.createFeatureToggle(
|
||||||
|
project1.id,
|
||||||
|
toggle,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
featureTogglesProject1.map((toggle) => {
|
||||||
|
return stores.eventStore.store(
|
||||||
|
new FeatureEnvironmentEvent({
|
||||||
|
enabled: true,
|
||||||
|
project: project1.id,
|
||||||
|
featureName: toggle.name,
|
||||||
|
environment: 'default',
|
||||||
|
createdBy: 'Fredrik',
|
||||||
|
tags: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
togglesProject1.map((toggle) =>
|
||||||
|
updateFeature(toggle.name, {
|
||||||
|
created_at: subDays(new Date(), toggle.subdays),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await featureToggleService.archiveToggle('average-prod-time-pta-12', user);
|
||||||
|
|
||||||
|
const resultProject1 = await projectService.getDoraMetrics(project1.id);
|
||||||
|
|
||||||
|
expect(resultProject1.features).toHaveLength(3);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user