MishkaBlog.Dashboard.Analytics (Mishka Blog v0.0.1)

Copy Markdown View Source

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

default_days()

@spec default_days() :: pos_integer()

The window the page opens on, in whole days.

load(user, days \\ 30)

@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.

ranges()

@spec ranges() :: [pos_integer()]

The windows the page offers, in whole days.