153 lines
4.3 KiB
TypeScript
153 lines
4.3 KiB
TypeScript
import type { DataPoint } from "regression";
|
|
import { linear } from "regression";
|
|
import type { PrismaClientOrTransaction } from "~/db.server";
|
|
import { env } from "~/env.server";
|
|
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
|
|
import { getUsageSeries } from "~/services/platform.v3.server";
|
|
import { createTimeSeriesData } from "~/utils/graphs";
|
|
import { BasePresenter } from "./basePresenter.server";
|
|
|
|
type Options = {
|
|
organizationId: string;
|
|
startDate: Date;
|
|
};
|
|
|
|
export type TaskUsageItem = {
|
|
taskIdentifier: string;
|
|
runCount: number;
|
|
averageDuration: number;
|
|
averageCost: number;
|
|
totalDuration: number;
|
|
totalCost: number;
|
|
totalBaseCost: number;
|
|
};
|
|
|
|
export type UsageSeriesData = {
|
|
date: string;
|
|
dollars: number;
|
|
}[];
|
|
|
|
export class UsagePresenter extends BasePresenter {
|
|
public async call({ organizationId, startDate }: Options) {
|
|
if (isNaN(startDate.getTime())) {
|
|
throw new Error("Invalid start date");
|
|
}
|
|
|
|
//month period
|
|
const startOfMonth = new Date(startDate);
|
|
startOfMonth.setUTCDate(1);
|
|
startOfMonth.setUTCHours(0, 0, 0, 0);
|
|
|
|
const endOfMonth = new Date(
|
|
startOfMonth.getFullYear(),
|
|
startOfMonth.getMonth() + 1,
|
|
0,
|
|
23,
|
|
59,
|
|
59,
|
|
999
|
|
);
|
|
|
|
//usage data from the platform
|
|
const usage = getUsageSeries(organizationId, {
|
|
from: startOfMonth,
|
|
to: endOfMonth,
|
|
window: "DAY",
|
|
}).then((data) => {
|
|
//we want to sum it to get the total usage
|
|
const current = (data?.data.reduce((acc, period) => acc + period.value, 0) ?? 0) / 100;
|
|
|
|
// Get the start day (the day the customer started using the product) or the first day of the month
|
|
const startDay = new Date(data?.data.at(0)?.windowStart ?? startOfMonth).getDate();
|
|
|
|
// We want to project so we convert the data into an array of tuples [dayNumber, value]
|
|
const projectionData =
|
|
data?.data.map((period, index) => {
|
|
// Each value should be the sum of the previous values + the current value
|
|
// Adjust the day number to start from 1 when the customer started using the product
|
|
return [
|
|
new Date(period.windowStart).getDate() - startDay + 1,
|
|
data.data.slice(0, index + 1).reduce((acc, period) => acc + period.value, 0) / 100,
|
|
] as DataPoint;
|
|
}) ?? ([] as DataPoint[]);
|
|
|
|
const result = linear(projectionData);
|
|
const [a, b] = result.equation;
|
|
|
|
// Adjust the total days in the month based on when the customer started
|
|
const totalDaysInMonth = endOfMonth.getDate() - startDay + 1;
|
|
const projected = a * totalDaysInMonth + b;
|
|
const overall = {
|
|
current,
|
|
projected,
|
|
};
|
|
|
|
//and create daily data for the graph
|
|
const timeSeries = createTimeSeriesData({
|
|
startDate: startOfMonth,
|
|
endDate: endOfMonth,
|
|
window: "DAY",
|
|
data: data
|
|
? data.data.map((period) => ({
|
|
date: new Date(period.windowStart),
|
|
value: period.value,
|
|
}))
|
|
: [],
|
|
}).map((period) => ({
|
|
date: period.date.toISOString(),
|
|
dollars: (period.value ?? 0) / 100,
|
|
}));
|
|
|
|
return {
|
|
overall,
|
|
timeSeries,
|
|
};
|
|
});
|
|
|
|
//usage by task
|
|
const tasks = await getTaskUsageByOrganization(
|
|
organizationId,
|
|
startOfMonth,
|
|
endOfMonth,
|
|
this._replica
|
|
);
|
|
|
|
return {
|
|
usage,
|
|
tasks,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function getTaskUsageByOrganization(
|
|
organizationId: string,
|
|
startOfMonth: Date,
|
|
endOfMonth: Date,
|
|
replica: PrismaClientOrTransaction
|
|
) {
|
|
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
|
|
organizationId,
|
|
"standard"
|
|
);
|
|
const [queryError, tasks] = await clickhouse.taskRuns.getTaskUsageByOrganization({
|
|
startTime: startOfMonth.getTime(),
|
|
endTime: endOfMonth.getTime(),
|
|
organizationId,
|
|
});
|
|
|
|
if (queryError) {
|
|
throw queryError;
|
|
}
|
|
|
|
return tasks
|
|
.map((task) => ({
|
|
taskIdentifier: task.task_identifier,
|
|
runCount: Number(task.run_count),
|
|
averageDuration: Number(task.average_duration),
|
|
averageCost: Number(task.average_cost) + env.CENTS_PER_RUN / 100,
|
|
totalDuration: Number(task.total_duration),
|
|
totalCost: Number(task.total_cost) + Number(task.total_base_cost),
|
|
}))
|
|
.sort((a, b) => b.totalCost - a.totalCost);
|
|
}
|