From 01706b7590df8d0f92f5a8e36c8e95f9fee5e62f Mon Sep 17 00:00:00 2001
From: Uku Taht <uku.taht@gmail.com>
Date: Fri, 3 Dec 2021 11:17:11 +0200
Subject: [PATCH] Remove dead code

---
 lib/plausible/stats/clickhouse.ex             | 967 ------------------
 lib/plausible/stats/compare.ex                |  28 +
 .../templates/email/weekly_report.html.eex    |  18 +-
 lib/workers/send_email_report.ex              |  27 +-
 mix.exs                                       |   3 +-
 mix.lock                                      |   2 +
 test/workers/send_email_report_test.exs       |  45 +
 7 files changed, 100 insertions(+), 990 deletions(-)
 create mode 100644 lib/plausible/stats/compare.ex

diff --git a/lib/plausible/stats/clickhouse.ex b/lib/plausible/stats/clickhouse.ex
index 71ac9d3b..098c93a3 100644
--- a/lib/plausible/stats/clickhouse.ex
+++ b/lib/plausible/stats/clickhouse.ex
@@ -5,175 +5,6 @@ defmodule Plausible.Stats.Clickhouse do
   use Plausible.Stats.Fragments
   @no_ref "Direct / None"
 
-  def compare_pageviews_and_visitors(site, query, {pageviews, visitors}) do
-    query = Query.shift_back(query, site)
-    {old_pageviews, old_visitors} = pageviews_and_visitors(site, query)
-
-    cond do
-      old_pageviews == 0 and pageviews > 0 ->
-        {100, 100}
-
-      old_pageviews == 0 and pageviews == 0 ->
-        {0, 0}
-
-      true ->
-        {
-          round((pageviews - old_pageviews) / old_pageviews * 100),
-          round((visitors - old_visitors) / old_visitors * 100)
-        }
-    end
-  end
-
-  def calculate_plot(site, %Query{interval: "month"} = query) do
-    n_steps = Timex.diff(query.date_range.last, query.date_range.first, :months)
-
-    steps =
-      Enum.map(n_steps..0, fn shift ->
-        Timex.now(site.timezone)
-        |> Timex.beginning_of_month()
-        |> Timex.shift(months: -shift)
-        |> DateTime.to_date()
-      end)
-
-    groups =
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          select:
-            {fragment("toStartOfMonth(toTimeZone(?, ?)) as month", e.timestamp, ^site.timezone),
-             uniq(e.user_id)},
-          group_by: fragment("month"),
-          order_by: fragment("month")
-      )
-      |> Enum.into(%{})
-
-    present_index =
-      Enum.find_index(steps, fn step ->
-        step == Timex.now(site.timezone) |> Timex.to_date() |> Timex.beginning_of_month()
-      end)
-
-    plot = Enum.map(steps, fn step -> groups[step] || 0 end)
-    labels = Enum.map(steps, fn step -> Timex.format!(step, "{ISOdate}") end)
-
-    {plot, labels, present_index}
-  end
-
-  def calculate_plot(site, %Query{interval: "date"} = query) do
-    steps = Enum.into(query.date_range, [])
-
-    groups =
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          select:
-            {fragment("toDate(toTimeZone(?, ?)) as day", e.timestamp, ^site.timezone),
-             uniq(e.user_id)},
-          group_by: fragment("day"),
-          order_by: fragment("day")
-      )
-      |> Enum.into(%{})
-
-    present_index =
-      Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> Timex.to_date() end)
-
-    steps_to_show = if present_index, do: present_index + 1, else: Enum.count(steps)
-    plot = Enum.map(steps, fn step -> groups[step] || 0 end) |> Enum.take(steps_to_show)
-    labels = Enum.map(steps, fn step -> Timex.format!(step, "{ISOdate}") end)
-
-    {plot, labels, present_index}
-  end
-
-  def calculate_plot(site, %Query{interval: "hour"} = query) do
-    steps = 0..23
-
-    groups =
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          select:
-            {fragment("toHour(toTimeZone(?, ?)) as hour", e.timestamp, ^site.timezone),
-             uniq(e.user_id)},
-          group_by: fragment("hour"),
-          order_by: fragment("hour")
-      )
-      |> Enum.into(%{})
-
-    now = Timex.now(site.timezone)
-    is_today = Timex.to_date(now) == query.date_range.first
-    present_index = is_today && Enum.find_index(steps, fn step -> step == now.hour end)
-    steps_to_show = if present_index, do: present_index + 1, else: Enum.count(steps)
-
-    labels =
-      Enum.map(steps, fn step ->
-        Timex.to_datetime(query.date_range.first)
-        |> Timex.shift(hours: step)
-        |> NaiveDateTime.to_iso8601()
-      end)
-
-    plot = Enum.map(steps, fn step -> groups[step] || 0 end) |> Enum.take(steps_to_show)
-    {plot, labels, present_index}
-  end
-
-  def calculate_plot(site, %Query{period: "realtime"} = query) do
-    query = %Query{query | period: "30m"}
-
-    groups =
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          select: {
-            fragment("dateDiff('minute', now(), ?) as relativeMinute", e.timestamp),
-            total()
-          },
-          group_by: fragment("relativeMinute"),
-          order_by: fragment("relativeMinute")
-      )
-      |> Enum.into(%{})
-
-    labels = Enum.into(-30..-1, [])
-    plot = Enum.map(labels, fn label -> groups[label] || 0 end)
-    {plot, labels, nil}
-  end
-
-  def bounce_rate(site, query) do
-    q = base_session_query(site, query) |> apply_page_as_entry_page(site, query)
-
-    ClickhouseRepo.one(
-      from s in q,
-        select: bounce_rate()
-    ) || 0
-  end
-
-  def visit_duration(site, query) do
-    q = base_session_query(site, query) |> apply_page_as_entry_page(site, query)
-
-    ClickhouseRepo.one(
-      from s in q,
-        select: visit_duration()
-    ) || 0
-  end
-
-  def total_pageviews(site, %Query{period: "realtime"} = query) do
-    query = %Query{query | period: "30m"}
-
-    q = base_session_query(site, query) |> apply_page_as_entry_page(site, query)
-
-    ClickhouseRepo.one(
-      from e in q,
-        select: fragment("sum(sign * pageviews)")
-    )
-  end
-
-  def total_pageviews(site, query) do
-    ClickhouseRepo.one(
-      from e in base_query_w_sessions(site, query),
-        select: total()
-    )
-  end
-
-  def total_events(site, query) do
-    ClickhouseRepo.one(
-      from e in base_query_w_sessions(site, query),
-        select: total()
-    )
-  end
-
   def usage_breakdown(domains) do
     range =
       Date.range(
@@ -203,53 +34,6 @@ defmodule Plausible.Stats.Clickhouse do
     end)
   end
 
-  def pageviews_and_visitors(site, query) do
-    {pageviews, visitors, _} = pageviews_and_visitors_with_sample_percent(site, query)
-    {pageviews, visitors}
-  end
-
-  def pageviews_and_visitors_with_sample_percent(site, query) do
-    ClickhouseRepo.one(
-      from e in base_query_w_sessions(site, query),
-        select: {total(), uniq(e.user_id), sample_percent()}
-    )
-  end
-
-  def unique_visitors(site, query) do
-    {visitors, _} = unique_visitors_with_sample_percent(site, query)
-    visitors
-  end
-
-  def unique_visitors_with_sample_percent(site, query) do
-    query = if query.period == "realtime", do: %Query{query | period: "30m"}, else: query
-
-    ClickhouseRepo.one(
-      from e in base_query_w_sessions(site, query),
-        select: {uniq(e.user_id), sample_percent()}
-    )
-  end
-
-  def top_referrers_for_goal(site, query, limit, page) do
-    offset = (page - 1) * limit
-
-    ClickhouseRepo.all(
-      from s in base_query_w_sessions(site, query),
-        where: s.referrer_source != "",
-        group_by: s.referrer_source,
-        order_by: [desc: uniq(s.user_id)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: s.referrer_source,
-          url: fragment("any(?)", s.referrer),
-          count: uniq(s.user_id)
-        }
-    )
-    |> Enum.map(fn ref ->
-      Map.update(ref, :url, nil, fn url -> url && URI.parse("http://" <> url).host end)
-    end)
-  end
-
   def top_sources(site, query, limit, page, show_noref \\ false, include_details) do
     offset = (page - 1) * limit
 
@@ -335,539 +119,6 @@ defmodule Plausible.Stats.Clickhouse do
     include_path_filter_entry(db_query, query.filters["page"])
   end
 
-  def utm_mediums(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
-    offset = (page - 1) * limit
-
-    q =
-      from(
-        s in base_session_query(site, query),
-        group_by: s.utm_medium,
-        order_by: [desc: uniq(s.user_id), asc: min(s.start)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: coalesce_string(s.utm_medium, @no_ref),
-          count: uniq(s.user_id),
-          bounce_rate: bounce_rate(),
-          visit_duration: visit_duration()
-        }
-      )
-
-    q =
-      if show_noref do
-        q
-      else
-        from(s in q, where: s.utm_medium != "")
-      end
-
-    q
-    |> filter_converted_sessions(site, query)
-    |> ClickhouseRepo.all()
-  end
-
-  def utm_campaigns(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
-    offset = (page - 1) * limit
-
-    q =
-      from(
-        s in base_session_query(site, query),
-        group_by: s.utm_campaign,
-        order_by: [desc: uniq(s.user_id), asc: min(s.start)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: coalesce_string(s.utm_campaign, @no_ref),
-          count: uniq(s.user_id),
-          bounce_rate: bounce_rate(),
-          visit_duration: visit_duration()
-        }
-      )
-
-    q =
-      if show_noref do
-        q
-      else
-        from(s in q, where: s.utm_campaign != "")
-      end
-
-    q
-    |> filter_converted_sessions(site, query)
-    |> ClickhouseRepo.all()
-  end
-
-  def utm_sources(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
-    offset = (page - 1) * limit
-
-    q =
-      from(
-        s in base_session_query(site, query),
-        group_by: s.utm_source,
-        order_by: [desc: uniq(s.user_id), asc: min(s.start)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: coalesce_string(s.utm_source, @no_ref),
-          count: uniq(s.user_id),
-          bounce_rate: bounce_rate(),
-          visit_duration: visit_duration()
-        }
-      )
-
-    q =
-      if show_noref do
-        q
-      else
-        from(s in q, where: s.utm_source != "")
-      end
-
-    q
-    |> filter_converted_sessions(site, query)
-    |> ClickhouseRepo.all()
-  end
-
-  def conversions_from_referrer(site, query, referrer) do
-    converted_sessions =
-      from(
-        from e in base_query(site, query),
-          select: %{session_id: e.session_id}
-      )
-
-    ClickhouseRepo.one(
-      from s in Plausible.ClickhouseSession,
-        join: cs in subquery(converted_sessions),
-        on: s.session_id == cs.session_id,
-        where: s.referrer_source == ^referrer,
-        select: uniq(s.user_id)
-    )
-  end
-
-  def referrer_drilldown(site, query, referrer, include_details, limit) do
-    referrer = if referrer == @no_ref, do: "", else: referrer
-
-    q =
-      from(
-        s in base_session_query(site, query),
-        group_by: s.referrer,
-        where: s.referrer_source == ^referrer,
-        order_by: [desc: uniq(s.user_id)],
-        limit: ^limit
-      )
-      |> filter_converted_sessions(site, query)
-
-    q =
-      if include_details do
-        from(
-          s in q,
-          select: %{
-            name: coalesce_string(s.referrer, @no_ref),
-            count: uniq(s.user_id),
-            bounce_rate: bounce_rate(),
-            visit_duration: visit_duration()
-          }
-        )
-      else
-        from(s in q,
-          select: %{
-            name: coalesce_string(s.referrer, @no_ref),
-            count: uniq(s.user_id)
-          }
-        )
-      end
-
-    ClickhouseRepo.all(q)
-    |> Enum.map(fn ref ->
-      url = if ref[:name] !== "", do: URI.parse("http://" <> ref[:name]).host
-      Map.put(ref, :url, url)
-    end)
-  end
-
-  def referrer_drilldown_for_goal(site, query, referrer) do
-    Plausible.ClickhouseRepo.all(
-      from s in base_query_w_sessions(site, query),
-        where: s.referrer_source == ^referrer,
-        group_by: s.referrer,
-        order_by: [desc: uniq(s.user_id)],
-        limit: 100,
-        select: %{
-          name: s.referrer,
-          count: uniq(s.user_id)
-        }
-    )
-  end
-
-  def entry_pages(site, query, limit, page \\ 1) do
-    offset = (page - 1) * limit
-
-    q =
-      from(
-        s in base_session_query(site, query),
-        group_by: s.entry_page,
-        order_by: [desc: uniq(s.user_id)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: s.entry_page,
-          count: uniq(s.user_id),
-          entries: uniq(s.session_id),
-          visit_duration: visit_duration()
-        }
-      )
-      |> filter_converted_sessions(site, query)
-
-    ClickhouseRepo.all(q)
-  end
-
-  def exit_pages(site, query, limit, page \\ 1) do
-    offset = (page - 1) * limit
-
-    q =
-      from(
-        s in base_session_query(site, query),
-        group_by: s.exit_page,
-        order_by: [desc: uniq(s.user_id)],
-        limit: ^limit,
-        offset: ^offset,
-        where: s.exit_page != "",
-        select: %{
-          name: s.exit_page,
-          count: uniq(s.user_id),
-          exits: uniq(s.session_id)
-        }
-      )
-      |> filter_converted_sessions(site, query)
-
-    result = ClickhouseRepo.all(q)
-
-    if Enum.count(result) > 0 do
-      pages = Enum.map(result, fn r -> r[:name] end)
-
-      event_q =
-        from(e in base_query_w_session_based_pageviews(site, query),
-          group_by: e.pathname,
-          where: fragment("? IN tuple(?)", e.pathname, ^pages),
-          where: e.name == "pageview",
-          select: {
-            e.pathname,
-            total()
-          }
-        )
-
-      total_pageviews = ClickhouseRepo.all(event_q) |> Enum.into(%{})
-
-      Enum.map(result, fn r ->
-        if Map.get(total_pageviews, r[:name]) do
-          exit_rate = r[:exits] / Map.get(total_pageviews, r[:name]) * 100
-          Map.put(r, :exit_rate, Float.floor(exit_rate))
-        else
-          Map.put(r, :exit_rate, nil)
-        end
-      end)
-    else
-      result
-    end
-  end
-
-  def top_pages(site, %Query{period: "realtime"} = query, limit, page, _include_details) do
-    offset = (page - 1) * limit
-
-    q = base_session_query(site, query) |> apply_page_as_entry_page(site, query)
-
-    ClickhouseRepo.all(
-      from s in q,
-        group_by: s.exit_page,
-        order_by: [desc: uniq(s.user_id)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: s.exit_page,
-          count: uniq(s.user_id)
-        }
-    )
-  end
-
-  def top_pages(site, query, limit, page, include_details) do
-    offset = (page - 1) * limit
-
-    q =
-      from(
-        e in base_query_w_sessions(site, query),
-        group_by: e.pathname,
-        order_by: [desc: uniq(e.user_id)],
-        limit: ^limit,
-        offset: ^offset,
-        select: %{
-          name: e.pathname,
-          count: uniq(e.user_id),
-          pageviews: total()
-        }
-      )
-
-    pages = ClickhouseRepo.all(q)
-
-    if include_details do
-      [{bounce_state, bounce_result}, {time_state, time_result}] =
-        Task.yield_many(
-          [
-            Task.async(fn -> bounce_rates_by_page_url(site, query) end),
-            Task.async(fn ->
-              {:ok, page_times} =
-                page_times_by_page_url(site, query, Enum.map(pages, fn p -> p.name end))
-
-              page_times.rows |> Enum.map(fn [a, b] -> {a, b} end) |> Enum.into(%{})
-            end)
-          ],
-          15000
-        )
-        |> Enum.map(fn {task, response} ->
-          case response do
-            nil ->
-              Task.shutdown(task, :brutal_kill)
-              {nil, nil}
-
-            {:ok, result} ->
-              {:ok, result}
-
-            _ ->
-              response
-          end
-        end)
-
-      Enum.map(pages, fn page ->
-        if bounce_state == :ok,
-          do: Map.put(page, :bounce_rate, bounce_result[page[:name]]),
-          else: page
-      end)
-      |> Enum.map(fn page ->
-        if time_state == :ok do
-          time = time_result[page[:name]]
-
-          Map.put(
-            page,
-            :time_on_page,
-            if(time, do: round(time), else: nil)
-          )
-        else
-          page
-        end
-      end)
-    else
-      pages
-    end
-  end
-
-  defp bounce_rates_by_page_url(site, query) do
-    q = base_session_query(site, query) |> apply_page_as_entry_page(site, query)
-
-    ClickhouseRepo.all(
-      from s in q,
-        group_by: s.entry_page,
-        order_by: [desc: total()],
-        limit: 100,
-        select: %{
-          entry_page: s.entry_page,
-          total: total(),
-          bounce_rate: bounce_rate()
-        }
-    )
-    |> Enum.map(fn row -> {row[:entry_page], row[:bounce_rate]} end)
-    |> Enum.into(%{})
-  end
-
-  def page_times_by_page_url(site, query, page_list \\ nil)
-
-  def page_times_by_page_url(site, query, _page_list = nil) do
-    {negated, updated_page_selection} = query.filters["page"] |> check_negated_filter()
-    {_, page_regex} = updated_page_selection |> convert_path_regex()
-
-    q =
-      from(
-        e in base_query_w_sessions(site, %Query{
-          query
-          | filters: Map.delete(query.filters, "page")
-        }),
-        select: {
-          fragment("? as p", e.pathname),
-          fragment("? as t", e.timestamp),
-          fragment("? as s", e.session_id)
-        },
-        order_by: [e.session_id, e.timestamp]
-      )
-
-    {base_query_raw, base_query_raw_params} = ClickhouseRepo.to_sql(:all, q)
-
-    time_query = "
-      SELECT
-        avg(ifNotFinite(avgTime, null))
-      FROM
-        (SELECT
-          p,
-          sum(td)/count(case when p2 != p then 1 end) as avgTime
-        FROM
-          (SELECT
-            p,
-            p2,
-            sum(t2-t) as td
-          FROM
-            (SELECT
-              *,
-              neighbor(t, 1) as t2,
-              neighbor(p, 1) as p2,
-              neighbor(s, 1) as s2
-            FROM (#{base_query_raw}))
-          WHERE s=s2 AND
-          #{if negated, do: "not(match(p, ?))", else: "match(p, ?)"}
-          GROUP BY p,p2,s)
-        GROUP BY p)"
-
-    time_query |> ClickhouseRepo.query(base_query_raw_params ++ [page_regex])
-  end
-
-  def page_times_by_page_url(site, query, page_list) do
-    q =
-      from(
-        e in base_query_w_sessions(site, %Query{
-          query
-          | filters: Map.delete(query.filters, "page")
-        }),
-        select: {
-          fragment("? as p", e.pathname),
-          fragment("? as t", e.timestamp),
-          fragment("? as s", e.session_id)
-        },
-        order_by: [e.session_id, e.timestamp]
-      )
-
-    {base_query_raw, base_query_raw_params} = ClickhouseRepo.to_sql(:all, q)
-
-    time_query = "
-      SELECT
-        p,
-        sum(td)/count(case when p2 != p then 1 end) as avgTime
-      FROM
-        (SELECT
-          p,
-          p2,
-          sum(t2-t) as td
-        FROM
-          (SELECT
-            *,
-            neighbor(t, 1) as t2,
-            neighbor(p, 1) as p2,
-            neighbor(s, 1) as s2
-          FROM (#{base_query_raw}))
-        WHERE s=s2 AND p IN tuple(?)
-        GROUP BY p,p2,s)
-      GROUP BY p"
-
-    time_query
-    |> ClickhouseRepo.query(
-      base_query_raw_params ++ [(Enum.count(page_list) > 0 && page_list) || ["/"]]
-    )
-  end
-
-  defp add_percentages(stat_list) do
-    total = Enum.reduce(stat_list, 0, fn %{count: count}, total -> total + count end)
-
-    Enum.map(stat_list, fn stat ->
-      Map.put(stat, :percentage, round(stat[:count] / total * 100))
-    end)
-  end
-
-  def top_screen_sizes(site, query) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.screen_size,
-        where: e.screen_size != "",
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: e.screen_size,
-          count: uniq(e.user_id)
-        }
-    )
-    |> add_percentages
-  end
-
-  def countries(site, query) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.country_code,
-        where: e.country_code != "\0\0",
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: e.country_code,
-          count: uniq(e.user_id)
-        }
-    )
-    |> Enum.map(fn stat ->
-      two_letter_code = stat[:name]
-
-      stat
-      |> Map.put(:name, Plausible.Stats.CountryName.to_alpha3(two_letter_code))
-      |> Map.put(:full_country_name, Plausible.Stats.CountryName.from_iso3166(two_letter_code))
-    end)
-    |> add_percentages
-  end
-
-  def browsers(site, query, limit \\ 5) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.browser,
-        where: e.browser != "",
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: e.browser,
-          count: uniq(e.user_id)
-        }
-    )
-    |> add_percentages
-    |> Enum.take(limit)
-  end
-
-  def browser_versions(site, query, limit \\ 5) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.browser_version,
-        where: e.browser_version != "",
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: e.browser_version,
-          count: uniq(e.user_id)
-        }
-    )
-    |> add_percentages
-    |> Enum.take(limit)
-  end
-
-  def operating_systems(site, query, limit \\ 5) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.operating_system,
-        where: e.operating_system != "",
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: e.operating_system,
-          count: uniq(e.user_id)
-        }
-    )
-    |> add_percentages
-    |> Enum.take(limit)
-  end
-
-  def operating_system_versions(site, query, limit \\ 5) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.operating_system_version,
-        where: e.operating_system_version != "",
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: e.operating_system_version,
-          count: uniq(e.user_id)
-        }
-    )
-    |> add_percentages
-    |> Enum.take(limit)
-  end
-
   def current_visitors(site, query) do
     Plausible.ClickhouseRepo.one(
       from e in base_query(site, query),
@@ -891,105 +142,6 @@ defmodule Plausible.Stats.Clickhouse do
     )
   end
 
-  def all_props(site, %Query{filters: %{"props" => meta}} = query) when is_map(meta) do
-    [{key, val}] = meta |> Enum.into([])
-
-    if val == "(none)" do
-      goal = query.filters["goal"]
-      %{goal => [key]}
-    else
-      ClickhouseRepo.all(
-        from [e, meta: meta] in base_query_w_sessions_bare(site, query),
-          select: {e.name, meta.key},
-          distinct: true
-      )
-      |> Enum.reduce(%{}, fn {goal_name, meta_key}, acc ->
-        Map.update(acc, goal_name, [meta_key], fn list -> [meta_key | list] end)
-      end)
-    end
-  end
-
-  def all_props(site, query) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions_bare(site, query),
-        inner_lateral_join: meta in fragment("meta as m"),
-        select: {e.name, meta.key},
-        distinct: true
-    )
-    |> Enum.reduce(%{}, fn {goal_name, meta_key}, acc ->
-      Map.update(acc, goal_name, [meta_key], fn list -> [meta_key | list] end)
-    end)
-  end
-
-  def property_breakdown(site, %Query{filters: %{"props" => meta}} = query, key)
-      when is_map(meta) do
-    [{_key, val}] = meta |> Enum.into([])
-
-    if val == "(none)" do
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          where: fragment("not has(meta.key, ?)", ^key),
-          order_by: [desc: uniq(e.user_id)],
-          select: %{
-            name: "(none)",
-            count: uniq(e.user_id),
-            total_count: total()
-          }
-      )
-    else
-      ClickhouseRepo.all(
-        from [e, meta: meta] in base_query_w_sessions(site, query),
-          group_by: meta.value,
-          order_by: [desc: uniq(e.user_id)],
-          select: %{
-            name: meta.value,
-            count: uniq(e.user_id),
-            total_count: total()
-          }
-      )
-    end
-  end
-
-  def property_breakdown(site, query, key) do
-    none =
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          where: fragment("not has(?.key, ?)", e.meta, ^key),
-          select: %{
-            name: "(none)",
-            count: uniq(e.user_id),
-            total_count: total()
-          }
-      )
-
-    values =
-      ClickhouseRepo.all(
-        from e in base_query_w_sessions(site, query),
-          inner_lateral_join: meta in fragment("meta as m"),
-          where: meta.key == ^key,
-          group_by: meta.value,
-          order_by: [desc: uniq(e.user_id)],
-          select: %{
-            name: meta.value,
-            count: uniq(e.user_id),
-            total_count: total()
-          }
-      )
-
-    (values ++ none)
-    |> Enum.sort(fn row1, row2 -> row1[:count] >= row2[:count] end)
-    |> Enum.filter(fn row -> row[:count] > 0 end)
-    |> Enum.map(fn row ->
-      uri = URI.parse(row[:name])
-
-      if uri.host && uri.scheme do
-        Map.put(row, :is_url, true)
-      else
-        row
-      end
-    end)
-  end
-
   def last_24h_visitors([]), do: %{}
 
   def last_24h_visitors(sites) do
@@ -1005,93 +157,6 @@ defmodule Plausible.Stats.Clickhouse do
     |> Enum.into(%{})
   end
 
-  def goal_conversions(site, %Query{filters: %{"goal" => goal}} = query) when is_binary(goal) do
-    ClickhouseRepo.all(
-      from e in base_query_w_sessions(site, query),
-        group_by: e.name,
-        order_by: [desc: uniq(e.user_id)],
-        select: %{
-          name: ^goal,
-          count: uniq(e.user_id),
-          total_count: total()
-        }
-    )
-  end
-
-  def goal_conversions(site, query) do
-    goals = Repo.all(from g in Plausible.Goal, where: g.domain == ^site.domain)
-    query = if query.period == "realtime", do: %Query{query | period: "30m"}, else: query
-
-    (fetch_pageview_goals(goals, site, query) ++
-       fetch_event_goals(goals, site, query))
-    |> sort_conversions()
-  end
-
-  defp fetch_event_goals(goals, site, query) do
-    events =
-      Enum.map(goals, fn goal -> goal.event_name end)
-      |> Enum.filter(& &1)
-
-    if Enum.count(events) > 0 do
-      q =
-        from(
-          e in base_query_w_sessions_bare(site, query),
-          where: fragment("? IN tuple(?)", e.name, ^events),
-          group_by: e.name,
-          select: %{
-            name: e.name,
-            count: uniq(e.user_id),
-            total_count: total()
-          }
-        )
-
-      ClickhouseRepo.all(q)
-    else
-      []
-    end
-  end
-
-  defp fetch_pageview_goals(goals, site, query) do
-    goals =
-      Enum.map(goals, fn goal -> goal.page_path end)
-      |> Enum.filter(& &1)
-
-    if Enum.count(goals) > 0 do
-      regex_goals =
-        Enum.map(goals, fn g ->
-          "^#{g}\/?$"
-          |> String.replace(~r/\*\*/, ".*")
-          |> String.replace(~r/(?<!\.)\*/, "[^/]*")
-        end)
-
-      from(
-        e in base_query_w_sessions(site, query),
-        where:
-          fragment(
-            "notEmpty(multiMatchAllIndices(?, array(?)) as indices)",
-            e.pathname,
-            ^regex_goals
-          ),
-        select: %{
-          index: fragment("arrayJoin(indices) as index"),
-          count: uniq(e.user_id),
-          total_count: total()
-        },
-        group_by: fragment("index")
-      )
-      |> ClickhouseRepo.all()
-      |> Enum.map(fn x ->
-        Map.put(x, :name, "Visit #{Enum.at(goals, x[:index] - 1)}") |> Map.delete(:index)
-      end)
-    else
-      []
-    end
-  end
-
-  defp sort_conversions(conversions) do
-    Enum.sort_by(conversions, fn conversion -> -conversion[:count] end)
-  end
-
   defp base_query_w_sessions_bare(site, query) do
     {first_datetime, last_datetime} = utc_boundaries(query, site.timezone)
 
@@ -1241,38 +306,6 @@ defmodule Plausible.Stats.Clickhouse do
     end
   end
 
-  defp base_query_w_session_based_pageviews(site, query) do
-    {first_datetime, last_datetime} = utc_boundaries(query, site.timezone)
-
-    sessions_q = base_session_query(site, query) |> filter_converted_sessions(site, query)
-
-    sessions_q =
-      from(s in sessions_q,
-        select: %{session_id: s.session_id}
-      )
-
-    q =
-      from(e in "events",
-        hints: ["SAMPLE 10000000"],
-        where: e.domain == ^site.domain,
-        where: e.timestamp >= ^first_datetime and e.timestamp < ^last_datetime
-      )
-
-    if query.filters["source"] || query.filters["referrer"] || query.filters["utm_medium"] ||
-         query.filters["utm_source"] || query.filters["utm_campaign"] || query.filters["screen"] ||
-         query.filters["browser"] || query.filters["browser_version"] || query.filters["os"] ||
-         query.filters["os_version"] || query.filters["country"] || query.filters["entry_page"] ||
-         query.filters["exit_page"] || query.filters["page"] || query.filters["goal"] do
-      from(
-        e in q,
-        join: sq in subquery(sessions_q),
-        on: e.session_id == sq.session_id
-      )
-    else
-      q
-    end
-  end
-
   defp base_query_w_sessions(site, query) do
     base_query_w_sessions_bare(site, query) |> include_goal_conversions(query)
   end
diff --git a/lib/plausible/stats/compare.ex b/lib/plausible/stats/compare.ex
new file mode 100644
index 00000000..2e98d908
--- /dev/null
+++ b/lib/plausible/stats/compare.ex
@@ -0,0 +1,28 @@
+defmodule Plausible.Stats.Compare do
+  def calculate_change("bounce_rate", old_stats, new_stats) do
+    old_count = old_stats["bounce_rate"]["value"]
+    new_count = new_stats["bounce_rate"]["value"]
+
+    if old_count > 0, do: new_count - old_count
+  end
+
+  def calculate_change(metric, old_stats, new_stats) do
+    old_count = old_stats[metric]["value"]
+    new_count = new_stats[metric]["value"]
+
+    percent_change(old_count, new_count)
+  end
+
+  defp percent_change(old_count, new_count) do
+    cond do
+      old_count == 0 and new_count > 0 ->
+        100
+
+      old_count == 0 and new_count == 0 ->
+        0
+
+      true ->
+        round((new_count - old_count) / old_count * 100)
+    end
+  end
+end
diff --git a/lib/plausible_web/templates/email/weekly_report.html.eex b/lib/plausible_web/templates/email/weekly_report.html.eex
index 5de9cd5f..6756d756 100644
--- a/lib/plausible_web/templates/email/weekly_report.html.eex
+++ b/lib/plausible_web/templates/email/weekly_report.html.eex
@@ -265,7 +265,7 @@ body {
                         <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 5px; padding-left: 5px; padding-top: 0px; padding-bottom: 5px; font-family: Arial, sans-serif"><![endif]-->
                         <div style="color:#555555;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;line-height:120%;padding-top:0px;padding-right:5px;padding-bottom:5px;padding-left:5px;">
                           <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 14px; color: #555555;">
-                            <p style="font-size: 12px; line-height: 14px; text-align: left; margin: 0;"><strong><span style="font-size: 20px; line-height: 24px;"><%= PlausibleWeb.StatsView.large_number_format(@pageviews) %></span></strong></p>
+                            <p style="font-size: 12px; line-height: 14px; text-align: left; margin: 0;"><strong><span id="pageviews" style="font-size: 20px; line-height: 24px;"><%= PlausibleWeb.StatsView.large_number_format(@pageviews) %></span></strong></p>
                           </div>
                         </div>
                         <!--[if mso]></td></tr></table><![endif]-->
@@ -307,7 +307,7 @@ body {
                         <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 5px; padding-left: 5px; padding-top: 0px; padding-bottom: 5px; font-family: Arial, sans-serif"><![endif]-->
                         <div style="color:#555555;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;line-height:120%;padding-top:0px;padding-right:5px;padding-bottom:5px;padding-left:5px;">
                           <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 14px; color: #555555;">
-                            <p style="font-size: 12px; line-height: 14px; text-align: left; margin: 0;"><strong><span style="font-size: 20px; line-height: 24px;"><%= @bounce_rate %>%</span></strong></p>
+                            <p style="font-size: 12px; line-height: 14px; text-align: left; margin: 0;"><strong><span id="bounce_rate" style="font-size: 20px; line-height: 24px;"><%= @bounce_rate %>%</span></strong></p>
                           </div>
                         </div>
                         <!--[if mso]></td></tr></table><![endif]-->
@@ -420,8 +420,8 @@ body {
                 </div>
               </div>
             </div>
-            <%= for referrer <- @referrers do %>
-              <div style="background-color:transparent;">
+            <%= for source <- @sources do %>
+              <div class="referrer" style="background-color:transparent;">
                 <div class="block-grid mixed-two-up" style="Margin: 0 auto; min-width: 320px; max-width: 480px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: transparent;">
                   <div style="border-collapse: collapse;display: table;width: 100%;background-color:transparent;">
                     <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:480px"><tr class="layout-full-width" style="background-color:transparent"><![endif]-->
@@ -434,7 +434,7 @@ body {
                           <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 5px; padding-left: 5px; padding-top: 5px; padding-bottom: 5px; font-family: Arial, sans-serif"><![endif]-->
                           <div style="color:#555555;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;line-height:120%;padding-top:5px;padding-right:5px;padding-bottom:5px;padding-left:5px;">
                             <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 14px; color: #555555;">
-                              <p style="font-size: 14px; line-height: 16px; margin: 0;"><%= referrer[:name] %></p>
+                              <p id="referrer-name" style="font-size: 14px; line-height: 16px; margin: 0;"><%= source["source"] %></p>
                             </div>
                           </div>
                           <!--[if mso]></td></tr></table><![endif]-->
@@ -453,7 +453,7 @@ body {
                           <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 5px; padding-left: 5px; padding-top: 5px; padding-bottom: 5px; font-family: Arial, sans-serif"><![endif]-->
                           <div style="color:#555555;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;line-height:120%;padding-top:5px;padding-right:5px;padding-bottom:5px;padding-left:5px;">
                             <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 14px; color: #555555;">
-                              <p style="font-size: 12px; line-height: 16px; text-align: right; margin: 0;"><span style="font-size: 14px;"><%= PlausibleWeb.StatsView.large_number_format(referrer[:count]) %></span></p>
+                              <p style="font-size: 12px; line-height: 16px; text-align: right; margin: 0;"><span id="referrer-count" style="font-size: 14px;"><%= PlausibleWeb.StatsView.large_number_format(source["visitors"]) %></span></p>
                             </div>
                           </div>
                           <!--[if mso]></td></tr></table><![endif]-->
@@ -550,7 +550,7 @@ body {
               </div>
             </div>
             <%= for page <- @pages do %>
-              <div style="background-color:transparent;">
+              <div class="page" style="background-color:transparent;">
                 <div class="block-grid mixed-two-up" style="Margin: 0 auto; min-width: 320px; max-width: 480px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: transparent;">
                   <div style="border-collapse: collapse;display: table;width: 100%;background-color:transparent;">
                     <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:480px"><tr class="layout-full-width" style="background-color:transparent"><![endif]-->
@@ -563,7 +563,7 @@ body {
                           <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 5px; padding-left: 5px; padding-top: 5px; padding-bottom: 5px; font-family: Arial, sans-serif"><![endif]-->
                           <div style="color:#555555;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;line-height:120%;padding-top:5px;padding-right:5px;padding-bottom:5px;padding-left:5px;">
                             <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 14px; color: #555555;">
-                              <p style="font-size: 14px; line-height: 16px; margin: 0;"><%= page[:name] %></p>
+                              <p id="page-name" style="font-size: 14px; line-height: 16px; margin: 0;"><%= page["page"] %></p>
                             </div>
                           </div>
                           <!--[if mso]></td></tr></table><![endif]-->
@@ -582,7 +582,7 @@ body {
                           <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 5px; padding-left: 5px; padding-top: 5px; padding-bottom: 5px; font-family: Arial, sans-serif"><![endif]-->
                           <div style="color:#555555;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;line-height:120%;padding-top:5px;padding-right:5px;padding-bottom:5px;padding-left:5px;">
                             <div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 14px; color: #555555;">
-                              <p style="font-size: 12px; line-height: 16px; text-align: right; margin: 0;"><span style="font-size: 14px;"><%= PlausibleWeb.StatsView.large_number_format(page[:count]) %></span></p>
+                              <p style="font-size: 12px; line-height: 16px; text-align: right; margin: 0;"><span id="page-count" style="font-size: 14px;"><%= PlausibleWeb.StatsView.large_number_format(page["visitors"]) %></span></p>
                             </div>
                           </div>
                           <!--[if mso]></td></tr></table><![endif]-->
diff --git a/lib/workers/send_email_report.ex b/lib/workers/send_email_report.ex
index ca4f15cb..f53ab48b 100644
--- a/lib/workers/send_email_report.ex
+++ b/lib/workers/send_email_report.ex
@@ -2,7 +2,7 @@ defmodule Plausible.Workers.SendEmailReport do
   use Plausible.Repo
   use Oban.Worker, queue: :send_email_reports, max_attempts: 1
   alias Plausible.Stats.Query
-  alias Plausible.Stats.Clickhouse, as: Stats
+  alias Plausible.Stats
 
   @impl Oban.Worker
   def perform(%Oban.Job{args: %{"interval" => "weekly", "site_id" => site_id}}) do
@@ -49,28 +49,29 @@ defmodule Plausible.Workers.SendEmailReport do
   end
 
   defp send_report(email, site, name, unsubscribe_link, query) do
-    {pageviews, unique_visitors} = Stats.pageviews_and_visitors(site, query)
+    prev_query = Query.shift_back(query, site)
+    curr_period = Stats.aggregate(site, query, ["pageviews", "visitors", "bounce_rate"])
+    prev_period = Stats.aggregate(site, prev_query, ["pageviews", "visitors", "bounce_rate"])
 
-    {change_pageviews, change_visitors} =
-      Stats.compare_pageviews_and_visitors(site, query, {pageviews, unique_visitors})
+    change_pageviews = Stats.Compare.calculate_change("pageviews", prev_period, curr_period)
+    change_visitors = Stats.Compare.calculate_change("visitors", prev_period, curr_period)
+    change_bounce_rate = Stats.Compare.calculate_change("bounce_rate", prev_period, curr_period)
 
-    bounce_rate = Stats.bounce_rate(site, query)
-    prev_bounce_rate = Stats.bounce_rate(site, Query.shift_back(query, site))
-    change_bounce_rate = if prev_bounce_rate > 0, do: bounce_rate - prev_bounce_rate
-    referrers = Stats.top_sources(site, query, 5, 1, [])
-    pages = Stats.top_pages(site, query, 5, 1, [])
+    source_query = Query.put_filter(query, "visit:source", {:is_not, "Direct / None"})
+    sources = Stats.breakdown(site, source_query, "visit:source", ["visitors"], {5, 1})
+    pages = Stats.breakdown(site, query, "event:page", ["visitors"], {5, 1})
     user = Plausible.Auth.find_user_by(email: email)
     login_link = user && Plausible.Sites.is_member?(user.id, site)
 
     template =
       PlausibleWeb.Email.weekly_report(email, site,
-        unique_visitors: unique_visitors,
+        unique_visitors: curr_period["visitors"]["value"],
         change_visitors: change_visitors,
-        pageviews: pageviews,
+        pageviews: curr_period["pageviews"]["value"],
         change_pageviews: change_pageviews,
-        bounce_rate: bounce_rate,
+        bounce_rate: curr_period["bounce_rate"]["value"],
         change_bounce_rate: change_bounce_rate,
-        referrers: referrers,
+        sources: sources,
         unsubscribe_link: unsubscribe_link,
         login_link: login_link,
         pages: pages,
diff --git a/mix.exs b/mix.exs
index aea4c9e0..f8cb2e03 100644
--- a/mix.exs
+++ b/mix.exs
@@ -101,7 +101,8 @@ defmodule Plausible.MixProject do
       {:opentelemetry_exporter, "1.0.0-rc.3"},
       {:opentelemetry_phoenix, "1.0.0-rc.5"},
       {:opentelemetry_ecto, "1.0.0-rc.3"},
-      {:opentelemetry_oban, "~> 0.2.0-rc.2"}
+      {:opentelemetry_oban, "~> 0.2.0-rc.2"},
+      {:floki, "~> 0.32.0", only: :test}
     ]
   end
 
diff --git a/mix.lock b/mix.lock
index 5aac48a2..b76bf89c 100644
--- a/mix.lock
+++ b/mix.lock
@@ -37,6 +37,7 @@
   "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
   "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"},
   "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+  "floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"},
   "gen_smtp": {:hex, :gen_smtp, "1.1.1", "bf9303c31735100631b1d708d629e4c65944319d1143b5c9952054f4a1311d85", [:rebar3], [{:hut, "1.3.0", [hex: :hut, repo: "hexpm", optional: false]}, {:ranch, ">= 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "51bc50cc017efd4a4248cbc39ea30fb60efa7d4a49688986fafad84434ff9ab7"},
   "geolix": {:hex, :geolix, "1.1.0", "8b0fe847fef486d9e8b7c21eae6cbc2d998fb249e61d3f4f136f8016b9c1c833", [:mix], [{:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "980854f2aef30c288dc79e86c5267806d704c4525fde1b75de9a92f67fb16300"},
   "geolix_adapter_mmdb2": {:hex, :geolix_adapter_mmdb2, "0.5.0", "5912723d9538ecddc6b29b1d8041b917b735a78fd3c122bfea8c44aa782e3369", [:mix], [{:geolix, "~> 1.1", [hex: :geolix, repo: "hexpm", optional: false]}, {:mmdb2_decoder, "~> 3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}], "hexpm", "cb1485b6a0a2d3e541949207428a245718dbf1258453a0df0e5fdd925bcecd3e"},
@@ -46,6 +47,7 @@
   "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
   "hammer": {:hex, :hammer, "6.0.0", "72ec6fff10e9d63856968988a22ee04c4d6d5248071ddccfbda50aa6c455c1d7", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "d8e1ec2e534c4aae508b906759e077c3c1eb3e2b9425235d4b7bbab0b016210a"},
   "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"},
+  "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
   "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
   "hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
   "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
diff --git a/test/workers/send_email_report_test.exs b/test/workers/send_email_report_test.exs
index 09f45e33..fb8b6267 100644
--- a/test/workers/send_email_report_test.exs
+++ b/test/workers/send_email_report_test.exs
@@ -60,6 +60,51 @@ defmodule Plausible.Workers.SendEmailReportTest do
       assert html_body =~
                ~s(<span id="visitors" style="line-height: 24px; font-size: 20px;">2</span>)
     end
+
+    test "includes the correct stats" do
+      site = insert(:site, domain: "test-site.com")
+      insert(:weekly_report, site: site, recipients: ["user@email.com"])
+      now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+
+      populate_stats(site, [
+        build(:pageview,
+          referrer_source: "Google",
+          user_id: 123,
+          timestamp: Timex.shift(now, days: -7)
+        ),
+        build(:pageview, user_id: 123, timestamp: Timex.shift(now, days: -7)),
+        build(:pageview, timestamp: Timex.shift(now, days: -7))
+      ])
+
+      perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"})
+
+      assert_delivered_email_matches(%{
+        to: [nil: "user@email.com"],
+        html_body: html_body
+      })
+
+      {:ok, document} = Floki.parse_document(html_body)
+
+      visitors = Floki.find(document, "#visitors") |> Floki.text()
+      assert visitors == "2"
+
+      pageviews = Floki.find(document, "#pageviews") |> Floki.text()
+      assert pageviews == "3"
+
+      referrer = Floki.find(document, ".referrer") |> List.first()
+      referrer_name = referrer |> Floki.find("#referrer-name") |> Floki.text()
+      referrer_count = referrer |> Floki.find("#referrer-count") |> Floki.text()
+
+      assert referrer_name == "Google"
+      assert referrer_count == "1"
+
+      page = Floki.find(document, ".page") |> List.first()
+      page_name = page |> Floki.find("#page-name") |> Floki.text()
+      page_count = page |> Floki.find("#page-count") |> Floki.text()
+
+      assert page_name == "/"
+      assert page_count == "2"
+    end
   end
 
   describe "monthly_reports" do
-- 
GitLab