mirror of
https://github.com/plausible/analytics.git
synced 2025-03-14 10:06:38 +00:00
Snippet integration verification (#4106)
* Allow running browserless.io locally * Compile tailwind classes based on extra/ too * Add browserless runtime configuration * Ignore verification events on ingestion * Improve extracting HTML text in tests * Update dependencies - Floki will be used on production to parse site contents - Req will be used to handle redundant stuff like retrying etc. * Add shuttle SVG to generic components Later on we'll use it to indicate verification errors * Connect live socket & allow skipping awaiting the first pageview * Connect live socket in general settings * Implement verification checks & diagnostics * Stub remote services with Req for testing * Change snippet screen copy * Update tracker script, so that: 1. headless browsers aren't ignored if `window.__plausible` is defined 2. callback optionally supplies the event response HTTP status This will be later used to check whether the server acknowledged the verification event. * Implement LiveView verification UI * Embed the verification UIs into settings and onboarding * Implement browserless puppeteer verification script It: - tries to visit the site - defines window.__plausible, so the tracker doesn't ignore test events - sends a verification event and instruments the callback - awaits the callback to fire and returns the result * Improve diagnostics for CSP Only report CSP error if the snippet is already found * Put verification behind a feature flag/env setting * Contact Us hint only for Enterprise Edition * For headless code, use JS context instead of EEx interpolation * Update diagnostics test with WordPress scenarios * Shorten exception/throw interception * Rename test * Tidy up * Bust URL always on headless check * Update moduledoc * Detect official Plausible WordPress Plugin and act accordingly on diagnostics interoperation * Stop using 'rating' in favour of 'interpretation' * Only report CSP error if no proxy is likely * Update CHANGELOG * Allow event-* attributes on snippet elements * Improve naive GTM detection, not to confuse it with GA4 * Update lib/plausible/verification.ex Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> * Update test/plausible/site/verification/checks_test.exs Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> * s/perform_wrapped/perform_safe * Update lib/plausible/verification/checks/installation.ex Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com> * Remove garbage --------- Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
This commit is contained in:
@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
- Snippet integration verification
|
||||
|
||||
### Removed
|
||||
|
||||
### Changed
|
||||
|
3
Makefile
3
Makefile
@ -37,6 +37,9 @@ postgres-prod: ## Start a container with the same version of postgres as the one
|
||||
postgres-stop: ## Stop and remove the postgres container
|
||||
docker stop plausible_db && docker rm plausible_db
|
||||
|
||||
browserless:
|
||||
docker run -e "TOKEN=dummy_token" -p 3000:3000 --network host ghcr.io/browserless/chromium
|
||||
|
||||
minio: ## Start a transient container with a recent version of minio (s3)
|
||||
docker run -d --rm -p 10000:10000 -p 10001:10001 --name plausible_minio minio/minio server /data --address ":10000" --console-address ":10001"
|
||||
while ! docker exec plausible_minio mc alias set local http://localhost:10000 minioadmin minioadmin; do sleep 1; done
|
||||
|
@ -5,7 +5,9 @@ module.exports = {
|
||||
content: [
|
||||
"./js/**/*.js",
|
||||
"../lib/*_web.ex",
|
||||
"../lib/*_web/**/*.*ex"
|
||||
"../lib/*_web/**/*.*ex",
|
||||
"../extra/*_web.ex",
|
||||
"../extra/*_web/**/*.*ex"
|
||||
],
|
||||
safelist: [
|
||||
// PlausibleWeb.StatsView.stats_container_class/1 uses this class
|
||||
|
@ -28,3 +28,5 @@ S3_REGION=us-east-1
|
||||
S3_ENDPOINT=http://localhost:10000
|
||||
S3_EXPORTS_BUCKET=dev-exports
|
||||
S3_IMPORTS_BUCKET=dev-imports
|
||||
|
||||
VERIFICATION_ENABLED=true
|
||||
|
@ -701,6 +701,15 @@ config :plausible, Plausible.PromEx,
|
||||
grafana: :disabled,
|
||||
metrics_server: :disabled
|
||||
|
||||
config :plausible, Plausible.Verification,
|
||||
enabled?:
|
||||
get_var_from_path_or_env(config_dir, "VERIFICATION_ENABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
config :plausible, Plausible.Verification.Checks.Installation,
|
||||
token: get_var_from_path_or_env(config_dir, "BROWSERLESS_TOKEN", "dummy_token"),
|
||||
endpoint: get_var_from_path_or_env(config_dir, "BROWSERLESS_ENDPOINT", "http://0.0.0.0:3000")
|
||||
|
||||
if not is_selfhost do
|
||||
site_default_ingest_threshold =
|
||||
case System.get_env("SITE_DEFAULT_INGEST_THRESHOLD") do
|
||||
|
@ -31,3 +31,13 @@ config :ex_money, api_module: Plausible.ExchangeRateMock
|
||||
config :plausible, Plausible.Ingestion.Counters, enabled: false
|
||||
|
||||
config :plausible, Oban, testing: :manual
|
||||
|
||||
config :plausible, Plausible.Verification.Checks.FetchBody,
|
||||
req_opts: [
|
||||
plug: {Req.Test, Plausible.Verification.Checks.FetchBody}
|
||||
]
|
||||
|
||||
config :plausible, Plausible.Verification.Checks.Installation,
|
||||
req_opts: [
|
||||
plug: {Req.Test, Plausible.Verification.Checks.Installation}
|
||||
]
|
||||
|
@ -22,7 +22,8 @@ defmodule Plausible.Ingestion.Event.Revenue do
|
||||
}
|
||||
|
||||
matching_goal.currency != revenue_source.currency ->
|
||||
converted = Money.to_currency!(revenue_source, matching_goal.currency)
|
||||
converted =
|
||||
Money.to_currency!(revenue_source, matching_goal.currency)
|
||||
|
||||
%{
|
||||
revenue_source_amount: Money.to_decimal(revenue_source),
|
||||
|
@ -21,6 +21,8 @@ defmodule Plausible.Ingestion.Event do
|
||||
salts: nil,
|
||||
changeset: nil
|
||||
|
||||
@verification_user_agent Plausible.Verification.user_agent()
|
||||
|
||||
@type drop_reason() ::
|
||||
:bot
|
||||
| :spam_referrer
|
||||
@ -31,6 +33,7 @@ defmodule Plausible.Ingestion.Event do
|
||||
| :site_country_blocklist
|
||||
| :site_page_blocklist
|
||||
| :site_hostname_allowlist
|
||||
| :verification_agent
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
domain: String.t() | nil,
|
||||
@ -104,6 +107,7 @@ defmodule Plausible.Ingestion.Event do
|
||||
|
||||
defp pipeline() do
|
||||
[
|
||||
drop_verification_agent: &drop_verification_agent/1,
|
||||
drop_datacenter_ip: &drop_datacenter_ip/1,
|
||||
drop_shield_rule_hostname: &drop_shield_rule_hostname/1,
|
||||
drop_shield_rule_page: &drop_shield_rule_page/1,
|
||||
@ -167,6 +171,16 @@ defmodule Plausible.Ingestion.Event do
|
||||
struct!(event, clickhouse_session_attrs: Map.merge(event.clickhouse_session_attrs, attrs))
|
||||
end
|
||||
|
||||
defp drop_verification_agent(%__MODULE__{} = event) do
|
||||
case event.request.user_agent do
|
||||
@verification_user_agent ->
|
||||
drop(event, :verification_agent)
|
||||
|
||||
_ ->
|
||||
event
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_datacenter_ip(%__MODULE__{} = event) do
|
||||
case event.request.ip_classification do
|
||||
"dc_ip" ->
|
||||
|
26
lib/plausible/verification.ex
Normal file
26
lib/plausible/verification.ex
Normal file
@ -0,0 +1,26 @@
|
||||
defmodule Plausible.Verification do
|
||||
@moduledoc """
|
||||
Module defining the user-agent used for site verification.
|
||||
"""
|
||||
use Plausible
|
||||
|
||||
@feature_flag :verification
|
||||
|
||||
def enabled?(user) do
|
||||
enabled_via_config? =
|
||||
:plausible |> Application.fetch_env!(__MODULE__) |> Keyword.fetch!(:enabled?)
|
||||
|
||||
enabled_for_user? = not is_nil(user) and FunWithFlags.enabled?(@feature_flag, for: user)
|
||||
enabled_via_config? or enabled_for_user?
|
||||
end
|
||||
|
||||
on_ee do
|
||||
def user_agent() do
|
||||
"Plausible Verification Agent - if abused, contact support@plausible.io"
|
||||
end
|
||||
else
|
||||
def user_agent() do
|
||||
"Plausible Community Edition"
|
||||
end
|
||||
end
|
||||
end
|
37
lib/plausible/verification/check.ex
Normal file
37
lib/plausible/verification/check.ex
Normal file
@ -0,0 +1,37 @@
|
||||
defmodule Plausible.Verification.Check do
|
||||
@moduledoc """
|
||||
Behaviour to be implemented by specific site verification checks.
|
||||
`friendly_name()` doesn't necessarily reflect the actual check description,
|
||||
it serves as a user-facing message grouping mechanism, to prevent frequent message flashing when checks rotate often.
|
||||
Each check operates on `state()` and is expected to return it, optionally modified, by all means.
|
||||
`perform_safe/1` is used to guarantee no exceptions are thrown by faulty implementations, not to interrupt LiveView.
|
||||
"""
|
||||
@type state() :: Plausible.Verification.State.t()
|
||||
@callback friendly_name() :: String.t()
|
||||
@callback perform(state()) :: state()
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
import Plausible.Verification.State
|
||||
|
||||
alias Plausible.Verification.Checks
|
||||
alias Plausible.Verification.State
|
||||
alias Plausible.Verification.Diagnostics
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Plausible.Verification.Check
|
||||
|
||||
def perform_safe(state) do
|
||||
perform(state)
|
||||
catch
|
||||
_, e ->
|
||||
Logger.error(
|
||||
"Error running check #{inspect(__MODULE__)} on #{state.url}: #{inspect(e)}"
|
||||
)
|
||||
|
||||
put_diagnostics(state, service_error: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
75
lib/plausible/verification/checks.ex
Normal file
75
lib/plausible/verification/checks.ex
Normal file
@ -0,0 +1,75 @@
|
||||
defmodule Plausible.Verification.Checks do
|
||||
@moduledoc """
|
||||
Checks that are performed during site verification.
|
||||
Each module defined in `@checks` implements the `Plausible.Verification.Check` behaviour.
|
||||
Checks are normally run asynchronously, except when synchronous execution is optionally required
|
||||
for tests. Slowdowns can be optionally added, the user doesn't benefit from running the checks too quickly.
|
||||
|
||||
In async execution, each check notifies the caller by sending a message to it.
|
||||
"""
|
||||
alias Plausible.Verification.Checks
|
||||
alias Plausible.Verification.State
|
||||
|
||||
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)
|
||||
|
||||
if async? do
|
||||
Task.start_link(fn -> do_run(url, data_domain, checks, report_to, slowdown) end)
|
||||
else
|
||||
do_run(url, data_domain, checks, report_to, slowdown)
|
||||
end
|
||||
end
|
||||
|
||||
def interpret_diagnostics(%State{} = state) do
|
||||
Plausible.Verification.Diagnostics.interpret(state.diagnostics, state.url)
|
||||
end
|
||||
|
||||
defp do_run(url, data_domain, checks, report_to, slowdown) do
|
||||
init_state = %State{url: url, data_domain: data_domain, report_to: report_to}
|
||||
|
||||
state =
|
||||
Enum.reduce(
|
||||
checks,
|
||||
init_state,
|
||||
fn check, state ->
|
||||
state
|
||||
|> notify_start(check, slowdown)
|
||||
|> check.perform_safe()
|
||||
end
|
||||
)
|
||||
|
||||
notify_verification_end(state, slowdown)
|
||||
end
|
||||
|
||||
defp notify_start(state, check, slowdown) do
|
||||
if is_pid(state.report_to) do
|
||||
if is_integer(slowdown) and slowdown > 0, do: :timer.sleep(slowdown)
|
||||
send(state.report_to, {:verification_check_start, {check, state}})
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
defp notify_verification_end(state, slowdown) do
|
||||
if is_pid(state.report_to) do
|
||||
if is_integer(slowdown) and slowdown > 0, do: :timer.sleep(slowdown)
|
||||
send(state.report_to, {:verification_end, state})
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
end
|
34
lib/plausible/verification/checks/csp.ex
Normal file
34
lib/plausible/verification/checks/csp.ex
Normal file
@ -0,0 +1,34 @@
|
||||
defmodule Plausible.Verification.Checks.CSP do
|
||||
@moduledoc """
|
||||
Scans the Content Security Policy header to ensure that the Plausible domain is allowed.
|
||||
See `Plausible.Verification.Checks` for the execution sequence.
|
||||
"""
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
@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
|
64
lib/plausible/verification/checks/fetch_body.ex
Normal file
64
lib/plausible/verification/checks/fetch_body.ex
Normal file
@ -0,0 +1,64 @@
|
||||
defmodule Plausible.Verification.Checks.FetchBody do
|
||||
@moduledoc """
|
||||
Fetches the body of the site and extracts the HTML document, if available, for
|
||||
further processing.
|
||||
See `Plausible.Verification.Checks` for the execution sequence.
|
||||
"""
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
@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: 2,
|
||||
connect_options: [timeout: 4_000],
|
||||
receive_timeout: 4_000,
|
||||
max_retries: 3,
|
||||
retry_log_level: :warning
|
||||
],
|
||||
fetch_body_opts
|
||||
)
|
||||
|
||||
req = Req.new(opts)
|
||||
|
||||
case Req.get(req) do
|
||||
{:ok, %Req.Response{status: status, body: body} = response}
|
||||
when is_binary(body) and status in 200..299 ->
|
||||
extract_document(state, response)
|
||||
|
||||
_ ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_document(state, response) when byte_size(response.body) <= 500_000 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 extract_document(state, response) when byte_size(response.body) > 500_000 do
|
||||
state
|
||||
end
|
||||
|
||||
defp html?(%Req.Response{headers: headers}) do
|
||||
headers
|
||||
|> Map.get("content-type", "")
|
||||
|> List.wrap()
|
||||
|> List.first()
|
||||
|> String.contains?("text/html")
|
||||
end
|
||||
end
|
69
lib/plausible/verification/checks/installation.ex
Normal file
69
lib/plausible/verification/checks/installation.ex
Normal file
@ -0,0 +1,69 @@
|
||||
defmodule Plausible.Verification.Checks.Installation do
|
||||
@verification_script_filename "verification/verify_plausible_installed.js"
|
||||
@verification_script_path Path.join(:code.priv_dir(:plausible), @verification_script_filename)
|
||||
@external_resource @verification_script_path
|
||||
@code File.read!(@verification_script_path)
|
||||
|
||||
@moduledoc """
|
||||
Calls the browserless.io service (local instance can be spawned with `make browserless`)
|
||||
and runs #{@verification_script_filename} via the [function API](https://docs.browserless.io/HTTP-APIs/function).
|
||||
|
||||
The successful execution assumes the following JSON payload:
|
||||
- `data.plausibleInstalled` - boolean indicating whether 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.Verification.user_agent/0`
|
||||
"""
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "We're verifying that your visitors are being counted correctly"
|
||||
|
||||
@impl true
|
||||
def perform(%State{url: url} = state) do
|
||||
opts = [
|
||||
headers: %{content_type: "application/json"},
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
code: @code,
|
||||
context: %{
|
||||
url: Plausible.Verification.URL.bust_url(url),
|
||||
userAgent: Plausible.Verification.user_agent(),
|
||||
debug: Application.get_env(:plausible, :environment) == "dev"
|
||||
}
|
||||
}),
|
||||
retry: :transient,
|
||||
retry_log_level: :warning,
|
||||
max_retries: 2,
|
||||
receive_timeout: 6_000
|
||||
]
|
||||
|
||||
extra_opts = Application.get_env(:plausible, __MODULE__)[:req_opts] || []
|
||||
opts = Keyword.merge(opts, extra_opts)
|
||||
|
||||
case Req.post(verification_endpoint(), opts) do
|
||||
{:ok,
|
||||
%{
|
||||
status: 200,
|
||||
body: %{
|
||||
"data" => %{"plausibleInstalled" => installed?, "callbackStatus" => callback_status}
|
||||
}
|
||||
}}
|
||||
when is_boolean(installed?) ->
|
||||
put_diagnostics(state, plausible_installed?: installed?, callback_status: callback_status)
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
put_diagnostics(state, plausible_installed?: false, service_error: status)
|
||||
|
||||
{:error, %{reason: reason}} ->
|
||||
put_diagnostics(state, plausible_installed?: false, service_error: reason)
|
||||
end
|
||||
end
|
||||
|
||||
defp verification_endpoint() do
|
||||
config = Application.fetch_env!(:plausible, __MODULE__)
|
||||
token = Keyword.fetch!(config, :token)
|
||||
endpoint = Keyword.fetch!(config, :endpoint)
|
||||
Path.join(endpoint, "function?token=#{token}")
|
||||
end
|
||||
end
|
65
lib/plausible/verification/checks/scan_body.ex
Normal file
65
lib/plausible/verification/checks/scan_body.ex
Normal file
@ -0,0 +1,65 @@
|
||||
defmodule Plausible.Verification.Checks.ScanBody do
|
||||
@moduledoc """
|
||||
Naive way of detecting GTM and WordPress powered sites.
|
||||
"""
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
@impl true
|
||||
def perform(%State{assigns: %{raw_body: body}} = state) when is_binary(body) do
|
||||
state
|
||||
|> scan_wp_plugin()
|
||||
|> scan_gtm()
|
||||
|> scan_wp()
|
||||
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
|
||||
end
|
52
lib/plausible/verification/checks/snippet.ex
Normal file
52
lib/plausible/verification/checks/snippet.ex
Normal file
@ -0,0 +1,52 @@
|
||||
defmodule Plausible.Verification.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.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, 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]")
|
||||
in_body = Floki.find(document, "body script[data-domain]")
|
||||
|
||||
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),
|
||||
snippet_unknown_attributes?: unknown_attributes?(all),
|
||||
data_domain_mismatch?: data_domain_mismatch?(all, state.data_domain)
|
||||
)
|
||||
end
|
||||
|
||||
def perform(state), do: state
|
||||
|
||||
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"]
|
||||
@known_prefix "event-"
|
||||
|
||||
defp unknown_attributes?(nodes) do
|
||||
Enum.any?(nodes, fn {_, attrs, _} ->
|
||||
Enum.any?(attrs, fn {key, _} ->
|
||||
key not in @known_attributes and not String.starts_with?(key, @known_prefix)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp data_domain_mismatch?(nodes, data_domain) do
|
||||
nodes
|
||||
|> Floki.attribute("data-domain")
|
||||
|> Enum.any?(&(&1 != data_domain and data_domain not in String.split(&1, ",")))
|
||||
end
|
||||
end
|
40
lib/plausible/verification/checks/snippet_cache_bust.ex
Normal file
40
lib/plausible/verification/checks/snippet_cache_bust.ex
Normal file
@ -0,0 +1,40 @@
|
||||
defmodule Plausible.Verification.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 `Plausible.Verification.Checks.FetchBody` and `Plausible.Verification.Checks.Snippet` checks.
|
||||
If the result is different this time, we'll assume cache likely.
|
||||
"""
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "We're looking for the Plausible snippet on your site"
|
||||
|
||||
@impl true
|
||||
def perform(
|
||||
%State{
|
||||
url: url,
|
||||
diagnostics: %Diagnostics{
|
||||
snippets_found_in_head: 0,
|
||||
snippets_found_in_body: 0,
|
||||
body_fetched?: true
|
||||
}
|
||||
} = state
|
||||
) do
|
||||
state2 =
|
||||
%{state | url: Plausible.Verification.URL.bust_url(url)}
|
||||
|> Plausible.Verification.Checks.FetchBody.perform()
|
||||
|> Plausible.Verification.Checks.ScanBody.perform()
|
||||
|> Plausible.Verification.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
|
378
lib/plausible/verification/diagnostics.ex
Normal file
378
lib/plausible/verification/diagnostics.ex
Normal file
@ -0,0 +1,378 @@
|
||||
defmodule Plausible.Verification.Diagnostics do
|
||||
@moduledoc """
|
||||
Module responsible for translating diagnostics to user-friendly messages and recommendations.
|
||||
"""
|
||||
require Logger
|
||||
|
||||
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,
|
||||
gtm_likely?: false,
|
||||
callback_status: -1,
|
||||
proxy_likely?: false,
|
||||
data_domain_mismatch?: false,
|
||||
wordpress_plugin?: false
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
defmodule Result do
|
||||
@moduledoc """
|
||||
Diagnostics interpretation result.
|
||||
"""
|
||||
defstruct ok?: false, errors: [], recommendations: []
|
||||
@type t :: %__MODULE__{}
|
||||
end
|
||||
|
||||
@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: 202,
|
||||
snippet_found_after_busting_cache?: false,
|
||||
service_error: nil,
|
||||
data_domain_mismatch?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{ok?: true}
|
||||
end
|
||||
|
||||
def interpret(%__MODULE__{plausible_installed?: false, gtm_likely?: true}, _url) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an issue with your Plausible integration"],
|
||||
recommendations: [
|
||||
{"As you're using Google Tag Manager, you'll need to use a GTM-specific Plausible snippet",
|
||||
"https://plausible.io/docs/google-tag-manager"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
disallowed_via_csp?: true,
|
||||
proxy_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an issue with your site's CSP"],
|
||||
recommendations: [
|
||||
{"Please add plausible.io domain specifically to the allowed list of domains in your Content Security Policy (CSP)",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
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
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We couldn't find the Plausible snippet on your site"],
|
||||
recommendations: [
|
||||
{"Please insert the snippet into your site", "https://plausible.io/docs/plausible-script"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
body_fetched?: false
|
||||
},
|
||||
url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We couldn't reach #{url}. Is your site up?"],
|
||||
recommendations: [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
service_error: service_error
|
||||
},
|
||||
_url
|
||||
)
|
||||
when not is_nil(service_error) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered a temporary problem verifying your website"],
|
||||
recommendations: [
|
||||
{"Please try again in a few minutes or manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
service_error: nil,
|
||||
body_fetched?: false
|
||||
},
|
||||
url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We couldn't reach #{url}. Is your site up?"],
|
||||
recommendations: [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: callback_status,
|
||||
proxy_likely?: true
|
||||
},
|
||||
_url
|
||||
)
|
||||
when callback_status != 202 do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an error with your Plausible proxy"],
|
||||
recommendations: [
|
||||
{"Please check whether you've configured the /event route correctly",
|
||||
"https://plausible.io/docs/proxy/introduction"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
proxy_likely?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an error with your Plausible proxy"],
|
||||
recommendations: [
|
||||
{"Please re-enable the proxy in our WordPress plugin to start counting your visitors",
|
||||
"https://plausible.io/wordpress-analytics-plugin"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippets_found_in_head: 1,
|
||||
proxy_likely?: true,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an error with your Plausible proxy"],
|
||||
recommendations: [
|
||||
{"Please check your proxy configuration to make sure it's set up correctly",
|
||||
"https://plausible.io/docs/proxy/introduction"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{snippets_found_in_head: count_head, snippets_found_in_body: count_body},
|
||||
_url
|
||||
)
|
||||
when count_head + count_body > 1 do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We've found multiple Plausible snippets on your site."],
|
||||
recommendations: [
|
||||
{"Please ensure that only one snippet is used",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: 202,
|
||||
snippet_found_after_busting_cache?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: true
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an issue with your site cache"],
|
||||
recommendations: [
|
||||
{"Please clear your WordPress cache to ensure that the latest version of your site is being displayed to all your visitors",
|
||||
"https://plausible.io/wordpress-analytics-plugin"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: 202,
|
||||
snippet_found_after_busting_cache?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an issue with your site cache"],
|
||||
recommendations: [
|
||||
{"Please install and activate our WordPress plugin to start counting your visitors",
|
||||
"https://plausible.io/wordpress-analytics-plugin"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: true,
|
||||
callback_status: 202,
|
||||
snippet_found_after_busting_cache?: true,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["We encountered an issue with your site cache"],
|
||||
recommendations: [
|
||||
{"Please clear your cache (or wait for your provider to clear it) to ensure that the latest version of your site is being displayed to all your visitors",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(%__MODULE__{snippets_found_in_head: 0, snippets_found_in_body: n}, _url)
|
||||
when n >= 1 do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["Plausible snippet is placed in the body of your site"],
|
||||
recommendations: [
|
||||
{"Please relocate the snippet to the header of your site",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(%__MODULE__{data_domain_mismatch?: true}, url) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["Your data-domain is different than #{url}"],
|
||||
recommendations: [
|
||||
{"Please ensure that the site in the data-domain attribute is an exact match to the site as you added it to your Plausible account",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: true
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["A performance optimization plugin seems to have altered our snippet"],
|
||||
recommendations: [
|
||||
{"Please whitelist our script in your performance optimization plugin to stop it from changing our snippet",
|
||||
"https://plausible.io/wordpress-analytics-plugin "}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
wordpress_likely?: true,
|
||||
wordpress_plugin?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["A performance optimization plugin seems to have altered our snippet"],
|
||||
recommendations: [
|
||||
{"Please install and activate our WordPress plugin to avoid the most common plugin conflicts",
|
||||
"https://plausible.io/wordpress-analytics-plugin "}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(
|
||||
%__MODULE__{
|
||||
plausible_installed?: false,
|
||||
snippet_unknown_attributes?: true,
|
||||
wordpress_likely?: false
|
||||
},
|
||||
_url
|
||||
) do
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["Something seems to have altered our snippet"],
|
||||
recommendations: [
|
||||
{"Please manually check your integration to make sure that nothing prevents our script from working",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def interpret(rating, url) do
|
||||
Sentry.capture_message("Unhandled case for site verification: #{url}",
|
||||
extra: %{
|
||||
message: inspect(rating)
|
||||
}
|
||||
)
|
||||
|
||||
%Result{
|
||||
ok?: false,
|
||||
errors: ["Your Plausible integration is not working"],
|
||||
recommendations: [
|
||||
{"Please manually check your integration to make sure that the Plausible snippet has been inserted correctly",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
27
lib/plausible/verification/state.ex
Normal file
27
lib/plausible/verification/state.ex
Normal file
@ -0,0 +1,27 @@
|
||||
defmodule Plausible.Verification.State do
|
||||
@moduledoc """
|
||||
The struct and interface describing the state of the site verification process.
|
||||
Assigns are meant to be used to communicate between checks, while diagnostics
|
||||
are later on interpreted (translated into user-friendly messages and recommendations)
|
||||
via `Plausible.Verification.Diagnostics` module.
|
||||
"""
|
||||
defstruct url: nil,
|
||||
data_domain: nil,
|
||||
report_to: nil,
|
||||
assigns: %{},
|
||||
diagnostics: %Plausible.Verification.Diagnostics{}
|
||||
|
||||
@type t() :: %__MODULE__{}
|
||||
|
||||
def assign(%__MODULE__{} = state, assigns) do
|
||||
%{state | assigns: Map.merge(state.assigns, Enum.into(assigns, %{}))}
|
||||
end
|
||||
|
||||
def put_diagnostics(%__MODULE__{} = state, diagnostics) when is_list(diagnostics) do
|
||||
%{state | diagnostics: struct!(state.diagnostics, diagnostics)}
|
||||
end
|
||||
|
||||
def put_diagnostics(%__MODULE__{} = state, diagnostics) do
|
||||
put_diagnostics(state, List.wrap(diagnostics))
|
||||
end
|
||||
end
|
25
lib/plausible/verification/url.ex
Normal file
25
lib/plausible/verification/url.ex
Normal file
@ -0,0 +1,25 @@
|
||||
defmodule Plausible.Verification.URL do
|
||||
@moduledoc """
|
||||
Busting some caches by appending ?plausible_verification=12345 to it.
|
||||
"""
|
||||
|
||||
def bust_url(url) do
|
||||
cache_invalidator = abs(:erlang.unique_integer())
|
||||
update_url(url, cache_invalidator)
|
||||
end
|
||||
|
||||
defp update_url(url, invalidator) do
|
||||
url
|
||||
|> URI.parse()
|
||||
|> then(fn uri ->
|
||||
updated_query =
|
||||
(uri.query || "")
|
||||
|> URI.decode_query()
|
||||
|> Map.put("plausible_verification", invalidator)
|
||||
|> URI.encode_query()
|
||||
|
||||
struct!(uri, query: updated_query)
|
||||
end)
|
||||
|> to_string()
|
||||
end
|
||||
end
|
File diff suppressed because one or more lines are too long
@ -129,6 +129,7 @@ defmodule PlausibleWeb.SiteController do
|
||||
|> render("settings_general.html",
|
||||
site: site,
|
||||
changeset: Plausible.Site.changeset(site, %{}),
|
||||
connect_live_socket: true,
|
||||
dogfood_page_path: "/:dashboard/settings/general",
|
||||
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
|
||||
)
|
||||
|
@ -56,9 +56,10 @@ defmodule PlausibleWeb.StatsController do
|
||||
can_see_stats? = not Sites.locked?(site) or conn.assigns[:current_user_role] == :super_admin
|
||||
demo = site.domain == PlausibleWeb.Endpoint.host()
|
||||
dogfood_page_path = if !demo, do: "/:dashboard"
|
||||
skip_to_dashboard? = conn.params["skip_to_dashboard"] == "true"
|
||||
|
||||
cond do
|
||||
stats_start_date && can_see_stats? ->
|
||||
(stats_start_date && can_see_stats?) || (can_see_stats? && skip_to_dashboard?) ->
|
||||
conn
|
||||
|> put_resp_header("x-robots-tag", "noindex, nofollow")
|
||||
|> render("stats.html",
|
||||
@ -80,7 +81,8 @@ defmodule PlausibleWeb.StatsController do
|
||||
!stats_start_date && can_see_stats? ->
|
||||
render(conn, "waiting_first_pageview.html",
|
||||
site: site,
|
||||
dogfood_page_path: dogfood_page_path
|
||||
dogfood_page_path: dogfood_page_path,
|
||||
connect_live_socket: true
|
||||
)
|
||||
|
||||
Sites.locked?(site) ->
|
||||
|
145
lib/plausible_web/live/components/verification.ex
Normal file
145
lib/plausible_web/live/components/verification.ex
Normal file
@ -0,0 +1,145 @@
|
||||
defmodule PlausibleWeb.Live.Components.Verification do
|
||||
@moduledoc """
|
||||
This component is responsible for rendering the verification progress
|
||||
and diagnostics.
|
||||
"""
|
||||
use Phoenix.LiveComponent
|
||||
use Plausible
|
||||
|
||||
import PlausibleWeb.Components.Generic
|
||||
|
||||
attr :domain, :string, required: true
|
||||
attr :modal?, :boolean, default: false
|
||||
|
||||
attr :message, :string,
|
||||
default: "We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
attr :finished?, :boolean, default: false
|
||||
attr :success?, :boolean, default: false
|
||||
attr :interpretation, Plausible.Verification.Diagnostics.Result, default: nil
|
||||
attr :attempts, :integer, default: 0
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class={[
|
||||
"bg-white dark:bg-gray-800 text-center h-96 flex flex-col",
|
||||
if(!@modal?, do: "shadow-md rounded px-8 pt-6 pb-4 mb-4 mt-16")
|
||||
]}>
|
||||
<h2 class="text-xl font-bold dark:text-gray-100">
|
||||
<%= if @success? && @finished? do %>
|
||||
Success!
|
||||
<% else %>
|
||||
Verifying your integration
|
||||
<% end %>
|
||||
</h2>
|
||||
<h2 class="text-xl dark:text-gray-100 text-xs">
|
||||
<%= if @finished? && @success? do %>
|
||||
Your integration is working and visitors are being counted accurately
|
||||
<% else %>
|
||||
on <%= @domain %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<div
|
||||
:if={!@finished? || @success?}
|
||||
class="flex justify-center w-full my-auto"
|
||||
id="progress-indicator"
|
||||
>
|
||||
<div class={["block pulsating-circle", if(@modal? && @finished?, do: "hidden")]}></div>
|
||||
<Heroicons.check_circle
|
||||
:if={@modal? && @finished? && @success?}
|
||||
id="check-circle"
|
||||
solid
|
||||
class="w-24 h-24 text-green-500 pt-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@finished? && !@success?}
|
||||
class="flex justify-center pt-3 h-14 mb-4 dark:text-gray-400 "
|
||||
id="progress-indicator"
|
||||
>
|
||||
<.shuttle width={50} height={50} />
|
||||
</div>
|
||||
<div
|
||||
id="progress"
|
||||
class={[
|
||||
"mt-2 dark:text-gray-400",
|
||||
if(!@finished?, do: "animate-pulse text-xs", else: "font-bold text-sm"),
|
||||
if(@finished? && !@success?, do: "text-red-500 dark:text-red-600")
|
||||
]}
|
||||
>
|
||||
<p id="progress-message" class="leading-normal">
|
||||
<span :if={!@finished?}><%= @message %></span>
|
||||
<span :if={@finished? && !@success? && @interpretation && @interpretation.errors}>
|
||||
<%= List.first(@interpretation.errors) %>
|
||||
<div class="text-xs dark:text-gray-400 font-normal mt-1" id="recommendations">
|
||||
<.recommendations interpretation={@interpretation} />
|
||||
</div>
|
||||
</span>
|
||||
<p
|
||||
:if={@finished? && @success? && !@modal?}
|
||||
class="leading-normal animate-pulse text-xs font-normal"
|
||||
>
|
||||
Awaiting your first pageview.
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pb-2 text-gray-600 dark:text-gray-400 text-xs w-full text-center leading-normal">
|
||||
<div :if={@finished?} class="mb-4">
|
||||
<div class="flex justify-center gap-x-4 mt-4">
|
||||
<.button_link :if={!@success?} href="#" phx-click="retry" class="text-xs font-bold">
|
||||
Verify integration again
|
||||
</.button_link>
|
||||
<.button_link
|
||||
:if={@success?}
|
||||
href={"/#{URI.encode_www_form(@domain)}?skip_to_dashboard=true"}
|
||||
class="text-xs font-bold"
|
||||
>
|
||||
Go to the dashboard
|
||||
</.button_link>
|
||||
</div>
|
||||
</div>
|
||||
<%= if ee?() && @finished? && !@success? && @attempts >= 3 do %>
|
||||
Need further help with your integration? Do
|
||||
<.styled_link href="https://plausible.io/contact">
|
||||
contact us
|
||||
</.styled_link>
|
||||
<br />
|
||||
<% end %>
|
||||
<%= if !@modal? && !@success? do %>
|
||||
Need to see the snippet again?
|
||||
<.styled_link href={"/#{URI.encode_www_form(@domain)}/snippet"}>
|
||||
Click here
|
||||
</.styled_link>
|
||||
<br /> Run verification later and go to Site Settings?
|
||||
<.styled_link href={"/#{URI.encode_www_form(@domain)}/settings/general"}>
|
||||
Click here
|
||||
</.styled_link>
|
||||
<br />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def recommendations(assigns) do
|
||||
~H"""
|
||||
<p class="leading-normal">
|
||||
<span :for={recommendation <- @interpretation.recommendations} class="recommendation">
|
||||
<span :if={is_binary(recommendation)}><%= recommendation %></span>
|
||||
<span :if={is_tuple(recommendation)}><%= elem(recommendation, 0) %> -</span>
|
||||
<.styled_link
|
||||
:if={is_tuple(recommendation)}
|
||||
href={elem(recommendation, 1)}
|
||||
new_tab={true}
|
||||
class="text-xs"
|
||||
>
|
||||
Learn more
|
||||
</.styled_link>
|
||||
<br />
|
||||
</span>
|
||||
</p>
|
||||
"""
|
||||
end
|
||||
end
|
156
lib/plausible_web/live/verification.ex
Normal file
156
lib/plausible_web/live/verification.ex
Normal file
@ -0,0 +1,156 @@
|
||||
defmodule PlausibleWeb.Live.Verification do
|
||||
@moduledoc """
|
||||
LiveView coordinating the site verification process.
|
||||
Onboarding new sites, renders a standalone component.
|
||||
Embedded modal variant is available for general site settings.
|
||||
"""
|
||||
use PlausibleWeb, :live_view
|
||||
use Phoenix.HTML
|
||||
|
||||
alias Plausible.Verification.{Checks, State}
|
||||
alias PlausibleWeb.Live.Components.Modal
|
||||
|
||||
@component PlausibleWeb.Live.Components.Verification
|
||||
@slowdown_for_frequent_checking :timer.seconds(5)
|
||||
|
||||
def mount(
|
||||
:not_mounted_at_router,
|
||||
%{"domain" => domain} = session,
|
||||
socket
|
||||
) do
|
||||
socket =
|
||||
assign(socket,
|
||||
domain: domain,
|
||||
modal?: !!session["modal?"],
|
||||
component: @component,
|
||||
report_to: session["report_to"] || self(),
|
||||
delay: session["slowdown"] || 500,
|
||||
slowdown: session["slowdown"] || 500,
|
||||
checks_pid: nil,
|
||||
attempts: 0
|
||||
)
|
||||
|
||||
if connected?(socket) and !session["modal?"] do
|
||||
launch_delayed(socket)
|
||||
end
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div :if={@modal?} phx-click-away="reset">
|
||||
<.live_component module={Modal} id="verification-modal">
|
||||
<.live_component
|
||||
module={@component}
|
||||
domain={@domain}
|
||||
id="verification-within-modal"
|
||||
modal?={@modal?}
|
||||
attempts={@attempts}
|
||||
/>
|
||||
</.live_component>
|
||||
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
id="launch-verification-button"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("verification-modal")}
|
||||
phx-click="launch-verification"
|
||||
class="mt-6"
|
||||
>
|
||||
Verify your integration
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
|
||||
<.live_component
|
||||
:if={!@modal?}
|
||||
module={@component}
|
||||
domain={@domain}
|
||||
id="verification-standalone"
|
||||
attempts={@attempts}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("launch-verification", _, socket) do
|
||||
launch_delayed(socket)
|
||||
{:noreply, reset_component(socket)}
|
||||
end
|
||||
|
||||
def handle_event("retry", _, socket) do
|
||||
launch_delayed(socket)
|
||||
{:noreply, reset_component(socket)}
|
||||
end
|
||||
|
||||
def handle_info({:start, report_to}, socket) do
|
||||
if is_pid(socket.assigns.checks_pid) and Process.alive?(socket.assigns.checks_pid) do
|
||||
{:noreply, socket}
|
||||
else
|
||||
case Plausible.RateLimit.check_rate(
|
||||
"site_verification_#{socket.assigns.domain}",
|
||||
:timer.minutes(60),
|
||||
3
|
||||
) do
|
||||
{:allow, _} -> :ok
|
||||
{:deny, _} -> :timer.sleep(@slowdown_for_frequent_checking)
|
||||
end
|
||||
|
||||
{:ok, pid} =
|
||||
Checks.run(
|
||||
"https://#{socket.assigns.domain}",
|
||||
socket.assigns.domain,
|
||||
report_to: report_to,
|
||||
slowdown: socket.assigns.slowdown
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, checks_pid: pid, attempts: socket.assigns.attempts + 1)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:verification_check_start, {check, _state}}, socket) do
|
||||
update_component(socket,
|
||||
message: check.friendly_name()
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_info({:verification_end, %State{} = state}, socket) do
|
||||
interpretation = Checks.interpret_diagnostics(state)
|
||||
|
||||
update_component(socket,
|
||||
finished?: true,
|
||||
success?: interpretation.ok?,
|
||||
interpretation: interpretation
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, checks_pid: nil)}
|
||||
end
|
||||
|
||||
defp reset_component(socket) do
|
||||
update_component(socket,
|
||||
message: "We're visiting your site to ensure that everything is working correctly",
|
||||
finished?: false,
|
||||
success?: false,
|
||||
diagnostics: nil
|
||||
)
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
defp update_component(socket, updates) do
|
||||
send_update(
|
||||
@component,
|
||||
Keyword.merge(updates,
|
||||
id:
|
||||
if(socket.assigns.modal?,
|
||||
do: "verification-within-modal",
|
||||
else: "verification-standalone"
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp launch_delayed(socket) do
|
||||
Process.send_after(self(), {:start, socket.assigns.report_to}, socket.assigns.delay)
|
||||
end
|
||||
end
|
@ -108,5 +108,16 @@
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div :if={Plausible.Verification.enabled?(@current_user)}>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.Verification,
|
||||
session: %{
|
||||
"site_id" => @site.id,
|
||||
"domain" => @site.domain,
|
||||
"modal?" => true,
|
||||
"slowdown" => @conn.private[:verification_slowdown]
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -7,7 +7,11 @@
|
||||
<%= form_for @conn, "/", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
|
||||
<h2 class="text-xl font-bold dark:text-gray-100">Add JavaScript snippet</h2>
|
||||
<div class="mt-4">
|
||||
<p class="dark:text-gray-100">
|
||||
<p :if={Plausible.Verification.enabled?(@current_user)} class="dark:text-gray-100">
|
||||
Include this snippet in the <code><head></code>
|
||||
section of your website.<br />To verify your integration, click the button below to confirm that everything is working correctly.
|
||||
</p>
|
||||
<p :if={not Plausible.Verification.enabled?(@current_user)} class="dark:text-gray-100">
|
||||
Paste this snippet in the <code><head></code> of your website.
|
||||
</p>
|
||||
|
||||
@ -60,7 +64,13 @@
|
||||
</.styled_link>
|
||||
</p>
|
||||
</div>
|
||||
<%= link("Start collecting data →",
|
||||
<% button_label =
|
||||
if Plausible.Verification.enabled?(@current_user) do
|
||||
"Verify your integration to start collecting data →"
|
||||
else
|
||||
"Start collecting data →"
|
||||
end %>
|
||||
<%= link(button_label,
|
||||
class: "button mt-4 w-full",
|
||||
to: "/#{URI.encode_www_form(@site.domain)}"
|
||||
) %>
|
||||
|
@ -11,7 +11,6 @@
|
||||
|
||||
setInterval(updateStatus, 5000)
|
||||
</script>
|
||||
|
||||
<div class="w-full max-w-md mx-auto mt-8">
|
||||
<%= if @site.locked do %>
|
||||
<div
|
||||
@ -22,7 +21,10 @@
|
||||
<p>This dashboard is actually locked. You are viewing it with super-admin access</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-16 relative text-center">
|
||||
<div
|
||||
:if={not Plausible.Verification.enabled?(assigns[:current_user])}
|
||||
class="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-16 relative text-center"
|
||||
>
|
||||
<h2 class="text-xl font-bold dark:text-gray-100">Waiting for first pageview</h2>
|
||||
<h2 class="text-xl font-bold dark:text-gray-100">on <%= @site.domain %></h2>
|
||||
<div class="my-44">
|
||||
@ -58,4 +60,14 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if Plausible.Verification.enabled?(assigns[:current_user]),
|
||||
do:
|
||||
live_render(@conn, PlausibleWeb.Live.Verification,
|
||||
session: %{
|
||||
"site_id" => @site.id,
|
||||
"domain" => @site.domain,
|
||||
"slowdown" => @conn.private[:verification_slowdown]
|
||||
}
|
||||
) %>
|
||||
</div>
|
||||
|
7
mix.exs
7
mix.exs
@ -84,8 +84,8 @@ defmodule Plausible.MixProject do
|
||||
{:eqrcode, "~> 0.1.10"},
|
||||
{:ex_machina, "~> 2.3", only: [:dev, :test, :ce_dev, :ce_test]},
|
||||
{:excoveralls, "~> 0.10", only: :test},
|
||||
{:finch, "~> 0.16.0"},
|
||||
{:floki, "~> 0.35.0", only: [:dev, :test, :ce_dev, :ce_test]},
|
||||
{:finch, "~> 0.17.0"},
|
||||
{:floki, "~> 0.35.0"},
|
||||
{:fun_with_flags, "~> 1.11.0"},
|
||||
{:fun_with_flags_ui, "~> 1.0"},
|
||||
{:locus, "~> 2.3"},
|
||||
@ -142,7 +142,8 @@ defmodule Plausible.MixProject do
|
||||
{:ex_aws_s3, "~> 2.5"},
|
||||
{:sweet_xml, "~> 0.7.4"},
|
||||
{:zstream, "~> 0.6.4"},
|
||||
{:con_cache, "~> 1.1.0"}
|
||||
{:con_cache, "~> 1.1.0"},
|
||||
{:req, "~> 0.4.14"}
|
||||
]
|
||||
end
|
||||
|
||||
|
11
mix.lock
11
mix.lock
@ -8,7 +8,7 @@
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
|
||||
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
|
||||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"ch": {:hex, :ch, "0.2.5", "b8d70689951bd14c8c8791dc72cdc957ba489ceae723e79cf1a91d95b6b855ae", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "97de104c8f513a23c6d673da37741f68ae743f6cdb654b96a728d382e2fba4de"},
|
||||
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
|
||||
@ -53,7 +53,7 @@
|
||||
"excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"},
|
||||
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
|
||||
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
|
||||
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
|
||||
"fun_with_flags": {:hex, :fun_with_flags, "1.11.0", "a9019d0300e9755c53111cf5b2aba640d7f0de2a8a03a0bd0c593e943c3e9ec5", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}], "hexpm", "448ec640cd1ade4728979ae5b3e7592b0fc8b0f99cf40785d048515c27d09743"},
|
||||
"fun_with_flags_ui": {:hex, :fun_with_flags_ui, "1.0.0", "d764a4d1cc1233bdbb18dfb416a6ef96d0ecf4a5dc5a0201f7aa0b13cf2e7802", [:mix], [{:cowboy, ">= 2.0.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:fun_with_flags, "~> 1.11", [hex: :fun_with_flags, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 2.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "b0c145894c00d65d5dc20ee5b1f18457985d1fd0b87866f0b41894d5979e55e0"},
|
||||
@ -65,7 +65,7 @@
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
"heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"},
|
||||
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
@ -81,7 +81,7 @@
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"},
|
||||
"mjml": {:hex, :mjml, "1.5.0", "20a4ed2490a60c6928d45a69b64fb45ce8d8bdac686ef689315b0adda69c6406", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "44dc36c0fccf52eeb8e0afcb26a863ba41a5f9adcb71bb32e084619a13bb4cdf"},
|
||||
"mjml_eex": {:hex, :mjml_eex, "0.9.1", "102b6b6e57bfd6db01e0feef801b573fcddb1ee34effb884695da8407544a5be", [:mix], [{:erlexec, "~> 2.0", [hex: :erlexec, repo: "hexpm", optional: true]}, {:mjml, "~> 1.5.0", [hex: :mjml, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "310f9364d4f1126170835db6fb8dad87e393b28860b0e710d870812fb0bd7892"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
@ -91,7 +91,7 @@
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"nimble_totp": {:hex, :nimble_totp, "1.0.0", "79753bae6ce59fd7cacdb21501a1dbac249e53a51c4cd22b34fa8438ee067283", [:mix], [], "hexpm", "6ce5e4c068feecdb782e85b18237f86f66541523e6bad123e02ee1adbe48eda9"},
|
||||
"oban": {:hex, :oban, "2.17.2", "bcd1276473d8635475076b01032c00474f9c7841d3a2ca46ead26e1ec023cdd3", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de90489c05c039a6d942fa54d8fa13b858db315da2178e4e2c35c82c0a3ab556"},
|
||||
"observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"},
|
||||
@ -128,6 +128,7 @@
|
||||
"recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"},
|
||||
"ref_inspector": {:hex, :ref_inspector, "2.0.0", "f3e97e51d9782de4c792f56eed26c80903bc39174c878285392ce76d5e67fe98", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "bf62f3f1a87d6b8b30f457a480668f965373e64f184611282b5e89d8dd81fd33"},
|
||||
"referrer_blocklist": {:git, "https://github.com/plausible/referrer-blocklist.git", "d6f52c225cccb4f04b80e3a5d588868ec234139d", []},
|
||||
"req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"},
|
||||
"rustler_precompiled": {:hex, :rustler_precompiled, "0.6.2", "d2218ba08a43fa331957f30481d00b666664d7e3861431b02bd3f4f30eec8e5b", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "b9048eaed8d7d14a53f758c91865cc616608a438d2595f621f6a4b32a5511709"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
||||
|
42
priv/verification/verify_plausible_installed.js
Normal file
42
priv/verification/verify_plausible_installed.js
Normal file
@ -0,0 +1,42 @@
|
||||
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);
|
||||
await page.waitForNetworkIdle({ idleTime: 1000 });
|
||||
|
||||
const plausibleInstalled = await page.evaluate(() => {
|
||||
window.__plausible = true;
|
||||
if (typeof (window.plausible) === "function") {
|
||||
window.plausible('verification-agent-test', {
|
||||
callback: function(options) {
|
||||
window.plausibleCallbackResult = () => options && options.status ? options.status : 1;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
window.plausibleCallbackResult = () => 0;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
await page.waitForFunction('window.plausibleCallbackResult', { timeout: 2000 });
|
||||
const callbackStatus = await page.evaluate(() => {
|
||||
if (typeof (window.plausibleCallbackResult) === "function") {
|
||||
return window.plausibleCallbackResult();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
plausibleInstalled, callbackStatus
|
||||
},
|
||||
type: "application/json"
|
||||
};
|
||||
}
|
@ -6,7 +6,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
alias Plausible.Ingestion.Request
|
||||
alias Plausible.Ingestion.Event
|
||||
|
||||
test "event pipeline processes a request into an event" do
|
||||
test "processes a request into an event" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -20,7 +20,25 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert {:ok, %{buffered: [_], dropped: []}} = Event.build_and_buffer(request)
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when site does not exists" do
|
||||
test "drops verification agent" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
name: "pageview",
|
||||
url: "http://#{site.domain}"
|
||||
}
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/api/events", payload)
|
||||
|> Plug.Conn.put_req_header("user-agent", Plausible.Verification.user_agent())
|
||||
|
||||
assert {:ok, request} = Request.build(conn)
|
||||
|
||||
assert {:ok, %{buffered: [], dropped: [dropped]}} = Event.build_and_buffer(request)
|
||||
assert dropped.drop_reason == :verification_agent
|
||||
end
|
||||
|
||||
test "drops a request when site does not exists" do
|
||||
payload = %{
|
||||
name: "pageview",
|
||||
url: "http://dummy.site"
|
||||
@ -33,7 +51,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :not_found
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when referrer is spam" do
|
||||
test "drops a request when referrer is spam" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -50,7 +68,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :spam_referrer
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when referrer is spam for multiple domains" do
|
||||
test "drops a request when referrer is spam for multiple domains" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -67,7 +85,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :spam_referrer
|
||||
end
|
||||
|
||||
test "event pipeline selectively drops an event for multiple domains" do
|
||||
test "selectively drops an event for multiple domains" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -83,7 +101,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :not_found
|
||||
end
|
||||
|
||||
test "event pipeline selectively drops an event when rate-limited" do
|
||||
test "selectively drops an event when rate-limited" do
|
||||
site = insert(:site, ingest_rate_limit_threshold: 1)
|
||||
|
||||
payload = %{
|
||||
@ -100,7 +118,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :throttle
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when header x-plausible-ip-type is dc_ip" do
|
||||
test "drops a request when header x-plausible-ip-type is dc_ip" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -117,7 +135,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :dc_ip
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when ip is on blocklist" do
|
||||
test "drops a request when ip is on blocklist" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -137,7 +155,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :site_ip_blocklist
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when country is on blocklist" do
|
||||
test "drops a request when country is on blocklist" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -158,7 +176,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :site_country_blocklist
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when page is on blocklist" do
|
||||
test "drops a request when page is on blocklist" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -177,7 +195,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :site_page_blocklist
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when hostname allowlist is defined and hostname is not on the list" do
|
||||
test "drops a request when hostname allowlist is defined and hostname is not on the list" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -196,7 +214,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :site_hostname_allowlist
|
||||
end
|
||||
|
||||
test "event pipeline passes a request when hostname allowlist is defined and hostname is on the list" do
|
||||
test "passes a request when hostname allowlist is defined and hostname is on the list" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
@ -214,7 +232,7 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert {:ok, %{buffered: [_], dropped: []}} = Event.build_and_buffer(request)
|
||||
end
|
||||
|
||||
test "event pipeline drops events for site with accept_trafic_until in the past" do
|
||||
test "drops events for site with accept_trafic_until in the past" do
|
||||
yesterday = Date.add(Date.utc_today(), -1)
|
||||
|
||||
site =
|
||||
|
39
test/plausible/site/verification/checks/csp_test.exs
Normal file
39
test/plausible/site/verification/checks/csp_test.exs
Normal file
@ -0,0 +1,39 @@
|
||||
defmodule Plausible.Verification.Checks.CSPTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
alias Plausible.Verification.State
|
||||
|
||||
@check Plausible.Verification.Checks.CSP
|
||||
|
||||
test "skips no headers" do
|
||||
state = %State{}
|
||||
assert ^state = @check.perform(state)
|
||||
end
|
||||
|
||||
test "skips no headers 2" do
|
||||
state = %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 =
|
||||
%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 =
|
||||
%State{}
|
||||
|> State.assign(headers: headers)
|
||||
|> @check.perform()
|
||||
|
||||
refute state.diagnostics.disallowed_via_csp?
|
||||
end
|
||||
end
|
64
test/plausible/site/verification/checks/fetch_body_test.exs
Normal file
64
test/plausible/site/verification/checks/fetch_body_test.exs
Normal file
@ -0,0 +1,64 @@
|
||||
defmodule Plausible.Verification.Checks.FetchBodyTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@check Plausible.Verification.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: %Plausible.Verification.State{
|
||||
url: "https://example.com"
|
||||
}}
|
||||
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 "doesn't extract on non-2xx", %{state: state} do
|
||||
stub(400)
|
||||
state = @check.perform(state)
|
||||
|
||||
assert map_size(state.assigns) == 0
|
||||
|
||||
refute 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 map_size(state.assigns) == 0
|
||||
|
||||
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
|
70
test/plausible/site/verification/checks/scan_body_test.exs
Normal file
70
test/plausible/site/verification/checks/scan_body_test.exs
Normal file
@ -0,0 +1,70 @@
|
||||
defmodule Plausible.Verification.Checks.ScanBodyTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
alias Plausible.Verification.State
|
||||
|
||||
@check Plausible.Verification.Checks.ScanBody
|
||||
|
||||
test "skips on no raw body" do
|
||||
state = %State{}
|
||||
assert ^state = @check.perform(state)
|
||||
end
|
||||
|
||||
test "detects nothing" do
|
||||
state =
|
||||
%State{}
|
||||
|> State.assign(raw_body: "...")
|
||||
|> @check.perform()
|
||||
|
||||
refute state.diagnostics.gtm_likely?
|
||||
refute state.diagnostics.wordpress_likely?
|
||||
end
|
||||
|
||||
test "detects GTM" do
|
||||
state =
|
||||
%State{}
|
||||
|> State.assign(raw_body: "...googletagmanager.com/gtm.js...")
|
||||
|> @check.perform()
|
||||
|
||||
assert state.diagnostics.gtm_likely?
|
||||
refute state.diagnostics.wordpress_likely?
|
||||
end
|
||||
|
||||
for signature <- ["wp-content", "wp-includes", "wp-json"] do
|
||||
test "detects WordPress: #{signature}" do
|
||||
state =
|
||||
%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 =
|
||||
%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 =
|
||||
%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
|
143
test/plausible/site/verification/checks/snippet_test.exs
Normal file
143
test/plausible/site/verification/checks/snippet_test.exs
Normal file
@ -0,0 +1,143 @@
|
||||
defmodule Plausible.Verification.Checks.SnippetTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
alias Plausible.Verification.State
|
||||
|
||||
@check Plausible.Verification.Checks.Snippet
|
||||
|
||||
test "skips when there's no document" do
|
||||
state = %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?
|
||||
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?
|
||||
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
|
||||
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?
|
||||
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?
|
||||
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 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)
|
||||
|
||||
State
|
||||
|> struct!(opts)
|
||||
|> State.assign(document: doc)
|
||||
end
|
||||
end
|
790
test/plausible/site/verification/checks_test.exs
Normal file
790
test/plausible/site/verification/checks_test.exs
Normal file
@ -0,0 +1,790 @@
|
||||
defmodule Plausible.Verification.ChecksTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
alias Plausible.Verification.Checks
|
||||
alias Plausible.Verification.State
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Plug.Conn
|
||||
|
||||
@normal_body """
|
||||
<html>
|
||||
<head>
|
||||
<script defer data-domain="example.com" src="http://localhost:8000/js/script.js"></script>
|
||||
</head>
|
||||
<body>Hello</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
describe "running checks" do
|
||||
test "success" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation()
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
assert interpretation.ok?
|
||||
assert interpretation.errors == []
|
||||
assert interpretation.recommendations == []
|
||||
end
|
||||
|
||||
test "service error - 400" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation(400, %{})
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
|
||||
assert interpretation.errors == [
|
||||
"We encountered a temporary problem verifying your website"
|
||||
]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please try again in a few minutes or manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "can't fetch body but headless reports ok" do
|
||||
stub_fetch_body(500, "")
|
||||
stub_installation()
|
||||
|
||||
{result, log} =
|
||||
with_log(fn ->
|
||||
run_checks()
|
||||
end)
|
||||
|
||||
assert log =~ "3 attempts left"
|
||||
assert log =~ "2 attempts left"
|
||||
assert log =~ "1 attempt left"
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't reach https://example.com. Is your site up?"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
end
|
||||
|
||||
test "fetching will follow 2 redirects" do
|
||||
ref = :counters.new(1, [:atomics])
|
||||
test = self()
|
||||
|
||||
Req.Test.stub(Plausible.Verification.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()
|
||||
|
||||
result = run_checks()
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
refute_receive _
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
assert interpretation.ok?
|
||||
assert interpretation.errors == []
|
||||
assert interpretation.recommendations == []
|
||||
end
|
||||
|
||||
test "fetching will not follow more than 2 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()
|
||||
|
||||
result = run_checks()
|
||||
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
assert_receive :redirect_sent
|
||||
refute_receive _
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't reach https://example.com. Is your site up?"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
end
|
||||
|
||||
test "fetching body fails at non-2xx status, but installation is ok" do
|
||||
stub_fetch_body(599, "boo")
|
||||
stub_installation()
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't reach https://example.com. Is your site up?"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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()
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["Plausible snippet is placed in the body of your site"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please relocate the snippet to the header of your site",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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()
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We've found multiple Plausible snippets on your site."]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please ensure that only one snippet is used",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
end
|
||||
|
||||
@body_no_snippet """
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an issue with your site cache"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please clear your cache (or wait for your provider to clear it) to ensure that the latest version of your site is being displayed to all your visitors",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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)
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an issue with your site cache"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please install and activate our WordPress plugin to start counting your visitors",
|
||||
"https://plausible.io/wordpress-analytics-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)
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an issue with your site cache"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please clear your WordPress cache to ensure that the latest version of your site is being displayed to all your visitors",
|
||||
"https://plausible.io/wordpress-analytics-plugin"}
|
||||
]
|
||||
end
|
||||
|
||||
test "detecting no snippet" do
|
||||
stub_fetch_body(200, @body_no_snippet)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't find the Plausible snippet on your site"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please insert the snippet into your site",
|
||||
"https://plausible.io/docs/plausible-script"}
|
||||
]
|
||||
end
|
||||
|
||||
test "a check that raises" do
|
||||
defmodule FaultyCheckRaise do
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "Faulty check"
|
||||
|
||||
@impl true
|
||||
def perform(_), do: raise("boom")
|
||||
end
|
||||
|
||||
{result, log} =
|
||||
with_log(fn ->
|
||||
run_checks(checks: [FaultyCheckRaise])
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
~s|Error running check Plausible.Verification.ChecksTest.FaultyCheckRaise on https://example.com: %RuntimeError{message: "boom"}|
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't reach https://example.com. Is your site up?"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
end
|
||||
|
||||
test "a check that throws" do
|
||||
defmodule FaultyCheckThrow do
|
||||
use Plausible.Verification.Check
|
||||
|
||||
@impl true
|
||||
def friendly_name, do: "Faulty check"
|
||||
|
||||
@impl true
|
||||
def perform(_), do: :erlang.throw(:boom)
|
||||
end
|
||||
|
||||
{result, log} =
|
||||
with_log(fn ->
|
||||
run_checks(checks: [FaultyCheckThrow])
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
~s|Error running check Plausible.Verification.ChecksTest.FaultyCheckThrow on https://example.com: :boom|
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't reach https://example.com. Is your site up?"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
|
||||
assert interpretation.errors == ["We encountered an issue with your site's CSP"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{
|
||||
"Please add plausible.io domain specifically to the allowed list of domains in your Content Security Policy (CSP)",
|
||||
"https://plausible.io/docs/troubleshoot-integration"
|
||||
}
|
||||
]
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
|
||||
assert interpretation.errors == ["We couldn't find the Plausible snippet on your site"]
|
||||
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()
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
assert interpretation.ok?
|
||||
assert interpretation.errors == []
|
||||
assert interpretation.recommendations == []
|
||||
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 {:verification_check_start, {Checks.FetchBody, %State{}}}
|
||||
assert_receive {:verification_check_start, {Checks.CSP, %State{}}}
|
||||
assert_receive {:verification_check_start, {Checks.ScanBody, %State{}}}
|
||||
assert_receive {:verification_check_start, {Checks.Snippet, %State{}}}
|
||||
assert_receive {:verification_check_start, {Checks.SnippetCacheBust, %State{}}}
|
||||
assert_receive {:verification_check_start, {Checks.Installation, %State{}}}
|
||||
assert_receive {:verification_end, %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 "detecting gtm" do
|
||||
stub_fetch_body(200, @gtm_body)
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an issue with your Plausible integration"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"As you're using Google Tag Manager, you'll need to use a GTM-specific Plausible snippet",
|
||||
"https://plausible.io/docs/google-tag-manager"}
|
||||
]
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We couldn't reach https://example.com. Is your site up?"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"If your site is running at a different location, please manually check your integration",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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()
|
||||
|
||||
result = run_checks()
|
||||
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
assert interpretation.ok?
|
||||
assert interpretation.errors == []
|
||||
assert interpretation.recommendations == []
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an error with your Plausible proxy"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please check whether you've configured the /event route correctly",
|
||||
"https://plausible.io/docs/proxy/introduction"}
|
||||
]
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an error with your Plausible proxy"]
|
||||
|
||||
assert interpretation.recommendations ==
|
||||
[
|
||||
{"Please re-enable the proxy in our WordPress plugin to start counting your visitors",
|
||||
"https://plausible.io/wordpress-analytics-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 = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["We encountered an error with your Plausible proxy"]
|
||||
|
||||
assert interpretation.recommendations ==
|
||||
[
|
||||
{"Please check your proxy configuration to make sure it's set up correctly",
|
||||
"https://plausible.io/docs/proxy/introduction"}
|
||||
]
|
||||
end
|
||||
|
||||
test "non-proxied setup, but callback fails to fire" do
|
||||
stub_fetch_body(200, @normal_body)
|
||||
stub_installation(200, plausible_installed(true, 0))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["Your Plausible integration is not working"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please manually check your integration to make sure that the Plausible snippet has been inserted correctly",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
assert interpretation.errors == ["Something seems to have altered our snippet"]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please manually check your integration to make sure that nothing prevents our script from working",
|
||||
"https://plausible.io/docs/troubleshoot-integration"}
|
||||
]
|
||||
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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
|
||||
assert interpretation.errors == [
|
||||
"A performance optimization plugin seems to have altered our snippet"
|
||||
]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please install and activate our WordPress plugin to avoid the most common plugin conflicts",
|
||||
"https://plausible.io/wordpress-analytics-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))
|
||||
|
||||
result = run_checks()
|
||||
interpretation = Checks.interpret_diagnostics(result)
|
||||
|
||||
refute interpretation.ok?
|
||||
|
||||
assert interpretation.errors == [
|
||||
"A performance optimization plugin seems to have altered our snippet"
|
||||
]
|
||||
|
||||
assert interpretation.recommendations == [
|
||||
{"Please whitelist our script in your performance optimization plugin to stop it from changing our snippet",
|
||||
"https://plausible.io/wordpress-analytics-plugin "}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp run_checks(extra_opts \\ []) do
|
||||
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(Plausible.Verification.Checks.FetchBody, f)
|
||||
end
|
||||
|
||||
defp stub_installation(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Plausible.Verification.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" => %{"plausibleInstalled" => bool, "callbackStatus" => callback_status}}
|
||||
end
|
||||
end
|
@ -77,6 +77,17 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||
assert text_of_attr(resp, @react_container, "data-logged-in") == "true"
|
||||
end
|
||||
|
||||
test "can view stats of a website I've created, enforcing pageviews check skip", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
resp = conn |> get("/" <> site.domain) |> html_response(200)
|
||||
refute text_of_attr(resp, @react_container, "data-logged-in") == "true"
|
||||
|
||||
resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200)
|
||||
assert text_of_attr(resp, @react_container, "data-logged-in") == "true"
|
||||
end
|
||||
|
||||
test "shows locked page if page is locked", %{conn: conn, user: user} do
|
||||
locked_site = insert(:site, locked: true, members: [user])
|
||||
conn = get(conn, "/" <> locked_site.domain)
|
||||
|
87
test/plausible_web/live/components/verification_test.exs
Normal file
87
test/plausible_web/live/components/verification_test.exs
Normal file
@ -0,0 +1,87 @@
|
||||
defmodule PlausibleWeb.Live.Components.VerificationTest do
|
||||
use PlausibleWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
|
||||
@component PlausibleWeb.Live.Components.Verification
|
||||
@progress ~s|div#progress|
|
||||
|
||||
@pulsating_circle ~s|div#progress-indicator div.pulsating-circle|
|
||||
@check_circle ~s|div#progress-indicator #check-circle|
|
||||
@shuttle ~s|div#progress-indicator svg#shuttle|
|
||||
@recommendations ~s|div#recommendations .recommendation|
|
||||
|
||||
test "renders initial state" do
|
||||
html = render_component(@component, domain: "example.com")
|
||||
assert element_exists?(html, @progress)
|
||||
|
||||
assert text_of_element(html, @progress) ==
|
||||
"We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
assert element_exists?(html, ~s|a[href="/example.com/snippet"]|)
|
||||
assert element_exists?(html, ~s|a[href="/example.com/settings/general"]|)
|
||||
assert element_exists?(html, @pulsating_circle)
|
||||
refute class_of_element(html, @pulsating_circle) =~ "hidden"
|
||||
refute element_exists?(html, @recommendations)
|
||||
refute element_exists?(html, @check_circle)
|
||||
end
|
||||
|
||||
test "renders shuttle on error" do
|
||||
html = render_component(@component, domain: "example.com", success?: false, finished?: true)
|
||||
refute element_exists?(html, @pulsating_circle)
|
||||
refute element_exists?(html, @check_circle)
|
||||
refute element_exists?(html, @recommendations)
|
||||
assert element_exists?(html, @shuttle)
|
||||
end
|
||||
|
||||
test "renders diagnostic interpretation" do
|
||||
interpretation =
|
||||
Plausible.Verification.Checks.interpret_diagnostics(%Plausible.Verification.State{
|
||||
url: "example.com"
|
||||
})
|
||||
|
||||
html =
|
||||
render_component(@component,
|
||||
domain: "example.com",
|
||||
success?: false,
|
||||
finished?: true,
|
||||
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"
|
||||
]
|
||||
end
|
||||
|
||||
test "hides pulsating circle when finished in a modal, shows check circle" do
|
||||
html =
|
||||
render_component(@component,
|
||||
domain: "example.com",
|
||||
modal?: true,
|
||||
success?: true,
|
||||
finished?: true
|
||||
)
|
||||
|
||||
assert class_of_element(html, @pulsating_circle) =~ "hidden"
|
||||
assert element_exists?(html, @check_circle)
|
||||
end
|
||||
|
||||
test "renders a progress message" do
|
||||
html = render_component(@component, domain: "example.com", message: "Arbitrary message")
|
||||
|
||||
assert text_of_element(html, @progress) == "Arbitrary message"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "renders contact link on >3 attempts" do
|
||||
html = render_component(@component, domain: "example.com", attempts: 2, finished?: true)
|
||||
refute html =~ "Need further help with your integration?"
|
||||
refute element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
|
||||
|
||||
html = render_component(@component, domain: "example.com", attempts: 3, finished?: true)
|
||||
assert html =~ "Need further help with your integration?"
|
||||
assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
|
||||
end
|
||||
end
|
@ -84,8 +84,7 @@ defmodule PlausibleWeb.Live.Shields.HostnamesTest do
|
||||
html = render(lv)
|
||||
|
||||
assert text(html) =~
|
||||
"NB: Once added, we will start rejecting traffic from non-matching hostnames within a few minutes.
|
||||
"
|
||||
"NB: Once added, we will start rejecting traffic from non-matching hostnames within a few minutes."
|
||||
|
||||
refute text(html) =~ "we will start accepting"
|
||||
end
|
||||
|
238
test/plausible_web/live/verification_test.exs
Normal file
238
test/plausible_web/live/verification_test.exs
Normal file
@ -0,0 +1,238 @@
|
||||
defmodule PlausibleWeb.Live.VerificationTest do
|
||||
use PlausibleWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
@verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
|
||||
@verification_modal ~s|div#verification-modal|
|
||||
@retry_button ~s|a[phx-click="retry"]|
|
||||
@go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]|
|
||||
@progress ~s|div#progress|
|
||||
|
||||
describe "GET /:domain" do
|
||||
test "static verification screen renders", %{conn: conn, site: site} do
|
||||
resp = conn |> no_slowdown() |> get("/#{site.domain}") |> html_response(200)
|
||||
|
||||
assert text_of_element(resp, @progress) =~
|
||||
"We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
assert resp =~ "Verifying your integration"
|
||||
assert resp =~ "on #{site.domain}"
|
||||
assert resp =~ "Need to see the snippet again?"
|
||||
assert resp =~ "Run verification later and go to Site Settings?"
|
||||
refute resp =~ "modal"
|
||||
refute element_exists?(resp, @verification_modal)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /settings/general" do
|
||||
test "verification elements render under the snippet", %{conn: conn, site: site} do
|
||||
resp =
|
||||
conn |> no_slowdown() |> get("/#{site.domain}/settings/general") |> html_response(200)
|
||||
|
||||
assert element_exists?(resp, @verify_button)
|
||||
assert element_exists?(resp, @verification_modal)
|
||||
end
|
||||
end
|
||||
|
||||
describe "LiveView: standalone" do
|
||||
test "LiveView mounts", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, "")
|
||||
stub_installation()
|
||||
|
||||
{_, html} = get_lv_standalone(conn, site)
|
||||
|
||||
assert html =~ "Verifying your integration"
|
||||
assert html =~ "on #{site.domain}"
|
||||
|
||||
assert text_of_element(html, @progress) =~
|
||||
"We're visiting your site to ensure that everything is working correctly"
|
||||
end
|
||||
|
||||
test "eventually verifies installation", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
|
||||
{:ok, lv} = kick_off_live_verification_standalone(conn, site)
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{
|
||||
text_of_element(html, @progress) =~
|
||||
"Awaiting your first pageview",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "Success!"
|
||||
assert html =~ "Your integration is working and visitors are being counted accurately"
|
||||
end
|
||||
|
||||
test "eventually fails to verify installation", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, "")
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
{:ok, lv} = kick_off_live_verification_standalone(conn, site)
|
||||
|
||||
assert html =
|
||||
eventually(fn ->
|
||||
html = render(lv)
|
||||
{html =~ "", html}
|
||||
|
||||
{
|
||||
text_of_element(html, @progress) =~
|
||||
"We couldn't find the Plausible snippet on your site",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
refute element_exists?(html, @verification_modal)
|
||||
assert element_exists?(html, @retry_button)
|
||||
|
||||
assert html =~ "Please insert the snippet into your site"
|
||||
end
|
||||
end
|
||||
|
||||
describe "LiveView: modal" do
|
||||
test "LiveView mounts", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, "")
|
||||
stub_installation()
|
||||
|
||||
{_, html} = get_lv_modal(conn, site)
|
||||
|
||||
text = text(html)
|
||||
|
||||
refute text =~ "Need to see the snippet again?"
|
||||
refute text =~ "Run verification later and go to Site Settings?"
|
||||
assert element_exists?(html, @verification_modal)
|
||||
end
|
||||
|
||||
test "Clicking the Verify modal launches verification", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
|
||||
{lv, html} = get_lv_modal(conn, site)
|
||||
|
||||
assert element_exists?(html, @verification_modal)
|
||||
assert element_exists?(html, @verify_button)
|
||||
assert text_of_attr(html, @verify_button, "x-on:click") =~ "open-modal"
|
||||
|
||||
assert text_of_element(html, @progress) =~
|
||||
"We're visiting your site to ensure that everything is working correctly"
|
||||
|
||||
lv |> element(@verify_button) |> render_click()
|
||||
|
||||
assert html =
|
||||
eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{
|
||||
html =~ "Success!",
|
||||
html
|
||||
}
|
||||
end)
|
||||
|
||||
refute html =~ "Awaiting your first pageview"
|
||||
assert element_exists?(html, @go_to_dashboard_button)
|
||||
end
|
||||
|
||||
test "failed verification can be retried", %{conn: conn, site: site} do
|
||||
stub_fetch_body(200, "")
|
||||
stub_installation(200, plausible_installed(false))
|
||||
|
||||
{lv, _html} = get_lv_modal(conn, site)
|
||||
|
||||
lv |> element(@verify_button) |> render_click()
|
||||
|
||||
assert html =
|
||||
eventually(fn ->
|
||||
html = render(lv)
|
||||
|
||||
{text_of_element(html, @progress) =~
|
||||
"We couldn't find the Plausible snippet on your site", html}
|
||||
end)
|
||||
|
||||
assert element_exists?(html, @retry_button)
|
||||
|
||||
stub_fetch_body(200, source(site.domain))
|
||||
stub_installation()
|
||||
|
||||
lv |> element(@retry_button) |> render_click()
|
||||
|
||||
assert eventually(fn ->
|
||||
html = render(lv)
|
||||
{html =~ "Success!", html}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_lv_standalone(conn, site) do
|
||||
conn = conn |> no_slowdown() |> assign(:live_module, PlausibleWeb.Live.Verification)
|
||||
{:ok, lv, html} = live(conn, "/#{site.domain}")
|
||||
{lv, html}
|
||||
end
|
||||
|
||||
defp get_lv_modal(conn, site) do
|
||||
conn = conn |> no_slowdown() |> assign(:live_module, PlausibleWeb.Live.Verification)
|
||||
{:ok, lv, html} = live(no_slowdown(conn), "/#{site.domain}/settings/general")
|
||||
{lv, html}
|
||||
end
|
||||
|
||||
defp kick_off_live_verification_standalone(conn, site) do
|
||||
{:ok, lv, _} =
|
||||
live_isolated(conn, PlausibleWeb.Live.Verification,
|
||||
session: %{
|
||||
"domain" => site.domain,
|
||||
"delay" => 0,
|
||||
"slowdown" => 0
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, lv}
|
||||
end
|
||||
|
||||
defp no_slowdown(conn) do
|
||||
Plug.Conn.put_private(conn, :verification_slowdown, 0)
|
||||
end
|
||||
|
||||
defp stub_fetch_body(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Plausible.Verification.Checks.FetchBody, f)
|
||||
end
|
||||
|
||||
defp stub_installation(f) when is_function(f, 1) do
|
||||
Req.Test.stub(Plausible.Verification.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" => %{"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
|
@ -38,6 +38,7 @@ defmodule Plausible.Test.Support.HTML do
|
||||
element
|
||||
|> Floki.text()
|
||||
|> String.trim()
|
||||
|> String.replace(~r/\s+/, " ")
|
||||
end
|
||||
|
||||
def class_of_element(html, element) do
|
||||
|
@ -1,3 +1,5 @@
|
||||
FunWithFlags.enable(:verification)
|
||||
|
||||
if not Enum.empty?(Path.wildcard("lib/**/*_test.exs")) do
|
||||
raise "Oops, test(s) found in `lib/` directory. Move them to `test/`."
|
||||
end
|
||||
|
@ -33,7 +33,7 @@
|
||||
if (/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(location.hostname) || location.protocol === 'file:') {
|
||||
return onIgnoredEvent('localhost', options)
|
||||
}
|
||||
if (window._phantom || window.__nightmare || window.navigator.webdriver || window.Cypress) {
|
||||
if ((window._phantom || window.__nightmare || window.navigator.webdriver || window.Cypress) && !window.__plausible) {
|
||||
return onIgnoredEvent(null, options)
|
||||
}
|
||||
{{/unless}}
|
||||
@ -115,7 +115,7 @@
|
||||
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState === 4) {
|
||||
options && options.callback && options.callback()
|
||||
options && options.callback && options.callback({status: request.status})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user