From 5208038fa424ef3dd50f6504f933b64b169e5b90 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 12 Apr 2023 11:19:53 +0200 Subject: [PATCH] Project stats 4.22.4 (#3504) --- .../__snapshots__/create-config.test.ts.snap | 2 + src/lib/db/feature-toggle-store.ts | 10 +- src/lib/db/project-stats-store.ts | 39 +++- .../time-to-production.test.ts | 26 +++ .../time-to-production/time-to-production.ts | 20 ++ .../time-to-production.test.ts | 216 ------------------ .../time-to-production/time-to-production.ts | 121 ---------- src/lib/services/project-service.ts | 80 ++----- src/lib/types/experimental.ts | 4 + src/lib/types/stores/feature-toggle-store.ts | 4 +- .../types/stores/project-stats-store-type.ts | 6 + .../e2e/services/project-service.e2e.test.ts | 90 +++++++- .../fixtures/fake-feature-toggle-store.ts | 6 +- src/test/fixtures/fake-project-stats-store.ts | 9 +- 14 files changed, 224 insertions(+), 409 deletions(-) create mode 100644 src/lib/features/feature-toggle/time-to-production/time-to-production.test.ts create mode 100644 src/lib/features/feature-toggle/time-to-production/time-to-production.ts delete mode 100644 src/lib/read-models/time-to-production/time-to-production.test.ts delete mode 100644 src/lib/read-models/time-to-production/time-to-production.ts diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 0e901a2942..45c30610f5 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -89,6 +89,7 @@ exports[`should create default config 1`] = ` "projectScopedSegments": false, "projectScopedStickiness": false, "projectStatusApi": false, + "projectStatusApiImprovements": false, "responseTimeWithAppNameKillSwitch": false, "strictSchemaValidation": false, }, @@ -117,6 +118,7 @@ exports[`should create default config 1`] = ` "projectScopedSegments": false, "projectScopedStickiness": false, "projectStatusApi": false, + "projectStatusApiImprovements": false, "responseTimeWithAppNameKillSwitch": false, "strictSchemaValidation": false, }, diff --git a/src/lib/db/feature-toggle-store.ts b/src/lib/db/feature-toggle-store.ts index 29613d3028..d43e62ed86 100644 --- a/src/lib/db/feature-toggle-store.ts +++ b/src/lib/db/feature-toggle-store.ts @@ -110,16 +110,16 @@ export default class FeatureToggleStore implements IFeatureToggleStore { return rows.map(this.rowToFeature); } - async getByDate(queryModifiers: { + async countByDate(queryModifiers: { archived?: boolean; project?: string; date?: string; range?: string[]; dateAccessor: string; - }): Promise { + }): Promise { const { project, archived, dateAccessor } = queryModifiers; let query = this.db - .select(FEATURE_COLUMNS) + .count() .from(TABLE) .where({ project }) .modify(FeatureToggleStore.filterByArchived, archived); @@ -135,8 +135,8 @@ export default class FeatureToggleStore implements IFeatureToggleStore { ]); } - const rows = await query; - return rows.map(this.rowToFeature); + const queryResult = await query.first(); + return parseInt(queryResult.count || 0); } /** diff --git a/src/lib/db/project-stats-store.ts b/src/lib/db/project-stats-store.ts index 3fd2491e3f..0f3211067b 100644 --- a/src/lib/db/project-stats-store.ts +++ b/src/lib/db/project-stats-store.ts @@ -4,7 +4,10 @@ import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import EventEmitter from 'events'; import { IProjectStats } from 'lib/services/project-service'; -import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type'; +import { + ICreateEnabledDates, + IProjectStatsStore, +} from 'lib/types/stores/project-stats-store-type'; import { Db } from './db'; const TABLE = 'project_stats'; @@ -107,6 +110,40 @@ class ProjectStatsStore implements IProjectStatsStore { row.project_members_added_current_window, }; } + + // we're not calculating time difference in a DB as it requires specialized + // time aware libraries + async getTimeToProdDates( + projectId: string, + ): Promise { + const result = await this.db + .select('events.feature_name') + // select only first enabled event, distinct works with orderBy + .distinctOn('events.feature_name') + .select( + this.db.raw( + 'events.created_at as enabled, features.created_at as created', + ), + ) + .from('events') + .innerJoin( + 'environments', + 'environments.name', + '=', + 'events.environment', + ) + .innerJoin('features', 'features.name', '=', 'events.feature_name') + .where('events.type', '=', 'feature-environment-enabled') + .where('environments.type', '=', 'production') + .where('features.type', '=', 'release') + // exclude events for features that were previously deleted + .where(this.db.raw('events.created_at > features.created_at')) + .where('features.project', '=', projectId) + .orderBy('events.feature_name') + // first enabled event + .orderBy('events.created_at', 'asc'); + return result; + } } export default ProjectStatsStore; diff --git a/src/lib/features/feature-toggle/time-to-production/time-to-production.test.ts b/src/lib/features/feature-toggle/time-to-production/time-to-production.test.ts new file mode 100644 index 0000000000..e2b90cc628 --- /dev/null +++ b/src/lib/features/feature-toggle/time-to-production/time-to-production.test.ts @@ -0,0 +1,26 @@ +import { calculateAverageTimeToProd } from './time-to-production'; + +describe('calculate average time to production', () => { + test('should calculate average correctly', () => { + const timeToProduction = calculateAverageTimeToProd([ + { + created: new Date('2022-12-05T09:37:32.483Z'), + enabled: new Date('2023-01-25T09:37:32.504Z'), + }, + { + created: new Date('2023-01-19T09:37:32.484Z'), + enabled: new Date('2023-01-31T09:37:32.506Z'), + }, + { + created: new Date('2023-01-19T09:37:32.484Z'), + enabled: new Date('2023-02-02T09:37:32.509Z'), + }, + { + created: new Date('2023-01-19T09:37:32.486Z'), + enabled: new Date('2023-01-26T09:37:32.508Z'), + }, + ]); + + expect(timeToProduction).toBe(21); + }); +}); diff --git a/src/lib/features/feature-toggle/time-to-production/time-to-production.ts b/src/lib/features/feature-toggle/time-to-production/time-to-production.ts new file mode 100644 index 0000000000..1bc75544ff --- /dev/null +++ b/src/lib/features/feature-toggle/time-to-production/time-to-production.ts @@ -0,0 +1,20 @@ +import { differenceInDays } from 'date-fns'; +import { ICreateEnabledDates } from '../../../types/stores/project-stats-store-type'; + +const calculateTimeToProdForFeatures = ( + items: ICreateEnabledDates[], +): number[] => + items.map((item) => differenceInDays(item.enabled, item.created)); + +export const calculateAverageTimeToProd = ( + items: ICreateEnabledDates[], +): number => { + const timeToProdPerFeature = calculateTimeToProdForFeatures(items); + if (timeToProdPerFeature.length) { + const sum = timeToProdPerFeature.reduce((acc, curr) => acc + curr, 0); + + return Number((sum / Object.keys(items).length).toFixed(1)); + } + + return 0; +}; diff --git a/src/lib/read-models/time-to-production/time-to-production.test.ts b/src/lib/read-models/time-to-production/time-to-production.test.ts deleted file mode 100644 index a0f50419c4..0000000000 --- a/src/lib/read-models/time-to-production/time-to-production.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { addDays, subDays } from 'date-fns'; -import { IEvent } from 'lib/types'; -import { TimeToProduction } from './time-to-production'; - -const modifyEventCreatedAt = (events: IEvent[], days: number): IEvent[] => { - return events.map((event) => { - const newEvent = { ...event }; - newEvent.createdAt = addDays(newEvent.createdAt, days); - newEvent.id = newEvent.id + days; - return newEvent; - }); -}; - -const createEvent = (env: string, overrides: Partial) => { - return { - id: Math.floor(Math.random() * 1000), - type: 'feature-environment-enabled', - createdBy: 'Fredrik', - createdAt: new Date('2023-01-25T09:37:32.504Z'), - data: null, - preData: null, - tags: [], - featureName: 'average-prod-time', - project: 'average-time-to-prod', - environment: env, - ...overrides, - }; -}; - -const events = [ - { - id: 65, - type: 'feature-environment-enabled', - createdBy: 'Fredrik', - createdAt: new Date('2023-01-25T09:37:32.504Z'), - data: null, - preData: null, - tags: [], - featureName: 'average-prod-time', - project: 'average-time-to-prod', - environment: 'default', - }, - { - id: 66, - type: 'feature-environment-enabled', - createdBy: 'Fredrik', - createdAt: new Date('2023-01-31T09:37:32.506Z'), - data: null, - preData: null, - tags: [], - featureName: 'average-prod-time-2', - project: 'average-time-to-prod', - environment: 'default', - }, - { - id: 67, - type: 'feature-environment-enabled', - createdBy: 'Fredrik', - createdAt: new Date('2023-01-26T09:37:32.508Z'), - data: null, - preData: null, - tags: [], - featureName: 'average-prod-time-3', - project: 'average-time-to-prod', - environment: 'default', - }, - { - id: 68, - type: 'feature-environment-enabled', - createdBy: 'Fredrik', - createdAt: new Date('2023-02-02T09:37:32.509Z'), - data: null, - preData: null, - tags: [], - featureName: 'average-prod-time-4', - project: 'average-time-to-prod', - environment: 'default', - }, -]; - -const environments = [ - { - name: 'default', - type: 'production', - sortOrder: 1, - enabled: true, - protected: true, - projectApiTokenCount: 0, - projectEnabledToggleCount: 0, - }, -]; - -const features = [ - { - name: 'average-prod-time', - description: null, - type: 'release', - project: 'average-time-to-prod', - stale: false, - createdAt: new Date('2022-12-05T09:37:32.483Z'), - lastSeenAt: null, - impressionData: false, - archivedAt: null, - archived: false, - }, - { - name: 'average-prod-time-4', - description: null, - type: 'release', - project: 'average-time-to-prod', - stale: false, - createdAt: new Date('2023-01-19T09:37:32.484Z'), - lastSeenAt: null, - impressionData: false, - archivedAt: null, - archived: false, - }, - { - name: 'average-prod-time-2', - description: null, - type: 'release', - project: 'average-time-to-prod', - stale: false, - createdAt: new Date('2023-01-19T09:37:32.484Z'), - lastSeenAt: null, - impressionData: false, - archivedAt: null, - archived: false, - }, - { - name: 'average-prod-time-3', - description: null, - type: 'release', - project: 'average-time-to-prod', - stale: false, - createdAt: new Date('2023-01-19T09:37:32.486Z'), - lastSeenAt: null, - impressionData: false, - archivedAt: null, - archived: false, - }, -]; - -describe('calculate average time to production', () => { - test('should build a map of feature events', () => { - const projectStatus = new TimeToProduction( - features, - environments, - events, - ); - - const featureEvents = projectStatus.getFeatureEvents(); - - expect(Object.keys(featureEvents).length).toBe(4); - expect(featureEvents['average-prod-time'].createdAt).toBeTruthy(); - expect(featureEvents['average-prod-time'].events).toBeInstanceOf(Array); - }); - - test('should calculate average correctly', () => { - const projectStatus = new TimeToProduction( - features, - environments, - events, - ); - - const timeToProduction = projectStatus.calculateAverageTimeToProd(); - - expect(timeToProduction).toBe(21); - }); - - test('should sort events by createdAt', () => { - const projectStatus = new TimeToProduction(features, environments, [ - ...modifyEventCreatedAt(events, 5), - ...events, - ]); - - const featureEvents = projectStatus.getFeatureEvents(); - const sortedFeatureEvents = - projectStatus.sortFeatureEventsByCreatedAt(featureEvents); - - const [firstEvent, secondEvent] = - sortedFeatureEvents['average-prod-time'].events; - - const firstEventCreatedAt = new Date(firstEvent.createdAt); - const secondEventCreatedAt = new Date(secondEvent.createdAt); - - expect(firstEventCreatedAt.getTime()).toBeLessThan( - secondEventCreatedAt.getTime(), - ); - - const [firstEvent2, secondEvent2] = - sortedFeatureEvents['average-prod-time-2'].events; - - const firstEventCreatedAt2 = new Date(firstEvent2.createdAt); - const secondEventCreatedAt2 = new Date(secondEvent2.createdAt); - - expect(firstEventCreatedAt2.getTime()).toBeLessThan( - secondEventCreatedAt2.getTime(), - ); - }); - - test('should not count events that are development environments', () => { - const projectStatus = new TimeToProduction(features, environments, [ - createEvent('development', { - createdAt: subDays(new Date('2023-01-25T09:37:32.504Z'), 10), - }), - createEvent('development', { - createdAt: subDays(new Date('2023-01-25T09:37:32.504Z'), 10), - }), - ...events, - ]); - - const timeToProduction = projectStatus.calculateAverageTimeToProd(); - expect(timeToProduction).toBe(21); - }); -}); diff --git a/src/lib/read-models/time-to-production/time-to-production.ts b/src/lib/read-models/time-to-production/time-to-production.ts deleted file mode 100644 index b402bd7fee..0000000000 --- a/src/lib/read-models/time-to-production/time-to-production.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { differenceInDays } from 'date-fns'; -import { FeatureToggle, IEvent, IProjectEnvironment } from 'lib/types'; - -interface IFeatureTimeToProdCalculationMap { - [index: string]: IFeatureTimeToProdData; -} - -interface IFeatureTimeToProdData { - createdAt: string; - events: IEvent[]; -} - -export class TimeToProduction { - private features: FeatureToggle[]; - - private productionEnvironments: IProjectEnvironment[]; - - private events: IEvent[]; - - constructor( - features: FeatureToggle[], - productionEnvironments: IProjectEnvironment[], - events: IEvent[], - ) { - this.features = features; - this.productionEnvironments = productionEnvironments; - this.events = events; - } - - calculateAverageTimeToProd(): number { - const featureEvents = this.getFeatureEvents(); - const sortedFeatureEvents = - this.sortFeatureEventsByCreatedAt(featureEvents); - - const timeToProdPerFeature = - this.calculateTimeToProdForFeatures(sortedFeatureEvents); - if (timeToProdPerFeature.length) { - const sum = timeToProdPerFeature.reduce( - (acc, curr) => acc + curr, - 0, - ); - - return Number( - (sum / Object.keys(sortedFeatureEvents).length).toFixed(1), - ); - } - - return 0; - } - - getFeatureEvents(): IFeatureTimeToProdCalculationMap { - return this.getProductionEvents(this.events).reduce((acc, event) => { - if (acc[event.featureName]) { - acc[event.featureName].events.push(event); - } else { - const foundFeature = this.features.find( - (feature) => feature.name === event.featureName, - ); - acc[event.featureName] = { events: [event] }; - acc[event.featureName].createdAt = foundFeature?.createdAt; - } - - return acc; - }, {}); - } - - getProductionEvents(events: IEvent[]): IEvent[] { - return events.filter((event) => { - const found = this.productionEnvironments.find( - (env) => env.name === event.environment, - ); - - if (found) { - return found.type === 'production'; - } - - return false; - }); - } - - calculateTimeToProdForFeatures( - featureEvents: IFeatureTimeToProdCalculationMap, - ): number[] { - return Object.keys(featureEvents).map((featureName) => { - const feature = featureEvents[featureName]; - - const earliestEvent = feature.events[0]; - - const createdAtDate = new Date(feature.createdAt); - const eventDate = new Date(earliestEvent.createdAt); - const diff = differenceInDays(eventDate, createdAtDate); - - return diff; - }); - } - - sortFeatureEventsByCreatedAt( - featureEvents: IFeatureTimeToProdCalculationMap, - ): IFeatureTimeToProdCalculationMap { - return Object.keys(featureEvents).reduce((acc, featureName) => { - const feature = featureEvents[featureName]; - acc[featureName] = { - ...feature, - events: feature.events.sort((a, b) => { - const aDate = new Date(a.createdAt); - const bDate = new Date(b.createdAt); - - if (aDate > bDate) { - return 1; - } - if (aDate < bDate) { - return -1; - } - return 0; - }), - }; - - return acc; - }, {}); - } -} diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 7d8d6b242b..2dfc01fa25 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -8,7 +8,6 @@ import { projectSchema } from './project-schema'; import NotFoundError from '../error/notfound-error'; import { DEFAULT_PROJECT, - FEATURE_ENVIRONMENT_ENABLED, FeatureToggle, IAccountStore, IEnvironmentStore, @@ -54,7 +53,7 @@ import { arraysHaveSameItems } from '../util'; import { GroupService } from './group-service'; import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group'; import { FavoritesService } from './favorites-service'; -import { TimeToProduction } from '../read-models/time-to-production/time-to-production'; +import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-production/time-to-production'; import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type'; import { uniqueByKey } from '../util/unique'; @@ -688,42 +687,32 @@ export default class ProjectService { } async getStatusUpdates(projectId: string): Promise { - // Get all features for project with type release - const features = await this.featureToggleStore.getAll({ - type: 'release', - 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(); - const [createdCurrentWindow, createdPastWindow] = await Promise.all([ - await this.featureToggleStore.getByDate({ + const [ + createdCurrentWindow, + createdPastWindow, + archivedCurrentWindow, + archivedPastWindow, + ] = await Promise.all([ + await this.featureToggleStore.countByDate({ project: projectId, dateAccessor: 'created_at', date: dateMinusThirtyDays, }), - await this.featureToggleStore.getByDate({ + await this.featureToggleStore.countByDate({ project: projectId, dateAccessor: 'created_at', range: [dateMinusSixtyDays, dateMinusThirtyDays], }), - ]); - - const [archivedCurrentWindow, archivedPastWindow] = await Promise.all([ - await this.featureToggleStore.getByDate({ + await this.featureToggleStore.countByDate({ project: projectId, archived: true, dateAccessor: 'archived_at', date: dateMinusThirtyDays, }), - await this.featureToggleStore.getByDate({ + await this.featureToggleStore.countByDate({ project: projectId, archived: true, dateAccessor: 'archived_at', @@ -755,33 +744,8 @@ export default class ProjectService { ]), ]); - // Get all project environments with type of production - const productionEnvironments = - await this.environmentStore.getProjectEnvironments(projectId, { - type: 'production', - }); - - // Get all events for features that correspond to feature toggle environment ON - // Filter out events that are not a production evironment - - const allFeatures = [...features, ...archivedFeatures]; - - const eventsData = await this.eventStore.query([ - { - op: 'forFeatures', - parameters: { - features: allFeatures.map((feature) => feature.name), - environments: productionEnvironments.map((env) => env.name), - type: FEATURE_ENVIRONMENT_ENABLED, - projectId, - }, - }, - ]); - - const currentWindowTimeToProdReadModel = new TimeToProduction( - allFeatures, - productionEnvironments, - eventsData, + const avgTimeToProdCurrentWindow = calculateAverageTimeToProd( + await this.projectStatsStore.getTimeToProdDates(projectId), ); const projectMembersAddedCurrentWindow = @@ -793,16 +757,14 @@ export default class ProjectService { return { projectId, updates: { - avgTimeToProdCurrentWindow: - currentWindowTimeToProdReadModel.calculateAverageTimeToProd(), - createdCurrentWindow: createdCurrentWindow.length, - createdPastWindow: createdPastWindow.length, - archivedCurrentWindow: archivedCurrentWindow.length, - archivedPastWindow: archivedPastWindow.length, - projectActivityCurrentWindow: projectActivityCurrentWindow, - projectActivityPastWindow: projectActivityPastWindow, - projectMembersAddedCurrentWindow: - projectMembersAddedCurrentWindow, + avgTimeToProdCurrentWindow, + createdCurrentWindow, + createdPastWindow, + archivedCurrentWindow, + archivedPastWindow, + projectActivityCurrentWindow, + projectActivityPastWindow, + projectMembersAddedCurrentWindow, }, }; } diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index e2017d0463..7148d5f70b 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -14,6 +14,10 @@ const flags = { process.env.UNLEASH_EXPERIMENTAL_PROJECT_STATUS_API, false, ), + projectStatusApiImprovements: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_PROJECT_STATUS_API_IMPROVEMENTS, + false, + ), newProjectOverview: parseEnvVarBoolean( process.env.NEW_PROJECT_OVERVIEW, false, diff --git a/src/lib/types/stores/feature-toggle-store.ts b/src/lib/types/stores/feature-toggle-store.ts index f6e1ad6326..441f9db0e1 100644 --- a/src/lib/types/stores/feature-toggle-store.ts +++ b/src/lib/types/stores/feature-toggle-store.ts @@ -25,13 +25,13 @@ export interface IFeatureToggleStore extends Store { revive(featureName: string): Promise; getAll(query?: Partial): Promise; getAllByNames(names: string[]): Promise; - getByDate(queryModifiers: { + countByDate(queryModifiers: { archived?: boolean; project?: string; date?: string; range?: string[]; dateAccessor: string; - }): Promise; + }): Promise; /** * @deprecated - Variants should be fetched from FeatureEnvironmentStore (since variants are now; since 4.18, connected to environments) * @param featureName diff --git a/src/lib/types/stores/project-stats-store-type.ts b/src/lib/types/stores/project-stats-store-type.ts index 56c59c21b2..b8b0558cbe 100644 --- a/src/lib/types/stores/project-stats-store-type.ts +++ b/src/lib/types/stores/project-stats-store-type.ts @@ -1,6 +1,12 @@ import { IProjectStats } from 'lib/services/project-service'; +export interface ICreateEnabledDates { + created: Date; + enabled: Date; +} + export interface IProjectStatsStore { updateProjectStats(projectId: string, status: IProjectStats): Promise; getProjectStats(projectId: string): Promise; + getTimeToProdDates(projectId: string): Promise; } diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 796f399026..a3b4e892cf 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -13,7 +13,7 @@ import { SegmentService } from '../../../lib/services/segment-service'; import { GroupService } from '../../../lib/services/group-service'; import { FavoritesService } from '../../../lib/services'; import { FeatureEnvironmentEvent } from '../../../lib/types/events'; -import { subDays } from 'date-fns'; +import { addDays, subDays } from 'date-fns'; import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model'; let stores; @@ -1310,6 +1310,94 @@ test('should calculate average time to production', async () => { expect(result.updates.avgTimeToProdCurrentWindow).toBe(11.4); }); +test('should calculate average time to production ignoring some items', async () => { + const project = { + id: 'average-time-to-prod-corner-cases', + name: 'average-time-to-prod', + mode: 'open' as const, + }; + const makeEvent = (featureName: string) => ({ + enabled: true, + project: project.id, + featureName, + environment: 'default', + createdBy: 'Fredrik', + tags: [], + }); + + await projectService.createProject(project, user.id); + await stores.environmentStore.create({ + name: 'customEnv', + type: 'development', + }); + await environmentService.addEnvironmentToProject('customEnv', project.id); + + // actual toggle we take for calculations + const toggle = { name: 'main-toggle' }; + await featureToggleService.createFeatureToggle(project.id, toggle, user); + await updateFeature(toggle.name, { + created_at: subDays(new Date(), 20), + }); + await stores.eventStore.store( + new FeatureEnvironmentEvent(makeEvent(toggle.name)), + ); + // ignore events added after first enabled + await updateEventCreatedAt(addDays(new Date(), 1), toggle.name); + await stores.eventStore.store( + new FeatureEnvironmentEvent(makeEvent(toggle.name)), + ); + + // ignore toggles enabled in non-prod envs + const devToggle = { name: 'dev-toggle' }; + await featureToggleService.createFeatureToggle(project.id, devToggle, user); + await stores.eventStore.store( + new FeatureEnvironmentEvent({ + ...makeEvent(devToggle.name), + environment: 'customEnv', + }), + ); + + // ignore toggles from other projects + const otherProjectToggle = { name: 'other-project' }; + await featureToggleService.createFeatureToggle( + 'default', + otherProjectToggle, + user, + ); + await stores.eventStore.store( + new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)), + ); + + // ignore non-release toggles + const nonReleaseToggle = { name: 'permission-toggle', type: 'permission' }; + await featureToggleService.createFeatureToggle( + project.id, + nonReleaseToggle, + user, + ); + await stores.eventStore.store( + new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)), + ); + + // ignore toggles with events before toggle creation time + const previouslyDeleteToggle = { name: 'previously-deleted' }; + await featureToggleService.createFeatureToggle( + project.id, + previouslyDeleteToggle, + user, + ); + await stores.eventStore.store( + new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)), + ); + await updateEventCreatedAt( + subDays(new Date(), 30), + previouslyDeleteToggle.name, + ); + + const result = await projectService.getStatusUpdates(project.id); + expect(result.updates.avgTimeToProdCurrentWindow).toBe(20); +}); + test('should get correct amount of features created in current and past window', async () => { const project = { id: 'features-created', diff --git a/src/test/fixtures/fake-feature-toggle-store.ts b/src/test/fixtures/fake-feature-toggle-store.ts index 5015766a4c..b4fb17a7eb 100644 --- a/src/test/fixtures/fake-feature-toggle-store.ts +++ b/src/test/fixtures/fake-feature-toggle-store.ts @@ -213,13 +213,13 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { return Promise.resolve(newVariants); } - async getByDate(queryModifiers: { + async countByDate(queryModifiers: { archived?: boolean; project?: string; date?: string; range?: string[]; dateAccessor: string; - }): Promise { + }): Promise { return this.features.filter((feature) => { if (feature.archived === queryModifiers.archived) { return true; @@ -245,7 +245,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { ) { return true; } - }); + }).length; } dropAllVariants(): Promise { diff --git a/src/test/fixtures/fake-project-stats-store.ts b/src/test/fixtures/fake-project-stats-store.ts index e518a49eb0..d42f25869e 100644 --- a/src/test/fixtures/fake-project-stats-store.ts +++ b/src/test/fixtures/fake-project-stats-store.ts @@ -1,5 +1,8 @@ import { IProjectStats } from 'lib/services/project-service'; -import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type'; +import { + ICreateEnabledDates, + IProjectStatsStore, +} from 'lib/types/stores/project-stats-store-type'; /* eslint-disable @typescript-eslint/no-unused-vars */ export default class FakeProjectStatsStore implements IProjectStatsStore { @@ -13,4 +16,8 @@ export default class FakeProjectStatsStore implements IProjectStatsStore { getProjectStats(projectId: string): Promise { throw new Error('not implemented'); } + + getTimeToProdDates(): Promise { + throw new Error('not implemented'); + } }