Verification / Detection follow-ups (#5836)

* make timeout configurable for a check via check_opts

* add an internal_check_timeout test case to verification/checks_test.exs

* move verification observability tests to a separate file

... make it sync and test capturing Sentry events too

* separate detection observability checks too

* test sentry events in detection

* consider internal check timeouts browserless issues in detection too

* test util function defs to ee_only
This commit is contained in:
RobertJoonas 2025-10-27 16:50:54 +00:00 committed by GitHub
parent fc34357865
commit 46f05d81c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 498 additions and 329 deletions

View File

@ -13,7 +13,6 @@ defmodule Plausible.InstallationSupport.Check do
"""
@type state() :: Plausible.InstallationSupport.State.t()
@callback report_progress_as() :: String.t()
@callback timeout_ms() :: integer()
@callback perform(state()) :: state()
defmacro __using__(_) do
@ -25,11 +24,9 @@ defmodule Plausible.InstallationSupport.Check do
@behaviour Plausible.InstallationSupport.Check
def timeout_ms, do: 10_000
def perform_safe(state, opts) do
timeout = Keyword.get(opts, :timeout, 10_000)
defoverridable timeout_ms: 0
def perform_safe(state) do
task =
Task.async(fn ->
try do
@ -45,7 +42,7 @@ defmodule Plausible.InstallationSupport.Check do
end)
try do
Task.await(task, timeout_ms())
Task.await(task, timeout)
catch
:exit, {:timeout, _} ->
Task.shutdown(task, :brutal_kill)
@ -54,7 +51,7 @@ defmodule Plausible.InstallationSupport.Check do
put_diagnostics(state,
service_error: %{
code: :internal_check_timeout,
extra: "#{check_name} timed out after #{timeout_ms()}ms"
extra: "#{check_name} timed out after #{timeout}ms"
}
)
end

View File

@ -30,14 +30,14 @@ defmodule Plausible.InstallationSupport.CheckRunner do
Enum.reduce_while(
checks,
state,
fn check, state ->
fn {check, check_opts}, state ->
if state.skip_further_checks? do
{:halt, state}
else
{:cont,
state
|> notify_check_start(check, slowdown)
|> check.perform_safe()}
|> check.perform_safe(check_opts)}
end
end
)

View File

@ -63,9 +63,6 @@ defmodule Plausible.InstallationSupport.Checks.Detection do
# To support browserless API being unavailable or overloaded, we retry the endpoint call if it doesn't return a successful response
@max_retries 1
@impl true
def timeout_ms, do: 6_000
@impl true
def report_progress_as, do: "We're checking your site to recommend the best installation method"

View File

@ -85,9 +85,6 @@ defmodule Plausible.InstallationSupport.Checks.VerifyInstallation do
@max_attempts 2
@timeout_between_attempts_ms 500
@impl true
def timeout_ms, do: 20_000
@impl true
def report_progress_as, do: "We're verifying that your visitors are being counted correctly"

View File

@ -15,9 +15,6 @@ defmodule Plausible.InstallationSupport.Checks.VerifyInstallationCacheBust do
alias Plausible.InstallationSupport
use Plausible.InstallationSupport.Check
@impl true
def timeout_ms, do: 20_000
@impl true
def report_progress_as, do: "We're verifying that your visitors are being counted correctly"

View File

@ -10,12 +10,15 @@ defmodule Plausible.InstallationSupport.Detection.Checks do
require Logger
@checks [
Checks.Url,
Checks.Detection
]
@detection_check_timeout 6000
def run(url, data_domain, opts \\ []) do
detection_check_timeout =
case Keyword.get(opts, :detection_check_timeout) do
int when is_integer(int) -> int
_ -> @detection_check_timeout
end
report_to = Keyword.get(opts, :report_to, self())
async? = Keyword.get(opts, :async?, true)
slowdown = Keyword.get(opts, :slowdown, 500)
@ -30,7 +33,12 @@ defmodule Plausible.InstallationSupport.Detection.Checks do
assigns: %{detect_v1?: detect_v1?}
}
CheckRunner.run(init_state, @checks,
checks = [
{Checks.Url, []},
{Checks.Detection, [timeout: detection_check_timeout]}
]
CheckRunner.run(init_state, checks,
async?: async?,
report_to: report_to,
slowdown: slowdown

View File

@ -65,7 +65,7 @@ defmodule Plausible.InstallationSupport.Detection.Diagnostics do
end
def interpret(%__MODULE__{service_error: %{code: code}}, _url)
when code in [:bad_browserless_response, :browserless_timeout] do
when code in [:bad_browserless_response, :browserless_timeout, :internal_check_timeout] do
failure(:browserless_issue)
end

View File

@ -9,14 +9,17 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
require Logger
@checks [
Checks.Url,
Checks.VerifyInstallation,
Checks.VerifyInstallationCacheBust
]
@verify_installation_check_timeout 20_000
@spec run(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, pid()} | State.t()
def run(url, data_domain, installation_type, opts \\ []) do
# Timeout option for testing purposes
verify_installation_check_timeout =
case Keyword.get(opts, :verify_installation_check_timeout) do
int when is_integer(int) -> int
_ -> @verify_installation_check_timeout
end
report_to = Keyword.get(opts, :report_to, self())
async? = Keyword.get(opts, :async?, true)
slowdown = Keyword.get(opts, :slowdown, 500)
@ -31,7 +34,13 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
}
}
CheckRunner.run(init_state, @checks,
checks = [
{Checks.Url, []},
{Checks.VerifyInstallation, [timeout: verify_installation_check_timeout]},
{Checks.VerifyInstallationCacheBust, [timeout: verify_installation_check_timeout]}
]
CheckRunner.run(init_state, checks,
async?: async?,
report_to: report_to,
slowdown: slowdown

View File

@ -237,7 +237,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do
})
def interpret(%__MODULE__{service_error: %{code: code}}, _expected_domain, _url)
when code in [:bad_browserless_response, :browserless_timeout] do
when code in [:bad_browserless_response, :browserless_timeout, :internal_check_timeout] do
unhandled_error(@error_browserless_temporary, browserless_issue: true)
end

View File

@ -30,7 +30,7 @@ defmodule Plausible.InstallationSupport.CheckTest do
{result, log} =
with_log(fn ->
FaultyCheckRaise.perform_safe(state)
FaultyCheckRaise.perform_safe(state, [])
end)
assert log =~
@ -64,7 +64,7 @@ defmodule Plausible.InstallationSupport.CheckTest do
{result, log} =
with_log(fn ->
FaultyCheckThrow.perform_safe(state)
FaultyCheckThrow.perform_safe(state, [])
end)
assert log =~
@ -79,9 +79,6 @@ defmodule Plausible.InstallationSupport.CheckTest do
defmodule FaultyCheckTimeout do
use Plausible.InstallationSupport.Check
@impl true
def timeout_ms, do: 100
@impl true
def report_progress_as, do: "Faulty check"
@ -95,8 +92,7 @@ defmodule Plausible.InstallationSupport.CheckTest do
diagnostics: %Verification.Diagnostics{}
}
result =
FaultyCheckTimeout.perform_safe(state)
result = FaultyCheckTimeout.perform_safe(state, timeout: 100)
assert_matches %Verification.Diagnostics{
service_error: %{

View File

@ -0,0 +1,263 @@
defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
@moduledoc """
Tests for capturing logs/telemetry/Sentry upon detection diagnostics interpretation.
Needs to be synchronous due to Sentry assertions, hence a separate module.
"""
use PlausibleWeb.ConnCase, async: false
@moduletag :ee_only
on_ee do
use Plausible.Test.Support.DNS
import ExUnit.CaptureLog
alias Plausible.InstallationSupport.Detection.Checks
@moduletag :capture_log
@expected_domain "example.com"
@working_url "https://#{@expected_domain}"
setup %{test: test, test_pid: test_pid} do
:telemetry.attach_many(
"#{test}-telemetry-handler",
[
Checks.telemetry_event_success(),
Checks.telemetry_event_failure()
],
fn event, %{}, _, _ ->
send(test_pid, {:telemetry_event, event})
end,
%{}
)
Sentry.put_config(:test_mode, true)
Sentry.put_config(:send_result, :sync)
Sentry.put_config(:dedup_events, false)
assert :ok = Sentry.Test.start_collecting(owner: test_pid)
on_exit(fn ->
Sentry.put_config(:test_mode, false)
Sentry.put_config(:send_result, :none)
Sentry.put_config(:dedup_events, true)
end)
end
test "successful detection -> no logs, no sentry, telemetry :success" do
stub_lookup_a_records(@expected_domain)
detection_stub =
json_response_detection_stub(%{
"completed" => true,
"v1Detected" => nil,
"gtmLikely" => false,
"npm" => false,
"wordpressLikely" => true,
"wordpressPlugin" => false
})
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log == ""
assert [] = Sentry.Test.pop_sentry_reports()
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_success()
end
test "domain not found -> customer website issue" do
stub_lookup_a_records(@expected_domain, [])
detection_counter =
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn _conn ->
raise "This check should've been skipped"
end)
state =
Checks.run(@working_url, @expected_domain,
report_to: nil,
async?: false,
slowdown: 0
)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert detection_counter
assert log =~ "[DETECTION] Failed due to an issue with the customer website"
assert log =~ "service_error: %{code: :domain_not_found}"
assert [] = Sentry.Test.pop_sentry_reports()
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
for msg <- ["Execution context destroyed", "net::ERR_CONNECTION_REFUSED"] do
test "failure due to a known :browserless_client_error (#{msg}) -> customer website issue" do
stub_lookup_a_records(@expected_domain)
detection_stub =
json_response_detection_stub(%{
"completed" => false,
"error" => %{"message" => unquote(msg)}
})
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to an issue with the customer website"
assert log =~ "code: :browserless_client_error"
assert log =~ ~s|extra: "#{unquote(msg)}"|
assert [] = Sentry.Test.pop_sentry_reports()
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
end
test "failure due to an unknown :browserless_client_error -> unknown failure" do
stub_lookup_a_records(@expected_domain)
detection_stub =
json_response_detection_stub(%{
"completed" => false,
"error" => %{"message" => "something unexpected"}
})
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Unknown failure"
assert log =~ "code: :browserless_client_error"
assert log =~ ~s|extra: "something unexpected"|
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "[DETECTION] Unknown failure"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure hitting the catch-all interpret clause -> unknown failure" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn conn ->
Req.Test.transport_error(conn, :econnrefused)
end
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Unknown failure"
assert log =~ "code: :req_error"
assert log =~ ~s|extra: :econnrefused|
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "[DETECTION] Unknown failure"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure due to a flaky browserless issue -> browserless issue" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn conn ->
conn
|> put_resp_content_type("text/html")
|> send_resp(400, "some error message")
end
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to a Browserless issue"
assert log =~ "code: :bad_browserless_response"
assert log =~ "extra: 400"
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "[DETECTION] Failed due to a Browserless issue"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure due to a browserless timeout -> browserless issue" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn conn ->
Req.Test.transport_error(conn, :timeout)
end
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to a Browserless issue"
assert log =~ "code: :browserless_timeout"
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "[DETECTION] Failed due to a Browserless issue"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure due to internal_check_timeout -> browserless issue" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn _conn ->
# times out
Process.sleep(1000)
end
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, detection_stub)
state =
Checks.run(@working_url, @expected_domain,
detection_check_timeout: 100,
report_to: nil,
async?: false,
slowdown: 0
)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to a Browserless issue"
assert log =~ "code: :internal_check_timeout"
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "[DETECTION] Failed due to a Browserless issue"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
defp json_response_detection_stub(js_data) do
fn conn ->
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(%{"data" => js_data}))
end
end
defp run_checks(detection_stub) do
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, detection_stub)
Checks.run(@working_url, @expected_domain,
report_to: nil,
async?: false,
slowdown: 0
)
end
end
end

View File

@ -1,20 +1,17 @@
defmodule Plausible.InstallationSupport.Detection.ChecksTest do
use Plausible
use Plausible.DataCase, async: true
use PlausibleWeb.ConnCase, async: true
@moduletag :ee_only
on_ee do
import Plausible.AssertMatches
alias Plausible.InstallationSupport.Detection.{Checks}
alias Plausible.InstallationSupport.Result
use Plausible.Test.Support.DNS
import Plug.Conn
import ExUnit.CaptureLog
import Plausible.AssertMatches
alias Plausible.InstallationSupport.Detection.Checks
alias Plausible.InstallationSupport.Result
@moduletag :capture_log
@expected_domain "example.com"
@working_url "https://#{@expected_domain}"
describe "running detection" do
test "handles wordpress detection, retrying on 429" do
@ -89,191 +86,6 @@ defmodule Plausible.InstallationSupport.Detection.ChecksTest do
end
end
describe "observability" do
setup %{test: test, test_pid: test_pid} do
:telemetry.attach_many(
"#{test}-telemetry-handler",
[
Checks.telemetry_event_success(),
Checks.telemetry_event_failure()
],
fn event, %{}, _, _ ->
send(test_pid, {:telemetry_event, event})
end,
%{}
)
end
test "successful detection -> no logs, telemetry :success" do
stub_lookup_a_records(@expected_domain)
detection_stub =
json_response_detection_stub(%{
"completed" => true,
"v1Detected" => nil,
"gtmLikely" => false,
"npm" => false,
"wordpressLikely" => true,
"wordpressPlugin" => false
})
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log == ""
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_success()
end
test "domain not found -> customer website issue" do
stub_lookup_a_records(@expected_domain, [])
detection_counter =
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn _conn ->
raise "This check should've been skipped"
end)
state =
Checks.run(@working_url, @expected_domain,
report_to: nil,
async?: false,
slowdown: 0
)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert detection_counter
assert log =~ "[DETECTION] Failed due to an issue with the customer website"
assert log =~ "service_error: %{code: :domain_not_found}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
for msg <- ["Execution context destroyed", "net::ERR_CONNECTION_REFUSED"] do
test "failure due to a known :browserless_client_error (#{msg}) -> customer website issue" do
stub_lookup_a_records(@expected_domain)
detection_stub =
json_response_detection_stub(%{
"completed" => false,
"error" => %{"message" => unquote(msg)}
})
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to an issue with the customer website"
assert log =~ "code: :browserless_client_error"
assert log =~ ~s|extra: "#{unquote(msg)}"|
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
end
test "failure due to an unknown :browserless_client_error -> unknown failure" do
stub_lookup_a_records(@expected_domain)
detection_stub =
json_response_detection_stub(%{
"completed" => false,
"error" => %{"message" => "something unexpected"}
})
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Unknown failure"
assert log =~ "code: :browserless_client_error"
assert log =~ ~s|extra: "something unexpected"|
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure hitting the catch-all interpret clause -> unknown failure" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn conn ->
Req.Test.transport_error(conn, :econnrefused)
end
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Unknown failure"
assert log =~ "code: :req_error"
assert log =~ ~s|extra: :econnrefused|
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure due to a flaky browserless issue -> browserless issue" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn conn ->
conn
|> put_resp_content_type("text/html")
|> send_resp(400, "some error message")
end
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to a Browserless issue"
assert log =~ "code: :bad_browserless_response"
assert log =~ "extra: 400"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
test "failure due to a browserless timeout -> browserless issue" do
stub_lookup_a_records(@expected_domain)
detection_stub = fn conn ->
Req.Test.transport_error(conn, :timeout)
end
state = run_checks(detection_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[DETECTION] Failed due to a Browserless issue"
assert log =~ "code: :browserless_timeout"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_failure()
end
end
defp json_response_detection_stub(js_data) do
fn conn ->
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(%{"data" => js_data}))
end
end
defp run_checks(detection_stub) do
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, detection_stub)
Checks.run(@working_url, @expected_domain,
report_to: nil,
async?: false,
slowdown: 0
)
end
defp stub_detection_result_with_counter(handler) do
counter = :atomics.new(1, [])

View File

@ -0,0 +1,188 @@
defmodule Plausible.InstallationSupport.Verification.ChecksObservabilityTest do
@moduledoc """
Tests for capturing logs/telemetry/Sentry upon verification diagnostics interpretation.
Needs to be synchronous due to Sentry assertions, hence a separate module.
"""
use PlausibleWeb.ConnCase, async: false
@moduletag :ee_only
on_ee do
use Plausible.Test.Support.DNS
import ExUnit.CaptureLog
alias Plausible.InstallationSupport.Verification.{Checks}
@moduletag :capture_log
@expected_domain "example.com"
@url_to_verify "https://#{@expected_domain}"
setup %{test: test, test_pid: test_pid} do
:telemetry.attach_many(
"#{test}-telemetry-handler",
[
Checks.telemetry_event_handled(),
Checks.telemetry_event_unhandled()
],
fn event, %{}, _, _ ->
send(test_pid, {:telemetry_event, event})
end,
%{}
)
Sentry.put_config(:test_mode, true)
Sentry.put_config(:send_result, :sync)
Sentry.put_config(:dedup_events, false)
assert :ok = Sentry.Test.start_collecting(owner: test_pid)
on_exit(fn ->
Sentry.put_config(:test_mode, false)
Sentry.put_config(:send_result, :none)
Sentry.put_config(:dedup_events, true)
end)
end
test "known installation issue detected is considered handled" do
wrong_domain_verification_stub =
json_response_verification_stub(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{
"normalizedBody" => %{
"domain" => "wrong-domain.com"
},
"responseStatus" => 200
}
})
state = run_checks(wrong_domain_verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log == ""
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_handled()
assert [] = Sentry.Test.pop_sentry_reports()
end
test "unhandled verification case" do
verification_stub =
json_response_verification_stub(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{}
})
state = run_checks(verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "test_event: %{}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "Unhandled case for site verification"
end
test "browserless request timing out is considered unhandled" do
verification_stub = fn conn -> Req.Test.transport_error(conn, :timeout) end
state = run_checks(verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "service_error: %{code: :browserless_timeout}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "Browserless failure in verification"
end
test "flaky Browserless response is considered unhandled" do
verification_stub = fn conn ->
conn
|> put_resp_content_type("text/html")
|> send_resp(400, "some error message")
end
state = run_checks(verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "service_error: %{code: :bad_browserless_response, extra: 400}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "Browserless failure in verification"
end
test "internal_check_timeout is considered unhandled" do
verification_stub = fn _conn ->
# times out
Process.sleep(1000)
end
stub_lookup_a_records(@expected_domain)
stub_verification_result(verification_stub)
state =
Checks.run(@url_to_verify, @expected_domain, "manual",
verify_installation_check_timeout: 100,
report_to: nil,
async?: false,
slowdown: 0
)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "code: :internal_check_timeout"
assert log =~ ~s|extra: "VerifyInstallation timed out after 100ms"|
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
assert [sentry_event] = Sentry.Test.pop_sentry_reports()
assert sentry_event.message.formatted == "Browserless failure in verification"
end
defp json_response_verification_stub(js_data) do
fn conn ->
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(%{"data" => js_data}))
end
end
defp run_checks(verification_stub) do
stub_lookup_a_records(@expected_domain)
stub_verification_result(verification_stub)
Checks.run(@url_to_verify, @expected_domain, "manual",
report_to: nil,
async?: false,
slowdown: 0
)
end
defp stub_verification_result(f) do
Req.Test.stub(Plausible.InstallationSupport.Checks.VerifyInstallation, f)
end
end
end

View File

@ -1,16 +1,14 @@
defmodule Plausible.InstallationSupport.Verification.ChecksTest do
use Plausible
use Plausible.DataCase, async: true
use PlausibleWeb.ConnCase, async: true
@moduletag :ee_only
on_ee do
use Plausible.Test.Support.DNS
import Plausible.AssertMatches
alias Plausible.InstallationSupport.Verification.{Checks, Diagnostics}
alias Plausible.InstallationSupport.Result
use Plausible.Test.Support.DNS
import Plug.Conn
import ExUnit.CaptureLog
@moduletag :capture_log
@expected_domain "example.com"
@ -529,99 +527,6 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
end
end
describe "observability" do
setup %{test: test, test_pid: test_pid} do
:telemetry.attach_many(
"#{test}-telemetry-handler",
[
Checks.telemetry_event_handled(),
Checks.telemetry_event_unhandled()
],
fn event, %{}, _, _ ->
send(test_pid, {:telemetry_event, event})
end,
%{}
)
end
test "known installation issue detected is considered handled" do
wrong_domain_verification_stub =
json_response_verification_stub(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{
"normalizedBody" => %{
"domain" => "wrong-domain.com"
},
"responseStatus" => 200
}
})
state = run_checks(wrong_domain_verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log == ""
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_handled()
end
test "unhandled verification case" do
verification_stub =
json_response_verification_stub(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{}
})
state = run_checks(verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "test_event: %{}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
end
test "browserless request timing out is considered unhandled" do
verification_stub = fn conn -> Req.Test.transport_error(conn, :timeout) end
state = run_checks(verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "service_error: %{code: :browserless_timeout}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
end
test "flaky Browserless response is considered unhandled" do
verification_stub = fn conn ->
conn
|> put_resp_content_type("text/html")
|> send_resp(400, "some error message")
end
state = run_checks(verification_stub)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
assert log =~ "service_error: %{code: :bad_browserless_response, extra: 400}"
assert_receive {:telemetry_event, telemetry_event}
assert telemetry_event == Checks.telemetry_event_unhandled()
end
end
defp json_response_verification_stub(js_data) do
fn conn ->
conn