defmodule Plausible.Workers.SendEmailReport do use Plausible use Plausible.Repo use Oban.Worker, queue: :send_email_reports, max_attempts: 1 alias Plausible.Stats.{Query, QueryResult} import Ecto.Query, only: [from: 2] @weekly "weekly" @monthly "monthly" def perform(%Oban.Job{args: %{"interval" => interval, "site_id" => site_id}}) when interval in [@weekly, @monthly] do report_type = report_type(interval) site = from(s in Plausible.Site, where: s.id == ^site_id, inner_join: r in assoc(s, ^report_type), inner_join: t in assoc(s, :team), preload: [{^report_type, r}, {:team, t}] ) |> Repo.one() with %Plausible.Site{} <- site, %{} = report <- Map.fetch!(site, report_type), true <- ok_to_send?(site) do date_range = date_range(site, interval) report_name = report_name(interval, date_range.first) date_label = Calendar.strftime(date_range.last, "%-d %b %Y") stats = stats(site, date_range) report |> Map.fetch!(:recipients) |> Enum.each(fn email -> assigns = %{ site: site, report_name: report_name, date_label: date_label, unsubscribe_link: unsubscribe_link(site, email, interval), site_member?: site_member?(site, email), interval: interval, stats: stats } email |> PlausibleWeb.Email.stats_report(assigns) |> Plausible.Mailer.send() end) else _ -> :discard end end defp report_type(@weekly), do: :weekly_report defp report_type(@monthly), do: :monthly_report defp site_member?(site, email) do user = Plausible.Auth.find_user_by(email: email) user && Plausible.Teams.Memberships.site_member?(site, user) end defp unsubscribe_link(site, email, interval) do PlausibleWeb.Endpoint.url() <> "/sites/#{URI.encode_www_form(site.domain)}/#{interval}-report/unsubscribe?email=#{email}" end defp report_name(@weekly, _), do: "Weekly" defp report_name(@monthly, first), do: Calendar.strftime(first, "%B") defp stats(site, date_range) do date_range = [ Date.to_iso8601(date_range.first), Date.to_iso8601(date_range.last) ] stats = stats_aggregates(site, date_range) pages = pages(site, date_range) sources = sources(site, date_range) goals = goals(site, date_range) stats |> Map.put(:pages, pages) |> Map.put(:sources, sources) |> Map.put(:goals, goals) end defp stats_aggregates(site, date_range) do query = Query.parse_and_build!( site, :internal, %{ "site_id" => site.domain, "metrics" => ["pageviews", "visitors", "bounce_rate"], "date_range" => date_range, "include" => %{"comparisons" => %{"mode" => "previous_period"}}, "pagination" => %{"limit" => 5} } ) %QueryResult{ results: [ %{ metrics: [pageviews, visitors, bounce_rate], comparison: %{ change: [pageviews_change, visitors_change, bounce_rate_change] } } ] } = Plausible.Stats.query(site, query) %{ pageviews: %{value: pageviews, change: pageviews_change}, visitors: %{value: visitors, change: visitors_change}, bounce_rate: %{value: bounce_rate, change: bounce_rate_change} } end defp pages(site, date_range) do query = Query.parse_and_build!( site, :internal, %{ "site_id" => site.domain, "metrics" => ["visitors"], "dimensions" => ["event:page"], "date_range" => date_range, "pagination" => %{"limit" => 5} } ) site |> Plausible.Stats.query(query) |> Map.fetch!(:results) |> Enum.map(fn %{metrics: [visitors], dimensions: [page]} -> %{ page: page, visitors: visitors } end) end defp sources(site, date_range) do query = Query.parse_and_build!( site, :internal, %{ "site_id" => site.domain, "metrics" => ["visitors"], "filters" => [["is_not", "visit:source", ["Direct / None"]]], "dimensions" => ["visit:source"], "date_range" => date_range, "pagination" => %{"limit" => 5} } ) site |> Plausible.Stats.query(query) |> Map.fetch!(:results) |> Enum.map(fn %{metrics: [visitors], dimensions: [source]} -> %{ source: source, visitors: visitors } end) end defp goals(site, date_range) do query = Query.parse_and_build!( site, :internal, %{ "site_id" => site.domain, "metrics" => ["visitors"], "dimensions" => ["event:goal"], "date_range" => date_range, "pagination" => %{"limit" => 5} } ) site |> Plausible.Stats.query(query) |> Map.fetch!(:results) |> Enum.map(fn %{metrics: [visitors], dimensions: [goal_name]} -> %{ goal: goal_name, visitors: visitors } end) end defp date_range(site, @weekly) do first = site.timezone |> DateTime.now!() |> Date.shift(day: -7) |> Date.beginning_of_week() last = site.timezone |> DateTime.now!() |> DateTime.to_date() |> Date.shift(day: -7) |> Date.end_of_week() Date.range(first, last) end defp date_range(site, @monthly) do first = site.timezone |> DateTime.now!() |> Date.shift(month: -1) |> Date.beginning_of_month() last = site.timezone |> DateTime.now!() |> DateTime.shift(month: -1) |> DateTime.to_date() |> Date.end_of_month() Date.range(first, last) end on_ee do defp ok_to_send?(site) do Plausible.Sites.regular?(site) or (Plausible.Sites.consolidated?(site) and Plausible.ConsolidatedView.ok_to_display?(site.team)) end else defp ok_to_send?(_site), do: always(true) end end