import { type ClickHouse } from "@internal/clickhouse"; import type { TaskRunStatus } from "@trigger.dev/database"; import { QUEUED_STATUSES } from "~/components/runs/v3/TaskRunStatus"; export type DailyTaskActivity = Record)[]>; export type CurrentRunningStats = Record; export type AverageDurations = Record; export interface EnvironmentMetricsRepository { getDailyTaskActivity(options: { organizationId: string; projectId: string; environmentId: string; days: number; tasks: string[]; }): Promise; getCurrentRunningStats(options: { organizationId: string; projectId: string; environmentId: string; days: number; tasks: string[]; }): Promise; getAverageDurations(options: { organizationId: string; projectId: string; environmentId: string; days: number; tasks: string[]; }): Promise; } export type ClickHouseEnvironmentMetricsRepositoryOptions = { clickhouse: ClickHouse; }; export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetricsRepository { constructor(private readonly options: ClickHouseEnvironmentMetricsRepositoryOptions) {} public async getDailyTaskActivity({ organizationId, projectId, environmentId, days, tasks, }: { organizationId: string; projectId: string; environmentId: string; days: number; tasks: string[]; }): Promise { if (tasks.length === 0) { return {}; } const [queryError, activity] = await this.options.clickhouse.taskRuns.getTaskActivity({ organizationId, projectId, environmentId, days, }); if (queryError) { throw queryError; } return fillInDailyTaskActivity( activity.map((a) => ({ taskIdentifier: a.task_identifier, status: a.status as TaskRunStatus, day: new Date(a.day), count: BigInt(a.count), })), days ); } public async getCurrentRunningStats({ organizationId, projectId, environmentId, days, tasks, }: { organizationId: string; projectId: string; environmentId: string; days: number; tasks: string[]; }): Promise { if (tasks.length === 0) { return {}; } const [queryError, stats] = await this.options.clickhouse.taskRuns.getCurrentRunningStats({ organizationId, projectId, environmentId, days, }); if (queryError) { throw queryError; } return fillInCurrentRunningStats( stats.map((s) => ({ taskIdentifier: s.task_identifier, status: s.status as TaskRunStatus, count: BigInt(s.count), })), tasks ); } public async getAverageDurations({ organizationId, projectId, environmentId, days, tasks, }: { organizationId: string; projectId: string; environmentId: string; days: number; tasks: string[]; }): Promise { if (tasks.length === 0) { return {}; } const [queryError, durations] = await this.options.clickhouse.taskRuns.getAverageDurations({ organizationId, projectId, environmentId, days, }); if (queryError) { throw queryError; } return Object.fromEntries(durations.map((d) => [d.task_identifier, Number(d.duration)])); } } type TaskActivityResults = Array<{ taskIdentifier: string; status: TaskRunStatus; day: Date; count: bigint; }>; function fillInDailyTaskActivity(activity: TaskActivityResults, days: number): DailyTaskActivity { //today with no time const today = new Date(); today.setUTCHours(0, 0, 0, 0); return activity.reduce((acc, a) => { let existingTask = acc[a.taskIdentifier]; if (!existingTask) { existingTask = []; //populate the array with the past 7 days for (let i = days; i >= 0; i--) { const day = new Date(today); day.setUTCDate(today.getDate() - i); day.setUTCHours(0, 0, 0, 0); existingTask.push({ day: day.toISOString(), ["COMPLETED_SUCCESSFULLY"]: 0, } as { day: string } & Record); } acc[a.taskIdentifier] = existingTask; } const dayString = a.day.toISOString(); const day = existingTask.find((d) => d.day === dayString); if (!day) { return acc; } day[a.status] = Number(a.count); return acc; }, {} as DailyTaskActivity); } type CurrentRunningStatsResults = Array<{ taskIdentifier: string; status: TaskRunStatus; count: bigint; }>; function fillInCurrentRunningStats( stats: CurrentRunningStatsResults, tasks: string[] ): CurrentRunningStats { //create an object combining the queued and concurrency counts const result: Record = {}; for (const task of tasks) { const queued = stats.filter( (q) => q.taskIdentifier === task && QUEUED_STATUSES.includes(q.status) ); const queuedCount = queued.length === 0 ? 0 : queued.reduce((acc, q) => { return acc + Number(q.count); }, 0); const running = stats.filter((r) => r.taskIdentifier === task && r.status === "EXECUTING"); const runningCount = running.length === 0 ? 0 : running.reduce((acc, r) => { return acc + Number(r.count); }, 0); result[task] = { queued: queuedCount, running: runningCount, }; } return result; }