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:
parent
2537071a3d
commit
e2ad0cae45
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
19
src/migrations/20230316092547-remove-project-stats-column.js
Normal file
19
src/migrations/20230316092547-remove-project-stats-column.js
Normal 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,
|
||||
);
|
||||
};
|
@ -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",
|
||||
|
@ -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 () => {
|
||||
|
Loading…
Reference in New Issue
Block a user