From e2ad0cae45004b88bf15b014ddc66a5d162e64e9 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Thu, 16 Mar 2023 15:45:24 +0100 Subject: [PATCH] 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. --- .../Project/ProjectStats/ProjectStats.tsx | 18 ++++-- .../Project/ProjectStats/StatusBox.tsx | 60 +++++++++++-------- src/lib/db/project-stats-store.ts | 5 -- src/lib/openapi/spec/project-stats-schema.ts | 7 --- src/lib/services/project-service.ts | 49 ++++----------- ...30316092547-remove-project-stats-column.js | 19 ++++++ .../__snapshots__/openapi.e2e.test.ts.snap | 6 -- .../e2e/services/project-service.e2e.test.ts | 3 +- 8 files changed, 80 insertions(+), 87 deletions(-) create mode 100644 src/migrations/20230316092547-remove-project-stats-column.js diff --git a/frontend/src/component/project/Project/ProjectStats/ProjectStats.tsx b/frontend/src/component/project/Project/ProjectStats/ProjectStats.tsx index a4748ec5b7..9ca09bd511 100644 --- a/frontend/src/component/project/Project/ProjectStats/ProjectStats.tsx +++ b/frontend/src/component/project/Project/ProjectStats/ProjectStats.tsx @@ -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) => { days } - change={calculatePercentage( - avgTimeToProdCurrentWindow, - avgTimeToProdPastWindow - )} + customChangeElement={ + + In project life + + } percentage > 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". diff --git a/frontend/src/component/project/Project/ProjectStats/StatusBox.tsx b/frontend/src/component/project/Project/ProjectStats/StatusBox.tsx index dce85d7e55..fd8c5e4736 100644 --- a/frontend/src/component/project/Project/ProjectStats/StatusBox.tsx +++ b/frontend/src/component/project/Project/ProjectStats/StatusBox.tsx @@ -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 = ({ change, percentage, children, + customChangeElement, }) => ( <> = ({ > {boxText} - - {resolveIcon(change)} - - {change > 0 ? '+' : ''} - {change} - {percentage ? '%' : ''} - - - - this month - + {customChangeElement} } elseShow={ - - - No change - - + + + {resolveIcon(change as number)} + + {(change as number) > 0 ? '+' : ''} + {change} + {percentage ? '%' : ''} + + + + this month + + + } + elseShow={ + + + No change + + + } + /> } /> diff --git a/src/lib/db/project-stats-store.ts b/src/lib/db/project-stats-store.ts index 8a5e24a5b2..3fd2491e3f 100644 --- a/src/lib/db/project-stats-store.ts +++ b/src/lib/db/project-stats-store.ts @@ -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, diff --git a/src/lib/openapi/spec/project-stats-schema.ts b/src/lib/openapi/spec/project-stats-schema.ts index 041ddba94f..0630d56e83 100644 --- a/src/lib/openapi/spec/project-stats-schema.ts +++ b/src/lib/openapi/spec/project-stats-schema.ts @@ -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, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 7ddd9ebb31..047b30e1a4 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -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, diff --git a/src/migrations/20230316092547-remove-project-stats-column.js b/src/migrations/20230316092547-remove-project-stats-column.js new file mode 100644 index 0000000000..1105730cf7 --- /dev/null +++ b/src/migrations/20230316092547-remove-project-stats-column.js @@ -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, + ); +}; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 17c6ccb394..c415e90176 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -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", diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 93ca9db6cf..c4fc70b545 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -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 () => {