add execution statistics to workflow list

This commit is contained in:
Ivan Atanasov 2024-10-30 11:20:34 +01:00
parent 497d637fc5
commit 197ef5c27b
No known key found for this signature in database
7 changed files with 89 additions and 53 deletions

View file

@ -1,28 +1,23 @@
import { Response, NextFunction } from 'express';
import type { WorkflowStatistics } from '@/databases/entities/workflow-statistics';
import { StatisticsNames } from '@/databases/entities/workflow-statistics';
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository';
import { Get, Middleware, RestController } from '@/decorators';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { IWorkflowStatisticsDataLoaded } from '@/interfaces';
import type { IWorkflowStatisticsDataLoaded, WorkflowStatisticsData } from '@/interfaces';
import { Logger } from '@/logging/logger.service';
import { StatisticsRequest } from './workflow-statistics.types';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
interface WorkflowStatisticsData<T> {
productionSuccess: T;
productionError: T;
manualSuccess: T;
manualError: T;
}
import { StatisticsRequest } from './workflow-statistics.types';
@RestController('/workflow-stats')
export class WorkflowStatisticsController {
constructor(
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowStatisticsRepository: WorkflowStatisticsRepository,
private readonly workflowStatisticsService: WorkflowStatisticsService,
private readonly logger: Logger,
) {}
@ -53,12 +48,12 @@ export class WorkflowStatisticsController {
@Get('/:id/counts/')
async getCounts(req: StatisticsRequest.GetOne): Promise<WorkflowStatisticsData<number>> {
return await this.getData(req.params.id, 'count', 0);
return await this.workflowStatisticsService.getData(req.params.id, 'count', 0);
}
@Get('/:id/times/')
async getTimes(req: StatisticsRequest.GetOne): Promise<WorkflowStatisticsData<Date | null>> {
return await this.getData(req.params.id, 'latestEvent', null);
return await this.workflowStatisticsService.getData(req.params.id, 'latestEvent', null);
}
@Get('/:id/data-loaded/')
@ -79,42 +74,4 @@ export class WorkflowStatisticsController {
dataLoaded: stats ? true : false,
};
}
private async getData<
C extends 'count' | 'latestEvent',
D = WorkflowStatistics[C] extends number ? 0 : null,
>(workflowId: string, columnName: C, defaultValue: WorkflowStatistics[C] | D) {
const stats = await this.workflowStatisticsRepository.find({
select: [columnName, 'name'],
where: { workflowId },
});
const data: WorkflowStatisticsData<WorkflowStatistics[C] | D> = {
productionSuccess: defaultValue,
productionError: defaultValue,
manualSuccess: defaultValue,
manualError: defaultValue,
};
stats.forEach(({ name, [columnName]: value }) => {
switch (name) {
case StatisticsNames.manualError:
data.manualError = value;
break;
case StatisticsNames.manualSuccess:
data.manualSuccess = value;
break;
case StatisticsNames.productionError:
data.productionError = value;
break;
case StatisticsNames.productionSuccess:
data.productionSuccess = value;
}
});
return data;
}
}

View file

@ -306,6 +306,13 @@ export interface IWorkflowStatisticsDataLoaded {
dataLoaded: boolean;
}
export interface WorkflowStatisticsData<T> {
productionSuccess: T;
productionError: T;
manualSuccess: T;
manualError: T;
}
// ----------------------------------
// community nodes
// ----------------------------------

View file

@ -1,7 +1,7 @@
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import { Service } from 'typedi';
import { StatisticsNames } from '@/databases/entities/workflow-statistics';
import { StatisticsNames, WorkflowStatistics } from '@/databases/entities/workflow-statistics';
import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository';
import { EventService } from '@/events/event.service';
import { Logger } from '@/logging/logger.service';
@ -9,6 +9,7 @@ import { UserService } from '@/services/user.service';
import { TypedEmitter } from '@/typed-emitter';
import { OwnershipService } from './ownership.service';
import { WorkflowStatisticsData } from '@/interfaces';
type WorkflowStatisticsEvents = {
nodeFetchedData: { workflowId: string; node: INode };
@ -129,4 +130,42 @@ export class WorkflowStatisticsService extends TypedEmitter<WorkflowStatisticsEv
this.eventService.emit('first-workflow-data-loaded', metrics);
}
async getData<
C extends 'count' | 'latestEvent',
D = WorkflowStatistics[C] extends number ? 0 : null,
>(workflowId: string, columnName: C, defaultValue: WorkflowStatistics[C] | D) {
const stats = await this.repository.find({
select: [columnName, 'name'],
where: { workflowId },
});
const data: WorkflowStatisticsData<WorkflowStatistics[C] | D> = {
productionSuccess: defaultValue,
productionError: defaultValue,
manualSuccess: defaultValue,
manualError: defaultValue,
};
stats.forEach(({ name, [columnName]: value }) => {
switch (name) {
case StatisticsNames.manualError:
data.manualError = value;
break;
case StatisticsNames.manualSuccess:
data.manualSuccess = value;
break;
case StatisticsNames.productionError:
data.productionError = value;
break;
case StatisticsNames.productionSuccess:
data.productionSuccess = value;
}
});
return data;
}
}

View file

@ -28,7 +28,12 @@ export declare namespace WorkflowRequest {
type Get = AuthenticatedRequest<{ workflowId: string }>;
type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & {
type GetMany = AuthenticatedRequest<
{},
{},
{},
ListQuery.Params & { includeScopes?: string; includeExecutionStatistics?: string }
> & {
listQueryOptions: ListQuery.Options;
};

View file

@ -31,6 +31,7 @@ import { OwnershipService } from '@/services/ownership.service';
import { ProjectService } from '@/services/project.service';
import { RoleService } from '@/services/role.service';
import { TagService } from '@/services/tag.service';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import * as WorkflowHelpers from '@/workflow-helpers';
import { WorkflowHistoryService } from './workflow-history/workflow-history.service.ee';
@ -55,9 +56,15 @@ export class WorkflowService {
private readonly projectService: ProjectService,
private readonly executionRepository: ExecutionRepository,
private readonly eventService: EventService,
private readonly workflowStatisticsService: WorkflowStatisticsService,
) {}
async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) {
async getMany(
user: User,
options?: ListQuery.Options,
includeScopes?: boolean,
includeExecutionStatistics?: boolean,
) {
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
scopes: ['workflow:read'],
});
@ -74,8 +81,22 @@ export class WorkflowService {
workflows = workflows.map((w) => this.roleService.addScopes(w, user, projectRelations));
}
if (includeExecutionStatistics) {
workflows = await Promise.all(
workflows.map(async (w) => {
const stats = await this.workflowStatisticsService.getData(w.id, 'count', 0);
return {
...w,
executionStatistics: {
errors: stats.productionError,
successes: stats.productionSuccess,
},
};
}),
);
}
workflows.forEach((w) => {
// @ts-expect-error: This is to emulate the old behaviour of removing the shared
// field as part of `addOwnedByAndSharedWith`. We need this field in `addScopes`
// though. So to avoid leaking the information we just delete it.
delete w.shared;

View file

@ -198,6 +198,7 @@ export class WorkflowsController {
req.user,
req.listQueryOptions,
!!req.query.includeScopes,
!!req.query.includeExecutionStatistics,
);
res.json({ count, data });

View file

@ -276,6 +276,11 @@ export interface WorkflowMetadata {
templateCredsSetupCompleted?: boolean;
}
export interface ExecutionStatistics {
errors: number;
successes: number;
}
// Almost identical to cli.Interfaces.ts
export interface IWorkflowDb {
id: string;
@ -294,6 +299,7 @@ export interface IWorkflowDb {
versionId: string;
usedCredentials?: IUsedCredential[];
meta?: WorkflowMetadata;
executionStatistics?: ExecutionStatistics;
}
// Identical to cli.Interfaces.ts