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__{}
|
||||
|
||||
defmodule Result do
|
||||
@moduledoc """
|
||||
Diagnostics interpretation result.
|
||||
"""
|
||||
defstruct ok?: false, errors: [], recommendations: []
|
||||
@type t :: %__MODULE__{}
|
||||
end
|
||||
|
||||
alias Plausible.InstallationSupport.Result
|
||||
@spec interpret(t(), String.t()) :: Result.t()
|
||||
def interpret(
|
||||
%__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: %{},
|
||||
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__{
|
||||
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
|
||||
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
alias Plausible.InstallationSupport.{State, LegacyVerification}
|
||||
alias Plausible.InstallationSupport.{State, Result}
|
||||
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
|||
attr(:finished?, :boolean, default: false)
|
||||
attr(:success?, :boolean, default: false)
|
||||
attr(:verification_state, State, default: nil)
|
||||
attr(:interpretation, LegacyVerification.Diagnostics.Result, default: nil)
|
||||
attr(:interpretation, Result, default: nil)
|
||||
attr(:attempts, :integer, default: 0)
|
||||
attr(:flow, :string, default: "")
|
||||
attr(:installation_type, :string, default: nil)
|
||||
|
|
@ -146,7 +146,8 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
|||
<.focus_list>
|
||||
<:item :for={{diag, value} <- Map.from_struct(@verification_state.diagnostics)}>
|
||||
<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>
|
||||
</:item>
|
||||
</.focus_list>
|
||||
|
|
@ -157,4 +158,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
|
|||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp to_string_value(value) when is_binary(value), do: value
|
||||
defp to_string_value(value), do: inspect(value)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
use Plausible
|
||||
use PlausibleWeb, :live_view
|
||||
|
||||
alias Plausible.InstallationSupport.{State, LegacyVerification}
|
||||
alias Plausible.InstallationSupport.{State, LegacyVerification, Verification}
|
||||
|
||||
@component PlausibleWeb.Live.Components.Verification
|
||||
@slowdown_for_frequent_checking :timer.seconds(5)
|
||||
|
|
@ -131,12 +131,24 @@ 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
|
||||
|
||||
{:ok, pid} =
|
||||
LegacyVerification.Checks.run(
|
||||
"https://#{socket.assigns.domain}",
|
||||
socket.assigns.domain,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
if(FunWithFlags.enabled?(:scriptv2, for: socket.assigns.site),
|
||||
do:
|
||||
Verification.Checks.run(url_to_verify, domain, installation_type,
|
||||
report_to: report_to,
|
||||
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)}
|
||||
|
|
@ -152,7 +164,11 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
end
|
||||
|
||||
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
|
||||
schedule_pageviews_check(socket)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@
|
|||
"entry_point": "installation_support/verifier-v1.js",
|
||||
"output_path": "priv/tracker/installation_support/verifier-v1.js",
|
||||
"globals": {}
|
||||
},
|
||||
{
|
||||
"name": "verifier-v2.js",
|
||||
"entry_point": "installation_support/verifier-v2.js",
|
||||
"output_path": "priv/tracker/installation_support/verifier-v2.js",
|
||||
"globals": {}
|
||||
}
|
||||
],
|
||||
"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 { verify } from '../support/installation-support-playwright-wrappers'
|
||||
import { verifyV1 } from '../support/installation-support-playwright-wrappers'
|
||||
import { delay } from '../support/test-utils'
|
||||
import { initializePageDynamically } from '../support/initialize-page-dynamically'
|
||||
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>`
|
||||
})
|
||||
|
||||
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.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.snippetsFoundInHead).toBe(1)
|
||||
|
|
@ -87,7 +87,7 @@ test.describe('v1 verifier (basic diagnostics)', () => {
|
|||
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.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>`
|
||||
})
|
||||
|
||||
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.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>`
|
||||
})
|
||||
|
||||
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.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>`
|
||||
})
|
||||
|
||||
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.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)
|
||||
})
|
||||
|
|
@ -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.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>`
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
|
@ -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>`
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
|
@ -249,7 +249,7 @@ test.describe('v1 verifier (window.plausible)', () => {
|
|||
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.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>`
|
||||
})
|
||||
|
||||
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.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>`
|
||||
})
|
||||
|
||||
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.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.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.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)
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -484,7 +484,7 @@ test.describe('v1 verifier (logging)', () => {
|
|||
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('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>`
|
||||
})
|
||||
|
||||
await verify(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||
await verifyV1(page, {url: url, expectedDataDomain: SOME_DOMAIN})
|
||||
|
||||
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
|
||||
endpoint: string
|
||||
} & 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