Every number on the blog analytics page (/admin/dashboard/blog/analytics), read from the database.
load/2 reads one window — seven, thirty or ninety whole days ending today — and returns the KPI
figures, the daily views series, the posts read most in that window, where their readers came from
and whether those readers were signed in. Nothing is estimated: the page carries a metric only
where MishkaBlog.PostView actually records one, which is why there is no read time, no session
length and no bounce rate — nothing in the schema observes a reader leaving.
A view row is written once per post per reader (per account when signed in, per IP otherwise) and
never updated, so "views" here means distinct readers who opened a post, and inserted_at is the
first time each one did. That is the unit every figure below is in.
Every figure is read as user through MishkaBlog.Dashboard.Scope, as
MishkaBlog.Dashboard.Overview reads its own: a master (a user whose site_id is nil) counts every
site, a site user only its own. MishkaBlog.PostView has no master read and carries no site_id;
both viewers read it through :read, which its policies keep to the viewer's site, joined to its
post (not is_nil(post.id)), so a view of an archived post is not counted.
The window is read once — five selected columns over one read — and the series, the reader count,
the referrers, the audience split and the top posts are folded out of those rows in memory. Each of
those panels is a GROUP BY that an Ash read does not express, and one narrow read beats five
hand-written queries. The all-time and previous-window totals stay Ash.count/2 calls, as they are
single numbers.
Views and posts are read only for a holder of admin:dashboard:blog:post, and comments only for a
holder of admin:dashboard:blog:comment (MishkaBlog.Dashboard.Overview.areas/1) — the
permissions those resources' policies ask for. A read of an area the viewer does not hold is
refused and counted as no data, so its figures are zero and an engagement rate needs both.
The caller runs this off the render — see MishkaBlogWeb.Live.Admin.Dashboard.Analytics.Index.
Summary
Functions
The window the page opens on, in whole days.
Reads the analytics page for the last days days, as user (see MishkaBlog.Dashboard.Scope).
The windows the page offers, in whole days.
Functions
@spec default_days() :: pos_integer()
The window the page opens on, in whole days.
@spec load(map(), pos_integer()) :: map()
Reads the analytics page for the last days days, as user (see MishkaBlog.Dashboard.Scope).
Returns a flat map: the all-time and windowed view totals, the window before it to compare
against, today and yesterday, the distinct readers behind them, the comment engagement rate, and
four collections — :series (one %{date, count} per day of the window, zeros included),
:top_posts (%{title, status, published_at, views}, most read first), :referrers
(%{label, count}, busiest first) and :audience (%{members, guests}).
:from and :to are the window's own bounds, so the page can say which days it is describing
rather than assuming they match the days it asked for.
@spec ranges() :: [pos_integer()]
The windows the page offers, in whole days.