diff --git a/apps/webapp/app/presenters/v3/UsagePresenter.server.ts b/apps/webapp/app/presenters/v3/UsagePresenter.server.ts index d001f04f8..2768baad3 100644 --- a/apps/webapp/app/presenters/v3/UsagePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/UsagePresenter.server.ts @@ -8,6 +8,15 @@ type Options = { organizationId: string; }; +export type TaskUsageItem = { + taskIdentifier: string; + runCount: number; + averageDuration: number; + averageCost: number; + totalDuration: number; + totalCost: number; +}; + export class UsagePresenter extends BasePresenter { public async call({ organizationId }: Options) { //periods @@ -18,6 +27,7 @@ export class UsagePresenter extends BasePresenter { endOfToday.setDate(endOfToday.getDate() + 1); endOfToday.setHours(23, 59, 59, 999); + //usage data from the platform const past30Days = getUsageSeries(organizationId, { from: thirtyDaysAgo, to: endOfToday, @@ -39,6 +49,40 @@ export class UsagePresenter extends BasePresenter { })); }); + //usage by task + const tasks = this._replica.$queryRaw` + SELECT + tr."taskIdentifier", + COUNT(*) AS "runCount", + AVG(tr."usageDurationMs") AS "averageDuration", + SUM(tr."usageDurationMs") AS "totalDuration", + AVG(tr."costInCents") / 100.0 AS "averageCost", + SUM(tr."costInCents") / 100.0 AS "totalCost" + FROM + ${sqlDatabaseSchema}."TaskRun" tr + JOIN ${sqlDatabaseSchema}."Project" pr ON pr.id = tr."projectId" + JOIN ${sqlDatabaseSchema}."Organization" org ON org.id = pr."organizationId" + WHERE + tr."createdAt" > ${thirtyDaysAgo} + AND tr."createdAt" < ${endOfToday} + + AND org.id = ${organizationId} + GROUP BY + tr."taskIdentifier" + ORDER BY + "totalCost" DESC; + `.then((data) => { + return data.map((item) => ({ + taskIdentifier: item.taskIdentifier, + runCount: Number(item.runCount), + averageDuration: Number(item.averageDuration), + averageCost: Number(item.averageCost), + totalDuration: Number(item.totalDuration), + totalCost: Number(item.totalCost), + })); + }); + + //month-to-date usage data with projection const startOfMonth = new Date(); startOfMonth.setDate(1); startOfMonth.setHours(0, 0, 0, 0); @@ -58,6 +102,7 @@ export class UsagePresenter extends BasePresenter { return { past30Days, usage, + tasks, }; } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx index ff656cdbc..79946456b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx @@ -11,13 +11,23 @@ import { featuresForRequest } from "~/features.server"; import { getUsage, getUsageSeries } from "~/services/platform.v3.server"; import { requireUserId } from "~/services/session.server"; import { createTimeSeriesData } from "~/utils/graphs"; -import { formatCurrency } from "~/utils/numberFormatter"; +import { formatCurrency, formatNumber } from "~/utils/numberFormatter"; import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { UsagePresenter } from "~/presenters/v3/UsagePresenter.server"; import { Suspense } from "react"; import { Await } from "@remix-run/react"; import { Spinner } from "~/components/primitives/Spinner"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; +import { Paragraph } from "~/components/primitives/Paragraph"; export async function loader({ params, request }: LoaderFunctionArgs) { await requireUserId(request); @@ -37,11 +47,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } const presenter = new UsagePresenter(); - const { past30Days, usage } = await presenter.call({ organizationId: organization.id }); + const { past30Days, usage, tasks } = await presenter.call({ organizationId: organization.id }); return typeddefer({ past30Days, usage, + tasks, }); } @@ -63,7 +74,7 @@ const tooltipStyle = { }; export default function ChoosePlanPage() { - const { usage, past30Days } = useTypedLoaderData(); + const { usage, past30Days, tasks } = useTypedLoaderData(); const currentPlan = useCurrentPlan(); return ( @@ -177,6 +188,63 @@ export default function ChoosePlanPage() { +
+ Tasks + }> + + {(tasks) => { + return ( + + + + Task + Runs + Average duration + Average cost + Total duration + Total cost + + + + {tasks.length === 0 ? ( + + + No runs to display yet. + + + ) : ( + tasks.map((task) => ( + + {task.taskIdentifier} + + {formatNumber(task.runCount)} + + + {formatDurationMilliseconds(task.averageDuration, { + style: "short", + })} + + + {formatCurrency(task.averageCost, false)} + + + {formatDurationMilliseconds(task.totalDuration, { + style: "short", + })} + + + {formatCurrency(task.totalCost, false)} + + + )) + )} + +
+ ); + }} +
+
+
diff --git a/apps/webapp/app/utils/graphs.ts b/apps/webapp/app/utils/graphs.ts index a3eb92b31..622071454 100644 --- a/apps/webapp/app/utils/graphs.ts +++ b/apps/webapp/app/utils/graphs.ts @@ -19,12 +19,6 @@ export function createTimeSeriesData({ startDate, endDate, window = "DAY", da const foundData = data.find((d) => { const time = d.date.getTime(); const inRange = time >= periodStart.getTime() && time < periodEnd.getTime(); - console.log({ - time, - periodStart: periodStart.getTime(), - periodEnd: periodEnd.getTime(), - inRange, - }); return inRange; }); if (!foundData) {