defmodule Plausible.Workers.SendEmailReportTest do use Plausible.DataCase use Bamboo.Test use Oban.Testing, repo: Plausible.Repo alias Plausible.Workers.SendEmailReport @green "#15803d" @red "#b91c1c" describe "weekly reports" do test "sends weekly report to all recipients" do site = new_site(domain: "test-site.com", timezone: "US/Eastern") insert(:weekly_report, site: site, recipients: ["user@email.com", "user2@email.com"]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_email_delivered_with( subject: "Weekly report for #{site.domain}", to: [nil: "user@email.com"] ) assert_email_delivered_with( subject: "Weekly report for #{site.domain}", to: [nil: "user2@email.com"] ) end test "does not crash for deleted sites" do assert :discard = perform_job(SendEmailReport, %{"site_id" => 28_378_237, "interval" => "weekly"}) end test "does not crash when weekly report has been deleted since scheduling job" do site = new_site(domain: "test-site.com", timezone: "US/Eastern") assert :discard = perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) end test "calculates timezone correctly" do site = new_site(timezone: "US/Eastern") insert(:weekly_report, site: site, recipients: ["user@email.com"]) now = DateTime.now!(site.timezone) last_monday = DateTime.shift(now, week: -1) |> Plausible.Times.beginning_of_week() last_sunday = DateTime.shift(now, week: -1) |> Plausible.Times.end_of_week() sunday_before_last = DateTime.shift(last_monday, minute: -1) this_monday = Plausible.Times.beginning_of_week(now) populate_stats(site, [ # Sunday before last, not counted build(:pageview, timestamp: DateTime.shift_zone!(sunday_before_last, "UTC")), # Sunday before last, not counted build(:pageview, timestamp: DateTime.shift_zone!(sunday_before_last, "UTC")), # Last monday, counted build(:pageview, timestamp: DateTime.shift_zone!(last_monday, "UTC")), # Last sunday, counted build(:pageview, timestamp: DateTime.shift_zone!(last_sunday, "UTC")), # This monday, not counted build(:pageview, timestamp: DateTime.shift_zone!(this_monday, "UTC")), # This monday, not counted build(:pageview, timestamp: DateTime.shift_zone!(this_monday, "UTC")) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) # Should find 2 visiors assert text_of_element(html_body, ".page-count") == "2" end test "includes the correct stats" do now = NaiveDateTime.utc_now(:second) site = new_site(domain: "test-site.com", inserted_at: NaiveDateTime.shift(now, day: -8)) insert(:weekly_report, site: site, recipients: ["user@email.com"]) populate_stats(site, [ build(:pageview, user_id: 123, timestamp: NaiveDateTime.shift(now, day: -7), referrer_source: "Google" ), build(:pageview, user_id: 123, timestamp: NaiveDateTime.shift(now, day: -7)), build(:pageview, timestamp: NaiveDateTime.shift(now, day: -7)) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) assert text_of_element(html_body, ".visitors") == "2" assert text_of_element(html_body, ".pageviews") == "3" assert text_of_element(html_body, ".referrer-name") == "Google" assert text_of_element(html_body, ".referrer-count") == "1" assert text_of_element(html_body, ".page-name") == "/" assert text_of_element(html_body, ".page-count") == "2" end on_ee do test "includes the correct stats for consolidated view" do now = NaiveDateTime.utc_now(:second) {:ok, team} = new_user() |> Plausible.Teams.get_or_create() site1 = new_site( domain: "testsite1.com", team: team, inserted_at: NaiveDateTime.shift(now, day: -8) ) site2 = new_site( domain: "testsite2.com", team: team, inserted_at: NaiveDateTime.shift(now, day: -8) ) consolidated_view = new_consolidated_view(team) insert(:weekly_report, site: consolidated_view, recipients: ["user@email.com"]) populate_stats(site1, [ build(:pageview, user_id: 123, timestamp: NaiveDateTime.shift(now, day: -7), referrer_source: "Google" ), build(:pageview, user_id: 123, timestamp: NaiveDateTime.shift(now, day: -7)) ]) populate_stats(site2, [ build(:pageview, timestamp: NaiveDateTime.shift(now, day: -7)) ]) perform_job(SendEmailReport, %{"site_id" => consolidated_view.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) assert html_body =~ "Consolidated view" assert text_of_element(html_body, ".visitors") == "2" assert text_of_element(html_body, ".pageviews") == "3" assert text_of_element(html_body, ".referrer-name") == "Google" assert text_of_element(html_body, ".referrer-count") == "1" assert text_of_element(html_body, ".page-name") == "/" assert text_of_element(html_body, ".page-count") == "2" end test "does not send weekly report for consolidated view with ok_to_display? false" do {:ok, team} = new_user() |> Plausible.Teams.get_or_create() site = new_site(team: team) new_site(team: team) consolidated_view = new_consolidated_view(team) insert(:weekly_report, site: consolidated_view, recipients: ["user@email.com"]) Plausible.Repo.delete!(site) refute Plausible.ConsolidatedView.ok_to_display?(team) perform_job(SendEmailReport, %{"site_id" => consolidated_view.id, "interval" => "weekly"}) assert_no_emails_delivered() end end test "renders correct signs (+/-) and trend colors for positive percentage changes" do now = NaiveDateTime.utc_now(:second) week_ago = now |> NaiveDateTime.shift(day: -7) two_weeks_ago = now |> NaiveDateTime.shift(day: -14) site = new_site(inserted_at: NaiveDateTime.shift(now, day: -15)) insert(:weekly_report, site: site, recipients: ["user@email.com"]) populate_stats(site, [ build(:pageview, timestamp: two_weeks_ago), build(:pageview, user_id: 1, timestamp: week_ago), build(:pageview, user_id: 2, timestamp: week_ago), build(:pageview, user_id: 2, timestamp: week_ago) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) visitors_change_container = find(html_body, ".change-visitors div") assert text(visitors_change_container) == "+100%" assert text_of_attr(visitors_change_container, "style") =~ @green pageviews_change_container = find(html_body, ".change-pageviews div") assert text(pageviews_change_container) == "+200%" assert text_of_attr(pageviews_change_container, "style") =~ @green bounce_rate_change_container = find(html_body, ".change-bounce-rate div") assert text(bounce_rate_change_container) == "-50%" assert text_of_attr(bounce_rate_change_container, "style") =~ @green end test "renders correct signs (+/-) and trend colors for negative percentage changes" do now = NaiveDateTime.utc_now(:second) week_ago = now |> NaiveDateTime.shift(day: -7) two_weeks_ago = now |> NaiveDateTime.shift(day: -14) site = new_site(inserted_at: NaiveDateTime.shift(now, day: -15)) insert(:weekly_report, site: site, recipients: ["user@email.com"]) populate_stats(site, [ build(:pageview, user_id: 1, timestamp: two_weeks_ago), build(:pageview, user_id: 2, timestamp: two_weeks_ago), build(:pageview, user_id: 2, timestamp: two_weeks_ago), build(:pageview, timestamp: week_ago) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) visitors_change_container = find(html_body, ".change-visitors div") assert text(visitors_change_container) == "-50%" assert text_of_attr(visitors_change_container, "style") =~ @red pageviews_change_container = find(html_body, ".change-pageviews div") assert text(pageviews_change_container) == "-67%" assert text_of_attr(pageviews_change_container, "style") =~ @red bounce_rate_change_container = find(html_body, ".change-bounce-rate div") assert text(bounce_rate_change_container) == "+50%" assert text_of_attr(bounce_rate_change_container, "style") =~ @red end test "renders 0% changes with a green color and without a sign" do now = NaiveDateTime.utc_now(:second) week_ago = now |> NaiveDateTime.shift(day: -7) two_weeks_ago = now |> NaiveDateTime.shift(day: -14) site = new_site(inserted_at: NaiveDateTime.shift(now, day: -15)) insert(:weekly_report, site: site, recipients: ["user@email.com"]) populate_stats(site, [ build(:pageview, timestamp: two_weeks_ago), build(:pageview, timestamp: week_ago) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) visitors_change_container = find(html_body, ".change-visitors div") assert text(visitors_change_container) == "0%" assert text_of_attr(visitors_change_container, "style") =~ @green pageviews_change_container = find(html_body, ".change-pageviews div") assert text(pageviews_change_container) == "0%" assert text_of_attr(pageviews_change_container, "style") =~ @green bounce_rate_change_container = find(html_body, ".change-bounce-rate div") assert text(bounce_rate_change_container) == "0%" assert text_of_attr(bounce_rate_change_container, "style") =~ @green end test "includes goal conversions when goals exist" do last_monday = NaiveDateTime.utc_now(:second) |> NaiveDateTime.shift(day: -7) |> Plausible.Times.beginning_of_week() site = new_site(domain: "test-site.com", inserted_at: NaiveDateTime.shift(last_monday, day: -1)) insert(:weekly_report, site: site, recipients: ["user@email.com"]) _goal1 = insert(:goal, site: site, event_name: "Signup") _goal2 = insert(:goal, site: site, event_name: "Purchase") _goal3 = insert(:goal, site: site, page_path: "/thank-you") populate_stats(site, [ build(:pageview, user_id: 123, timestamp: last_monday ), build(:pageview, user_id: 124, timestamp: NaiveDateTime.shift(last_monday, day: 1) ), build(:event, user_id: 123, name: "Signup", timestamp: last_monday ), build(:event, user_id: 124, name: "Signup", timestamp: NaiveDateTime.shift(last_monday, day: 1) ), build(:event, user_id: 125, name: "Purchase", timestamp: NaiveDateTime.shift(last_monday, day: 2) ), build(:pageview, user_id: 126, pathname: "/thank-you", timestamp: NaiveDateTime.shift(last_monday, day: 3) ) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) goal_names = find(html_body, ".goal-name") |> Enum.map(&text/1) goal_conversions = find(html_body, ".goal-conversions") |> Enum.map(&text/1) assert goal_names == ["Signup", "Purchase", "Visit /thank-you"] assert goal_conversions == ["2", "1", "1"] end end describe "monthly_reports" do test "sends monthly report to all recipients" do site = new_site(domain: "test-site.com", timezone: "US/Eastern") insert(:monthly_report, site: site, recipients: ["user@email.com", "user2@email.com"]) last_month = DateTime.now!(site.timezone) |> DateTime.shift(month: -1) |> Plausible.Times.beginning_of_month() |> Calendar.strftime("%B") perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "monthly"}) assert_email_delivered_with( subject: "#{last_month} report for #{site.domain}", to: [nil: "user@email.com"] ) assert_email_delivered_with( subject: "#{last_month} report for #{site.domain}", to: [nil: "user2@email.com"] ) end test "does not crash when monthly report has been deleted since scheduling job" do site = new_site(domain: "test-site.com", timezone: "US/Eastern") assert :discard = perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "monthly"}) end test "calculates timezone correctly" do site = new_site(timezone: "US/Eastern") insert(:monthly_report, site: site, recipients: ["user@email.com"]) now = DateTime.now!(site.timezone) last_month_first = DateTime.shift(now, month: -1) |> Plausible.Times.beginning_of_month() last_month_last = DateTime.shift(now, month: -1) |> Plausible.Times.end_of_month() month_before_last = DateTime.shift(last_month_first, minute: -1) this_month_first = Plausible.Times.beginning_of_month(now) populate_stats(site, [ # Month before last, not counted build(:pageview, timestamp: DateTime.shift_zone!(month_before_last, "UTC") ), # Month before last, not counted build(:pageview, timestamp: DateTime.shift_zone!(month_before_last, "UTC") ), # Last month first day, counted build(:pageview, user_id: 123, timestamp: DateTime.shift_zone!(last_month_first, "UTC") ), # Last month last day, counted build(:pageview, user_id: 124, timestamp: DateTime.shift_zone!(last_month_last, "UTC") ), # This month first day, not counted build(:pageview, timestamp: DateTime.shift_zone!(this_month_first, "UTC") ), # This month first day, not counted build(:pageview, timestamp: DateTime.shift_zone!(this_month_first, "UTC") ) ]) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "monthly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) # Should find 2 visitors assert text_of_element(html_body, ".page-count") == "2" end test "limits all stats sections to 5 entries" do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) site = new_site(domain: "test-site.com", inserted_at: Timex.shift(now, months: -2)) insert(:monthly_report, site: site, recipients: ["user@email.com"]) for i <- 1..6, do: insert(:goal, site: site, event_name: "Goal#{i}") last_month_stats = for i <- 1..6 do [ build(:pageview, user_id: 100 + i, pathname: "/page#{i}", referrer_source: if(i == 6, do: "Direct / None", else: "Source#{i}"), timestamp: Date.shift(now, month: -1) ), build(:event, user_id: 100 + i, name: "Goal#{i}", timestamp: Date.shift(now, month: -1) ) ] end |> List.flatten() populate_stats(site, last_month_stats) perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "monthly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) assert elem_count(html_body, ".page-name") == 5 assert elem_count(html_body, ".referrer-name") == 5 assert elem_count(html_body, ".goal-name") == 5 end test "email subject includes month name" do site = new_site(domain: "test-site.com", timezone: "US/Eastern") insert(:monthly_report, site: site, recipients: ["user@email.com"]) last_month = site.timezone |> DateTime.now!() |> Date.shift(month: -1) |> Date.beginning_of_month() |> Calendar.strftime("%B") perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "monthly"}) assert_email_delivered_with( subject: "#{last_month} report for #{site.domain}", to: [nil: "user@email.com"] ) end on_ee do test "includes goal conversions when goals exist on a consolidate view" do beginning_of_last_month = Date.utc_today() |> Date.shift(month: -1) |> Date.beginning_of_month() |> NaiveDateTime.new!(~T[00:00:00]) {:ok, team} = new_user() |> Plausible.Teams.get_or_create() [site1, site2] = for _ <- 1..2 do new_site( team: team, inserted_at: NaiveDateTime.shift(beginning_of_last_month, day: -1) ) end consolidated_view = new_consolidated_view(team) insert(:monthly_report, site: consolidated_view, recipients: ["user@email.com"]) _goal1 = insert(:goal, site: consolidated_view, event_name: "Signup") _goal2 = insert(:goal, site: consolidated_view, event_name: "Purchase") _goal3 = insert(:goal, site: consolidated_view, page_path: "/thank-you") populate_stats(site1, [ build(:pageview, user_id: 123, timestamp: beginning_of_last_month ), build(:event, user_id: 123, name: "Signup", timestamp: beginning_of_last_month ), build(:event, user_id: 125, name: "Purchase", timestamp: NaiveDateTime.shift(beginning_of_last_month, day: 2) ) ]) populate_stats(site2, [ build(:pageview, user_id: 124, timestamp: NaiveDateTime.shift(beginning_of_last_month, day: 1) ), build(:event, user_id: 124, name: "Signup", timestamp: NaiveDateTime.shift(beginning_of_last_month, day: 1) ), build(:pageview, user_id: 126, pathname: "/thank-you", timestamp: NaiveDateTime.shift(beginning_of_last_month, day: 3) ) ]) perform_job(SendEmailReport, %{"site_id" => consolidated_view.id, "interval" => "monthly"}) assert_delivered_email_matches(%{ to: [nil: "user@email.com"], html_body: html_body }) assert html_body =~ "Consolidated view" goal_names = find(html_body, ".goal-name") |> Enum.map(&text/1) goal_conversions = find(html_body, ".goal-conversions") |> Enum.map(&text/1) assert goal_names == ["Signup", "Purchase", "Visit /thank-you"] assert goal_conversions == ["2", "1", "1"] end test "does not send monthly report for consolidated view with ok_to_display? false" do {:ok, team} = new_user() |> Plausible.Teams.get_or_create() site = new_site(team: team) new_site(team: team) consolidated_view = new_consolidated_view(team) insert(:monthly_report, site: consolidated_view, recipients: ["user@email.com"]) Plausible.Repo.delete!(site) refute Plausible.ConsolidatedView.ok_to_display?(site.team) perform_job(SendEmailReport, %{"site_id" => consolidated_view.id, "interval" => "monthly"}) assert_no_emails_delivered() end end end end