Pixelex.Query (Pixelex v0.1.0)

Copy Markdown View Source

Reading the event log: traffic, funnels, retention, cohorts.

Nothing on Hex does funnels, retention or cohorts. Plausible Community Edition withholds funnels as a paid feature. This is the part of pixelex that exists nowhere else.

Every read is bounded, and the bound is not optional

from and to are required arguments, not options with defaults, and every grouped query carries a LIMIT. On a table that takes every write in the system, an unbounded read is not a slow query — it is an outage waiting for the day the table gets big enough. Monthly partitioning means a bounded range touches only the partitions it overlaps and the planner prunes the rest, so the cost of a report is proportional to the window asked for rather than to the age of the site.

Anonymous or identified — the distinction runs through everything

visitor_id is a keyed hash under a salt that rotates every UTC day. Within a day it identifies a person; across days it cannot, by construction. That is what makes the scheme cookieless and it is not negotiable, so:

questionworks onacross days?
page views, sessions, sources, devicesanonymousyes — they are counts, not people
unique visitors over a rangeanonymoussum of dailies, an over-count
a funnel completed in one visitvisitor_idno — same-day only
a funnel completed over weeksuser_idyes, for signed-in users
retention, cohortsuser_idyes, for signed-in users

Retention and cohorts therefore operate on user_id and say so. This is not a gap to be closed later: a durable cross-day identifier for anonymous visitors is a cookie, and putting one on the device is the thing that requires the banner pixelex exists to avoid.

Call Pixelex.identify/3 when a user signs in and the identified questions become answerable for them.

Summary

Functions

A time bucket appropriate to the window: hourly up to two days, daily up to a quarter, weekly beyond, monthly past a year.

The window's length in days, rounded up. Used to pick a sensible bucket.

Build a range, or raise.

Functions

bucket(range)

@spec bucket(Pixelex.Query.Range.t()) :: String.t()

A time bucket appropriate to the window: hourly up to two days, daily up to a quarter, weekly beyond, monthly past a year.

Sized so a chart never asks for more points than a chart can show. A year of hourly buckets is 8,760 rows to render eight hundred pixels.

days(range)

@spec days(Pixelex.Query.Range.t()) :: pos_integer()

The window's length in days, rounded up. Used to pick a sensible bucket.

range(from, to \\ nil)

@spec range(DateTime.t() | atom(), DateTime.t() | nil) :: Pixelex.Query.Range.t()

Build a range, or raise.

Pixelex.Query.range(~U[2026-09-01 00:00:00Z], ~U[2026-10-01 00:00:00Z])
Pixelex.Query.range(:last_30_days)