berrypod/lib/berrypod_web/controllers/analytics_export_controller.ex

210 lines
5.9 KiB
Elixir
Raw Permalink Normal View History

defmodule BerrypodWeb.AnalyticsExportController do
use BerrypodWeb, :controller
alias Berrypod.Analytics
@periods %{
"today" => 0,
"7d" => 6,
"30d" => 29,
"12m" => 364
}
@filterable ~w(pathname referrer_source country_code browser os screen_size)
def export(conn, params) do
period = if Map.has_key?(@periods, params["period"]), do: params["period"], else: "30d"
filters = parse_filters(params)
range = date_range(period)
trend_data =
if period == "today",
do: Analytics.visitors_by_hour(range, filters),
else: Analytics.visitors_by_date(range, filters)
visitors = Analytics.count_visitors(range, filters)
pageviews = Analytics.count_pageviews(range, filters)
bounce_rate = Analytics.bounce_rate(range, filters)
avg_duration = Analytics.avg_duration(range, filters)
top_pages = Analytics.top_pages(range, filters: filters, limit: 10_000)
entry_pages = Analytics.entry_pages(range, filters: filters, limit: 10_000)
exit_pages = Analytics.exit_pages(range, filters: filters, limit: 10_000)
top_sources = Analytics.top_sources(range, filters: filters, limit: 10_000)
top_referrers = Analytics.top_referrers(range, filters: filters, limit: 10_000)
countries = Analytics.top_countries(range, filters: filters, limit: 10_000)
browsers = Analytics.device_breakdown(range, :browser, filters)
oses = Analytics.device_breakdown(range, :os, filters)
screen_sizes = Analytics.device_breakdown(range, :screen_size, filters)
funnel = Analytics.funnel(range, filters)
revenue = Analytics.total_revenue(range, filters)
csv_files = [
{"overview.csv", overview_csv(visitors, pageviews, bounce_rate, avg_duration)},
{trend_filename(period), trend_csv(trend_data, period)},
{"pages.csv", pages_csv(top_pages)},
{"entry_pages.csv", entry_exit_csv(entry_pages)},
{"exit_pages.csv", entry_exit_csv(exit_pages)},
{"sources.csv", sources_csv(top_sources)},
{"referrers.csv", referrers_csv(top_referrers)},
{"countries.csv", countries_csv(countries)},
{"browsers.csv", devices_csv(browsers)},
{"operating_systems.csv", devices_csv(oses)},
{"screen_sizes.csv", devices_csv(screen_sizes)},
{"funnel.csv", funnel_csv(funnel, revenue)}
]
zip_entries =
Enum.map(csv_files, fn {name, content} ->
{String.to_charlist(name), content}
end)
{:ok, {_name, zip_data}} = :zip.create(~c"export.zip", zip_entries, [:memory])
today = Date.to_iso8601(Date.utc_today())
filename = "analytics-#{period}-#{today}.zip"
conn
|> put_resp_content_type("application/zip")
|> put_resp_header("content-disposition", ~s(attachment; filename="#{filename}"))
|> send_resp(200, zip_data)
end
# -- CSV builders --
defp overview_csv(visitors, pageviews, bounce_rate, avg_duration) do
encode_csv([
["metric", "value"],
["visitors", visitors],
["pageviews", pageviews],
["bounce_rate_pct", bounce_rate],
["avg_duration_seconds", avg_duration]
])
end
defp trend_filename("today"), do: "visitors_by_hour.csv"
defp trend_filename(_), do: "visitors_by_date.csv"
defp trend_csv(data, "today") do
rows = [["hour", "visitors"] | Enum.map(data, fn r -> [r.hour, r.visitors] end)]
encode_csv(rows)
end
defp trend_csv(data, _period) do
rows = [["date", "visitors"] | Enum.map(data, fn r -> [r.date, r.visitors] end)]
encode_csv(rows)
end
defp pages_csv(rows) do
encode_csv([
["pathname", "visitors", "pageviews"]
| Enum.map(rows, fn r -> [r.pathname, r.visitors, r.pageviews] end)
])
end
defp entry_exit_csv(rows) do
encode_csv([
["pathname", "sessions"]
| Enum.map(rows, fn r -> [r.pathname, r.sessions] end)
])
end
defp sources_csv(rows) do
encode_csv([
["source", "visitors"]
| Enum.map(rows, fn r -> [r.source, r.visitors] end)
])
end
defp referrers_csv(rows) do
encode_csv([
["referrer", "visitors"]
| Enum.map(rows, fn r -> [r.referrer, r.visitors] end)
])
end
defp countries_csv(rows) do
encode_csv([
["country_code", "visitors"]
| Enum.map(rows, fn r -> [r.country_code, r.visitors] end)
])
end
defp devices_csv(rows) do
encode_csv([
["name", "visitors"]
| Enum.map(rows, fn r -> [r.name, r.visitors] end)
])
end
defp funnel_csv(funnel, revenue) do
top = funnel.product_views
steps = [
{"product_views", funnel.product_views},
{"add_to_carts", funnel.add_to_carts},
{"checkouts", funnel.checkouts},
{"purchases", funnel.purchases}
]
step_rows =
Enum.map(steps, fn {name, count} ->
pct = if top > 0, do: Float.round(count / top * 100, 1), else: 0.0
[name, count, pct]
end)
encode_csv(
[["step", "count", "conversion_pct"]] ++ step_rows ++ [["revenue_pence", revenue, ""]]
)
end
# -- CSV encoding --
defp encode_csv(rows) do
rows
|> Enum.map(fn row ->
row
|> Enum.map(&encode_field/1)
|> Enum.join(",")
end)
|> Enum.join("\r\n")
|> then(&(&1 <> "\r\n"))
end
defp encode_field(nil), do: ""
defp encode_field(val) when is_binary(val) do
if String.contains?(val, [",", "\"", "\n", "\r"]) do
~s("#{String.replace(val, "\"", "\"\"")}")
else
val
end
end
defp encode_field(val), do: to_string(val)
# -- Helpers --
defp date_range(period) do
days = Map.fetch!(@periods, period)
today = Date.utc_today()
start_date = Date.add(today, -days)
end_date = Date.add(today, 1)
{DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC"),
DateTime.new!(end_date, ~T[00:00:00], "Etc/UTC")}
end
defp parse_filters(params) do
filter_params = Map.get(params, "filter", %{})
Enum.reduce(filter_params, %{}, fn {key, val}, acc ->
if key in @filterable do
Map.put(acc, String.to_existing_atom(key), val)
else
acc
end
end)
end
end