All notable changes to this project are documented here. This project follows Semantic Versioning.
0.5.0 - 2026-10-07
⚠️ Upgrade
- Add
:x_headersto the LiveView socket'sconnect_infofirst, then deploy — on both transports:connect_info: [:peer_data, :x_headers, :user_agent, session: @session_options]. Without it, a socket whose peer is a private or loopback address — a reverse proxy, a container network, a LAN,localhostin development — can no longer name its visitor, and the hook records nothing for it (no live navigation, interaction or "Right now" entry) instead of recording the peer as the visitor. Settings warns while that happens, and the "no JavaScript" bot check pauses while such skips are at least 5 % of a node's live visits in the last 24 hours. The skip count lives in memory, so after a restart the node that runs the check leaves alone every visit that started before it had been up for 30 minutes. X-Forwarded-Forfrom a private or loopback peer is now trusted with no configuration, and its LAST entry is taken — the one the proxy appended, as PhoenixKit core reads it for a login;X-Real-IPonly when there is no readableX-Forwarded-For. Before, the FIRST entry was read, and only withtrust_x_forwarded_for: true. The proxy must therefore set or appendX-Forwarded-Foritself: one that only setsX-Real-IP(nginx with justproxy_set_header X-Real-IP) passes a visitor's own header through, and the visitor can name any address. The same holds on an intranet where visitors have private addresses and reach the app directly.- Had
trust_x_forwarded_for: truebehind a CDN or a proxy with a public address (a CDN in front of the origin)? Add aRemoteIpplug (remote_ip) beforePhoenixKitWebAnalytics.Plug: a public peer's headers are ignored now, so without it every visitor is one of the CDN's addresses. Behind a chain (a CDN in front of a load balancer) the last entry is the CDN's, soRemoteIpis needed there as well. - LiveView behind a CDN and a load balancer:
RemoteIpfixes the plug but not the socket, which reads:x_headersby the rule above and sees the CDN's edge. Page load and live connection then hash to different visitors, and every LiveView visit is flagged a "no JavaScript" bot after 30 minutes. On such a site switch Spot bots by behaviour off (web_analytics_detect_bots). Withtrust_x_forwarded_for: trueand:x_headers, 0.4.0 read the first entry for both and didn't have this problem. - Expect more visitors and fewer bots from the day of the deploy. Behind a
proxy every visitor used to hash to the proxy's address: visitors sharing a
browser were merged into one, and the merged "visitor" was often flagged
rate(too many page views a minute). Visits recorded before the deploy are not recounted.
Fixed
- Behind a reverse proxy every visitor was the proxy. The plug, the beacon
and the LiveView hook read the client address by core's rule
(
PhoenixKit.Utils.IpAddress.client_address/1/client_address_from_socket/1, whose answer is taken for a public peer): behind a private or loopback peer, the lastX-Forwarded-Forentry across all its lines, thenX-Real-IP. The headers are read by the module itself there, because a core that can't parse a port (2.55 and earlier) passes over such anX-Forwarded-Forand answers withX-Real-IP, which a visitor can send. A port the proxy appended is dropped froma.b.c.d:portand[v6]:port(Caddy's{remote}); a bare IPv6 address is never cut (2001:db8::1:443is an address).::ffff:a.b.c.dis read as IPv4. A public peer is the visitor and its forwarded headers are ignored.
Changed
- The
connect_infosnippets (README, Settings,LiveHookdocs) list:x_headers. The README has a "Behind a reverse proxy" section, including the proxy-side fix for Caddy ({remote_host}, or noheader_upat all).
Deprecated
config :phoenix_kit_web_analytics, trust_x_forwarded_for:has no effect.trueis warned about once as the module starts;falseis ignored.
0.4.0 - 2026-10-07
⚠️ Upgrade
- Run
mix phoenix_kit.updatein the host — migration V07 adds two nullable columns tophoenix_kit_web_analytics_eventsand two partial indexes. On a large events table, build the indexesCONCURRENTLYfirst (see thePhoenixKitWebAnalytics.Migrationsdocs) and the migration skips them. - Rows written before the upgrade are not reclassified: earlier ad visits keep the channel they were recorded under ("direct", or "organic" when Google's Referer came along). An alert channel filter that leaves out "paid" will no longer alert on auto-tagged ad visits.
Added
- Ad-click identifiers are kept.
gclid,gbraid,wbraid(Google),msclkid(Microsoft Ads),fbclid(Meta),ttclid(TikTok) andli_fat_id(LinkedIn) are read off the landing URL alongsideutm_*; the first one present goes into the newclick_idcolumn and its parameter name intoclick_param(Google's conversion upload takesgclid,gbraidandwbraidin separate fields). Previously everything but the fiveutm_*keys was discarded, so the identifier needed to report a conversion back to the ad platform was lost on arrival. See the README's Privacy section. Web.BeaconPayload.utm_params/1(public) now returns ad-click identifiers alongsideutm_*; the name is kept.
Fixed
- An auto-tagged ad visit was recorded as "direct". Auto-tagging adds a
click identifier, not
utm_medium=cpc, and an ad click often arrives without a referrer, so the Acquisition report showed no paid traffic at all. A visit carrying an ad-only click identifier is now recorded under thepaidchannel, with the platform (named as for a referrer: "Google", "Bing", …) as its source unlessutm_sourcenames one.fbclidis the exception: Meta appends it to organic and Instagram clicks too, so it never overrides the referrer and only counts associalwhen nothing else classifies the visit. An internal page view stays internal when the identifier rides along on the link (Google'surl_passthrough). The Paid channel's help text on the Overview and Sources pages says so. - A campaign parameter that isn't valid UTF-8 (
?utm_source=%FF) made the insert fail and the hit was lost; the value is now dropped and the hit kept.
0.3.0 - 2026-10-01
⚠️ Upgrade
- Run
mix phoenix_kit.updatein the host — migrations V02–V06 add the session, rollup and recording columns/tables (each step is lock-light; the session-start backfill runs in batches from the retention pass, never in the migration). Requiresphoenix_kit ~> 2.38. - The visitor-hash secret moved to
web_analytics_hash_secret(see Fixed). A new one is generated on first use, so visitor IDs restart once. The oldweb_analytics_hash_saltrow is no longer read and can be deleted; earliersetting.changedactivity entries for it hold the old value in plaintext — purge them.
Added
- Server-side tracking: a LiveView
on_mounthook records live navigations, interactions (allow-listed params only) and exits straight from the socket;LivePresencepowers the new Right now page. - Sessions and a per-session timeline; Pages, Acquisition, Technology and Events reports rebuilt on shared dimensions.
- Optional client script (via
js_sources/0): outbound/download clicks, scroll depth, exits from non-LiveView pages. No cookies or storage. - Optional session recordings with a replay player on the visit page (coordinates and element positions only — never text or form values).
- Behavioural bot signals (automation flag, page-view speed, no JavaScript).
- Daily rollups (
daily_stats,daily_dims) read together with raw events, a single-flight report cache, and hourly retention that never prunes past the rollup watermark. - Website activity alerts (new visitors, tracked events, sign-ups) routed through core's notifications.
Adminmodule: every operator mutation is logged toPhoenixKit.Activity.- Estonian and Russian translations.
Fixed
- The visitor-hash salt was written in plaintext to the permanent activity
log (and again on every rotation), because core only withholds values of
keys whose name marks a secret. The key is now
web_analytics_hash_secret. - Rollup-backed reports (
overview,timeseries,sites) raised on a database error instead of degrading to empty results. - Monthly chart buckets vanished when the database session time zone was east of UTC.
- One-time tokens in core's route paths (
/users/reset-password/:token,/users/confirm/:token, magic-link, QR-login, confirm-email, access links) were stored, shown in the Pages report and put into alert text; they are now stored as:token. - Excluded paths and declared bots appeared under "Right now" and wrote leave events.
- "Tracked events" alerts were forgeable and uncapped; alert text could carry a
visitor-crafted
utm_sourcesentence; crawlers alerted with bot tracking on. Event alerts now have their own hourly cap, visitor-side text is cleaned, and bots never alert. - A page that only saw interactions listed as a zero-view top page until its day was rolled up.
- The rollup watermark was written once per rolled day (one permanent
setting.changedentry each); it is now written once per pass. - A NUL byte in a path, campaign parameter or event value lost the whole hit.
- The body-size limits on the beacon and recording endpoints only applied to
text/plain; a JSON body over the limit is now dropped too. - Settings errors named six fields in English only; 23 empty
enstrings filled.
0.2.3 - 2026-09-07
Fixed
- 0.2.0–0.2.2 fail to compile on Elixir 1.18.4 / OTP 28 with
cannot escape #Reference<...>inUserAgent: a compiled regex nested inside a{name, ~r//}list module attribute can't be injected intoparse/1's body on that combination.@browsers/@operating_systemsare now private functions instead of attributes; behaviour is unchanged. (#1, thanks @timujinne) - Pages report crashed once a period had 100+ ranked paths: the "showing the top N paths" note referenced
@page_limitinside the template, where@nameis always an assign, never the module attribute of the same name — now assigned inmount/3.
0.2.2 - 2026-09-07
Fixed
- Removed duplicate page headings across Web Analytics admin pages (Dashboard, Events, Pages, Acquisition, Technology, Settings) — each repeated the page title already shown in the top breadcrumb bar.
0.2.1 - 2026-08-11
Changed
- Dependency updates:
phoenix_kit2.2.0 and the transitive set it pulls (phoenix1.8.10,hackney4.7.3). No source changes in this package.
[0.2.0] - 2026-08-10
Changed
⚠️ Requires
phoenix_kit ~> 2.0. The core pin moved to~> 2.0, so this release no longer resolves against core 1.7.Core 2.0.0 squashes the migration chain into a single
V135baseline and makes V135 the chain's floor:mix ecto.migratenow refuses on a database below it rather than migrating. Checkmix phoenix_kit.statusbefore upgrading. A host below V135 must installphoenix_kit 1.7.236— the migration bridge, the last release carrying the full pre-squash chain — migrate until the reported version is at least V135, and only then move to 2.0.This package does not call migration internals, so the change is the pin itself.
Fixed
- This is the first release of this package to actually reach Hex. Its
package
files:list named aprivdirectory that does not exist, andmix hex.buildrefuses to build a package whose declared files are missing ("Missing files: priv") — so every publish attempt had failed before reaching the registry. The entry is dropped; add it back ifpriv/ever gains content. - The dashboard no longer fails to compile against core 2.0. Core 2.0 added
PhoenixKitWeb.Components.Core.Chart.bar_chart/1, which every LiveView imports viause PhoenixKitWeb, :live_view. That collided with this package's own same-aritybar_chart/1, making the unqualified call in the dashboard ambiguous and failing the build. The local component is renamedtraffic_chart/1; core's is a generic SVG chart keyed onid/data, while this one is bucket-aware and takesseries/metric/bucket, so they are not interchangeable and the local one is kept. Rendered output is unchanged. Callers usingPhoenixKitWebAnalytics.Web.Components.bar_chart/1directly must rename the call.
0.1.0 - 2026-07-26
Initial release.
Collection
PhoenixKitWebAnalytics.Plug— server-side page view tracking. One line in the host's:browserpipeline; nothing is added to rendered pages. Writes happen in a supervised task after the response is sent, with amax_childrencap that drops rather than queues under load.PhoenixKitWebAnalytics.LiveHook— anon_mounthook that counts LiveViewpush_patch/push_navigatenavigation. Stays inert unless the endpoint socket exposes:peer_dataand:user_agent, rather than recording hits under a mismatched visitor hash.- Cookieless visitor identification: a daily-rotating salted SHA-256 of IP + User-Agent, truncated to 32 hex characters. No IP is stored.
- Server-side session stitching on an inactivity window (30 minutes by default) — no session cookie.
- Built-in User-Agent classification (browser, OS, device class, bot detection) and referrer classification into channels, with no external dependency or IP database.
- Campaign parameters (
utm_*) are extracted into their own columns; the rest of the query string is never stored. - Skips non-
GETrequests, non-2xx and non-HTML responses, excluded paths,DNT/Sec-GPCopt-outs, and bots. - Optional public collection endpoints, off by default: a ~300-byte inline
beacon snippet for browser-side custom events and a 1×1 pixel for
CDN-cached pages.
PhoenixKitWebAnalytics.Web.BeaconPayloadenforces the trust boundary — a payload controls content, never identity or origin. PhoenixKitWebAnalytics.track_event/2for server-side custom events.PhoenixKitWebAnalytics.Geobehaviour for optional country resolution, plus automatic use of CDN country headers (Cloudflare, Vercel, Fastly) when present.
Reports
PhoenixKitWebAnalytics.Reports— overview totals with period-over-period comparison, trend series (hour / day / month buckets), top pages, slowest pages by response time, referrers, channels, UTM campaigns and sources, browsers, operating systems, devices, countries, languages, custom events, a recent-hits feed, and live visitor count.- Six admin pages: Overview, Pages, Acquisition, Technology, Events, Settings. Charts are CSS-only — no charting library ships with this package.
- Period and site filters live in the URL, so a filtered report can be bookmarked and shared.
Storage
- Module-owned versioned migrations (
PhoenixKitWebAnalytics.Migrations), applied bymix phoenix_kit.update, withCOMMENT ON TABLEversion tracking and full--prefix(named-schema) support. phoenix_kit_web_analytics_events(append-only hits) andphoenix_kit_web_analytics_daily_stats(per-day, per-site rollups), both with UUIDv7 primary keys.PhoenixKitWebAnalytics.Retention— hourly rollup of completed days and batched pruning of raw events past the retention window (365 days by default). Pruning never runs ahead of the rollup that preserves the trend line;daily_timeseries/1falls back to rollups for pruned days.