Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions components/charts/tracker-analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,11 @@ function BreakdownPanel({
<RankRows data={rows} total={total} />
</div>
) : (
<EmptyRange message="No data in this timeframe." range={range} />
<EmptyRange
message="No data in this timeframe."
range={range}
ranges={ranges}
/>
)}
</PanelFrame>
);
Expand Down Expand Up @@ -502,7 +506,7 @@ function RankedPanel({
<RankRows data={rows} total={total} />
</>
) : (
<EmptyRange message={empty} range={range} />
<EmptyRange message={empty} range={range} ranges={ranges} />
)}
</PanelFrame>
);
Expand All @@ -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 (
<p className="text-sm text-[var(--color-muted)]">
{message}{" "}
<span className="text-xs">
({trackerRange(range).description.toLowerCase()})
</span>
<span className="text-xs">({described.description.toLowerCase()})</span>
</p>
);
}
Expand Down
16 changes: 14 additions & 2 deletions lib/tracker/panels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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<{
Expand Down
10 changes: 9 additions & 1 deletion lib/tracker/ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,16 @@ export const PANEL_RANGE_KEYS: Record<string, TrackerRangeKey[]> = {
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,
);
}
95 changes: 95 additions & 0 deletions tests/tracker-rollup-ranges.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }> = [];
return {
calls,
rpc(fn: string, args: Record<string, unknown>) {
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);
});
});
Loading