mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
refactor: switching to new stats calculations (#3477)
This commit is contained in:
parent
52cc633a0a
commit
f1133556bd
@ -135,7 +135,6 @@ class ProjectStatsStore implements IProjectStatsStore {
|
|||||||
.innerJoin('features', 'features.name', '=', 'events.feature_name')
|
.innerJoin('features', 'features.name', '=', 'events.feature_name')
|
||||||
.where('events.type', '=', 'feature-environment-enabled')
|
.where('events.type', '=', 'feature-environment-enabled')
|
||||||
.where('environments.type', '=', 'production')
|
.where('environments.type', '=', 'production')
|
||||||
// kill-switch is long lived
|
|
||||||
.where('features.type', '=', 'release')
|
.where('features.type', '=', 'release')
|
||||||
// exclude events for features that were previously deleted
|
// exclude events for features that were previously deleted
|
||||||
.where(this.db.raw('events.created_at > features.created_at'))
|
.where(this.db.raw('events.created_at > features.created_at'))
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
||||||
|
};
|
@ -1,227 +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<IEvent>) => {
|
|
||||||
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',
|
|
||||||
type: 'release',
|
|
||||||
project: 'average-time-to-prod',
|
|
||||||
stale: false,
|
|
||||||
createdAt: new Date('2022-12-05T09:37:32.483Z'),
|
|
||||||
impressionData: false,
|
|
||||||
archived: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'average-prod-time-4',
|
|
||||||
type: 'release',
|
|
||||||
project: 'average-time-to-prod',
|
|
||||||
stale: false,
|
|
||||||
createdAt: new Date('2023-01-19T09:37:32.484Z'),
|
|
||||||
impressionData: false,
|
|
||||||
archived: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'average-prod-time-2',
|
|
||||||
type: 'release',
|
|
||||||
project: 'average-time-to-prod',
|
|
||||||
stale: false,
|
|
||||||
createdAt: new Date('2023-01-19T09:37:32.484Z'),
|
|
||||||
impressionData: false,
|
|
||||||
archived: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'average-prod-time-3',
|
|
||||||
type: 'release',
|
|
||||||
project: 'average-time-to-prod',
|
|
||||||
stale: false,
|
|
||||||
createdAt: new Date('2023-01-19T09:37:32.486Z'),
|
|
||||||
impressionData: false,
|
|
||||||
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('[legacy] should calculate average correctly', () => {
|
|
||||||
const projectStatus = new TimeToProduction(
|
|
||||||
features,
|
|
||||||
environments,
|
|
||||||
events,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeToProduction = projectStatus.calculateAverageTimeToProd();
|
|
||||||
|
|
||||||
expect(timeToProduction).toBe(21);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should calculate average correctly', () => {
|
|
||||||
const timeToProduction = 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,150 +0,0 @@
|
|||||||
import { differenceInDays } from 'date-fns';
|
|
||||||
import { FeatureToggle, IEvent, IProjectEnvironment } from 'lib/types';
|
|
||||||
import { ICreateEnabledDates } from '../../types/stores/project-stats-store-type';
|
|
||||||
|
|
||||||
interface IFeatureTimeToProdCalculationMap {
|
|
||||||
[index: string]: IFeatureTimeToProdData;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IFeatureTimeToProdData {
|
|
||||||
createdAt: string;
|
|
||||||
events: IEvent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TimeToProduction {
|
|
||||||
private features: FeatureToggle[];
|
|
||||||
|
|
||||||
private productionEnvironments: IProjectEnvironment[];
|
|
||||||
|
|
||||||
private events: IEvent[];
|
|
||||||
|
|
||||||
// todo: remove
|
|
||||||
constructor(
|
|
||||||
features: FeatureToggle[],
|
|
||||||
productionEnvironments: IProjectEnvironment[],
|
|
||||||
events: IEvent[],
|
|
||||||
) {
|
|
||||||
this.features = features;
|
|
||||||
this.productionEnvironments = productionEnvironments;
|
|
||||||
this.events = events;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static calculateAverageTimeToProd(items: ICreateEnabledDates[]): number {
|
|
||||||
const timeToProdPerFeature =
|
|
||||||
TimeToProduction.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove, as DB query can handle it
|
|
||||||
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;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove it as DB query can handle it
|
|
||||||
private 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static calculateTimeToProdForFeatures(
|
|
||||||
items: ICreateEnabledDates[],
|
|
||||||
): number[] {
|
|
||||||
return items.map((item) =>
|
|
||||||
differenceInDays(item.enabled, item.created),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove as DB query can handle it
|
|
||||||
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;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import { projectSchema } from './project-schema';
|
|||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
import {
|
import {
|
||||||
DEFAULT_PROJECT,
|
DEFAULT_PROJECT,
|
||||||
FEATURE_ENVIRONMENT_ENABLED,
|
|
||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
IAccountStore,
|
IAccountStore,
|
||||||
IEnvironmentStore,
|
IEnvironmentStore,
|
||||||
@ -54,7 +53,7 @@ import { arraysHaveSameItems } from '../util';
|
|||||||
import { GroupService } from './group-service';
|
import { GroupService } from './group-service';
|
||||||
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
||||||
import { FavoritesService } from './favorites-service';
|
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 { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type';
|
||||||
import { uniqueByKey } from '../util/unique';
|
import { uniqueByKey } from '../util/unique';
|
||||||
|
|
||||||
@ -688,20 +687,6 @@ export default class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStatusUpdates(projectId: string): Promise<ICalculateStatus> {
|
async getStatusUpdates(projectId: string): Promise<ICalculateStatus> {
|
||||||
// Get all features for project with type release
|
|
||||||
// todo: remove after release of the improved query
|
|
||||||
const features = await this.featureToggleStore.getAll({
|
|
||||||
type: 'release',
|
|
||||||
project: projectId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo: remove after release of the improved query
|
|
||||||
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();
|
||||||
|
|
||||||
@ -759,56 +744,9 @@ export default class ProjectService {
|
|||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get all project environments with type of production
|
const avgTimeToProdCurrentWindow = calculateAverageTimeToProd(
|
||||||
// todo: remove after release of the improved query
|
|
||||||
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
|
|
||||||
// todo: remove after release of the improved query
|
|
||||||
const allFeatures = [...features, ...archivedFeatures];
|
|
||||||
|
|
||||||
// todo: remove after release of the improved query
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// todo: remove after release of the improved query
|
|
||||||
const timeToProduction = new TimeToProduction(
|
|
||||||
allFeatures,
|
|
||||||
productionEnvironments,
|
|
||||||
eventsData,
|
|
||||||
);
|
|
||||||
|
|
||||||
const avgTimeToProdCurrentWindowFast =
|
|
||||||
TimeToProduction.calculateAverageTimeToProd(
|
|
||||||
await this.projectStatsStore.getTimeToProdDates(projectId),
|
await this.projectStatsStore.getTimeToProdDates(projectId),
|
||||||
);
|
);
|
||||||
const avgTimeToProdCurrentWindowSlow =
|
|
||||||
timeToProduction.calculateAverageTimeToProd();
|
|
||||||
|
|
||||||
const avgTimeToProdCurrentWindow = this.flagResolver.isEnabled(
|
|
||||||
'projectStatusApiImprovements',
|
|
||||||
)
|
|
||||||
? avgTimeToProdCurrentWindowFast
|
|
||||||
: avgTimeToProdCurrentWindowSlow;
|
|
||||||
|
|
||||||
if (avgTimeToProdCurrentWindowFast != avgTimeToProdCurrentWindowSlow) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Lead time calculation difference, old ${avgTimeToProdCurrentWindowSlow}, new ${avgTimeToProdCurrentWindowFast}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectMembersAddedCurrentWindow =
|
const projectMembersAddedCurrentWindow =
|
||||||
await this.store.getMembersCountByProjectAfterDate(
|
await this.store.getMembersCountByProjectAfterDate(
|
||||||
|
@ -13,7 +13,7 @@ import { SegmentService } from '../../../lib/services/segment-service';
|
|||||||
import { GroupService } from '../../../lib/services/group-service';
|
import { GroupService } from '../../../lib/services/group-service';
|
||||||
import { FavoritesService } from '../../../lib/services';
|
import { FavoritesService } from '../../../lib/services';
|
||||||
import { FeatureEnvironmentEvent } from '../../../lib/types/events';
|
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';
|
import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model';
|
||||||
|
|
||||||
let stores;
|
let stores;
|
||||||
@ -1269,6 +1269,94 @@ test('should calculate average time to production', async () => {
|
|||||||
expect(result.updates.avgTimeToProdCurrentWindow).toBe(11.4);
|
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 () => {
|
test('should get correct amount of features created in current and past window', async () => {
|
||||||
const project = {
|
const project = {
|
||||||
id: 'features-created',
|
id: 'features-created',
|
||||||
|
Loading…
Reference in New Issue
Block a user