add analytics CSV export
All checks were successful
deploy / deploy (push) Successful in 1m25s

Downloads a ZIP with one CSV per report (overview, trend, pages, entry/exit
pages, sources, referrers, countries, devices, funnel). Export button lives
next to the period selector and picks up the current period and any active
filters using a JS hook + JS.set_attribute, so the downloaded data always
matches what's on screen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-24 09:37:45 +00:00
parent 01ff8decd5
commit 758e66db5c
4 changed files with 269 additions and 5 deletions

View File

@@ -144,15 +144,30 @@ defmodule BerrypodWeb.Admin.Analytics do
<.header>Analytics</.header>
<%!-- Period selector --%>
<div class="analytics-periods" style="display: flex; gap: 0.25rem; margin-top: 1rem;">
<div
class="analytics-periods"
style="display: flex; gap: 0.25rem; margin-top: 1rem; align-items: center;"
>
<button
:for={period <- ["today", "7d", "30d", "12m"]}
phx-click="change_period"
phx-value-period={period}
phx-click={
JS.push("change_period", value: %{period: period})
|> JS.set_attribute({"data-period", period}, to: "#analytics-export-link")
}
class={["admin-btn admin-btn-sm", @period == period && "admin-btn-primary"]}
>
{period_label(period)}
</button>
<a
id="analytics-export-link"
data-period={@period}
href={export_url(@period, @filters)}
class="admin-btn admin-btn-sm"
style="margin-left: auto;"
phx-hook="AnalyticsExport"
>
Export CSV
</a>
</div>
<%!-- Active filters --%>
@@ -308,7 +323,11 @@ defmodule BerrypodWeb.Admin.Analytics do
assigns = assign(assigns, label: filter_label(assigns.dimension, assigns.value))
~H"""
<span style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; background: var(--color-base-200, #e5e5e5); border-radius: 0.25rem;">
<span
data-filter-dimension={@dimension}
data-filter-value={@value}
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; background: var(--color-base-200, #e5e5e5); border-radius: 0.25rem;"
>
{@label}
<button
phx-click="remove_filter"
@@ -745,6 +764,15 @@ defmodule BerrypodWeb.Admin.Analytics do
defp period_label("30d"), do: "30 days"
defp period_label("12m"), do: "12 months"
defp export_url(period, filters) do
params =
Enum.reduce(filters, %{"period" => period}, fn {k, v}, acc ->
Map.put(acc, "filter[#{k}]", v)
end)
"/admin/analytics/export?" <> URI.encode_query(params)
end
defp format_number(n) when n >= 1_000_000, do: "#{Float.round(n / 1_000_000, 1)}M"
defp format_number(n) when n >= 1_000, do: "#{Float.round(n / 1_000, 1)}k"
defp format_number(n), do: to_string(n)