Clean up legacy verification code and script v2 flag (#5824)
* add module name to service_error when check times out Otherwise, it can sometimes remain unclear in the diagnostics, whether it was InstallationV2 or InstallationV2CacheBust that timed out. * Remove duplicate timeout logic The current production logs show two types of verification timeouts: * service_error: "Unhandled Browserless response status: 408" (vast majority of cases) * service_error: :timeout (only a few cases) The latter happens when we hit the Req receive_timeout (endpoint_timeout + 2s). I've seen Browserless not respect the timeout param from time to time, so it's better to keep the timeout logic "in-house" only. * make service_error into a map with code and extra * interpret temporary service errors ...but still consider them "unhandled" for telemetry, also notifying Sentry and logging the warning. * separate sentry messages (verification) * make Verification.ChecksTest more DRY * organize tests into describe blocks * test verification telemetry and logging * fix codespell * get rid of legacy verification * rename Checks.InstallationV2 -> Checks.VerifyInstallation * delete Live.Installation and rename Live.InstallationV2 -> Live.Installation * rename installationv2 (live) files as well * delete old change-domain routes Also rename current liveview modules and routes, removing the v2 suffix * rename domain_change_v2 files, removing v2 suffix * remove legacy JS verifier code Also fix dockerignore and elixir.yml referencing a wrong priv path * rename verification_v2_test -> verification_test * remove v2 prefix from logs and sentry messages * clean up duplicate external_sites_controller_test.exs tests * remove flag * fix typespec * pass timeout as query param to Browserless too * Fixup external sites controller test module (#5826) * fix test description --------- Co-authored-by: Artur Pata <artur.pata@gmail.com>
This commit is contained in:
parent
ad2c8e8e39
commit
a83b4f3583
|
|
@ -59,7 +59,7 @@ npm-debug.log
|
|||
|
||||
# Auto-generated tracker files
|
||||
/priv/tracker/js/*.js
|
||||
/priv/tracker/verifier/
|
||||
/priv/tracker/installation_support/
|
||||
|
||||
# Dializer
|
||||
/priv/plts/*.plt
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
_build
|
||||
tracker/node_modules
|
||||
priv/tracker/js
|
||||
priv/tracker/verifier
|
||||
priv/tracker/installation_support
|
||||
${{ env.PERSISTENT_CACHE_DIR }}
|
||||
key: ${{ env.MIX_ENV }}-${{ env.CACHE_VERSION }}-${{ github.head_ref || github.ref }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
|
|
@ -97,7 +97,7 @@ jobs:
|
|||
- 'tracker/**'
|
||||
- name: Check if tracker and verifier are built already
|
||||
run: |
|
||||
if [ -f priv/tracker/js/plausible-web.js ] && [ -f priv/tracker/verifier/verifier-v1.js ]; then
|
||||
if [ -f priv/tracker/js/plausible-web.js ] && [ -f priv/tracker/installation_support/verifier.js ]; then
|
||||
echo "HAS_BUILT_TRACKER=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "HAS_BUILT_TRACKER=false" >> $GITHUB_ENV
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ config :plausible, Plausible.InstallationSupport.Checks.Detection,
|
|||
plug: {Req.Test, Plausible.InstallationSupport.Checks.Detection}
|
||||
]
|
||||
|
||||
config :plausible, Plausible.InstallationSupport.Checks.InstallationV2,
|
||||
config :plausible, Plausible.InstallationSupport.Checks.VerifyInstallation,
|
||||
req_opts: [
|
||||
plug: {Req.Test, Plausible.InstallationSupport.Checks.InstallationV2}
|
||||
plug: {Req.Test, Plausible.InstallationSupport.Checks.VerifyInstallation}
|
||||
]
|
||||
|
||||
config :plausible, Plausible.Session.Salts, interval: :timer.hours(1)
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.CSP do
|
||||
@moduledoc """
|
||||
Scans the Content Security Policy header to ensure that the Plausible domain is allowed.
|
||||
See `Plausible.InstallationSupport.LegacyVerification.Checks` for the execution sequence.
|
||||
"""
|
||||
use Plausible.InstallationSupport.Check
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're visiting your site to ensure that everything is working"
|
||||
|
||||
@impl true
|
||||
def perform(%State{assigns: %{headers: headers}} = state) do
|
||||
case headers["content-security-policy"] do
|
||||
[policy] ->
|
||||
directives = String.split(policy, ";")
|
||||
|
||||
allowed? =
|
||||
Enum.any?(directives, fn directive ->
|
||||
String.contains?(directive, PlausibleWeb.Endpoint.host())
|
||||
end)
|
||||
|
||||
if allowed? do
|
||||
state
|
||||
else
|
||||
put_diagnostics(state, disallowed_via_csp?: true)
|
||||
end
|
||||
|
||||
_ ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
def perform(state), do: state
|
||||
end
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.FetchBody do
|
||||
@moduledoc """
|
||||
Fetches the body of the site and extracts the HTML document, if available, for
|
||||
further processing. See `Plausible.InstallationSupport.LegacyVerification.Checks`
|
||||
for the execution sequence.
|
||||
"""
|
||||
use Plausible.InstallationSupport.Check
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're visiting your site to ensure that everything is working"
|
||||
|
||||
@impl true
|
||||
|
||||
def perform(%State{url: "https://" <> _ = url} = state) do
|
||||
fetch_body_opts = Application.get_env(:plausible, __MODULE__)[:req_opts] || []
|
||||
|
||||
opts =
|
||||
Keyword.merge(
|
||||
[
|
||||
base_url: url,
|
||||
max_redirects: 4,
|
||||
max_retries: 3,
|
||||
retry_log_level: :warning
|
||||
],
|
||||
fetch_body_opts
|
||||
)
|
||||
|
||||
{req, resp} = opts |> Req.new() |> Req.Request.run_request()
|
||||
|
||||
case resp do
|
||||
%Req.Response{body: body}
|
||||
when is_binary(body) ->
|
||||
state
|
||||
|> assign(final_domain: req.url.host)
|
||||
|> extract_document(resp)
|
||||
|
||||
_ ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_document(state, response) do
|
||||
with true <- html?(response),
|
||||
{:ok, document} <- Floki.parse_document(response.body) do
|
||||
state
|
||||
|> assign(raw_body: response.body, document: document, headers: response.headers)
|
||||
|> put_diagnostics(body_fetched?: true)
|
||||
else
|
||||
_ ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp html?(%Req.Response{headers: headers}) do
|
||||
headers
|
||||
|> Map.get("content-type", "")
|
||||
|> List.wrap()
|
||||
|> List.first()
|
||||
|> String.contains?("text/html")
|
||||
end
|
||||
end
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.Installation do
|
||||
require Logger
|
||||
alias Plausible.InstallationSupport.BrowserlessConfig
|
||||
|
||||
@verifier_code_path "priv/tracker/installation_support/verifier-v1.js"
|
||||
@external_resource @verifier_code_path
|
||||
|
||||
# On CI, the file might not be present for static checks so we default to empty string
|
||||
@verifier_code (case File.read(Application.app_dir(:plausible, @verifier_code_path)) do
|
||||
{:ok, content} -> content
|
||||
{:error, _} -> ""
|
||||
end)
|
||||
|
||||
# Puppeteer wrapper function that executes the vanilla JS verifier code.
|
||||
|
||||
# ### NO AUTOMATIC TEST COVERAGE
|
||||
|
||||
# Unfortunately, as things stand today, this Puppeteer wrapper logic
|
||||
# cannot be tested without spinning up a real Browserless instance or
|
||||
# bringing in a bunch of test deps for Puppeteer. Therefore, take extra
|
||||
# care when changing this and make sure to run manual tests on local
|
||||
# browserless (`make browserless`) before releasing an update.
|
||||
|
||||
# ### TRICKY: Handling client side JS navigation.
|
||||
|
||||
# We've seen numerous cases where client JS navigates or refreshes the
|
||||
# page after load. Any such JS behaviour destroys the Puppeteer page
|
||||
# context, meaning that our verifier execution gets interrupted and we
|
||||
# end up in the `catch` clause.
|
||||
|
||||
# To make our best effort verifying these sites, we retry (up to twice)
|
||||
# running the verifier again if we encounter this specific error.
|
||||
|
||||
# Important: On retries, we work with the client-modified page context
|
||||
# instead of calling `page.goto(context.url)` again (which would most
|
||||
# likely result in another interruptive navigation).
|
||||
@puppeteer_wrapper_code """
|
||||
export default async function({ page, context }) {
|
||||
const MAX_RETRIES = 2
|
||||
|
||||
async function attemptVerification() {
|
||||
await page.evaluate(() => {
|
||||
#{@verifier_code}
|
||||
});
|
||||
|
||||
return await page.evaluate(async (expectedDataDomain, debug) => {
|
||||
return await window.verifyPlausibleInstallation(expectedDataDomain, debug);
|
||||
}, context.expectedDataDomain, context.debug);
|
||||
}
|
||||
|
||||
try {
|
||||
await page.setUserAgent(context.userAgent);
|
||||
await page.goto(context.url);
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return await attemptVerification()
|
||||
} catch (error) {
|
||||
const shouldRetry = typeof error?.message === 'string' && error.message.toLowerCase().includes('execution context')
|
||||
|
||||
if (shouldRetry && attempt <= MAX_RETRIES) {
|
||||
// Brief delay before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
continue
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = error.message ? error.message : JSON.stringify(error)
|
||||
return {data: {completed: false, error: msg}}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@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).
|
||||
|
||||
The verification uses a vanilla JS script that runs in the browser context,
|
||||
performing a comprehensive Plausible installation verification. Providing
|
||||
the following information:
|
||||
|
||||
- `data.snippetsFoundInHead` - plausible snippets found in <head>
|
||||
|
||||
- `data.snippetsFoundInBody` - plausible snippets found in <body>
|
||||
|
||||
- `data.plausibleInstalled` - whether or not the `plausible()` window function was found
|
||||
|
||||
- `data.callbackStatus` - integer. 202 indicates that the server acknowledged the test event.
|
||||
The test event ingestion is discarded based on user-agent, see:
|
||||
`Plausible.InstallationSupport.user_agent/0`
|
||||
|
||||
- `data.dataDomainMismatch` - whether or not script[data-domain] mismatched with site.domain
|
||||
|
||||
- `data.proxyLikely` - whether the script[src] is not a plausible.io URL
|
||||
|
||||
- `data.manualScriptExtension` - whether the site is using script.manual.js
|
||||
|
||||
- `data.unknownAttributes` - whether the script tag has any unknown attributes
|
||||
|
||||
- `data.wordpressPlugin` - whether or not there's a `<meta>` tag with the WP plugin version
|
||||
|
||||
- `data.wordpressLikely` - whether or not the site is built on WordPress
|
||||
|
||||
- `data.gtmLikely` - whether or not the site uses GTM
|
||||
|
||||
- `data.cookieBannerLikely` - whether or not there's a cookie banner blocking Plausible
|
||||
"""
|
||||
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, data_domain: data_domain} = state) do
|
||||
opts = [
|
||||
headers: %{content_type: "application/json"},
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
code: @puppeteer_wrapper_code,
|
||||
context: %{
|
||||
expectedDataDomain: data_domain,
|
||||
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(BrowserlessConfig.browserless_function_api_endpoint(), opts) do
|
||||
{:ok, %{status: 200, body: %{"data" => %{"completed" => true} = js_data}}} ->
|
||||
emit_telemetry_and_log(state.diagnostics, js_data, data_domain)
|
||||
|
||||
put_diagnostics(state,
|
||||
plausible_installed?: js_data["plausibleInstalled"],
|
||||
callback_status: js_data["callbackStatus"]
|
||||
)
|
||||
|
||||
{:ok, %{status: status, body: %{"data" => %{"error" => error}}}} ->
|
||||
Logger.warning(
|
||||
"[VERIFICATION] Browserless JS error (data_domain='#{data_domain}'): #{inspect(error)}"
|
||||
)
|
||||
|
||||
put_diagnostics(state, plausible_installed?: false, service_error: status)
|
||||
|
||||
{:ok, %{status: status, body: body}} ->
|
||||
Logger.warning(
|
||||
"[VERIFICATION] Unexpected Browserless response (data_domain='#{data_domain}'): status=#{status}, body=#{inspect(body)}"
|
||||
)
|
||||
|
||||
put_diagnostics(state, plausible_installed?: false, service_error: status)
|
||||
|
||||
{:error, %{reason: reason}} ->
|
||||
Logger.warning(
|
||||
"[VERIFICATION] Browserless request error (data_domain='#{data_domain}'): #{inspect(reason)}"
|
||||
)
|
||||
|
||||
put_diagnostics(state, plausible_installed?: false, service_error: reason)
|
||||
end
|
||||
end
|
||||
|
||||
def telemetry_event(true = _diff), do: [:plausible, :verification, :js_elixir_diff]
|
||||
def telemetry_event(false = _diff), do: [:plausible, :verification, :js_elixir_match]
|
||||
|
||||
def emit_telemetry_and_log(elixir_data, js_data, data_domain) do
|
||||
diffs =
|
||||
for {diff, elixir_diagnostic, js_diagnostic} <- [
|
||||
{:data_domain_mismatch_diff, :data_domain_mismatch?, "dataDomainMismatch"},
|
||||
{:proxy_likely_diff, :proxy_likely?, "proxyLikely"},
|
||||
{:manual_script_extension_diff, :manual_script_extension?, "manualScriptExtension"},
|
||||
{:unknown_attributes_diff, :snippet_unknown_attributes?, "unknownAttributes"},
|
||||
{:wordpress_plugin_diff, :wordpress_plugin?, "wordpressPlugin"},
|
||||
{:wordpress_likely_diff, :wordpress_likely?, "wordpressLikely"},
|
||||
{:gtm_likely_diff, :gtm_likely?, "gtmLikely"},
|
||||
{:cookie_banner_likely_diff, :cookie_banner_likely?, "cookieBannerLikely"}
|
||||
] do
|
||||
case {Map.get(elixir_data, elixir_diagnostic), js_data[js_diagnostic]} do
|
||||
{true, false} -> {diff, -1}
|
||||
{false, true} -> {diff, 1}
|
||||
{_, _} -> {diff, 0}
|
||||
end
|
||||
end
|
||||
|> Map.new()
|
||||
|> Map.merge(%{
|
||||
snippets_head_diff: js_data["snippetsFoundInHead"] - elixir_data.snippets_found_in_head,
|
||||
snippets_body_diff: js_data["snippetsFoundInBody"] - elixir_data.snippets_found_in_body
|
||||
})
|
||||
|> Map.reject(fn {_k, v} -> v == 0 end)
|
||||
|
||||
any_diff? = map_size(diffs) > 0
|
||||
|
||||
if any_diff? do
|
||||
info =
|
||||
%{
|
||||
domain: data_domain,
|
||||
plausible_installed_js: js_data["plausibleInstalled"],
|
||||
callback_status_js: js_data["callbackStatus"]
|
||||
}
|
||||
|> Map.merge(diffs)
|
||||
|
||||
Logger.warning("[VERIFICATION] js_elixir_diff: #{inspect(info)}")
|
||||
end
|
||||
|
||||
:telemetry.execute(telemetry_event(any_diff?), %{})
|
||||
end
|
||||
end
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.ScanBody do
|
||||
@moduledoc """
|
||||
Naive way of detecting GTM and WordPress powered sites.
|
||||
"""
|
||||
use Plausible.InstallationSupport.Check
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're visiting your site to ensure that everything is working"
|
||||
|
||||
@impl true
|
||||
def perform(%State{assigns: %{raw_body: body}} = state) when is_binary(body) do
|
||||
state
|
||||
|> scan_wp_plugin()
|
||||
|> scan_gtm()
|
||||
|> scan_wp()
|
||||
|> scan_cookie_banners()
|
||||
end
|
||||
|
||||
def perform(state), do: state
|
||||
|
||||
defp scan_wp_plugin(%{assigns: %{document: document}} = state) do
|
||||
case Floki.find(document, ~s|meta[name="plausible-analytics-version"]|) do
|
||||
[] ->
|
||||
state
|
||||
|
||||
[_] ->
|
||||
state
|
||||
|> assign(skip_wordpress_check: true)
|
||||
|> put_diagnostics(wordpress_likely?: true, wordpress_plugin?: true)
|
||||
end
|
||||
end
|
||||
|
||||
defp scan_wp_plugin(state) do
|
||||
state
|
||||
end
|
||||
|
||||
@gtm_signatures [
|
||||
"googletagmanager.com/gtm.js"
|
||||
]
|
||||
|
||||
defp scan_gtm(state) do
|
||||
if Enum.any?(@gtm_signatures, &String.contains?(state.assigns.raw_body, &1)) do
|
||||
put_diagnostics(state, gtm_likely?: true)
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
@wordpress_signatures [
|
||||
"wp-content",
|
||||
"wp-includes",
|
||||
"wp-json"
|
||||
]
|
||||
|
||||
defp scan_wp(%{assigns: %{skip_wordpress_check: true}} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp scan_wp(state) do
|
||||
if Enum.any?(@wordpress_signatures, &String.contains?(state.assigns.raw_body, &1)) do
|
||||
put_diagnostics(state, wordpress_likely?: true)
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp scan_cookie_banners(%{assigns: %{raw_body: body}} = state) do
|
||||
# We'll start with CookieBot. Not using the selectors yet, as seen at
|
||||
# https://github.com/cavi-au/Consent-O-Matic/blob/master/rules/cookiebot.json
|
||||
# because those don't seem to be appearing without JS evaluation.
|
||||
# If this ever becomes an issue, we'll have to move that check to headless.
|
||||
if String.contains?(body, "cookiebot") do
|
||||
put_diagnostics(state, cookie_banner_likely?: true)
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp scan_cookie_banners(state) do
|
||||
state
|
||||
end
|
||||
end
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.Snippet do
|
||||
@moduledoc """
|
||||
The check looks for Plausible snippets and tries to address the common
|
||||
integration issues, such as bad placement, data-domain typos, unknown
|
||||
attributes frequently added by performance optimization plugins, etc.
|
||||
"""
|
||||
use Plausible.InstallationSupport.Check
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're looking for the Plausible snippet on your site"
|
||||
|
||||
@impl true
|
||||
def perform(%State{assigns: %{document: document}} = state) do
|
||||
in_head = Floki.find(document, "head script[data-domain][src]")
|
||||
in_body = Floki.find(document, "body script[data-domain][src]")
|
||||
|
||||
all = in_head ++ in_body
|
||||
|
||||
put_diagnostics(state,
|
||||
snippets_found_in_head: Enum.count(in_head),
|
||||
snippets_found_in_body: Enum.count(in_body),
|
||||
proxy_likely?: proxy_likely?(all),
|
||||
manual_script_extension?: manual_script_extension?(all),
|
||||
snippet_unknown_attributes?: unknown_attributes?(all),
|
||||
data_domain_mismatch?:
|
||||
data_domain_mismatch?(all, state.data_domain, state.assigns[:final_domain])
|
||||
)
|
||||
end
|
||||
|
||||
def perform(state), do: state
|
||||
|
||||
defp manual_script_extension?(nodes) do
|
||||
nodes
|
||||
|> Floki.attribute("src")
|
||||
|> Enum.any?(&String.contains?(&1, "manual."))
|
||||
end
|
||||
|
||||
defp proxy_likely?(nodes) do
|
||||
nodes
|
||||
|> Floki.attribute("src")
|
||||
|> Enum.any?(&(not String.starts_with?(&1, PlausibleWeb.Endpoint.url())))
|
||||
end
|
||||
|
||||
@known_attributes [
|
||||
"data-domain",
|
||||
"src",
|
||||
"defer",
|
||||
"data-api",
|
||||
"data-exclude",
|
||||
"data-include",
|
||||
"data-cfasync"
|
||||
]
|
||||
|
||||
defp unknown_attributes?(nodes) do
|
||||
Enum.any?(nodes, fn {_, attrs, _} ->
|
||||
Enum.any?(attrs, fn
|
||||
{"type", "text/javascript"} ->
|
||||
false
|
||||
|
||||
{"event-" <> _, _} ->
|
||||
false
|
||||
|
||||
{key, _} ->
|
||||
key not in @known_attributes
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp data_domain_mismatch?(nodes, data_domain, final_data_domain) do
|
||||
nodes
|
||||
|> Floki.attribute("data-domain")
|
||||
|> Enum.any?(fn script_data_domain ->
|
||||
multiple = String.split(script_data_domain, ",")
|
||||
|
||||
data_domain not in multiple and final_data_domain not in multiple
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.SnippetCacheBust do
|
||||
@moduledoc """
|
||||
A naive way of trying to figure out whether the latest site contents
|
||||
is wrapped with some CDN/caching layer.
|
||||
|
||||
In case no snippets were found, we'll try to bust the cache by appending
|
||||
a random query parameter and re-run `FetchBody` and `Snippet` checks.
|
||||
If the result is different this time, we'll assume cache likely.
|
||||
"""
|
||||
use Plausible.InstallationSupport.Check
|
||||
|
||||
alias Plausible.InstallationSupport.{LegacyVerification, Checks, URL}
|
||||
|
||||
@impl true
|
||||
def report_progress_as, do: "We're looking for the Plausible snippet on your site"
|
||||
|
||||
@impl true
|
||||
def perform(
|
||||
%State{
|
||||
url: url,
|
||||
diagnostics: %LegacyVerification.Diagnostics{
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
body_fetched?: true
|
||||
}
|
||||
} = state
|
||||
) do
|
||||
state2 =
|
||||
%{state | url: URL.bust_url(url)}
|
||||
|> Checks.FetchBody.perform()
|
||||
|> Checks.ScanBody.perform()
|
||||
|> Checks.Snippet.perform()
|
||||
|
||||
if state2.diagnostics.snippets_found_in_head > 0 or
|
||||
state2.diagnostics.snippets_found_in_body > 0 do
|
||||
put_diagnostics(state2, snippet_found_after_busting_cache?: true)
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
def perform(state), do: state
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
||||
defmodule Plausible.InstallationSupport.Checks.VerifyInstallation do
|
||||
@moduledoc """
|
||||
Calls the browserless.io service (local instance can be spawned with `make browserless`)
|
||||
and runs verifier script via the [function API](https://docs.browserless.io/HTTP-APIs/function).
|
||||
|
|
@ -8,7 +8,7 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
use Plausible.InstallationSupport.Check
|
||||
alias Plausible.InstallationSupport.BrowserlessConfig
|
||||
|
||||
@verifier_code_path "priv/tracker/installation_support/verifier-v2.js"
|
||||
@verifier_code_path "priv/tracker/installation_support/verifier.js"
|
||||
@external_resource @verifier_code_path
|
||||
|
||||
# On CI, the file might not be present for static checks so we default to empty string
|
||||
|
|
@ -172,7 +172,7 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2 do
|
|||
end
|
||||
|
||||
defp warning_message(message, state) do
|
||||
"[VERIFICATION v2] #{message} (data_domain='#{state.data_domain}')"
|
||||
"[VERIFICATION] #{message} (data_domain='#{state.data_domain}')"
|
||||
end
|
||||
|
||||
defp parse_to_diagnostics(data),
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.InstallationV2CacheBust do
|
||||
defmodule Plausible.InstallationSupport.Checks.VerifyInstallationCacheBust 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.
|
||||
and running VerifyInstallation again.
|
||||
|
||||
Whatever the result from the rerun, that is what we use to interpret the installation.
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ defmodule Plausible.InstallationSupport.Checks.InstallationV2CacheBust do
|
|||
state
|
||||
|> struct!(diagnostics: reset_diagnostics)
|
||||
|> struct!(url: InstallationSupport.URL.bust_url(url))
|
||||
|> InstallationSupport.Checks.InstallationV2.perform()
|
||||
|> InstallationSupport.Checks.VerifyInstallation.perform()
|
||||
|> put_diagnostics(diagnostics_are_from_cache_bust: true)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.LegacyVerification.Checks do
|
||||
@moduledoc """
|
||||
Checks that are performed during v1 site verification.
|
||||
|
||||
In async execution, each check notifies the caller by sending a message to it.
|
||||
"""
|
||||
alias Plausible.InstallationSupport.LegacyVerification
|
||||
alias Plausible.InstallationSupport.{State, CheckRunner, Checks}
|
||||
|
||||
require Logger
|
||||
|
||||
@checks [
|
||||
Checks.FetchBody,
|
||||
Checks.CSP,
|
||||
Checks.ScanBody,
|
||||
Checks.Snippet,
|
||||
Checks.SnippetCacheBust,
|
||||
Checks.Installation
|
||||
]
|
||||
|
||||
def run(url, data_domain, 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: %LegacyVerification.Diagnostics{}
|
||||
}
|
||||
|
||||
CheckRunner.run(init_state, checks,
|
||||
async?: async?,
|
||||
report_to: report_to,
|
||||
slowdown: slowdown
|
||||
)
|
||||
end
|
||||
|
||||
def interpret_diagnostics(%State{} = state) do
|
||||
LegacyVerification.Diagnostics.interpret(
|
||||
state.diagnostics,
|
||||
state.url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,391 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.LegacyVerification.Diagnostics do
|
||||
@moduledoc """
|
||||
Module responsible for translating diagnostics to user-friendly errors and recommendations.
|
||||
"""
|
||||
require Logger
|
||||
|
||||
@errors Plausible.InstallationSupport.LegacyVerification.Errors.all()
|
||||
|
||||
defstruct plausible_installed?: false,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
snippet_unknown_attributes?: false,
|
||||
disallowed_via_csp?: false,
|
||||
service_error: nil,
|
||||
body_fetched?: false,
|
||||
wordpress_likely?: false,
|
||||
cookie_banner_likely?: false,
|
||||
gtm_likely?: false,
|
||||
callback_status: 0,
|
||||
proxy_likely?: false,
|
||||
manual_script_extension?: false,
|
||||
data_domain_mismatch?: false,
|
||||
wordpress_plugin?: false
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
alias Plausible.InstallationSupport.Result
|
||||
@spec interpret(t(), String.t()) :: Result.t()
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
snippets_found_in_head: 1,
|
||||
snippets_found_in_body: 0,
|
||||
callback_status: callback_status,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
service_error: nil,
|
||||
data_domain_mismatch?: false
|
||||
},
|
||||
_url
|
||||
)
|
||||
when callback_status in [200, 202] do
|
||||
%Result{ok?: true}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{plausible_installed?: false, gtm_likely?: true, disallowed_via_csp?: true},
|
||||
_url
|
||||
) do
|
||||
error(@errors.csp)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
gtm_likely?: true,
|
||||
cookie_banner_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.gtm_cookie_banner)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{plausible_installed?: false, gtm_likely?: true, wordpress_plugin?: false},
|
||||
_url
|
||||
) do
|
||||
error(@errors.gtm)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
disallowed_via_csp?: true,
|
||||
proxy_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.csp)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
body_fetched?: true,
|
||||
service_error: nil,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.no_snippet)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
body_fetched?: true,
|
||||
gtm_likely?: false,
|
||||
callback_status: callback_status
|
||||
},
|
||||
_url
|
||||
)
|
||||
when is_integer(callback_status) and callback_status > 202 do
|
||||
error(@errors.no_snippet)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
body_fetched?: true,
|
||||
service_error: nil,
|
||||
wordpress_likely?: true
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.no_snippet_wp)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
body_fetched?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.unreachable)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
service_error: :timeout
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.generic)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
service_error: service_error
|
||||
},
|
||||
_url
|
||||
)
|
||||
when not is_nil(service_error) do
|
||||
error(@errors.temporary)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
service_error: nil,
|
||||
body_fetched?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.unreachable)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
wordpress_likely?: false,
|
||||
callback_status: -1
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.generic)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false,
|
||||
callback_status: -1
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.old_script_wp_no_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: true,
|
||||
callback_status: -1
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.old_script_wp_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: callback_status,
|
||||
proxy_likely?: true
|
||||
},
|
||||
_url
|
||||
)
|
||||
when callback_status in [0, 500] do
|
||||
error(@errors.proxy_misconfigured)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
proxy_likely?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.proxy_wp_no_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
proxy_likely?: true,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.proxy_general)
|
||||
end
|
||||
|
||||
def interpret(%__MODULE__{data_domain_mismatch?: true}, "https://" <> domain) do
|
||||
error(@errors.different_data_domain, domain: domain)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
snippets_found_in_head: count_head,
|
||||
snippets_found_in_body: count_body,
|
||||
manual_script_extension?: false
|
||||
},
|
||||
_url
|
||||
)
|
||||
when count_head + count_body > 1 do
|
||||
error(@errors.multiple_snippets)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: callback_status,
|
||||
snippet_found_after_busting_cache?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: true
|
||||
},
|
||||
_url
|
||||
)
|
||||
when callback_status in [200, 202] do
|
||||
error(@errors.cache_wp_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: callback_status,
|
||||
snippet_found_after_busting_cache?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
)
|
||||
when callback_status in [200, 202] do
|
||||
error(@errors.cache_wp_no_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: 202,
|
||||
snippet_found_after_busting_cache?: true,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.cache_general)
|
||||
end
|
||||
|
||||
def interpret(%__MODULE__{snippets_found_in_head: 0, snippets_found_in_body: n}, _url)
|
||||
when n >= 1 do
|
||||
error(@errors.snippet_in_body)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: true
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.illegal_attrs_wp_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.illegal_attrs_wp_no_plugin)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
error(@errors.illegal_attrs_general)
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
callback_status: callback_status,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
service_error: nil
|
||||
},
|
||||
_url
|
||||
)
|
||||
when callback_status in [200, 202] do
|
||||
%Result{ok?: true}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
snippets_found_in_head: count_head,
|
||||
snippets_found_in_body: count_body,
|
||||
callback_status: callback_status,
|
||||
service_error: nil,
|
||||
manual_script_extension?: true
|
||||
},
|
||||
_url
|
||||
)
|
||||
when count_head + count_body > 1 and callback_status in [200, 202] do
|
||||
%Result{ok?: true}
|
||||
end
|
||||
|
||||
def interpret(diagnostics, url) do
|
||||
Sentry.capture_message("Unhandled case for site verification",
|
||||
extra: %{
|
||||
message: inspect(diagnostics),
|
||||
url: url,
|
||||
hash: :erlang.phash2(diagnostics)
|
||||
}
|
||||
)
|
||||
|
||||
error(@errors.unknown)
|
||||
end
|
||||
|
||||
defp error(error) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: [error.message],
|
||||
recommendations: [%{text: error.recommendation, url: error.url}]
|
||||
}
|
||||
end
|
||||
|
||||
defp error(error, assigns) do
|
||||
recommendation = EEx.eval_string(error.recommendation, assigns: assigns)
|
||||
error(%{error | recommendation: recommendation})
|
||||
end
|
||||
end
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.LegacyVerification.Errors do
|
||||
@moduledoc """
|
||||
A go-to definition of all legacy verification errors
|
||||
"""
|
||||
|
||||
@errors %{
|
||||
gtm: %{
|
||||
message: "We encountered an issue with your Plausible integration",
|
||||
recommendation:
|
||||
"As you're using Google Tag Manager, you'll need to use a GTM-specific Plausible snippet",
|
||||
url: "https://plausible.io/docs/google-tag-manager"
|
||||
},
|
||||
gtm_cookie_banner: %{
|
||||
message: "We couldn't verify your website",
|
||||
recommendation:
|
||||
"As you're using Google Tag Manager, you'll need to use a GTM-specific Plausible snippet. Please make sure no cookie consent banner is blocking our script",
|
||||
url: "https://plausible.io/docs/google-tag-manager"
|
||||
},
|
||||
csp: %{
|
||||
message: "We encountered an issue with your site's CSP",
|
||||
recommendation:
|
||||
"Please add plausible.io domain specifically to the allowed list of domains in your Content Security Policy (CSP)",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp"
|
||||
},
|
||||
unreachable: %{
|
||||
message: "We couldn't reach your site",
|
||||
recommendation:
|
||||
"If your site is running at a different location, please manually check your integration",
|
||||
url: "https://plausible.io/docs/troubleshoot-integration"
|
||||
},
|
||||
no_snippet: %{
|
||||
message: "We couldn't find the Plausible snippet",
|
||||
recommendation: "Please insert the snippet into your site",
|
||||
url: "https://plausible.io/docs/plausible-script"
|
||||
},
|
||||
no_snippet_wp: %{
|
||||
message: "We couldn't find the Plausible snippet on your site",
|
||||
recommendation:
|
||||
"Please install and activate our WordPress plugin to start counting your visitors",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
temporary: %{
|
||||
message: "We encountered a temporary problem",
|
||||
recommendation: "Please try again in a few minutes or manually check your integration",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
|
||||
},
|
||||
generic: %{
|
||||
message: "We couldn't automatically verify your website",
|
||||
recommendation:
|
||||
"Please manually check your integration by following the instructions provided",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
|
||||
},
|
||||
old_script_wp_no_plugin: %{
|
||||
message: "We couldn't verify your website",
|
||||
recommendation:
|
||||
"You're running an older version of our script so we cannot verify it. Please use our WordPress plugin instead",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
old_script_wp_plugin: %{
|
||||
message: "We couldn't verify your website",
|
||||
recommendation:
|
||||
"You're running an older version of our script so we cannot verify it. Please re-enable the proxy in our plugin",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
proxy_misconfigured: %{
|
||||
message: "We encountered an error with your Plausible proxy",
|
||||
recommendation: "Please check whether you've configured the /event route correctly",
|
||||
url: "https://plausible.io/docs/proxy/introduction"
|
||||
},
|
||||
proxy_wp_no_plugin: %{
|
||||
message: "We encountered an error with your Plausible proxy",
|
||||
recommendation:
|
||||
"Please re-enable the proxy in our WordPress plugin to start counting your visitors",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
proxy_general: %{
|
||||
message: "We encountered an error with your Plausible proxy",
|
||||
recommendation: "Please check your proxy configuration to make sure it's set up correctly",
|
||||
url: "https://plausible.io/docs/proxy/introduction"
|
||||
},
|
||||
multiple_snippets: %{
|
||||
message: "We've found multiple Plausible snippets",
|
||||
recommendation: "Please ensure that only one snippet is used",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#did-you-insert-multiple-plausible-snippets-into-your-site"
|
||||
},
|
||||
cache_wp_plugin: %{
|
||||
message: "We encountered an issue with your site cache",
|
||||
recommendation:
|
||||
"Please clear your WordPress cache to ensure that the latest version is displayed to your visitors",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
cache_wp_no_plugin: %{
|
||||
message: "We encountered an issue with your site cache",
|
||||
recommendation:
|
||||
"Please install and activate our WordPress plugin to start counting your visitors",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
cache_general: %{
|
||||
message: "We encountered an issue with your site cache",
|
||||
recommendation:
|
||||
"Please clear your cache (or wait for your provider to clear it) to ensure that the latest version is displayed to your visitors",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site"
|
||||
},
|
||||
snippet_in_body: %{
|
||||
message: "Plausible snippet is placed in the body",
|
||||
recommendation: "Please relocate the snippet to the header of your site",
|
||||
url: "https://plausible.io/docs/troubleshoot-integration"
|
||||
},
|
||||
different_data_domain: %{
|
||||
message: "Your data-domain is different",
|
||||
recommendation: "Please ensure that the data-domain matches <%= @domain %> exactly",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#have-you-added-the-correct-data-domain-attribute-in-the-plausible-snippet"
|
||||
},
|
||||
illegal_attrs_wp_plugin: %{
|
||||
message: "A performance optimization plugin seems to have altered our snippet",
|
||||
recommendation:
|
||||
"Please whitelist our script in your performance optimization plugin to stop it from changing our snippet",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#has-some-other-plugin-altered-our-snippet"
|
||||
},
|
||||
illegal_attrs_wp_no_plugin: %{
|
||||
message: "A performance optimization plugin seems to have altered our snippet",
|
||||
recommendation:
|
||||
"Please install and activate our WordPress plugin to avoid the most common plugin conflicts",
|
||||
url: "https://plausible.io/wordpress-analytics-plugin"
|
||||
},
|
||||
illegal_attrs_general: %{
|
||||
message: "Something seems to have altered our snippet",
|
||||
recommendation:
|
||||
"Please manually check your integration to make sure that nothing prevents our script from working",
|
||||
url:
|
||||
"https://plausible.io/docs/troubleshoot-integration#has-some-other-plugin-altered-our-snippet"
|
||||
},
|
||||
unknown: %{
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
def all(), do: @errors
|
||||
|
||||
for {_, %{message: message, recommendation: recommendation} = e} <- @errors do
|
||||
if String.ends_with?(message, ".") or String.ends_with?(recommendation, ".") do
|
||||
raise "Error message/recommendation should not end with a period: #{inspect(e)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,8 +14,7 @@ defmodule Plausible.InstallationSupport.State do
|
|||
skip_further_checks?: false
|
||||
|
||||
@type diagnostics_type ::
|
||||
Plausible.InstallationSupport.LegacyVerification.Diagnostics.t()
|
||||
| Plausible.InstallationSupport.Verification.Diagnostics.t()
|
||||
Plausible.InstallationSupport.Verification.Diagnostics.t()
|
||||
| Plausible.InstallationSupport.Detection.Diagnostics.t()
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
|
|||
|
||||
@checks [
|
||||
Checks.Url,
|
||||
Checks.InstallationV2,
|
||||
Checks.InstallationV2CacheBust
|
||||
Checks.VerifyInstallation,
|
||||
Checks.VerifyInstallationCacheBust
|
||||
]
|
||||
|
||||
@spec run(String.t(), String.t(), String.t(), Keyword.t()) :: :ok
|
||||
@spec run(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, pid()} | State.t()
|
||||
def run(url, data_domain, installation_type, opts \\ []) do
|
||||
report_to = Keyword.get(opts, :report_to, self())
|
||||
async? = Keyword.get(opts, :async?, true)
|
||||
|
|
@ -65,8 +65,8 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
|
|||
{_, %{unhandled: true, browserless_issue: browserless_issue}} ->
|
||||
sentry_msg =
|
||||
if browserless_issue,
|
||||
do: "Browserless failure in verification (v2)",
|
||||
else: "Unhandled case for site verification (v2)"
|
||||
do: "Browserless failure in verification",
|
||||
else: "Unhandled case for site verification"
|
||||
|
||||
Sentry.capture_message(sentry_msg,
|
||||
extra: %{
|
||||
|
|
@ -77,7 +77,7 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
|
|||
)
|
||||
|
||||
Logger.warning(
|
||||
"[VERIFICATION v2] Unhandled case (data_domain='#{data_domain}'): #{inspect(diagnostics)}"
|
||||
"[VERIFICATION] Unhandled case (data_domain='#{data_domain}'): #{inspect(diagnostics)}"
|
||||
)
|
||||
|
||||
:telemetry.execute(telemetry_event_unhandled(), %{})
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
case Repo.transact(fn ->
|
||||
with {:ok, %{site: site}} <- Sites.create(user, params, team),
|
||||
{:ok, tracker_script_configuration} <-
|
||||
get_or_create_config(site, params["tracker_script_configuration"] || %{}, user) do
|
||||
get_or_create_config(site, params["tracker_script_configuration"] || %{}) do
|
||||
{:ok,
|
||||
struct(site,
|
||||
tracker_script_configuration: tracker_script_configuration
|
||||
|
|
@ -135,7 +135,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
end
|
||||
end) do
|
||||
{:ok, site} ->
|
||||
json(conn, get_site_response(site, user))
|
||||
json(conn, get_site_response(site))
|
||||
|
||||
{:error, {_, {:over_limit, limit}, _}} ->
|
||||
conn
|
||||
|
|
@ -176,16 +176,10 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
team = conn.assigns.current_team
|
||||
|
||||
with {:ok, site} <- find_site(user, team, site_id, [:owner, :admin, :editor, :viewer]),
|
||||
{:ok, tracker_script_configuration} <- get_or_create_config(site, %{}, user) do
|
||||
{:ok, tracker_script_configuration} <- get_or_create_config(site, %{}) do
|
||||
site = struct(site, tracker_script_configuration: tracker_script_configuration)
|
||||
|
||||
json(
|
||||
conn,
|
||||
get_site_response(
|
||||
site,
|
||||
user
|
||||
)
|
||||
)
|
||||
json(conn, get_site_response(site))
|
||||
else
|
||||
{:error, :site_not_found} ->
|
||||
H.not_found(conn, "Site could not be found")
|
||||
|
|
@ -212,8 +206,8 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
|
||||
with {:ok, params} <- validate_update_payload(params),
|
||||
{:ok, site} <- find_site(user, team, site_id, [:owner, :admin, :editor]),
|
||||
{:ok, site} <- do_update_site(site, params, user) do
|
||||
json(conn, get_site_response(site, user))
|
||||
{:ok, site} <- do_update_site(site, params) do
|
||||
json(conn, get_site_response(site))
|
||||
else
|
||||
{:error, :site_not_found} ->
|
||||
H.not_found(conn, "Site could not be found")
|
||||
|
|
@ -246,7 +240,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
end
|
||||
end
|
||||
|
||||
defp do_update_site(site, params, user) do
|
||||
defp do_update_site(site, params) do
|
||||
Repo.transact(fn ->
|
||||
with {:ok, site} <-
|
||||
if(params["domain"],
|
||||
|
|
@ -255,8 +249,8 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
),
|
||||
{:ok, tracker_script_configuration} <-
|
||||
if(params["tracker_script_configuration"],
|
||||
do: update_config(site, params["tracker_script_configuration"], user),
|
||||
else: get_or_create_config(site, %{}, user)
|
||||
do: update_config(site, params["tracker_script_configuration"]),
|
||||
else: get_or_create_config(site, %{})
|
||||
) do
|
||||
{:ok, struct(site, tracker_script_configuration: tracker_script_configuration)}
|
||||
end
|
||||
|
|
@ -581,31 +575,23 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_or_create_config(site, params, user) do
|
||||
if PlausibleWeb.Tracker.scriptv2?(site, user) do
|
||||
case PlausibleWeb.Tracker.get_or_create_tracker_script_configuration(site, params) do
|
||||
{:ok, tracker_script_configuration} ->
|
||||
{:ok, tracker_script_configuration}
|
||||
defp get_or_create_config(site, params) do
|
||||
case PlausibleWeb.Tracker.get_or_create_tracker_script_configuration(site, params) do
|
||||
{:ok, tracker_script_configuration} ->
|
||||
{:ok, tracker_script_configuration}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, {:tracker_script_configuration_invalid, changeset}}
|
||||
end
|
||||
else
|
||||
{:ok, %{}}
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, {:tracker_script_configuration_invalid, changeset}}
|
||||
end
|
||||
end
|
||||
|
||||
defp update_config(site, params, user) do
|
||||
if PlausibleWeb.Tracker.scriptv2?(site, user) do
|
||||
case PlausibleWeb.Tracker.update_script_configuration(site, params, :installation) do
|
||||
{:ok, tracker_script_configuration} ->
|
||||
{:ok, tracker_script_configuration}
|
||||
defp update_config(site, params) do
|
||||
case PlausibleWeb.Tracker.update_script_configuration(site, params, :installation) do
|
||||
{:ok, tracker_script_configuration} ->
|
||||
{:ok, tracker_script_configuration}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, {:tracker_script_configuration_invalid, changeset}}
|
||||
end
|
||||
else
|
||||
{:ok, %{}}
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:error, {:tracker_script_configuration_invalid, changeset}}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -613,15 +599,9 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
|||
site |> Map.take([:domain, :timezone])
|
||||
end
|
||||
|
||||
defp get_site_response(site, user) do
|
||||
serializable_properties =
|
||||
if(PlausibleWeb.Tracker.scriptv2?(site, user),
|
||||
do: [:domain, :timezone, :tracker_script_configuration],
|
||||
else: [:domain, :timezone]
|
||||
)
|
||||
|
||||
defp get_site_response(site) do
|
||||
site
|
||||
|> Map.take(serializable_properties)
|
||||
|> Map.take([:domain, :timezone, :tracker_script_configuration])
|
||||
# remap to `custom_properties`
|
||||
|> Map.put(:custom_properties, site.allowed_event_props || [])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
alias Plausible.InstallationSupport.{State, LegacyVerification, Verification}
|
||||
alias Plausible.InstallationSupport.{State, Verification}
|
||||
|
||||
@component PlausibleWeb.Live.Components.Verification
|
||||
@slowdown_for_frequent_checking :timer.seconds(5)
|
||||
|
|
@ -38,8 +38,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
super_admin? = Plausible.Auth.is_super_admin?(current_user)
|
||||
has_pageviews? = has_pageviews?(site)
|
||||
|
||||
custom_url_input? =
|
||||
PlausibleWeb.Tracker.scriptv2?(site, current_user) and params["custom_url"] == "true"
|
||||
custom_url_input? = params["custom_url"] == "true"
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
|
|
@ -49,7 +48,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
domain: domain,
|
||||
has_pageviews?: has_pageviews?,
|
||||
component: @component,
|
||||
installation_type: get_installation_type(params, site, current_user),
|
||||
installation_type: get_installation_type(params, site),
|
||||
report_to: self(),
|
||||
delay: private[:delay] || 500,
|
||||
slowdown: private[:slowdown] || 500,
|
||||
|
|
@ -122,22 +121,13 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
end
|
||||
|
||||
{:ok, pid} =
|
||||
if PlausibleWeb.Tracker.scriptv2?(socket.assigns.site, socket.assigns.current_user) do
|
||||
Verification.Checks.run(
|
||||
socket.assigns.url_to_verify,
|
||||
domain,
|
||||
socket.assigns.installation_type,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
)
|
||||
else
|
||||
LegacyVerification.Checks.run(
|
||||
"https://#{domain}",
|
||||
domain,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
)
|
||||
end
|
||||
Verification.Checks.run(
|
||||
socket.assigns.url_to_verify,
|
||||
domain,
|
||||
socket.assigns.installation_type,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, checks_pid: pid, attempts: socket.assigns.attempts + 1)}
|
||||
end
|
||||
|
|
@ -159,12 +149,7 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
end
|
||||
|
||||
def handle_info({:all_checks_done, %State{} = state}, socket) do
|
||||
interpretation =
|
||||
if PlausibleWeb.Tracker.scriptv2?(socket.assigns.site, socket.assigns.current_user) do
|
||||
Verification.Checks.interpret_diagnostics(state)
|
||||
else
|
||||
LegacyVerification.Checks.interpret_diagnostics(state)
|
||||
end
|
||||
interpretation = Verification.Checks.interpret_diagnostics(state)
|
||||
|
||||
if not socket.assigns.has_pageviews? do
|
||||
schedule_pageviews_check(socket)
|
||||
|
|
@ -195,20 +180,16 @@ defmodule PlausibleWeb.Live.Verification do
|
|||
|
||||
@supported_installation_types_atoms PlausibleWeb.Tracker.supported_installation_types()
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
defp get_installation_type(params, site, current_user) do
|
||||
if PlausibleWeb.Tracker.scriptv2?(site, current_user) do
|
||||
cond do
|
||||
params["installation_type"] in PlausibleWeb.Tracker.supported_installation_types() ->
|
||||
params["installation_type"]
|
||||
defp get_installation_type(params, site) do
|
||||
cond do
|
||||
params["installation_type"] in PlausibleWeb.Tracker.supported_installation_types() ->
|
||||
params["installation_type"]
|
||||
|
||||
(saved_installation_type = get_saved_installation_type(site)) in @supported_installation_types_atoms ->
|
||||
Atom.to_string(saved_installation_type)
|
||||
(saved_installation_type = get_saved_installation_type(site)) in @supported_installation_types_atoms ->
|
||||
Atom.to_string(saved_installation_type)
|
||||
|
||||
true ->
|
||||
PlausibleWeb.Tracker.fallback_installation_type()
|
||||
end
|
||||
else
|
||||
params["installation_type"]
|
||||
true ->
|
||||
PlausibleWeb.Tracker.fallback_installation_type()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -182,22 +182,6 @@ defmodule Plausible.PromEx.Plugins.PlausibleMetrics do
|
|||
tags: [:status],
|
||||
tag_values: &%{status: &1.status}
|
||||
),
|
||||
on_ee(
|
||||
do:
|
||||
counter(
|
||||
metric_prefix ++ [:verification, :js_elixir_diff],
|
||||
event_name:
|
||||
Plausible.InstallationSupport.Checks.Installation.telemetry_event(_diff = true)
|
||||
)
|
||||
),
|
||||
on_ee(
|
||||
do:
|
||||
counter(
|
||||
metric_prefix ++ [:verification, :js_elixir_match],
|
||||
event_name:
|
||||
Plausible.InstallationSupport.Checks.Installation.telemetry_event(_diff = false)
|
||||
)
|
||||
),
|
||||
on_ee(
|
||||
do:
|
||||
counter(
|
||||
|
|
|
|||
|
|
@ -747,41 +747,6 @@ defmodule PlausibleWeb.SiteController do
|
|||
)
|
||||
end
|
||||
|
||||
def change_domain(conn, _params) do
|
||||
if PlausibleWeb.Tracker.scriptv2?(conn.assigns.site) do
|
||||
redirect(conn,
|
||||
to: Routes.site_path(conn, :change_domain_v2, conn.assigns.site.domain)
|
||||
)
|
||||
else
|
||||
changeset = Plausible.Site.update_changeset(conn.assigns.site)
|
||||
|
||||
render(conn, "change_domain.html",
|
||||
skip_plausible_tracking: true,
|
||||
changeset: changeset
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def change_domain_submit(conn, %{"site" => %{"domain" => new_domain}}) do
|
||||
case Plausible.Site.Domain.change(conn.assigns.site, new_domain) do
|
||||
{:ok, updated_site} ->
|
||||
conn
|
||||
|> put_flash(:success, "Website domain changed successfully")
|
||||
|> redirect(
|
||||
to:
|
||||
Routes.site_path(conn, :installation, updated_site.domain,
|
||||
flow: PlausibleWeb.Flows.domain_change()
|
||||
)
|
||||
)
|
||||
|
||||
{:error, changeset} ->
|
||||
render(conn, "change_domain.html",
|
||||
skip_plausible_tracking: true,
|
||||
changeset: changeset
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp tolerate_unique_contraint_violation(result, name) do
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
defmodule PlausibleWeb.Live.ChangeDomainV2 do
|
||||
defmodule PlausibleWeb.Live.ChangeDomain do
|
||||
@moduledoc """
|
||||
LiveView for the change domain v2 flow.
|
||||
LiveView for the change domain flow.
|
||||
"""
|
||||
use Plausible
|
||||
use PlausibleWeb, :live_view
|
||||
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
alias PlausibleWeb.Live.ChangeDomainV2.Form
|
||||
alias PlausibleWeb.Live.ChangeDomain.Form
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
|
||||
on_ee do
|
||||
|
|
@ -59,7 +59,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2 do
|
|||
end
|
||||
end
|
||||
|
||||
def render(%{live_action: :change_domain_v2} = assigns) do
|
||||
def render(%{live_action: :change_domain} = assigns) do
|
||||
render_form_step(assigns)
|
||||
end
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule PlausibleWeb.Live.ChangeDomainV2.Form do
|
||||
defmodule PlausibleWeb.Live.ChangeDomain.Form do
|
||||
@moduledoc """
|
||||
Live component for the change domain form
|
||||
"""
|
||||
|
|
@ -2,33 +2,21 @@ defmodule PlausibleWeb.Live.Installation do
|
|||
@moduledoc """
|
||||
User assistance module around Plausible installation instructions/onboarding
|
||||
"""
|
||||
|
||||
use Plausible
|
||||
use PlausibleWeb, :live_view
|
||||
|
||||
require Logger
|
||||
|
||||
alias PlausibleWeb.Flows
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
alias PlausibleWeb.Live.Installation.Icons
|
||||
alias PlausibleWeb.Live.Installation.Instructions
|
||||
|
||||
on_ee do
|
||||
alias Plausible.InstallationSupport.{State, Checks, LegacyVerification}
|
||||
alias Plausible.InstallationSupport.{Detection, Result}
|
||||
end
|
||||
|
||||
@script_extension_params %{
|
||||
"outbound_links" => "outbound-links",
|
||||
"tagged_events" => "tagged-events",
|
||||
"file_downloads" => "file-downloads",
|
||||
"hash_based_routing" => "hash",
|
||||
"pageview_props" => "pageview-props",
|
||||
"revenue_tracking" => "revenue"
|
||||
}
|
||||
|
||||
@script_config_params ["track_404_pages" | Map.keys(@script_extension_params)]
|
||||
|
||||
@installation_types [
|
||||
"gtm",
|
||||
"manual",
|
||||
"wordpress"
|
||||
]
|
||||
|
||||
@valid_qs_params @script_config_params ++ ["installation_type", "flow"]
|
||||
|
||||
def script_extension_params, do: @script_extension_params
|
||||
|
||||
def mount(
|
||||
%{"domain" => domain} = params,
|
||||
_session,
|
||||
|
|
@ -45,506 +33,320 @@ defmodule PlausibleWeb.Live.Installation do
|
|||
]
|
||||
)
|
||||
|
||||
if PlausibleWeb.Tracker.scriptv2?(site) do
|
||||
{:ok,
|
||||
redirect(socket,
|
||||
to:
|
||||
Routes.site_path(
|
||||
socket,
|
||||
:installation_v2,
|
||||
site.domain,
|
||||
[flow: params["flow"], type: params["installation_type"]]
|
||||
|> Keyword.filter(fn {_k, v} -> not is_nil(v) and v != "" end)
|
||||
)
|
||||
)}
|
||||
else
|
||||
flow = params["flow"]
|
||||
|
||||
tracker_script_configuration =
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site)
|
||||
|
||||
installation_type = get_installation_type(flow, tracker_script_configuration, params)
|
||||
|
||||
config =
|
||||
Map.new(@script_config_params, fn key ->
|
||||
string_key = String.to_existing_atom(key)
|
||||
{key, Map.get(tracker_script_configuration, string_key)}
|
||||
end)
|
||||
flow = params["flow"] || Flows.provisioning()
|
||||
|
||||
socket =
|
||||
on_ee do
|
||||
if connected?(socket) and is_nil(installation_type) do
|
||||
LegacyVerification.Checks.run("https://#{domain}", domain,
|
||||
checks: [
|
||||
Checks.FetchBody,
|
||||
Checks.ScanBody
|
||||
if connected?(socket) do
|
||||
assign_async(
|
||||
socket,
|
||||
[
|
||||
:recommended_installation_type,
|
||||
:installation_type,
|
||||
:tracker_script_configuration_form,
|
||||
:v1_detected
|
||||
],
|
||||
report_to: self(),
|
||||
async?: true,
|
||||
slowdown: 0
|
||||
fn -> initialize_installation_data(flow, site, params) end
|
||||
)
|
||||
else
|
||||
assign_loading_states(socket)
|
||||
end
|
||||
else
|
||||
# On Community Edition, there's no v1 detection, nor pre-installation
|
||||
# site scan - we just default the pre-selected tab to "manual".
|
||||
|
||||
# Although it's functionally unnecessary, we stick to using `%AsyncResult{}`
|
||||
# for these assigns to minimize branching out the CE code and maintain only
|
||||
# a single `render` function.
|
||||
|
||||
{:ok, installation_data} = initialize_installation_data(flow, site, params)
|
||||
|
||||
assign(socket,
|
||||
recommended_installation_type: %AsyncResult{
|
||||
result: installation_data.recommended_installation_type,
|
||||
ok?: true
|
||||
},
|
||||
installation_type: %AsyncResult{
|
||||
result: installation_data.installation_type,
|
||||
ok?: true
|
||||
},
|
||||
tracker_script_configuration_form: %AsyncResult{
|
||||
result: installation_data.tracker_script_configuration_form,
|
||||
ok?: true
|
||||
},
|
||||
v1_detected: %AsyncResult{
|
||||
result: installation_data.v1_detected,
|
||||
ok?: true
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
uri_params: Map.take(params, @valid_qs_params),
|
||||
connected?: connected?(socket),
|
||||
site: site,
|
||||
site_created?: params["site_created"] == "true",
|
||||
flow: flow,
|
||||
installation_type: installation_type,
|
||||
initial_installation_type: installation_type,
|
||||
domain: domain,
|
||||
config: config
|
||||
)}
|
||||
end
|
||||
{:ok,
|
||||
assign(socket,
|
||||
site: site,
|
||||
flow: flow
|
||||
)}
|
||||
end
|
||||
|
||||
on_ee do
|
||||
def handle_info({:all_checks_done, %State{} = state}, socket) do
|
||||
installation_type =
|
||||
case state.diagnostics do
|
||||
%{wordpress_likely?: true} -> "wordpress"
|
||||
%{gtm_likely?: true} -> "gtm"
|
||||
_ -> "manual"
|
||||
end
|
||||
def handle_params(params, _url, socket) do
|
||||
socket =
|
||||
if connected?(socket) && socket.assigns.recommended_installation_type.result &&
|
||||
params["type"] in PlausibleWeb.Tracker.supported_installation_types() do
|
||||
assign(socket,
|
||||
installation_type: %AsyncResult{result: params["type"]}
|
||||
)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
initial_installation_type: installation_type,
|
||||
installation_type: installation_type
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.flash_messages flash={@flash} />
|
||||
<PlausibleWeb.Components.FirstDashboardLaunchBanner.set :if={@site_created?} site={@site} />
|
||||
<PlausibleWeb.Components.FlowProgress.render flow={@flow} current_step="Install Plausible" />
|
||||
|
||||
<.focus_box>
|
||||
<:title :if={is_nil(@installation_type)}>
|
||||
<div class="flex w-full mx-auto justify-center">
|
||||
<.spinner class="spinner block text-center h-8 w-8" />
|
||||
</div>
|
||||
</:title>
|
||||
<:title :if={@installation_type == "wordpress"}>
|
||||
Install WordPress plugin
|
||||
</:title>
|
||||
<:title :if={@installation_type == "gtm"}>
|
||||
Install Google Tag Manager
|
||||
</:title>
|
||||
<:title :if={@installation_type == "manual"}>
|
||||
Manual installation
|
||||
</:title>
|
||||
<.async_result :let={recommended_installation_type} assign={@recommended_installation_type}>
|
||||
<:loading>
|
||||
<div class="text-center text-gray-500">
|
||||
{if(@flow == Flows.review(),
|
||||
do: "Scanning your site to detect how Plausible is integrated...",
|
||||
else: "Determining the simplest integration path for your website..."
|
||||
)}
|
||||
</div>
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<.spinner class="w-6 h-6" />
|
||||
</div>
|
||||
</:loading>
|
||||
|
||||
<:subtitle :if={is_nil(@installation_type)}>
|
||||
<div class="text-center mt-8">
|
||||
Determining installation type...
|
||||
<.styled_link
|
||||
:if={@connected?}
|
||||
href="#"
|
||||
phx-click="switch-installation-type"
|
||||
phx-value-method="manual"
|
||||
<div class="grid grid-cols-2 sm:flex sm:flex-row gap-2 bg-gray-100 dark:bg-gray-900 rounded-md p-1">
|
||||
<.tab
|
||||
patch={"?type=manual&flow=#{@flow}"}
|
||||
selected={@installation_type.result == "manual"}
|
||||
>
|
||||
Skip
|
||||
</.styled_link>
|
||||
</div>
|
||||
</:subtitle>
|
||||
|
||||
<:subtitle :if={@flow == PlausibleWeb.Flows.domain_change()}>
|
||||
<p class="mb-4">
|
||||
Your domain has been changed.
|
||||
<strong>
|
||||
You must update the Plausible Installation on your site within 72 hours to guarantee continuous tracking.
|
||||
</strong>
|
||||
<br />
|
||||
<br /> If you're using the API, please also make sure to update your API credentials.
|
||||
</p>
|
||||
</:subtitle>
|
||||
|
||||
<:subtitle :if={@flow == PlausibleWeb.Flows.review() and not is_nil(@installation_type)}>
|
||||
<p class="mb-4">
|
||||
Review your existing installation. You can skip this step and proceed to verifying your installation.
|
||||
</p>
|
||||
</:subtitle>
|
||||
|
||||
<:subtitle :if={@installation_type == "wordpress"}>
|
||||
We've detected your website is using WordPress. Here's how to integrate Plausible:
|
||||
<.focus_list>
|
||||
<:item>
|
||||
<.styled_link href="https://plausible.io/wordpress-analytics-plugin" new_tab={true}>
|
||||
Install our WordPress plugin
|
||||
</.styled_link>
|
||||
</:item>
|
||||
<:item>
|
||||
After activating our plugin, click the button below to verify your installation
|
||||
</:item>
|
||||
</.focus_list>
|
||||
</:subtitle>
|
||||
<:subtitle :if={@installation_type == "gtm"}>
|
||||
We've detected your website is using Google Tag Manager. Here's how to integrate Plausible:
|
||||
<.focus_list>
|
||||
<:item>
|
||||
<.styled_link href="https://plausible.io/docs/google-tag-manager" new_tab={true}>
|
||||
Read our Tag Manager guide
|
||||
</.styled_link>
|
||||
</:item>
|
||||
<:item>
|
||||
Paste this snippet into GTM's Custom HTML section. Once done, click the button below to verify your installation.
|
||||
</:item>
|
||||
</.focus_list>
|
||||
</:subtitle>
|
||||
|
||||
<:subtitle :if={@installation_type == "manual"}>
|
||||
Paste this snippet into the <code><head></code>
|
||||
section of your site. See our
|
||||
<.styled_link href="https://plausible.io/docs/integration-guides" new_tab={true}>
|
||||
installation guides.
|
||||
</.styled_link>
|
||||
Once done, click the button below to verify your installation.
|
||||
</:subtitle>
|
||||
|
||||
<div :if={@installation_type in ["manual", "gtm"]}>
|
||||
<.snippet_form installation_type={@installation_type} config={@config} domain={@domain} />
|
||||
</div>
|
||||
|
||||
<.button_link
|
||||
:if={not is_nil(@installation_type)}
|
||||
href={"/#{URI.encode_www_form(@domain)}/verification?#{URI.encode_query(@uri_params)}"}
|
||||
type="submit"
|
||||
class="w-full mt-8"
|
||||
>
|
||||
<%= if @flow == PlausibleWeb.Flows.domain_change() do %>
|
||||
I understand, I'll update my website
|
||||
<% else %>
|
||||
<%= if @flow == PlausibleWeb.Flows.review() do %>
|
||||
Verify your installation
|
||||
<% else %>
|
||||
Start collecting data
|
||||
<Icons.script_icon /> Script
|
||||
</.tab>
|
||||
<.tab
|
||||
patch={"?type=wordpress&flow=#{@flow}"}
|
||||
selected={@installation_type.result == "wordpress"}
|
||||
>
|
||||
<Icons.wordpress_icon /> WordPress
|
||||
</.tab>
|
||||
<%= on_ee do %>
|
||||
<.tab patch={"?type=gtm&flow=#{@flow}"} selected={@installation_type.result == "gtm"}>
|
||||
<Icons.tag_manager_icon /> Tag Manager
|
||||
</.tab>
|
||||
<% end %>
|
||||
<.tab patch={"?type=npm&flow=#{@flow}"} selected={@installation_type.result == "npm"}>
|
||||
<Icons.npm_icon /> NPM
|
||||
</.tab>
|
||||
</div>
|
||||
|
||||
<%= on_ee do %>
|
||||
<.outdated_script_notice
|
||||
:if={@v1_detected.result == true}
|
||||
recommended_installation_type={@recommended_installation_type}
|
||||
installation_type={@installation_type}
|
||||
/>
|
||||
<% end %>
|
||||
</.button_link>
|
||||
|
||||
<:footer :if={@initial_installation_type == "wordpress" and @installation_type == "manual"}>
|
||||
<.styled_link href={} phx-click="switch-installation-type" phx-value-method="wordpress">
|
||||
Click here
|
||||
</.styled_link>
|
||||
if you prefer WordPress installation method.
|
||||
</:footer>
|
||||
<.form for={@tracker_script_configuration_form.result} phx-submit="submit" class="mt-4">
|
||||
<.input
|
||||
type="hidden"
|
||||
field={@tracker_script_configuration_form.result[:installation_type]}
|
||||
value={@installation_type.result}
|
||||
/>
|
||||
<Instructions.manual_instructions
|
||||
:if={@installation_type.result == "manual"}
|
||||
tracker_script_configuration_form={@tracker_script_configuration_form.result}
|
||||
/>
|
||||
|
||||
<:footer :if={
|
||||
(@initial_installation_type == "gtm" and @installation_type == "manual") or
|
||||
(@initial_installation_type == "manual" and @installation_type == "manual")
|
||||
}>
|
||||
<.styled_link href={} phx-click="switch-installation-type" phx-value-method="gtm">
|
||||
Click here
|
||||
</.styled_link>
|
||||
if you prefer Google Tag Manager installation method.
|
||||
</:footer>
|
||||
<Instructions.wordpress_instructions
|
||||
:if={@installation_type.result == "wordpress"}
|
||||
flow={@flow}
|
||||
recommended_installation_type={recommended_installation_type}
|
||||
/>
|
||||
<%= on_ee do %>
|
||||
<Instructions.gtm_instructions
|
||||
:if={@installation_type.result == "gtm"}
|
||||
recommended_installation_type={recommended_installation_type}
|
||||
tracker_script_configuration_form={@tracker_script_configuration_form.result}
|
||||
/>
|
||||
<% end %>
|
||||
<Instructions.npm_instructions :if={@installation_type.result == "npm"} />
|
||||
|
||||
<:footer :if={not is_nil(@installation_type) and @installation_type != "manual"}>
|
||||
<.styled_link href={} phx-click="switch-installation-type" phx-value-method="manual">
|
||||
Click here
|
||||
</.styled_link>
|
||||
if you prefer manual installation method.
|
||||
<.button type="submit" class="w-full mt-8">
|
||||
{verify_cta(@installation_type.result)}
|
||||
</.button>
|
||||
</.form>
|
||||
</.async_result>
|
||||
<:footer :if={ce?() and @installation_type.result == "manual"}>
|
||||
<.focus_list>
|
||||
<:item>
|
||||
Still using the legacy snippet with the data-domain attribute? See
|
||||
<.styled_link href="https://plausible.io/docs/script-update-guide">
|
||||
migration guide
|
||||
</.styled_link>
|
||||
</:item>
|
||||
</.focus_list>
|
||||
</:footer>
|
||||
</.focus_box>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_snippet("manual", domain, %{"track_404_pages" => true} = script_config) do
|
||||
script_config = Map.put(script_config, "track_404_pages", false)
|
||||
defp verify_cta("manual"), do: "Verify Script installation"
|
||||
defp verify_cta("wordpress"), do: "Verify WordPress installation"
|
||||
defp verify_cta("gtm"), do: "Verify Tag Manager installation"
|
||||
defp verify_cta("npm"), do: "Verify NPM installation"
|
||||
|
||||
"""
|
||||
#{render_snippet("manual", domain, script_config)}
|
||||
#{render_snippet_404()}
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_snippet("manual", domain, script_config) do
|
||||
~s|<script defer data-domain="#{domain}" src="#{tracker_url(script_config)}"></script>|
|
||||
end
|
||||
|
||||
defp render_snippet("gtm", domain, %{"track_404_pages" => true} = script_config) do
|
||||
script_config = Map.put(script_config, "track_404_pages", false)
|
||||
|
||||
"""
|
||||
#{render_snippet("gtm", domain, script_config)}
|
||||
#{render_snippet_404("gtm")}
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_snippet("gtm", domain, script_config) do
|
||||
"""
|
||||
<script>
|
||||
var script = document.createElement('script');
|
||||
script.defer = true;
|
||||
script.dataset.domain = "#{domain}";
|
||||
script.dataset.api = "https://plausible.io/api/event";
|
||||
script.src = "#{tracker_url(script_config)}";
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
</script>
|
||||
"""
|
||||
end
|
||||
|
||||
def render_snippet_404() do
|
||||
"<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>"
|
||||
end
|
||||
|
||||
def render_snippet_404("gtm") do
|
||||
render_snippet_404()
|
||||
end
|
||||
|
||||
defp script_extension_control(assigns) do
|
||||
~H"""
|
||||
<div class="mt-2 p-1 text-sm">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={"check-#{@variant}"}
|
||||
name={@variant}
|
||||
checked={Map.get(@config, @variant, false)}
|
||||
class="block h-5 w-5 rounded-sm dark:bg-gray-700 border-gray-300 text-indigo-600 focus:ring-indigo-600 mr-2"
|
||||
/>
|
||||
<label for={"check-#{@variant}"}>
|
||||
{@label}
|
||||
</label>
|
||||
<div class="ml-2 collapse md:visible">
|
||||
<.tooltip sticky?={false}>
|
||||
<:tooltip_content>
|
||||
{@tooltip}
|
||||
<br /><br />Click to learn more.
|
||||
</:tooltip_content>
|
||||
<a href={@learn_more} target="_blank" rel="noopener noreferrer">
|
||||
<Heroicons.information_circle class="text-indigo-700 dark:text-gray-500 w-5 h-5 hover:stroke-2" />
|
||||
</a>
|
||||
</.tooltip>
|
||||
</div>
|
||||
<div class="ml-2 visible md:invisible">
|
||||
<a href={@learn_more} target="_blank" rel="noopener noreferrer">
|
||||
<Heroicons.information_circle class="text-indigo-700 dark:text-gray-500 w-5 h-5 hover:stroke-2" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp snippet_form(assigns) do
|
||||
~H"""
|
||||
<form id="snippet-form" phx-change="update-script-config">
|
||||
<div class="relative">
|
||||
<textarea
|
||||
id="snippet"
|
||||
class="w-full border-1 border-gray-300 rounded-md p-4 text-sm text-gray-700 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300"
|
||||
rows="5"
|
||||
readonly
|
||||
><%= render_snippet(@installation_type, @domain, @config) %></textarea>
|
||||
|
||||
<a
|
||||
onclick="var input = document.getElementById('snippet'); input.focus(); input.select(); document.execCommand('copy'); event.stopPropagation();"
|
||||
href="javascript:void(0)"
|
||||
class="absolute flex items-center text-xs font-medium text-indigo-600 no-underline hover:underline bottom-2 right-4 p-2 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<Heroicons.document_duplicate class="pr-1 text-indigo-600 dark:text-indigo-500 w-5 h-5" />
|
||||
<span>
|
||||
COPY
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<.h2 class="mt-8 text-sm font-medium">Enable optional measurements:</.h2>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="outbound_links"
|
||||
label="Outbound links"
|
||||
tooltip="Automatically track clicks on external links. These count towards your billable pageviews."
|
||||
learn_more="https://plausible.io/docs/outbound-link-click-tracking"
|
||||
/>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="file_downloads"
|
||||
label="File downloads"
|
||||
tooltip="Automatically track file downloads. These count towards your billable pageviews."
|
||||
learn_more="https://plausible.io/docs/file-downloads-tracking"
|
||||
/>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="track_404_pages"
|
||||
label="404 error pages"
|
||||
tooltip="Find 404 error pages on your site. These count towards your billable pageviews. Additional action required."
|
||||
learn_more="https://plausible.io/docs/error-pages-tracking-404"
|
||||
/>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="hash_based_routing"
|
||||
label="Hashed page paths"
|
||||
tooltip="Automatically track page paths that use a # in the URL."
|
||||
learn_more="https://plausible.io/docs/hash-based-routing"
|
||||
/>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="tagged_events"
|
||||
label="Custom events"
|
||||
tooltip="Tag site elements like buttons, links and forms to track user activity. These count towards your billable pageviews. Additional action required."
|
||||
learn_more="https://plausible.io/docs/custom-event-goals"
|
||||
/>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="pageview_props"
|
||||
label="Custom properties"
|
||||
tooltip="Attach custom properties (also known as custom dimensions) to pageviews or custom events to create custom metrics. Additional action required."
|
||||
learn_more="https://plausible.io/docs/custom-props/introduction"
|
||||
/>
|
||||
<.script_extension_control
|
||||
config={@config}
|
||||
variant="revenue_tracking"
|
||||
label="Ecommerce revenue"
|
||||
tooltip="Assign monetary values to purchases and track revenue attribution. Additional action required."
|
||||
learn_more="https://plausible.io/docs/ecommerce-revenue-tracking"
|
||||
/>
|
||||
</form>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("switch-installation-type", %{"method" => method}, socket)
|
||||
when method in @installation_types do
|
||||
socket = update_uri_params(socket, %{"installation_type" => method})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("update-script-config", params, socket) do
|
||||
new_config =
|
||||
@script_config_params
|
||||
|> Map.new(fn key -> {key, Map.get(params, key) == "on"} end)
|
||||
|
||||
flash = snippet_change_flash(socket.assigns.config, new_config)
|
||||
|
||||
socket =
|
||||
if flash do
|
||||
put_live_flash(socket, :success, flash)
|
||||
on_ee do
|
||||
defp detect_recommended_installation_type(flow, site) do
|
||||
with {:ok, detection_result} <-
|
||||
Detection.Checks.run_with_rate_limit(nil, site.domain,
|
||||
detect_v1?: flow == Flows.review(),
|
||||
report_to: nil,
|
||||
slowdown: 0,
|
||||
async?: false
|
||||
),
|
||||
%Result{ok?: true, data: data} <-
|
||||
Detection.Checks.interpret_diagnostics(detection_result) do
|
||||
{data.suggested_technology, data.v1_detected}
|
||||
else
|
||||
socket
|
||||
_ -> {PlausibleWeb.Tracker.fallback_installation_type(), false}
|
||||
end
|
||||
end
|
||||
else
|
||||
defp detect_recommended_installation_type(_flow, _site) do
|
||||
{PlausibleWeb.Tracker.fallback_installation_type(), false}
|
||||
end
|
||||
end
|
||||
|
||||
on_ee do
|
||||
defp outdated_script_notice(assigns) do
|
||||
~H"""
|
||||
<div :if={
|
||||
@recommended_installation_type.result == "manual" and
|
||||
@installation_type.result == "manual"
|
||||
}>
|
||||
<.notice class="mt-4" theme={:yellow}>
|
||||
Your website is running an outdated version of the tracking script. Please
|
||||
<.styled_link new_tab href="https://plausible.io/docs/script-update-guide">
|
||||
update
|
||||
</.styled_link>
|
||||
your tracking script before continuing
|
||||
</.notice>
|
||||
</div>
|
||||
|
||||
<div :if={
|
||||
@recommended_installation_type.result == "gtm" and
|
||||
@installation_type.result == "gtm"
|
||||
}>
|
||||
<.notice class="mt-4" theme={:yellow}>
|
||||
Your website might be using an outdated version of our Google Tag Manager template.
|
||||
If so,
|
||||
<.styled_link new_tab href="https://plausible.io/docs/script-update-guide#gtm">
|
||||
update
|
||||
</.styled_link>
|
||||
your Google Tag Manager template before continuing
|
||||
</.notice>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp assign_loading_states(socket) do
|
||||
assign(socket,
|
||||
recommended_installation_type: AsyncResult.loading(),
|
||||
v1_detected: AsyncResult.loading(),
|
||||
installation_type: AsyncResult.loading(),
|
||||
tracker_script_configuration_form: AsyncResult.loading()
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
attr :selected, :boolean, default: false
|
||||
attr :patch, :string, required: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
defp tab(assigns) do
|
||||
assigns =
|
||||
if assigns[:selected] do
|
||||
assign(assigns,
|
||||
class:
|
||||
"bg-white dark:bg-gray-800 rounded-md px-3.5 py-2.5 text-sm font-medium flex items-center flex-1 justify-center whitespace-nowrap"
|
||||
)
|
||||
else
|
||||
assign(assigns,
|
||||
class:
|
||||
"bg-gray-100 dark:bg-gray-700 rounded-md px-3.5 py-2.5 text-sm font-medium flex items-center cursor-pointer flex-1 justify-center whitespace-nowrap"
|
||||
)
|
||||
end
|
||||
|
||||
socket = update_uri_params(socket, new_config)
|
||||
{:noreply, socket}
|
||||
~H"""
|
||||
<.link patch={@patch} class={@class}>
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_params(params, _uri, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> update_installation_type(params)
|
||||
|> update_script_config(params)
|
||||
|> persist_tracker_script_configuration()
|
||||
def handle_event("submit", %{"tracker_script_configuration" => params}, socket) do
|
||||
config =
|
||||
PlausibleWeb.Tracker.update_script_configuration!(
|
||||
socket.assigns.site,
|
||||
params,
|
||||
:installation
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply,
|
||||
push_navigate(socket,
|
||||
to:
|
||||
Routes.site_path(socket, :verification, socket.assigns.site.domain,
|
||||
flow: socket.assigns.flow,
|
||||
installation_type: config.installation_type
|
||||
)
|
||||
)}
|
||||
end
|
||||
|
||||
defp update_installation_type(socket, %{"installation_type" => installation_type})
|
||||
when installation_type in @installation_types do
|
||||
assign(socket,
|
||||
installation_type: installation_type,
|
||||
uri_params: Map.put(socket.assigns.uri_params, "installation_type", installation_type)
|
||||
)
|
||||
end
|
||||
defp initialize_installation_data(flow, site, params) do
|
||||
{recommended_installation_type, v1_detected} =
|
||||
detect_recommended_installation_type(flow, site)
|
||||
|
||||
defp update_installation_type(socket, _params), do: socket
|
||||
|
||||
defp update_script_config(socket, params) do
|
||||
configuration_update =
|
||||
@script_config_params
|
||||
|> Enum.filter(&Map.has_key?(params, &1))
|
||||
|> Map.new(fn key -> {key, Map.get(params, key) == "true"} end)
|
||||
|
||||
assign(socket,
|
||||
config: Map.merge(socket.assigns.config, configuration_update)
|
||||
)
|
||||
end
|
||||
|
||||
defp update_uri_params(socket, params) when is_map(params) do
|
||||
uri_params = Map.merge(socket.assigns.uri_params, params)
|
||||
|
||||
socket
|
||||
|> assign(uri_params: uri_params)
|
||||
|> push_patch(
|
||||
to:
|
||||
Routes.site_path(
|
||||
socket,
|
||||
:installation,
|
||||
socket.assigns.domain,
|
||||
uri_params
|
||||
),
|
||||
replace: true
|
||||
)
|
||||
end
|
||||
|
||||
@domain_change PlausibleWeb.Flows.domain_change()
|
||||
defp get_installation_type(@domain_change, tracker_script_configuration, params) do
|
||||
case tracker_script_configuration.installation_type do
|
||||
nil ->
|
||||
get_installation_type(nil, nil, params)
|
||||
|
||||
installation_type ->
|
||||
Atom.to_string(installation_type)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_installation_type(_type, _tracker_script_configuration, params) do
|
||||
Enum.find(@installation_types, &(&1 == params["installation_type"]))
|
||||
end
|
||||
|
||||
defp tracker_url(script_config) do
|
||||
extensions =
|
||||
@script_extension_params
|
||||
|> Enum.flat_map(fn {key, extension} ->
|
||||
if(Map.get(script_config, key), do: [extension], else: [])
|
||||
end)
|
||||
|
||||
tracker = Enum.join(["script" | extensions], ".")
|
||||
|
||||
"#{PlausibleWeb.Endpoint.url()}/js/#{tracker}.js"
|
||||
end
|
||||
|
||||
defp persist_tracker_script_configuration(socket) do
|
||||
tracker_script_config_update =
|
||||
Map.merge(socket.assigns.config, %{
|
||||
"site_id" => socket.assigns.site.id,
|
||||
"installation_type" => socket.assigns.installation_type
|
||||
tracker_script_configuration =
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
|
||||
outbound_links: true,
|
||||
form_submissions: true,
|
||||
file_downloads: true,
|
||||
track_404_pages: true,
|
||||
installation_type: recommended_installation_type
|
||||
})
|
||||
|
||||
PlausibleWeb.Tracker.update_script_configuration!(
|
||||
socket.assigns.site,
|
||||
tracker_script_config_update,
|
||||
:installation
|
||||
)
|
||||
selected_installation_type =
|
||||
cond do
|
||||
params["type"] in PlausibleWeb.Tracker.supported_installation_types() ->
|
||||
params["type"]
|
||||
|
||||
socket
|
||||
end
|
||||
flow == Flows.review() and
|
||||
not is_nil(tracker_script_configuration.installation_type) ->
|
||||
Atom.to_string(tracker_script_configuration.installation_type)
|
||||
|
||||
defp snippet_change_flash(old_config, new_config) do
|
||||
change =
|
||||
Enum.find(new_config, fn {key, new_value} ->
|
||||
Map.get(old_config, key) != new_value
|
||||
end)
|
||||
true ->
|
||||
recommended_installation_type
|
||||
end
|
||||
|
||||
case change do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
{k, false} when k in ["outbound_links", "file_downloads", "track_404_pages"] ->
|
||||
"Snippet updated and goal deleted. Please insert the newest snippet into your site"
|
||||
|
||||
{_, _} ->
|
||||
"Snippet updated. Please insert the newest snippet into your site"
|
||||
end
|
||||
{:ok,
|
||||
%{
|
||||
recommended_installation_type: recommended_installation_type,
|
||||
v1_detected: v1_detected,
|
||||
installation_type: selected_installation_type,
|
||||
tracker_script_configuration_form:
|
||||
to_form(
|
||||
Plausible.Site.TrackerScriptConfiguration.installation_changeset(
|
||||
tracker_script_configuration,
|
||||
%{}
|
||||
)
|
||||
)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule PlausibleWeb.Live.InstallationV2.Icons do
|
||||
defmodule PlausibleWeb.Live.Installation.Icons do
|
||||
@moduledoc """
|
||||
Icon components for InstallationV2 module
|
||||
Icon components for the Installation module
|
||||
"""
|
||||
use Phoenix.Component
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule PlausibleWeb.Live.InstallationV2.Instructions do
|
||||
defmodule PlausibleWeb.Live.Installation.Instructions do
|
||||
@moduledoc """
|
||||
Instruction forms and components for InstallationV2 module
|
||||
Instruction forms and components for the Installation module
|
||||
"""
|
||||
use PlausibleWeb, :component
|
||||
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
defmodule PlausibleWeb.Live.InstallationV2 do
|
||||
@moduledoc """
|
||||
User assistance module around Plausible installation instructions/onboarding
|
||||
"""
|
||||
|
||||
use Plausible
|
||||
use PlausibleWeb, :live_view
|
||||
|
||||
require Logger
|
||||
|
||||
alias PlausibleWeb.Flows
|
||||
alias Phoenix.LiveView.AsyncResult
|
||||
alias PlausibleWeb.Live.InstallationV2.Icons
|
||||
alias PlausibleWeb.Live.InstallationV2.Instructions
|
||||
|
||||
on_ee do
|
||||
alias Plausible.InstallationSupport.{Detection, Result}
|
||||
end
|
||||
|
||||
def mount(
|
||||
%{"domain" => domain} = params,
|
||||
_session,
|
||||
socket
|
||||
) do
|
||||
site =
|
||||
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain,
|
||||
roles: [
|
||||
:owner,
|
||||
:admin,
|
||||
:editor,
|
||||
:super_admin,
|
||||
:viewer
|
||||
]
|
||||
)
|
||||
|
||||
flow = params["flow"] || Flows.provisioning()
|
||||
|
||||
socket =
|
||||
on_ee do
|
||||
if connected?(socket) do
|
||||
assign_async(
|
||||
socket,
|
||||
[
|
||||
:recommended_installation_type,
|
||||
:installation_type,
|
||||
:tracker_script_configuration_form,
|
||||
:v1_detected
|
||||
],
|
||||
fn -> initialize_installation_data(flow, site, params) end
|
||||
)
|
||||
else
|
||||
assign_loading_states(socket)
|
||||
end
|
||||
else
|
||||
# On Community Edition, there's no v1 detection, nor pre-installation
|
||||
# site scan - we just default the pre-selected tab to "manual".
|
||||
|
||||
# Although it's functionally unnecessary, we stick to using `%AsyncResult{}`
|
||||
# for these assigns to minimize branching out the CE code and maintain only
|
||||
# a single `render` function.
|
||||
|
||||
{:ok, installation_data} = initialize_installation_data(flow, site, params)
|
||||
|
||||
assign(socket,
|
||||
recommended_installation_type: %AsyncResult{
|
||||
result: installation_data.recommended_installation_type,
|
||||
ok?: true
|
||||
},
|
||||
installation_type: %AsyncResult{
|
||||
result: installation_data.installation_type,
|
||||
ok?: true
|
||||
},
|
||||
tracker_script_configuration_form: %AsyncResult{
|
||||
result: installation_data.tracker_script_configuration_form,
|
||||
ok?: true
|
||||
},
|
||||
v1_detected: %AsyncResult{
|
||||
result: installation_data.v1_detected,
|
||||
ok?: true
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
site: site,
|
||||
flow: flow
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_params(params, _url, socket) do
|
||||
socket =
|
||||
if connected?(socket) && socket.assigns.recommended_installation_type.result &&
|
||||
params["type"] in PlausibleWeb.Tracker.supported_installation_types() do
|
||||
assign(socket,
|
||||
installation_type: %AsyncResult{result: params["type"]}
|
||||
)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<PlausibleWeb.Components.FlowProgress.render flow={@flow} current_step="Install Plausible" />
|
||||
|
||||
<.focus_box>
|
||||
<.async_result :let={recommended_installation_type} assign={@recommended_installation_type}>
|
||||
<:loading>
|
||||
<div class="text-center text-gray-500">
|
||||
{if(@flow == Flows.review(),
|
||||
do: "Scanning your site to detect how Plausible is integrated...",
|
||||
else: "Determining the simplest integration path for your website..."
|
||||
)}
|
||||
</div>
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<.spinner class="w-6 h-6" />
|
||||
</div>
|
||||
</:loading>
|
||||
|
||||
<div class="grid grid-cols-2 sm:flex sm:flex-row gap-2 bg-gray-100 dark:bg-gray-900 rounded-md p-1">
|
||||
<.tab
|
||||
patch={"?type=manual&flow=#{@flow}"}
|
||||
selected={@installation_type.result == "manual"}
|
||||
>
|
||||
<Icons.script_icon /> Script
|
||||
</.tab>
|
||||
<.tab
|
||||
patch={"?type=wordpress&flow=#{@flow}"}
|
||||
selected={@installation_type.result == "wordpress"}
|
||||
>
|
||||
<Icons.wordpress_icon /> WordPress
|
||||
</.tab>
|
||||
<%= on_ee do %>
|
||||
<.tab patch={"?type=gtm&flow=#{@flow}"} selected={@installation_type.result == "gtm"}>
|
||||
<Icons.tag_manager_icon /> Tag Manager
|
||||
</.tab>
|
||||
<% end %>
|
||||
<.tab patch={"?type=npm&flow=#{@flow}"} selected={@installation_type.result == "npm"}>
|
||||
<Icons.npm_icon /> NPM
|
||||
</.tab>
|
||||
</div>
|
||||
|
||||
<%= on_ee do %>
|
||||
<.outdated_script_notice
|
||||
:if={@v1_detected.result == true}
|
||||
recommended_installation_type={@recommended_installation_type}
|
||||
installation_type={@installation_type}
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<.form for={@tracker_script_configuration_form.result} phx-submit="submit" class="mt-4">
|
||||
<.input
|
||||
type="hidden"
|
||||
field={@tracker_script_configuration_form.result[:installation_type]}
|
||||
value={@installation_type.result}
|
||||
/>
|
||||
<Instructions.manual_instructions
|
||||
:if={@installation_type.result == "manual"}
|
||||
tracker_script_configuration_form={@tracker_script_configuration_form.result}
|
||||
/>
|
||||
|
||||
<Instructions.wordpress_instructions
|
||||
:if={@installation_type.result == "wordpress"}
|
||||
flow={@flow}
|
||||
recommended_installation_type={recommended_installation_type}
|
||||
/>
|
||||
<%= on_ee do %>
|
||||
<Instructions.gtm_instructions
|
||||
:if={@installation_type.result == "gtm"}
|
||||
recommended_installation_type={recommended_installation_type}
|
||||
tracker_script_configuration_form={@tracker_script_configuration_form.result}
|
||||
/>
|
||||
<% end %>
|
||||
<Instructions.npm_instructions :if={@installation_type.result == "npm"} />
|
||||
|
||||
<.button type="submit" class="w-full mt-8">
|
||||
{verify_cta(@installation_type.result)}
|
||||
</.button>
|
||||
</.form>
|
||||
</.async_result>
|
||||
<:footer :if={ce?() and @installation_type.result == "manual"}>
|
||||
<.focus_list>
|
||||
<:item>
|
||||
Still using the legacy snippet with the data-domain attribute? See
|
||||
<.styled_link href="https://plausible.io/docs/script-update-guide">
|
||||
migration guide
|
||||
</.styled_link>
|
||||
</:item>
|
||||
</.focus_list>
|
||||
</:footer>
|
||||
</.focus_box>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp verify_cta("manual"), do: "Verify Script installation"
|
||||
defp verify_cta("wordpress"), do: "Verify WordPress installation"
|
||||
defp verify_cta("gtm"), do: "Verify Tag Manager installation"
|
||||
defp verify_cta("npm"), do: "Verify NPM installation"
|
||||
|
||||
on_ee do
|
||||
defp detect_recommended_installation_type(flow, site) do
|
||||
with {:ok, detection_result} <-
|
||||
Detection.Checks.run_with_rate_limit(nil, site.domain,
|
||||
detect_v1?: flow == Flows.review(),
|
||||
report_to: nil,
|
||||
slowdown: 0,
|
||||
async?: false
|
||||
),
|
||||
%Result{ok?: true, data: data} <-
|
||||
Detection.Checks.interpret_diagnostics(detection_result) do
|
||||
{data.suggested_technology, data.v1_detected}
|
||||
else
|
||||
_ -> {PlausibleWeb.Tracker.fallback_installation_type(), false}
|
||||
end
|
||||
end
|
||||
else
|
||||
defp detect_recommended_installation_type(_flow, _site) do
|
||||
{PlausibleWeb.Tracker.fallback_installation_type(), false}
|
||||
end
|
||||
end
|
||||
|
||||
on_ee do
|
||||
defp outdated_script_notice(assigns) do
|
||||
~H"""
|
||||
<div :if={
|
||||
@recommended_installation_type.result == "manual" and
|
||||
@installation_type.result == "manual"
|
||||
}>
|
||||
<.notice class="mt-4" theme={:yellow}>
|
||||
Your website is running an outdated version of the tracking script. Please
|
||||
<.styled_link new_tab href="https://plausible.io/docs/script-update-guide">
|
||||
update
|
||||
</.styled_link>
|
||||
your tracking script before continuing
|
||||
</.notice>
|
||||
</div>
|
||||
|
||||
<div :if={
|
||||
@recommended_installation_type.result == "gtm" and
|
||||
@installation_type.result == "gtm"
|
||||
}>
|
||||
<.notice class="mt-4" theme={:yellow}>
|
||||
Your website might be using an outdated version of our Google Tag Manager template.
|
||||
If so,
|
||||
<.styled_link new_tab href="https://plausible.io/docs/script-update-guide#gtm">
|
||||
update
|
||||
</.styled_link>
|
||||
your Google Tag Manager template before continuing
|
||||
</.notice>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp assign_loading_states(socket) do
|
||||
assign(socket,
|
||||
recommended_installation_type: AsyncResult.loading(),
|
||||
v1_detected: AsyncResult.loading(),
|
||||
installation_type: AsyncResult.loading(),
|
||||
tracker_script_configuration_form: AsyncResult.loading()
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
attr :selected, :boolean, default: false
|
||||
attr :patch, :string, required: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
defp tab(assigns) do
|
||||
assigns =
|
||||
if assigns[:selected] do
|
||||
assign(assigns,
|
||||
class:
|
||||
"bg-white dark:bg-gray-800 rounded-md px-3.5 py-2.5 text-sm font-medium flex items-center flex-1 justify-center whitespace-nowrap"
|
||||
)
|
||||
else
|
||||
assign(assigns,
|
||||
class:
|
||||
"bg-gray-100 dark:bg-gray-700 rounded-md px-3.5 py-2.5 text-sm font-medium flex items-center cursor-pointer flex-1 justify-center whitespace-nowrap"
|
||||
)
|
||||
end
|
||||
|
||||
~H"""
|
||||
<.link patch={@patch} class={@class}>
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"tracker_script_configuration" => params}, socket) do
|
||||
config =
|
||||
PlausibleWeb.Tracker.update_script_configuration!(
|
||||
socket.assigns.site,
|
||||
params,
|
||||
:installation
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
push_navigate(socket,
|
||||
to:
|
||||
Routes.site_path(socket, :verification, socket.assigns.site.domain,
|
||||
flow: socket.assigns.flow,
|
||||
installation_type: config.installation_type
|
||||
)
|
||||
)}
|
||||
end
|
||||
|
||||
defp initialize_installation_data(flow, site, params) do
|
||||
{recommended_installation_type, v1_detected} =
|
||||
detect_recommended_installation_type(flow, site)
|
||||
|
||||
tracker_script_configuration =
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
|
||||
outbound_links: true,
|
||||
form_submissions: true,
|
||||
file_downloads: true,
|
||||
track_404_pages: true,
|
||||
installation_type: recommended_installation_type
|
||||
})
|
||||
|
||||
selected_installation_type =
|
||||
cond do
|
||||
params["type"] in PlausibleWeb.Tracker.supported_installation_types() ->
|
||||
params["type"]
|
||||
|
||||
flow == Flows.review() and
|
||||
not is_nil(tracker_script_configuration.installation_type) ->
|
||||
Atom.to_string(tracker_script_configuration.installation_type)
|
||||
|
||||
true ->
|
||||
recommended_installation_type
|
||||
end
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
recommended_installation_type: recommended_installation_type,
|
||||
v1_detected: v1_detected,
|
||||
installation_type: selected_installation_type,
|
||||
tracker_script_configuration_form:
|
||||
to_form(
|
||||
Plausible.Site.TrackerScriptConfiguration.installation_changeset(
|
||||
tracker_script_configuration,
|
||||
%{}
|
||||
)
|
||||
)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
|
@ -554,8 +554,6 @@ defmodule PlausibleWeb.Router do
|
|||
|
||||
get "/sites/new", SiteController, :new
|
||||
post "/sites", SiteController, :create_site
|
||||
get "/sites/:domain/change-domain", SiteController, :change_domain
|
||||
put "/sites/:domain/change-domain", SiteController, :change_domain_submit
|
||||
post "/sites/:domain/make-public", SiteController, :make_public
|
||||
post "/sites/:domain/make-private", SiteController, :make_private
|
||||
post "/sites/:domain/weekly-report/enable", SiteController, :enable_weekly_report
|
||||
|
|
@ -636,12 +634,6 @@ defmodule PlausibleWeb.Router do
|
|||
live "/:domain/installation", Installation, :installation, as: :site
|
||||
end
|
||||
|
||||
scope assigns: %{
|
||||
dogfood_page_path: "/:website/installationv2"
|
||||
} do
|
||||
live "/:domain/installationv2", InstallationV2, :installation_v2, as: :site
|
||||
end
|
||||
|
||||
scope assigns: %{
|
||||
dogfood_page_path: "/:website/verification"
|
||||
} do
|
||||
|
|
@ -652,10 +644,10 @@ defmodule PlausibleWeb.Router do
|
|||
end
|
||||
|
||||
scope assigns: %{
|
||||
dogfood_page_path: "/:website/change-domain-v2"
|
||||
dogfood_page_path: "/:website/change-domain"
|
||||
} do
|
||||
live "/:domain/change-domain-v2", ChangeDomainV2, :change_domain_v2, as: :site
|
||||
live "/:domain/change-domain-v2/success", ChangeDomainV2, :success, as: :site
|
||||
live "/:domain/change-domain", ChangeDomain, :change_domain, as: :site
|
||||
live "/:domain/change-domain/success", ChangeDomain, :success, as: :site
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<PlausibleWeb.Components.FlowProgress.render
|
||||
flow={PlausibleWeb.Flows.domain_change()}
|
||||
current_step="Set up new domain"
|
||||
/>
|
||||
<.focus_box>
|
||||
<:title>Change your website domain</:title>
|
||||
|
||||
<:subtitle>
|
||||
Once you change your domain, you <i>must</i>
|
||||
update Plausible Installation on your site within 72 hours to guarantee continuous tracking.
|
||||
<br /><br />If you're using the API, please also make sure to update your API credentials. Visit our
|
||||
<.styled_link new_tab href="https://plausible.io/docs/change-domain-name/">
|
||||
documentation
|
||||
</.styled_link>
|
||||
for details.
|
||||
</:subtitle>
|
||||
|
||||
<:footer>
|
||||
<.focus_list>
|
||||
<:item>
|
||||
Changed your mind? Go back to
|
||||
<.styled_link href={Routes.site_path(@conn, :settings_general, @site.domain)}>
|
||||
Site Settings
|
||||
</.styled_link>
|
||||
</:item>
|
||||
</.focus_list>
|
||||
</:footer>
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
action={
|
||||
Routes.site_path(@conn, :change_domain_submit, @site.domain,
|
||||
flow: PlausibleWeb.Flows.domain_change()
|
||||
)
|
||||
}
|
||||
>
|
||||
<.input
|
||||
help_text="Just the naked domain or subdomain without 'www', 'https' etc."
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
field={f[:domain]}
|
||||
label="Domain"
|
||||
/>
|
||||
|
||||
<.button type="submit" class="mt-4 w-full">
|
||||
Change Domain and add new Snippet
|
||||
</.button>
|
||||
</.form>
|
||||
</.focus_box>
|
||||
|
|
@ -14,10 +14,6 @@ defmodule PlausibleWeb.Tracker do
|
|||
@plausible_main_script File.read!(path)
|
||||
@external_resource "priv/tracker/js/plausible-web.js"
|
||||
|
||||
def scriptv2?(site, user \\ nil) do
|
||||
FunWithFlags.enabled?(:scriptv2, for: site) or FunWithFlags.enabled?(:scriptv2, for: user)
|
||||
end
|
||||
|
||||
@spec get_plausible_main_script(String.t(), Keyword.t()) :: String.t() | nil
|
||||
def get_plausible_main_script(id, cache_opts \\ []) do
|
||||
on_ee do
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
export default async function({ page, context }) {
|
||||
|
||||
if (context.debug) {
|
||||
page.on('console', (msg) => console[msg.type()]('PAGE LOG:', msg.text()));
|
||||
}
|
||||
|
||||
await page.setUserAgent(context.userAgent);
|
||||
await page.goto(context.url);
|
||||
|
||||
try {
|
||||
await page.waitForFunction('window.plausible', { timeout: 5000 });
|
||||
await page.evaluate(() => {
|
||||
window.__plausible = true;
|
||||
window.plausible('verification-agent-test', {
|
||||
callback: function(options) {
|
||||
window.plausibleCallbackResult = () => options && options.status ? options.status : -1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await page.waitForFunction('window.plausibleCallbackResult', { timeout: 5000 });
|
||||
const status = await page.evaluate(() => { return window.plausibleCallbackResult() });
|
||||
return { data: { plausibleInstalled: true, callbackStatus: status } };
|
||||
} catch ({ err, message }) {
|
||||
return { data: { plausibleInstalled: true, callbackStatus: 0, error: message } };
|
||||
}
|
||||
} catch ({ err, message }) {
|
||||
return {
|
||||
data: {
|
||||
plausibleInstalled: false, callbackStatus: 0, error: message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.CSPTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
on_ee do
|
||||
alias Plausible.InstallationSupport.{State, LegacyVerification}
|
||||
|
||||
@check Plausible.InstallationSupport.Checks.CSP
|
||||
@default_state %State{diagnostics: %LegacyVerification.Diagnostics{}}
|
||||
|
||||
test "skips no headers" do
|
||||
state = @default_state
|
||||
assert ^state = @check.perform(state)
|
||||
end
|
||||
|
||||
test "skips no headers 2" do
|
||||
state = @default_state |> State.assign(headers: %{})
|
||||
assert ^state = @check.perform(state)
|
||||
end
|
||||
|
||||
test "disallowed" do
|
||||
headers = %{"content-security-policy" => ["default-src 'self' foo.local; example.com"]}
|
||||
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(headers: headers)
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.disallowed_via_csp?
|
||||
end
|
||||
|
||||
test "allowed" do
|
||||
headers = %{"content-security-policy" => ["default-src 'self' example.com; localhost"]}
|
||||
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(headers: headers)
|
||||
|> @check.perform()
|
||||
|
||||
refute state.diagnostics.disallowed_via_csp?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.FetchBodyTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
on_ee do
|
||||
import Plug.Conn
|
||||
|
||||
alias Plausible.InstallationSupport.{State, Checks, LegacyVerification}
|
||||
|
||||
@check Checks.FetchBody
|
||||
|
||||
@normal_body """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
setup do
|
||||
{:ok,
|
||||
state: %State{
|
||||
url: "https://example.com",
|
||||
diagnostics: %LegacyVerification.Diagnostics{}
|
||||
}}
|
||||
end
|
||||
|
||||
test "extracts document", %{state: state} do
|
||||
stub()
|
||||
state = @check.perform(state)
|
||||
|
||||
assert state.assigns.raw_body == @normal_body
|
||||
assert state.assigns.document == Floki.parse_document!(@normal_body)
|
||||
assert state.assigns.headers["content-type"] == ["text/html; charset=utf-8"]
|
||||
|
||||
assert state.diagnostics.body_fetched?
|
||||
end
|
||||
|
||||
test "does extract on non-2xx", %{state: state} do
|
||||
stub(400)
|
||||
state = @check.perform(state)
|
||||
assert state.diagnostics.body_fetched?
|
||||
end
|
||||
|
||||
test "doesn't extract non-HTML", %{state: state} do
|
||||
stub(200, @normal_body, "text/plain")
|
||||
state = @check.perform(state)
|
||||
|
||||
assert state.assigns == %{final_domain: "example.com"}
|
||||
|
||||
refute state.diagnostics.body_fetched?
|
||||
end
|
||||
|
||||
defp stub(f) when is_function(f, 1) do
|
||||
Req.Test.stub(@check, f)
|
||||
end
|
||||
|
||||
defp stub(status \\ 200, body \\ @normal_body, content_type \\ "text/html") do
|
||||
stub(fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type(content_type)
|
||||
|> send_resp(status, body)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.ScanBodyTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
on_ee do
|
||||
alias Plausible.InstallationSupport.{State, Checks, LegacyVerification}
|
||||
|
||||
@check Checks.ScanBody
|
||||
@default_state %State{diagnostics: %LegacyVerification.Diagnostics{}}
|
||||
|
||||
test "skips on no raw body" do
|
||||
assert @default_state = @check.perform(@default_state)
|
||||
end
|
||||
|
||||
test "detects nothing" do
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(raw_body: "...")
|
||||
|> @check.perform()
|
||||
|
||||
refute state.diagnostics.gtm_likely?
|
||||
refute state.diagnostics.wordpress_likely?
|
||||
end
|
||||
|
||||
test "detects GTM" do
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(raw_body: "...googletagmanager.com/gtm.js...")
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.gtm_likely?
|
||||
refute state.diagnostics.wordpress_likely?
|
||||
refute state.diagnostics.cookie_banner_likely?
|
||||
end
|
||||
|
||||
test "detects GTM and cookie banner" do
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(raw_body: "...googletagmanager.com/gtm.js...cookiebot...")
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.gtm_likely?
|
||||
assert state.diagnostics.cookie_banner_likely?
|
||||
refute state.diagnostics.wordpress_likely?
|
||||
end
|
||||
|
||||
for signature <- ["wp-content", "wp-includes", "wp-json"] do
|
||||
test "detects WordPress: #{signature}" do
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(raw_body: "...#{unquote(signature)}...")
|
||||
|> @check.perform()
|
||||
|
||||
refute state.diagnostics.gtm_likely?
|
||||
assert state.diagnostics.wordpress_likely?
|
||||
refute state.diagnostics.wordpress_plugin?
|
||||
end
|
||||
end
|
||||
|
||||
test "detects GTM and WordPress" do
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(raw_body: "...googletagmanager.com/gtm.js....wp-content...")
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.gtm_likely?
|
||||
assert state.diagnostics.wordpress_likely?
|
||||
refute state.diagnostics.wordpress_plugin?
|
||||
end
|
||||
|
||||
@d """
|
||||
<meta name='plausible-analytics-version' content='2.0.9' />
|
||||
"""
|
||||
|
||||
test "detects official plugin" do
|
||||
state =
|
||||
@default_state
|
||||
|> State.assign(raw_body: @d, document: Floki.parse_document!(@d))
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.wordpress_likely?
|
||||
assert state.diagnostics.wordpress_plugin?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.Checks.SnippetTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
on_ee do
|
||||
alias Plausible.InstallationSupport.{State, Checks, LegacyVerification}
|
||||
|
||||
@check Checks.Snippet
|
||||
@default_state %State{diagnostics: %LegacyVerification.Diagnostics{}}
|
||||
|
||||
test "skips when there's no document" do
|
||||
state = @default_state
|
||||
assert ^state = @check.perform(state)
|
||||
end
|
||||
|
||||
@well_placed """
|
||||
<head>
|
||||
<script defer data-domain="example.com" event-author="Me" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
"""
|
||||
|
||||
test "figures out well placed snippet" do
|
||||
state =
|
||||
@well_placed
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.snippets_found_in_head == 1
|
||||
assert state.diagnostics.snippets_found_in_body == 0
|
||||
refute state.diagnostics.data_domain_mismatch?
|
||||
refute state.diagnostics.snippet_unknown_attributes?
|
||||
refute state.diagnostics.proxy_likely?
|
||||
refute state.diagnostics.manual_script_extension?
|
||||
end
|
||||
|
||||
@multi_domain """
|
||||
<head>
|
||||
<script defer data-domain="example.org,example.com,example.net" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
"""
|
||||
|
||||
test "figures out well placed snippet in a multi-domain setup" do
|
||||
state =
|
||||
@multi_domain
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.snippets_found_in_head == 1
|
||||
assert state.diagnostics.snippets_found_in_body == 0
|
||||
refute state.diagnostics.data_domain_mismatch?
|
||||
refute state.diagnostics.snippet_unknown_attributes?
|
||||
refute state.diagnostics.proxy_likely?
|
||||
refute state.diagnostics.manual_script_extension?
|
||||
end
|
||||
|
||||
@crazy """
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</body>
|
||||
"""
|
||||
test "counts snippets" do
|
||||
state =
|
||||
@crazy
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.snippets_found_in_head == 2
|
||||
assert state.diagnostics.snippets_found_in_body == 3
|
||||
refute state.diagnostics.manual_script_extension?
|
||||
end
|
||||
|
||||
test "figures out data-domain mismatch" do
|
||||
state =
|
||||
@well_placed
|
||||
|> new_state(data_domain: "example.typo")
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.snippets_found_in_head == 1
|
||||
assert state.diagnostics.snippets_found_in_body == 0
|
||||
assert state.diagnostics.data_domain_mismatch?
|
||||
refute state.diagnostics.snippet_unknown_attributes?
|
||||
refute state.diagnostics.proxy_likely?
|
||||
refute state.diagnostics.manual_script_extension?
|
||||
end
|
||||
|
||||
@proxy_likely """
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="http://my-domain.example.com/js/script.js"></script>
|
||||
</head>
|
||||
"""
|
||||
|
||||
test "figures out proxy likely" do
|
||||
state =
|
||||
@proxy_likely
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.snippets_found_in_head == 1
|
||||
assert state.diagnostics.snippets_found_in_body == 0
|
||||
refute state.diagnostics.data_domain_mismatch?
|
||||
refute state.diagnostics.snippet_unknown_attributes?
|
||||
assert state.diagnostics.proxy_likely?
|
||||
refute state.diagnostics.manual_script_extension?
|
||||
end
|
||||
|
||||
@manual_extension """
|
||||
<head>
|
||||
<script defer data-domain="example.com" event-author="Me" src="http://localhost:8000/js/script.manual.js"></script>
|
||||
</head>
|
||||
"""
|
||||
|
||||
test "figures out manual script extension" do
|
||||
state =
|
||||
@manual_extension
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.manual_script_extension?
|
||||
end
|
||||
|
||||
@unknown_attributes """
|
||||
<head>
|
||||
<script defer data-api="some" data-include="some" data-exclude="some" weird="one" data-domain="example.com" src="http://my-domain.example.com/js/script.js"></script>
|
||||
</head>
|
||||
"""
|
||||
|
||||
@valid_attributes """
|
||||
<head>
|
||||
<script defer type="text/javascript" data-cfasync='false' data-api="some" data-include="some" data-exclude="some" data-domain="example.com" src="http://my-domain.example.com/js/script.js"></script>
|
||||
</head>
|
||||
"""
|
||||
|
||||
test "figures out unknown attributes" do
|
||||
state =
|
||||
@valid_attributes
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
refute state.diagnostics.snippet_unknown_attributes?
|
||||
|
||||
state =
|
||||
@unknown_attributes
|
||||
|> new_state()
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.snippet_unknown_attributes?
|
||||
end
|
||||
|
||||
defp new_state(html, opts \\ []) do
|
||||
doc = Floki.parse_document!(html)
|
||||
|
||||
opts =
|
||||
[data_domain: "example.com"]
|
||||
|> Keyword.merge(opts)
|
||||
|
||||
@default_state
|
||||
|> struct!(opts)
|
||||
|> State.assign(document: doc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,925 +0,0 @@
|
|||
defmodule Plausible.InstallationSupport.LegacyVerification.ChecksTest do
|
||||
use Plausible.DataCase, async: true
|
||||
use Plausible
|
||||
|
||||
@moduletag :ee_only
|
||||
|
||||
on_ee do
|
||||
alias Plausible.InstallationSupport.{State, Checks, LegacyVerification}
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Plug.Conn
|
||||
|
||||
@errors LegacyVerification.Errors.all()
|
||||
|
||||
@moduletag :capture_log
|
||||
|
||||
describe "successful verification" do
|
||||
@normal_body """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "definite success" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
end
|
||||
|
||||
test "fetching will follow 2 redirects" do
|
||||
ref = :counters.new(1, [:atomics])
|
||||
test = self()
|
||||
|
||||
Req.Test.stub(Checks.FetchBody, fn conn ->
|
||||
if :counters.get(ref, 1) < 2 do
|
||||
:counters.add(ref, 1, 1)
|
||||
send(test, :redirect_sent)
|
||||
|
||||
conn
|
||||
|> put_resp_header("location", "https://example.com")
|
||||
|> send_resp(302, "redirecting to https://example.com")
|
||||
else
|
||||
conn
|
||||
|> put_resp_header("content-type", "text/html")
|
||||
|> send_resp(200, @normal_body)
|
||||
end
|
||||
end)
|
||||
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
refute_receive _
|
||||
end
|
||||
|
||||
test "allowed via content-security-policy" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_header(
|
||||
"content-security-policy",
|
||||
Enum.random([
|
||||
"default-src 'self'; script-src plausible.io; connect-src #{PlausibleWeb.Endpoint.host()}",
|
||||
"default-src 'self' *.#{PlausibleWeb.Endpoint.host()}"
|
||||
])
|
||||
)
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @normal_body)
|
||||
end)
|
||||
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
end
|
||||
|
||||
@proxied_script_body """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="https://proxy.example.com/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "proxied setup working OK" do
|
||||
stub_fetch_body(200, @proxied_script_body)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
end
|
||||
|
||||
@body_no_snippet """
|
||||
<html> <head> </head> <body> Hello </body> </html>
|
||||
"""
|
||||
|
||||
test "non-standard integration where the snippet cannot be found but it works ok in headless" do
|
||||
stub_fetch_body(200, @body_no_snippet)
|
||||
stub_installation(200, plausible_installed(true, 202))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
end
|
||||
|
||||
@different_data_domain_body """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="www.example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "data-domain mismatch on redirect chain" do
|
||||
ref = :counters.new(1, [:atomics])
|
||||
test = self()
|
||||
|
||||
Req.Test.stub(Checks.FetchBody, fn conn ->
|
||||
if :counters.get(ref, 1) == 0 do
|
||||
:counters.add(ref, 1, 1)
|
||||
send(test, :redirect_sent)
|
||||
|
||||
conn
|
||||
|> put_resp_header("location", "https://www.example.com")
|
||||
|> send_resp(302, "redirecting to https://www.example.com")
|
||||
else
|
||||
conn
|
||||
|> put_resp_header("content-type", "text/html")
|
||||
|> send_resp(200, @different_data_domain_body)
|
||||
end
|
||||
end)
|
||||
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
|
||||
assert_receive :redirect_sent
|
||||
end
|
||||
end
|
||||
|
||||
describe "errors" do
|
||||
test "service error - 400" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation(400, %{})
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.temporary)
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "can't fetch body but headless reports ok" do
|
||||
stub_fetch_body(500, "")
|
||||
stub_installation()
|
||||
|
||||
{_, log} =
|
||||
with_log(fn ->
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
end)
|
||||
|
||||
assert log =~ "3 attempts left"
|
||||
assert log =~ "2 attempts left"
|
||||
assert log =~ "1 attempt left"
|
||||
end
|
||||
|
||||
test "fetching will give up at 5th redirect" do
|
||||
test = self()
|
||||
|
||||
stub_fetch_body(fn conn ->
|
||||
send(test, :redirect_sent)
|
||||
|
||||
conn
|
||||
|> put_resp_header("location", "https://example.com")
|
||||
|> send_resp(302, "redirecting to https://example.com")
|
||||
end)
|
||||
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.unreachable, url: "https://example.com")
|
||||
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
refute_receive _
|
||||
end
|
||||
|
||||
@snippet_in_body """
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "detecting snippet in body" do
|
||||
stub_fetch_body(200, @snippet_in_body)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.snippet_in_body)
|
||||
end
|
||||
|
||||
@many_snippets """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
|
||||
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
|
||||
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
|
||||
<!-- maybe proxy? -->
|
||||
<script defer data-domain="example.com" src="https://example.com/js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "detecting many snippets" do
|
||||
stub_fetch_body(200, @many_snippets)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.multiple_snippets)
|
||||
end
|
||||
|
||||
@no_src_scripts """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
<script defer data-domain="example.com"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
test "no src attr doesn't count as snippet" do
|
||||
stub_fetch_body(200, @no_src_scripts)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.no_snippet)
|
||||
end
|
||||
|
||||
@many_snippets_ok """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="https://plausible.io/js/script.js"></script>
|
||||
<script defer data-domain="example.com" src="https://plausible.io/js/script.manual.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "skipping many snippets when manual found" do
|
||||
stub_fetch_body(200, @many_snippets_ok)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_ok()
|
||||
end
|
||||
|
||||
test "detecting snippet after busting cache" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn = fetch_query_params(conn)
|
||||
|
||||
if conn.query_params["plausible_verification"] do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @normal_body)
|
||||
else
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @body_no_snippet)
|
||||
end
|
||||
end)
|
||||
|
||||
stub_installation(fn conn ->
|
||||
{:ok, body, _} = read_body(conn)
|
||||
|
||||
if String.contains?(body, "?plausible_verification") do
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(plausible_installed()))
|
||||
else
|
||||
raise "Should not get here even"
|
||||
end
|
||||
end)
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.cache_general)
|
||||
end
|
||||
|
||||
@normal_body_wordpress """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="foo" content="/wp-content/plugins/bar"/>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "detecting snippet after busting WordPress cache - no official plugin" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn = fetch_query_params(conn)
|
||||
|
||||
if conn.query_params["plausible_verification"] do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @normal_body_wordpress)
|
||||
else
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @body_no_snippet)
|
||||
end
|
||||
end)
|
||||
|
||||
stub_installation(fn conn ->
|
||||
{:ok, body, _} = read_body(conn)
|
||||
|
||||
if String.contains?(body, "?plausible_verification") do
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(plausible_installed()))
|
||||
else
|
||||
raise "Should not get here even"
|
||||
end
|
||||
end)
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.cache_wp_no_plugin)
|
||||
end
|
||||
|
||||
@normal_body_wordpress_official_plugin """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="foo" content="/wp-content/plugins/bar"/>
|
||||
<meta name='plausible-analytics-version' content='2.0.9' />
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "detecting snippet after busting WordPress cache - official plugin" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn = fetch_query_params(conn)
|
||||
|
||||
if conn.query_params["plausible_verification"] do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @normal_body_wordpress_official_plugin)
|
||||
else
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @body_no_snippet)
|
||||
end
|
||||
end)
|
||||
|
||||
stub_installation(fn conn ->
|
||||
{:ok, body, _} = read_body(conn)
|
||||
|
||||
if String.contains?(body, "?plausible_verification") do
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(plausible_installed()))
|
||||
else
|
||||
raise "Should not get here even"
|
||||
end
|
||||
end)
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.cache_wp_plugin)
|
||||
end
|
||||
|
||||
test "detecting no snippet" do
|
||||
stub_fetch_body(200, @body_no_snippet)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.no_snippet)
|
||||
end
|
||||
|
||||
@body_no_snippet_wp """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="foo" content="/wp-content/plugins/bar"/>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "detecting no snippet on a wordpress site" do
|
||||
stub_fetch_body(200, @body_no_snippet_wp)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.no_snippet_wp)
|
||||
end
|
||||
|
||||
test "disallowed via content-security-policy" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_header("content-security-policy", "default-src 'self' foo.local")
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @normal_body)
|
||||
end)
|
||||
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.csp)
|
||||
end
|
||||
|
||||
test "disallowed via content-security-policy with no snippet should make the latter a priority" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_header("content-security-policy", "default-src 'self' foo.local")
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @body_no_snippet)
|
||||
end)
|
||||
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.no_snippet)
|
||||
end
|
||||
|
||||
test "running checks sends progress messages" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation()
|
||||
|
||||
final_state = run_checks(report_to: self())
|
||||
|
||||
assert_receive {:check_start, {Checks.FetchBody, %State{}}}
|
||||
assert_receive {:check_start, {Checks.CSP, %State{}}}
|
||||
assert_receive {:check_start, {Checks.ScanBody, %State{}}}
|
||||
assert_receive {:check_start, {Checks.Snippet, %State{}}}
|
||||
|
||||
assert_receive {:check_start, {Checks.SnippetCacheBust, %State{}}}
|
||||
|
||||
assert_receive {:check_start, {Checks.Installation, %State{}}}
|
||||
assert_receive {:all_checks_done, %State{} = ^final_state}
|
||||
refute_receive _
|
||||
end
|
||||
|
||||
@gtm_body """
|
||||
<html>
|
||||
<head>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','XXXX');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "disallowed via content-security-policy and GTM should make CSP a priority" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_header("content-security-policy", "default-src 'self' foo.local")
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(200, @gtm_body)
|
||||
end)
|
||||
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.csp)
|
||||
end
|
||||
|
||||
test "detecting gtm" do
|
||||
stub_fetch_body(200, @gtm_body)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.gtm)
|
||||
end
|
||||
|
||||
@gtm_body_with_cookiebot """
|
||||
<html>
|
||||
<head>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','XXXX');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="some-uuid" data-blockingmode="auto" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "detecting gtm with cookie consent" do
|
||||
stub_fetch_body(200, @gtm_body_with_cookiebot)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.gtm_cookie_banner)
|
||||
end
|
||||
|
||||
test "non-html body" do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("image/png")
|
||||
|> send_resp(200, :binary.copy(<<0>>, 100))
|
||||
end)
|
||||
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.unreachable, url: "https://example.com")
|
||||
end
|
||||
|
||||
test "proxied setup, function defined but callback won't fire" do
|
||||
stub_fetch_body(200, @proxied_script_body)
|
||||
stub_installation(200, plausible_installed(true, 0))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.proxy_misconfigured)
|
||||
end
|
||||
|
||||
@proxied_script_body_wordpress """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="foo" content="/wp-content/plugins/bar"/>
|
||||
<script defer data-domain="example.com" src="https://proxy.example.com/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "proxied WordPress setup, function undefined, callback won't fire" do
|
||||
stub_fetch_body(200, @proxied_script_body_wordpress)
|
||||
stub_installation(200, plausible_installed(false, 0))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.proxy_wp_no_plugin)
|
||||
end
|
||||
|
||||
test "proxied setup, function undefined, callback won't fire" do
|
||||
stub_fetch_body(200, @proxied_script_body)
|
||||
stub_installation(200, plausible_installed(false, 0))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = LegacyVerification.Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an error with your Plausible proxy"]
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.proxy_general)
|
||||
end
|
||||
|
||||
test "non-proxied setup, but callback fails to fire" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation(200, plausible_installed(true, 0))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.unknown)
|
||||
end
|
||||
|
||||
@body_unknown_attributes """
|
||||
<html>
|
||||
<head>
|
||||
<script foo="bar" defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "unknown attributes" do
|
||||
stub_fetch_body(200, @body_unknown_attributes)
|
||||
stub_installation(200, plausible_installed(false, 0))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.illegal_attrs_general)
|
||||
end
|
||||
|
||||
@body_unknown_attributes_wordpress """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="foo" content="/wp-content/plugins/bar"/>
|
||||
<script foo="bar" defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "unknown attributes for WordPress installation" do
|
||||
stub_fetch_body(200, @body_unknown_attributes_wordpress)
|
||||
stub_installation(200, plausible_installed(false, 0))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.illegal_attrs_wp_no_plugin)
|
||||
end
|
||||
|
||||
@body_unknown_attributes_wordpress_official_plugin """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="foo" content="/wp-content/plugins/bar"/>
|
||||
<meta name='plausible-analytics-version' content='2.0.9' />
|
||||
<script foo="bar" defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "unknown attributes for WordPress installation - official plugin" do
|
||||
stub_fetch_body(200, @body_unknown_attributes_wordpress_official_plugin)
|
||||
stub_installation(200, plausible_installed(false, 0))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.illegal_attrs_wp_plugin)
|
||||
end
|
||||
|
||||
test "callback handling not found for non-wordpress site" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation(200, plausible_installed(true, -1))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.generic)
|
||||
end
|
||||
|
||||
test "callback handling not found for wordpress site" do
|
||||
stub_fetch_body(200, @normal_body_wordpress)
|
||||
stub_installation(200, plausible_installed(true, -1))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.old_script_wp_no_plugin)
|
||||
end
|
||||
|
||||
test "callback handling not found for wordpress site using our plugin" do
|
||||
stub_fetch_body(200, @normal_body_wordpress_official_plugin)
|
||||
stub_installation(200, plausible_installed(true, -1))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.old_script_wp_plugin)
|
||||
end
|
||||
|
||||
test "fails due to callback status being something unlikely like 500" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation(200, plausible_installed(true, 500))
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.unknown)
|
||||
end
|
||||
|
||||
test "data-domain mismatch" do
|
||||
stub_fetch_body(200, @different_data_domain_body)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.different_data_domain, domain: "example.com")
|
||||
end
|
||||
|
||||
@many_snippets_with_domain_mismatch """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.org" src="https://plausible.io/js/script.js"></script>
|
||||
<script defer data-domain="example.org" src="https://plausible.io/js/script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test "prioritizes data-domain mismatch over multiple snippets" do
|
||||
stub_fetch_body(200, @many_snippets_with_domain_mismatch)
|
||||
stub_installation()
|
||||
|
||||
run_checks()
|
||||
|> LegacyVerification.Checks.interpret_diagnostics()
|
||||
|> assert_error(@errors.different_data_domain, domain: "example.com")
|
||||
end
|
||||
end
|
||||
|
||||
describe "unhhandled cases from sentry" do
|
||||
test "APP-58: 4b1435e3f8a048eb949cc78fa578d1e4" do
|
||||
%LegacyVerification.Diagnostics{
|
||||
plausible_installed?: true,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
snippet_unknown_attributes?: false,
|
||||
disallowed_via_csp?: false,
|
||||
service_error: nil,
|
||||
body_fetched?: true,
|
||||
wordpress_likely?: true,
|
||||
cookie_banner_likely?: false,
|
||||
gtm_likely?: false,
|
||||
callback_status: -1,
|
||||
proxy_likely?: false,
|
||||
manual_script_extension?: false,
|
||||
data_domain_mismatch?: false,
|
||||
wordpress_plugin?: false
|
||||
}
|
||||
|> interpret_sentry_case()
|
||||
|> assert_error(@errors.old_script_wp_no_plugin)
|
||||
end
|
||||
|
||||
test "service timeout" do
|
||||
%LegacyVerification.Diagnostics{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
snippets_found_in_body: 0,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
snippet_unknown_attributes?: false,
|
||||
disallowed_via_csp?: false,
|
||||
service_error: :timeout,
|
||||
body_fetched?: true,
|
||||
wordpress_likely?: true,
|
||||
cookie_banner_likely?: false,
|
||||
gtm_likely?: false,
|
||||
callback_status: 0,
|
||||
proxy_likely?: true,
|
||||
manual_script_extension?: false,
|
||||
data_domain_mismatch?: false,
|
||||
wordpress_plugin?: false
|
||||
}
|
||||
|> interpret_sentry_case()
|
||||
|> assert_error(@errors.generic)
|
||||
end
|
||||
|
||||
test "malformed snippet code, that headless somewhat accepts" do
|
||||
%LegacyVerification.Diagnostics{
|
||||
plausible_installed?: true,
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
snippet_unknown_attributes?: false,
|
||||
disallowed_via_csp?: false,
|
||||
service_error: nil,
|
||||
body_fetched?: true,
|
||||
wordpress_likely?: false,
|
||||
cookie_banner_likely?: false,
|
||||
gtm_likely?: false,
|
||||
callback_status: 405,
|
||||
proxy_likely?: false,
|
||||
manual_script_extension?: false,
|
||||
data_domain_mismatch?: false,
|
||||
wordpress_plugin?: false
|
||||
}
|
||||
|> interpret_sentry_case()
|
||||
|> assert_error(@errors.no_snippet)
|
||||
end
|
||||
|
||||
test "gtm+wp detected, but likely script id attribute interfering" do
|
||||
%LegacyVerification.Diagnostics{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
snippets_found_in_body: 0,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
disallowed_via_csp?: false,
|
||||
service_error: nil,
|
||||
body_fetched?: true,
|
||||
wordpress_likely?: true,
|
||||
cookie_banner_likely?: true,
|
||||
gtm_likely?: true,
|
||||
callback_status: 0,
|
||||
proxy_likely?: true,
|
||||
manual_script_extension?: false,
|
||||
data_domain_mismatch?: false,
|
||||
wordpress_plugin?: true
|
||||
}
|
||||
|> interpret_sentry_case()
|
||||
|> assert_error(@errors.illegal_attrs_wp_plugin)
|
||||
end
|
||||
end
|
||||
|
||||
defp interpret_sentry_case(diagnostics) do
|
||||
diagnostics
|
||||
|> LegacyVerification.Diagnostics.interpret("example.com")
|
||||
|> refute_unhandled()
|
||||
end
|
||||
|
||||
defp run_checks(extra_opts \\ []) do
|
||||
LegacyVerification.Checks.run(
|
||||
"https://example.com",
|
||||
"example.com",
|
||||
Keyword.merge([async?: false, report_to: nil, slowdown: 0], extra_opts)
|
||||
)
|
||||
end
|
||||
|
||||
defp stub_fetch_body(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Checks.FetchBody, f)
|
||||
end
|
||||
|
||||
defp stub_installation(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Checks.Installation, f)
|
||||
end
|
||||
|
||||
defp stub_fetch_body(status, body) do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(status, body)
|
||||
end)
|
||||
end
|
||||
|
||||
defp stub_installation(status \\ 200, json \\ plausible_installed()) do
|
||||
stub_installation(fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(status, Jason.encode!(json))
|
||||
end)
|
||||
end
|
||||
|
||||
defp plausible_installed(bool \\ true, callback_status \\ 202) do
|
||||
%{
|
||||
"data" => %{
|
||||
"completed" => true,
|
||||
"snippetsFoundInHead" => 0,
|
||||
"snippetsFoundInBody" => 0,
|
||||
"plausibleInstalled" => bool,
|
||||
"callbackStatus" => callback_status
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp refute_unhandled(interpretation) do
|
||||
refute interpretation.errors == [
|
||||
@errors.unknown.message
|
||||
]
|
||||
|
||||
refute interpretation.recommendations == [
|
||||
@errors.unknown.recommendation
|
||||
]
|
||||
|
||||
interpretation
|
||||
end
|
||||
|
||||
defp assert_error(interpretation, error) do
|
||||
refute interpretation.ok?
|
||||
|
||||
assert interpretation.errors == [
|
||||
error.message
|
||||
]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
%{text: error.recommendation, url: error.url}
|
||||
]
|
||||
end
|
||||
|
||||
defp assert_error(interpretation, error, assigns) do
|
||||
recommendation = EEx.eval_string(error.recommendation, assigns: assigns)
|
||||
assert_error(interpretation, %{error | recommendation: recommendation})
|
||||
end
|
||||
|
||||
defp assert_ok(interpretation) do
|
||||
assert interpretation.ok?
|
||||
assert interpretation.errors == []
|
||||
assert interpretation.recommendations == []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -71,7 +71,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "InstallationV2 check" do
|
||||
describe "VerifyInstallation check" do
|
||||
for status <- [200, 202] do
|
||||
test "returns success if test event response status is #{status} and domain is as expected" do
|
||||
verification_stub =
|
||||
|
|
@ -379,7 +379,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "InstallationV2 & InstallationV2CacheBust" do
|
||||
describe "VerifyInstallation & VerifyInstallationCacheBust" do
|
||||
test "returns error when it 'succeeds', but only after cache bust" do
|
||||
counter = :atomics.new(1, [])
|
||||
|
||||
|
|
@ -583,7 +583,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
|
|||
|
||||
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
|
||||
|
||||
assert log =~ "[VERIFICATION v2] Unhandled case (data_domain='#{@expected_domain}')"
|
||||
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
|
||||
assert log =~ "test_event: %{}"
|
||||
|
||||
assert_receive {:telemetry_event, telemetry_event}
|
||||
|
|
@ -596,7 +596,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
|
|||
|
||||
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
|
||||
|
||||
assert log =~ "[VERIFICATION v2] Unhandled case (data_domain='#{@expected_domain}')"
|
||||
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
|
||||
assert log =~ "service_error: %{code: :browserless_timeout}"
|
||||
|
||||
assert_receive {:telemetry_event, telemetry_event}
|
||||
|
|
@ -614,7 +614,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
|
|||
|
||||
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
|
||||
|
||||
assert log =~ "[VERIFICATION v2] Unhandled case (data_domain='#{@expected_domain}')"
|
||||
assert log =~ "[VERIFICATION] Unhandled case (data_domain='#{@expected_domain}')"
|
||||
assert log =~ "service_error: %{code: :bad_browserless_response, extra: 400}"
|
||||
|
||||
assert_receive {:telemetry_event, telemetry_event}
|
||||
|
|
@ -666,7 +666,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
|
|||
end
|
||||
|
||||
defp stub_verification_result(f) do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.InstallationV2, f)
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.VerifyInstallation, f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
|
||||
@moduledoc """
|
||||
Tests for Sites create/read/update/delete API with `scriptv2` feature flag enabled.
|
||||
It has overlap with some of the tests in `PlausibleWeb.Api.ExternalSitesControllerTest` test suite.
|
||||
The overlapped tests from that suite can be deleted once the feature flag is enabled globally.
|
||||
Tests for Sites create/read/update/delete API.
|
||||
"""
|
||||
use Plausible
|
||||
use PlausibleWeb.ConnCase
|
||||
|
|
@ -14,7 +12,6 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
|
|||
setup :create_user
|
||||
|
||||
setup %{conn: conn, user: user} do
|
||||
FunWithFlags.enable(:scriptv2, for_actor: user)
|
||||
api_key = insert(:api_key, user: user, scopes: ["sites:provision:*"])
|
||||
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
||||
{:ok, api_key: api_key, conn: conn}
|
||||
|
|
@ -61,6 +58,49 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
|
|||
}) = response
|
||||
end
|
||||
|
||||
test "can create more sites than their Enterprise plans is limited to (we sort the bills post-fact)",
|
||||
%{conn: conn} do
|
||||
user =
|
||||
new_user()
|
||||
|> subscribe_to_enterprise_plan(
|
||||
features: [
|
||||
Plausible.Billing.Feature.StatsAPI,
|
||||
Plausible.Billing.Feature.SitesAPI
|
||||
],
|
||||
site_limit: 10
|
||||
)
|
||||
|
||||
sites = for _ <- 1..10, do: new_site(owner: user)
|
||||
assert 10 == length(sites)
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn"
|
||||
})
|
||||
|
||||
response = json_response(conn, 200)
|
||||
|
||||
assert_matches ^strict_map(%{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn",
|
||||
"custom_properties" => [],
|
||||
"tracker_script_configuration" =>
|
||||
^strict_map(%{
|
||||
"id" => ^any(:string),
|
||||
"installation_type" => nil,
|
||||
"track_404_pages" => false,
|
||||
"hash_based_routing" => false,
|
||||
"outbound_links" => false,
|
||||
"file_downloads" => false,
|
||||
"revenue_tracking" => false,
|
||||
"tagged_events" => false,
|
||||
"form_submissions" => false,
|
||||
"pageview_props" => false
|
||||
})
|
||||
}) = response
|
||||
end
|
||||
|
||||
test "can create a site with a specific tracker script configuration", %{conn: conn} do
|
||||
payload = %{
|
||||
"domain" => "some-site.domain",
|
||||
|
|
@ -464,6 +504,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
|
|||
site2 = new_site(owner: user)
|
||||
|
||||
_unrelated_site = new_site()
|
||||
_consolidated_excluded = new_site(owner: user, consolidated: true)
|
||||
|
||||
conn = get(conn, "/api/v1/sites")
|
||||
|
||||
|
|
@ -699,7 +740,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
|
|||
end
|
||||
|
||||
@tag :capture_log
|
||||
test "is 404 when user is not a member of the site", %{conn: conn} do
|
||||
test "is 401 when user is not a member of the site", %{conn: conn} do
|
||||
site = new_site()
|
||||
|
||||
conn = get(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,25 @@
|
|||
defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||
@moduledoc """
|
||||
Tests for the following endpoints
|
||||
|
||||
GET /api/v1/sites/teams
|
||||
|
||||
GET /api/v1/sites/guests
|
||||
PUT /api/v1/sites/guests
|
||||
DELETE /api/v1/sites/guests
|
||||
|
||||
PUT /api/v1/sites/shared-links
|
||||
|
||||
GET /api/v1/custom-props
|
||||
PUT /api/v1/sites/custom-props
|
||||
DELETE /api/v1/sites/custom-props/:property
|
||||
|
||||
GET /api/v1/goals
|
||||
PUT /api/v1/sites/goals
|
||||
DELETE /api/v1/sites/goals/:goal_id
|
||||
|
||||
Site CRUD endpoints tests are in ExternalSitesControllerSitesCrudApiTest
|
||||
"""
|
||||
use Plausible
|
||||
use PlausibleWeb.ConnCase, async: false
|
||||
use Plausible.Repo
|
||||
|
|
@ -105,254 +126,6 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/sites" do
|
||||
test "can create a site", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn",
|
||||
"custom_properties" => []
|
||||
}
|
||||
end
|
||||
|
||||
test "can't create site in a team where not permitted to", %{conn: conn, user: user} do
|
||||
owner = new_user() |> subscribe_to_growth_plan()
|
||||
team = owner |> team_of() |> Plausible.Teams.complete_setup()
|
||||
add_member(team, user: user, role: :viewer)
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"team_id" => team.identifier,
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn"
|
||||
})
|
||||
|
||||
assert %{"error" => error} = json_response(conn, 402)
|
||||
|
||||
assert error =~ "API key does not have access to Sites API"
|
||||
end
|
||||
|
||||
test "can create a site under a specific team if permitted", %{conn: conn, user: user} do
|
||||
_site = new_site(owner: user)
|
||||
|
||||
owner =
|
||||
new_user()
|
||||
|> subscribe_to_enterprise_plan(
|
||||
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
|
||||
)
|
||||
|
||||
team = owner |> team_of() |> Plausible.Teams.complete_setup()
|
||||
add_member(team, user: user, role: :owner)
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"team_id" => team.identifier,
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn",
|
||||
"custom_properties" => []
|
||||
}
|
||||
|
||||
assert Repo.get_by(Plausible.Site, domain: "some-site.domain").team_id == team.id
|
||||
end
|
||||
|
||||
test "creates under a particular team when team-scoped key used", %{conn: conn, user: user} do
|
||||
personal_team = user |> subscribe_to_business_plan() |> team_of()
|
||||
|
||||
another_team =
|
||||
new_user()
|
||||
|> subscribe_to_enterprise_plan(
|
||||
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
|
||||
)
|
||||
|> team_of()
|
||||
|
||||
add_member(another_team, user: user, role: :admin)
|
||||
|
||||
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
|
||||
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
# is ignored
|
||||
"team_id" => personal_team.identifier,
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn",
|
||||
"custom_properties" => []
|
||||
}
|
||||
|
||||
assert Repo.get_by(Plausible.Site, domain: "some-site.domain").team_id == another_team.id
|
||||
end
|
||||
|
||||
test "timezone is validated", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "d"
|
||||
})
|
||||
|
||||
assert json_response(conn, 400) == %{
|
||||
"error" => "timezone: is invalid"
|
||||
}
|
||||
end
|
||||
|
||||
test "timezone defaults to Etc/UTC", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"domain" => "some-site.domain"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Etc/UTC",
|
||||
"custom_properties" => []
|
||||
}
|
||||
end
|
||||
|
||||
test "domain is required", %{conn: conn} do
|
||||
conn = post(conn, "/api/v1/sites", %{})
|
||||
|
||||
assert json_response(conn, 400) == %{
|
||||
"error" => "domain: can't be blank"
|
||||
}
|
||||
end
|
||||
|
||||
test "accepts international domain names", %{conn: conn} do
|
||||
["müllers-café.test", "音乐.cn", "до.101домен.рф/pages"]
|
||||
|> Enum.each(fn idn_domain ->
|
||||
conn = post(conn, "/api/v1/sites", %{"domain" => idn_domain})
|
||||
assert %{"domain" => ^idn_domain} = json_response(conn, 200)
|
||||
end)
|
||||
end
|
||||
|
||||
test "validates uri breaking domains", %{conn: conn} do
|
||||
["quero:café.test", "h&llo.test", "iamnotsur&about?this.com"]
|
||||
|> Enum.each(fn bad_domain ->
|
||||
conn = post(conn, "/api/v1/sites", %{"domain" => bad_domain})
|
||||
|
||||
assert %{"error" => error} = json_response(conn, 400)
|
||||
assert error =~ "domain: must not contain URI reserved characters"
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not allow creating more sites than the limit", %{conn: conn, user: user} do
|
||||
for _ <- 1..10, do: new_site(owner: user)
|
||||
|
||||
conn =
|
||||
post(conn, "/api/v1/sites", %{
|
||||
"domain" => "some-site.domain",
|
||||
"timezone" => "Europe/Tallinn"
|
||||
})
|
||||
|
||||
assert json_response(conn, 402) == %{
|
||||
"error" =>
|
||||
"Your account has reached the limit of 10 sites. To unlock more sites, please upgrade your subscription."
|
||||
}
|
||||
end
|
||||
|
||||
test "cannot access with a bad API key scope", %{conn: conn, user: user} do
|
||||
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> post("/api/v1/sites", %{"site" => %{"domain" => "domain.com"}})
|
||||
|
||||
assert json_response(conn, 401) == %{
|
||||
"error" =>
|
||||
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/v1/sites/:site_id" do
|
||||
setup :create_site
|
||||
|
||||
setup %{user: user} do
|
||||
subscribe_to_enterprise_plan(user,
|
||||
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "delete a site by its domain", %{conn: conn, site: site} do
|
||||
conn = delete(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert json_response(conn, 200) == %{"deleted" => true}
|
||||
end
|
||||
|
||||
test "delete a site by its old domain after domain change", %{conn: conn, site: site} do
|
||||
old_domain = site.domain
|
||||
new_domain = "new.example.com"
|
||||
|
||||
Plausible.Site.Domain.change(site, new_domain)
|
||||
|
||||
conn = delete(conn, "/api/v1/sites/" <> old_domain)
|
||||
|
||||
assert json_response(conn, 200) == %{"deleted" => true}
|
||||
end
|
||||
|
||||
test "is 404 when site cannot be found", %{conn: conn} do
|
||||
conn = delete(conn, "/api/v1/sites/foobar.baz")
|
||||
|
||||
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
||||
end
|
||||
|
||||
@tag :capture_log
|
||||
test "cannot delete a site that the user does not own", %{conn: conn, user: user} do
|
||||
site = new_site()
|
||||
add_guest(site, user: user, role: :editor)
|
||||
conn = delete(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert %{"error" => error} = json_response(conn, 402)
|
||||
assert error =~ "API key does not have access to Sites API"
|
||||
end
|
||||
|
||||
test "cannot delete if team not matching team-scoped API key", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
site: site
|
||||
} do
|
||||
another_team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
add_member(another_team, user: user, role: :admin)
|
||||
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
|
||||
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
||||
|
||||
conn = delete(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert %{"error" => error} = json_response(conn, 401)
|
||||
assert error =~ "Invalid API key"
|
||||
end
|
||||
|
||||
test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do
|
||||
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> delete("/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert json_response(conn, 401) == %{
|
||||
"error" =>
|
||||
"Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/v1/sites/shared-links" do
|
||||
setup :create_site
|
||||
|
||||
|
|
@ -1153,149 +926,6 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/sites" do
|
||||
test "returns empty when there are no sites for user", %{conn: conn} do
|
||||
conn = get(conn, "/api/v1/sites")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"sites" => [],
|
||||
"meta" => %{
|
||||
"before" => nil,
|
||||
"after" => nil,
|
||||
"limit" => 100
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
test "returns sites when present", %{conn: conn, user: user} do
|
||||
site1 = new_site(owner: user)
|
||||
site2 = new_site(owner: user)
|
||||
|
||||
_unrelated_site = new_site()
|
||||
_consolidated_excluded = new_site(owner: user, consolidated: true)
|
||||
|
||||
conn = get(conn, "/api/v1/sites")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"sites" => [
|
||||
%{"domain" => site2.domain, "timezone" => site2.timezone},
|
||||
%{"domain" => site1.domain, "timezone" => site1.timezone}
|
||||
],
|
||||
"meta" => %{
|
||||
"before" => nil,
|
||||
"after" => nil,
|
||||
"limit" => 100
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
test "returns sites where user is only a viewer", %{conn: conn, user: user} do
|
||||
%{domain: owned_site_domain} = new_site(owner: user)
|
||||
other_site = %{domain: other_site_domain} = new_site()
|
||||
add_guest(other_site, user: user, role: :viewer)
|
||||
|
||||
conn = get(conn, "/api/v1/sites")
|
||||
|
||||
assert %{
|
||||
"sites" => [
|
||||
%{"domain" => ^other_site_domain},
|
||||
%{"domain" => ^owned_site_domain}
|
||||
]
|
||||
} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns sites scoped to a given team for full memberships", %{conn: conn, user: user} do
|
||||
_owned_site = new_site(owner: user)
|
||||
other_site = new_site()
|
||||
add_guest(other_site, user: user, role: :viewer)
|
||||
other_team_site = new_site()
|
||||
add_member(other_team_site.team, user: user, role: :viewer)
|
||||
|
||||
conn = get(conn, "/api/v1/sites?team_id=" <> other_team_site.team.identifier)
|
||||
|
||||
assert_matches %{
|
||||
"sites" => [
|
||||
%{"domain" => ^other_team_site.domain}
|
||||
]
|
||||
} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "implicitly scopes to a team for a team-scoped key", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
another_team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
add_member(another_team, user: user, role: :admin)
|
||||
site = new_site(team: another_team)
|
||||
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
|
||||
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
||||
|
||||
_owned_site = new_site(owner: user)
|
||||
other_site = new_site()
|
||||
add_guest(other_site, user: user, role: :viewer)
|
||||
other_team_site = new_site()
|
||||
add_member(other_team_site.team, user: user, role: :viewer)
|
||||
|
||||
# `team_id` paramaeter is ignored
|
||||
conn = get(conn, "/api/v1/sites?team_id=" <> other_team_site.team.identifier)
|
||||
|
||||
assert_matches %{
|
||||
"sites" => [
|
||||
%{"domain" => ^site.domain}
|
||||
]
|
||||
} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "handles pagination correctly", %{conn: conn, user: user} do
|
||||
[
|
||||
%{domain: site1_domain},
|
||||
%{domain: site2_domain},
|
||||
%{domain: site3_domain}
|
||||
] = for _ <- 1..3, do: new_site(owner: user)
|
||||
|
||||
conn1 = get(conn, "/api/v1/sites?limit=2")
|
||||
|
||||
assert %{
|
||||
"sites" => [
|
||||
%{"domain" => ^site3_domain},
|
||||
%{"domain" => ^site2_domain}
|
||||
],
|
||||
"meta" => %{
|
||||
"before" => nil,
|
||||
"after" => after_cursor,
|
||||
"limit" => 2
|
||||
}
|
||||
} = json_response(conn1, 200)
|
||||
|
||||
conn2 = get(conn, "/api/v1/sites?limit=2&after=" <> after_cursor)
|
||||
|
||||
assert %{
|
||||
"sites" => [
|
||||
%{"domain" => ^site1_domain}
|
||||
],
|
||||
"meta" => %{
|
||||
"before" => before_cursor,
|
||||
"after" => nil,
|
||||
"limit" => 2
|
||||
}
|
||||
} = json_response(conn2, 200)
|
||||
|
||||
assert is_binary(before_cursor)
|
||||
end
|
||||
|
||||
test "lists sites for user with read-only scope", %{conn: conn, user: user} do
|
||||
%{domain: site_domain} = new_site(owner: user)
|
||||
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/api/v1/sites")
|
||||
|
||||
assert %{"sites" => [%{"domain" => ^site_domain}]} = json_response(conn, 200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/sites/guests" do
|
||||
test "returns empty when there are no guests for site", %{conn: conn, user: user} do
|
||||
site = new_site(owner: user)
|
||||
|
|
@ -1606,83 +1236,6 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/sites/:site_id" do
|
||||
setup :create_site
|
||||
|
||||
test "get a site by its domain", %{conn: conn, site: site} do
|
||||
site =
|
||||
site
|
||||
|> Ecto.Changeset.change(allowed_event_props: ["logged_in", "author"])
|
||||
|> Repo.update!()
|
||||
|
||||
conn = get(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => site.domain,
|
||||
"timezone" => site.timezone,
|
||||
"custom_properties" => ["logged_in", "author"]
|
||||
}
|
||||
end
|
||||
|
||||
test "get a site by old site_id after domain change", %{conn: conn, site: site} do
|
||||
old_domain = site.domain
|
||||
new_domain = "new.example.com"
|
||||
|
||||
Plausible.Site.Domain.change(site, new_domain)
|
||||
|
||||
conn = get(conn, "/api/v1/sites/" <> old_domain)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => new_domain,
|
||||
"timezone" => site.timezone,
|
||||
"custom_properties" => []
|
||||
}
|
||||
end
|
||||
|
||||
test "get a site for user with read-only scope", %{conn: conn, user: user, site: site} do
|
||||
api_key = insert(:api_key, user: user, scopes: ["stats:read:*"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||
|> get("/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => site.domain,
|
||||
"timezone" => site.timezone,
|
||||
"custom_properties" => []
|
||||
}
|
||||
end
|
||||
|
||||
test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do
|
||||
another_team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
add_member(another_team, user: user, role: :admin)
|
||||
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
|
||||
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
||||
|
||||
conn = get(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
||||
res = json_response(conn, 401)
|
||||
assert res["error"] =~ "Invalid API key"
|
||||
end
|
||||
|
||||
test "is 404 when site cannot be found", %{conn: conn} do
|
||||
conn = get(conn, "/api/v1/sites/foobar.baz")
|
||||
|
||||
assert json_response(conn, 404) == %{"error" => "Site could not be found"}
|
||||
end
|
||||
|
||||
@tag :capture_log
|
||||
test "is 401 when user is not a member of the site", %{conn: conn} do
|
||||
site = new_site()
|
||||
|
||||
conn = get(conn, "/api/v1/sites/" <> site.domain)
|
||||
|
||||
assert %{"error" => error} = json_response(conn, 401)
|
||||
assert error =~ "Invalid API key"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/custom-props" do
|
||||
setup :create_site
|
||||
|
||||
|
|
@ -1938,76 +1491,5 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
|||
assert error =~ "Invalid API key"
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/v1/sites/:site_id" do
|
||||
setup :create_site
|
||||
|
||||
setup %{user: user} do
|
||||
subscribe_to_enterprise_plan(user,
|
||||
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.SitesAPI]
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "can change domain name", %{conn: conn, site: site} do
|
||||
old_domain = site.domain
|
||||
assert old_domain != "new.example.com"
|
||||
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/#{old_domain}", %{
|
||||
"domain" => "new.example.com"
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"domain" => "new.example.com",
|
||||
"timezone" => "Etc/UTC",
|
||||
"custom_properties" => []
|
||||
}
|
||||
|
||||
site = Repo.reload!(site)
|
||||
|
||||
assert site.domain == "new.example.com"
|
||||
assert site.domain_changed_from == old_domain
|
||||
end
|
||||
|
||||
test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do
|
||||
another_team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
add_member(another_team, user: user, role: :admin)
|
||||
api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"])
|
||||
conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}")
|
||||
|
||||
old_domain = site.domain
|
||||
assert old_domain != "new.example.com"
|
||||
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/#{old_domain}", %{
|
||||
"domain" => "new.example.com"
|
||||
})
|
||||
|
||||
res = json_response(conn, 401)
|
||||
assert res["error"] =~ "Invalid API key"
|
||||
end
|
||||
|
||||
test "can't make a no-op change", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/#{site.domain}", %{
|
||||
"domain" => site.domain
|
||||
})
|
||||
|
||||
assert json_response(conn, 400) == %{
|
||||
"error" => "domain: New domain must be different than the current one"
|
||||
}
|
||||
end
|
||||
|
||||
test "domain parameter is required", %{conn: conn, site: site} do
|
||||
conn = put(conn, "/api/v1/sites/#{site.domain}", %{})
|
||||
|
||||
assert json_response(conn, 400) == %{
|
||||
"error" =>
|
||||
"Payload must contain at least one of the parameters 'domain', 'tracker_script_configuration'"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -483,30 +483,21 @@ defmodule PlausibleWeb.SiteControllerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "GET /:domain/installation" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "static render - spinner determining installation type", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
conn = get(conn, "/#{site.domain}/installation")
|
||||
|
||||
assert html_response(conn, 200) =~ "Determining installation type"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /:domain/settings/general" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
setup_patch_env(:google, client_id: "some", api_url: "https://www.googleapis.com")
|
||||
|
||||
test "shows settings form", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/#{site.domain}/settings/general")
|
||||
conn = get(conn, Routes.site_path(conn, :settings_general, site.domain))
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "Site timezone"
|
||||
assert resp =~ "Site domain"
|
||||
assert resp =~ "Change domain"
|
||||
assert resp =~ Routes.site_path(conn, :change_domain, site.domain)
|
||||
|
||||
assert resp =~ "Site timezone"
|
||||
|
||||
assert resp =~ "Site installation"
|
||||
end
|
||||
|
||||
|
|
@ -1861,104 +1852,6 @@ defmodule PlausibleWeb.SiteControllerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "domain change" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "shows domain change in the settings form", %{conn: conn, site: site} do
|
||||
conn = get(conn, Routes.site_path(conn, :settings_general, site.domain))
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "Site domain"
|
||||
assert resp =~ "Change domain"
|
||||
assert resp =~ Routes.site_path(conn, :change_domain, site.domain)
|
||||
end
|
||||
|
||||
test "domain change form renders", %{conn: conn, site: site} do
|
||||
conn = get(conn, Routes.site_path(conn, :change_domain, site.domain))
|
||||
resp = html_response(conn, 200)
|
||||
assert resp =~ Routes.site_path(conn, :change_domain_submit, site.domain)
|
||||
|
||||
assert resp =~
|
||||
"Once you change your domain, you <i>must</i>\n update Plausible Installation on your site within 72 hours"
|
||||
end
|
||||
|
||||
test "domain change form submission when no change is made", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, Routes.site_path(conn, :change_domain_submit, site.domain), %{
|
||||
"site" => %{"domain" => site.domain}
|
||||
})
|
||||
|
||||
resp = html_response(conn, 200)
|
||||
assert resp =~ "New domain must be different than the current one"
|
||||
end
|
||||
|
||||
test "domain change form submission to an existing domain", %{conn: conn, site: site} do
|
||||
another_site = insert(:site)
|
||||
|
||||
conn =
|
||||
put(conn, Routes.site_path(conn, :change_domain_submit, site.domain), %{
|
||||
"site" => %{"domain" => another_site.domain}
|
||||
})
|
||||
|
||||
resp = html_response(conn, 200)
|
||||
assert resp =~ "This domain cannot be registered"
|
||||
|
||||
site = Repo.reload!(site)
|
||||
assert site.domain != another_site.domain
|
||||
assert is_nil(site.domain_changed_from)
|
||||
end
|
||||
|
||||
test "domain change form submission to a domain in transition period", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
another_site = insert(:site, domain_changed_from: "foo.example.com")
|
||||
|
||||
conn =
|
||||
put(conn, Routes.site_path(conn, :change_domain_submit, site.domain), %{
|
||||
"site" => %{"domain" => "foo.example.com"}
|
||||
})
|
||||
|
||||
resp = html_response(conn, 200)
|
||||
assert resp =~ "This domain cannot be registered"
|
||||
|
||||
site = Repo.reload!(site)
|
||||
assert site.domain != another_site.domain
|
||||
assert is_nil(site.domain_changed_from)
|
||||
end
|
||||
|
||||
test "domain change successful form submission redirects to installation", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
original_domain = site.domain
|
||||
new_domain = "â-example.com"
|
||||
|
||||
conn =
|
||||
put(conn, Routes.site_path(conn, :change_domain_submit, site.domain), %{
|
||||
"site" => %{"domain" => new_domain}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) ==
|
||||
Routes.site_path(conn, :installation, new_domain,
|
||||
flow: PlausibleWeb.Flows.domain_change()
|
||||
)
|
||||
|
||||
site = Repo.reload!(site)
|
||||
assert site.domain == new_domain
|
||||
assert site.domain_changed_from == original_domain
|
||||
end
|
||||
|
||||
test "change_domain redirects to v2 when scriptv2 flag is enabled", %{conn: conn, site: site} do
|
||||
FunWithFlags.enable(:scriptv2, for_actor: site)
|
||||
|
||||
conn = get(conn, Routes.site_path(conn, :change_domain, site.domain))
|
||||
|
||||
assert redirected_to(conn) ==
|
||||
Routes.site_path(conn, :change_domain_v2, site.domain)
|
||||
end
|
||||
end
|
||||
|
||||
describe "reset stats" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
||||
defmodule PlausibleWeb.Live.ChangeDomainTest do
|
||||
use PlausibleWeb.ConnCase, async: false
|
||||
use Plausible
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
alias Plausible.Repo
|
||||
|
||||
describe "ChangeDomainV2 LiveView" do
|
||||
describe "ChangeDomain LiveView" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
on_ee do
|
||||
|
|
@ -28,13 +28,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
end
|
||||
|
||||
test "mounts and renders form", %{conn: conn, site: site} do
|
||||
{:ok, _lv, html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, _lv, html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
assert html =~ "Change your website domain"
|
||||
end
|
||||
|
||||
test "form submission when no change is made", %{conn: conn, site: site} do
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
html =
|
||||
lv
|
||||
|
|
@ -46,7 +46,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
test "form submission to an existing domain", %{conn: conn, site: site} do
|
||||
another_site = insert(:site)
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
html =
|
||||
lv
|
||||
|
|
@ -62,7 +62,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
test "form submission to a domain in transition period", %{conn: conn, site: site} do
|
||||
_another_site = insert(:site, domain_changed_from: "foo.example.com")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
html =
|
||||
lv
|
||||
|
|
@ -88,7 +88,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
original_domain = site.domain
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|
|
@ -111,13 +111,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
original_domain = site.domain
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Domain Changed Successfully"
|
||||
|
|
@ -126,7 +126,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
end
|
||||
|
||||
test "form validation shows error for empty domain", %{conn: conn, site: site} do
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
html =
|
||||
lv
|
||||
|
|
@ -137,7 +137,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
end
|
||||
|
||||
test "form validation shows error for invalid domain format", %{conn: conn, site: site} do
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
html =
|
||||
lv
|
||||
|
|
@ -148,7 +148,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
end
|
||||
|
||||
test "renders back to settings link with correct path", %{conn: conn, site: site} do
|
||||
{:ok, _lv, html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, _lv, html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
expected_link = Routes.site_path(conn, :settings_general, site.domain)
|
||||
assert html =~ expected_link
|
||||
|
|
@ -164,13 +164,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
})
|
||||
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "<i>must</i>"
|
||||
|
|
@ -180,7 +180,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
assert element_exists?(
|
||||
html,
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomainV2.change_domain_docs_link()}']"
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomain.change_domain_docs_link()}']"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -197,13 +197,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
})
|
||||
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "<i>must</i>"
|
||||
|
|
@ -214,7 +214,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
assert element_exists?(
|
||||
html,
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomainV2.change_domain_docs_link()}']"
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomain.change_domain_docs_link()}']"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -228,13 +228,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
})
|
||||
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
refute html =~ "Additional Steps Required"
|
||||
|
|
@ -243,7 +243,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
refute element_exists?(
|
||||
html,
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomainV2.change_domain_docs_link()}']"
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomain.change_domain_docs_link()}']"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -258,13 +258,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
})
|
||||
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "<i>must</i>"
|
||||
|
|
@ -274,7 +274,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
assert element_exists?(
|
||||
html,
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomainV2.change_domain_docs_link()}']"
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomain.change_domain_docs_link()}']"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -302,13 +302,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
"wordpressPlugin" => false
|
||||
})
|
||||
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Additional Steps Required"
|
||||
|
|
@ -322,13 +322,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
stub_detection_error()
|
||||
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Additional Steps Required"
|
||||
|
|
@ -343,13 +343,13 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
site: site
|
||||
} do
|
||||
new_domain = "new.#{site.domain}"
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain-v2")
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/change-domain")
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{site: %{domain: new_domain}})
|
||||
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain-v2/success")
|
||||
assert_patch(lv, "/#{URI.encode_www_form(new_domain)}/change-domain/success")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
notice = text_of_element(html, "div[data-testid='ce-generic-notice']")
|
||||
|
|
@ -358,7 +358,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|
|||
|
||||
assert element_exists?(
|
||||
html,
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomainV2.change_domain_docs_link()}']"
|
||||
"a[href='#{PlausibleWeb.Live.ChangeDomain.change_domain_docs_link()}']"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,9 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
|
|||
import Phoenix.LiveViewTest, only: [render_component: 2]
|
||||
import Plausible.Test.Support.HTML
|
||||
|
||||
alias Plausible.InstallationSupport.{State, LegacyVerification, Verification}
|
||||
alias Plausible.InstallationSupport.{State, Verification}
|
||||
|
||||
@moduletag :capture_log
|
||||
|
||||
@component PlausibleWeb.Live.Components.Verification
|
||||
@progress ~s|#verification-ui p#progress|
|
||||
|
|
@ -41,9 +43,10 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
|
|||
|
||||
test "renders diagnostic interpretation" do
|
||||
interpretation =
|
||||
LegacyVerification.Checks.interpret_diagnostics(%State{
|
||||
url: "example.com",
|
||||
diagnostics: %LegacyVerification.Diagnostics{}
|
||||
Verification.Checks.interpret_diagnostics(%State{
|
||||
url: "https://example.com",
|
||||
data_domain: "example.com",
|
||||
diagnostics: %Verification.Diagnostics{service_error: %{code: :domain_not_found}}
|
||||
})
|
||||
|
||||
html =
|
||||
|
|
@ -54,23 +57,20 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
|
|||
interpretation: interpretation
|
||||
)
|
||||
|
||||
recommendations = html |> find(@recommendations) |> Enum.map(&text/1)
|
||||
|
||||
assert recommendations == [
|
||||
"If your site is running at a different location, please manually check your integration. Learn more"
|
||||
]
|
||||
assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1)
|
||||
assert recommendation =~ "check that the domain you entered is correct"
|
||||
|
||||
refute element_exists?(html, @super_admin_report)
|
||||
end
|
||||
|
||||
test "renders super-admin report" do
|
||||
state = %State{
|
||||
url: "example.com",
|
||||
diagnostics: %LegacyVerification.Diagnostics{}
|
||||
url: "https://example.com",
|
||||
data_domain: "example.com",
|
||||
diagnostics: %Verification.Diagnostics{}
|
||||
}
|
||||
|
||||
interpretation =
|
||||
LegacyVerification.Checks.interpret_diagnostics(state)
|
||||
interpretation = Verification.Checks.interpret_diagnostics(state)
|
||||
|
||||
html =
|
||||
render_component(@component,
|
||||
|
|
@ -83,7 +83,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
|
|||
)
|
||||
|
||||
assert element_exists?(html, @super_admin_report)
|
||||
assert text_of_element(html, @super_admin_report) =~ "Snippets found in body: 0"
|
||||
assert text_of_element(html, @super_admin_report) =~ "Plausible is on window: nil"
|
||||
end
|
||||
|
||||
test "hides pulsating circle when finished, shows check circle" do
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,713 +0,0 @@
|
|||
defmodule PlausibleWeb.Live.InstallationV2Test do
|
||||
use PlausibleWeb.ConnCase
|
||||
use Plausible
|
||||
use Plausible.Test.Support.DNS
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
import Plausible.Teams.Test
|
||||
|
||||
alias Plausible.Site.TrackerScriptConfiguration
|
||||
|
||||
@migration_guide_link "https://plausible.io/docs/script-update-guide"
|
||||
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
setup %{site: site} do
|
||||
FunWithFlags.enable(:scriptv2, for_actor: site)
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "GET /:domain/installationv2" do
|
||||
@tag :ee_only
|
||||
test "renders loading installation screen on EE", %{conn: conn, site: site} do
|
||||
resp = get(conn, "/#{site.domain}/installationv2") |> html_response(200)
|
||||
|
||||
assert resp =~ "animate-spin"
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "no loading spinner, no GTM tab on CE", %{conn: conn, site: site} do
|
||||
resp = get(conn, "/#{site.domain}/installationv2") |> html_response(200)
|
||||
|
||||
tabs_text = text_of_element(resp, "a[data-phx-link='patch']")
|
||||
|
||||
assert length(String.split(tabs_text)) == 3
|
||||
|
||||
assert tabs_text =~ "Script"
|
||||
assert tabs_text =~ "WordPress"
|
||||
assert tabs_text =~ "NPM"
|
||||
|
||||
refute resp =~ "animate-spin"
|
||||
end
|
||||
end
|
||||
|
||||
describe "LiveView" do
|
||||
@tag :ee_only
|
||||
test "detects installation type when mounted", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify WordPress installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "When ?type=wordpress URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=wordpress")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify WordPress installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "When ?type=gtm URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=gtm")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify Tag Manager installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "When ?type=npm URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=npm")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify NPM installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "When ?type=manual URL parameter is supplied, detected type is unused", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Verify Script installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "allows switching between installation tabs (EE)", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
|
||||
lv
|
||||
|> element("a[href*=\"type=wordpress\"]")
|
||||
|> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Verify WordPress installation"
|
||||
|
||||
lv
|
||||
|> element("a[href*=\"type=gtm\"]")
|
||||
|> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Verify Tag Manager installation"
|
||||
|
||||
lv
|
||||
|> element("a[href*=\"type=npm\"]")
|
||||
|> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Verify NPM installation"
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "allows switching between installation tabs (CE)", %{conn: conn, site: site} do
|
||||
{lv, _html} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
|
||||
lv
|
||||
|> element("a[href*=\"type=wordpress\"]")
|
||||
|> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Verify WordPress installation"
|
||||
end
|
||||
|
||||
test "manual installations has script snippet with expected ID", %{conn: conn, site: site} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
{html =~ "Verify Script installation", html}
|
||||
end)
|
||||
|
||||
html = render(lv)
|
||||
config = Plausible.Repo.get_by!(TrackerScriptConfiguration, site_id: site.id)
|
||||
assert html =~ "Privacy-friendly analytics by Plausible"
|
||||
assert html =~ "/js/#{config.id}.js"
|
||||
assert html =~ "async"
|
||||
end
|
||||
|
||||
test "manual installation shows optional measurements", %{conn: conn, site: site} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
assert html =~ "Optional measurements"
|
||||
assert html =~ "Outbound links"
|
||||
assert html =~ "File downloads"
|
||||
assert html =~ "Form submissions"
|
||||
end
|
||||
|
||||
test "manual installation shows advanced options in disclosure", %{conn: conn, site: site} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
assert html =~ "Advanced options"
|
||||
assert html =~ "Manual tagging"
|
||||
assert html =~ "404 error pages"
|
||||
assert html =~ "Hashed page paths"
|
||||
assert html =~ "Custom properties"
|
||||
assert html =~ "Ecommerce revenue"
|
||||
end
|
||||
|
||||
test "toggling optional measurements updates tracker configuration", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
|
||||
config = TrackerScriptConfiguration |> Plausible.Repo.get_by!(site_id: site.id)
|
||||
assert config.outbound_links == true
|
||||
assert config.file_downloads == true
|
||||
assert config.form_submissions == true
|
||||
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => "manual",
|
||||
"outbound_links" => "false",
|
||||
"file_downloads" => "true",
|
||||
"form_submissions" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
updated_config = TrackerScriptConfiguration |> Plausible.Repo.get_by!(site_id: site.id)
|
||||
assert updated_config.outbound_links == false
|
||||
assert updated_config.file_downloads == true
|
||||
assert updated_config.form_submissions == true
|
||||
end
|
||||
|
||||
on_ee do
|
||||
for {type, expected_text} <- [
|
||||
{"manual", "Verify Script installation"},
|
||||
{"wordpress", "Verify WordPress installation"},
|
||||
{"gtm", "Verify Tag Manager installation"},
|
||||
{"npm", "Verify NPM installation"}
|
||||
] do
|
||||
test "submitting form with #{type} redirects to verification (EE)", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
{lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ unquote(expected_text)
|
||||
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => unquote(type),
|
||||
"outbound_links" => "true",
|
||||
"file_downloads" => "true",
|
||||
"form_submissions" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
assert_redirect(
|
||||
lv,
|
||||
Routes.site_path(conn, :verification, site.domain,
|
||||
flow: "provisioning",
|
||||
installation_type: unquote(type)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "submitting the form redirects to verification (CE)", %{conn: conn, site: site} do
|
||||
{lv, _html} = get_lv(conn, site)
|
||||
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => "manual",
|
||||
"outbound_links" => "true",
|
||||
"file_downloads" => "true",
|
||||
"form_submissions" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
assert_redirect(
|
||||
lv,
|
||||
Routes.site_path(conn, :verification, site.domain,
|
||||
flow: "provisioning",
|
||||
installation_type: "manual"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "404 goal gets created regardless of user options", %{conn: conn, site: site} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
|
||||
# Test with all options disabled
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => "manual",
|
||||
"outbound_links" => "false",
|
||||
"file_downloads" => "false",
|
||||
"form_submissions" => "false"
|
||||
}
|
||||
})
|
||||
|
||||
# 404 goal should still be created
|
||||
goals = Plausible.Goals.for_site(site)
|
||||
assert Enum.any?(goals, &(&1.event_name == "404"))
|
||||
end
|
||||
|
||||
test "submitting form with review flow redirects to verification with flow param", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
|
||||
lv
|
||||
|> element("form[phx-submit='submit']")
|
||||
|> render_submit(%{
|
||||
"tracker_script_configuration" => %{
|
||||
"installation_type" => "manual",
|
||||
"outbound_links" => "true",
|
||||
"file_downloads" => "true",
|
||||
"form_submissions" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
assert_redirect(
|
||||
lv,
|
||||
Routes.site_path(conn, :verification, site.domain,
|
||||
flow: "review",
|
||||
installation_type: "manual"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "detected WordPress installation shows special message", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "We've detected your website is using WordPress"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "if ratelimit for detection is exceeded, does not make detection request and falls back to recommending manual installation",
|
||||
%{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
# exceed the rate limit for site detection
|
||||
Plausible.RateLimit.check_rate(
|
||||
Plausible.RateLimit,
|
||||
"site_detection:#{site.domain}",
|
||||
:timer.minutes(60),
|
||||
1,
|
||||
100
|
||||
)
|
||||
|
||||
# this won't be used: if it were used, the output would be different
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
|
||||
refute text(html) =~ "We've detected your website is using WordPress"
|
||||
assert text(html) =~ "Verify Script installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "detected GTM installation shows special message", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_gtm()
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Tag Manager installation"
|
||||
|
||||
assert text(html) =~ "We've detected your website is using Google Tag Manager"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "detected NPM installation shows npm tab", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => false,
|
||||
"gtmLikely" => false,
|
||||
"npm" => true,
|
||||
"wordpressLikely" => false,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify NPM installation"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "shows v1 detection warning and migration guide link for manual installation", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual_with_v1()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=manual")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Your website is running an outdated version of the tracking script"
|
||||
assert element_exists?(html, "a[href='#{@migration_guide_link}']")
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "shows v1 migration guide link for manual instructions", %{conn: conn, site: site} do
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert text(html) =~ "Still using the legacy snippet"
|
||||
assert element_exists?(html, "a[href='#{@migration_guide_link}']")
|
||||
end
|
||||
|
||||
test "does not render link to migrate guide on WordPress installation tab", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress_with_v1()
|
||||
end
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=wordpress")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify WordPress installation"
|
||||
refute element_exists?(html, "a[href='#{@migration_guide_link}']")
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "falls back to manual installation when detection fails at dns check level", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain, [])
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
# Should default to manual installation when detection returns {:error, _}
|
||||
{html =~ "Verify Script installation", html}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "falls back to manual installation when dns succeeds but detection fails", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_error()
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
# Should default to manual installation when detection returns {:error, _}
|
||||
assert html =~ "Verify Script installation"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Authorization" do
|
||||
test "requires site access permissions", %{conn: conn} do
|
||||
other_user = insert(:user)
|
||||
other_site = new_site(owner: other_user)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
get_lv(conn, other_site)
|
||||
end
|
||||
end
|
||||
|
||||
test "allows viewer access to installation page", %{conn: conn, user: user} do
|
||||
site = new_site()
|
||||
add_guest(site, user: user, role: :viewer)
|
||||
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
end
|
||||
|
||||
test "allows editor access to installation page", %{conn: conn, user: user} do
|
||||
site = new_site()
|
||||
add_guest(site, user: user, role: :editor)
|
||||
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
end
|
||||
end
|
||||
|
||||
describe "URL Parameter Handling" do
|
||||
test "falls back to manual installation when invalid installation type parameter supplied",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?type=invalid")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
end
|
||||
|
||||
test "falls back to provisioning flow when invalid flow parameter supplied", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
on_ee do
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_manual()
|
||||
end
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?flow=invalid")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Script installation"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Detection Result Combinations" do
|
||||
@describetag :ee_only
|
||||
|
||||
test "When GTM + Wordpress detected, GTM takes precedence", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => false,
|
||||
"gtmLikely" => true,
|
||||
"wordpressLikely" => true,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
|
||||
{lv, _} = get_lv(conn, site)
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify Tag Manager installation"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Legacy Installations" do
|
||||
@tag :ee_only
|
||||
test "uses detected type in review flow when installation_type is nil", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
_config =
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
|
||||
installation_type: nil,
|
||||
outbound_links: true,
|
||||
file_downloads: false,
|
||||
form_submissions: true
|
||||
})
|
||||
|
||||
stub_lookup_a_records(site.domain)
|
||||
stub_detection_wordpress()
|
||||
|
||||
{lv, _} = get_lv(conn, site, "?flow=review")
|
||||
|
||||
html = render_async(lv, 500)
|
||||
assert html =~ "Verify WordPress installation"
|
||||
end
|
||||
end
|
||||
|
||||
defp stub_detection_manual do
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => false,
|
||||
"gtmLikely" => false,
|
||||
"npm" => false,
|
||||
"wordpressLikely" => false,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
end
|
||||
|
||||
defp stub_detection_wordpress do
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => false,
|
||||
"gtmLikely" => false,
|
||||
"npm" => false,
|
||||
"wordpressLikely" => true,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
end
|
||||
|
||||
defp stub_detection_gtm do
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => false,
|
||||
"gtmLikely" => true,
|
||||
"npm" => false,
|
||||
"wordpressLikely" => false,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
end
|
||||
|
||||
defp stub_detection_manual_with_v1 do
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => true,
|
||||
"gtmLikely" => false,
|
||||
"npm" => false,
|
||||
"wordpressLikely" => false,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
end
|
||||
|
||||
on_ee do
|
||||
defp stub_detection_wordpress_with_v1 do
|
||||
stub_detection_result(%{
|
||||
"v1Detected" => true,
|
||||
"gtmLikely" => false,
|
||||
"npm" => false,
|
||||
"wordpressLikely" => true,
|
||||
"wordpressPlugin" => false
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp stub_detection_result(js_data) do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(%{"data" => Map.put(js_data, "completed", true)}))
|
||||
end)
|
||||
end
|
||||
|
||||
defp stub_detection_error do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(
|
||||
200,
|
||||
Jason.encode!(%{"data" => %{"error" => %{"message" => "Simulated browser error"}}})
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_lv(conn, site, qs \\ nil) do
|
||||
{:ok, lv, html} = live(conn, "/#{site.domain}/installationv2#{qs}")
|
||||
|
||||
{lv, html}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
defmodule PlausibleWeb.Live.VerificationTest do
|
||||
use PlausibleWeb.ConnCase, async: true
|
||||
use PlausibleWeb.ConnCase
|
||||
|
||||
use Plausible.Test.Support.DNS
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
|
|
@ -41,8 +43,12 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
|||
describe "LiveView" do
|
||||
@tag :ee_only
|
||||
test "LiveView mounts", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, "")
|
||||
stub_installation()
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => false,
|
||||
"error" => %{"message" => "Error"}
|
||||
})
|
||||
|
||||
{_, html} = get_lv(conn, site)
|
||||
|
||||
|
|
@ -59,35 +65,50 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
|||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "ignores v2 verification custom URL input", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
test "from custom URL input form to verification", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => false,
|
||||
"error" => %{"message" => "Error"}
|
||||
})
|
||||
|
||||
# Get liveview with ?custom_url=true query param
|
||||
{:ok, lv, html} =
|
||||
conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true")
|
||||
|
||||
verifying_installation_text = "Verifying your installation"
|
||||
|
||||
# Assert form is rendered instead of kicking off verification automatically
|
||||
assert html =~ "Enter Your Custom URL"
|
||||
assert html =~ ~s[value="https://#{site.domain}"]
|
||||
assert html =~ ~s[placeholder="https://#{site.domain}"]
|
||||
refute html =~ verifying_installation_text
|
||||
|
||||
# Submit custom URL form
|
||||
html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"})
|
||||
|
||||
# Should now show verification progress and hide custom URL form
|
||||
assert html =~ verifying_installation_text
|
||||
refute html =~ "Enter Your Custom URL"
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{
|
||||
text_of_element(html, @awaiting) =~
|
||||
"Awaiting your first pageview",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Success!"
|
||||
assert html =~ "Awaiting your first pageview"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "eventually verifies installation", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => true,
|
||||
"plausibleIsOnWindow" => true,
|
||||
"plausibleIsInitialized" => true,
|
||||
"testEvent" => %{
|
||||
"normalizedBody" => %{
|
||||
"domain" => site.domain
|
||||
},
|
||||
"responseStatus" => 200
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
|
|
@ -112,8 +133,20 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
|||
build(:pageview)
|
||||
])
|
||||
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => true,
|
||||
"plausibleIsOnWindow" => true,
|
||||
"plausibleIsInitialized" => true,
|
||||
"testEvent" => %{
|
||||
"normalizedBody" => %{
|
||||
"domain" => site.domain
|
||||
},
|
||||
"responseStatus" => 200
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
|
|
@ -133,8 +166,20 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
|||
end
|
||||
|
||||
test "will redirect when first pageview arrives", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => true,
|
||||
"plausibleIsOnWindow" => true,
|
||||
"plausibleIsInitialized" => true,
|
||||
"testEvent" => %{
|
||||
"normalizedBody" => %{
|
||||
"domain" => site.domain
|
||||
},
|
||||
"responseStatus" => 200
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
|
|
@ -166,40 +211,89 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
|||
assert_redirect(lv, "/#{URI.encode_www_form(site.domain)}/")
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "eventually fails to verify installation", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, "")
|
||||
stub_installation(200, plausible_installed(false))
|
||||
for {installation_type_param, expected_text, saved_installation_type} <- [
|
||||
{"manual",
|
||||
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
|
||||
nil},
|
||||
{"npm",
|
||||
"Please make sure you've initialized Plausible on your site, or verify your installation manually.",
|
||||
nil},
|
||||
{"gtm",
|
||||
"Please make sure you've configured the GTM template correctly, or verify your installation manually.",
|
||||
nil},
|
||||
{"wordpress",
|
||||
"Please make sure you've enabled the plugin, or verify your installation manually.",
|
||||
nil},
|
||||
# trusts param over saved installation type
|
||||
{"wordpress",
|
||||
"Please make sure you've enabled the plugin, or verify your installation manually.",
|
||||
"npm"},
|
||||
# falls back to saved installation type if no param
|
||||
{"",
|
||||
"Please make sure you've initialized Plausible on your site, or verify your installation manually.",
|
||||
"npm"},
|
||||
# falls back to manual if no param and no saved installation type
|
||||
{"",
|
||||
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
|
||||
nil}
|
||||
] do
|
||||
@tag :ee_only
|
||||
test "eventually fails to verify installation (?installation_type=#{installation_type_param}) if saved installation type is #{inspect(saved_installation_type)}",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => false,
|
||||
"plausibleIsOnWindow" => false,
|
||||
"plausibleIsInitialized" => false
|
||||
})
|
||||
|
||||
assert html =
|
||||
eventually(fn ->
|
||||
html = render(lv)
|
||||
{html =~ "", html}
|
||||
if unquote(saved_installation_type) do
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
|
||||
"installation_type" => unquote(saved_installation_type)
|
||||
})
|
||||
end
|
||||
|
||||
{
|
||||
text_of_element(html, @heading) =~
|
||||
"We couldn't find the Plausible snippet",
|
||||
html
|
||||
}
|
||||
end)
|
||||
{:ok, lv} =
|
||||
kick_off_live_verification(
|
||||
conn,
|
||||
site,
|
||||
"?installation_type=#{unquote(installation_type_param)}"
|
||||
)
|
||||
|
||||
assert element_exists?(html, @retry_button)
|
||||
assert html =
|
||||
eventually(fn ->
|
||||
html = render(lv)
|
||||
{html =~ "", html}
|
||||
|
||||
assert html =~ "Please insert the snippet into your site"
|
||||
refute element_exists?(html, "#super-admin-report")
|
||||
{
|
||||
text_of_element(html, @heading) =~
|
||||
"We couldn't detect Plausible on your site",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
assert element_exists?(html, @retry_button)
|
||||
|
||||
assert html =~ htmlize_quotes(unquote(expected_text))
|
||||
refute element_exists?(html, "#super-admin-report")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_lv(conn, site) do
|
||||
{:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification")
|
||||
|
||||
defp get_lv(conn, site, qs \\ nil) do
|
||||
{:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification#{qs}")
|
||||
{lv, html}
|
||||
end
|
||||
|
||||
defp kick_off_live_verification(conn, site) do
|
||||
{:ok, lv, _html} = conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification")
|
||||
defp kick_off_live_verification(conn, site, qs \\ nil) do
|
||||
{:ok, lv, _html} =
|
||||
conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification#{qs}")
|
||||
|
||||
{:ok, lv}
|
||||
end
|
||||
|
||||
|
|
@ -211,47 +305,11 @@ defmodule PlausibleWeb.Live.VerificationTest do
|
|||
Plug.Conn.put_private(conn, :delay, 0)
|
||||
end
|
||||
|
||||
defp stub_fetch_body(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.FetchBody, f)
|
||||
end
|
||||
|
||||
defp stub_installation(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.Installation, f)
|
||||
end
|
||||
|
||||
defp stub_fetch_body(status, body) do
|
||||
stub_fetch_body(fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_resp(status, body)
|
||||
end)
|
||||
end
|
||||
|
||||
defp stub_installation(status \\ 200, json \\ plausible_installed()) do
|
||||
stub_installation(fn conn ->
|
||||
defp stub_verification_result(js_data) do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.VerifyInstallation, fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(status, Jason.encode!(json))
|
||||
|> send_resp(200, Jason.encode!(%{"data" => js_data}))
|
||||
end)
|
||||
end
|
||||
|
||||
defp plausible_installed(bool \\ true, callback_status \\ 202) do
|
||||
%{
|
||||
"data" => %{
|
||||
"completed" => true,
|
||||
"snippetsFoundInHead" => 0,
|
||||
"snippetsFoundInBody" => 0,
|
||||
"plausibleInstalled" => bool,
|
||||
"callbackStatus" => callback_status
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp source(domain) do
|
||||
"""
|
||||
<head>
|
||||
<script defer data-domain="#{domain}" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,321 +0,0 @@
|
|||
defmodule PlausibleWeb.Live.VerificationV2Test do
|
||||
use PlausibleWeb.ConnCase
|
||||
|
||||
use Plausible.Test.Support.DNS
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
|
||||
@moduletag :capture_log
|
||||
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
# @verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
|
||||
@retry_button ~s|a[phx-click="retry"]|
|
||||
# @go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]|
|
||||
@progress ~s|#verification-ui p#progress|
|
||||
@awaiting ~s|#verification-ui span#awaiting|
|
||||
@heading ~s|#verification-ui h2|
|
||||
|
||||
setup %{site: site} do
|
||||
FunWithFlags.enable(:scriptv2, for_actor: site)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "GET /:domain" do
|
||||
@tag :ee_only
|
||||
test "static verification screen renders", %{conn: conn, site: site} do
|
||||
resp =
|
||||
get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
|
||||
|> html_response(200)
|
||||
|
||||
assert text_of_element(resp, @progress) =~
|
||||
"We're visiting your site to ensure that everything is working"
|
||||
|
||||
assert resp =~ "Verifying your installation"
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "static verification screen renders (ce)", %{conn: conn, site: site} do
|
||||
resp =
|
||||
get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
|
||||
|> html_response(200)
|
||||
|
||||
assert resp =~ "Awaiting your first pageview …"
|
||||
end
|
||||
end
|
||||
|
||||
describe "LiveView" do
|
||||
@tag :ee_only
|
||||
test "LiveView mounts", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => false,
|
||||
"error" => %{"message" => "Error"}
|
||||
})
|
||||
|
||||
{_, html} = get_lv(conn, site)
|
||||
|
||||
assert html =~ "Verifying your installation"
|
||||
|
||||
assert text_of_element(html, @progress) =~
|
||||
"We're visiting your site to ensure that everything is working"
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "LiveView mounts (ce)", %{conn: conn, site: site} do
|
||||
{_, html} = get_lv(conn, site)
|
||||
assert html =~ "Awaiting your first pageview …"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "from custom URL input form to verification", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => false,
|
||||
"error" => %{"message" => "Error"}
|
||||
})
|
||||
|
||||
# Get liveview with ?custom_url=true query param
|
||||
{:ok, lv, html} =
|
||||
conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true")
|
||||
|
||||
verifying_installation_text = "Verifying your installation"
|
||||
|
||||
# Assert form is rendered instead of kicking off verification automatically
|
||||
assert html =~ "Enter Your Custom URL"
|
||||
assert html =~ ~s[value="https://#{site.domain}"]
|
||||
assert html =~ ~s[placeholder="https://#{site.domain}"]
|
||||
refute html =~ verifying_installation_text
|
||||
|
||||
# Submit custom URL form
|
||||
html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"})
|
||||
|
||||
# Should now show verification progress and hide custom URL form
|
||||
assert html =~ verifying_installation_text
|
||||
refute html =~ "Enter Your Custom URL"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "eventually verifies installation", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => true,
|
||||
"plausibleIsOnWindow" => true,
|
||||
"plausibleIsInitialized" => true,
|
||||
"testEvent" => %{
|
||||
"normalizedBody" => %{
|
||||
"domain" => site.domain
|
||||
},
|
||||
"responseStatus" => 200
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{
|
||||
text_of_element(html, @awaiting) =~
|
||||
"Awaiting your first pageview",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Success!"
|
||||
assert html =~ "Awaiting your first pageview"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "won't await first pageview if site has pageviews", %{conn: conn, site: site} do
|
||||
populate_stats(site, [
|
||||
build(:pageview)
|
||||
])
|
||||
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => true,
|
||||
"plausibleIsOnWindow" => true,
|
||||
"plausibleIsInitialized" => true,
|
||||
"testEvent" => %{
|
||||
"normalizedBody" => %{
|
||||
"domain" => site.domain
|
||||
},
|
||||
"responseStatus" => 200
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{
|
||||
text(html) =~ "Success",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
html = render(lv)
|
||||
|
||||
refute text_of_element(html, @awaiting) =~ "Awaiting your first pageview"
|
||||
refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}/")
|
||||
end
|
||||
|
||||
test "will redirect when first pageview arrives", %{conn: conn, site: site} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => true,
|
||||
"plausibleIsOnWindow" => true,
|
||||
"plausibleIsInitialized" => true,
|
||||
"testEvent" => %{
|
||||
"normalizedBody" => %{
|
||||
"domain" => site.domain
|
||||
},
|
||||
"responseStatus" => 200
|
||||
}
|
||||
})
|
||||
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{
|
||||
text(html) =~ "Awaiting",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:pageview)
|
||||
])
|
||||
|
||||
assert_redirect(lv, "/#{URI.encode_www_form(site.domain)}/")
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "will redirect when first pageview arrives (ce)", %{conn: conn, site: site} do
|
||||
{:ok, lv} = kick_off_live_verification(conn, site)
|
||||
|
||||
html = render(lv)
|
||||
assert text(html) =~ "Awaiting your first pageview …"
|
||||
|
||||
populate_stats(site, [build(:pageview)])
|
||||
|
||||
assert_redirect(lv, "/#{URI.encode_www_form(site.domain)}/")
|
||||
end
|
||||
|
||||
for {installation_type_param, expected_text, saved_installation_type} <- [
|
||||
{"manual",
|
||||
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
|
||||
nil},
|
||||
{"npm",
|
||||
"Please make sure you've initialized Plausible on your site, or verify your installation manually.",
|
||||
nil},
|
||||
{"gtm",
|
||||
"Please make sure you've configured the GTM template correctly, or verify your installation manually.",
|
||||
nil},
|
||||
{"wordpress",
|
||||
"Please make sure you've enabled the plugin, or verify your installation manually.",
|
||||
nil},
|
||||
# trusts param over saved installation type
|
||||
{"wordpress",
|
||||
"Please make sure you've enabled the plugin, or verify your installation manually.",
|
||||
"npm"},
|
||||
# falls back to saved installation type if no param
|
||||
{"",
|
||||
"Please make sure you've initialized Plausible on your site, or verify your installation manually.",
|
||||
"npm"},
|
||||
# falls back to manual if no param and no saved installation type
|
||||
{"",
|
||||
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
|
||||
nil}
|
||||
] do
|
||||
@tag :ee_only
|
||||
test "eventually fails to verify installation (?installation_type=#{installation_type_param}) if saved installation type is #{inspect(saved_installation_type)}",
|
||||
%{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
stub_lookup_a_records(site.domain)
|
||||
|
||||
stub_verification_result(%{
|
||||
"completed" => true,
|
||||
"trackerIsInHtml" => false,
|
||||
"plausibleIsOnWindow" => false,
|
||||
"plausibleIsInitialized" => false
|
||||
})
|
||||
|
||||
if unquote(saved_installation_type) do
|
||||
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
|
||||
"installation_type" => unquote(saved_installation_type)
|
||||
})
|
||||
end
|
||||
|
||||
{:ok, lv} =
|
||||
kick_off_live_verification(
|
||||
conn,
|
||||
site,
|
||||
"?installation_type=#{unquote(installation_type_param)}"
|
||||
)
|
||||
|
||||
assert html =
|
||||
eventually(fn ->
|
||||
html = render(lv)
|
||||
{html =~ "", html}
|
||||
|
||||
{
|
||||
text_of_element(html, @heading) =~
|
||||
"We couldn't detect Plausible on your site",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
assert element_exists?(html, @retry_button)
|
||||
|
||||
assert html =~ htmlize_quotes(unquote(expected_text))
|
||||
refute element_exists?(html, "#super-admin-report")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_lv(conn, site, qs \\ nil) do
|
||||
{:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification#{qs}")
|
||||
{lv, html}
|
||||
end
|
||||
|
||||
defp kick_off_live_verification(conn, site, qs \\ nil) do
|
||||
{:ok, lv, _html} =
|
||||
conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification#{qs}")
|
||||
|
||||
{:ok, lv}
|
||||
end
|
||||
|
||||
defp no_slowdown(conn) do
|
||||
Plug.Conn.put_private(conn, :slowdown, 0)
|
||||
end
|
||||
|
||||
defp no_delay(conn) do
|
||||
Plug.Conn.put_private(conn, :delay, 0)
|
||||
end
|
||||
|
||||
defp stub_verification_result(js_data) do
|
||||
Req.Test.stub(Plausible.InstallationSupport.Checks.InstallationV2, fn conn ->
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, Jason.encode!(%{"data" => js_data}))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -38,15 +38,9 @@
|
|||
"globals": {}
|
||||
},
|
||||
{
|
||||
"name": "verifier-v1.js",
|
||||
"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",
|
||||
"name": "verifier.js",
|
||||
"entry_point": "installation_support/verifier.js",
|
||||
"output_path": "priv/tracker/installation_support/verifier.js",
|
||||
"globals": {}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
const SELECTORS = {
|
||||
// https://github.com/cavi-au/Consent-O-Matic/blob/master/rules/cookiebot.json
|
||||
// We check whether any of the selectors mentioner under
|
||||
// `cookiebot.detectors[0].showingMatcher[0].target.selector`
|
||||
// is visible on the page.
|
||||
cookiebot: [
|
||||
'#CybotCookiebotDialogBodyButtonAccept',
|
||||
'#CybotCookiebotDialogBody',
|
||||
'#CybotCookiebotDialogBodyLevelButtonPreferences',
|
||||
'#cb-cookieoverlay',
|
||||
'#CybotCookiebotDialog',
|
||||
'#cookiebanner'
|
||||
]
|
||||
}
|
||||
|
||||
function isVisible(element) {
|
||||
const style = window.getComputedStyle(element)
|
||||
|
||||
return (
|
||||
style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
element.offsetParent !== null
|
||||
)
|
||||
}
|
||||
|
||||
export function checkCookieBanner() {
|
||||
for (const provider of Object.keys(SELECTORS)) {
|
||||
for (const selector of SELECTORS[provider]) {
|
||||
const element = document.querySelector(selector)
|
||||
|
||||
if (element && isVisible(element)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
export function checkDataDomainMismatch(snippets, expectedDataDomain) {
|
||||
if (!snippets || snippets.length === 0) return false
|
||||
|
||||
return snippets.some((snippet) => {
|
||||
const scriptDataDomain = snippet.getAttribute('data-domain')
|
||||
|
||||
const multiple = scriptDataDomain.split(',').map((d) => d.trim())
|
||||
const dataDomainMismatch = !multiple.some(
|
||||
(domain) => domain.replace(/^www\./, '') === expectedDataDomain
|
||||
)
|
||||
|
||||
return dataDomainMismatch
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export function checkManualExtension(snippets) {
|
||||
if (!snippets || snippets.length === 0) return false
|
||||
|
||||
return snippets.some((snippet) => {
|
||||
return snippet.getAttribute('src').includes('manual.')
|
||||
})
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export function checkProxyLikely(snippets) {
|
||||
if (!snippets || snippets.length === 0) return false
|
||||
|
||||
return snippets.some((snippet) => {
|
||||
const src = snippet.getAttribute('src')
|
||||
return src && !/^https:\/\/plausible\.io\//.test(src)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
const KNOWN_ATTRIBUTES = [
|
||||
'data-domain',
|
||||
'src',
|
||||
'defer',
|
||||
'async',
|
||||
'data-api',
|
||||
'data-exclude',
|
||||
'data-include',
|
||||
'data-cfasync'
|
||||
]
|
||||
|
||||
export function checkUnknownAttributes(snippets) {
|
||||
if (!snippets || snippets.length === 0) return false
|
||||
|
||||
return snippets.some((snippet) => {
|
||||
const attributes = snippet.attributes
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i]
|
||||
|
||||
if (attr.name === 'type' && attr.value === 'text/javascript') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (attr.name.startsWith('event-')) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!KNOWN_ATTRIBUTES.includes(attr.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { waitForPlausibleFunction } from './plausible-function-check'
|
||||
import { waitForPlausibleFunction } from './wait-for-plausible-function'
|
||||
import { checkWordPress } from './check-wordpress'
|
||||
import { checkGTM } from './check-gtm'
|
||||
import { checkNPM } from './check-npm'
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import { runThrottledCheck } from './run-check'
|
||||
|
||||
export async function plausibleFunctionCheck(log) {
|
||||
log('Checking for Plausible function...')
|
||||
const plausibleFound = await waitForPlausibleFunction()
|
||||
|
||||
if (plausibleFound) {
|
||||
log('Plausible function found. Executing test event...')
|
||||
const callbackResult = await testPlausibleCallback(log)
|
||||
log(`Test event callback response: ${callbackResult.status}`)
|
||||
return { plausibleInstalled: true, callbackStatus: callbackResult.status }
|
||||
} else {
|
||||
log('Plausible function not found')
|
||||
return { plausibleInstalled: false }
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForPlausibleFunction(timeout = 5000) {
|
||||
const checkFn = (opts) => {
|
||||
if (window.plausible?.l) {
|
||||
return true
|
||||
}
|
||||
if (opts.timeout) {
|
||||
return false
|
||||
}
|
||||
return 'continue'
|
||||
}
|
||||
return await runThrottledCheck(checkFn, { timeout: timeout, interval: 100 })
|
||||
}
|
||||
|
||||
function testPlausibleCallback(log) {
|
||||
return new Promise((resolve) => {
|
||||
let callbackResolved = false
|
||||
|
||||
const callbackTimeout = setTimeout(() => {
|
||||
if (!callbackResolved) {
|
||||
callbackResolved = true
|
||||
log('Timeout waiting for Plausible function callback')
|
||||
resolve({ status: undefined })
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
try {
|
||||
window.plausible('verification-agent-test', {
|
||||
callback: function (options) {
|
||||
if (!callbackResolved) {
|
||||
callbackResolved = true
|
||||
clearTimeout(callbackTimeout)
|
||||
resolve({ status: options && options.status ? options.status : -1 })
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (!callbackResolved) {
|
||||
callbackResolved = true
|
||||
clearTimeout(callbackTimeout)
|
||||
log('Error calling plausible function:', error)
|
||||
resolve({ status: -1 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import { runThrottledCheck } from './run-check'
|
||||
|
||||
export async function waitForSnippetsV1(log) {
|
||||
log('Starting snippet detection...')
|
||||
|
||||
let snippetCounts = await waitForFirstSnippet()
|
||||
|
||||
if (snippetCounts.all > 0) {
|
||||
log(
|
||||
`Found snippets: head=${snippetCounts.head}; body=${snippetCounts.body}`
|
||||
)
|
||||
log('Waiting for additional snippets to appear...')
|
||||
|
||||
snippetCounts = await waitForAdditionalSnippets()
|
||||
|
||||
log(
|
||||
`Final snippet count: head=${snippetCounts.head}; body=${snippetCounts.body}`
|
||||
)
|
||||
} else {
|
||||
log('No snippets found after 5 seconds')
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: [...getHeadSnippets(), ...getBodySnippets()],
|
||||
counts: snippetCounts
|
||||
}
|
||||
}
|
||||
|
||||
function getHeadSnippets() {
|
||||
return document.querySelectorAll('head script[data-domain][src]')
|
||||
}
|
||||
|
||||
function getBodySnippets() {
|
||||
return document.querySelectorAll('body script[data-domain][src]')
|
||||
}
|
||||
|
||||
function countSnippets() {
|
||||
const headSnippets = getHeadSnippets()
|
||||
const bodySnippets = getBodySnippets()
|
||||
|
||||
return {
|
||||
head: headSnippets.length,
|
||||
body: bodySnippets.length,
|
||||
all: headSnippets.length + bodySnippets.length
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForFirstSnippet() {
|
||||
const checkFn = (opts) => {
|
||||
const snippetsFound = countSnippets()
|
||||
|
||||
if (snippetsFound.all > 0 || opts.timeout) {
|
||||
return snippetsFound
|
||||
}
|
||||
|
||||
return 'continue'
|
||||
}
|
||||
|
||||
return await runThrottledCheck(checkFn, { timeout: 5000, interval: 100 })
|
||||
}
|
||||
|
||||
async function waitForAdditionalSnippets() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(countSnippets())
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { waitForSnippetsV1 } from './snippet-checks'
|
||||
import { plausibleFunctionCheck } from './plausible-function-check'
|
||||
import { checkDataDomainMismatch } from './check-data-domain-mismatch'
|
||||
import { checkProxyLikely } from './check-proxy-likely'
|
||||
import { checkWordPress } from './check-wordpress'
|
||||
import { checkGTM } from './check-gtm'
|
||||
import { checkCookieBanner } from './check-cookie-banner'
|
||||
import { checkManualExtension } from './check-manual-extension'
|
||||
import { checkUnknownAttributes } from './check-unknown-attributes'
|
||||
|
||||
window.verifyPlausibleInstallation = async function (
|
||||
expectedDataDomain,
|
||||
debug
|
||||
) {
|
||||
function log(message) {
|
||||
if (debug) console.log('[Plausible Verification]', message)
|
||||
}
|
||||
|
||||
const [snippetData, plausibleFunctionDiagnostics] = await Promise.all([
|
||||
waitForSnippetsV1(log),
|
||||
plausibleFunctionCheck(log)
|
||||
])
|
||||
|
||||
const plausibleInstalled = plausibleFunctionDiagnostics.plausibleInstalled
|
||||
const callbackStatus = plausibleFunctionDiagnostics.callbackStatus || 0
|
||||
|
||||
const dataDomainMismatch = checkDataDomainMismatch(
|
||||
snippetData.nodes,
|
||||
expectedDataDomain
|
||||
)
|
||||
log(`dataDomainMismatch: ${dataDomainMismatch}`)
|
||||
|
||||
const manualScriptExtension = checkManualExtension(snippetData.nodes)
|
||||
log(`manualScriptExtension: ${manualScriptExtension}`)
|
||||
|
||||
const unknownAttributes = checkUnknownAttributes(snippetData.nodes)
|
||||
log(`unknownAttributes: ${unknownAttributes}`)
|
||||
|
||||
const proxyLikely = checkProxyLikely(snippetData.nodes)
|
||||
log(`proxyLikely: ${proxyLikely}`)
|
||||
|
||||
const { wordpressPlugin, wordpressLikely } = checkWordPress(document)
|
||||
log(`wordpressPlugin: ${wordpressPlugin}`)
|
||||
log(`wordpressLikely: ${wordpressLikely}`)
|
||||
|
||||
const gtmLikely = checkGTM(document)
|
||||
log(`gtmLikely: ${gtmLikely}`)
|
||||
|
||||
let cookieBannerLikely
|
||||
|
||||
if (plausibleInstalled && [200, 202].includes(callbackStatus)) {
|
||||
cookieBannerLikely = false
|
||||
} else {
|
||||
cookieBannerLikely = checkCookieBanner()
|
||||
}
|
||||
|
||||
log(`cookieBannerLikely: ${cookieBannerLikely}`)
|
||||
|
||||
return {
|
||||
data: {
|
||||
completed: true,
|
||||
plausibleInstalled: plausibleFunctionDiagnostics.plausibleInstalled,
|
||||
callbackStatus: plausibleFunctionDiagnostics.callbackStatus || 0,
|
||||
snippetsFoundInHead: snippetData.counts.head,
|
||||
snippetsFoundInBody: snippetData.counts.body,
|
||||
dataDomainMismatch: dataDomainMismatch,
|
||||
proxyLikely: proxyLikely,
|
||||
wordpressPlugin: wordpressPlugin,
|
||||
wordpressLikely: wordpressLikely,
|
||||
gtmLikely: gtmLikely,
|
||||
cookieBannerLikely: cookieBannerLikely,
|
||||
manualScriptExtension: manualScriptExtension,
|
||||
unknownAttributes: unknownAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
/** @typedef {import('../test/support/types').VerifyV2Args} VerifyV2Args */
|
||||
/** @typedef {import('../test/support/types').VerifyV2Result} VerifyV2Result */
|
||||
/** @typedef {import('../test/support/types').VerifierArgs} VerifierArgs */
|
||||
/** @typedef {import('../test/support/types').VerifierResult} VerifierResult */
|
||||
import { initializeCookieConsentEngine } from './autoconsent-to-cookies'
|
||||
import { checkDisallowedByCSP } from './check-disallowed-by-csp'
|
||||
|
||||
/**
|
||||
* Function that verifies if Plausible is installed correctly.
|
||||
* @param {VerifyV2Args}
|
||||
* @returns {Promise<VerifyV2Result>}
|
||||
* @param {VerifierArgs}
|
||||
* @returns {Promise<VerifierResult>}
|
||||
*/
|
||||
|
||||
const DEFAULT_TRACKER_SCRIPT_SELECTOR = 'script[src^="https://plausible.io/js"]'
|
||||
|
|
@ -24,7 +24,7 @@ async function verifyPlausibleInstallation(options) {
|
|||
}
|
||||
|
||||
function log(message) {
|
||||
if (debug) console.log('[VERIFICATION v2]', message)
|
||||
if (debug) console.log('[VERIFICATION]', message)
|
||||
}
|
||||
|
||||
const disallowedByCsp = checkDisallowedByCSP(responseHeaders, cspHostToCheck)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { runThrottledCheck } from './run-check'
|
||||
|
||||
export async function waitForPlausibleFunction(timeout = 5000) {
|
||||
const checkFn = (opts) => {
|
||||
if (window.plausible?.l) {
|
||||
return true
|
||||
}
|
||||
if (opts.timeout) {
|
||||
return false
|
||||
}
|
||||
return 'continue'
|
||||
}
|
||||
return await runThrottledCheck(checkFn, { timeout: timeout, interval: 100 })
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
import { checkDataDomainMismatch } from '../../installation_support/check-data-domain-mismatch'
|
||||
|
||||
function mockSnippet(dataDomain) {
|
||||
return { getAttribute: (_) => dataDomain }
|
||||
}
|
||||
|
||||
test.describe('checkDataDomainMismatch', () => {
|
||||
test('returns false when no snippets provided', () => {
|
||||
expect(checkDataDomainMismatch([], 'example.com')).toBe(false)
|
||||
expect(checkDataDomainMismatch(null, 'example.com')).toBe(false)
|
||||
expect(checkDataDomainMismatch(undefined, 'example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('handles empty data-domain attribute', () => {
|
||||
const snippets = [mockSnippet('')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when snippet data-domain matches expected domain', () => {
|
||||
const snippets = [mockSnippet('example.com')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when snippet data-domain does not match expected domain', () => {
|
||||
const snippets = [mockSnippet('wrong.com')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(true)
|
||||
})
|
||||
|
||||
test('allows www. in data-domain', () => {
|
||||
const snippets = [mockSnippet('www.example.com')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('handles multiple domains in data-domain attribute', () => {
|
||||
const snippets = [mockSnippet('example.org,example.com,example.net')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('handles multiple domains with spaces in data-domain attribute', () => {
|
||||
const snippets = [mockSnippet('example.org, example.com, example.net')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('handles multiple domains with www prefix', () => {
|
||||
const snippets = [
|
||||
mockSnippet('www.example.org, www.example.com, www.example.net')
|
||||
]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when expected domain not in multi-domain list', () => {
|
||||
const snippets = [mockSnippet('example.org,example.com,example.net')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.typo')).toBe(true)
|
||||
})
|
||||
|
||||
test('handles multiple snippets - returns true if any snippet has domain mismatch', () => {
|
||||
const snippets = [mockSnippet('example.com'), mockSnippet('wrong.com')]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(true)
|
||||
})
|
||||
|
||||
test('handles multiple snippets - returns false if all snippets match', () => {
|
||||
const snippets = [
|
||||
mockSnippet('example.com'),
|
||||
mockSnippet('example.org,example.com,example.net')
|
||||
]
|
||||
expect(checkDataDomainMismatch(snippets, 'example.com')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
import { checkManualExtension } from '../../installation_support/check-manual-extension'
|
||||
|
||||
function mockSnippet(dataDomain) {
|
||||
return { getAttribute: (_) => dataDomain }
|
||||
}
|
||||
|
||||
test.describe('checkManualExtension', () => {
|
||||
test('returns false when no snippets provided', () => {
|
||||
expect(checkManualExtension([])).toBe(false)
|
||||
expect(checkManualExtension(null)).toBe(false)
|
||||
expect(checkManualExtension(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
test('handles empty src attribute', () => {
|
||||
const snippets = [mockSnippet('')]
|
||||
expect(checkManualExtension(snippets)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when snippet src includes manual', () => {
|
||||
const snippets = [mockSnippet('https://plausible.io/js/script.manual.js')]
|
||||
expect(checkManualExtension(snippets)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when snippet src does not include manual', () => {
|
||||
const snippets = [mockSnippet('https://plausible.io/js/script.js')]
|
||||
expect(checkManualExtension(snippets)).toBe(false)
|
||||
})
|
||||
|
||||
test('handles multiple snippets - returns true if any src includes manual', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://plausible.io/js/plausible.manual.js'),
|
||||
mockSnippet('https://plausible.io/js/plausible.js')
|
||||
]
|
||||
expect(checkManualExtension(snippets)).toBe(true)
|
||||
})
|
||||
|
||||
test('handles multiple snippets - returns false if no manual snippets', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://plausible.io/js/plausible.outbound-links.js'),
|
||||
mockSnippet('https://plausible.io/js/plausible.compat.js')
|
||||
]
|
||||
expect(checkManualExtension(snippets)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
import { checkProxyLikely } from '../../installation_support/check-proxy-likely'
|
||||
|
||||
function mockSnippet(src) {
|
||||
return { getAttribute: (_) => src }
|
||||
}
|
||||
|
||||
test.describe('checkProxyLikely', () => {
|
||||
test('returns false when no snippets provided', () => {
|
||||
expect(checkProxyLikely([])).toBe(false)
|
||||
expect(checkProxyLikely(null)).toBe(false)
|
||||
expect(checkProxyLikely(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
test('handles empty src attribute', () => {
|
||||
const snippets = [mockSnippet('')]
|
||||
expect(checkProxyLikely(snippets)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when snippet src is official plausible.io URL', () => {
|
||||
const snippets = [mockSnippet('https://plausible.io/js/plausible.js')]
|
||||
expect(checkProxyLikely(snippets)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when snippet src is official plausible.io URL with query params', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://plausible.io/js/plausible.js?v=1.0.0')
|
||||
]
|
||||
expect(checkProxyLikely(snippets)).toBe(false)
|
||||
})
|
||||
|
||||
test('handles similar domain names (should be true)', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://plausible.io.example.com/js/plausible.js')
|
||||
]
|
||||
expect(checkProxyLikely(snippets)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true when snippet src is relative path', () => {
|
||||
const snippets = [mockSnippet('/js/plausible.js')]
|
||||
expect(checkProxyLikely(snippets)).toBe(true)
|
||||
})
|
||||
|
||||
test('handles multiple snippets - returns true if any snippet is proxied', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://plausible.io/js/plausible.js'),
|
||||
mockSnippet('https://analytics.example.com/js/plausible.js')
|
||||
]
|
||||
expect(checkProxyLikely(snippets)).toBe(true)
|
||||
})
|
||||
|
||||
test('handles multiple snippets - returns false if all snippets are official', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://plausible.io/js/plausible.js'),
|
||||
mockSnippet('https://plausible.io/js/plausible.outbound-links.js')
|
||||
]
|
||||
expect(checkProxyLikely(snippets)).toBe(false)
|
||||
})
|
||||
|
||||
test('handles plausible.io subdomain (should be true)', () => {
|
||||
const snippets = [
|
||||
mockSnippet('https://staging.plausible.io/js/plausible.js')
|
||||
]
|
||||
expect(checkProxyLikely(snippets)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
import { checkUnknownAttributes } from '../../installation_support/check-unknown-attributes.js'
|
||||
|
||||
test.describe('checkUnknownAttributes', () => {
|
||||
test('returns false when no snippets', () => {
|
||||
expect(checkUnknownAttributes([])).toBe(false)
|
||||
expect(checkUnknownAttributes(null)).toBe(false)
|
||||
expect(checkUnknownAttributes(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when all attributes are known', () => {
|
||||
const mockSnippet = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'async', value: '' },
|
||||
{ name: 'data-api', value: '/api/event' },
|
||||
{ name: 'data-exclude', value: '/admin/*' },
|
||||
{ name: 'data-include', value: '/blog/*' },
|
||||
{ name: 'data-cfasync', value: 'false' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet])).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when type="text/javascript" attribute is present', () => {
|
||||
const mockSnippet = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'type', value: 'text/javascript' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet])).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when event-* attributes are present', () => {
|
||||
const mockSnippet = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'event-click', value: 'handler' },
|
||||
{ name: 'event-load', value: 'loadHandler' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet])).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when unknown attributes are present', () => {
|
||||
const mockSnippet = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'unknown-attribute', value: 'value' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet])).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true when multiple unknown attributes are present', () => {
|
||||
const mockSnippet = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'unknown-attribute-1', value: 'value1' },
|
||||
{ name: 'unknown-attribute-2', value: 'value2' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet])).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true when at least one snippet has unknown attributes', () => {
|
||||
const mockSnippet1 = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' }
|
||||
]
|
||||
}
|
||||
|
||||
const mockSnippet2 = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'unknown-attribute', value: 'value' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet1, mockSnippet2])).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when all snippets have only known attributes', () => {
|
||||
const mockSnippet1 = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' }
|
||||
]
|
||||
}
|
||||
|
||||
const mockSnippet2 = {
|
||||
attributes: [
|
||||
{ name: 'data-domain', value: 'example.com' },
|
||||
{ name: 'src', value: 'https://plausible.io/js/script.js' },
|
||||
{ name: 'async', value: '' }
|
||||
]
|
||||
}
|
||||
|
||||
expect(checkUnknownAttributes([mockSnippet1, mockSnippet2])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,734 +0,0 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
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'
|
||||
|
||||
const SOME_DOMAIN = 'somesite.com'
|
||||
|
||||
async function mockEventResponseSuccess(page, responseDelay = 0) {
|
||||
await page.context().route('**/api/event', async (route) => {
|
||||
if (responseDelay > 0) {
|
||||
await delay(responseDelay)
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 202,
|
||||
contentType: 'text/plain',
|
||||
body: 'ok'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('v1 verifier (basic diagnostics)', () => {
|
||||
test('correct installation', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||
expect(result.data.snippetsFoundInBody).toBe(0)
|
||||
expect(result.data.callbackStatus).toBe(202)
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
expect(result.data.wordpressPlugin).toBe(false)
|
||||
expect(result.data.wordpressLikely).toBe(false)
|
||||
expect(result.data.cookieBannerLikely).toBe(false)
|
||||
expect(result.data.manualScriptExtension).toBe(false)
|
||||
|
||||
// `data.proxyLikely` is mostly expected to be true in tests because
|
||||
// any local script src is considered a proxy. More involved behaviour
|
||||
// is covered by unit tests under `check-proxy-likely.spec.js`
|
||||
expect(result.data.proxyLikely).toBe(true)
|
||||
})
|
||||
|
||||
test('handles a dynamically loaded snippet', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<script>
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.async = true
|
||||
script.dataset.domain = '${SOME_DOMAIN}'
|
||||
script.src = '/tracker/js/plausible.local.manual.js'
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementsByTagName('head')[0].appendChild(script)
|
||||
}, 500)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN,
|
||||
debug: true
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||
expect(result.data.snippetsFoundInBody).toBe(0)
|
||||
expect(result.data.callbackStatus).toBe(202)
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
})
|
||||
|
||||
test('missing snippet', async ({ page }, { testId }) => {
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: ''
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(false)
|
||||
expect(result.data.callbackStatus).toBe(0)
|
||||
expect(result.data.snippetsFoundInHead).toBe(0)
|
||||
expect(result.data.snippetsFoundInBody).toBe(0)
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
})
|
||||
|
||||
test('snippet in body', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `<body>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</body>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.snippetsFoundInHead).toBe(0)
|
||||
expect(result.data.snippetsFoundInBody).toBe(1)
|
||||
expect(result.data.callbackStatus).toBe(202)
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
})
|
||||
|
||||
test('figures out well placed snippet in a multi-domain setup', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `<head>
|
||||
<script
|
||||
async
|
||||
data-domain="example.org,example.com,example.net"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</head>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: 'example.com'
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||
expect(result.data.snippetsFoundInBody).toBe(0)
|
||||
expect(result.data.callbackStatus).toBe(202)
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
})
|
||||
|
||||
test('figures out well placed snippet in a multi-domain mismatch', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `<head>
|
||||
<script
|
||||
async
|
||||
data-domain="example.org,example.com,example.net"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</head>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: 'example.typo'
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.snippetsFoundInHead).toBe(1)
|
||||
expect(result.data.snippetsFoundInBody).toBe(0)
|
||||
expect(result.data.callbackStatus).toBe(202)
|
||||
expect(result.data.dataDomainMismatch).toBe(true)
|
||||
})
|
||||
|
||||
test('proxyLikely is false when every snippet starts with an official plausible.io URL', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
const prodScriptLocation = 'https://plausible.io/js/'
|
||||
|
||||
mockEventResponseSuccess(page)
|
||||
|
||||
// We speed up the test by serving "just some script"
|
||||
// (avoiding the event callback delay in verifier)
|
||||
const code = await compileFile(
|
||||
{
|
||||
name: 'plausible.local.js',
|
||||
globals: {
|
||||
COMPILE_LOCAL: true,
|
||||
COMPILE_PLAUSIBLE_LEGACY_VARIANT: true
|
||||
}
|
||||
},
|
||||
{ returnCode: true }
|
||||
)
|
||||
|
||||
await page.context().route(`${prodScriptLocation}**`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/javascript',
|
||||
body: code
|
||||
})
|
||||
})
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
src="${prodScriptLocation + 'script.js'}"
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
async
|
||||
src="${prodScriptLocation + 'plausible.outbound-links.js'}"
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
></script>
|
||||
</body>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.proxyLikely).toBe(false)
|
||||
})
|
||||
|
||||
test('counting snippets', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
data-domain="example.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
<script
|
||||
async
|
||||
data-domain="example.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
async
|
||||
data-domain="example.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
<script
|
||||
async
|
||||
data-domain="example.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
<script
|
||||
async
|
||||
data-domain="example.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</body>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: 'example.com'
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.snippetsFoundInHead).toBe(2)
|
||||
expect(result.data.snippetsFoundInBody).toBe(3)
|
||||
expect(result.data.callbackStatus).toBe(202)
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
})
|
||||
|
||||
test('detects dataDomainMismatch', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="wrong.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: 'right.com'
|
||||
})
|
||||
|
||||
expect(result.data.dataDomainMismatch).toBe(true)
|
||||
})
|
||||
|
||||
test('dataDomainMismatch is false when data-domain without "www." prefix matches', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="www.right.com"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: 'right.com'
|
||||
})
|
||||
|
||||
expect(result.data.dataDomainMismatch).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (window.plausible)', () => {
|
||||
test('callbackStatus is 404 when /api/event not found', async ({ page }, {
|
||||
testId
|
||||
}) => {
|
||||
await page.context().route('**/api/event', async (route) => {
|
||||
await route.fulfill({ status: 404 })
|
||||
})
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.callbackStatus).toBe(404)
|
||||
})
|
||||
|
||||
test('callBackStatus is 0 when event request times out', async ({ page }, {
|
||||
testId
|
||||
}) => {
|
||||
mockEventResponseSuccess(page, 20000)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.callbackStatus).toBe(0)
|
||||
})
|
||||
|
||||
test('callBackStatus is -1 when a network error occurs on sending event', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await page.context().route('**/api/event', async (route) => {
|
||||
await route.abort()
|
||||
})
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.plausibleInstalled).toBe(true)
|
||||
expect(result.data.callbackStatus).toBe(-1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (WordPress detection)', () => {
|
||||
test('if wordpress plugin detected, wordpressLikely is also true', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<head>
|
||||
<meta name="plausible-analytics-version" content="2.3.1" />
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</head>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.wordpressPlugin).toBe(true)
|
||||
expect(result.data.wordpressLikely).toBe(true)
|
||||
})
|
||||
|
||||
test('detects wordpressLikely by wp signatures', async ({ page }, {
|
||||
testId
|
||||
}) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/wp-content/themes/mytheme/script.js"></script>
|
||||
</body>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.wordpressPlugin).toBe(false)
|
||||
expect(result.data.wordpressLikely).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (GTM detection)', () => {
|
||||
test('detects GTM', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<html>
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>
|
||||
;(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []
|
||||
w[l].push({
|
||||
'gtm.start': new Date().getTime(),
|
||||
event: 'gtm.js'
|
||||
})
|
||||
var f = d.getElementsByTagName(s)[0],
|
||||
j = d.createElement(s),
|
||||
dl = l != 'dataLayer' ? '&l=' + l : ''
|
||||
j.async = true
|
||||
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl
|
||||
f.parentNode.insertBefore(j, f)
|
||||
})(window, document, 'script', 'dataLayer', 'XXXX')
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.gtmLikely).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (cookieBanner detection)', () => {
|
||||
test('detects a dynamically loaded cookiebot', async ({ page }, {
|
||||
testId
|
||||
}) => {
|
||||
// While in real world the plausible script would be prevented
|
||||
// from loading when cookiebot is present, to speed up the test
|
||||
// we let it load, but mock a general network error. That is to
|
||||
// avoid the a 202 response which skips cookiebot detection.
|
||||
await page.context().route('**/api/event', async (route) => {
|
||||
// To make sure the banner gets dynamically loaded before the
|
||||
// event callback finishes, we mock a 1s delay before aborting.
|
||||
await delay(1000)
|
||||
await route.abort()
|
||||
})
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<html>
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
const banner = document.createElement('div')
|
||||
banner.id = 'CybotCookiebotDialog'
|
||||
document.body.appendChild(banner)
|
||||
}, 500)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.cookieBannerLikely).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (manualScriptExtension detection)', () => {
|
||||
test('manualScriptExtension is true when any snippet src has "manual." in it', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<html>
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.hash.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.manual.js"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.manualScriptExtension).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (unknownAttributes detection)', () => {
|
||||
test('unknownAttributes is false when all attrs are known', async ({ page }, {
|
||||
testId
|
||||
}) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<html>
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
type="text/javascript"
|
||||
data-cfasync="false"
|
||||
data-api="some"
|
||||
data-include="some"
|
||||
data-exclude="some"
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.manual.js"
|
||||
></script>
|
||||
</head>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.unknownAttributes).toBe(false)
|
||||
})
|
||||
|
||||
test('unknownAttributes is true when any unknown attributes are present', async ({
|
||||
page
|
||||
}, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
response: /* HTML */ `
|
||||
<html>
|
||||
<head>
|
||||
<script
|
||||
async
|
||||
weird="one"
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/script.js"
|
||||
></script>
|
||||
</head>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
const result = await verifyV1(page, {
|
||||
url: url,
|
||||
expectedDataDomain: SOME_DOMAIN
|
||||
})
|
||||
|
||||
expect(result.data.unknownAttributes).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('v1 verifier (logging)', () => {
|
||||
test('console logs in debug mode', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
let logs = []
|
||||
page
|
||||
.context()
|
||||
.on('console', (msg) => msg.type() === 'log' && logs.push(msg.text()))
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
test('does not log by default', async ({ page }, { testId }) => {
|
||||
await mockEventResponseSuccess(page)
|
||||
|
||||
let logs = []
|
||||
page
|
||||
.context()
|
||||
.on('console', (msg) => msg.type() === 'log' && logs.push(msg.text()))
|
||||
|
||||
const { url } = await initializePageDynamically(page, {
|
||||
testId,
|
||||
scriptConfig: /* HTML */ `<script
|
||||
async
|
||||
data-domain="${SOME_DOMAIN}"
|
||||
src="/tracker/js/plausible.local.js"
|
||||
></script>`
|
||||
})
|
||||
|
||||
await verifyV1(page, { url: url, expectedDataDomain: SOME_DOMAIN })
|
||||
|
||||
expect(logs.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { test, expect } from '@playwright/test'
|
||||
import { executeVerifyV2 } from '../support/installation-support-playwright-wrappers'
|
||||
import { executeVerifier } 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'
|
||||
|
|
@ -46,7 +46,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -108,7 +108,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders,
|
||||
timeoutMs
|
||||
|
|
@ -168,7 +168,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -216,7 +216,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -270,7 +270,7 @@ test.describe('installed plausible web variant', () => {
|
|||
await expect(page.getByText('alfa')).toBeVisible()
|
||||
|
||||
const [result, _] = await Promise.all([
|
||||
executeVerifyV2(page, {
|
||||
executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
timeoutMs: 1500,
|
||||
responseHeaders
|
||||
|
|
@ -350,7 +350,7 @@ test.describe('installed plausible web variant', () => {
|
|||
await expect(page.getByText('alfa')).toBeVisible()
|
||||
|
||||
const [result] = await Promise.all([
|
||||
executeVerifyV2(page, {
|
||||
executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
timeoutMs: 1500,
|
||||
timeoutBetweenAttemptsMs: 100,
|
||||
|
|
@ -426,7 +426,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -488,7 +488,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
cspHostToCheck,
|
||||
responseHeaders
|
||||
|
|
@ -551,7 +551,7 @@ test.describe('installed plausible web variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
cspHostToCheck,
|
||||
responseHeaders
|
||||
|
|
@ -616,7 +616,7 @@ test.describe('installed plausible esm variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -681,7 +681,7 @@ test.describe('installed plausible esm variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -746,7 +746,7 @@ test.describe('installed plausible esm variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -800,7 +800,7 @@ test.describe('installed plausible esm variant', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -861,7 +861,7 @@ test.describe('installed legacy .compat script', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
responseHeaders
|
||||
})
|
||||
|
|
@ -929,7 +929,7 @@ test.describe('opts in on cookie banners', () => {
|
|||
const response = await page.goto(url)
|
||||
const responseHeaders = response?.headers() ?? {}
|
||||
|
||||
const result = await executeVerifyV2(page, {
|
||||
const result = await executeVerifier(page, {
|
||||
...DEFAULT_VERIFICATION_OPTIONS,
|
||||
timeoutMs: 2000,
|
||||
responseHeaders
|
||||
|
|
@ -1,28 +1,25 @@
|
|||
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'
|
||||
import { VerifierArgs, VerifierResult } 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 VERIFIER_JS_VARIANT = variantsFile.manualVariants.find(
|
||||
(variant) => variant.name === 'verifier.js'
|
||||
)
|
||||
const DETECTOR_JS_VARIANT = variantsFile.manualVariants.find(
|
||||
(variant) => variant.name === 'detector.js'
|
||||
)
|
||||
|
||||
export async function executeVerifyV2(
|
||||
export async function executeVerifier(
|
||||
page: Page,
|
||||
{
|
||||
responseHeaders,
|
||||
maxAttempts,
|
||||
timeoutBetweenAttemptsMs,
|
||||
...functionContext
|
||||
}: VerifyV2Args & { maxAttempts: number; timeoutBetweenAttemptsMs: number }
|
||||
): Promise<VerifyV2Result> {
|
||||
const verifierCode = (await compileFile(VERIFIER_V2_JS_VARIANT, {
|
||||
}: VerifierArgs & { maxAttempts: number; timeoutBetweenAttemptsMs: number }
|
||||
): Promise<VerifierResult> {
|
||||
const verifierCode = (await compileFile(VERIFIER_JS_VARIANT, {
|
||||
returnCode: true
|
||||
})) as string
|
||||
|
||||
|
|
@ -75,26 +72,6 @@ export async function executeVerifyV2(
|
|||
}
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
// @ts-expect-error - window.verifyPlausibleInstallation has been injected
|
||||
return await window.verifyPlausibleInstallation(expectedDataDomain, debug)
|
||||
},
|
||||
{ expectedDataDomain, debug }
|
||||
)
|
||||
}
|
||||
|
||||
export async function detect(page, context) {
|
||||
const { url, detectV1, timeoutMs } = context
|
||||
const debug = context.debug ? true : false
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export type ScriptConfig = {
|
|||
endpoint: string
|
||||
} & Partial<Options>
|
||||
|
||||
export type VerifyV2Args = {
|
||||
export type VerifierArgs = {
|
||||
debug: boolean
|
||||
responseHeaders: Record<string, string>
|
||||
timeoutMs: number
|
||||
|
|
@ -43,7 +43,7 @@ type ConsentResult =
|
|||
engineLifecycle: string
|
||||
}
|
||||
|
||||
export type VerifyV2Result = {
|
||||
export type VerifierResult = {
|
||||
data:
|
||||
| {
|
||||
completed: true
|
||||
|
|
|
|||
Loading…
Reference in New Issue