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); + }); +});