add analytics v2 plan, demo seed data, and improved funnel display

- analytics-v2 plan with prioritised improvements (comparison mode, filtering, CSV export, entry/exit pages)
- seed script generating ~35k realistic events over 12 months (weighted traffic, referrers, devices, e-commerce funnel)
- funnel chart now shows overall conversion rate from product views instead of step-to-step percentages
- summary line with overall conversion rate and revenue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-22 22:30:24 +00:00
parent f91b47f0c3
commit 65e646a7eb
4 changed files with 470 additions and 12 deletions

View File

@@ -361,20 +361,24 @@ defmodule BerrypodWeb.Admin.Analytics do
{"Purchase", assigns.funnel.purchases}
]
max_val = steps |> Enum.map(&elem(&1, 1)) |> Enum.max(fn -> 1 end)
top_count = assigns.funnel.product_views
conversion_rate =
if top_count > 0,
do: Float.round(assigns.funnel.purchases / top_count * 100, 1),
else: 0.0
steps_with_rates =
steps
|> Enum.with_index()
|> Enum.map(fn {{label, count}, i} ->
prev_count = if i > 0, do: elem(Enum.at(steps, i - 1), 1), else: count
rate = if prev_count > 0, do: round(count / prev_count * 100), else: 0
width_pct = if max_val > 0, do: max(count / max_val * 100, 5), else: 5
overall_rate = if top_count > 0, do: Float.round(count / top_count * 100, 1), else: 0.0
width_pct = if top_count > 0, do: max(count / top_count * 100, 5), else: 5
%{label: label, count: count, rate: rate, width_pct: width_pct, index: i}
%{label: label, count: count, overall_rate: overall_rate, width_pct: width_pct, index: i}
end)
assigns = assign(assigns, steps: steps_with_rates)
assigns = assign(assigns, steps: steps_with_rates, conversion_rate: conversion_rate)
~H"""
<div
@@ -390,18 +394,24 @@ defmodule BerrypodWeb.Admin.Analytics do
</div>
<div style={"flex: 0 0 #{step.width_pct}%; height: 2rem; background: var(--color-primary, #4f46e5); border-radius: 0.25rem; opacity: #{1 - step.index * 0.15}; display: flex; align-items: center; padding-left: 0.5rem;"}>
<span style="font-size: 0.75rem; font-weight: 600; color: white;">
{step.count}
{format_number(step.count)}
</span>
</div>
<span
:if={step.index > 0}
style="font-size: 0.75rem; color: color-mix(in oklch, var(--color-base-content) 60%, transparent);"
style="font-size: 0.8125rem; font-weight: 600;"
>
{step.rate}%
{step.overall_rate}%
</span>
</div>
<div :if={@revenue > 0} style="margin-top: 0.5rem; font-size: 0.875rem; font-weight: 600;">
Revenue: {Cart.format_price(@revenue)}
<div style="margin-top: 0.75rem; font-size: 0.875rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
<span style="font-weight: 600;">{@conversion_rate}% overall conversion</span>
<span
:if={@revenue > 0}
style="color: color-mix(in oklch, var(--color-base-content) 60%, transparent);"
>
· Revenue: {Cart.format_price(@revenue)}
</span>
</div>
</div>
"""