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
8 changes: 8 additions & 0 deletions lib/dashboard/stats-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type StatsAnswerish = {
sources?: StatsItem[] | null;
referrers?: StatsItem[] | null;
pages?: StatsItem[] | null;
countries?: StatsItem[] | null;
cities?: StatsItem[] | null;
};

const list = (v: StatsItem[] | null | undefined): StatsItem[] => (Array.isArray(v) ? v : []);
Expand Down Expand Up @@ -42,6 +44,12 @@ export function renderStats(
section("Sources", list(answer.sources));
section("Referrers", list(answer.referrers));
section("Pages", list(answer.pages));
// Last, but the two that settle an argument. A referrer is whatever the
// client typed; the country is where the packets came from. Traffic that
// reads as a broad audience in Sources and as a single city here is one
// operator, not an audience.
section("Countries", list(answer.countries));
section("Cities", list(answer.cities));

// Nothing at all is a real answer, and the likeliest cause is worth naming.
if (!(totals.pageviews ?? 0) && !(totals.events ?? 0) && !list(answer.sources).length) {
Expand Down
23 changes: 22 additions & 1 deletion lib/tracker/apiStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,24 @@ export type ProjectRow = { id: string; name: string; url: string; tracker_enable
* Deliberately not every panel: a CLI answer people read in a terminal is the
* sources, the pages and the shape over time. Devices and browsers are a
* different question and cost another query each.
*
* Geography is here because it answers a question the others cannot. Sources
* and referrers describe what a caller SAYS it is, and both are trivially
* forged; where the packets entered from is not. When traffic to one site
* turned out to be almost entirely one city, that single fact identified it as
* one operator on a cloud region rather than the scattered readership the
* referrer panel implied — and nothing in the terminal could show it, because
* these two panels rendered only in the dashboard. Two extra queries is a
* cheap price for the panel that tells you who you are actually talking to.
*/
export const STATS_PANELS: PanelKey[] = ["series", "sources", "pages", "referrers"];
export const STATS_PANELS: PanelKey[] = [
"series",
"sources",
"pages",
"referrers",
"countries",
"cities",
];

export type ResolveResult =
| { ok: true; project: ProjectRow }
Expand Down Expand Up @@ -123,6 +139,9 @@ export type StatsAnswer = {
sources: ListItem[];
referrers: ListItem[];
pages: ListItem[];
/** Where the traffic entered from. Unforgeable, unlike a referrer. */
countries: ListItem[];
cities: ListItem[];
/**
* The shape over time, present only with `detail`. It is what the totals were
* summed from, so asking for it costs nothing extra.
Expand Down Expand Up @@ -241,6 +260,8 @@ export async function projectStats(
sources: asList(panels.sources),
referrers: asList(panels.referrers),
pages: asList(panels.pages),
countries: asList(panels.countries),
cities: asList(panels.cities),
};
if (!detail) return answer;

Expand Down
26 changes: 26 additions & 0 deletions tests/dashboard-package-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,30 @@ describe("renderStats", () => {
const text = renderStats({ totals: { visitors: 0, pageviews: 0 } }, { range: "1d", who: "humans" });
expect(text).toContain("Check the tag is on the page");
});

it("prints where the traffic came from", () => {
const text = renderStats(
{
project: { name: "site.com" },
totals: { visitors: 10, pageviews: 40 },
countries: [{ label: "Singapore (SG)", value: 38 }],
cities: [{ label: "Singapore, SG", value: 38 }],
},
{ range: "1d", who: "all" },
);
expect(text).toContain("Countries");
expect(text).toContain("Singapore (SG)");
expect(text).toContain("Cities");
});

it("omits the geo sections when the panels are empty", () => {
// Same rule as every other section: an empty panel prints nothing rather
// than an empty heading, so a quiet window stays readable.
const text = renderStats(
{ project: { name: "site.com" }, totals: { visitors: 1, pageviews: 1 }, pages: [{ label: "/", value: 1 }] },
{ range: "1d", who: "humans" },
);
expect(text).not.toContain("Countries");
expect(text).not.toContain("Cities");
});
});