diff --git a/components/charts/tracker-analytics.tsx b/components/charts/tracker-analytics.tsx
index d204b4b..097a48b 100644
--- a/components/charts/tracker-analytics.tsx
+++ b/components/charts/tracker-analytics.tsx
@@ -418,7 +418,11 @@ function BreakdownPanel({
) : (
-
+
)}
);
@@ -502,7 +506,7 @@ function RankedPanel({
>
) : (
-
+
)}
);
@@ -513,16 +517,20 @@ function RankedPanel({
function EmptyRange({
message,
range,
+ ranges,
}: {
message: string;
range: TrackerRangeKey;
+ ranges?: TrackerRange[];
}) {
+ // Describe the range from the panel's own list, not the global table: the
+ // rollup-only panels relabel 1D as today's UTC day, and the global lookup
+ // would name a rolling 24h window they never queried.
+ const described = ranges?.find((r) => r.key === range) ?? trackerRange(range);
return (
{message}{" "}
-
- ({trackerRange(range).description.toLowerCase()})
-
+ ({described.description.toLowerCase()})
);
}
diff --git a/lib/tracker/panels.ts b/lib/tracker/panels.ts
index 94239c5..26b6b7a 100644
--- a/lib/tracker/panels.ts
+++ b/lib/tracker/panels.ts
@@ -93,6 +93,18 @@ export async function resolveDays(
return Math.min(Math.max(spanDays, 1), MAX_ALL_DAYS);
}
+// The four rollup-only panels (exit pages, devices, browsers, operating
+// systems) offer the "1D" tab, but that range is defined with `minutes`, not
+// `days` — it is a raw-event window for every other panel. Handing it to
+// resolveDays finds no `days`, falls through to tracker_first_day and returns
+// the project's entire history, so the 1D tab rendered All-time totals: on a
+// site younger than 30 days, 1D, 1M and All were the same number. A sub-day
+// window against a day-resolution rollup is one day — today's UTC rollup —
+// which is exactly what ROLLUP_ONLY_RANGES already documents it to mean.
+export function rollupDays(range: TrackerRange, days: number): number {
+ return isRawRange(range) ? 1 : days;
+}
+
/** Fetch one panel. Errors surface as an empty panel rather than a broken page. */
export async function fetchPanel(
sb: Sb,
@@ -182,7 +194,7 @@ export async function fetchPanel(
// Rollup-only: tracker_exit_sessions keys on a date, not a timestamp.
const { data } = await sb.rpc("tracker_top_exit_pages", {
p_project: projectId,
- days,
+ days: rollupDays(range, days),
lim: TOP_N,
});
return (
@@ -300,7 +312,7 @@ export async function fetchPanel(
// device_type / browser / os columns.
const { data } = await sb.rpc("tracker_device_totals", {
p_project: projectId,
- days,
+ days: rollupDays(range, days),
});
const rows = (
(data ?? []) as Array<{
diff --git a/lib/tracker/ranges.ts b/lib/tracker/ranges.ts
index 4e6c226..1d28740 100644
--- a/lib/tracker/ranges.ts
+++ b/lib/tracker/ranges.ts
@@ -91,8 +91,16 @@ export const PANEL_RANGE_KEYS: Record = {
exitPages: ROLLUP_ONLY_RANGES,
};
+// These panels answer "1D" from today's UTC rollup, so the shared description
+// ("Last 24 hours") would promise a rolling window they cannot serve. Relabel
+// it here, where the list is already being narrowed, so the tab tooltip
+// describes the number the panel actually returns.
+const ROLLUP_1D_DESCRIPTION = "Today so far, UTC day";
+
export function rangesForPanel(panel: string): TrackerRange[] {
const allowed = PANEL_RANGE_KEYS[panel];
if (!allowed) return TRACKER_RANGES;
- return TRACKER_RANGES.filter((r) => allowed.includes(r.key));
+ return TRACKER_RANGES.filter((r) => allowed.includes(r.key)).map((r) =>
+ r.key === "1d" ? { ...r, description: ROLLUP_1D_DESCRIPTION } : r,
+ );
}
diff --git a/tests/tracker-rollup-ranges.test.ts b/tests/tracker-rollup-ranges.test.ts
new file mode 100644
index 0000000..dde1818
--- /dev/null
+++ b/tests/tracker-rollup-ranges.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, it } from "vitest";
+import { fetchPanel, rollupDays } from "@/lib/tracker/panels";
+import {
+ PANEL_RANGE_KEYS,
+ ROLLUP_ONLY_RANGES,
+ rangesForPanel,
+ trackerRange,
+} from "@/lib/tracker/ranges";
+
+// The "1D" tab is a raw-event range (minutes, no days) for most panels, but the
+// four rollup-only panels accept it too. Before this was pinned, those panels
+// resolved it to the project's whole history, so 1D and All returned the same
+// number and nobody could tell a spike from a total.
+
+// Records the arguments each RPC is called with; every rollup RPC here returns
+// an empty list, which is enough since we assert on the call, not the payload.
+function spySb() {
+ const calls: Array<{ fn: string; args: Record }> = [];
+ return {
+ calls,
+ rpc(fn: string, args: Record) {
+ calls.push({ fn, args });
+ return Promise.resolve({ data: [], error: null });
+ },
+ };
+}
+
+describe("rollupDays", () => {
+ it("collapses a sub-day range to a single rollup day", () => {
+ expect(rollupDays(trackerRange("1d"), 999)).toBe(1);
+ expect(rollupDays(trackerRange("1h"), 999)).toBe(1);
+ });
+
+ it("passes rollup ranges through untouched", () => {
+ expect(rollupDays(trackerRange("1w"), 7)).toBe(7);
+ expect(rollupDays(trackerRange("1m"), 30)).toBe(30);
+ // "All" resolves upstream in resolveDays; whatever it computed survives.
+ expect(rollupDays(trackerRange("all"), 412)).toBe(412);
+ });
+});
+
+describe("rollup-only panels at the 1D tab", () => {
+ const rollupOnly = Object.keys(PANEL_RANGE_KEYS);
+
+ it("offers 1D on every rollup-only panel", () => {
+ expect(rollupOnly.length).toBeGreaterThan(0);
+ for (const panel of rollupOnly) {
+ expect(PANEL_RANGE_KEYS[panel]).toContain("1d");
+ }
+ expect(ROLLUP_ONLY_RANGES).toContain("1d");
+ });
+
+ it("asks for one day, not the resolved history span", async () => {
+ for (const panel of rollupOnly) {
+ const sb = spySb();
+ // 412 is what resolveDays hands back for a raw range today: the whole
+ // project history. The panel must not use it.
+ await fetchPanel(sb as never, "p1", panel as never, trackerRange("1d"), 412);
+ expect(sb.calls).toHaveLength(1);
+ expect(sb.calls[0].args.days, `${panel} at 1D`).toBe(1);
+ }
+ });
+
+ it("still honours a real rollup range", async () => {
+ for (const panel of rollupOnly) {
+ const sb = spySb();
+ await fetchPanel(sb as never, "p1", panel as never, trackerRange("1m"), 30);
+ expect(sb.calls[0].args.days, `${panel} at 1M`).toBe(30);
+ }
+ });
+
+ it("labels the 1D tab as the UTC day it actually reads", () => {
+ for (const panel of rollupOnly) {
+ const ranges = rangesForPanel(panel);
+ expect(ranges).toHaveLength(ROLLUP_ONLY_RANGES.length);
+ expect(
+ ranges.find((r) => r.key === "1d")?.description,
+ `${panel} 1D tooltip`,
+ ).toBe("Today so far, UTC day");
+ }
+ });
+
+ it("leaves the raw-capable panels alone", async () => {
+ expect(rangesForPanel("pages")).toHaveLength(7);
+ expect(rangesForPanel("pages").find((r) => r.key === "1d")?.description).toBe(
+ "Last 24 hours, hourly buckets",
+ );
+ // The top-pages panel keeps its rolling 24h source at 1D — that is why it
+ // reads ~4k for /login while exit pages reads today's rollup.
+ const sb = spySb();
+ await fetchPanel(sb as never, "p1", "pages" as never, trackerRange("1d"), 412);
+ expect(sb.calls[0].fn).toBe("tracker_recent_top_pages");
+ expect(sb.calls[0].args.p_minutes).toBe(1440);
+ });
+});