Script v2: Allow verifying that tracker installed correctly (1st iteration) (#5572)
* Sketch out verification for v2 installs * WIP * Fix naming * Implement CSP check, refactor what test event output looks like * Update error matcher * Better typedefs * Unify error format * WIP diagnostics * Delete superfluos doc * Remove prettierrc * Fix type * Remove superfluous error file * Remove foobar.md * Fix format and variable names, ensure compliance for errors * Add cache bust check, fix success diagnostics * Fix v2 verifier spec and add moduledocs * Make test not dependent on tracker script version * Make verifier less CPU intensive * Change the signature of checkDisallowedByCSP * Fix unused service_errror matcher * Refactor data_domain to expected_domain * Ignore request URL * Add case for for succeeding after cache bust * Fix infinite recursion * Relax CSP error interpretation * Fix sentry message, ignore plausible.s
This commit is contained in:
parent
cd875eee96
commit
bb17a17e5a
|
|
@ -0,0 +1,127 @@
|
||||||
|
defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
path = Application.app_dir(:plausible, "priv/tracker/installation_support/verifier-v2.js")
|
||||||
|
# On CI, the file might not be present for static checks so we create an empty one
|
||||||
|
File.touch!(path)
|
||||||
|
|
||||||
|
@verifier_code File.read!(path)
|
||||||
|
@external_resource "priv/tracker/installation_support/verifier-v2.js"
|
||||||
|
|
||||||
|
@function_check_timeout 10_000
|
||||||
|
|
||||||
|
# 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);
|
||||||
|
const response = await page.goto(context.url);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
#{@verifier_code}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await page.evaluate(async ({ responseHeaders, debug, timeoutMs, cspHostToCheck }) => {
|
||||||
|
return await window.verifyPlausibleInstallation({ responseHeaders, debug, timeoutMs, cspHostToCheck });
|
||||||
|
}, {
|
||||||
|
timeoutMs: context.timeoutMs,
|
||||||
|
responseHeaders: response.headers(),
|
||||||
|
debug: context.debug,
|
||||||
|
cspHostToCheck: context.cspHostToCheck
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
completed: false,
|
||||||
|
error: {
|
||||||
|
message: error?.message ?? JSON.stringify(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def report_progress_as, do: "We're verifying that your visitors are being counted correctly"
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def perform(%State{url: url} = state) do
|
||||||
|
opts = [
|
||||||
|
headers: %{content_type: "application/json"},
|
||||||
|
body:
|
||||||
|
JSON.encode!(%{
|
||||||
|
code: @puppeteer_wrapper_code,
|
||||||
|
context: %{
|
||||||
|
cspHostToCheck: PlausibleWeb.Endpoint.host(),
|
||||||
|
timeoutMs: @function_check_timeout,
|
||||||
|
url: Plausible.InstallationSupport.URL.bust_url(url),
|
||||||
|
userAgent: Plausible.InstallationSupport.user_agent(),
|
||||||
|
debug: Application.get_env(:plausible, :environment) == "dev"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
retry: :transient,
|
||||||
|
retry_log_level: :warning,
|
||||||
|
max_retries: 2
|
||||||
|
]
|
||||||
|
|
||||||
|
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
|
||||||
|
{: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,
|
||||||
|
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"],
|
||||||
|
service_error: nil
|
||||||
|
)
|
||||||
|
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
|
||||||
|
"[VERIFICATION v2] #{message} (data_domain='#{state.data_domain}')"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
defmodule Plausible.InstallationSupport.Checks.InstallationV2CacheBust do
|
||||||
|
@moduledoc """
|
||||||
|
If the output of previous checks can not be interpreted as successful,
|
||||||
|
as a last resort, we try to bust the cache of the site under test by adding a query parameter to the URL,
|
||||||
|
and running InstallationV2 again.
|
||||||
|
|
||||||
|
Whatever the result from the rerun, that is what we use to interpret the installation.
|
||||||
|
|
||||||
|
The idea is to make sure that any issues we detect will be about the latest version of their website.
|
||||||
|
|
||||||
|
We also want to avoid reporting a successful installation if it took a special cache-busting action to make it work.
|
||||||
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
alias Plausible.InstallationSupport
|
||||||
|
use Plausible.InstallationSupport.Check
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def report_progress_as, do: "We're verifying that your visitors are being counted correctly"
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def perform(%State{url: url} = state) do
|
||||||
|
if InstallationSupport.Verification.Checks.interpret_diagnostics(state) ==
|
||||||
|
%InstallationSupport.Result{ok?: true} do
|
||||||
|
state
|
||||||
|
else
|
||||||
|
url_that_maybe_busts_cache =
|
||||||
|
Plausible.InstallationSupport.URL.bust_url(url)
|
||||||
|
|
||||||
|
state_after_cache_bust =
|
||||||
|
Plausible.InstallationSupport.Checks.InstallationV2.perform(%{
|
||||||
|
state
|
||||||
|
| url: url_that_maybe_busts_cache
|
||||||
|
})
|
||||||
|
|
||||||
|
put_diagnostics(state_after_cache_bust, diagnostics_are_from_cache_bust: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -25,14 +25,7 @@ defmodule Plausible.InstallationSupport.LegacyVerification.Diagnostics do
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
defmodule Result do
|
alias Plausible.InstallationSupport.Result
|
||||||
@moduledoc """
|
|
||||||
Diagnostics interpretation result.
|
|
||||||
"""
|
|
||||||
defstruct ok?: false, errors: [], recommendations: []
|
|
||||||
@type t :: %__MODULE__{}
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec interpret(t(), String.t()) :: Result.t()
|
@spec interpret(t(), String.t()) :: Result.t()
|
||||||
def interpret(
|
def interpret(
|
||||||
%__MODULE__{
|
%__MODULE__{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule Plausible.InstallationSupport.Result do
|
||||||
|
@moduledoc """
|
||||||
|
Diagnostics interpretation result.
|
||||||
|
"""
|
||||||
|
defstruct ok?: false, errors: [], recommendations: []
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
end
|
||||||
|
|
@ -12,7 +12,9 @@ defmodule Plausible.InstallationSupport.State do
|
||||||
assigns: %{},
|
assigns: %{},
|
||||||
diagnostics: %{}
|
diagnostics: %{}
|
||||||
|
|
||||||
@type diagnostics_type :: Plausible.InstallationSupport.LegacyVerification.Diagnostics.t()
|
@type diagnostics_type ::
|
||||||
|
Plausible.InstallationSupport.LegacyVerification.Diagnostics.t()
|
||||||
|
| Plausible.InstallationSupport.Verification.Diagnostics.t()
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
url: String.t() | nil,
|
url: String.t() | nil,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
defmodule Plausible.InstallationSupport.Verification.Checks do
|
||||||
|
@moduledoc """
|
||||||
|
Checks that are performed during tracker script installation verification.
|
||||||
|
|
||||||
|
In async execution, each check notifies the caller by sending a message to it.
|
||||||
|
"""
|
||||||
|
alias Plausible.InstallationSupport.Verification
|
||||||
|
alias Plausible.InstallationSupport.{State, CheckRunner, Checks}
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@checks [
|
||||||
|
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)
|
||||||
|
|
||||||
|
init_state =
|
||||||
|
%State{
|
||||||
|
url: url,
|
||||||
|
data_domain: data_domain,
|
||||||
|
report_to: report_to,
|
||||||
|
diagnostics: %Verification.Diagnostics{
|
||||||
|
selected_installation_type: installation_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckRunner.run(init_state, checks,
|
||||||
|
async?: async?,
|
||||||
|
report_to: report_to,
|
||||||
|
slowdown: slowdown
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def interpret_diagnostics(%State{} = state) do
|
||||||
|
Verification.Diagnostics.interpret(
|
||||||
|
state.diagnostics,
|
||||||
|
state.data_domain,
|
||||||
|
state.url
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
defmodule Plausible.InstallationSupport.Verification.Diagnostics do
|
||||||
|
@moduledoc """
|
||||||
|
Module responsible for translating diagnostics to user-friendly errors and recommendations.
|
||||||
|
"""
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
# in this struct, nil means indeterminate
|
||||||
|
defstruct selected_installation_type: nil,
|
||||||
|
disallowed_by_csp: nil,
|
||||||
|
plausible_is_on_window: nil,
|
||||||
|
plausible_is_initialized: nil,
|
||||||
|
plausible_version: nil,
|
||||||
|
plausible_variant: nil,
|
||||||
|
diagnostics_are_from_cache_bust: nil,
|
||||||
|
test_event: nil,
|
||||||
|
cookie_banner_likely: nil,
|
||||||
|
service_error: nil
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
alias Plausible.InstallationSupport.Result
|
||||||
|
|
||||||
|
defmodule Error do
|
||||||
|
@moduledoc """
|
||||||
|
Error that has compile-time enforced checks for the attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enforce_keys [:message, :recommendation]
|
||||||
|
defstruct [:message, :recommendation, :url]
|
||||||
|
|
||||||
|
def new!(attrs) do
|
||||||
|
message = Map.fetch!(attrs, :message)
|
||||||
|
|
||||||
|
if String.ends_with?(message, ".") do
|
||||||
|
raise ArgumentError, "Error message must not end with a period: #{inspect(message)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if String.ends_with?(attrs[:recommendation], ".") do
|
||||||
|
raise ArgumentError,
|
||||||
|
"Error recommendation must not end with a period: #{inspect(attrs[:recommendation])}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_binary(attrs[:url]) and not String.starts_with?(attrs[:url], "https://plausible.io") do
|
||||||
|
raise ArgumentError,
|
||||||
|
"Recommendation url must start with 'https://plausible.io': #{inspect(attrs[:url])}"
|
||||||
|
end
|
||||||
|
|
||||||
|
struct!(__MODULE__, attrs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@error_unexpected_domain Error.new!(%{
|
||||||
|
message: "Plausible test event is not for this site",
|
||||||
|
recommendation:
|
||||||
|
"Please check that the snippet on your site matches the installation instructions exactly",
|
||||||
|
url:
|
||||||
|
"https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
|
||||||
|
})
|
||||||
|
@error_succeeds_only_after_cache_bust Error.new!(%{
|
||||||
|
message: "We detected an issue with your site's cache",
|
||||||
|
recommendation:
|
||||||
|
"Please clear the cache for your site to ensure that your visitors will load the latest version of your site that has Plausible correctly installed",
|
||||||
|
url:
|
||||||
|
"https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site"
|
||||||
|
})
|
||||||
|
@spec interpret(t(), String.t(), String.t()) :: Result.t()
|
||||||
|
def interpret(
|
||||||
|
%__MODULE__{
|
||||||
|
plausible_is_on_window: true,
|
||||||
|
plausible_is_initialized: true,
|
||||||
|
test_event: %{
|
||||||
|
"normalizedBody" => %{
|
||||||
|
"domain" => domain
|
||||||
|
},
|
||||||
|
"responseStatus" => response_status
|
||||||
|
},
|
||||||
|
service_error: nil,
|
||||||
|
diagnostics_are_from_cache_bust: diagnostics_are_from_cache_bust
|
||||||
|
} = diagnostics,
|
||||||
|
expected_domain,
|
||||||
|
url
|
||||||
|
)
|
||||||
|
when response_status in [200, 202] do
|
||||||
|
domain_is_expected? = domain == expected_domain
|
||||||
|
|
||||||
|
cond do
|
||||||
|
domain_is_expected? and diagnostics_are_from_cache_bust ->
|
||||||
|
error(@error_succeeds_only_after_cache_bust)
|
||||||
|
|
||||||
|
domain_is_expected? ->
|
||||||
|
success()
|
||||||
|
|
||||||
|
not domain_is_expected? ->
|
||||||
|
error(@error_unexpected_domain)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
unknown_error(diagnostics, url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@error_csp_disallowed Error.new!(%{
|
||||||
|
message:
|
||||||
|
"We encountered an issue with your site's Content Security Policy (CSP)",
|
||||||
|
recommendation:
|
||||||
|
"Please add plausible.io domain specifically to the allowed list of domains in your site's CSP",
|
||||||
|
url:
|
||||||
|
"https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp"
|
||||||
|
})
|
||||||
|
def interpret(
|
||||||
|
%__MODULE__{
|
||||||
|
disallowed_by_csp: true,
|
||||||
|
service_error: nil
|
||||||
|
},
|
||||||
|
_expected_domain,
|
||||||
|
_url
|
||||||
|
) do
|
||||||
|
error(@error_csp_disallowed)
|
||||||
|
end
|
||||||
|
|
||||||
|
def interpret(%__MODULE__{} = diagnostics, _expected_domain, url),
|
||||||
|
do: unknown_error(diagnostics, url)
|
||||||
|
|
||||||
|
defp success() do
|
||||||
|
%Result{ok?: true}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp error(%Error{} = error) do
|
||||||
|
%Result{
|
||||||
|
ok?: false,
|
||||||
|
errors: [error.message],
|
||||||
|
recommendations: [%{text: error.recommendation, url: error.url}]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@unknown_error Error.new!(%{
|
||||||
|
message: "Your Plausible integration is not working",
|
||||||
|
recommendation:
|
||||||
|
"Please manually check your integration to make sure that the Plausible snippet has been inserted correctly",
|
||||||
|
url:
|
||||||
|
"https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
|
||||||
|
})
|
||||||
|
defp unknown_error(diagnostics, url) do
|
||||||
|
Sentry.capture_message("Unhandled case for site verification (v2)",
|
||||||
|
extra: %{
|
||||||
|
message: inspect(diagnostics),
|
||||||
|
url: url,
|
||||||
|
hash: :erlang.phash2(diagnostics)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
error(@unknown_error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||||
use Plausible
|
use Plausible
|
||||||
|
|
||||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||||
alias Plausible.InstallationSupport.{State, LegacyVerification}
|
alias Plausible.InstallationSupport.{State, Result}
|
||||||
|
|
||||||
import PlausibleWeb.Components.Generic
|
import PlausibleWeb.Components.Generic
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||||
attr(:finished?, :boolean, default: false)
|
attr(:finished?, :boolean, default: false)
|
||||||
attr(:success?, :boolean, default: false)
|
attr(:success?, :boolean, default: false)
|
||||||
attr(:verification_state, State, default: nil)
|
attr(:verification_state, State, default: nil)
|
||||||
attr(:interpretation, LegacyVerification.Diagnostics.Result, default: nil)
|
attr(:interpretation, Result, default: nil)
|
||||||
attr(:attempts, :integer, default: 0)
|
attr(:attempts, :integer, default: 0)
|
||||||
attr(:flow, :string, default: "")
|
attr(:flow, :string, default: "")
|
||||||
attr(:installation_type, :string, default: nil)
|
attr(:installation_type, :string, default: nil)
|
||||||
|
|
@ -146,7 +146,8 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||||
<.focus_list>
|
<.focus_list>
|
||||||
<:item :for={{diag, value} <- Map.from_struct(@verification_state.diagnostics)}>
|
<:item :for={{diag, value} <- Map.from_struct(@verification_state.diagnostics)}>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{Phoenix.Naming.humanize(diag)}: <span class="font-mono">{value}</span>
|
{Phoenix.Naming.humanize(diag)}:
|
||||||
|
<span class="font-mono">{to_string_value(value)}</span>
|
||||||
</span>
|
</span>
|
||||||
</:item>
|
</:item>
|
||||||
</.focus_list>
|
</.focus_list>
|
||||||
|
|
@ -157,4 +158,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp to_string_value(value) when is_binary(value), do: value
|
||||||
|
defp to_string_value(value), do: inspect(value)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule PlausibleWeb.Live.Verification do
|
||||||
use Plausible
|
use Plausible
|
||||||
use PlausibleWeb, :live_view
|
use PlausibleWeb, :live_view
|
||||||
|
|
||||||
alias Plausible.InstallationSupport.{State, LegacyVerification}
|
alias Plausible.InstallationSupport.{State, LegacyVerification, Verification}
|
||||||
|
|
||||||
@component PlausibleWeb.Live.Components.Verification
|
@component PlausibleWeb.Live.Components.Verification
|
||||||
@slowdown_for_frequent_checking :timer.seconds(5)
|
@slowdown_for_frequent_checking :timer.seconds(5)
|
||||||
|
|
@ -131,12 +131,24 @@ defmodule PlausibleWeb.Live.Verification do
|
||||||
{:deny, _} -> :timer.sleep(@slowdown_for_frequent_checking)
|
{:deny, _} -> :timer.sleep(@slowdown_for_frequent_checking)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
url_to_verify = "https://#{socket.assigns.domain}"
|
||||||
|
domain = socket.assigns.domain
|
||||||
|
installation_type = socket.assigns.installation_type
|
||||||
|
|
||||||
{:ok, pid} =
|
{:ok, pid} =
|
||||||
LegacyVerification.Checks.run(
|
if(FunWithFlags.enabled?(:scriptv2, for: socket.assigns.site),
|
||||||
"https://#{socket.assigns.domain}",
|
do:
|
||||||
socket.assigns.domain,
|
Verification.Checks.run(url_to_verify, domain, installation_type,
|
||||||
report_to: report_to,
|
report_to: report_to,
|
||||||
slowdown: socket.assigns.slowdown
|
slowdown: socket.assigns.slowdown
|
||||||
|
),
|
||||||
|
else:
|
||||||
|
LegacyVerification.Checks.run(
|
||||||
|
url_to_verify,
|
||||||
|
domain,
|
||||||
|
report_to: report_to,
|
||||||
|
slowdown: socket.assigns.slowdown
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
{:noreply, assign(socket, checks_pid: pid, attempts: socket.assigns.attempts + 1)}
|
{:noreply, assign(socket, checks_pid: pid, attempts: socket.assigns.attempts + 1)}
|
||||||
|
|
@ -152,7 +164,11 @@ defmodule PlausibleWeb.Live.Verification do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:all_checks_done, %State{} = state}, socket) do
|
def handle_info({:all_checks_done, %State{} = state}, socket) do
|
||||||
interpretation = LegacyVerification.Checks.interpret_diagnostics(state)
|
interpretation =
|
||||||
|
if(FunWithFlags.enabled?(:scriptv2, for: socket.assigns.site),
|
||||||
|
do: Verification.Checks.interpret_diagnostics(state),
|
||||||
|
else: LegacyVerification.Checks.interpret_diagnostics(state)
|
||||||
|
)
|
||||||
|
|
||||||
if not socket.assigns.has_pageviews? do
|
if not socket.assigns.has_pageviews? do
|
||||||
schedule_pageviews_check(socket)
|
schedule_pageviews_check(socket)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@
|
||||||
"entry_point": "installation_support/verifier-v1.js",
|
"entry_point": "installation_support/verifier-v1.js",
|
||||||
"output_path": "priv/tracker/installation_support/verifier-v1.js",
|
"output_path": "priv/tracker/installation_support/verifier-v1.js",
|
||||||
"globals": {}
|
"globals": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "verifier-v2.js",
|
||||||
|
"entry_point": "installation_support/verifier-v2.js",
|
||||||
|
"output_path": "priv/tracker/installation_support/verifier-v2.js",
|
||||||
|
"globals": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"legacyVariants": [
|
"legacyVariants": [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Checks if the CSP policy disallows the given host/domain.
|
||||||
|
* @param {Record<string, string>} responseHeaders - Response headers with keys normalized to lowercase like { "x-foo": "bar" }
|
||||||
|
* @param {string} hostToCheck - Domain/host to check. Must be provided.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function checkDisallowedByCSP(responseHeaders, hostToCheck) {
|
||||||
|
if (!hostToCheck || typeof hostToCheck !== 'string') {
|
||||||
|
throw new Error('hostToCheck must be a non-empty string')
|
||||||
|
}
|
||||||
|
const policy = responseHeaders?.['content-security-policy']
|
||||||
|
if (!policy) return false
|
||||||
|
|
||||||
|
const directives = policy.split(';')
|
||||||
|
|
||||||
|
const allowed = directives.some((directive) => {
|
||||||
|
const d = directive.trim()
|
||||||
|
// Check for the provided host/domain
|
||||||
|
return d.includes(hostToCheck)
|
||||||
|
})
|
||||||
|
|
||||||
|
return !allowed
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
/** @typedef {import('../test/support/types').VerifyV2Args} VerifyV2Args */
|
||||||
|
/** @typedef {import('../test/support/types').VerifyV2Result} VerifyV2Result */
|
||||||
|
import { checkCookieBanner } from './check-cookie-banner'
|
||||||
|
import { checkDisallowedByCSP } from './check-disallowed-by-csp'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that verifies if Plausible is installed correctly.
|
||||||
|
* @param {VerifyV2Args}
|
||||||
|
* @returns {Promise<VerifyV2Result>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function verifyPlausibleInstallation({
|
||||||
|
timeoutMs,
|
||||||
|
responseHeaders,
|
||||||
|
debug,
|
||||||
|
cspHostToCheck
|
||||||
|
}) {
|
||||||
|
function log(message) {
|
||||||
|
if (debug) console.log('[VERIFICATION v2]', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const disallowedByCsp = checkDisallowedByCSP(responseHeaders, cspHostToCheck)
|
||||||
|
|
||||||
|
const { stopRecording, getInterceptedFetch } = startRecordingEventFetchCalls()
|
||||||
|
|
||||||
|
const {
|
||||||
|
plausibleIsInitialized,
|
||||||
|
plausibleIsOnWindow,
|
||||||
|
plausibleVersion,
|
||||||
|
plausibleVariant,
|
||||||
|
testEvent,
|
||||||
|
error: testPlausibleFunctionError
|
||||||
|
} = await testPlausibleFunction({
|
||||||
|
timeoutMs
|
||||||
|
})
|
||||||
|
|
||||||
|
if (testPlausibleFunctionError) {
|
||||||
|
log(
|
||||||
|
`There was an error testing plausible function: ${testPlausibleFunctionError}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRecording()
|
||||||
|
|
||||||
|
const interceptedTestEvent = getInterceptedFetch('verification-agent-test')
|
||||||
|
|
||||||
|
if (!interceptedTestEvent) {
|
||||||
|
log(`No test event request was among intercepted requests`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagnostics = {
|
||||||
|
disallowedByCsp,
|
||||||
|
plausibleIsOnWindow,
|
||||||
|
plausibleIsInitialized,
|
||||||
|
plausibleVersion,
|
||||||
|
plausibleVariant,
|
||||||
|
testEvent: {
|
||||||
|
...testEvent,
|
||||||
|
requestUrl: interceptedTestEvent?.request?.url,
|
||||||
|
normalizedBody: interceptedTestEvent?.request?.normalizedBody,
|
||||||
|
responseStatus: interceptedTestEvent?.response?.status,
|
||||||
|
error: interceptedTestEvent?.error
|
||||||
|
},
|
||||||
|
cookieBannerLikely: checkCookieBanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
log({
|
||||||
|
diagnostics
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
...diagnostics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNormalizedPlausibleEventBody(fetchOptions) {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(fetchOptions.body ?? '{}')
|
||||||
|
|
||||||
|
let name = null
|
||||||
|
let domain = null
|
||||||
|
let version = null
|
||||||
|
|
||||||
|
if (
|
||||||
|
fetchOptions.method === 'POST' &&
|
||||||
|
(typeof body?.n === 'string' || typeof body?.name === 'string') &&
|
||||||
|
(typeof body?.d === 'string' || typeof body?.domain === 'string')
|
||||||
|
) {
|
||||||
|
name = body?.n || body?.name
|
||||||
|
domain = body?.d || body?.domain
|
||||||
|
version = body?.v || body?.version
|
||||||
|
}
|
||||||
|
return name && domain ? { name, domain, version } : null
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRecordingEventFetchCalls() {
|
||||||
|
const interceptions = new Map()
|
||||||
|
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
window.fetch = function (url, options = {}) {
|
||||||
|
let identifier = null
|
||||||
|
|
||||||
|
const normalizedEventBody = getNormalizedPlausibleEventBody(options)
|
||||||
|
if (normalizedEventBody) {
|
||||||
|
identifier = normalizedEventBody.name
|
||||||
|
interceptions.set(identifier, {
|
||||||
|
request: { url, normalizedBody: normalizedEventBody }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalFetch
|
||||||
|
.apply(this, arguments)
|
||||||
|
.then(async (response) => {
|
||||||
|
const eventRequest = interceptions.get(identifier)
|
||||||
|
if (eventRequest) {
|
||||||
|
const responseClone = response.clone()
|
||||||
|
const body = await responseClone.text()
|
||||||
|
eventRequest.response = { status: response.status, body }
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const eventRequest = interceptions.get(identifier)
|
||||||
|
if (eventRequest) {
|
||||||
|
eventRequest.error = {
|
||||||
|
message: error?.message || 'Unknown error during fetch'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getInterceptedFetch: (identifier) => interceptions.get(identifier),
|
||||||
|
stopRecording: () => {
|
||||||
|
window.fetch = originalFetch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlausibleOnWindow() {
|
||||||
|
return !!window.plausible
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlausibleInitialized() {
|
||||||
|
return window.plausible?.l
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlausibleVersion() {
|
||||||
|
return window.plausible?.v
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlausibleVariant() {
|
||||||
|
return window.plausible?.s
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPlausibleFunction({ timeoutMs }) {
|
||||||
|
return new Promise(async (_resolve) => {
|
||||||
|
let plausibleIsOnWindow = isPlausibleOnWindow()
|
||||||
|
let plausibleIsInitialized = isPlausibleInitialized()
|
||||||
|
let plausibleVersion = getPlausibleVersion()
|
||||||
|
let plausibleVariant = getPlausibleVariant()
|
||||||
|
let testEvent = {}
|
||||||
|
|
||||||
|
let resolved = false
|
||||||
|
|
||||||
|
function resolve(additionalData) {
|
||||||
|
resolved = true
|
||||||
|
_resolve({
|
||||||
|
plausibleIsInitialized,
|
||||||
|
plausibleIsOnWindow,
|
||||||
|
plausibleVersion,
|
||||||
|
plausibleVariant,
|
||||||
|
testEvent,
|
||||||
|
...additionalData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
error: 'Test Plausible function timeout exceeded'
|
||||||
|
})
|
||||||
|
}, timeoutMs)
|
||||||
|
|
||||||
|
while (!plausibleIsOnWindow) {
|
||||||
|
if (isPlausibleOnWindow()) {
|
||||||
|
plausibleIsOnWindow = true
|
||||||
|
}
|
||||||
|
await delay(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!plausibleIsInitialized) {
|
||||||
|
if (isPlausibleInitialized()) {
|
||||||
|
plausibleIsInitialized = true
|
||||||
|
plausibleVersion = getPlausibleVersion()
|
||||||
|
plausibleVariant = getPlausibleVariant()
|
||||||
|
}
|
||||||
|
await delay(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.plausible('verification-agent-test', {
|
||||||
|
callback: (testEventCallbackResult) => {
|
||||||
|
if (resolved) return
|
||||||
|
clearTimeout(timeout)
|
||||||
|
resolve({
|
||||||
|
testEvent: { callbackResult: testEventCallbackResult }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
window.verifyPlausibleInstallation = verifyPlausibleInstallation
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { checkDisallowedByCSP } from '../../installation_support/check-disallowed-by-csp'
|
||||||
|
|
||||||
|
const HOST_TO_CHECK = 'plausible.io'
|
||||||
|
|
||||||
|
test.describe('checkDisallowedByCSP', () => {
|
||||||
|
test('returns false if no CSP header', () => {
|
||||||
|
expect(checkDisallowedByCSP({}, HOST_TO_CHECK)).toBe(false)
|
||||||
|
expect(checkDisallowedByCSP({foo: 'bar'}, HOST_TO_CHECK)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false if CSP header is empty', () => {
|
||||||
|
expect(checkDisallowedByCSP({'content-security-policy': ''}, HOST_TO_CHECK)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns true if plausible.io is not allowed', () => {
|
||||||
|
const headers = {'content-security-policy': "default-src 'self' foo.local; example.com"}
|
||||||
|
expect(checkDisallowedByCSP(headers, HOST_TO_CHECK)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false if plausible.io is allowed', () => {
|
||||||
|
const headers = {'content-security-policy': "default-src 'self' plausible.io; example.com"}
|
||||||
|
expect(checkDisallowedByCSP(headers, HOST_TO_CHECK)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false if plausible.io subdomain is allowed', () => {
|
||||||
|
const headers = {'content-security-policy': "default-src 'self' staging.plausible.io; example.com"}
|
||||||
|
expect(checkDisallowedByCSP(headers, HOST_TO_CHECK)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false if plausible.io is allowed with https', () => {
|
||||||
|
const headers = {'content-security-policy': "default-src 'self' https://plausible.io; example.com"}
|
||||||
|
expect(checkDisallowedByCSP(headers, HOST_TO_CHECK)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns true if plausible.io is not present in any directive', () => {
|
||||||
|
const headers = {'content-security-policy': "default-src 'self' foo.com; bar.com"}
|
||||||
|
expect(checkDisallowedByCSP(headers, HOST_TO_CHECK)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { verify } from '../support/installation-support-playwright-wrappers'
|
import { verifyV1 } from '../support/installation-support-playwright-wrappers'
|
||||||
import { delay } from '../support/test-utils'
|
import { delay } from '../support/test-utils'
|
||||||
import { initializePageDynamically } from '../support/initialize-page-dynamically'
|
import { initializePageDynamically } from '../support/initialize-page-dynamically'
|
||||||
import { compileFile } from '../../compiler'
|
import { compileFile } from '../../compiler'
|
||||||
|
|
@ -29,7 +29,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||||
|
|
@ -72,7 +72,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN, debug: true})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN, debug: true})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||||
|
|
@ -87,7 +87,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
scriptConfig: ''
|
scriptConfig: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(false)
|
expect(result.data.plausibleInstalled).toBe(false)
|
||||||
expect(result.data.callbackStatus).toBe(0)
|
expect(result.data.callbackStatus).toBe(0)
|
||||||
|
|
@ -104,7 +104,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
response: `<body><script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script></body>`
|
response: `<body><script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script></body>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.snippetsFoundInHead).toBe(0)
|
expect(result.data.snippetsFoundInHead).toBe(0)
|
||||||
|
|
@ -121,7 +121,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
response: `<head><script defer data-domain="example.org,example.com,example.net" src="/tracker/js/plausible.local.js"></script></head>`
|
response: `<head><script defer data-domain="example.org,example.com,example.net" src="/tracker/js/plausible.local.js"></script></head>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: "example.com"})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: "example.com"})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||||
|
|
@ -138,7 +138,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
response: `<head><script defer data-domain="example.org,example.com,example.net" src="/tracker/js/plausible.local.js"></script></head>`
|
response: `<head><script defer data-domain="example.org,example.com,example.net" src="/tracker/js/plausible.local.js"></script></head>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: "example.typo"})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: "example.typo"})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||||
|
|
@ -178,7 +178,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.proxyLikely).toBe(false)
|
expect(result.data.proxyLikely).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
@ -201,7 +201,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: "example.com"})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: "example.com"})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.snippetsFoundInHead).toBe(2)
|
expect(result.data.snippetsFoundInHead).toBe(2)
|
||||||
|
|
@ -218,7 +218,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
scriptConfig: `<script defer data-domain="wrong.com" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="wrong.com" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: 'right.com'})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: 'right.com'})
|
||||||
|
|
||||||
expect(result.data.dataDomainMismatch).toBe(true)
|
expect(result.data.dataDomainMismatch).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -231,7 +231,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
||||||
scriptConfig: `<script defer data-domain="www.right.com" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="www.right.com" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: 'right.com'})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: 'right.com'})
|
||||||
|
|
||||||
expect(result.data.dataDomainMismatch).toBe(false)
|
expect(result.data.dataDomainMismatch).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
@ -249,7 +249,7 @@ test.describe('v1 verifier (window.plausible)', () => {
|
||||||
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.callbackStatus).toBe(404)
|
expect(result.data.callbackStatus).toBe(404)
|
||||||
|
|
@ -263,7 +263,7 @@ test.describe('v1 verifier (window.plausible)', () => {
|
||||||
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.callbackStatus).toBe(0)
|
expect(result.data.callbackStatus).toBe(0)
|
||||||
|
|
@ -279,7 +279,7 @@ test.describe('v1 verifier (window.plausible)', () => {
|
||||||
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.plausibleInstalled).toBe(true)
|
expect(result.data.plausibleInstalled).toBe(true)
|
||||||
expect(result.data.callbackStatus).toBe(-1)
|
expect(result.data.callbackStatus).toBe(-1)
|
||||||
|
|
@ -300,7 +300,7 @@ test.describe('v1 verifier (WordPress detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.wordpressPlugin).toBe(true)
|
expect(result.data.wordpressPlugin).toBe(true)
|
||||||
expect(result.data.wordpressLikely).toBe(true)
|
expect(result.data.wordpressLikely).toBe(true)
|
||||||
|
|
@ -321,7 +321,7 @@ test.describe('v1 verifier (WordPress detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.wordpressPlugin).toBe(false)
|
expect(result.data.wordpressPlugin).toBe(false)
|
||||||
expect(result.data.wordpressLikely).toBe(true)
|
expect(result.data.wordpressLikely).toBe(true)
|
||||||
|
|
@ -353,7 +353,7 @@ test.describe('v1 verifier (GTM detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.gtmLikely).toBe(true)
|
expect(result.data.gtmLikely).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -392,7 +392,7 @@ test.describe('v1 verifier (cookieBanner detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.cookieBannerLikely).toBe(true)
|
expect(result.data.cookieBannerLikely).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -417,7 +417,7 @@ test.describe('v1 verifier (manualScriptExtension detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.manualScriptExtension).toBe(true)
|
expect(result.data.manualScriptExtension).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -447,7 +447,7 @@ test.describe('v1 verifier (unknownAttributes detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.unknownAttributes).toBe(false)
|
expect(result.data.unknownAttributes).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
@ -466,7 +466,7 @@ test.describe('v1 verifier (unknownAttributes detection)', () => {
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
const result = await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(result.data.unknownAttributes).toBe(true)
|
expect(result.data.unknownAttributes).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -484,7 +484,7 @@ test.describe('v1 verifier (logging)', () => {
|
||||||
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN, debug: true})
|
await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN, debug: true})
|
||||||
|
|
||||||
expect(logs.find(str => str.includes('Starting snippet detection'))).toContain('[Plausible Verification] Starting snippet detection')
|
expect(logs.find(str => str.includes('Starting snippet detection'))).toContain('[Plausible Verification] Starting snippet detection')
|
||||||
expect(logs.find(str => str.includes('Checking for Plausible function'))).toContain('[Plausible Verification] Checking for Plausible function')
|
expect(logs.find(str => str.includes('Checking for Plausible function'))).toContain('[Plausible Verification] Checking for Plausible function')
|
||||||
|
|
@ -501,7 +501,7 @@ test.describe('v1 verifier (logging)', () => {
|
||||||
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
scriptConfig: `<script defer data-domain="${SOME_DOMAIN}" src="/tracker/js/plausible.local.js"></script>`
|
||||||
})
|
})
|
||||||
|
|
||||||
await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||||
|
|
||||||
expect(logs.length).toBe(0)
|
expect(logs.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,449 @@
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { executeVerifyV2 } from '../support/installation-support-playwright-wrappers'
|
||||||
|
import { initializePageDynamically } from '../support/initialize-page-dynamically'
|
||||||
|
import { mockManyRequests } from '../support/mock-many-requests'
|
||||||
|
import { LOCAL_SERVER_ADDR } from '../support/server'
|
||||||
|
import { tracker_script_version as version } from '../support/test-utils'
|
||||||
|
|
||||||
|
const CSP_HOST_TO_CHECK = 'plausible.io'
|
||||||
|
|
||||||
|
test.describe('installed plausible web variant', () => {
|
||||||
|
test('using provided snippet', async ({ page }, { testId }) => {
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `https://plausible.io/api/event`,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
fulfill: {
|
||||||
|
status: 202,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'ok'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `https://plausible.io/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
},
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'web',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: { status: 202 },
|
||||||
|
requestUrl: 'https://plausible.io/api/event',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: 202,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('using provided snippet and the events endpoint responds slower than the timeout', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `https://plausible.io/api/event`,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
fulfill: {
|
||||||
|
status: 202,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'ok'
|
||||||
|
},
|
||||||
|
responseDelay: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `https://plausible.io/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
},
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'web',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: undefined,
|
||||||
|
requestUrl: 'https://plausible.io/api/event',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: undefined,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('using provided snippet and the events endpoint responds with 400', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `https://plausible.io/api/event`,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
fulfill: {
|
||||||
|
status: 400,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'Bad Request'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: {
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `https://plausible.io/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
},
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'web',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: { status: 400 },
|
||||||
|
requestUrl: 'https://plausible.io/api/event',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: 400,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('using provided snippet and captureOnLocalhost: false', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: {
|
||||||
|
domain: 'example.com/foobar',
|
||||||
|
endpoint: 'https://plausible.io/api/event',
|
||||||
|
captureOnLocalhost: false
|
||||||
|
},
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'web',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: undefined,
|
||||||
|
requestUrl: undefined,
|
||||||
|
normalizedBody: undefined,
|
||||||
|
responseStatus: undefined,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('installed plausible esm variant', () => {
|
||||||
|
test('using <script type="module"> tag', async ({ page }, { testId }) => {
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `https://plausible.io/api/event`,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
fulfill: {
|
||||||
|
status: 202,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'ok'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; window.init = init; window.track = track; init(${JSON.stringify(
|
||||||
|
{
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `https://plausible.io/api/event`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'npm',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: { status: 202 },
|
||||||
|
requestUrl: 'https://plausible.io/api/event',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: 202,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('using <script type="module"> tag and endpoint: "/events"', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `${LOCAL_SERVER_ADDR}/events`,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
fulfill: {
|
||||||
|
status: 202,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'ok'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; window.init = init; window.track = track; init(${JSON.stringify(
|
||||||
|
{
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `/events`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'npm',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: { status: 202 },
|
||||||
|
requestUrl: '/events',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: 202,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('using <script type="module"> tag and endpoint: "https://example.com/events"', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; window.init = init; window.track = track; init(${JSON.stringify(
|
||||||
|
{
|
||||||
|
domain: 'example.com',
|
||||||
|
endpoint: `https://example.com/events`,
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await mockManyRequests({
|
||||||
|
page,
|
||||||
|
path: `https://example.com/events`,
|
||||||
|
awaitedRequestCount: 1,
|
||||||
|
fulfill: {
|
||||||
|
status: 500,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
body: 'Unknown error'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'npm',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: { status: 500 },
|
||||||
|
requestUrl: 'https://example.com/events',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: 500,
|
||||||
|
error: undefined
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('using <script type="module"> tag and invalid endpoint: "invalid:/plausible.io/api/event"', async ({
|
||||||
|
page
|
||||||
|
}, { testId }) => {
|
||||||
|
const { url } = await initializePageDynamically(page, {
|
||||||
|
testId,
|
||||||
|
scriptConfig: `<script type="module">import { init, track } from '/tracker/js/npm_package/plausible.js'; window.init = init; window.track = track; init(${JSON.stringify(
|
||||||
|
{
|
||||||
|
domain: 'example.com/foobar',
|
||||||
|
endpoint: 'invalid:/plausible.io/api/event',
|
||||||
|
captureOnLocalhost: true
|
||||||
|
}
|
||||||
|
)})</script>`,
|
||||||
|
bodyContent: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
|
||||||
|
const result = await executeVerifyV2(page, {
|
||||||
|
responseHeaders: {},
|
||||||
|
debug: true,
|
||||||
|
timeoutMs: 1000,
|
||||||
|
cspHostToCheck: CSP_HOST_TO_CHECK
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
data: {
|
||||||
|
completed: true,
|
||||||
|
plausibleIsInitialized: true,
|
||||||
|
plausibleIsOnWindow: true,
|
||||||
|
disallowedByCsp: false,
|
||||||
|
plausibleVersion: version,
|
||||||
|
plausibleVariant: 'npm',
|
||||||
|
testEvent: {
|
||||||
|
callbackResult: {
|
||||||
|
error: expect.objectContaining({ message: 'Failed to fetch' })
|
||||||
|
},
|
||||||
|
requestUrl: 'invalid:/plausible.io/api/event',
|
||||||
|
normalizedBody: {
|
||||||
|
domain: 'example.com/foobar',
|
||||||
|
name: 'verification-agent-test',
|
||||||
|
version
|
||||||
|
},
|
||||||
|
responseStatus: undefined,
|
||||||
|
error: { message: 'Failed to fetch' }
|
||||||
|
},
|
||||||
|
cookieBannerLikely: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { compileFile } from '../../compiler/index.js'
|
|
||||||
import variantsFile from '../../compiler/variants.json' with { type: 'json' }
|
|
||||||
|
|
||||||
const VERIFIER_V1_JS_VARIANT = variantsFile.manualVariants.find(variant => variant.name === 'verifier-v1.js')
|
|
||||||
const DETECTOR_JS_VARIANT = variantsFile.manualVariants.find(variant => variant.name === 'detector.js')
|
|
||||||
|
|
||||||
export async function verify(page, context) {
|
|
||||||
const {url, expectedDataDomain} = context
|
|
||||||
const debug = context.debug ? true : false
|
|
||||||
|
|
||||||
const verifierCode = await compileFile(VERIFIER_V1_JS_VARIANT, { returnCode: true })
|
|
||||||
|
|
||||||
await page.goto(url)
|
|
||||||
await page.evaluate(verifierCode)
|
|
||||||
|
|
||||||
return await page.evaluate(async ({expectedDataDomain, debug}) => {
|
|
||||||
return await window.verifyPlausibleInstallation(expectedDataDomain, debug)
|
|
||||||
}, {expectedDataDomain, debug})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function detect(page, context) {
|
|
||||||
const {url, detectV1} = context
|
|
||||||
const debug = context.debug ? true : false
|
|
||||||
|
|
||||||
const detectorCode = await compileFile(DETECTOR_JS_VARIANT, { returnCode: true })
|
|
||||||
|
|
||||||
await page.goto(url)
|
|
||||||
await page.evaluate(detectorCode)
|
|
||||||
|
|
||||||
return await page.evaluate(async ({detectV1, debug}) => {
|
|
||||||
return await window.scanPageBeforePlausibleInstallation(detectV1, debug)
|
|
||||||
}, {detectV1, debug})
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { compileFile } from '../../compiler/index.js'
|
||||||
|
import variantsFile from '../../compiler/variants.json' with { type: 'json' }
|
||||||
|
import { Page } from '@playwright/test'
|
||||||
|
import { VerifyV2Args, VerifyV2Result } from './types'
|
||||||
|
|
||||||
|
const VERIFIER_V1_JS_VARIANT = variantsFile.manualVariants.find(
|
||||||
|
(variant) => variant.name === 'verifier-v1.js'
|
||||||
|
)
|
||||||
|
const VERIFIER_V2_JS_VARIANT = variantsFile.manualVariants.find(
|
||||||
|
(variant) => variant.name === 'verifier-v2.js'
|
||||||
|
)
|
||||||
|
const DETECTOR_JS_VARIANT = variantsFile.manualVariants.find(
|
||||||
|
(variant) => variant.name === 'detector.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function executeVerifyV2(
|
||||||
|
page: Page,
|
||||||
|
{ debug, responseHeaders, timeoutMs, cspHostToCheck }: VerifyV2Args
|
||||||
|
): Promise<VerifyV2Result> {
|
||||||
|
const verifierCode = (await compileFile(VERIFIER_V2_JS_VARIANT, {
|
||||||
|
returnCode: true
|
||||||
|
})) as string
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.evaluate(verifierCode)
|
||||||
|
|
||||||
|
return await page.evaluate(
|
||||||
|
async ({ responseHeaders, debug, timeoutMs, cspHostToCheck }) => {
|
||||||
|
return await (window as any).verifyPlausibleInstallation({
|
||||||
|
responseHeaders,
|
||||||
|
debug,
|
||||||
|
timeoutMs,
|
||||||
|
cspHostToCheck
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ responseHeaders, debug, timeoutMs, cspHostToCheck }
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
completed: false,
|
||||||
|
error: {
|
||||||
|
message: error?.message ?? JSON.stringify(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyV1(page, context) {
|
||||||
|
const { url, expectedDataDomain } = context
|
||||||
|
const debug = context.debug ? true : false
|
||||||
|
|
||||||
|
const verifierCode = await compileFile(VERIFIER_V1_JS_VARIANT, {
|
||||||
|
returnCode: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
await page.evaluate(verifierCode)
|
||||||
|
|
||||||
|
return await page.evaluate(
|
||||||
|
async ({ expectedDataDomain, debug }) => {
|
||||||
|
return await (window as any).verifyPlausibleInstallation(
|
||||||
|
expectedDataDomain,
|
||||||
|
debug
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ expectedDataDomain, debug }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detect(page, context) {
|
||||||
|
const { url, detectV1 } = context
|
||||||
|
const debug = context.debug ? true : false
|
||||||
|
|
||||||
|
const detectorCode = await compileFile(DETECTOR_JS_VARIANT, {
|
||||||
|
returnCode: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url)
|
||||||
|
await page.evaluate(detectorCode)
|
||||||
|
|
||||||
|
return await page.evaluate(
|
||||||
|
async ({ detectV1, debug }) => {
|
||||||
|
return await (window as any).scanPageBeforePlausibleInstallation(
|
||||||
|
detectV1,
|
||||||
|
debug
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ detectV1, debug }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -11,3 +11,52 @@ export type ScriptConfig = {
|
||||||
domain: string
|
domain: string
|
||||||
endpoint: string
|
endpoint: string
|
||||||
} & Partial<Options>
|
} & Partial<Options>
|
||||||
|
|
||||||
|
export type VerifyV2Args = {
|
||||||
|
debug: boolean
|
||||||
|
responseHeaders: Record<string, string>
|
||||||
|
timeoutMs: number
|
||||||
|
cspHostToCheck: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VerifyV2Result = {
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
completed: true
|
||||||
|
plausibleIsOnWindow: boolean
|
||||||
|
plausibleIsInitialized: boolean
|
||||||
|
plausibleVersion: number
|
||||||
|
plausibleVariant?: string
|
||||||
|
disallowedByCsp: boolean
|
||||||
|
cookieBannerLikely: boolean
|
||||||
|
testEvent: {
|
||||||
|
/**
|
||||||
|
* window.plausible (track) callback
|
||||||
|
*/
|
||||||
|
callbackResult?: any
|
||||||
|
/**
|
||||||
|
* intercepted fetch response status
|
||||||
|
*/
|
||||||
|
responseStatus?: number
|
||||||
|
/**
|
||||||
|
* error caught during intercepted fetch
|
||||||
|
*/
|
||||||
|
error?: {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* intercepted fetch request url
|
||||||
|
*/
|
||||||
|
requestUrl?: string
|
||||||
|
/**
|
||||||
|
* intercepted fetch request body normalized
|
||||||
|
*/
|
||||||
|
normalizedBody?: {
|
||||||
|
domain: string
|
||||||
|
name: string
|
||||||
|
version?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| { completed: false; error: { message: string } }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue