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

fix: refactor calculate time to prod (#3333)

This PR changes how we calculate average time to production. Instead of
calculating fleeting 30 day windows and calculating the past and current
window, we now calculate a flat average across the entire project life.
This is less error prone as each feature will be tied to the earliest
time it was turned on in a production environment.
This commit is contained in:
Fredrik Strand Oseberg 2023-03-16 15:45:24 +01:00 committed by GitHub
parent 2537071a3d
commit e2ad0cae45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 80 additions and 87 deletions

View File

@ -35,6 +35,12 @@ const StyledWidget = styled(Box)(({ theme }) => ({
}, },
})); }));
const StyledTimeToProductionDescription = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
lineHeight: theme.typography.body2.lineHeight,
}));
interface IProjectStatsProps { interface IProjectStatsProps {
stats: ProjectStatsSchema; stats: ProjectStatsSchema;
} }
@ -95,16 +101,18 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
<Typography component="span">days</Typography> <Typography component="span">days</Typography>
</Box> </Box>
} }
change={calculatePercentage( customChangeElement={
avgTimeToProdCurrentWindow, <StyledTimeToProductionDescription>
avgTimeToProdPastWindow In project life
)} </StyledTimeToProductionDescription>
}
percentage percentage
> >
<HelpPopper id="avg-time-to-prod"> <HelpPopper id="avg-time-to-prod">
How long did it take on average from a feature toggle How long did it take on average from a feature toggle
was created until it was enabled in an environment of was created until it was enabled in an environment of
type production. type production. This is calculated only from feature
toggles with the type of "release".
</HelpPopper> </HelpPopper>
</StatusBox> </StatusBox>
</StyledWidget> </StyledWidget>

View File

@ -33,8 +33,9 @@ const StyledTypographyChange = styled(Typography)(({ theme }) => ({
interface IStatusBoxProps { interface IStatusBoxProps {
title?: string; title?: string;
boxText: ReactNode; boxText: ReactNode;
change: number; change?: number;
percentage?: boolean; percentage?: boolean;
customChangeElement?: ReactNode;
} }
const resolveIcon = (change: number) => { const resolveIcon = (change: number) => {
@ -59,6 +60,7 @@ export const StatusBox: FC<IStatusBoxProps> = ({
change, change,
percentage, percentage,
children, children,
customChangeElement,
}) => ( }) => (
<> <>
<ConditionallyRender <ConditionallyRender
@ -75,7 +77,15 @@ export const StatusBox: FC<IStatusBoxProps> = ({
> >
<StyledTypographyCount>{boxText}</StyledTypographyCount> <StyledTypographyCount>{boxText}</StyledTypographyCount>
<ConditionallyRender <ConditionallyRender
condition={change !== 0} condition={Boolean(customChangeElement)}
show={
<StyledBoxChangeContainer>
{customChangeElement}
</StyledBoxChangeContainer>
}
elseShow={
<ConditionallyRender
condition={change !== undefined && change !== 0}
show={ show={
<StyledBoxChangeContainer> <StyledBoxChangeContainer>
<Box <Box
@ -83,11 +93,11 @@ export const StatusBox: FC<IStatusBoxProps> = ({
...flexRow, ...flexRow,
}} }}
> >
{resolveIcon(change)} {resolveIcon(change as number)}
<StyledTypographyChange <StyledTypographyChange
color={resolveColor(change)} color={resolveColor(change as number)}
> >
{change > 0 ? '+' : ''} {(change as number) > 0 ? '+' : ''}
{change} {change}
{percentage ? '%' : ''} {percentage ? '%' : ''}
</StyledTypographyChange> </StyledTypographyChange>
@ -105,6 +115,8 @@ export const StatusBox: FC<IStatusBoxProps> = ({
</StyledBoxChangeContainer> </StyledBoxChangeContainer>
} }
/> />
}
/>
</Box> </Box>
</> </>
); );

View File

@ -11,7 +11,6 @@ const TABLE = 'project_stats';
const PROJECT_STATS_COLUMNS = [ const PROJECT_STATS_COLUMNS = [
'avg_time_to_prod_current_window', 'avg_time_to_prod_current_window',
'avg_time_to_prod_past_window',
'project', 'project',
'features_created_current_window', 'features_created_current_window',
'features_created_past_window', 'features_created_past_window',
@ -24,7 +23,6 @@ const PROJECT_STATS_COLUMNS = [
interface IProjectStatsRow { interface IProjectStatsRow {
avg_time_to_prod_current_window: number; avg_time_to_prod_current_window: number;
avg_time_to_prod_past_window: number;
features_created_current_window: number; features_created_current_window: number;
features_created_past_window: number; features_created_past_window: number;
features_archived_current_window: number; features_archived_current_window: number;
@ -59,7 +57,6 @@ class ProjectStatsStore implements IProjectStatsStore {
.insert({ .insert({
avg_time_to_prod_current_window: avg_time_to_prod_current_window:
status.avgTimeToProdCurrentWindow, status.avgTimeToProdCurrentWindow,
avg_time_to_prod_past_window: status.avgTimeToProdPastWindow,
project: projectId, project: projectId,
features_created_current_window: status.createdCurrentWindow, features_created_current_window: status.createdCurrentWindow,
features_created_past_window: status.createdPastWindow, features_created_past_window: status.createdPastWindow,
@ -88,7 +85,6 @@ class ProjectStatsStore implements IProjectStatsStore {
if (!row) { if (!row) {
return { return {
avgTimeToProdCurrentWindow: 0, avgTimeToProdCurrentWindow: 0,
avgTimeToProdPastWindow: 0,
createdCurrentWindow: 0, createdCurrentWindow: 0,
createdPastWindow: 0, createdPastWindow: 0,
archivedCurrentWindow: 0, archivedCurrentWindow: 0,
@ -101,7 +97,6 @@ class ProjectStatsStore implements IProjectStatsStore {
return { return {
avgTimeToProdCurrentWindow: row.avg_time_to_prod_current_window, avgTimeToProdCurrentWindow: row.avg_time_to_prod_current_window,
avgTimeToProdPastWindow: row.avg_time_to_prod_past_window,
createdCurrentWindow: row.features_created_current_window, createdCurrentWindow: row.features_created_current_window,
createdPastWindow: row.features_created_past_window, createdPastWindow: row.features_created_past_window,
archivedCurrentWindow: row.features_archived_current_window, archivedCurrentWindow: row.features_archived_current_window,

View File

@ -6,7 +6,6 @@ export const projectStatsSchema = {
additionalProperties: false, additionalProperties: false,
required: [ required: [
'avgTimeToProdCurrentWindow', 'avgTimeToProdCurrentWindow',
'avgTimeToProdPastWindow',
'createdCurrentWindow', 'createdCurrentWindow',
'createdPastWindow', 'createdPastWindow',
'archivedCurrentWindow', 'archivedCurrentWindow',
@ -27,12 +26,6 @@ Stats are divided into current and previous **windows**.
description: description:
'The average time from when a feature was created to when it was enabled in the "production" environment during the current window', 'The average time from when a feature was created to when it was enabled in the "production" environment during the current window',
}, },
avgTimeToProdPastWindow: {
type: 'number',
example: 10,
description:
'The average time from when a feature was created to when it was enabled in the "production" environment during the previous window',
},
createdCurrentWindow: { createdCurrentWindow: {
type: 'number', type: 'number',
example: 15, example: 15,

View File

@ -65,7 +65,6 @@ type Count = number;
export interface IProjectStats { export interface IProjectStats {
avgTimeToProdCurrentWindow: Days; avgTimeToProdCurrentWindow: Days;
avgTimeToProdPastWindow: Days;
createdCurrentWindow: Count; createdCurrentWindow: Count;
createdPastWindow: Count; createdPastWindow: Count;
archivedCurrentWindow: Count; archivedCurrentWindow: Count;
@ -679,6 +678,12 @@ export default class ProjectService {
project: projectId, project: projectId,
}); });
const archivedFeatures = await this.featureToggleStore.getAll({
archived: true,
type: 'release',
project: projectId,
});
const dateMinusThirtyDays = subDays(new Date(), 30).toISOString(); const dateMinusThirtyDays = subDays(new Date(), 30).toISOString();
const dateMinusSixtyDays = subDays(new Date(), 60).toISOString(); const dateMinusSixtyDays = subDays(new Date(), 60).toISOString();
@ -743,54 +748,24 @@ export default class ProjectService {
// Get all events for features that correspond to feature toggle environment ON // Get all events for features that correspond to feature toggle environment ON
// Filter out events that are not a production evironment // Filter out events that are not a production evironment
const eventsCurrentWindow = await this.eventStore.query([ const allFeatures = [...features, ...archivedFeatures];
{
op: 'forFeatures',
parameters: {
features: features.map((feature) => feature.name),
environments: productionEnvironments.map((env) => env.name),
type: FEATURE_ENVIRONMENT_ENABLED,
projectId,
},
},
{
op: 'beforeDate',
parameters: {
dateAccessor: 'created_at',
date: dateMinusThirtyDays,
},
},
]);
const eventsPastWindow = await this.eventStore.query([ const eventsData = await this.eventStore.query([
{ {
op: 'forFeatures', op: 'forFeatures',
parameters: { parameters: {
features: features.map((feature) => feature.name), features: allFeatures.map((feature) => feature.name),
environments: productionEnvironments.map((env) => env.name), environments: productionEnvironments.map((env) => env.name),
type: FEATURE_ENVIRONMENT_ENABLED, type: FEATURE_ENVIRONMENT_ENABLED,
projectId, projectId,
}, },
}, },
{
op: 'betweenDate',
parameters: {
dateAccessor: 'created_at',
range: [dateMinusSixtyDays, dateMinusThirtyDays],
},
},
]); ]);
const currentWindowTimeToProdReadModel = new TimeToProduction( const currentWindowTimeToProdReadModel = new TimeToProduction(
features, allFeatures,
productionEnvironments, productionEnvironments,
eventsCurrentWindow, eventsData,
);
const pastWindowTimeToProdReadModel = new TimeToProduction(
features,
productionEnvironments,
eventsPastWindow,
); );
const projectMembersAddedCurrentWindow = const projectMembersAddedCurrentWindow =
@ -804,8 +779,6 @@ export default class ProjectService {
updates: { updates: {
avgTimeToProdCurrentWindow: avgTimeToProdCurrentWindow:
currentWindowTimeToProdReadModel.calculateAverageTimeToProd(), currentWindowTimeToProdReadModel.calculateAverageTimeToProd(),
avgTimeToProdPastWindow:
pastWindowTimeToProdReadModel.calculateAverageTimeToProd(),
createdCurrentWindow: createdCurrentWindow.length, createdCurrentWindow: createdCurrentWindow.length,
createdPastWindow: createdPastWindow.length, createdPastWindow: createdPastWindow.length,
archivedCurrentWindow: archivedCurrentWindow.length, archivedCurrentWindow: archivedCurrentWindow.length,

View File

@ -0,0 +1,19 @@
exports.up = function (db, cb) {
db.runSql(
`
ALTER table project_stats
DROP COLUMN avg_time_to_prod_past_window
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
ALTER table project_stats
ADD COLUMN IF NOT EXISTS avg_time_to_prod_past_window INTEGER DEFAULT 0
`,
cb,
);
};

View File

@ -2937,11 +2937,6 @@ Stats are divided into current and previous **windows**.
"example": 10, "example": 10,
"type": "number", "type": "number",
}, },
"avgTimeToProdPastWindow": {
"description": "The average time from when a feature was created to when it was enabled in the "production" environment during the previous window",
"example": 10,
"type": "number",
},
"createdCurrentWindow": { "createdCurrentWindow": {
"description": "The number of feature toggles created during the current window", "description": "The number of feature toggles created during the current window",
"example": 15, "example": 15,
@ -2970,7 +2965,6 @@ Stats are divided into current and previous **windows**.
}, },
"required": [ "required": [
"avgTimeToProdCurrentWindow", "avgTimeToProdCurrentWindow",
"avgTimeToProdPastWindow",
"createdCurrentWindow", "createdCurrentWindow",
"createdPastWindow", "createdPastWindow",
"archivedCurrentWindow", "archivedCurrentWindow",

View File

@ -1231,8 +1231,7 @@ test('should calculate average time to production', async () => {
}); });
const result = await projectService.getStatusUpdates(project.id); const result = await projectService.getStatusUpdates(project.id);
expect(result.updates.avgTimeToProdCurrentWindow).toBe(14); expect(result.updates.avgTimeToProdCurrentWindow).toBe(11.4);
expect(result.updates.avgTimeToProdPastWindow).toBe(1);
}); });
test('should get correct amount of features created in current and past window', async () => { test('should get correct amount of features created in current and past window', async () => {