Skip to content

Commit 6293290

Browse files
committed
Better prediction of usage (uses Linear regression)
1 parent da592d8 commit 6293290

File tree

4 files changed

+64
-25
lines changed

4 files changed

+64
-25
lines changed

apps/webapp/app/presenters/v3/UsagePresenter.server.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { env } from "~/env.server";
33
import { getUsage, getUsageSeries } from "~/services/platform.v3.server";
44
import { createTimeSeriesData } from "~/utils/graphs";
55
import { BasePresenter } from "./basePresenter.server";
6-
import { start } from "@popperjs/core";
6+
import { DataPoint, linear } from "regression";
77

88
type Options = {
99
organizationId: string;
@@ -43,12 +43,41 @@ export class UsagePresenter extends BasePresenter {
4343
);
4444

4545
//usage data from the platform
46-
const past30Days = getUsageSeries(organizationId, {
46+
const usage = getUsageSeries(organizationId, {
4747
from: startOfMonth,
4848
to: endOfMonth,
4949
window: "DAY",
5050
}).then((data) => {
51-
return createTimeSeriesData({
51+
//we want to sum it to get the total usage
52+
const current = (data?.data.reduce((acc, period) => acc + period.value, 0) ?? 0) / 100;
53+
54+
// Get the start day (the day the customer started using the product) or the first day of the month
55+
const startDay = new Date(data?.data.at(0)?.windowStart ?? startOfMonth).getDate();
56+
57+
// We want to project so we convert the data into an array of tuples [dayNumber, value]
58+
const projectionData =
59+
data?.data.map((period, index) => {
60+
// Each value should be the sum of the previous values + the current value
61+
// Adjust the day number to start from 1 when the customer started using the product
62+
return [
63+
new Date(period.windowStart).getDate() - startDay + 1,
64+
data.data.slice(0, index + 1).reduce((acc, period) => acc + period.value, 0) / 100,
65+
] as DataPoint;
66+
}) ?? ([] as DataPoint[]);
67+
68+
const result = linear(projectionData);
69+
const [a, b] = result.equation;
70+
71+
// Adjust the total days in the month based on when the customer started
72+
const totalDaysInMonth = endOfMonth.getDate() - startDay + 1;
73+
const projected = a * totalDaysInMonth + b;
74+
const overall = {
75+
current,
76+
projected,
77+
};
78+
79+
//and create daily data for the graph
80+
const timeSeries = createTimeSeriesData({
5281
startDate: startOfMonth,
5382
endDate: endOfMonth,
5483
window: "DAY",
@@ -62,6 +91,11 @@ export class UsagePresenter extends BasePresenter {
6291
date: period.date.toISOString(),
6392
dollars: (period.value ?? 0) / 100,
6493
}));
94+
95+
return {
96+
overall,
97+
timeSeries,
98+
};
6599
});
66100

67101
//usage by task
@@ -99,17 +133,7 @@ export class UsagePresenter extends BasePresenter {
99133
.sort((a, b) => b.totalCost - a.totalCost);
100134
});
101135

102-
const usage = getUsage(organizationId, { from: startOfMonth, to: endOfMonth }).then((data) => {
103-
const current = (data?.cents ?? 0) / 100;
104-
const percentageThroughMonth = new Date().getDate() / endOfMonth.getDate();
105-
return {
106-
current: current,
107-
projected: current / percentageThroughMonth,
108-
};
109-
});
110-
111136
return {
112-
usageOverTime: past30Days,
113137
usage,
114138
tasks,
115139
};

apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
7171
startDate.setUTCHours(0, 0, 0, 0);
7272

7373
const presenter = new UsagePresenter();
74-
const { usageOverTime, usage, tasks } = await presenter.call({
74+
const { usage, tasks } = await presenter.call({
7575
organizationId: organization.id,
7676
startDate,
7777
});
7878

7979
return typeddefer({
80-
usageOverTime,
8180
usage,
8281
tasks,
8382
months,
@@ -91,8 +90,7 @@ const monthDateFormatter = new Intl.DateTimeFormat("en-US", {
9190
});
9291

9392
export default function Page() {
94-
const { usage, usageOverTime, tasks, months, isCurrentMonth } =
95-
useTypedLoaderData<typeof loader>();
93+
const { usage, tasks, months, isCurrentMonth } = useTypedLoaderData<typeof loader>();
9694
const currentPlan = useCurrentPlan();
9795
const { value, replace } = useSearchParams();
9896

@@ -148,7 +146,7 @@ export default function Page() {
148146
{isCurrentMonth ? "Month-to-date" : "Usage"}
149147
</Header3>
150148
<p className="text-3xl font-medium text-text-bright">
151-
{formatCurrency(usage.current, false)}
149+
{formatCurrency(usage.overall.current, false)}
152150
</p>
153151
</div>
154152
{isCurrentMonth ? (
@@ -157,15 +155,15 @@ export default function Page() {
157155
<div className="flex flex-col gap-2 text-text-dimmed">
158156
<Header3 className="text-text-dimmed">Projected</Header3>
159157
<p className="text-3xl font-medium">
160-
{formatCurrency(usage.projected, false)}
158+
{formatCurrency(usage.overall.projected, false)}
161159
</p>
162160
</div>
163161
</>
164162
) : null}
165163
</div>
166164
<UsageBar
167-
current={usage.current}
168-
projectedUsage={isCurrentMonth ? usage.projected : undefined}
165+
current={usage.overall.current}
166+
projectedUsage={isCurrentMonth ? usage.overall.projected : undefined}
169167
isPaying={currentPlan?.v3Subscription?.isPaying ?? false}
170168
tierLimit={
171169
isCurrentMonth
@@ -190,14 +188,14 @@ export default function Page() {
190188
}
191189
>
192190
<Await
193-
resolve={usageOverTime}
191+
resolve={usage}
194192
errorElement={
195193
<div className="flex min-h-40 items-center justify-center">
196194
<Paragraph variant="small">Failed to load graph.</Paragraph>
197195
</div>
198196
}
199197
>
200-
{(past30Days) => <UsageChart data={past30Days} />}
198+
{(u) => <UsageChart data={u.timeSeries} />}
201199
</Await>
202200
</Suspense>
203201
</div>

apps/webapp/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@
9191
"@tailwindcss/container-queries": "^0.1.1",
9292
"@tanstack/react-virtual": "^3.0.4",
9393
"@team-plain/typescript-sdk": "^3.5.0",
94-
"@trigger.dev/platform": "1.0.12",
9594
"@trigger.dev/companyicons": "^1.5.35",
9695
"@trigger.dev/core": "workspace:*",
9796
"@trigger.dev/core-backend": "workspace:*",
9897
"@trigger.dev/database": "workspace:*",
9998
"@trigger.dev/otlp-importer": "workspace:*",
99+
"@trigger.dev/platform": "1.0.12",
100100
"@trigger.dev/sdk": "workspace:*",
101101
"@trigger.dev/yalt": "workspace:*",
102102
"@types/pg": "8.6.6",
@@ -152,6 +152,7 @@
152152
"react-stately": "^3.29.1",
153153
"react-use": "^17.4.0",
154154
"recharts": "^2.12.6",
155+
"regression": "^2.0.1",
155156
"remix-auth": "^3.6.0",
156157
"remix-auth-email-link": "2.0.2",
157158
"remix-auth-github": "^1.6.0",
@@ -205,6 +206,7 @@
205206
"@types/react": "18.2.69",
206207
"@types/react-collapse": "^5.0.4",
207208
"@types/react-dom": "18.2.7",
209+
"@types/regression": "^2.0.6",
208210
"@types/seedrandom": "^3.0.8",
209211
"@types/semver": "^7.3.13",
210212
"@types/simple-oauth2": "^5.0.4",

pnpm-lock.yaml

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)