From 2c3c56e9b08ba5062e5575a8131523ff889e9792 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 05:02:19 +0000 Subject: [PATCH 1/2] Fix the 1D tab showing all-time totals on rollup-only stats panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit pages, devices, browsers and operating systems have no sub-day data source, so #222 gave them the rollup ranges only — with "1D" documented to mean today's UTC rollup rather than a rolling 24 hours. But the 1D range object is defined with `minutes: 1440` and no `days`, since it is a raw-event window for every other panel. resolveDays looks for `days`, finds nothing, and falls through to tracker_first_day, which returns the project's entire history. So those four panels answered the 1D tab with all-time totals, and on any site younger than 30 days 1D, 1M and All were byte-identical. rssamplifier.com is 15 days old, which is how this surfaced: its exit-pages card read 169,207 for /login at 1D, 1M and All alike, while the top-pages card — raw-backed, and therefore correct — read ~4k for the same path over the same 24 hours. Today's actual figure is 536. A sub-day window against a day-resolution rollup is one day, so rollupDays() collapses any raw range to 1 for these panels and passes real rollup ranges through untouched. The 1D tooltip is relabelled from "Last 24 hours, hourly buckets" to "Today so far, UTC day" for the same four panels, so the tab no longer promises a rolling window it cannot serve. Panels with a raw source are unaffected. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WaCMqhvNYfKtjLHmHQD1oq --- lib/tracker/panels.ts | 16 ++++- lib/tracker/ranges.ts | 10 ++- tests/tracker-rollup-ranges.test.ts | 95 +++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 tests/tracker-rollup-ranges.test.ts 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); + }); +}); From 268690368b550d3566f8e1e014eb27cbdd33ffbc Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 05:05:03 +0000 Subject: [PATCH 2/2] Describe an empty rollup-only panel with its own range label EmptyRange looked the range description up in the global table, so an exit pages / devices / browsers / OS card with no data for 1D still said "last 24 hours, hourly buckets" while its own tab tooltip said "today so far, UTC day". Both call sites already hold the panel's range list; pass it through and read the label from there, falling back to the global lookup. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WaCMqhvNYfKtjLHmHQD1oq --- components/charts/tracker-analytics.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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()})

); }