mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
refactor: Improved lead time calculation (#3475)
This commit is contained in:
parent
9c4322d1fb
commit
8d61332543
@ -89,6 +89,7 @@ exports[`should create default config 1`] = `
|
|||||||
"projectScopedSegments": false,
|
"projectScopedSegments": false,
|
||||||
"projectScopedStickiness": false,
|
"projectScopedStickiness": false,
|
||||||
"projectStatusApi": false,
|
"projectStatusApi": false,
|
||||||
|
"projectStatusApiImprovements": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
},
|
},
|
||||||
@ -117,6 +118,7 @@ exports[`should create default config 1`] = `
|
|||||||
"projectScopedSegments": false,
|
"projectScopedSegments": false,
|
||||||
"projectScopedStickiness": false,
|
"projectScopedStickiness": false,
|
||||||
"projectStatusApi": false,
|
"projectStatusApi": false,
|
||||||
|
"projectStatusApiImprovements": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,10 @@ import metricsHelper from '../util/metrics-helper';
|
|||||||
import { DB_TIME } from '../metric-events';
|
import { DB_TIME } from '../metric-events';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { IProjectStats } from 'lib/services/project-service';
|
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';
|
import { Db } from './db';
|
||||||
|
|
||||||
const TABLE = 'project_stats';
|
const TABLE = 'project_stats';
|
||||||
@ -107,6 +110,41 @@ class ProjectStatsStore implements IProjectStatsStore {
|
|||||||
row.project_members_added_current_window,
|
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<ICreateEnabledDates[]> {
|
||||||
|
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')
|
||||||
|
// kill-switch is long lived
|
||||||
|
.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;
|
export default ProjectStatsStore;
|
||||||
|
@ -93,50 +93,38 @@ const environments = [
|
|||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
name: 'average-prod-time',
|
name: 'average-prod-time',
|
||||||
description: null,
|
|
||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'average-time-to-prod',
|
project: 'average-time-to-prod',
|
||||||
stale: false,
|
stale: false,
|
||||||
createdAt: new Date('2022-12-05T09:37:32.483Z'),
|
createdAt: new Date('2022-12-05T09:37:32.483Z'),
|
||||||
lastSeenAt: null,
|
|
||||||
impressionData: false,
|
impressionData: false,
|
||||||
archivedAt: null,
|
|
||||||
archived: false,
|
archived: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'average-prod-time-4',
|
name: 'average-prod-time-4',
|
||||||
description: null,
|
|
||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'average-time-to-prod',
|
project: 'average-time-to-prod',
|
||||||
stale: false,
|
stale: false,
|
||||||
createdAt: new Date('2023-01-19T09:37:32.484Z'),
|
createdAt: new Date('2023-01-19T09:37:32.484Z'),
|
||||||
lastSeenAt: null,
|
|
||||||
impressionData: false,
|
impressionData: false,
|
||||||
archivedAt: null,
|
|
||||||
archived: false,
|
archived: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'average-prod-time-2',
|
name: 'average-prod-time-2',
|
||||||
description: null,
|
|
||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'average-time-to-prod',
|
project: 'average-time-to-prod',
|
||||||
stale: false,
|
stale: false,
|
||||||
createdAt: new Date('2023-01-19T09:37:32.484Z'),
|
createdAt: new Date('2023-01-19T09:37:32.484Z'),
|
||||||
lastSeenAt: null,
|
|
||||||
impressionData: false,
|
impressionData: false,
|
||||||
archivedAt: null,
|
|
||||||
archived: false,
|
archived: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'average-prod-time-3',
|
name: 'average-prod-time-3',
|
||||||
description: null,
|
|
||||||
type: 'release',
|
type: 'release',
|
||||||
project: 'average-time-to-prod',
|
project: 'average-time-to-prod',
|
||||||
stale: false,
|
stale: false,
|
||||||
createdAt: new Date('2023-01-19T09:37:32.486Z'),
|
createdAt: new Date('2023-01-19T09:37:32.486Z'),
|
||||||
lastSeenAt: null,
|
|
||||||
impressionData: false,
|
impressionData: false,
|
||||||
archivedAt: null,
|
|
||||||
archived: false,
|
archived: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -156,7 +144,7 @@ describe('calculate average time to production', () => {
|
|||||||
expect(featureEvents['average-prod-time'].events).toBeInstanceOf(Array);
|
expect(featureEvents['average-prod-time'].events).toBeInstanceOf(Array);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should calculate average correctly', () => {
|
test('[legacy] should calculate average correctly', () => {
|
||||||
const projectStatus = new TimeToProduction(
|
const projectStatus = new TimeToProduction(
|
||||||
features,
|
features,
|
||||||
environments,
|
environments,
|
||||||
@ -168,6 +156,29 @@ describe('calculate average time to production', () => {
|
|||||||
expect(timeToProduction).toBe(21);
|
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', () => {
|
test('should sort events by createdAt', () => {
|
||||||
const projectStatus = new TimeToProduction(features, environments, [
|
const projectStatus = new TimeToProduction(features, environments, [
|
||||||
...modifyEventCreatedAt(events, 5),
|
...modifyEventCreatedAt(events, 5),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { FeatureToggle, IEvent, IProjectEnvironment } from 'lib/types';
|
import { FeatureToggle, IEvent, IProjectEnvironment } from 'lib/types';
|
||||||
|
import { ICreateEnabledDates } from '../../types/stores/project-stats-store-type';
|
||||||
|
|
||||||
interface IFeatureTimeToProdCalculationMap {
|
interface IFeatureTimeToProdCalculationMap {
|
||||||
[index: string]: IFeatureTimeToProdData;
|
[index: string]: IFeatureTimeToProdData;
|
||||||
@ -17,6 +18,7 @@ export class TimeToProduction {
|
|||||||
|
|
||||||
private events: IEvent[];
|
private events: IEvent[];
|
||||||
|
|
||||||
|
// todo: remove
|
||||||
constructor(
|
constructor(
|
||||||
features: FeatureToggle[],
|
features: FeatureToggle[],
|
||||||
productionEnvironments: IProjectEnvironment[],
|
productionEnvironments: IProjectEnvironment[],
|
||||||
@ -27,6 +29,7 @@ export class TimeToProduction {
|
|||||||
this.events = events;
|
this.events = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: remove
|
||||||
calculateAverageTimeToProd(): number {
|
calculateAverageTimeToProd(): number {
|
||||||
const featureEvents = this.getFeatureEvents();
|
const featureEvents = this.getFeatureEvents();
|
||||||
const sortedFeatureEvents =
|
const sortedFeatureEvents =
|
||||||
@ -48,6 +51,22 @@ export class TimeToProduction {
|
|||||||
return 0;
|
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 {
|
getFeatureEvents(): IFeatureTimeToProdCalculationMap {
|
||||||
return this.getProductionEvents(this.events).reduce((acc, event) => {
|
return this.getProductionEvents(this.events).reduce((acc, event) => {
|
||||||
if (acc[event.featureName]) {
|
if (acc[event.featureName]) {
|
||||||
@ -64,7 +83,8 @@ export class TimeToProduction {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
getProductionEvents(events: IEvent[]): IEvent[] {
|
// todo: remove it as DB query can handle it
|
||||||
|
private getProductionEvents(events: IEvent[]): IEvent[] {
|
||||||
return events.filter((event) => {
|
return events.filter((event) => {
|
||||||
const found = this.productionEnvironments.find(
|
const found = this.productionEnvironments.find(
|
||||||
(env) => env.name === event.environment,
|
(env) => env.name === event.environment,
|
||||||
@ -78,7 +98,7 @@ export class TimeToProduction {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateTimeToProdForFeatures(
|
private calculateTimeToProdForFeatures(
|
||||||
featureEvents: IFeatureTimeToProdCalculationMap,
|
featureEvents: IFeatureTimeToProdCalculationMap,
|
||||||
): number[] {
|
): number[] {
|
||||||
return Object.keys(featureEvents).map((featureName) => {
|
return Object.keys(featureEvents).map((featureName) => {
|
||||||
@ -94,6 +114,15 @@ export class TimeToProduction {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static calculateTimeToProdForFeatures(
|
||||||
|
items: ICreateEnabledDates[],
|
||||||
|
): number[] {
|
||||||
|
return items.map((item) =>
|
||||||
|
differenceInDays(item.enabled, item.created),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: remove as DB query can handle it
|
||||||
sortFeatureEventsByCreatedAt(
|
sortFeatureEventsByCreatedAt(
|
||||||
featureEvents: IFeatureTimeToProdCalculationMap,
|
featureEvents: IFeatureTimeToProdCalculationMap,
|
||||||
): IFeatureTimeToProdCalculationMap {
|
): IFeatureTimeToProdCalculationMap {
|
||||||
|
@ -689,11 +689,13 @@ export default class ProjectService {
|
|||||||
|
|
||||||
async getStatusUpdates(projectId: string): Promise<ICalculateStatus> {
|
async getStatusUpdates(projectId: string): Promise<ICalculateStatus> {
|
||||||
// Get all features for project with type release
|
// Get all features for project with type release
|
||||||
|
// todo: remove after release of the improved query
|
||||||
const features = await this.featureToggleStore.getAll({
|
const features = await this.featureToggleStore.getAll({
|
||||||
type: 'release',
|
type: 'release',
|
||||||
project: projectId,
|
project: projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// todo: remove after release of the improved query
|
||||||
const archivedFeatures = await this.featureToggleStore.getAll({
|
const archivedFeatures = await this.featureToggleStore.getAll({
|
||||||
archived: true,
|
archived: true,
|
||||||
type: 'release',
|
type: 'release',
|
||||||
@ -703,7 +705,12 @@ export default class ProjectService {
|
|||||||
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();
|
||||||
|
|
||||||
const [createdCurrentWindow, createdPastWindow] = await Promise.all([
|
const [
|
||||||
|
createdCurrentWindow,
|
||||||
|
createdPastWindow,
|
||||||
|
archivedCurrentWindow,
|
||||||
|
archivedPastWindow,
|
||||||
|
] = await Promise.all([
|
||||||
await this.featureToggleStore.countByDate({
|
await this.featureToggleStore.countByDate({
|
||||||
project: projectId,
|
project: projectId,
|
||||||
dateAccessor: 'created_at',
|
dateAccessor: 'created_at',
|
||||||
@ -714,9 +721,6 @@ export default class ProjectService {
|
|||||||
dateAccessor: 'created_at',
|
dateAccessor: 'created_at',
|
||||||
range: [dateMinusSixtyDays, dateMinusThirtyDays],
|
range: [dateMinusSixtyDays, dateMinusThirtyDays],
|
||||||
}),
|
}),
|
||||||
]);
|
|
||||||
|
|
||||||
const [archivedCurrentWindow, archivedPastWindow] = await Promise.all([
|
|
||||||
await this.featureToggleStore.countByDate({
|
await this.featureToggleStore.countByDate({
|
||||||
project: projectId,
|
project: projectId,
|
||||||
archived: true,
|
archived: true,
|
||||||
@ -756,6 +760,7 @@ export default class ProjectService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Get all project environments with type of production
|
// Get all project environments with type of production
|
||||||
|
// todo: remove after release of the improved query
|
||||||
const productionEnvironments =
|
const productionEnvironments =
|
||||||
await this.environmentStore.getProjectEnvironments(projectId, {
|
await this.environmentStore.getProjectEnvironments(projectId, {
|
||||||
type: 'production',
|
type: 'production',
|
||||||
@ -763,9 +768,10 @@ export default class ProjectService {
|
|||||||
|
|
||||||
// Get all events for features that correspond to feature toggle environment ON
|
// Get all events for features that correspond to feature toggle environment ON
|
||||||
// Filter out events that are not a production evironment
|
// Filter out events that are not a production evironment
|
||||||
|
// todo: remove after release of the improved query
|
||||||
const allFeatures = [...features, ...archivedFeatures];
|
const allFeatures = [...features, ...archivedFeatures];
|
||||||
|
|
||||||
|
// todo: remove after release of the improved query
|
||||||
const eventsData = await this.eventStore.query([
|
const eventsData = await this.eventStore.query([
|
||||||
{
|
{
|
||||||
op: 'forFeatures',
|
op: 'forFeatures',
|
||||||
@ -778,12 +784,32 @@ export default class ProjectService {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const currentWindowTimeToProdReadModel = new TimeToProduction(
|
// todo: remove after release of the improved query
|
||||||
|
const timeToProduction = new TimeToProduction(
|
||||||
allFeatures,
|
allFeatures,
|
||||||
productionEnvironments,
|
productionEnvironments,
|
||||||
eventsData,
|
eventsData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const avgTimeToProdCurrentWindowFast =
|
||||||
|
TimeToProduction.calculateAverageTimeToProd(
|
||||||
|
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(
|
||||||
projectId,
|
projectId,
|
||||||
@ -793,15 +819,13 @@ export default class ProjectService {
|
|||||||
return {
|
return {
|
||||||
projectId,
|
projectId,
|
||||||
updates: {
|
updates: {
|
||||||
avgTimeToProdCurrentWindow:
|
avgTimeToProdCurrentWindow,
|
||||||
currentWindowTimeToProdReadModel.calculateAverageTimeToProd(),
|
createdCurrentWindow,
|
||||||
createdCurrentWindow: createdCurrentWindow,
|
createdPastWindow,
|
||||||
createdPastWindow: createdPastWindow,
|
archivedCurrentWindow,
|
||||||
archivedCurrentWindow: archivedCurrentWindow,
|
archivedPastWindow,
|
||||||
archivedPastWindow: archivedPastWindow,
|
projectActivityCurrentWindow,
|
||||||
projectActivityCurrentWindow: projectActivityCurrentWindow,
|
projectActivityPastWindow,
|
||||||
projectActivityPastWindow: projectActivityPastWindow,
|
|
||||||
projectMembersAddedCurrentWindow:
|
|
||||||
projectMembersAddedCurrentWindow,
|
projectMembersAddedCurrentWindow,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,10 @@ const flags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_PROJECT_STATUS_API,
|
process.env.UNLEASH_EXPERIMENTAL_PROJECT_STATUS_API,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
projectStatusApiImprovements: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_PROJECT_STATUS_API_IMPROVEMENTS,
|
||||||
|
false,
|
||||||
|
),
|
||||||
newProjectOverview: parseEnvVarBoolean(
|
newProjectOverview: parseEnvVarBoolean(
|
||||||
process.env.NEW_PROJECT_OVERVIEW,
|
process.env.NEW_PROJECT_OVERVIEW,
|
||||||
false,
|
false,
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { IProjectStats } from 'lib/services/project-service';
|
import { IProjectStats } from 'lib/services/project-service';
|
||||||
|
|
||||||
|
export interface ICreateEnabledDates {
|
||||||
|
created: Date;
|
||||||
|
enabled: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IProjectStatsStore {
|
export interface IProjectStatsStore {
|
||||||
updateProjectStats(projectId: string, status: IProjectStats): Promise<void>;
|
updateProjectStats(projectId: string, status: IProjectStats): Promise<void>;
|
||||||
getProjectStats(projectId: string): Promise<IProjectStats>;
|
getProjectStats(projectId: string): Promise<IProjectStats>;
|
||||||
|
getTimeToProdDates(projectId: string): Promise<ICreateEnabledDates[]>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { IProjectStats } from 'lib/services/project-service';
|
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 */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
export default class FakeProjectStatsStore implements IProjectStatsStore {
|
export default class FakeProjectStatsStore implements IProjectStatsStore {
|
||||||
@ -13,4 +16,8 @@ export default class FakeProjectStatsStore implements IProjectStatsStore {
|
|||||||
getProjectStats(projectId: string): Promise<IProjectStats> {
|
getProjectStats(projectId: string): Promise<IProjectStats> {
|
||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimeToProdDates(): Promise<ICreateEnabledDates[]> {
|
||||||
|
throw new Error('not implemented');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user