241 lines
6.0 KiB
Elixir
241 lines
6.0 KiB
Elixir
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
|