192 lines
5.2 KiB
Elixir
192 lines
5.2 KiB
Elixir
defmodule Plausible.Workers.TrafficChangeNotifier do
|
|
@moduledoc """
|
|
Oban service sending out traffic drop/spike notifications
|
|
"""
|
|
use Plausible
|
|
use Plausible.Repo
|
|
alias Plausible.Stats.{Query, Clickhouse}
|
|
alias Plausible.Site.TrafficChangeNotification
|
|
|
|
alias PlausibleWeb.Router.Helpers, as: Routes
|
|
|
|
use Oban.Worker, queue: :spike_notifications
|
|
@min_interval_hours 12
|
|
|
|
@impl Oban.Worker
|
|
def perform(_job, now \\ NaiveDateTime.utc_now(:second)) do
|
|
today = NaiveDateTime.to_date(now)
|
|
|
|
notifications =
|
|
Repo.all(
|
|
from sn in TrafficChangeNotification,
|
|
where:
|
|
is_nil(sn.last_sent) or
|
|
sn.last_sent < ^NaiveDateTime.add(now, -@min_interval_hours, :hour),
|
|
inner_join: s in assoc(sn, :site),
|
|
inner_join: t in assoc(s, :team),
|
|
where: not t.locked,
|
|
where: is_nil(t.accept_traffic_until) or t.accept_traffic_until > ^today,
|
|
preload: [site: {s, team: t}]
|
|
)
|
|
|
|
for notification <- notifications, ok_to_send?(notification.site) do
|
|
handle_notification(notification, now)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
defp handle_notification(%TrafficChangeNotification{type: :spike} = notification, now) do
|
|
current_visitors = Clickhouse.current_visitors(notification.site)
|
|
|
|
if current_visitors >= notification.threshold do
|
|
stats =
|
|
notification.site
|
|
|> get_traffic_spike_stats()
|
|
|> Map.put(:current_visitors, current_visitors)
|
|
|
|
notify_spike(notification, stats, now)
|
|
end
|
|
end
|
|
|
|
defp handle_notification(%TrafficChangeNotification{type: :drop} = notification, now) do
|
|
current_visitors = Clickhouse.current_visitors_12h(notification.site)
|
|
|
|
if current_visitors < notification.threshold do
|
|
notify_drop(notification, current_visitors, now)
|
|
end
|
|
end
|
|
|
|
defp notify_spike(notification, stats, now) do
|
|
for recipient_email <- notification.recipients do
|
|
send_spike_notification(recipient_email, notification.site, stats)
|
|
end
|
|
|
|
notification
|
|
|> TrafficChangeNotification.was_sent(now)
|
|
|> Repo.update()
|
|
end
|
|
|
|
defp notify_drop(notification, current_visitors, now) do
|
|
for recipient_email <- notification.recipients do
|
|
send_drop_notification(recipient_email, notification.site, current_visitors)
|
|
end
|
|
|
|
notification
|
|
|> TrafficChangeNotification.was_sent(now)
|
|
|> Repo.update()
|
|
end
|
|
|
|
defp send_spike_notification(recipient_email, site, stats) do
|
|
dashboard_link =
|
|
if site_member?(site, recipient_email) do
|
|
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []) <>
|
|
"?__team=#{site.team.identifier}"
|
|
end
|
|
|
|
template =
|
|
PlausibleWeb.Email.spike_notification(
|
|
recipient_email,
|
|
site,
|
|
stats,
|
|
dashboard_link
|
|
)
|
|
|
|
Plausible.Mailer.send(template)
|
|
end
|
|
|
|
defp send_drop_notification(recipient_email, site, current_visitors) do
|
|
site_member? = site_member?(site, recipient_email)
|
|
|
|
dashboard_link =
|
|
if site_member? do
|
|
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []) <>
|
|
"?__team=#{site.team.identifier}"
|
|
end
|
|
|
|
installation_link =
|
|
if site_member? and Plausible.Sites.regular?(site) do
|
|
Routes.site_url(PlausibleWeb.Endpoint, :installation, site.domain,
|
|
flow: PlausibleWeb.Flows.review()
|
|
) <> "&__team=#{site.team.identifier}"
|
|
end
|
|
|
|
template =
|
|
PlausibleWeb.Email.drop_notification(
|
|
recipient_email,
|
|
site,
|
|
current_visitors,
|
|
dashboard_link,
|
|
installation_link
|
|
)
|
|
|
|
Plausible.Mailer.send(template)
|
|
end
|
|
|
|
defp get_traffic_spike_stats(site) do
|
|
%{}
|
|
|> put_sources(site)
|
|
|> put_pages(site)
|
|
end
|
|
|
|
@base_query_params %{
|
|
"metrics" => ["visitors"],
|
|
"pagination" => %{"limit" => 3},
|
|
"date_range" => "realtime"
|
|
}
|
|
|
|
defp put_sources(stats, site) do
|
|
query =
|
|
Query.parse_and_build!(
|
|
site,
|
|
:internal,
|
|
Map.merge(@base_query_params, %{
|
|
"site_id" => site.domain,
|
|
"dimensions" => ["visit:source"],
|
|
"filters" => [["is_not", "visit:source", ["Direct / None"]]]
|
|
})
|
|
)
|
|
|
|
%{results: sources} = Plausible.Stats.query(site, query)
|
|
|
|
Map.put(stats, :sources, sources)
|
|
end
|
|
|
|
defp put_pages(stats, site) do
|
|
query =
|
|
Query.parse_and_build!(
|
|
site,
|
|
:internal,
|
|
Map.merge(@base_query_params, %{
|
|
"site_id" => site.domain,
|
|
"dimensions" => ["event:page"]
|
|
})
|
|
)
|
|
|
|
%{results: pages} = Plausible.Stats.query(site, query)
|
|
|
|
Map.put(stats, :pages, pages)
|
|
end
|
|
|
|
defp site_member?(site, recipient_email) do
|
|
from(tm in Plausible.Teams.Membership,
|
|
inner_join: u in assoc(tm, :user),
|
|
left_join: gm in assoc(tm, :guest_memberships),
|
|
where: tm.team_id == ^site.team_id,
|
|
where: tm.role != :guest or gm.site_id == ^site.id,
|
|
where: u.email == ^recipient_email
|
|
)
|
|
|> Repo.exists?()
|
|
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
|