Skip to content

Commit 610ea59

Browse files
committed
fix(webapp): label model sparkline tooltips with their real bucket times
The Your models sparklines use dynamic bucket sizes (6h at 7d, etc.), but the tooltip assumed hourly buckets and showed wrong dates. Thread the bucket interval and start through so each bar is labelled correctly. Also pin the library tab cross-tenant p50 TTFC column to a fixed 7-day window so it no longer follows the Your models time selector.
1 parent 57dd835 commit 610ea59

3 files changed

Lines changed: 51 additions & 11 deletions

File tree

apps/webapp/app/components/primitives/UsageSparkline.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ type UsageDatum = { date: Date; count: number };
1717
type UnitLabel = { singular: string; plural: string };
1818

1919
export type UsageSparklineProps = {
20-
/** Trailing 24 hourly buckets; the last entry is the most recent hour. */
20+
/** Equal-width time buckets, oldest first. */
2121
data?: number[];
22+
/** Epoch ms of the first bucket's start. When omitted, the last bucket is anchored to now. */
23+
bucketStartMs?: number;
24+
/** Width of each bucket in ms. Defaults to one hour. */
25+
bucketIntervalMs?: number;
2226
/** Bar colour. Defaults to blue. */
2327
color?: string;
2428
/** Unit shown in the tooltip (e.g. calls, tokens). */
@@ -36,6 +40,8 @@ export type UsageSparklineProps = {
3640
*/
3741
export function UsageSparkline({
3842
data,
43+
bucketStartMs,
44+
bucketIntervalMs,
3945
color = "#3B82F6",
4046
unitLabel = { singular: "call", plural: "calls" },
4147
formatTotal,
@@ -48,11 +54,13 @@ export function UsageSparkline({
4854
const total = data.reduce((a, b) => a + b, 0);
4955
const max = Math.max(...data);
5056

51-
// Map the 24-bucket array to dated points so the tooltip can show the
52-
// hour each bar represents. Bucket i is `23 - i` hours before now.
53-
const now = new Date();
57+
// Map each bucket to a dated point so the tooltip can show the window it
58+
// represents. Buckets are `intervalMs` wide; if the caller didn't pass the
59+
// first bucket's start, anchor the last bucket to now (hourly default).
60+
const intervalMs = bucketIntervalMs ?? 3600_000;
61+
const startMs = bucketStartMs ?? Date.now() - (data.length - 1) * intervalMs;
5462
const chartData: UsageDatum[] = data.map((count, i) => ({
55-
date: new Date(now.getTime() - (data.length - 1 - i) * 3600_000),
63+
date: new Date(startMs + i * intervalMs),
5664
count,
5765
}));
5866

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -712,10 +712,23 @@ export class ModelRegistryPresenter extends BasePresenter {
712712
responseModels: string[],
713713
from: Date,
714714
to: Date
715-
): Promise<{ calls: Record<string, number[]>; tokens: Record<string, number[]> }> {
716-
if (responseModels.length === 0) return { calls: {}, tokens: {} };
717-
715+
): Promise<{
716+
calls: Record<string, number[]>;
717+
tokens: Record<string, number[]>;
718+
bucketIntervalMs: number;
719+
bucketStartMs: number;
720+
}> {
718721
const intervalSeconds = sparklineBucketSeconds(to.getTime() - from.getTime());
722+
const intervalMs = intervalSeconds * 1000;
723+
// Epoch-aligned start of the first bucket, matching sparklineBucketKeys and
724+
// ClickHouse toStartOfInterval. Returned so the sparkline tooltip can label
725+
// each bar with its true time rather than assuming hourly buckets.
726+
const bucketStartMs = Math.floor(from.getTime() / intervalMs) * intervalMs;
727+
728+
if (responseModels.length === 0) {
729+
return { calls: {}, tokens: {}, bucketIntervalMs: intervalMs, bucketStartMs };
730+
}
731+
719732
const bucketKeys = sparklineBucketKeys(from, to, intervalSeconds);
720733

721734
// intervalSeconds is a server-derived integer from a fixed ladder, so it's
@@ -760,6 +773,8 @@ export class ModelRegistryPresenter extends BasePresenter {
760773
return {
761774
calls: this.#buildSparklineMap(callsResult, responseModels, bucketKeys),
762775
tokens: this.#buildSparklineMap(tokensResult, responseModels, bucketKeys),
776+
bucketIntervalMs: intervalMs,
777+
bucketStartMs,
763778
};
764779
}
765780

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
138138
const to = parseFiniteInt(url.searchParams.get("to"));
139139
const time = timeFilterFromTo({ period, from, to, defaultPeriod: "7d" });
140140

141-
// popularModels = cross-tenant aggregate (powers the library's p50 TTFC column).
141+
// popularModels powers the library tab's cross-tenant p50 TTFC column — a
142+
// stable "typical latency" reference, so it always uses a fixed 7-day window
143+
// independent of the Your models time selector (the library tab has none).
144+
const popularTo = new Date();
145+
const popularFrom = new Date(popularTo.getTime() - 7 * 24 * 60 * 60 * 1000);
146+
142147
// projectUsage = tenant-scoped models with usage in this env (the "Your models" tab).
143148
const [popularModels, projectUsage] = await Promise.all([
144-
presenter.getPopularModels(time.from, time.to, 50),
149+
presenter.getPopularModels(popularFrom, popularTo, 50),
145150
presenter.getProjectModelUsage(project.id, environment.id, time.from, time.to),
146151
]);
147152

@@ -1172,6 +1177,8 @@ function YourModelsTab({
11721177
usage,
11731178
callSparklines,
11741179
tokenSparklines,
1180+
bucketStartMs,
1181+
bucketIntervalMs,
11751182
organizationId,
11761183
projectId,
11771184
environmentId,
@@ -1186,6 +1193,8 @@ function YourModelsTab({
11861193
usage: ProjectModelUsageItem[];
11871194
callSparklines: Record<string, number[]>;
11881195
tokenSparklines: Record<string, number[]>;
1196+
bucketStartMs: number;
1197+
bucketIntervalMs: number;
11891198
organizationId: string;
11901199
projectId: string;
11911200
environmentId: string;
@@ -1304,11 +1313,17 @@ function YourModelsTab({
13041313
{u.avgTps > 0 ? u.avgTps.toFixed(0) : "—"}
13051314
</TableCell>
13061315
<TableCell onClick={select}>
1307-
<UsageSparkline data={callSparklines[u.responseModel]} />
1316+
<UsageSparkline
1317+
data={callSparklines[u.responseModel]}
1318+
bucketStartMs={bucketStartMs}
1319+
bucketIntervalMs={bucketIntervalMs}
1320+
/>
13081321
</TableCell>
13091322
<TableCell onClick={select}>
13101323
<UsageSparkline
13111324
data={tokenSparklines[u.responseModel]}
1325+
bucketStartMs={bucketStartMs}
1326+
bucketIntervalMs={bucketIntervalMs}
13121327
color="#10B981"
13131328
unitLabel={{ singular: "token", plural: "tokens" }}
13141329
formatTotal={(t) => formatNumberCompact(t)}
@@ -1459,6 +1474,8 @@ export default function ModelsPage() {
14591474
usage={projectUsage}
14601475
callSparklines={usageSparklines.calls}
14611476
tokenSparklines={usageSparklines.tokens}
1477+
bucketStartMs={usageSparklines.bucketStartMs}
1478+
bucketIntervalMs={usageSparklines.bucketIntervalMs}
14621479
organizationId={organizationId}
14631480
projectId={projectId}
14641481
environmentId={environmentId}

0 commit comments

Comments
 (0)