One shared, tested audience-breakdown query surface for every consumer.
breakdown/3 returns rows of %{value: term(), <metrics...>} for a single
dimension over a workspace + time window. Dimensions and metrics are declared
once (data-driven) so there is no per-dimension copy-paste.
Grain (why some metrics are non-additive)
- Event-grain dimensions (
:device_type,:browser,:os,:source_platform,:source_medium,:source_campaign) group events by the event's own column — a device is the context of an interaction, not an attribute of the identity. - Person-grain
:userscountsDISTINCT coalesce(merged_into_id, id), so one cross-device person is counted in every device/browser/os bucket they were active in. "mobile users + desktop users" can therefore exceed total users — and that is correct (GA4 behaviour).:usersis non-additive by design; an additive partition would be a separately named:primary_device_usersmetric, deferred until requested. - Session-grain metrics (
:sessions,:bounce_rate,:avg_duration,:engaged_rate) aggregatega_sessions. A session's device/source is its first event's, stored on the session row, so they group cleanly bys.device_typeetc.
Metrics
| metric | grain | definition |
|---|---|---|
:events | event | count(e.id) |
:pageviews | event | count(e.id) FILTER (event_type = 'pageview') |
:users | person | count(DISTINCT coalesce(v.merged_into_id, v.id)) (non-additive) |
:sessions | session | count(s.id) |
:bounce_rate | session | avg(s.is_bounce::int) |
:avg_duration | session | avg(s.duration_seconds) |
:engaged_rate | session | avg(s.is_engaged::int) |
Usage
GoodAnalytics.Core.Audience.breakdown(workspace_id, :device_type,
window: %{start_at: from, end_at: to},
metrics: [:events, :users, :sessions, :bounce_rate],
filters: [device_type: "mobile"],
limit: 10
)
Summary
Functions
Returns a dimension breakdown for workspace_id over the window.
The supported breakdown dimensions, in declaration order.
The supported metrics, in declaration order.
Functions
@spec breakdown(Ecto.UUID.t(), atom(), keyword()) :: [map()]
Returns a dimension breakdown for workspace_id over the window.
Options
:window—%{start_at: from, end_at: to}(required), an inclusive-start/exclusive-endDateTimerange matched oninserted_at.:metrics— subset ofmetrics/0(default: all). Mixing event/person metrics with session metrics composes a session sub-aggregate.:filters— keyword of dimension equality filters for drilldown, e.g.[device_type: "mobile"]. Keys must be supported dimensions.:limit— cap on returned rows.:order_by—{metric, :asc | :desc}(default: the first requested metric, descending).
Unknown dimension or metric raises ArgumentError. An empty window yields
[]. Null dimension values bucket as (not set).
@spec dimensions() :: [atom()]
The supported breakdown dimensions, in declaration order.
@spec metrics() :: [atom()]
The supported metrics, in declaration order.