| defmodule Plausible.Workers.SendEmailReport do |
| use Plausible |
| use Plausible.Repo |
| use Oban.Worker, queue: :send_email_reports, max_attempts: 1 |
|
|
| alias Plausible.Stats.{QueryResult, QueryBuilder, ParsedQueryParams, QueryInclude} |
|
|
| 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 |
| shared_params = %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: {:date_range, date_range.first, date_range.last}, |
| pagination: %{limit: 5, offset: 0} |
| } |
|
|
| stats = stats_aggregates(site, shared_params) |
| pages = pages(site, shared_params) |
| sources = sources(site, shared_params) |
| goals = goals(site, shared_params) |
|
|
| stats |
| |> Map.put(:pages, pages) |
| |> Map.put(:sources, sources) |
| |> Map.put(:goals, goals) |
| end |
|
|
| defp stats_aggregates(site, %ParsedQueryParams{} = shared_params) do |
| query = |
| QueryBuilder.build!( |
| site, |
| struct!(shared_params, |
| metrics: [:pageviews, :visitors, :bounce_rate], |
| include: %QueryInclude{compare: :previous_period} |
| ) |
| ) |
|
|
| %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, %ParsedQueryParams{} = shared_params) do |
| query = QueryBuilder.build!(site, struct!(shared_params, dimensions: ["event:page"])) |
|
|
| site |
| |> Plausible.Stats.query(query) |
| |> Map.fetch!(:results) |
| |> Enum.map(fn %{metrics: [visitors], dimensions: [page]} -> |
| %{ |
| page: page, |
| visitors: visitors |
| } |
| end) |
| end |
|
|
| defp sources(site, %ParsedQueryParams{} = shared_params) do |
| query = |
| QueryBuilder.build!( |
| site, |
| struct!(shared_params, |
| dimensions: ["visit:source"], |
| filters: [[:is_not, "visit:source", ["Direct / None"]]] |
| ) |
| ) |
|
|
| site |
| |> Plausible.Stats.query(query) |
| |> Map.fetch!(:results) |
| |> Enum.map(fn %{metrics: [visitors], dimensions: [source]} -> |
| %{ |
| source: source, |
| visitors: visitors |
| } |
| end) |
| end |
|
|
| defp goals(site, %ParsedQueryParams{} = shared_params) do |
| query = QueryBuilder.build!(site, struct!(shared_params, dimensions: ["event:goal"])) |
|
|
| 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 |
|
|