Script v2: Make detection take less time (#5635)
* Add fast failing dns check to verification * Convert Detection to a checks pipeline * Convert detection to checks pipeline * Unify browserless checks, set retry policy, timeouts * Fix spelling * Update change domain v2 * Fix issue with handling errors with detection * Include timeoutMs in detector function args * Allow saving npm installation type (#5639) * small code style/comment improvements --------- Co-authored-by: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Co-authored-by: Robert Joonas <robertjoonas16@gmail.com>
This commit is contained in:
parent
bb1db557a3
commit
276f95cda2
|
|
@ -937,7 +937,7 @@ config :plausible, Plausible.PromEx,
|
|||
grafana: :disabled,
|
||||
metrics_server: :disabled
|
||||
|
||||
config :plausible, Plausible.InstallationSupport,
|
||||
config :plausible, Plausible.InstallationSupport.BrowserlessConfig,
|
||||
token: get_var_from_path_or_env(config_dir, "BROWSERLESS_TOKEN", "dummy_token"),
|
||||
endpoint: get_var_from_path_or_env(config_dir, "BROWSERLESS_ENDPOINT", "http://0.0.0.0:3000")
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ config :plausible,
|
|||
session_timeout: 0,
|
||||
http_impl: Plausible.HTTPClient.Mock
|
||||
|
||||
config :plausible,
|
||||
dns_lookup_impl: Plausible.DnsLookup.Mock
|
||||
|
||||
config :plausible, Plausible.Cache, enabled: false
|
||||
|
||||
config :ex_money, api_module: Plausible.ExchangeRateMock
|
||||
|
|
@ -49,7 +52,7 @@ config :plausible, Plausible.HelpScout,
|
|||
plug: {Req.Test, Plausible.HelpScout}
|
||||
]
|
||||
|
||||
config :plausible, Plausible.InstallationSupport.Detection,
|
||||
config :plausible, Plausible.InstallationSupport.Checks.Detection,
|
||||
req_opts: [
|
||||
plug: {Req.Test, :global}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
defmodule Plausible.DnsLookupInterface do
|
||||
@moduledoc """
|
||||
Behaviour module for DNS lookup operations.
|
||||
"""
|
||||
|
||||
@callback lookup(
|
||||
name :: charlist(),
|
||||
class :: atom(),
|
||||
type :: atom(),
|
||||
opts :: list(),
|
||||
timeout :: integer()
|
||||
) ::
|
||||
list() | []
|
||||
end
|
||||
|
||||
defmodule Plausible.DnsLookup do
|
||||
@moduledoc """
|
||||
Thin wrapper around `:inet_res.lookup/5`.
|
||||
To use, call `Plausible.DnsLookup.impl().lookup/5`,
|
||||
this allows for mocking DNS lookups in tests.
|
||||
"""
|
||||
|
||||
@behaviour Plausible.DnsLookupInterface
|
||||
|
||||
@impl Plausible.DnsLookupInterface
|
||||
def lookup(name, class, type, opts, timeout),
|
||||
do: :inet_res.lookup(name, class, type, opts, timeout)
|
||||
|
||||
@spec impl() :: Plausible.DnsLookup
|
||||
def impl(), do: Application.get_env(:plausible, :dns_lookup_impl, __MODULE__)
|
||||
end
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Plausible.InstallationSupport.BrowserlessConfig do
|
||||
@moduledoc """
|
||||
Req options for browserless.io requests
|
||||
"""
|
||||
use Plausible
|
||||
|
||||
def retry_browserless_request(_request, %{status: status}) do
|
||||
case status do
|
||||
# rate limit
|
||||
429 -> {:delay, 1000}
|
||||
# timeout
|
||||
408 -> {:delay, 500}
|
||||
# other errors
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
on_ee do
|
||||
def browserless_function_api_endpoint() do
|
||||
config = Application.fetch_env!(:plausible, __MODULE__)
|
||||
token = Keyword.fetch!(config, :token)
|
||||
endpoint = Keyword.fetch!(config, :endpoint)
|
||||
Path.join(endpoint, "function?token=#{token}&stealth")
|
||||
end
|
||||
else
|
||||
def browserless_function_api_endpoint() do
|
||||
"Browserless API should not be called on Community Edition"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,13 +27,18 @@ defmodule Plausible.InstallationSupport.CheckRunner do
|
|||
|
||||
defp do_run(state, checks, slowdown) do
|
||||
state =
|
||||
Enum.reduce(
|
||||
Enum.reduce_while(
|
||||
checks,
|
||||
state,
|
||||
fn check, state ->
|
||||
state
|
||||
|> notify_check_start(check, slowdown)
|
||||
|> check.perform_safe()
|
||||
if state.skip_further_checks? do
|
||||
{:halt, state}
|
||||
else
|
||||
{:cont,
|
||||
state
|
||||
|> notify_check_start(check, slowdown)
|
||||
|> check.perform_safe()}
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.Detection do
|
||||
@moduledoc """
|
||||
Calls the browserless.io service (local instance can be spawned with `make browserless`)
|
||||
and runs detector script via the [function API](https://docs.browserless.io/HTTP-APIs/function).
|
||||
|
||||
* v1_detected (optional - detection can take up to @plausible_window_check_timeout_ms)
|
||||
* gtm_likely
|
||||
* wordpress_likely
|
||||
* wordpress_plugin
|
||||
|
||||
These diagnostics are used to determine what installation type to recommend,
|
||||
and whether to provide a notice for upgrading an existing v1 integration to v2.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
use Plausible.InstallationSupport.Check
|
||||
alias Plausible.InstallationSupport.BrowserlessConfig
|
||||
|
||||
@detector_code_path "priv/tracker/installation_support/detector.js"
|
||||
@external_resource @detector_code_path
|
||||
|
||||
# On CI, the file might not be present for static checks so we default to empty string
|
||||
@detector_code (case File.read(Application.app_dir(:plausible, @detector_code_path)) do
|
||||
{:ok, content} -> content
|
||||
{:error, _} -> ""
|
||||
end)
|
||||
|
||||
# Puppeteer wrapper function that executes the vanilla JS detector code
|
||||
@puppeteer_wrapper_code """
|
||||
export default async function({ page, context: { url, userAgent, ...functionContext } }) {
|
||||
try {
|
||||
await page.setUserAgent(userAgent);
|
||||
await page.goto(url);
|
||||
|
||||
await page.evaluate(() => {
|
||||
#{@detector_code} // injects window.scanPageBeforePlausibleInstallation
|
||||
});
|
||||
|
||||
return await page.evaluate(
|
||||
(c) => window.scanPageBeforePlausibleInstallation(c),
|
||||
{ ...functionContext }
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
data: {
|
||||
completed: false,
|
||||
error: {
|
||||
message: error?.message ?? JSON.stringify(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# We define a timeout for the browserless endpoint call to avoid waiting too long for a response
|
||||
@endpoint_timeout_ms 2_000
|
||||
|
||||
# This timeout determines how long we wait for window.plausible to be initialized on the page, used for detecting whether v1 installed
|
||||
@plausible_window_check_timeout_ms 1_500
|
||||
|
||||
# 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 report_progress_as, do: "We're checking your site to recommend the best installation method"
|
||||
|
||||
@impl true
|
||||
def perform(%State{url: url, assigns: %{detect_v1?: detect_v1?}} = state) do
|
||||
opts =
|
||||
[
|
||||
headers: %{content_type: "application/json"},
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
code: @puppeteer_wrapper_code,
|
||||
context: %{
|
||||
url: url,
|
||||
userAgent: Plausible.InstallationSupport.user_agent(),
|
||||
detectV1: detect_v1?,
|
||||
timeoutMs: @plausible_window_check_timeout_ms,
|
||||
debug: Application.get_env(:plausible, :environment) == "dev"
|
||||
}
|
||||
}),
|
||||
params: %{timeout: @endpoint_timeout_ms},
|
||||
retry: &BrowserlessConfig.retry_browserless_request/2,
|
||||
retry_log_level: :warning,
|
||||
max_retries: @max_retries
|
||||
]
|
||||
|> Keyword.merge(Application.get_env(:plausible, __MODULE__)[:req_opts] || [])
|
||||
|
||||
extra_opts = Application.get_env(:plausible, __MODULE__)[:req_opts] || []
|
||||
opts = Keyword.merge(opts, extra_opts)
|
||||
|
||||
case Req.post(BrowserlessConfig.browserless_function_api_endpoint(), opts) do
|
||||
{:ok, %{body: body, status: status}} ->
|
||||
handle_browserless_response(state, body, status)
|
||||
|
||||
{:error, %{reason: reason}} ->
|
||||
Logger.warning(warning_message("Browserless request error: #{inspect(reason)}", state))
|
||||
|
||||
put_diagnostics(state, service_error: reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_browserless_response(
|
||||
state,
|
||||
%{"data" => %{"completed" => completed} = data},
|
||||
_status
|
||||
) do
|
||||
if completed do
|
||||
put_diagnostics(
|
||||
state,
|
||||
parse_to_diagnostics(data)
|
||||
)
|
||||
else
|
||||
Logger.warning(
|
||||
warning_message(
|
||||
"Browserless function returned with completed: false, error.message: #{inspect(data["error"]["message"])}",
|
||||
state
|
||||
)
|
||||
)
|
||||
|
||||
put_diagnostics(state, service_error: data["error"]["message"])
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_browserless_response(state, _body, status) do
|
||||
error = "Unhandled browserless response with status: #{status}"
|
||||
Logger.warning(warning_message(error, state))
|
||||
|
||||
put_diagnostics(state, service_error: error)
|
||||
end
|
||||
|
||||
defp warning_message(message, state) do
|
||||
"[DETECTION] #{message} (data_domain='#{state.data_domain}')"
|
||||
end
|
||||
|
||||
defp parse_to_diagnostics(data),
|
||||
do: [
|
||||
v1_detected: data["v1Detected"],
|
||||
gtm_likely: data["gtmLikely"],
|
||||
wordpress_likely: data["wordpressLikely"],
|
||||
wordpress_plugin: data["wordpressPlugin"],
|
||||
service_error: nil
|
||||
]
|
||||
end
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.Installation do
|
||||
require Logger
|
||||
alias Plausible.InstallationSupport.BrowserlessConfig
|
||||
|
||||
@verifier_code_path "priv/tracker/installation_support/verifier-v1.js"
|
||||
@external_resource @verifier_code_path
|
||||
|
|
@ -134,7 +135,7 @@ defmodule Plausible.InstallationSupport.Checks.Installation do
|
|||
extra_opts = Application.get_env(:plausible, __MODULE__)[:req_opts] || []
|
||||
opts = Keyword.merge(opts, extra_opts)
|
||||
|
||||
case Req.post(Plausible.InstallationSupport.browserless_function_api_endpoint(), opts) do
|
||||
case Req.post(BrowserlessConfig.browserless_function_api_endpoint(), opts) do
|
||||
{:ok, %{status: 200, body: %{"data" => %{"completed" => true} = js_data}}} ->
|
||||
emit_telemetry_and_log(state.diagnostics, js_data, data_domain)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
||||
@moduledoc """
|
||||
Calls the browserless.io service (local instance can be spawned with `make browserless`)
|
||||
and runs verifier script via the [function API](https://docs.browserless.io/HTTP-APIs/function).
|
||||
"""
|
||||
|
||||
require Logger
|
||||
use Plausible.InstallationSupport.Check
|
||||
alias Plausible.InstallationSupport.BrowserlessConfig
|
||||
|
||||
@verifier_code_path "priv/tracker/installation_support/verifier-v2.js"
|
||||
@external_resource @verifier_code_path
|
||||
|
|
@ -10,19 +17,6 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
{:error, _} -> ""
|
||||
end)
|
||||
|
||||
# To support browserless API being unavailable or overloaded, we retry the endpoint call if it doesn't return a successful response
|
||||
@max_retries 2
|
||||
|
||||
# We define a timeout for the browserless endpoint call to avoid waiting too long for a response
|
||||
@endpoint_timeout_ms 10_000
|
||||
|
||||
# This timeout determines how long we wait for window.plausible to be initialized on the page, including sending the test event
|
||||
@plausible_window_check_timeout_ms 4_000
|
||||
|
||||
# To handle navigation that happens immediately on the page, we attempt to verify the installation multiple times _within a single browserless endpoint call_
|
||||
@max_attempts 2
|
||||
@timeout_between_attempts_ms 500
|
||||
|
||||
# Puppeteer wrapper function that executes the vanilla JS verifier code
|
||||
@puppeteer_wrapper_code """
|
||||
export default async function({ page, context: { url, userAgent, maxAttempts, timeoutBetweenAttemptsMs, ...functionContext } }) {
|
||||
|
|
@ -75,11 +69,18 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
}
|
||||
"""
|
||||
|
||||
@moduledoc """
|
||||
Calls the browserless.io service (local instance can be spawned with `make browserless`)
|
||||
and runs verifier script via the [function API](https://docs.browserless.io/HTTP-APIs/function).
|
||||
"""
|
||||
use Plausible.InstallationSupport.Check
|
||||
# To support browserless API being unavailable or overloaded, we retry the endpoint call if it doesn't return a successful response
|
||||
@max_retries 1
|
||||
|
||||
# We define a timeout for the browserless endpoint call to avoid waiting too long for a response
|
||||
@endpoint_timeout_ms 10_000
|
||||
|
||||
# This timeout determines how long we wait for window.plausible to be initialized on the page, including sending the test event
|
||||
@plausible_window_check_timeout_ms 4_000
|
||||
|
||||
# To handle navigation that happens immediately on the page, we attempt to verify the installation multiple times _within a single browserless endpoint call_
|
||||
@max_attempts 2
|
||||
@timeout_between_attempts_ms 500
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're verifying that your visitors are being counted correctly"
|
||||
|
|
@ -102,7 +103,7 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
}
|
||||
}),
|
||||
params: %{timeout: @endpoint_timeout_ms},
|
||||
retry: :transient,
|
||||
retry: &BrowserlessConfig.retry_browserless_request/2,
|
||||
retry_log_level: :warning,
|
||||
max_retries: @max_retries
|
||||
]
|
||||
|
|
@ -110,7 +111,7 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
extra_opts = Application.get_env(:plausible, __MODULE__)[:req_opts] || []
|
||||
opts = Keyword.merge(opts, extra_opts)
|
||||
|
||||
case Req.post(Plausible.InstallationSupport.browserless_function_api_endpoint(), opts) do
|
||||
case Req.post(BrowserlessConfig.browserless_function_api_endpoint(), opts) do
|
||||
{:ok, %{body: body, status: status}} ->
|
||||
handle_browserless_response(state, body, status)
|
||||
|
||||
|
|
@ -129,15 +130,7 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
if completed do
|
||||
put_diagnostics(
|
||||
state,
|
||||
disallowed_by_csp: data["disallowedByCsp"],
|
||||
plausible_is_on_window: data["plausibleIsOnWindow"],
|
||||
plausible_is_initialized: data["plausibleIsInitialized"],
|
||||
plausible_version: data["plausibleVersion"],
|
||||
plausible_variant: data["plausibleVariant"],
|
||||
test_event: data["testEvent"],
|
||||
cookie_banner_likely: data["cookieBannerLikely"],
|
||||
attempts: data["attempts"],
|
||||
service_error: nil
|
||||
parse_to_diagnostics(data)
|
||||
)
|
||||
else
|
||||
Logger.warning(
|
||||
|
|
@ -161,4 +154,17 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
defp warning_message(message, state) do
|
||||
"[VERIFICATION v2] #{message} (data_domain='#{state.data_domain}')"
|
||||
end
|
||||
|
||||
defp parse_to_diagnostics(data),
|
||||
do: [
|
||||
disallowed_by_csp: data["disallowedByCsp"],
|
||||
plausible_is_on_window: data["plausibleIsOnWindow"],
|
||||
plausible_is_initialized: data["plausibleIsInitialized"],
|
||||
plausible_version: data["plausibleVersion"],
|
||||
plausible_variant: data["plausibleVariant"],
|
||||
test_event: data["testEvent"],
|
||||
cookie_banner_likely: data["cookieBannerLikely"],
|
||||
attempts: data["attempts"],
|
||||
service_error: nil
|
||||
]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.Url do
|
||||
@moduledoc """
|
||||
Checks if site domain has an A record.
|
||||
If not, checks if prepending `www.` helps,
|
||||
because we have specifically requested customers to register the domain with `www.` prefix.
|
||||
If not, skips all further checks.
|
||||
"""
|
||||
|
||||
use Plausible.InstallationSupport.Check
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're trying to reach your website"
|
||||
|
||||
@impl true
|
||||
@spec perform(Plausible.InstallationSupport.State.t()) ::
|
||||
Plausible.InstallationSupport.State.t()
|
||||
def perform(%State{url: url} = state) when is_binary(url) do
|
||||
with {:ok, %URI{scheme: scheme} = uri} when scheme in ["https"] <- URI.new(url),
|
||||
:ok <- check_domain(uri.host) do
|
||||
stripped_url = URI.to_string(%URI{uri | query: nil, fragment: nil})
|
||||
%State{state | url: stripped_url}
|
||||
else
|
||||
{:error, :no_a_record} ->
|
||||
put_diagnostics(%State{state | skip_further_checks?: true},
|
||||
service_error: :domain_not_found
|
||||
)
|
||||
|
||||
_ ->
|
||||
put_diagnostics(%State{state | skip_further_checks?: true},
|
||||
service_error: :invalid_url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(%State{data_domain: domain} = state) when is_binary(domain) do
|
||||
case find_working_url(domain) do
|
||||
{:ok, working_url} ->
|
||||
%State{state | url: working_url}
|
||||
|
||||
{:error, :domain_not_found} ->
|
||||
put_diagnostics(%State{state | url: nil, skip_further_checks?: true},
|
||||
service_error: :domain_not_found
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Check A records of the the domains [domain, "www.#{domain}"]
|
||||
# at this point, domain can contain path
|
||||
@spec find_working_url(String.t()) :: {:ok, String.t()} | {:error, :domain_not_found}
|
||||
defp find_working_url(domain) do
|
||||
[domain_without_path | rest] = split_domain(domain)
|
||||
|
||||
[
|
||||
domain_without_path,
|
||||
"www.#{domain_without_path}"
|
||||
]
|
||||
|> Enum.reduce_while({:error, :domain_not_found}, fn d, _acc ->
|
||||
case check_domain(d) do
|
||||
:ok -> {:halt, {:ok, "https://" <> unsplit_domain(d, rest)}}
|
||||
{:error, :no_a_record} -> {:cont, {:error, :domain_not_found}}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec check_domain(String.t()) :: :ok | {:error, :no_a_record}
|
||||
defp check_domain(domain) do
|
||||
lookup_timeout = 1_000
|
||||
resolve_timeout = 1_000
|
||||
|
||||
case Plausible.DnsLookup.impl().lookup(
|
||||
to_charlist(domain),
|
||||
:in,
|
||||
:a,
|
||||
[timeout: resolve_timeout],
|
||||
lookup_timeout
|
||||
) do
|
||||
[{a, b, c, d} | _]
|
||||
when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) ->
|
||||
:ok
|
||||
|
||||
# this may mean timeout or no DNS record
|
||||
[] ->
|
||||
{:error, :no_a_record}
|
||||
end
|
||||
end
|
||||
|
||||
@spec split_domain(String.t()) :: [String.t()]
|
||||
defp split_domain(domain) do
|
||||
String.split(domain, "/", parts: 2)
|
||||
end
|
||||
|
||||
@spec unsplit_domain(String.t(), [String.t()]) :: String.t()
|
||||
defp unsplit_domain(domain_without_path, rest) do
|
||||
Enum.join([domain_without_path] ++ rest, "/")
|
||||
end
|
||||
end
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Detection do
|
||||
@moduledoc """
|
||||
Exposes a perform function which visits the given URL via a Browserless
|
||||
/function API call, and in a returns the following diagnostics:
|
||||
|
||||
* v1_detected (optional - detection can take up to 3s)
|
||||
* gtm_likely
|
||||
* wordpress_likely
|
||||
* wordpress_plugin
|
||||
|
||||
These diagnostics are used to determine what installation type to recommend,
|
||||
and whether to provide a notice for upgrading an existing v1 integration to v2.
|
||||
"""
|
||||
require Logger
|
||||
alias Plausible.InstallationSupport
|
||||
|
||||
@detector_code_path "priv/tracker/installation_support/detector.js"
|
||||
@external_resource @detector_code_path
|
||||
|
||||
# On CI, the file might not be present for static checks so we default to empty string
|
||||
@detector_code (case File.read(Application.app_dir(:plausible, @detector_code_path)) do
|
||||
{:ok, content} -> content
|
||||
{:error, _} -> ""
|
||||
end)
|
||||
|
||||
# Puppeteer wrapper function that executes the vanilla JS verifier code
|
||||
@puppeteer_wrapper_code """
|
||||
export default async function({ page, context }) {
|
||||
try {
|
||||
await page.setUserAgent(context.userAgent);
|
||||
await page.goto(context.url);
|
||||
|
||||
await page.evaluate(() => {
|
||||
#{@detector_code}
|
||||
});
|
||||
|
||||
return await page.evaluate(async (detectV1, debug) => {
|
||||
return await window.scanPageBeforePlausibleInstallation(detectV1, debug);
|
||||
}, context.detectV1, context.debug);
|
||||
} catch (error) {
|
||||
const msg = error.message ? error.message : JSON.stringify(error)
|
||||
return {data: {completed: false, error: msg}}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def perform(url, opts \\ []) do
|
||||
req_opts =
|
||||
[
|
||||
headers: %{content_type: "application/json"},
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
code: @puppeteer_wrapper_code,
|
||||
context: %{
|
||||
url: url,
|
||||
userAgent: InstallationSupport.user_agent(),
|
||||
detectV1: Keyword.get(opts, :detect_v1?, false),
|
||||
debug: Application.get_env(:plausible, :environment) == "dev"
|
||||
}
|
||||
}),
|
||||
retry: :transient,
|
||||
retry_log_level: :warning,
|
||||
max_retries: 2
|
||||
]
|
||||
|> Keyword.merge(Application.get_env(:plausible, __MODULE__)[:req_opts] || [])
|
||||
|
||||
case Req.post(InstallationSupport.browserless_function_api_endpoint(), req_opts) do
|
||||
{:ok, %{status: 200, body: %{"data" => %{"completed" => true} = js_data}}} ->
|
||||
{:ok,
|
||||
%{
|
||||
v1_detected: js_data["v1Detected"],
|
||||
gtm_likely: js_data["gtmLikely"],
|
||||
wordpress_likely: js_data["wordpressLikely"],
|
||||
wordpress_plugin: js_data["wordpressPlugin"]
|
||||
}}
|
||||
|
||||
{:ok, %{body: %{"data" => %{"error" => error}}}} ->
|
||||
Logger.warning("[DETECTION] Browserless JS error (url='#{url}'): #{inspect(error)}")
|
||||
|
||||
{:error, {:browserless, error}}
|
||||
|
||||
{:error, %{reason: reason}} ->
|
||||
Logger.warning("[DETECTION] Browserless request error (url='#{url}'): #{inspect(reason)}")
|
||||
|
||||
{:error, {:req, reason}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
defmodule Plausible.InstallationSupport.Detection.Checks do
|
||||
@moduledoc """
|
||||
Checks that are performed pre-installation, providing recommended installation
|
||||
methods and whether v1 is used on the site.
|
||||
|
||||
In async execution, each check notifies the caller by sending a message to it.
|
||||
"""
|
||||
alias Plausible.InstallationSupport.Detection
|
||||
alias Plausible.InstallationSupport.{State, CheckRunner, Checks}
|
||||
|
||||
require Logger
|
||||
|
||||
@checks [
|
||||
Checks.Url,
|
||||
Checks.Detection
|
||||
]
|
||||
|
||||
def run(url, data_domain, opts \\ []) do
|
||||
report_to = Keyword.get(opts, :report_to, self())
|
||||
async? = Keyword.get(opts, :async?, true)
|
||||
slowdown = Keyword.get(opts, :slowdown, 500)
|
||||
detect_v1? = Keyword.get(opts, :detect_v1?, false)
|
||||
|
||||
init_state =
|
||||
%State{
|
||||
url: url,
|
||||
data_domain: data_domain,
|
||||
report_to: report_to,
|
||||
diagnostics: %Detection.Diagnostics{},
|
||||
assigns: %{detect_v1?: detect_v1?}
|
||||
}
|
||||
|
||||
CheckRunner.run(init_state, @checks,
|
||||
async?: async?,
|
||||
report_to: report_to,
|
||||
slowdown: slowdown
|
||||
)
|
||||
end
|
||||
|
||||
def interpret_diagnostics(%State{} = state) do
|
||||
Detection.Diagnostics.interpret(
|
||||
state.diagnostics,
|
||||
state.url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
defmodule Plausible.InstallationSupport.Detection.Diagnostics do
|
||||
@moduledoc """
|
||||
Module responsible for translating diagnostics to user-friendly errors and recommendations.
|
||||
"""
|
||||
require Logger
|
||||
|
||||
# in this struct, nil means indeterminate
|
||||
defstruct v1_detected: nil,
|
||||
gtm_likely: nil,
|
||||
wordpress_likely: nil,
|
||||
wordpress_plugin: nil,
|
||||
service_error: nil
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
alias Plausible.InstallationSupport.Result
|
||||
|
||||
@spec interpret(t(), String.t()) :: Result.t()
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
gtm_likely: true,
|
||||
service_error: nil
|
||||
} = diagnostics,
|
||||
_url
|
||||
) do
|
||||
get_result("gtm", diagnostics)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
wordpress_likely: true,
|
||||
service_error: nil
|
||||
} = diagnostics,
|
||||
_url
|
||||
) do
|
||||
get_result(
|
||||
"wordpress",
|
||||
diagnostics
|
||||
)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
service_error: nil
|
||||
} = diagnostics,
|
||||
_url
|
||||
) do
|
||||
get_result("manual", diagnostics)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
service_error: service_error
|
||||
},
|
||||
_url
|
||||
)
|
||||
when service_error in [:domain_not_found, :invalid_url],
|
||||
do: %Result{ok?: false, data: nil, errors: [Atom.to_string(service_error)]}
|
||||
|
||||
def interpret(%__MODULE__{} = diagnostics, url),
|
||||
do: unhandled_case(diagnostics, url)
|
||||
|
||||
defp unhandled_case(diagnostics, url) do
|
||||
Sentry.capture_message("Unhandled case for detection",
|
||||
extra: %{
|
||||
message: inspect(diagnostics),
|
||||
url: url,
|
||||
hash: :erlang.phash2(diagnostics)
|
||||
}
|
||||
)
|
||||
|
||||
%Result{ok?: false, data: nil, errors: ["Unhandled detection case"]}
|
||||
end
|
||||
|
||||
defp get_result(suggested_technology, diagnostics) do
|
||||
%Result{
|
||||
ok?: true,
|
||||
data: %{
|
||||
v1_detected: diagnostics.v1_detected,
|
||||
wordpress_plugin: diagnostics.wordpress_plugin,
|
||||
suggested_technology: suggested_technology
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -4,8 +4,7 @@ defmodule Plausible.InstallationSupport do
|
|||
site scans and verification of whether Plausible has been installed
|
||||
correctly.
|
||||
|
||||
Defines the user-agent used by Elixir-native HTTP requests as well
|
||||
as headless browser checks on the client side via Browserless.
|
||||
Defines the user-agent used with checks.
|
||||
"""
|
||||
use Plausible
|
||||
|
||||
|
|
@ -13,18 +12,7 @@ defmodule Plausible.InstallationSupport do
|
|||
def user_agent() do
|
||||
"Plausible Verification Agent - if abused, contact support@plausible.io"
|
||||
end
|
||||
|
||||
def browserless_function_api_endpoint() do
|
||||
config = Application.fetch_env!(:plausible, __MODULE__)
|
||||
token = Keyword.fetch!(config, :token)
|
||||
endpoint = Keyword.fetch!(config, :endpoint)
|
||||
Path.join(endpoint, "function?token=#{token}&stealth")
|
||||
end
|
||||
else
|
||||
def browserless_function_api_endpoint() do
|
||||
"Browserless API should not be called on Community Edition"
|
||||
end
|
||||
|
||||
def user_agent() do
|
||||
"Plausible Community Edition"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
defmodule Plausible.InstallationSupport.Result do
|
||||
@moduledoc """
|
||||
Diagnostics interpretation result.
|
||||
|
||||
## Example
|
||||
ok?: false,
|
||||
data: nil,
|
||||
errors: [error.message],
|
||||
recommendations: [%{text: error.recommendation, url: error.url}]
|
||||
|
||||
ok?: true,
|
||||
data: %{},
|
||||
errors: [],
|
||||
recommendations: []
|
||||
"""
|
||||
defstruct ok?: false, errors: [], recommendations: []
|
||||
defstruct ok?: false, errors: [], recommendations: [], data: nil
|
||||
@type t :: %__MODULE__{}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,18 +10,21 @@ defmodule Plausible.InstallationSupport.State do
|
|||
data_domain: nil,
|
||||
report_to: nil,
|
||||
assigns: %{},
|
||||
diagnostics: %{}
|
||||
diagnostics: %{},
|
||||
skip_further_checks?: false
|
||||
|
||||
@type diagnostics_type ::
|
||||
Plausible.InstallationSupport.LegacyVerification.Diagnostics.t()
|
||||
| Plausible.InstallationSupport.Verification.Diagnostics.t()
|
||||
| Plausible.InstallationSupport.Detection.Diagnostics.t()
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
url: String.t() | nil,
|
||||
data_domain: String.t() | nil,
|
||||
report_to: pid() | nil,
|
||||
assigns: map(),
|
||||
diagnostics: diagnostics_type()
|
||||
diagnostics: diagnostics_type(),
|
||||
skip_further_checks?: boolean()
|
||||
}
|
||||
|
||||
def assign(%__MODULE__{} = state, assigns) do
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
|
|||
require Logger
|
||||
|
||||
@checks [
|
||||
Checks.Url,
|
||||
Checks.InstallationV2,
|
||||
Checks.InstallationV2CacheBust
|
||||
]
|
||||
|
||||
def run(url, data_domain, installation_type, opts \\ []) do
|
||||
checks = Keyword.get(opts, :checks, @checks)
|
||||
report_to = Keyword.get(opts, :report_to, self())
|
||||
async? = Keyword.get(opts, :async?, true)
|
||||
slowdown = Keyword.get(opts, :slowdown, 500)
|
||||
|
|
@ -30,7 +30,7 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
|
|||
}
|
||||
}
|
||||
|
||||
CheckRunner.run(init_state, checks,
|
||||
CheckRunner.run(init_state, @checks,
|
||||
async?: async?,
|
||||
report_to: report_to,
|
||||
slowdown: slowdown
|
||||
|
|
|
|||
|
|
@ -198,6 +198,17 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do
|
|||
),
|
||||
do: error(@error_gtm_selected_maybe_cookie_banner)
|
||||
|
||||
@error_domain_not_found Error.new!(%{
|
||||
message: "We couldn't verify your website",
|
||||
recommendation:
|
||||
"Please check that the domain you entered is correct and that the website is reachable publicly. If it's intentionally private, you'll need to verify that Plausible works manually",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
|
||||
})
|
||||
def interpret(%__MODULE__{service_error: service_error}, _expected_domain, _url)
|
||||
when service_error in [:domain_not_found, :invalid_url],
|
||||
do: error(@error_domain_not_found)
|
||||
|
||||
def interpret(%__MODULE__{} = diagnostics, _expected_domain, url),
|
||||
do: unknown_error(diagnostics, url)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ defmodule Plausible.Site.TrackerScriptConfiguration do
|
|||
|
||||
@primary_key {:id, Plausible.Ecto.Types.TrackerScriptNanoid, autogenerate: true}
|
||||
schema "tracker_script_configuration" do
|
||||
field :installation_type, Ecto.Enum, values: [:manual, :wordpress, :gtm, nil]
|
||||
field :installation_type, Ecto.Enum, values: [:manual, :wordpress, :gtm, :npm, nil]
|
||||
|
||||
field :track_404_pages, :boolean, default: false
|
||||
field :hash_based_routing, :boolean, default: false
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2 do
|
|||
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
alias PlausibleWeb.Live.ChangeDomainV2.Form
|
||||
alias Plausible.InstallationSupport.Detection
|
||||
alias Plausible.InstallationSupport.{Detection, Result}
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
|
||||
def mount(
|
||||
|
|
@ -34,7 +34,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2 do
|
|||
site_domain = socket.assigns.site.domain
|
||||
|
||||
assign_async(socket, :detection_result, fn ->
|
||||
run_detection("https://#{site_domain}")
|
||||
run_detection(site_domain)
|
||||
end)
|
||||
else
|
||||
socket
|
||||
|
|
@ -155,10 +155,34 @@ defmodule PlausibleWeb.Live.ChangeDomainV2 do
|
|||
|> push_patch(to: Routes.site_path(socket, :success, updated_site.domain))}
|
||||
end
|
||||
|
||||
defp run_detection(url) do
|
||||
case Detection.perform(url, detect_v1?: true) do
|
||||
{:ok, result} -> {:ok, %{detection_result: result}}
|
||||
e -> e
|
||||
defp run_detection(domain) do
|
||||
detection_result =
|
||||
Detection.Checks.run(nil, domain,
|
||||
detect_v1?: true,
|
||||
report_to: nil,
|
||||
async?: false,
|
||||
slowdown: 0
|
||||
)
|
||||
|> Detection.Checks.interpret_diagnostics()
|
||||
|
||||
case detection_result do
|
||||
%Result{
|
||||
ok?: true,
|
||||
data: %{
|
||||
v1_detected: v1_detected,
|
||||
wordpress_plugin: wordpress_plugin
|
||||
}
|
||||
} ->
|
||||
{:ok,
|
||||
%{
|
||||
detection_result: %{
|
||||
v1_detected: v1_detected,
|
||||
wordpress_plugin: wordpress_plugin
|
||||
}
|
||||
}}
|
||||
|
||||
%Result{ok?: false, errors: errors} ->
|
||||
{:error, List.first(errors, :unknown_reason)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ defmodule PlausibleWeb.Live.InstallationV2 do
|
|||
"""
|
||||
alias PlausibleWeb.Flows
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
alias Plausible.InstallationSupport.Detection
|
||||
alias Plausible.InstallationSupport.{Detection, Result}
|
||||
alias PlausibleWeb.Live.InstallationV2.Icons
|
||||
alias PlausibleWeb.Live.InstallationV2.Instructions
|
||||
use PlausibleWeb, :live_view
|
||||
|
|
@ -144,19 +144,22 @@ defmodule PlausibleWeb.Live.InstallationV2 do
|
|||
defp verify_cta("gtm"), do: "Verify Tag Manager installation"
|
||||
defp verify_cta("npm"), do: "Verify NPM installation"
|
||||
|
||||
defp get_recommended_installation_type(flow, url) do
|
||||
case Detection.perform(url, detect_v1?: flow == Flows.review()) do
|
||||
{:ok, result} ->
|
||||
Logger.debug("Detection result: #{inspect(result)}")
|
||||
defp get_recommended_installation_type(flow, site) do
|
||||
detection_result =
|
||||
Detection.Checks.run(nil, site.domain,
|
||||
detect_v1?: flow == Flows.review(),
|
||||
report_to: nil,
|
||||
slowdown: 0,
|
||||
async?: false
|
||||
)
|
||||
|> Detection.Checks.interpret_diagnostics()
|
||||
|
||||
type =
|
||||
case result do
|
||||
%{gtm_likely: true} -> "gtm"
|
||||
%{wordpress_likely: true} -> "wordpress"
|
||||
_ -> "manual"
|
||||
end
|
||||
|
||||
{type, result.v1_detected}
|
||||
case detection_result do
|
||||
%Result{
|
||||
ok?: true,
|
||||
data: %{suggested_technology: suggested_technology, v1_detected: v1_detected}
|
||||
} ->
|
||||
{suggested_technology, v1_detected}
|
||||
|
||||
_ ->
|
||||
{"manual", false}
|
||||
|
|
@ -203,7 +206,7 @@ defmodule PlausibleWeb.Live.InstallationV2 do
|
|||
|
||||
defp initialize_installation_data(flow, site, params) do
|
||||
{recommended_installation_type, v1_detected} =
|
||||
get_recommended_installation_type(flow, "https://#{site.domain}")
|
||||
get_recommended_installation_type(flow, site)
|
||||
|
||||
tracker_script_configuration =
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
|
||||
socket =
|
||||
assign(socket,
|
||||
url_to_verify: nil,
|
||||
site: site,
|
||||
super_admin?: super_admin?,
|
||||
domain: domain,
|
||||
|
|
@ -110,12 +111,12 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
|
||||
def handle_event("launch-verification", _, socket) do
|
||||
launch_delayed(socket)
|
||||
{:noreply, reset_component(socket)}
|
||||
{:noreply, reset_component(socket, nil)}
|
||||
end
|
||||
|
||||
def handle_event("retry", _, socket) do
|
||||
def handle_event("retry", params, socket) do
|
||||
launch_delayed(socket)
|
||||
{:noreply, reset_component(socket)}
|
||||
{:noreply, reset_component(socket, params["url_to_verify"])}
|
||||
end
|
||||
|
||||
def handle_info({:start, report_to}, socket) do
|
||||
|
|
@ -131,7 +132,6 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
{:deny, _} -> :timer.sleep(@slowdown_for_frequent_checking)
|
||||
end
|
||||
|
||||
url_to_verify = "https://#{socket.assigns.domain}"
|
||||
domain = socket.assigns.domain
|
||||
installation_type = socket.assigns.installation_type
|
||||
|
||||
|
|
@ -140,13 +140,13 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
FunWithFlags.enabled?(:scriptv2, for: socket.assigns.site) or
|
||||
FunWithFlags.enabled?(:scriptv2, for: socket.assigns.current_user),
|
||||
do:
|
||||
Verification.Checks.run(url_to_verify, domain, installation_type,
|
||||
Verification.Checks.run(socket.assigns.url_to_verify, domain, installation_type,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
),
|
||||
else:
|
||||
LegacyVerification.Checks.run(
|
||||
url_to_verify,
|
||||
"https://#{socket.assigns.domain}",
|
||||
domain,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
|
|
@ -157,10 +157,17 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_info({:check_start, {check, _state}}, socket) do
|
||||
update_component(socket,
|
||||
message: check.report_progress_as()
|
||||
)
|
||||
def handle_info({:check_start, {check, state}}, socket) do
|
||||
to_update = [message: check.report_progress_as()]
|
||||
|
||||
to_update =
|
||||
if is_binary(state.url) do
|
||||
Keyword.put(to_update, :url_to_verify, state.url)
|
||||
else
|
||||
to_update
|
||||
end
|
||||
|
||||
update_component(socket, to_update)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -207,7 +214,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
redirect(socket, to: stats_url)
|
||||
end
|
||||
|
||||
defp reset_component(socket) do
|
||||
defp reset_component(socket, url_to_verify) do
|
||||
update_component(socket,
|
||||
message: "We're visiting your site to ensure that everything is working",
|
||||
finished?: false,
|
||||
|
|
@ -215,7 +222,11 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
diagnostics: nil
|
||||
)
|
||||
|
||||
socket
|
||||
if is_binary(url_to_verify) do
|
||||
assign(socket, url_to_verify: url_to_verify)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp update_component(_socket, updates) do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.UrlTest do
|
||||
@moduledoc """
|
||||
Tests for URL check that is used in detection and verification checks pipelines
|
||||
to fail fast on non-existent domains.
|
||||
"""
|
||||
use Plausible.DataCase, async: true
|
||||
import Mox
|
||||
|
||||
alias Plausible.InstallationSupport.{State, Checks, Verification}
|
||||
|
||||
@check Checks.Url
|
||||
|
||||
describe "when domain is set" do
|
||||
for {site_domain, expected_lookup_domain} <- [
|
||||
{"plausible.io", ~c"plausible.io"},
|
||||
{"www.plausible.io", ~c"www.plausible.io"},
|
||||
{"plausible.io/sites", ~c"plausible.io"}
|
||||
] do
|
||||
test "guesses 'https://#{site_domain}' if A-record is found for '#{site_domain}'" do
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, fn unquote(expected_lookup_domain), _type, _record, _opts, _timeout ->
|
||||
[{192, 168, 1, 1}]
|
||||
end)
|
||||
|
||||
state =
|
||||
@check.perform(%State{
|
||||
data_domain: unquote(site_domain),
|
||||
url: nil,
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
})
|
||||
|
||||
assert state.url == "https://#{unquote(site_domain)}"
|
||||
refute state.diagnostics.service_error
|
||||
refute state.skip_further_checks?
|
||||
end
|
||||
end
|
||||
|
||||
test "guesses 'www.{domain}' if A record is not found for 'domain'" do
|
||||
site_domain = "example.com/any/deeper/path"
|
||||
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, fn ~c"example.com", _type, _record, _opts, _timeout ->
|
||||
[]
|
||||
end)
|
||||
|> expect(:lookup, fn ~c"www.example.com", _type, _record, _opts, _timeout ->
|
||||
[{192, 168, 1, 2}]
|
||||
end)
|
||||
|
||||
state =
|
||||
@check.perform(%State{
|
||||
data_domain: site_domain,
|
||||
url: nil,
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
})
|
||||
|
||||
assert state.url == "https://www.example.com/any/deeper/path"
|
||||
refute state.diagnostics.service_error
|
||||
refute state.skip_further_checks?
|
||||
end
|
||||
|
||||
test "fails if no A-record is found for 'domain' or 'www.{domain}'" do
|
||||
expected_lookups = 2
|
||||
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, expected_lookups, fn _domain, _type, _record, _opts, _timeout ->
|
||||
[]
|
||||
end)
|
||||
|
||||
domain = "any.example.com"
|
||||
|
||||
state =
|
||||
@check.perform(%State{
|
||||
data_domain: domain,
|
||||
url: nil,
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
})
|
||||
|
||||
assert state.url == nil
|
||||
assert state.diagnostics.service_error == :domain_not_found
|
||||
assert state.skip_further_checks?
|
||||
end
|
||||
end
|
||||
|
||||
describe "when url is set" do
|
||||
test "for legitimate urls on domains that have an A-record, strips query and fragment" do
|
||||
site_domain = "example-com-rollup"
|
||||
url = "https://blog.example.com/recipes?foo=bar#baz"
|
||||
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, fn ~c"blog.example.com", _type, _record, _opts, _timeout ->
|
||||
[{192, 168, 1, 1}]
|
||||
end)
|
||||
|
||||
state =
|
||||
@check.perform(%State{
|
||||
data_domain: site_domain,
|
||||
url: url,
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
})
|
||||
|
||||
assert state.url == "https://blog.example.com/recipes"
|
||||
refute state.diagnostics.service_error
|
||||
refute state.skip_further_checks?
|
||||
end
|
||||
|
||||
for scheme <- ["http", "file"] do
|
||||
test "rejects not-https scheme '#{scheme}', does not check domain" do
|
||||
state =
|
||||
@check.perform(%State{
|
||||
data_domain: "example-com-rollup",
|
||||
url: "#{unquote(scheme)}://example.com/archives/news?p=any#fragment",
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
})
|
||||
|
||||
assert state.url == "#{unquote(scheme)}://example.com/archives/news?p=any#fragment"
|
||||
assert state.diagnostics.service_error == :invalid_url
|
||||
assert state.skip_further_checks?
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects invalid urls" do
|
||||
site_domain = "example-com-rollup"
|
||||
url = "https://example.com/archives/news?p=any#fragment"
|
||||
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, fn ~c"example.com", _type, _record, _opts, _timeout ->
|
||||
[]
|
||||
end)
|
||||
|
||||
state =
|
||||
@check.perform(%State{
|
||||
data_domain: site_domain,
|
||||
url: url,
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
})
|
||||
|
||||
assert state.url == url
|
||||
assert state.diagnostics.service_error == :domain_not_found
|
||||
assert state.skip_further_checks?
|
||||
end
|
||||
end
|
||||
|
||||
test "reports progress correctly" do
|
||||
assert @check.report_progress_as() ==
|
||||
"We're trying to reach your website"
|
||||
end
|
||||
end
|
||||
|
|
@ -3,12 +3,23 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
import Phoenix.LiveViewTest
|
||||
import Plausible.TestUtils
|
||||
import ExUnit.CaptureLog
|
||||
import Mox
|
||||
|
||||
alias Plausible.Repo
|
||||
|
||||
describe "ChangeDomainV2 LiveView" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
setup do
|
||||
# mock all domains resolve
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, fn _domain, _type, _record, _opts, _timeout ->
|
||||
[{192, 168, 1, 2}]
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "mounts and renders form", %{conn: conn, site: site} do
|
||||
{:ok, _lv, html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
|
||||
|
|
@ -241,7 +252,10 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
Req.Test.stub(:global, fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(%{"data" => %{"error" => "Simulated browser error"}}))
|
||||
|> send_resp(
|
||||
200,
|
||||
Jason.encode!(%{"data" => %{"error" => %{"message" => "Simulated browser error"}}})
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
import Plausible.Teams.Test
|
||||
import Mox
|
||||
|
||||
alias Plausible.Site.TrackerScriptConfiguration
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
|
||||
describe "LiveView" do
|
||||
test "detects installation type when mounted", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
|
@ -32,10 +34,24 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
assert text(html) =~ "Verify WordPress installation"
|
||||
end
|
||||
|
||||
test "When ?type URL parameter is supplied, detected type is unused", %{
|
||||
test "When ?type=wordpress URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=wordpress")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify WordPress installation"
|
||||
end
|
||||
|
||||
test "When ?type=gtm URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=gtm")
|
||||
|
|
@ -44,7 +60,34 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
assert text(html) =~ "Verify Tag Manager installation"
|
||||
end
|
||||
|
||||
test "When ?type=npm URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=npm")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify NPM installation"
|
||||
end
|
||||
|
||||
test "When ?type=manual URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify Script installation"
|
||||
end
|
||||
|
||||
test "allows switching between installation tabs", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
|
|
@ -74,6 +117,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
end
|
||||
|
||||
test "manual installations has script snippet with expected ID", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
|
|
@ -90,6 +134,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
end
|
||||
|
||||
test "manual installation shows optional measurements", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
|
|
@ -102,6 +147,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
end
|
||||
|
||||
test "manual installation shows advanced options in disclosure", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
|
|
@ -119,6 +165,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
|
|
@ -147,31 +194,40 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
assert updated_config.form_submissions == true
|
||||
end
|
||||
|
||||
test "submitting form redirects to verification", %{conn: conn, site: site} do
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual")
|
||||
for {type, expected_text} <- [
|
||||
{"manual", "Verify Script installation"},
|
||||
{"wordpress", "Verify WordPress installation"},
|
||||
{"gtm", "Verify Tag Manager installation"},
|
||||
{"npm", "Verify NPM installation"}
|
||||
] do
|
||||
test "submitting form with #{type} redirects to verification", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ unquote(expected_text)
|
||||
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => "manual",
|
||||
"outbound_links" => "true",
|
||||
"file_downloads" => "true",
|
||||
"form_submissions" => "true"
|
||||
}
|
||||
})
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => unquote(type),
|
||||
"outbound_links" => "true",
|
||||
"file_downloads" => "true",
|
||||
"form_submissions" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
assert_redirect(
|
||||
lv,
|
||||
Routes.site_path(conn, :verification, site.domain, flow: "provisioning")
|
||||
)
|
||||
assert_redirect(
|
||||
lv,
|
||||
Routes.site_path(conn, :verification, site.domain, flow: "provisioning")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "404 goal gets created regardless of user options", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
|
|
@ -199,6 +255,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
|
|
@ -220,6 +277,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
end
|
||||
|
||||
test "detected WordPress installation shows special message", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
|
@ -229,6 +287,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
end
|
||||
|
||||
test "detected GTM installation shows special message", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_gtm()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
|
@ -239,6 +298,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
end
|
||||
|
||||
test "shows v1 detection warning for manual installation", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual_with_v1()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=manual")
|
||||
|
|
@ -251,6 +311,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress_with_v1()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=wordpress")
|
||||
|
|
@ -260,7 +321,28 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
refute text(html) =~ "Your website is running an outdated version of the tracking script"
|
||||
end
|
||||
|
||||
test "falls back to manual installation when detection fails", %{conn: conn, site: site} do
|
||||
test "falls back to manual installation when detection fails at dns check level", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain, [])
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
# Should default to manual installation when detection returns {:error, _}
|
||||
{html =~ "Verify Script installation", html}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "falls back to manual installation when dns succeeds but detection fails", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_error()
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
|
|
@ -286,6 +368,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
test "allows viewer access to installation page", %{conn: conn, user: user} do
|
||||
site = new_site()
|
||||
add_guest(site, user: user, role: :viewer)
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
|
@ -297,6 +380,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
test "allows editor access to installation page", %{conn: conn, user: user} do
|
||||
site = new_site()
|
||||
add_guest(site, user: user, role: :editor)
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
|
@ -311,6 +395,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=invalid")
|
||||
|
|
@ -323,6 +408,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?flow=invalid")
|
||||
|
|
@ -334,6 +420,8 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
|
||||
describe "Detection Result Combinations" do
|
||||
test "When GTM + Wordpress detected, GTM takes precedence", %{conn: conn, site: site} do
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => false,
|
||||
"gtmLikely" => true,
|
||||
|
|
@ -361,6 +449,7 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
form_submissions: true
|
||||
})
|
||||
|
||||
stub_dns_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?flow=review")
|
||||
|
|
@ -427,7 +516,10 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
Req.Test.stub(:global, fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(%{"data" => %{"error" => "Simulated browser error"}}))
|
||||
|> send_resp(
|
||||
200,
|
||||
Jason.encode!(%{"data" => %{"error" => %{"message" => "Simulated browser error"}}})
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -436,4 +528,13 @@ defmodule PlausibleWeb.Live.InstallationV2Test do
|
|||
|
||||
{lv, html}
|
||||
end
|
||||
|
||||
defp stub_dns_lookup_a_records(domain, a_records \\ [{192, 168, 1, 1}]) do
|
||||
lookup_domain = to_charlist(domain)
|
||||
|
||||
Plausible.DnsLookup.Mock
|
||||
|> expect(:lookup, fn ^lookup_domain, _type, _record, _opts, _timeout ->
|
||||
a_records
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ end
|
|||
|
||||
{:ok, _} = Application.ensure_all_started(:ex_machina)
|
||||
Mox.defmock(Plausible.HTTPClient.Mock, for: Plausible.HTTPClient.Interface)
|
||||
|
||||
Mox.defmock(Plausible.DnsLookup.Mock,
|
||||
for: Plausible.DnsLookupInterface
|
||||
)
|
||||
|
||||
Application.ensure_all_started(:double)
|
||||
|
||||
FunWithFlags.enable(:channels)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { waitForPlausibleFunction } from "./plausible-function-check"
|
|||
import { checkWordPress } from "./check-wordpress"
|
||||
import { checkGTM } from "./check-gtm"
|
||||
|
||||
window.scanPageBeforePlausibleInstallation = async function(detectV1, debug) {
|
||||
window.scanPageBeforePlausibleInstallation = async function({ detectV1, debug, timeoutMs }) {
|
||||
function log(message) {
|
||||
if (debug) console.log('[Plausible Verification]', message)
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ window.scanPageBeforePlausibleInstallation = async function(detectV1, debug) {
|
|||
|
||||
if (detectV1) {
|
||||
log('Waiting for Plausible function...')
|
||||
const plausibleFound = await waitForPlausibleFunction(3000)
|
||||
const plausibleFound = await waitForPlausibleFunction(timeoutMs)
|
||||
log(`plausibleFound: ${plausibleFound}`)
|
||||
v1Detected = plausibleFound && typeof window.plausible.s === 'undefined'
|
||||
log(`v1Detected: ${v1Detected}`)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ test.describe('detector.js (tech recognition)', () => {
|
|||
`
|
||||
})
|
||||
|
||||
const result = await detect(page, {url: url, detectV1: false})
|
||||
const result = await detect(page, {url: url, detectV1: false, timeoutMs: 1000})
|
||||
|
||||
expect(result.data.v1Detected).toBe(null)
|
||||
})
|
||||
|
|
@ -34,7 +34,7 @@ test.describe('detector.js (tech recognition)', () => {
|
|||
`
|
||||
})
|
||||
|
||||
const result = await detect(page, {url: url, detectV1: false})
|
||||
const result = await detect(page, {url: url, detectV1: false, timeoutMs: 1000})
|
||||
|
||||
expect(result.data.wordpressPlugin).toBe(true)
|
||||
expect(result.data.wordpressLikely).toBe(true)
|
||||
|
|
@ -47,7 +47,7 @@ test.describe('detector.js (tech recognition)', () => {
|
|||
response: '<html><head></head></html>'
|
||||
})
|
||||
|
||||
const result = await detect(page, {url: url, detectV1: false})
|
||||
const result = await detect(page, {url: url, detectV1: false, timeoutMs: 1000})
|
||||
|
||||
expect(result.data.wordpressPlugin).toBe(false)
|
||||
expect(result.data.wordpressLikely).toBe(false)
|
||||
|
|
@ -71,7 +71,7 @@ test.describe('detector.js (v1 detection)', () => {
|
|||
`
|
||||
})
|
||||
|
||||
const result = await detect(page, {url: url, detectV1: true})
|
||||
const result = await detect(page, {url: url, detectV1: true, timeoutMs: 1000})
|
||||
|
||||
expect(result.data.v1Detected).toBe(true)
|
||||
expect(result.data.wordpressPlugin).toBe(true)
|
||||
|
|
@ -85,7 +85,7 @@ test.describe('detector.js (v1 detection)', () => {
|
|||
response: '<html><head></head></html>'
|
||||
})
|
||||
|
||||
const result = await detect(page, {url: url, detectV1: true})
|
||||
const result = await detect(page, {url: url, detectV1: true, timeoutMs: 1000})
|
||||
|
||||
expect(result.data.v1Detected).toBe(false)
|
||||
expect(result.data.wordpressPlugin).toBe(false)
|
||||
|
|
@ -102,7 +102,7 @@ test.describe('detector.js (v1 detection)', () => {
|
|||
}
|
||||
})
|
||||
|
||||
const result = await detect(page, {url: url, detectV1: true})
|
||||
const result = await detect(page, {url: url, detectV1: true, timeoutMs: 1000})
|
||||
|
||||
expect(result.data.v1Detected).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export async function verifyV1(page, context) {
|
|||
}
|
||||
|
||||
export async function detect(page, context) {
|
||||
const { url, detectV1 } = context
|
||||
const { url, detectV1, timeoutMs } = context
|
||||
const debug = context.debug ? true : false
|
||||
|
||||
const detectorCode = await compileFile(DETECTOR_JS_VARIANT, {
|
||||
|
|
@ -99,12 +99,9 @@ export async function detect(page, context) {
|
|||
await page.evaluate(detectorCode)
|
||||
|
||||
return await page.evaluate(
|
||||
async ({ detectV1, debug }) => {
|
||||
return await (window as any).scanPageBeforePlausibleInstallation(
|
||||
detectV1,
|
||||
debug
|
||||
)
|
||||
async (d) => {
|
||||
return await (window as any).scanPageBeforePlausibleInstallation(d)
|
||||
},
|
||||
{ detectV1, debug }
|
||||
{ detectV1, debug, timeoutMs }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue