mirror of
https://github.com/plausible/analytics.git
synced 2024-08-17 05:40:24 +03:00
Implement traffic drop notifications (#4300)
* Expose current visitors 12h aggregate * Remove unused site association * Distinct drop/spike notification factories * Rename modules accordingly + implement drop handling * Rename periodic oban service * Implement drop email * Rest of the owl * Update changelog * Update moduledoc * Update moduledoc * Min threshold to 1 * Threshold 1 * Remove merge artifact * Put panel behind a feature flag * Format
This commit is contained in:
parent
3ab47e6401
commit
d56bb2b4d5
@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Icons for browsers plausible/analytics#4239
|
||||
- Automatic custom property selection in the dashboard Properties report
|
||||
- Add `does_not_contain` filter support to dashboard
|
||||
- Traffic drop notifications plausible/analytics#4300
|
||||
|
||||
### Removed
|
||||
- Deprecate `ECTO_IPV6` and `ECTO_CH_IPV6` env vars in CE plausible/analytics#4245
|
||||
|
@ -528,7 +528,7 @@ base_cron = [
|
||||
# Daily at midday
|
||||
{"0 12 * * *", Plausible.Workers.SendCheckStatsEmails},
|
||||
# Every 15 minutes
|
||||
{"*/15 * * * *", Plausible.Workers.SpikeNotifier},
|
||||
{"*/15 * * * *", Plausible.Workers.TrafficChangeNotifier},
|
||||
# Every day at 1am
|
||||
{"0 1 * * *", Plausible.Workers.CleanInvitations},
|
||||
# Every 2 hours
|
||||
|
@ -40,7 +40,6 @@ defmodule Plausible.Site do
|
||||
has_one :google_auth, GoogleAuth
|
||||
has_one :weekly_report, Plausible.Site.WeeklyReport
|
||||
has_one :monthly_report, Plausible.Site.MonthlyReport
|
||||
has_one :spike_notification, Plausible.Site.SpikeNotification
|
||||
has_one :ownership, Plausible.Site.Membership, where: [role: :owner]
|
||||
has_one :owner, through: [:ownership, :user]
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
defmodule Plausible.Site.SpikeNotification do
|
||||
defmodule Plausible.Site.TrafficChangeNotification do
|
||||
@moduledoc """
|
||||
Configuration schema for site-specific traffic change notifications.
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
# legacy table name since traffic drop notifications were introduced
|
||||
schema "spike_notifications" do
|
||||
field :recipients, {:array, :string}
|
||||
field :threshold, :integer
|
||||
@ -14,8 +18,9 @@ defmodule Plausible.Site.SpikeNotification do
|
||||
|
||||
def changeset(schema, attrs) do
|
||||
schema
|
||||
|> cast(attrs, [:site_id, :recipients, :threshold])
|
||||
|> validate_required([:site_id, :recipients, :threshold])
|
||||
|> cast(attrs, [:site_id, :recipients, :threshold, :type])
|
||||
|> validate_required([:site_id, :recipients, :threshold, :type])
|
||||
|> validate_number(:threshold, greater_than_or_equal_to: 1)
|
||||
|> unique_constraint([:site_id, :type])
|
||||
end
|
||||
|
@ -41,9 +41,9 @@ defmodule Plausible.Stats do
|
||||
Timeseries.timeseries(site, query, metrics)
|
||||
end
|
||||
|
||||
def current_visitors(site) do
|
||||
def current_visitors(site, shift \\ [minutes: -5]) do
|
||||
include_sentry_replay_info()
|
||||
CurrentVisitors.current_visitors(site)
|
||||
CurrentVisitors.current_visitors(site, shift)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
|
@ -107,6 +107,10 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
Plausible.Stats.current_visitors(site)
|
||||
end
|
||||
|
||||
def current_visitors_12h(site) do
|
||||
Plausible.Stats.current_visitors(site, hours: -12)
|
||||
end
|
||||
|
||||
def has_pageviews?(site) do
|
||||
ClickhouseRepo.exists?(
|
||||
from(e in "events_v2",
|
||||
|
@ -2,10 +2,10 @@ defmodule Plausible.Stats.CurrentVisitors do
|
||||
use Plausible.ClickhouseRepo
|
||||
use Plausible.Stats.SQL.Fragments
|
||||
|
||||
def current_visitors(site) do
|
||||
def current_visitors(site, shift \\ [minutes: -5]) do
|
||||
first_datetime =
|
||||
NaiveDateTime.utc_now()
|
||||
|> Timex.shift(minutes: -5)
|
||||
|> Timex.shift(shift)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
ClickhouseRepo.one(
|
||||
|
@ -211,7 +211,10 @@ defmodule PlausibleWeb.SiteController do
|
||||
site: site,
|
||||
weekly_report: Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id),
|
||||
monthly_report: Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id),
|
||||
spike_notification: Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id),
|
||||
spike_notification:
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification, site_id: site.id, type: :spike),
|
||||
drop_notification:
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification, site_id: site.id, type: :drop),
|
||||
dogfood_page_path: "/:dashboard/settings/email-reports",
|
||||
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
|
||||
)
|
||||
@ -472,44 +475,58 @@ defmodule PlausibleWeb.SiteController do
|
||||
|> redirect(external: Routes.site_path(conn, :settings_email_reports, site.domain))
|
||||
end
|
||||
|
||||
def enable_spike_notification(conn, _params) do
|
||||
def enable_traffic_change_notification(conn, %{"type" => type}) do
|
||||
site = conn.assigns[:site]
|
||||
|
||||
res =
|
||||
Plausible.Site.SpikeNotification.changeset(%Plausible.Site.SpikeNotification{}, %{
|
||||
site_id: site.id,
|
||||
threshold: 10,
|
||||
recipients: [conn.assigns[:current_user].email]
|
||||
})
|
||||
Plausible.Site.TrafficChangeNotification.changeset(
|
||||
%Plausible.Site.TrafficChangeNotification{},
|
||||
%{
|
||||
site_id: site.id,
|
||||
type: type,
|
||||
threshold: if(type == "spike", do: 10, else: 1),
|
||||
recipients: [conn.assigns[:current_user].email]
|
||||
}
|
||||
)
|
||||
|> Repo.insert()
|
||||
|
||||
case res do
|
||||
{:ok, _} ->
|
||||
conn
|
||||
|> put_flash(:success, "You will a notification with traffic spikes going forward")
|
||||
|> put_flash(:success, "Traffic #{type} notifications enabled")
|
||||
|> redirect(external: Routes.site_path(conn, :settings_email_reports, site.domain))
|
||||
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> put_flash(:error, "Unable to create a spike notification")
|
||||
|> put_flash(:error, "Unable to create a #{type} notification")
|
||||
|> redirect(external: Routes.site_path(conn, :settings_email_reports, site.domain))
|
||||
end
|
||||
end
|
||||
|
||||
def disable_spike_notification(conn, _params) do
|
||||
def disable_traffic_change_notification(conn, %{"type" => type}) do
|
||||
site = conn.assigns[:site]
|
||||
Repo.delete_all(from(mr in Plausible.Site.SpikeNotification, where: mr.site_id == ^site.id))
|
||||
|
||||
Repo.delete_all(
|
||||
from(mr in Plausible.Site.TrafficChangeNotification,
|
||||
where: mr.site_id == ^site.id and mr.type == ^type
|
||||
)
|
||||
)
|
||||
|
||||
conn
|
||||
|> put_flash(:success, "Spike notification disabled")
|
||||
|> put_flash(:success, "Traffic #{type} notifications disabled")
|
||||
|> redirect(external: Routes.site_path(conn, :settings_email_reports, site.domain))
|
||||
end
|
||||
|
||||
def update_spike_notification(conn, %{"spike_notification" => params}) do
|
||||
def update_traffic_change_notification(conn, %{
|
||||
"traffic_change_notification" => params,
|
||||
"type" => type
|
||||
}) do
|
||||
site = conn.assigns[:site]
|
||||
notification = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
|
||||
Plausible.Site.SpikeNotification.changeset(notification, params)
|
||||
notification =
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification, site_id: site.id, type: type)
|
||||
|
||||
Plausible.Site.TrafficChangeNotification.changeset(notification, params)
|
||||
|> Repo.update!()
|
||||
|
||||
conn
|
||||
@ -517,11 +534,11 @@ defmodule PlausibleWeb.SiteController do
|
||||
|> redirect(external: Routes.site_path(conn, :settings_email_reports, site.domain))
|
||||
end
|
||||
|
||||
def add_spike_notification_recipient(conn, %{"recipient" => recipient}) do
|
||||
def add_traffic_change_notification_recipient(conn, %{"recipient" => recipient, "type" => type}) do
|
||||
site = conn.assigns[:site]
|
||||
|
||||
Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
|> Plausible.Site.SpikeNotification.add_recipient(recipient)
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification, site_id: site.id, type: type)
|
||||
|> Plausible.Site.TrafficChangeNotification.add_recipient(recipient)
|
||||
|> Repo.update!()
|
||||
|
||||
conn
|
||||
@ -529,11 +546,14 @@ defmodule PlausibleWeb.SiteController do
|
||||
|> redirect(external: Routes.site_path(conn, :settings_email_reports, site.domain))
|
||||
end
|
||||
|
||||
def remove_spike_notification_recipient(conn, %{"recipient" => recipient}) do
|
||||
def remove_traffic_change_notification_recipient(conn, %{
|
||||
"recipient" => recipient,
|
||||
"type" => type
|
||||
}) do
|
||||
site = conn.assigns[:site]
|
||||
|
||||
Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
|> Plausible.Site.SpikeNotification.remove_recipient(recipient)
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification, site_id: site.id, type: type)
|
||||
|> Plausible.Site.TrafficChangeNotification.remove_recipient(recipient)
|
||||
|> Repo.update!()
|
||||
|
||||
conn
|
||||
|
@ -141,6 +141,19 @@ defmodule PlausibleWeb.Email do
|
||||
})
|
||||
end
|
||||
|
||||
def drop_notification(email, site, current_visitors, dashboard_link, settings_link) do
|
||||
base_email()
|
||||
|> to(email)
|
||||
|> tag("drop-notification")
|
||||
|> subject("Traffic Drop on #{site.domain}")
|
||||
|> render("drop_notification.html", %{
|
||||
site: site,
|
||||
current_visitors: current_visitors,
|
||||
dashboard_link: dashboard_link,
|
||||
settings_link: settings_link
|
||||
})
|
||||
end
|
||||
|
||||
def over_limit_email(user, usage, suggested_plan) do
|
||||
priority_email()
|
||||
|> to(user)
|
||||
|
@ -331,17 +331,25 @@ defmodule PlausibleWeb.Router do
|
||||
SiteController,
|
||||
:remove_monthly_report_recipient
|
||||
|
||||
post "/sites/:website/spike-notification/enable", SiteController, :enable_spike_notification
|
||||
post "/sites/:website/spike-notification/disable", SiteController, :disable_spike_notification
|
||||
put "/sites/:website/spike-notification", SiteController, :update_spike_notification
|
||||
|
||||
post "/sites/:website/spike-notification/recipients",
|
||||
post "/sites/:website/traffic-change-notification/:type/enable",
|
||||
SiteController,
|
||||
:add_spike_notification_recipient
|
||||
:enable_traffic_change_notification
|
||||
|
||||
delete "/sites/:website/spike-notification/recipients/:recipient",
|
||||
post "/sites/:website/traffic-change-notification/:type/disable",
|
||||
SiteController,
|
||||
:disable_traffic_change_notification
|
||||
|
||||
put "/sites/:website/traffic-change-notification/:type",
|
||||
SiteController,
|
||||
:update_traffic_change_notification
|
||||
|
||||
post "/sites/:website/traffic-change-notification/:type/recipients",
|
||||
SiteController,
|
||||
:add_traffic_change_notification_recipient
|
||||
|
||||
delete "/sites/:website/traffic-change-notification/:type/recipients/:recipient",
|
||||
SiteController,
|
||||
:remove_spike_notification_recipient
|
||||
:remove_traffic_change_notification_recipient
|
||||
|
||||
get "/sites/:website/shared-links/new", SiteController, :new_shared_link
|
||||
post "/sites/:website/shared-links", SiteController, :create_shared_link
|
||||
|
@ -0,0 +1,9 @@
|
||||
We've recorded <%= @current_visitors %> visitors on <%= link(@site.domain, to: "https://" <> @site.domain) %> in the last 12 hours.
|
||||
|
||||
<%= if @dashboard_link do %>
|
||||
<br /><br />
|
||||
View dashboard: <%= link(@dashboard_link, to: @dashboard_link) %>
|
||||
<br /><br />
|
||||
Something looks off? Please use our integration testing tool to verify that Plausible has been integrated correctly:
|
||||
<%= link(@settings_link, to: @settings_link) %>
|
||||
<% end %>
|
@ -195,135 +195,26 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Traffic Spike Notifications
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Get notified when your site has unusually high number of current visitors
|
||||
</p>
|
||||
<%= render("traffic_change_form",
|
||||
conn: @conn,
|
||||
notification: @spike_notification,
|
||||
site: @site,
|
||||
heading: "Traffic Spike Notifications",
|
||||
subtitle: "Get notified when your site has unusually high number of current visitors",
|
||||
toggle_text: "Send notifications of traffic spikes",
|
||||
threshold_label: "Current visitors threshold",
|
||||
type: :spike
|
||||
) %>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="traffic-spikes" />
|
||||
</header>
|
||||
|
||||
<div class="my-8 flex items-center">
|
||||
<%= if @spike_notification do %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-5 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-0 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="ml-2 dark:text-gray-100">Send notifications of traffic spikes</span>
|
||||
</div>
|
||||
|
||||
<%= if @spike_notification do %>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 mt-6">
|
||||
<%= form_for Plausible.Site.SpikeNotification.changeset(@spike_notification, %{}), "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification", fn f -> %>
|
||||
<h4 class="font-bold my-2">Current visitor threshold</h4>
|
||||
<div class="mt-1 flex rounded-md shadow-sm max-w-md">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<!-- Heroicon name: users -->
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= number_input(f, :threshold,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:text-gray-100"
|
||||
) %>
|
||||
</div>
|
||||
<button class="-ml-px relative button rounded-l-none">
|
||||
<span>Save threshold</span>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<h4 class="font-bold mt-6 dark:text-gray-100">Notification recipients</h4>
|
||||
<%= for recipient <- @spike_notification.recipients do %>
|
||||
<div class="p-2 pl-3 flex justify-between bg-gray-100 dark:bg-gray-900 rounded my-2 max-w-md">
|
||||
<span>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 inline mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<%= recipient %>
|
||||
</span>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/recipients/#{recipient}", method: :delete) do %>
|
||||
<svg
|
||||
class="w-4 h-4 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/recipients", fn f -> %>
|
||||
<div class="max-w-md mt-4">
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= email_input(f, :recipient,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100",
|
||||
placeholder: "recipient@example.com",
|
||||
required: "true"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<%= submit class: "-ml-px relative button rounded-l-none" do %>
|
||||
<svg
|
||||
class="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Add recipient</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if FunWithFlags.enabled?(:traffic_drop_notifications, for: @site) do %>
|
||||
<%= render("traffic_change_form",
|
||||
conn: @conn,
|
||||
notification: @drop_notification,
|
||||
site: @site,
|
||||
heading: "Traffic Drop Notifications",
|
||||
subtitle: "Get notified when your site has unusually low number of visitors within 12 hours",
|
||||
toggle_text: "Send notifications of traffic drops",
|
||||
threshold_label: "12 hours visitor threshold",
|
||||
type: :drop
|
||||
) %>
|
||||
<% end %>
|
||||
|
@ -65,7 +65,7 @@
|
||||
<%= form_for @conn, "/", [class: "shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6"], fn f -> %>
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
JavaScript Snippet
|
||||
<a id="snippet">JavaScript Snippet</a>
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Include this Snippet in the <code><head></code> of your Website.
|
||||
|
133
lib/plausible_web/templates/site/traffic_change_form.heex
Normal file
133
lib/plausible_web/templates/site/traffic_change_form.heex
Normal file
@ -0,0 +1,133 @@
|
||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
<%= @heading %>
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
<%= @subtitle %>
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="traffic-spikes" />
|
||||
</header>
|
||||
|
||||
<div class="my-8 flex items-center">
|
||||
<%= if @notification do %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-5 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %>
|
||||
<span class="translate-x-0 inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-800 shadow transform transition ease-in-out duration-200">
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<span class="ml-2 dark:text-gray-100"><%= @toggle_text %></span>
|
||||
</div>
|
||||
|
||||
<%= if @notification do %>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 mt-6">
|
||||
<%= form_for Plausible.Site.TrafficChangeNotification.changeset(@notification, %{}), "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}", fn f -> %>
|
||||
<h4 class="font-bold my-2"><%= @threshold_label %></h4>
|
||||
<div class="mt-1 flex rounded-md shadow-sm max-w-md">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<!-- Heroicon name: users -->
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= number_input(f, :threshold,
|
||||
min: 1,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:text-gray-100"
|
||||
) %>
|
||||
</div>
|
||||
<button class="-ml-px relative button rounded-l-none">
|
||||
<span>Save threshold</span>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<h4 class="font-bold mt-6 dark:text-gray-100">Notification recipients</h4>
|
||||
<%= for recipient <- @notification.recipients do %>
|
||||
<div class="p-2 pl-3 flex justify-between bg-gray-100 dark:bg-gray-900 rounded my-2 max-w-md">
|
||||
<span>
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400 inline mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<%= recipient %>
|
||||
</span>
|
||||
<%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/recipients/#{recipient}", method: :delete) do %>
|
||||
<svg
|
||||
class="w-4 h-4 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/traffic-change-notification/#{@type}/recipients", fn f -> %>
|
||||
<div class="max-w-md mt-4">
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<div class="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= email_input(f, :recipient,
|
||||
class:
|
||||
"focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100",
|
||||
placeholder: "recipient@example.com",
|
||||
required: "true"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<%= submit class: "-ml-px relative button rounded-l-none" do %>
|
||||
<svg
|
||||
class="w-5 h-5 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Add recipient</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
@ -1,63 +0,0 @@
|
||||
defmodule Plausible.Workers.SpikeNotifier do
|
||||
use Plausible.Repo
|
||||
alias Plausible.Stats.Query
|
||||
alias Plausible.Site.SpikeNotification
|
||||
use Oban.Worker, queue: :spike_notifications
|
||||
@at_most_every "12 hours"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(_job, clickhouse \\ Plausible.Stats.Clickhouse) do
|
||||
notifications =
|
||||
Repo.all(
|
||||
from sn in SpikeNotification,
|
||||
where: is_nil(sn.last_sent),
|
||||
or_where: sn.last_sent < fragment("now() - INTERVAL ?", @at_most_every),
|
||||
join: s in Plausible.Site,
|
||||
on: sn.site_id == s.id,
|
||||
where: not s.locked,
|
||||
preload: [site: s]
|
||||
)
|
||||
|
||||
for notification <- notifications do
|
||||
current_visitors = clickhouse.current_visitors(notification.site)
|
||||
|
||||
if current_visitors >= notification.threshold do
|
||||
query = Query.from(notification.site, %{"period" => "realtime"})
|
||||
sources = clickhouse.top_sources_for_spike(notification.site, query, 3, 1)
|
||||
notify(notification, current_visitors, sources)
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp notify(notification, current_visitors, sources) do
|
||||
for recipient <- notification.recipients do
|
||||
send_notification(recipient, notification.site, current_visitors, sources)
|
||||
end
|
||||
|
||||
notification
|
||||
|> SpikeNotification.was_sent()
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp send_notification(recipient, site, current_visitors, sources) do
|
||||
site = Repo.preload(site, :members)
|
||||
|
||||
dashboard_link =
|
||||
if Enum.any?(site.members, &(&1.email == recipient)) do
|
||||
PlausibleWeb.Endpoint.url() <> "/" <> URI.encode_www_form(site.domain)
|
||||
end
|
||||
|
||||
template =
|
||||
PlausibleWeb.Email.spike_notification(
|
||||
recipient,
|
||||
site,
|
||||
current_visitors,
|
||||
sources,
|
||||
dashboard_link
|
||||
)
|
||||
|
||||
Plausible.Mailer.send(template)
|
||||
end
|
||||
end
|
123
lib/workers/traffic_change_notifier.ex
Normal file
123
lib/workers/traffic_change_notifier.ex
Normal file
@ -0,0 +1,123 @@
|
||||
defmodule Plausible.Workers.TrafficChangeNotifier do
|
||||
@moduledoc """
|
||||
Oban service sending out traffic drop/spike notifications
|
||||
"""
|
||||
use Plausible.Repo
|
||||
alias Plausible.Stats.Query
|
||||
alias Plausible.Site.TrafficChangeNotification
|
||||
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
|
||||
use Oban.Worker, queue: :spike_notifications
|
||||
@at_most_every "12 hours"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(_job, clickhouse \\ Plausible.Stats.Clickhouse) do
|
||||
today = Date.utc_today()
|
||||
|
||||
notifications =
|
||||
Repo.all(
|
||||
from sn in TrafficChangeNotification,
|
||||
where: is_nil(sn.last_sent),
|
||||
or_where: sn.last_sent < fragment("now() - INTERVAL ?", @at_most_every),
|
||||
join: s in Plausible.Site,
|
||||
on: sn.site_id == s.id,
|
||||
where: not s.locked,
|
||||
join: sm in Plausible.Site.Membership,
|
||||
on: sm.site_id == s.id,
|
||||
where: sm.role == :owner,
|
||||
join: u in Plausible.Auth.User,
|
||||
on: u.id == sm.user_id,
|
||||
where: is_nil(u.accept_traffic_until) or u.accept_traffic_until > ^today,
|
||||
preload: [site: s]
|
||||
)
|
||||
|
||||
for notification <- notifications do
|
||||
case notification.type do
|
||||
:spike ->
|
||||
current_visitors = clickhouse.current_visitors(notification.site)
|
||||
|
||||
if current_visitors >= notification.threshold do
|
||||
query = Query.from(notification.site, %{"period" => "realtime"})
|
||||
sources = clickhouse.top_sources_for_spike(notification.site, query, 3, 1)
|
||||
|
||||
notify_spike(notification, current_visitors, sources)
|
||||
end
|
||||
|
||||
:drop ->
|
||||
current_visitors = clickhouse.current_visitors_12h(notification.site)
|
||||
|
||||
if current_visitors < notification.threshold do
|
||||
notify_drop(notification, current_visitors)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp notify_spike(notification, current_visitors, sources) do
|
||||
for recipient <- notification.recipients do
|
||||
send_spike_notification(recipient, notification.site, current_visitors, sources)
|
||||
end
|
||||
|
||||
notification
|
||||
|> TrafficChangeNotification.was_sent()
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp notify_drop(notification, current_visitors) do
|
||||
for recipient <- notification.recipients do
|
||||
send_drop_notification(recipient, notification.site, current_visitors)
|
||||
end
|
||||
|
||||
notification
|
||||
|> TrafficChangeNotification.was_sent()
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp send_spike_notification(recipient, site, current_visitors, sources) do
|
||||
site = Repo.preload(site, :members)
|
||||
|
||||
dashboard_link =
|
||||
if Enum.any?(site.members, &(&1.email == recipient)) do
|
||||
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, [])
|
||||
end
|
||||
|
||||
template =
|
||||
PlausibleWeb.Email.spike_notification(
|
||||
recipient,
|
||||
site,
|
||||
current_visitors,
|
||||
sources,
|
||||
dashboard_link
|
||||
)
|
||||
|
||||
Plausible.Mailer.send(template)
|
||||
end
|
||||
|
||||
defp send_drop_notification(recipient, site, current_visitors) do
|
||||
site = Repo.preload(site, :members)
|
||||
|
||||
{dashboard_link, verification_link} =
|
||||
if Enum.any?(site.members, &(&1.email == recipient)) do
|
||||
{
|
||||
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []),
|
||||
Routes.site_url(PlausibleWeb.Endpoint, :settings_general, site.domain)
|
||||
}
|
||||
else
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
template =
|
||||
PlausibleWeb.Email.drop_notification(
|
||||
recipient,
|
||||
site,
|
||||
current_visitors,
|
||||
dashboard_link,
|
||||
verification_link
|
||||
)
|
||||
|
||||
Plausible.Mailer.send(template)
|
||||
end
|
||||
end
|
@ -609,6 +609,7 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
site = insert(:site, members: [user])
|
||||
insert(:google_auth, user: user, site: site)
|
||||
insert(:spike_notification, site: site)
|
||||
insert(:drop_notification, site: site)
|
||||
|
||||
delete(conn, "/#{site.domain}")
|
||||
|
||||
@ -1227,106 +1228,146 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /sites/:website/spike-notification/enable" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
for type <- [:spike, :drop] do
|
||||
describe "POST /sites/:website/traffic-change-notification/#{type}/enable" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "creates a spike notification record with the user email", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
user: user
|
||||
} do
|
||||
post(conn, "/sites/#{site.domain}/spike-notification/enable")
|
||||
test "creates a #{type} notification record with the user email", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
user: user
|
||||
} do
|
||||
post(conn, "/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/enable")
|
||||
|
||||
notification = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
assert notification.recipients == [user.email]
|
||||
notification =
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification,
|
||||
site_id: site.id,
|
||||
type: unquote(type)
|
||||
)
|
||||
|
||||
assert notification.recipients == [user.email]
|
||||
end
|
||||
|
||||
test "does not allow duplicate #{type} notification to be created", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
post(conn, "/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/enable")
|
||||
post(conn, "/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/enable")
|
||||
|
||||
assert Repo.aggregate(
|
||||
from(s in Plausible.Site.TrafficChangeNotification,
|
||||
where: s.site_id == ^site.id and s.type == ^unquote(type)
|
||||
),
|
||||
:count
|
||||
) == 1
|
||||
end
|
||||
end
|
||||
|
||||
test "does not allow duplicate spike notification to be created", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
post(conn, "/sites/#{site.domain}/spike-notification/enable")
|
||||
post(conn, "/sites/#{site.domain}/spike-notification/enable")
|
||||
describe "POST /sites/:website/traffic-change-notification/#{type}/disable" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
assert Repo.aggregate(
|
||||
from(s in Plausible.Site.SpikeNotification, where: s.site_id == ^site.id),
|
||||
:count
|
||||
) == 1
|
||||
end
|
||||
end
|
||||
test "deletes the #{type} notification record", %{conn: conn, site: site} do
|
||||
insert(:"#{unquote(type)}_notification", site: site)
|
||||
|
||||
describe "POST /sites/:website/spike-notification/disable" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
post(conn, "/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/disable")
|
||||
|
||||
test "deletes the spike notification record", %{conn: conn, site: site} do
|
||||
insert(:spike_notification, site: site)
|
||||
|
||||
post(conn, "/sites/#{site.domain}/spike-notification/disable")
|
||||
|
||||
refute Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /sites/:website/spike-notification" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "updates spike notification threshold", %{conn: conn, site: site} do
|
||||
insert(:spike_notification, site: site, threshold: 10)
|
||||
|
||||
put(conn, "/sites/#{site.domain}/spike-notification", %{
|
||||
"spike_notification" => %{"threshold" => "15"}
|
||||
})
|
||||
|
||||
notification = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
assert notification.threshold == 15
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /sites/:website/spike-notification/recipients" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "adds a recipient to the spike notification", %{conn: conn, site: site} do
|
||||
insert(:spike_notification, site: site)
|
||||
|
||||
post(conn, "/sites/#{site.domain}/spike-notification/recipients",
|
||||
recipient: "user@email.com"
|
||||
)
|
||||
|
||||
report = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
assert report.recipients == ["user@email.com"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /sites/:website/spike-notification/recipients/:recipient" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "removes a recipient from the spike notification", %{conn: conn, site: site} do
|
||||
insert(:spike_notification, site: site, recipients: ["recipient@email.com"])
|
||||
|
||||
delete(conn, "/sites/#{site.domain}/spike-notification/recipients/recipient@email.com")
|
||||
|
||||
report = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
assert report.recipients == []
|
||||
refute Repo.get_by(Plausible.Site.TrafficChangeNotification, site_id: site.id)
|
||||
end
|
||||
end
|
||||
|
||||
test "fails to remove a recipient from the spike notification in a foreign website", %{
|
||||
conn: conn
|
||||
} do
|
||||
site = insert(:site)
|
||||
insert(:spike_notification, site: site, recipients: ["recipient@email.com"])
|
||||
describe "PUT /sites/:website/traffic-change-notification/#{type}" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
conn =
|
||||
delete(conn, "/sites/#{site.domain}/spike-notification/recipients/recipient@email.com")
|
||||
test "updates #{type} notification threshold", %{conn: conn, site: site} do
|
||||
insert(:"#{unquote(type)}_notification", site: site, threshold: 10)
|
||||
|
||||
assert conn.status == 404
|
||||
put(conn, "/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}", %{
|
||||
"traffic_change_notification" => %{"threshold" => "15"}
|
||||
})
|
||||
|
||||
conn =
|
||||
delete(conn, "/sites/#{site.domain}/spike-notification/recipients/recipient%40email.com")
|
||||
notification =
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification,
|
||||
site_id: site.id,
|
||||
type: unquote(type)
|
||||
)
|
||||
|
||||
assert conn.status == 404
|
||||
assert notification.threshold == 15
|
||||
end
|
||||
end
|
||||
|
||||
report = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id)
|
||||
assert [_] = report.recipients
|
||||
describe "POST /sites/:website/traffic-change-notification/#{type}/recipients" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "adds a recipient to the #{type} notification", %{conn: conn, site: site} do
|
||||
insert(:"#{unquote(type)}_notification", site: site)
|
||||
|
||||
post(
|
||||
conn,
|
||||
"/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/recipients",
|
||||
recipient: "user@email.com"
|
||||
)
|
||||
|
||||
report =
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification,
|
||||
site_id: site.id,
|
||||
type: unquote(type)
|
||||
)
|
||||
|
||||
assert report.recipients == ["user@email.com"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /sites/:website/traffic-change-notification/#{type}/recipients/:recipient" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "removes a recipient from the #{type} notification", %{conn: conn, site: site} do
|
||||
insert(:"#{unquote(type)}_notification", site: site, recipients: ["recipient@email.com"])
|
||||
|
||||
delete(
|
||||
conn,
|
||||
"/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/recipients/recipient@email.com"
|
||||
)
|
||||
|
||||
report =
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification,
|
||||
site_id: site.id,
|
||||
type: unquote(type)
|
||||
)
|
||||
|
||||
assert report.recipients == []
|
||||
end
|
||||
|
||||
test "fails to remove a recipient from the #{type} notification in a foreign website", %{
|
||||
conn: conn
|
||||
} do
|
||||
site = insert(:site)
|
||||
insert(:"#{unquote(type)}_notification", site: site, recipients: ["recipient@email.com"])
|
||||
|
||||
conn =
|
||||
delete(
|
||||
conn,
|
||||
"/sites/#{site.domain}/traffic-change-notification/#{unquote(type)}/recipients/recipient@email.com"
|
||||
)
|
||||
|
||||
assert conn.status == 404
|
||||
|
||||
conn =
|
||||
delete(
|
||||
conn,
|
||||
"/sites/#{site.domain}/traffic-change-notification/recipients/#{unquote(type)}/recipient%40email.com"
|
||||
)
|
||||
|
||||
assert conn.status == 404
|
||||
|
||||
report =
|
||||
Repo.get_by(Plausible.Site.TrafficChangeNotification,
|
||||
site_id: site.id,
|
||||
type: unquote(type)
|
||||
)
|
||||
|
||||
assert [_] = report.recipients
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -18,8 +18,16 @@ defmodule Plausible.Factory do
|
||||
end
|
||||
|
||||
def spike_notification_factory do
|
||||
%Plausible.Site.SpikeNotification{
|
||||
threshold: 10
|
||||
%Plausible.Site.TrafficChangeNotification{
|
||||
threshold: 10,
|
||||
type: :spike
|
||||
}
|
||||
end
|
||||
|
||||
def drop_notification_factory do
|
||||
%Plausible.Site.TrafficChangeNotification{
|
||||
threshold: 1,
|
||||
type: :drop
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -1,102 +0,0 @@
|
||||
defmodule Plausible.Workers.SpikeNotifierTest do
|
||||
use Plausible.DataCase, async: true
|
||||
use Bamboo.Test
|
||||
import Double
|
||||
alias Plausible.Workers.SpikeNotifier
|
||||
|
||||
test "does not notify anyone if current visitors does not exceed notification threshold" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:spike_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 5 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
SpikeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "notifies all recipients when traffic is higher than configured threshold" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:spike_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
SpikeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Spike on #{site.domain}",
|
||||
to: [nil: "jerod@example.com"]
|
||||
)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Spike on #{site.domain}",
|
||||
to: [nil: "uku@example.com"]
|
||||
)
|
||||
end
|
||||
|
||||
test "does not check site if it is locked" do
|
||||
site = insert(:site, locked: true)
|
||||
|
||||
insert(:spike_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
SpikeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "does not notify anyone if a notification already went out in the last 12 hours" do
|
||||
site = insert(:site)
|
||||
insert(:spike_notification, site: site, threshold: 10, recipients: ["uku@example.com"])
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
SpikeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Spike on #{site.domain}",
|
||||
to: [nil: "uku@example.com"]
|
||||
)
|
||||
|
||||
SpikeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "adds a dashboard link if recipient has access to the site" do
|
||||
user = insert(:user, email: "robert@example.com")
|
||||
site = insert(:site, domain: "example.com", members: [user])
|
||||
insert(:spike_notification, site: site, threshold: 10, recipients: ["robert@example.com"])
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
SpikeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(html_body: ~r/View dashboard: <a href=\"http.+\/example.com/)
|
||||
end
|
||||
end
|
243
test/workers/traffic_change_notifier_test.exs
Normal file
243
test/workers/traffic_change_notifier_test.exs
Normal file
@ -0,0 +1,243 @@
|
||||
defmodule Plausible.Workers.TrafficChangeNotifierTest do
|
||||
use Plausible.DataCase, async: true
|
||||
use Bamboo.Test
|
||||
import Double
|
||||
alias Plausible.Workers.TrafficChangeNotifier
|
||||
|
||||
describe "drops" do
|
||||
test "does not notify anyone if we've stopped accepting traffic for the owner" do
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership,
|
||||
user: build(:user, accept_traffic_until: Date.utc_today()),
|
||||
role: :owner
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
insert(:drop_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors_12h, fn _site -> 1 end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "does notify if threshold reached and we're accepting traffic" do
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership,
|
||||
user: build(:user, accept_traffic_until: Date.utc_today() |> Date.add(+1)),
|
||||
role: :owner
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
insert(:drop_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors_12h, fn _site -> 1 end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Drop on #{site.domain}",
|
||||
to: [nil: "jerod@example.com"]
|
||||
)
|
||||
end
|
||||
|
||||
test "does not notify anyone if current visitors does not drop below notification threshold" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:drop_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors_12h, fn _site -> 11 end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "notifies all recipients when traffic drops under configured threshold" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:drop_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors_12h, fn _site -> 7 end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Drop on #{site.domain}",
|
||||
to: [nil: "jerod@example.com"]
|
||||
)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Drop on #{site.domain}",
|
||||
to: [nil: "uku@example.com"]
|
||||
)
|
||||
end
|
||||
|
||||
test "does not notify anyone if a notification already went out in the last 12 hours" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:drop_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors_12h, fn _site -> 4 end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Drop on #{site.domain}",
|
||||
to: [nil: "uku@example.com"]
|
||||
)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "adds settings link if recipient has access to the site" do
|
||||
user = insert(:user, email: "robert@example.com")
|
||||
site = insert(:site, domain: "example.com", members: [user])
|
||||
|
||||
insert(:drop_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["robert@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors_12h, fn _site -> 6 end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(html_body: ~r|http://localhost:8000/example.com/settings|)
|
||||
end
|
||||
end
|
||||
|
||||
describe "spikes" do
|
||||
test "does not notify anyone if current visitors does not exceed notification threshold" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:spike_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 5 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "notifies all recipients when traffic is higher than configured threshold" do
|
||||
site = insert(:site)
|
||||
|
||||
insert(:spike_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["jerod@example.com", "uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Spike on #{site.domain}",
|
||||
to: [nil: "jerod@example.com"]
|
||||
)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Spike on #{site.domain}",
|
||||
to: [nil: "uku@example.com"]
|
||||
)
|
||||
end
|
||||
|
||||
test "does not check site if it is locked" do
|
||||
site = insert(:site, locked: true)
|
||||
|
||||
insert(:spike_notification,
|
||||
site: site,
|
||||
threshold: 10,
|
||||
recipients: ["uku@example.com"]
|
||||
)
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "does not notify anyone if a notification already went out in the last 12 hours" do
|
||||
site = insert(:site)
|
||||
insert(:spike_notification, site: site, threshold: 10, recipients: ["uku@example.com"])
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
subject: "Traffic Spike on #{site.domain}",
|
||||
to: [nil: "uku@example.com"]
|
||||
)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_no_emails_delivered()
|
||||
end
|
||||
|
||||
test "adds a dashboard link if recipient has access to the site" do
|
||||
user = insert(:user, email: "robert@example.com")
|
||||
site = insert(:site, domain: "example.com", members: [user])
|
||||
insert(:spike_notification, site: site, threshold: 10, recipients: ["robert@example.com"])
|
||||
|
||||
clickhouse_stub =
|
||||
stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site -> 10 end)
|
||||
|> stub(:top_sources_for_spike, fn _site, _query, _limit, _page -> [] end)
|
||||
|
||||
TrafficChangeNotifier.perform(nil, clickhouse_stub)
|
||||
|
||||
assert_email_delivered_with(html_body: ~r/View dashboard: <a href=\"http.+\/example.com/)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user