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 {
stats: ProjectStatsSchema;
}
@ -95,16 +101,18 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
<Typography component="span">days</Typography>
</Box>
}
change={calculatePercentage(
avgTimeToProdCurrentWindow,
avgTimeToProdPastWindow
)}
customChangeElement={
<StyledTimeToProductionDescription>
In project life
</StyledTimeToProductionDescription>
}
percentage
>
<HelpPopper id="avg-time-to-prod">
How long did it take on average from a feature toggle
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>
</StatusBox>
</StyledWidget>

View File

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

View File

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

View File

@ -6,7 +6,6 @@ export const projectStatsSchema = {
additionalProperties: false,
required: [
'avgTimeToProdCurrentWindow',
'avgTimeToProdPastWindow',
'createdCurrentWindow',
'createdPastWindow',
'archivedCurrentWindow',
@ -27,12 +26,6 @@ Stats are divided into current and previous **windows**.
description:
'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: {
type: 'number',
example: 15,

View File

@ -65,7 +65,6 @@ type Count = number;
export interface IProjectStats {
avgTimeToProdCurrentWindow: Days;
avgTimeToProdPastWindow: Days;
createdCurrentWindow: Count;
createdPastWindow: Count;
archivedCurrentWindow: Count;
@ -679,6 +678,12 @@ export default class ProjectService {
project: projectId,
});
const archivedFeatures = await this.featureToggleStore.getAll({
archived: true,
type: 'release',
project: projectId,
});
const dateMinusThirtyDays = subDays(new Date(), 30).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
// Filter out events that are not a production evironment
const eventsCurrentWindow = await this.eventStore.query([
{
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 allFeatures = [...features, ...archivedFeatures];
const eventsPastWindow = await this.eventStore.query([
const eventsData = await this.eventStore.query([
{
op: 'forFeatures',
parameters: {
features: features.map((feature) => feature.name),
features: allFeatures.map((feature) => feature.name),
environments: productionEnvironments.map((env) => env.name),
type: FEATURE_ENVIRONMENT_ENABLED,
projectId,
},
},
{
op: 'betweenDate',
parameters: {
dateAccessor: 'created_at',
range: [dateMinusSixtyDays, dateMinusThirtyDays],
},
},
]);
const currentWindowTimeToProdReadModel = new TimeToProduction(
features,
allFeatures,
productionEnvironments,
eventsCurrentWindow,
);
const pastWindowTimeToProdReadModel = new TimeToProduction(
features,
productionEnvironments,
eventsPastWindow,
eventsData,
);
const projectMembersAddedCurrentWindow =
@ -804,8 +779,6 @@ export default class ProjectService {
updates: {
avgTimeToProdCurrentWindow:
currentWindowTimeToProdReadModel.calculateAverageTimeToProd(),
avgTimeToProdPastWindow:
pastWindowTimeToProdReadModel.calculateAverageTimeToProd(),
createdCurrentWindow: createdCurrentWindow.length,
createdPastWindow: createdPastWindow.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,
"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": {
"description": "The number of feature toggles created during the current window",
"example": 15,
@ -2970,7 +2965,6 @@ Stats are divided into current and previous **windows**.
},
"required": [
"avgTimeToProdCurrentWindow",
"avgTimeToProdPastWindow",
"createdCurrentWindow",
"createdPastWindow",
"archivedCurrentWindow",

View File

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