File size: 5,128 Bytes
c27e67a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | defmodule Plausible.Workers.TrafficChangeNotifier do
@moduledoc """
Oban service sending out traffic drop/spike notifications
"""
use Plausible
use Plausible.Repo
alias Plausible.Stats.{Clickhouse, ParsedQueryParams, QueryBuilder}
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 %ParsedQueryParams{
metrics: [:visitors],
pagination: %{limit: 3, offset: 0},
input_date_range: :realtime
}
defp put_sources(stats, site) do
query =
QueryBuilder.build!(
site,
struct!(@base_query_params,
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 = QueryBuilder.build!(site, struct!(@base_query_params, 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
|