add entry/exit pages panel to analytics dashboard
All checks were successful
deploy / deploy (push) Successful in 1m28s

ROW_NUMBER() window function picks first/last pageview per session.
Both tables live in the pages tab and support the pathname filter.
6 new tests, 1061 total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-23 21:14:24 +00:00
parent 162a5bfe9a
commit 9b78793701
3 changed files with 161 additions and 0 deletions

View File

@@ -302,6 +302,68 @@ defmodule Berrypod.Analytics do
|> Repo.one()
end
@doc """
Top entry pages — the first page visited per session, by session count.
"""
def entry_pages(date_range, opts \\ []) do
limit = Keyword.get(opts, :limit, 10)
filters = Keyword.get(opts, :filters, %{})
inner =
base_query(date_range, filters)
|> where([e], e.name == "pageview")
|> select([e], %{
session_hash: e.session_hash,
pathname: e.pathname,
rn:
fragment(
"ROW_NUMBER() OVER (PARTITION BY ? ORDER BY ? ASC)",
e.session_hash,
e.inserted_at
)
})
from(s in subquery(inner),
where: s.rn == 1,
group_by: s.pathname,
select: %{pathname: s.pathname, sessions: count()},
order_by: [desc: count()],
limit: ^limit
)
|> Repo.all()
end
@doc """
Top exit pages — the last page visited per session, by session count.
"""
def exit_pages(date_range, opts \\ []) do
limit = Keyword.get(opts, :limit, 10)
filters = Keyword.get(opts, :filters, %{})
inner =
base_query(date_range, filters)
|> where([e], e.name == "pageview")
|> select([e], %{
session_hash: e.session_hash,
pathname: e.pathname,
rn:
fragment(
"ROW_NUMBER() OVER (PARTITION BY ? ORDER BY ? DESC)",
e.session_hash,
e.inserted_at
)
})
from(s in subquery(inner),
where: s.rn == 1,
group_by: s.pathname,
select: %{pathname: s.pathname, sessions: count()},
order_by: [desc: count()],
limit: ^limit
)
|> Repo.all()
end
@doc """
Deletes events older than the given datetime. Used by the retention worker.
"""

View File

@@ -107,6 +107,8 @@ defmodule BerrypodWeb.Admin.Analytics do
|> assign(:screen_sizes, Analytics.device_breakdown(range, :screen_size, filters))
|> assign(:funnel, Analytics.funnel(range, filters))
|> assign(:revenue, Analytics.total_revenue(range, filters))
|> assign(:entry_pages, Analytics.entry_pages(range, filters: filters))
|> assign(:exit_pages, Analytics.exit_pages(range, filters: filters))
end
defp date_range(period) do
@@ -508,6 +510,24 @@ defmodule BerrypodWeb.Admin.Analytics do
%{label: "Pageviews", key: :pageviews, align: :right}
]}
/>
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">Entry pages</h3>
<.detail_table
rows={@entry_pages}
empty_message="No entry page data yet"
columns={[
%{label: "Page", key: :pathname, filter: {:pathname, :pathname}},
%{label: "Sessions", key: :sessions, align: :right}
]}
/>
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">Exit pages</h3>
<.detail_table
rows={@exit_pages}
empty_message="No exit page data yet"
columns={[
%{label: "Page", key: :pathname, filter: {:pathname, :pathname}},
%{label: "Sessions", key: :sessions, align: :right}
]}
/>
"""
end