add dashboard filtering to analytics
All checks were successful
deploy / deploy (push) Successful in 1m19s
All checks were successful
deploy / deploy (push) Successful in 1m19s
Click any row in pages, sources, countries, or devices tables to filter the entire dashboard by that dimension. Active filters show as dismissible chips. Filters thread through all queries including previous-period deltas. 1050 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,62 @@ defmodule Berrypod.AnalyticsTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering" do
|
||||
test "count_visitors respects country_code filter" do
|
||||
v1 = :crypto.strong_rand_bytes(8)
|
||||
v2 = :crypto.strong_rand_bytes(8)
|
||||
|
||||
insert_event(%{visitor_hash: v1, country_code: "GB"})
|
||||
insert_event(%{visitor_hash: v2, country_code: "US"})
|
||||
|
||||
assert Analytics.count_visitors(today_range(), %{country_code: "GB"}) == 1
|
||||
end
|
||||
|
||||
test "top_pages respects referrer_source filter" do
|
||||
v1 = :crypto.strong_rand_bytes(8)
|
||||
v2 = :crypto.strong_rand_bytes(8)
|
||||
|
||||
insert_event(%{visitor_hash: v1, pathname: "/", referrer_source: "Google"})
|
||||
insert_event(%{visitor_hash: v2, pathname: "/about", referrer_source: "Facebook"})
|
||||
|
||||
pages = Analytics.top_pages(today_range(), filters: %{referrer_source: "Google"})
|
||||
assert length(pages) == 1
|
||||
assert hd(pages).pathname == "/"
|
||||
end
|
||||
|
||||
test "bounce_rate respects filter" do
|
||||
s1 = :crypto.strong_rand_bytes(8)
|
||||
s2 = :crypto.strong_rand_bytes(8)
|
||||
|
||||
# GB visitor bounces (1 pageview)
|
||||
insert_event(%{session_hash: s1, country_code: "GB"})
|
||||
# US visitor doesn't bounce (2 pageviews)
|
||||
insert_event(%{session_hash: s2, country_code: "US", pathname: "/"})
|
||||
insert_event(%{session_hash: s2, country_code: "US", pathname: "/about"})
|
||||
|
||||
assert Analytics.bounce_rate(today_range(), %{country_code: "GB"}) == 100
|
||||
assert Analytics.bounce_rate(today_range(), %{country_code: "US"}) == 0
|
||||
end
|
||||
|
||||
test "multiple filters combine with AND logic" do
|
||||
v1 = :crypto.strong_rand_bytes(8)
|
||||
v2 = :crypto.strong_rand_bytes(8)
|
||||
|
||||
insert_event(%{visitor_hash: v1, country_code: "GB", browser: "Chrome"})
|
||||
insert_event(%{visitor_hash: v2, country_code: "GB", browser: "Firefox"})
|
||||
|
||||
assert Analytics.count_visitors(today_range(), %{country_code: "GB", browser: "Chrome"}) ==
|
||||
1
|
||||
end
|
||||
|
||||
test "unknown filter keys are ignored" do
|
||||
v1 = :crypto.strong_rand_bytes(8)
|
||||
insert_event(%{visitor_hash: v1})
|
||||
|
||||
assert Analytics.count_visitors(today_range(), %{nonexistent: "val"}) == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_events_before/1" do
|
||||
test "deletes old events" do
|
||||
old = DateTime.add(DateTime.utc_now(), -400, :day) |> DateTime.truncate(:second)
|
||||
|
||||
@@ -102,5 +102,63 @@ defmodule BerrypodWeb.Admin.AnalyticsTest do
|
||||
html = render_click(view, "change_tab", %{"tab" => "funnel"})
|
||||
assert html =~ "Conversion funnel"
|
||||
end
|
||||
|
||||
test "clicking a source adds a filter chip", %{conn: conn} do
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
|
||||
Repo.insert_all(Event, [
|
||||
[
|
||||
id: Ecto.UUID.generate(),
|
||||
name: "pageview",
|
||||
pathname: "/",
|
||||
visitor_hash: :crypto.strong_rand_bytes(8),
|
||||
session_hash: :crypto.strong_rand_bytes(8),
|
||||
referrer_source: "Google",
|
||||
inserted_at: now
|
||||
]
|
||||
])
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/analytics")
|
||||
|
||||
# Switch to sources tab and click the source
|
||||
render_click(view, "change_tab", %{"tab" => "sources"})
|
||||
|
||||
html =
|
||||
render_click(view, "add_filter", %{"dimension" => "referrer_source", "value" => "Google"})
|
||||
|
||||
assert html =~ "Source: Google"
|
||||
assert has_element?(view, "#analytics-filters")
|
||||
end
|
||||
|
||||
test "removing a filter chip clears it", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/analytics")
|
||||
|
||||
render_click(view, "add_filter", %{"dimension" => "country_code", "value" => "GB"})
|
||||
assert render(view) =~ "Country: United Kingdom"
|
||||
|
||||
html = render_click(view, "remove_filter", %{"dimension" => "country_code"})
|
||||
refute html =~ "Country: United Kingdom"
|
||||
end
|
||||
|
||||
test "clear all removes all filters", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/analytics")
|
||||
|
||||
render_click(view, "add_filter", %{"dimension" => "country_code", "value" => "GB"})
|
||||
render_click(view, "add_filter", %{"dimension" => "browser", "value" => "Chrome"})
|
||||
assert render(view) =~ "Clear all"
|
||||
|
||||
html = render_click(view, "clear_filters", %{})
|
||||
refute html =~ "Country: United Kingdom"
|
||||
refute html =~ "Browser: Chrome"
|
||||
end
|
||||
|
||||
test "changing period preserves filters", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/analytics")
|
||||
|
||||
render_click(view, "add_filter", %{"dimension" => "country_code", "value" => "GB"})
|
||||
html = render_click(view, "change_period", %{"period" => "7d"})
|
||||
|
||||
assert html =~ "Country: United Kingdom"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user