From 830f3fa7e5e158e66d12e3ac9b0fd5b4a06a7f61 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Tue, 14 Jul 2026 15:20:46 +0100 Subject: [PATCH 01/21] make verification work in a banner --- assets/js/dashboard/index.tsx | 6 +- .../dashboard/stats/graph/visitor-graph.tsx | 2 +- assets/js/dashboard/verification/portal.tsx | 9 + extra/lib/plausible_web/live/verification.ex | 128 ++++----- .../controllers/stats_controller.ex | 35 ++- lib/plausible_web/live/awaiting_pageviews.ex | 99 ------- .../live/components/verification.ex | 257 +++++++++--------- lib/plausible_web/live/installation.ex | 23 +- lib/plausible_web/live/sites.ex | 25 +- lib/plausible_web/router.ex | 9 - .../templates/stats/stats.html.heex | 6 + .../controllers/stats_controller_test.exs | 59 ++-- .../live/components/verification_test.exs | 14 +- test/plausible_web/live/installation_test.exs | 56 ++-- test/plausible_web/live/verification_test.exs | 100 +++---- 15 files changed, 395 insertions(+), 433 deletions(-) create mode 100644 assets/js/dashboard/verification/portal.tsx delete mode 100644 lib/plausible_web/live/awaiting_pageviews.ex diff --git a/assets/js/dashboard/index.tsx b/assets/js/dashboard/index.tsx index 1736f963d227..b4c8e0c1cb15 100644 --- a/assets/js/dashboard/index.tsx +++ b/assets/js/dashboard/index.tsx @@ -11,6 +11,7 @@ import { isRealTimeDashboard } from './util/filters' import { GraphIntervalProvider } from './stats/graph/graph-interval-context' import { ImportsIncludedProvider } from './stats/graph/imports-included-context' import { CurrentVisitorsProvider } from './current-visitors-context' +import { VerificationLiveViewPortal } from './verification/portal' function DashboardStats({ importedDataInView, @@ -21,7 +22,10 @@ function DashboardStats({ }) { return ( <> - +
+ + +
diff --git a/assets/js/dashboard/stats/graph/visitor-graph.tsx b/assets/js/dashboard/stats/graph/visitor-graph.tsx index e16a9d955eef..1483c712ad7e 100644 --- a/assets/js/dashboard/stats/graph/visitor-graph.tsx +++ b/assets/js/dashboard/stats/graph/visitor-graph.tsx @@ -113,7 +113,7 @@ export default function VisitorGraph({ !showFullLoader return ( -
+
<>
{ + return
+ + }, + () => true +) diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 0fc148d3a2b0..21e4179aeb2a 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -1,8 +1,7 @@ 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. + LiveView coordinating the site verification process. Rendered as a banner + on top of the React dashboard. """ use PlausibleWeb, :live_view @@ -12,10 +11,11 @@ defmodule PlausibleWeb.Live.Verification do @component PlausibleWeb.Live.Components.Verification @slowdown_for_frequent_checking :timer.seconds(5) + @use_portal? Mix.env() not in [:test, :ce_test] def mount( - %{"domain" => domain} = params, - _session, + _params, + %{"domain" => domain} = session, socket ) do current_user = socket.assigns.current_user @@ -38,8 +38,6 @@ defmodule PlausibleWeb.Live.Verification do super_admin? = Plausible.Auth.super_admin?(current_user) has_pageviews? = has_pageviews?(site) - custom_url_input? = params["custom_url"] == "true" - socket = assign(socket, url_to_verify: nil, @@ -48,18 +46,18 @@ defmodule PlausibleWeb.Live.Verification do domain: domain, has_pageviews?: has_pageviews?, component: @component, - installation_type: get_installation_type(params, site), + installation_type: get_installation_type(session["installation_type"], site), report_to: self(), delay: private[:delay] || 500, slowdown: private[:slowdown] || 500, - flow: params["flow"] || "", + flow: session["flow"] || "", checks_pid: nil, attempts: 0, polling_pageviews?: false, - custom_url_input?: custom_url_input? + custom_url_input?: false ) - if connected?(socket) and not custom_url_input? do + if connected?(socket) do launch_delayed(socket) end @@ -67,8 +65,23 @@ defmodule PlausibleWeb.Live.Verification do end def render(assigns) do + assigns = assign(assigns, :use_portal?, @use_portal?) + + ~H""" +
+ <%= if @use_portal? do %> + <.portal id="verification-portal-source" target="#verification-portal-target"> + <.verification_content {assigns} /> + + <% else %> + <.verification_content {assigns} /> + <% end %> +
+ """ + end + + defp verification_content(assigns) do ~H""" - <.custom_url_form :if={@custom_url_input?} domain={@domain} /> <.live_component :if={not @custom_url_input?} @@ -94,6 +107,10 @@ defmodule PlausibleWeb.Live.Verification do {:noreply, reset_component(socket)} end + def handle_event("show-custom-url-form", _, socket) do + {:noreply, assign(socket, custom_url_input?: true)} + end + def handle_event("verify-custom-url", %{"custom_url" => custom_url}, socket) do socket = socket @@ -166,24 +183,24 @@ defmodule PlausibleWeb.Live.Verification do end def handle_info(:check_pageviews, socket) do - socket = - if has_pageviews?(socket.assigns.site) do - redirect_to_stats(socket) - else + if has_pageviews?(socket.assigns.site) do + {:noreply, assign(socket, has_pageviews?: true, polling_pageviews?: false)} + else + socket = socket |> assign(polling_pageviews?: false) |> schedule_pageviews_check() - end - {:noreply, socket} + {:noreply, socket} + end end @supported_installation_types_atoms PlausibleWeb.Tracker.supported_installation_types() |> Enum.map(&String.to_atom/1) - defp get_installation_type(params, site) do + defp get_installation_type(installation_type, site) do cond do - params["installation_type"] in PlausibleWeb.Tracker.supported_installation_types() -> - params["installation_type"] + installation_type in PlausibleWeb.Tracker.supported_installation_types() -> + installation_type (saved_installation_type = get_saved_installation_type(site)) in @supported_installation_types_atoms -> Atom.to_string(saved_installation_type) @@ -212,11 +229,6 @@ defmodule PlausibleWeb.Live.Verification do end end - defp redirect_to_stats(socket) do - stats_url = Routes.stats_path(PlausibleWeb.Endpoint, :stats, socket.assigns.domain, []) - redirect(socket, to: stats_url) - end - defp reset_component(socket) do update_component(socket, message: "We're visiting your site to ensure that everything is working", @@ -245,44 +257,32 @@ defmodule PlausibleWeb.Live.Verification do defp custom_url_form(assigns) do ~H""" - <.focus_box> -
- -
-
-

- Enter Your Custom URL -

-

- Please enter the URL where your website with the Plausible script is located. -

-
-
- - -
- -
-
- + <.notice title="Enter your custom URL" theme={:gray} class="mb-4"> + <:icon> + + +

+ Please enter the URL where your website with the Plausible script is located. +

+
+ + + +
+ """ end end diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 38e9169f66f9..f96cd2f94855 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -70,9 +70,6 @@ defmodule PlausibleWeb.StatsController do team_identifier = site.team.identifier - skip_to_dashboard? = - conn.params["skip_to_dashboard"] == "true" or consolidated_view? - {:ok, segments} = Plausible.Segments.get_all_for_site(site, site_role) segments = Enum.map(segments, &Plausible.Segments.to_response_map(&1, site)) @@ -80,9 +77,19 @@ defmodule PlausibleWeb.StatsController do consolidated_view? and not consolidated_view_available? and site_role != :super_admin -> redirect(conn, to: Routes.site_path(conn, :index)) - (stats_start_date && can_see_stats?) || (can_see_stats? && skip_to_dashboard?) -> + not can_see_stats? -> + site = Plausible.Repo.preload(site, :owners) + render(conn, "site_locked.html", site: site, dogfood_page_path: dogfood_page_path) + + true -> flags = get_flags(current_user, site) + verify_installation? = + ee?() and + not is_nil(current_user) and + not consolidated_view? and + conn.params["verify_installation"] == "true" + conn |> put_resp_header("x-robots-tag", "noindex, nofollow") |> render("stats.html", @@ -106,15 +113,15 @@ defmodule PlausibleWeb.StatsController do exploration_journey_end_event: exploration_journey_end_event, exploration_max_journey_steps: exploration_max_journey_steps, team_identifier: team_identifier, - limited_to_segment_id: nil + limited_to_segment_id: nil, + connect_live_socket: verify_installation?, + verify_installation?: verify_installation?, + verification_session: %{ + "domain" => site.domain, + "flow" => conn.params["flow"], + "installation_type" => conn.params["installation_type"] + } ) - - !stats_start_date && can_see_stats? -> - redirect(conn, to: Routes.site_path(conn, :verification, site.domain)) - - Teams.locked?(site.team) -> - site = Plausible.Repo.preload(site, :owners) - render(conn, "site_locked.html", site: site, dogfood_page_path: dogfood_page_path) end end @@ -422,7 +429,9 @@ defmodule PlausibleWeb.StatsController do exploration_journey_end_event: exploration_journey_end_event, exploration_max_journey_steps: exploration_max_journey_steps, team_identifier: team_identifier, - limited_to_segment_id: limited_to_segment_id + limited_to_segment_id: limited_to_segment_id, + verify_installation?: false, + verification_session: %{} ) end end diff --git a/lib/plausible_web/live/awaiting_pageviews.ex b/lib/plausible_web/live/awaiting_pageviews.ex deleted file mode 100644 index 29c4f3e731d4..000000000000 --- a/lib/plausible_web/live/awaiting_pageviews.ex +++ /dev/null @@ -1,99 +0,0 @@ -defmodule PlausibleWeb.Live.AwaitingPageviews do - @moduledoc """ - A replacement for installation verification on Community Edition. - """ - use PlausibleWeb, :live_view - - import PlausibleWeb.Components.Generic - - def mount( - %{"domain" => domain} = params, - _session, - socket - ) do - current_user = socket.assigns.current_user - - site = - Plausible.Sites.get_for_user!(current_user, domain, - roles: [ - :owner, - :admin, - :editor, - :super_admin, - :viewer - ] - ) - - private = Map.get(socket.private.connect_info, :private, %{}) - - has_pageviews? = has_pageviews?(site) - - socket = - assign(socket, - site: site, - domain: domain, - has_pageviews?: has_pageviews?, - delay: private[:delay] || 500, - flow: params["flow"] || "", - polling_pageviews?: false - ) - - socket = - if has_pageviews? do - redirect_to_stats(socket) - else - schedule_pageviews_check(socket) - end - - {:ok, socket} - end - - def render(assigns) do - ~H""" - - <.awaiting_pageviews /> - """ - end - - defp awaiting_pageviews(assigns) do - ~H""" - <.focus_box> -
-
-

Awaiting your first pageview …

-
- - """ - end - - def handle_info(:check_pageviews, socket) do - socket = - if has_pageviews?(socket.assigns.site) do - redirect_to_stats(socket) - else - socket - |> assign(polling_pageviews?: false) - |> schedule_pageviews_check() - end - - {:noreply, socket} - end - - defp schedule_pageviews_check(socket) do - if socket.assigns.polling_pageviews? do - socket - else - Process.send_after(self(), :check_pageviews, socket.assigns.delay * 2) - assign(socket, polling_pageviews?: true) - end - end - - defp redirect_to_stats(socket) do - stats_url = Routes.stats_path(PlausibleWeb.Endpoint, :stats, socket.assigns.domain, []) - redirect(socket, to: stats_url) - end - - defp has_pageviews?(site) do - Plausible.Stats.Clickhouse.has_pageviews?(site) - end -end diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index ef8113d0a0a8..a4fd779d3880 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -1,7 +1,7 @@ defmodule PlausibleWeb.Live.Components.Verification do @moduledoc """ This component is responsible for rendering the verification progress - and diagnostics. + and diagnostics as a compact banner on top of the dashboard. """ use Phoenix.LiveComponent use Plausible @@ -11,6 +11,17 @@ defmodule PlausibleWeb.Live.Components.Verification do import PlausibleWeb.Components.Generic + @container_id "verification-ui" + # Dismissing hides the banner immediately and strips `verify_installation` + # from the URL (the same param that got it rendered in the first place - + # see PlausibleWeb.StatsController), so a refresh doesn't bring it back. + @dismiss_onclick "document.getElementById('#{@container_id}').classList.add('hidden');" <> + "var u = new window.URL(window.location.href);" <> + "u.searchParams.delete('verify_installation');" <> + "u.searchParams.delete('installation_type');" <> + "u.searchParams.delete('flow');" <> + "window.history.replaceState(null, '', u);" + attr(:domain, :string, required: true) attr(:message, :string, @@ -28,13 +39,28 @@ defmodule PlausibleWeb.Live.Components.Verification do attr(:awaiting_first_pageview?, :boolean, default: false) def render(assigns) do + assigns = + assigns + |> assign(:dismiss_onclick, @dismiss_onclick) + |> assign(:container_id, @container_id) + ~H""" -
+
+ <.render_progress :if={not @finished?} message={@message} /> <.render_success :if={@finished? and @success?} awaiting_first_pageview?={@awaiting_first_pageview?} domain={@domain} + super_admin?={@super_admin?} + verification_state={@verification_state} /> <.render_failed :if={@finished? and not @success?} @@ -43,9 +69,7 @@ defmodule PlausibleWeb.Live.Components.Verification do domain={@domain} flow={@flow} installation_type={@installation_type} - /> - <.render_super_admin_diagnostics - :if={not is_nil(@verification_state) && @super_admin? && @finished?} + super_admin?={@super_admin?} verification_state={@verification_state} />
@@ -54,149 +78,122 @@ defmodule PlausibleWeb.Live.Components.Verification do defp render_progress(assigns) do ~H""" - <.focus_box> -
-
-
-
- <.title>Verifying your installation -

{@message}

-
- + <.notice title="Verifying your installation" theme={:gray}> + <:icon> +
+ +

{@message}

+ """ end defp render_success(assigns) do ~H""" - <.focus_box> -
- -
- -
- <.title>Success! -

- Your installation is working and visitors are being counted accurately. - - Awaiting your first pageview... - -

-
- <.button_link - href={"/#{URI.encode_www_form(@domain)}?skip_to_dashboard=true"} - class="w-full font-bold mb-4" - > - Go to the dashboard - - + <.notice title="Success!" theme={:gray} icon_class="text-green-600 dark:text-green-500"> + <:icon> + + + Your installation is working and visitors are being counted accurately. + + Awaiting your first pageview... + + <.super_admin_diagnostics + :if={@super_admin? and not is_nil(@verification_state)} + verification_state={@verification_state} + /> + """ end defp render_failed(assigns) do ~H""" - <.focus_box> -
- -
- -
- <.title>{List.first(@interpretation.errors)} -

- {List.first(@interpretation.recommendations).text}.  - <.styled_link href={List.first(@interpretation.recommendations).url} new_tab={true}> - Learn more - -

-
- -
- <.button_link mt?={false} href="#" phx-click="retry" class="w-full"> + <.notice + title={ + if @interpretation, + do: List.first(@interpretation.errors), + else: "We couldn't verify your installation" + } + theme={:red} + > + <:icon> + + + <:actions> + <.button_link mt?={false} href="#" phx-click="retry" size="sm"> Verify installation again -
- <:footer> - <.focus_list> - <:item :if={ - @interpretation && is_map(@interpretation.data) && - @interpretation.data[:offer_custom_url_input] + +

+ {List.first(@interpretation.recommendations).text}.  + <.styled_link href={List.first(@interpretation.recommendations).url} new_tab={true}> + Learn more + +

+

+ + Is your website located at a different URL? + <.styled_link href="#" phx-click="show-custom-url-form" id="verify-custom-url-link"> + Click here + + + = 3}> + Need further help with your installation? + <.styled_link href="https://plausible.io/contact"> + Contact us + + + + Need to see installation instructions again? + <.styled_link href={ + Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, + flow: @flow, + installation_type: @installation_type + ) }> - - Is your website located at a different URL? - <.styled_link href={ - Routes.site_path(PlausibleWeb.Endpoint, :verification, @domain, - flow: @flow, - installation_type: @installation_type, - custom_url: true - ) - }> - Click here - - - - <:item :if={ee?() and @attempts >= 3}> - Need further help with your installation? - <.styled_link href="https://plausible.io/contact"> - Contact us - - - <:item> - Need to see installation instructions again? - <.styled_link href={ - Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, - flow: @flow, - installation_type: @installation_type - ) - }> - Click here - - - <:item> - Run verification later and go to site settings? - <.styled_link href={"/#{URI.encode_www_form(@domain)}/settings/general"}> - Click here - - - - - + Click here + + +

+ <.super_admin_diagnostics + :if={@super_admin? and not is_nil(@verification_state)} + verification_state={@verification_state} + /> + """ end - defp render_super_admin_diagnostics(assigns) do + defp super_admin_diagnostics(assigns) do ~H""" - <.focus_box> -
-

- - As a super-admin, you're eligible to see diagnostics details. Click to expand. - -

-
- <.focus_list> - <:item :for={{diag, value} <- Map.from_struct(@verification_state.diagnostics)}> - - {Phoenix.Naming.humanize(diag)}: - {to_string_value(value)} - - - -
+
+

+ + As a super-admin, you're eligible to see diagnostics details. Click to expand. + +

+
+ <.focus_list> + <:item :for={{diag, value} <- Map.from_struct(@verification_state.diagnostics)}> + + {Phoenix.Naming.humanize(diag)}: {to_string_value(value)} + + +
- +
""" end diff --git a/lib/plausible_web/live/installation.ex b/lib/plausible_web/live/installation.ex index 88a16ae2e742..b34c6f11ae22 100644 --- a/lib/plausible_web/live/installation.ex +++ b/lib/plausible_web/live/installation.ex @@ -328,14 +328,21 @@ defmodule PlausibleWeb.Live.Installation do :installation ) - {:noreply, - push_navigate(socket, - to: - Routes.site_path(socket, :verification, socket.assigns.site.domain, - flow: socket.assigns.flow, - installation_type: config.installation_type - ) - )} + domain = socket.assigns.site.domain + + destination = + on_ee do + Routes.stats_path(socket, :stats, domain, + verify_installation: true, + flow: socket.assigns.flow, + installation_type: config.installation_type + ) + else + _need_to_use_variable_on_ce = config + Routes.stats_path(socket, :stats, domain, []) + end + + {:noreply, push_navigate(socket, to: destination)} end defp initialize_installation_data(flow, site, params) do diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index 11217bd370a1..911835aacb1a 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -533,6 +533,13 @@ defmodule PlausibleWeb.Live.Sites do attr(:sparkline, :map, required: true) def site(assigns) do + assigns = + assign( + assigns, + :needs_verification?, + ee?() and is_nil(Plausible.Sites.stats_start_date(assigns.site)) + ) + ~H"""
  • <.unstyled_link - href={Routes.stats_path(PlausibleWeb.Endpoint, :stats, @site.domain, [])} + href={ + Routes.stats_path( + PlausibleWeb.Endpoint, + :stats, + @site.domain, + if(@needs_verification?, + do: [verify_installation: true, flow: PlausibleWeb.Flows.provisioning()], + else: [] + ) + ) + } class="block group-has-[.phx-click-loading]/sort:animate-pulse group-has-[.phx-click-loading]/sort:pointer-events-none" >
    @@ -565,6 +582,12 @@ defmodule PlausibleWeb.Live.Sites do > {@site.domain} + + Setup pending +
  • <.site_stats sparkline={@sparkline} /> diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 6c1439f81f25..b88211a93809 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -602,15 +602,6 @@ defmodule PlausibleWeb.Router do live "/:domain/installation", Installation, :installation, as: :site end - scope assigns: %{ - dogfood_page_path: "/:website/verification" - } do - live "/:domain/verification", - on_ee(do: Verification, else: AwaitingPageviews), - :verification, - as: :site - end - scope assigns: %{ dogfood_page_path: "/:website/change-domain" } do diff --git a/lib/plausible_web/templates/stats/stats.html.heex b/lib/plausible_web/templates/stats/stats.html.heex index 6a8058bb7d73..aceb45a07ff2 100644 --- a/lib/plausible_web/templates/stats/stats.html.heex +++ b/lib/plausible_web/templates/stats/stats.html.heex @@ -57,6 +57,12 @@ data-limited-to-segment-id={Jason.encode!(@limited_to_segment_id)} >
    + <%= if @verify_installation? do %> + {live_render(@conn, PlausibleWeb.Live.Verification, + id: "live-verification", + session: @verification_session + )} + <% end %> <%= if ee?() && !@conn.assigns[:current_user] && @conn.assigns[:demo] do %>
    diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 38155954a91d..fab0eef0ba75 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -104,28 +104,28 @@ defmodule PlausibleWeb.StatsControllerTest do assert resp =~ "Getting started" end - test "public site - redirect to /login when no stats because verification requires it", %{ - conn: conn - } do + test "public site - shows an empty dashboard without stats (no verification banner)", + %{ + conn: conn + } do new_site(domain: "some-other-public-site.io", public: true) - conn = get(conn, conn |> get("/some-other-public-site.io") |> redirected_to()) + resp = get(conn, "/some-other-public-site.io") |> html_response(200) - assert redirected_to(conn) == - Routes.auth_path(conn, :login_form, - return_to: "/some-other-public-site.io/verification" - ) + refute resp =~ "Verifying your installation" end - test "public site - no stats with skip_to_dashboard", %{ - conn: conn - } do + test "public site - anonymous visitors never see the verification banner, even with the param", + %{ + conn: conn + } do new_site(domain: "some-other-public-site.io", public: true) - conn = get(conn, "/some-other-public-site.io?skip_to_dashboard=true") - resp = html_response(conn, 200) + resp = + get(conn, "/some-other-public-site.io?verify_installation=true") |> html_response(200) assert text_of_attr(resp, @react_container, "data-logged-in") == "false" + refute resp =~ "Verifying your installation" end test "can not view stats of a private website", %{conn: conn} do @@ -147,15 +147,18 @@ defmodule PlausibleWeb.StatsControllerTest do assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" end - test "can view stats of a website I've created, enforcing pageviews check skip", %{ - conn: conn, - site: site - } do - resp = conn |> get(conn |> get("/" <> site.domain) |> redirected_to()) |> html_response(200) - refute text_of_attr(resp, @react_container, "data-logged-in") == "true" + test "can view stats of a website I've created; verification banner only shows with the explicit param", + %{ + conn: conn, + site: site + } do + resp = get(conn, "/" <> site.domain) |> html_response(200) + assert text_of_attr(resp, @react_container, "data-logged-in") == "true" + refute resp =~ "Verifying your installation" - resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200) + resp = conn |> get("/" <> site.domain <> "?verify_installation=true") |> html_response(200) assert text_of_attr(resp, @react_container, "data-logged-in") == "true" + assert resp =~ "Verifying your installation" end on_ee do @@ -334,9 +337,9 @@ defmodule PlausibleWeb.StatsControllerTest do end test "does not show CRM link to the site", %{conn: conn, site: site} do - conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to()) + resp = get(conn, "/" <> site.domain) |> html_response(200) - refute html_response(conn, 200) =~ "/cs/sites" + refute resp =~ "/cs/sites" end test "all segments (personal or site) are stuffed into dataset, with their associated owner_id and owner_name", @@ -390,8 +393,8 @@ defmodule PlausibleWeb.StatsControllerTest do test "can enter verification when site is without stats", %{conn: conn} do site = new_site() - conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to()) - assert html_response(conn, 200) =~ "Verifying your installation" + resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) + assert resp =~ "Verifying your installation" end test "can view a private locked dashboard with stats", %{conn: conn} do @@ -410,8 +413,8 @@ defmodule PlausibleWeb.StatsControllerTest do site = new_site(owner: user) site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() - conn = get(conn, conn |> get("/#{site.domain}") |> redirected_to()) - assert html_response(conn, 200) =~ "Verifying your installation" + resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) + assert resp =~ "Verifying your installation" end test "can view a locked public dashboard", %{conn: conn} do @@ -427,9 +430,9 @@ defmodule PlausibleWeb.StatsControllerTest do on_ee do test "shows CRM link to the site", %{conn: conn} do site = new_site() - conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to()) + resp = get(conn, "/" <> site.domain) |> html_response(200) - assert html_response(conn, 200) =~ + assert resp =~ Routes.customer_support_site_path(PlausibleWeb.Endpoint, :show, site.id) end end diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_test.exs index 2645de32cc4e..bf3d6a1a479d 100644 --- a/test/plausible_web/live/components/verification_test.exs +++ b/test/plausible_web/live/components/verification_test.exs @@ -112,7 +112,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|) end - test "renders link to verify installation at a different URL" do + test "renders a click-to-show-form link to verify installation at a different URL" do interpretation = Verification.Checks.interpret_diagnostics(%State{ url: "example.com", @@ -125,9 +125,6 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do assert interpretation.data.offer_custom_url_input == true - expected_link_href = - PlausibleWeb.Router.Helpers.site_path(PlausibleWeb.Endpoint, :verification, "example.com") - html = render_component(@component, domain: "example.com", @@ -136,12 +133,11 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do interpretation: interpretation ) - assert text_of_element(html, "#verify-custom-url-link") =~ "different URL?" - assert text_of_attr(html, "#verify-custom-url-link a", "href") =~ expected_link_href - assert text_of_attr(html, "#verify-custom-url-link a", "href") =~ "custom_url=true" + assert text_of_element(html, "#verify-custom-url-link") =~ "Click here" + assert element_exists?(html, ~s|a#verify-custom-url-link[phx-click="show-custom-url-form"]|) end - test "offers escape paths: settings and installation instructions on failure" do + test "offers an installation-instructions escape path on failure, no more settings link" do html = render_component(@component, domain: "example.com", @@ -151,7 +147,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do flow: PlausibleWeb.Flows.review() ) - assert element_exists?(html, ~s|a[href="/example.com/settings/general"]|) + refute element_exists?(html, ~s|a[href="/example.com/settings/general"]|) assert element_exists?( html, diff --git a/test/plausible_web/live/installation_test.exs b/test/plausible_web/live/installation_test.exs index 84e379b321fa..30d7bf05d325 100644 --- a/test/plausible_web/live/installation_test.exs +++ b/test/plausible_web/live/installation_test.exs @@ -246,11 +246,15 @@ defmodule PlausibleWeb.Live.InstallationTest do {"gtm", "Verify Tag Manager installation"}, {"npm", "Verify NPM installation"} ] do - test "submitting form with #{type} redirects to verification (EE)", %{ - conn: conn, - site: site - } do + test "submitting form with #{type} redirects to the dashboard with the verification banner (EE)", + %{ + conn: conn, + site: site + } do stub_dns() + + stub_lookup_a_records(site.domain) + stub_detection_manual() {lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}") @@ -270,7 +274,8 @@ defmodule PlausibleWeb.Live.InstallationTest do assert_redirect( lv, - Routes.site_path(conn, :verification, site.domain, + Routes.stats_path(conn, :stats, site.domain, + verify_installation: true, flow: "provisioning", installation_type: unquote(type) ) @@ -280,7 +285,10 @@ defmodule PlausibleWeb.Live.InstallationTest do end @tag :ce_build_only - test "submitting the form redirects to verification (CE)", %{conn: conn, site: site} do + test "submitting the form redirects straight to the dashboard, no banner (CE)", %{ + conn: conn, + site: site + } do {lv, _html} = get_lv(conn, site) lv @@ -294,13 +302,7 @@ defmodule PlausibleWeb.Live.InstallationTest do } }) - assert_redirect( - lv, - Routes.site_path(conn, :verification, site.domain, - flow: "provisioning", - installation_type: "manual" - ) - ) + assert_redirect(lv, Routes.stats_path(conn, :stats, site.domain)) end test "404 goal gets created regardless of user options", %{conn: conn, site: site} do @@ -331,10 +333,11 @@ defmodule PlausibleWeb.Live.InstallationTest do assert Enum.any?(goals, &(&1.event_name == "404")) end - test "submitting form with review flow redirects to verification with flow param", %{ - conn: conn, - site: site - } do + test "submitting form with review flow redirects to the dashboard with the flow param preserved", + %{ + conn: conn, + site: site + } do on_ee do stub_dns() stub_detection_manual() @@ -356,13 +359,20 @@ defmodule PlausibleWeb.Live.InstallationTest do } }) - assert_redirect( - lv, - Routes.site_path(conn, :verification, site.domain, - flow: "review", - installation_type: "manual" + on_ee do + assert_redirect( + lv, + Routes.stats_path(conn, :stats, site.domain, + verify_installation: true, + flow: "review", + installation_type: "manual" + ) ) - ) + end + + on_ce do + assert_redirect(lv, Routes.stats_path(conn, :stats, site.domain)) + end end @tag :ee_only diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index 1e2928306459..bde788fa7ddc 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -9,18 +9,21 @@ defmodule PlausibleWeb.Live.VerificationTest do setup [:create_user, :log_in, :create_site] - # @verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]| @retry_button ~s|a[phx-click="retry"]| - # @go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]| @progress ~s|#verification-ui p#progress| @awaiting ~s|#verification-ui span#awaiting| - @heading ~s|#verification-ui h2| + @heading ~s|#verification-ui h3| describe "GET /:domain" do @tag :ee_only - test "static verification screen renders", %{conn: conn, site: site} do + test "static verification banner renders on a freshly provisioned site", %{ + conn: conn, + site: site + } do resp = - get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to) + conn + |> no_slowdown() + |> get("/#{site.domain}?verify_installation=true") |> html_response(200) assert text_of_element(resp, @progress) =~ @@ -30,12 +33,10 @@ defmodule PlausibleWeb.Live.VerificationTest do end @tag :ce_build_only - test "static verification screen renders (ce)", %{conn: conn, site: site} do - resp = - get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to) - |> html_response(200) + test "no verification banner renders on CE", %{conn: conn, site: site} do + resp = get(conn, "/#{site.domain}") |> html_response(200) - assert resp =~ "Awaiting your first pageview …" + refute resp =~ "verification-ui" end end @@ -57,39 +58,35 @@ defmodule PlausibleWeb.Live.VerificationTest do "We're visiting your site to ensure that everything is working" end - @tag :ce_build_only - test "LiveView mounts (ce)", %{conn: conn, site: site} do - {_, html} = get_lv(conn, site) - assert html =~ "Awaiting your first pageview …" - end - @tag :ee_only - test "from custom URL input form to verification", %{conn: conn, site: site} do + test "clicking the custom URL link swaps in the form, submitting kicks off a new run", %{ + conn: conn, + site: site + } do stub_dns() + stub_lookup_a_records(site.domain) + stub_verification_result(%{ "completed" => false, "error" => %{"message" => "Error"} }) - # Get liveview with ?custom_url=true query param - {:ok, lv, html} = - conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true") + {lv, _html} = get_lv(conn, site) verifying_installation_text = "Verifying your installation" - # Assert form is rendered instead of kicking off verification automatically - assert html =~ "Enter Your Custom URL" + html = lv |> render_click("show-custom-url-form") + + assert html =~ "Enter your custom URL" assert html =~ ~s[value="https://#{site.domain}"] assert html =~ ~s[placeholder="https://#{site.domain}"] refute html =~ verifying_installation_text - # Submit custom URL form html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"}) - # Should now show verification progress and hide custom URL form assert html =~ verifying_installation_text - refute html =~ "Enter Your Custom URL" + refute html =~ "Enter your custom URL" end @tag :ee_only @@ -161,12 +158,18 @@ defmodule PlausibleWeb.Live.VerificationTest do html = render(lv) refute text_of_element(html, @awaiting) =~ "Awaiting your first pageview" - refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}/") + refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}") end - test "will redirect when first pageview arrives", %{conn: conn, site: site} do + @tag :ee_only + test "shows success in place (no redirect) once first pageview arrives", %{ + conn: conn, + site: site + } do stub_dns() + stub_lookup_a_records(site.domain) + stub_verification_result(%{ "completed" => true, "trackerIsInHtml" => true, @@ -184,30 +187,19 @@ defmodule PlausibleWeb.Live.VerificationTest do assert eventually(fn -> html = render(lv) - - { - text(html) =~ "Awaiting", - html - } + {text(html) =~ "Awaiting", html} end) populate_stats(site, [ build(:pageview) ]) - assert_redirect(lv, Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain)) - end - - @tag :ce_build_only - test "will redirect when first pageview arrives (ce)", %{conn: conn, site: site} do - {:ok, lv} = kick_off_live_verification(conn, site) - - html = render(lv) - assert text(html) =~ "Awaiting your first pageview …" - - populate_stats(site, [build(:pageview)]) + assert eventually(fn -> + html = render(lv) + {not (text(html) =~ "Awaiting your first pageview"), html} + end) - assert_redirect(lv, Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain)) + refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}") end for {installation_type_param, expected_text, saved_installation_type} <- [ @@ -261,7 +253,7 @@ defmodule PlausibleWeb.Live.VerificationTest do kick_off_live_verification( conn, site, - "?installation_type=#{unquote(installation_type_param)}" + "installation_type=#{unquote(installation_type_param)}" ) assert html = @@ -285,17 +277,31 @@ defmodule PlausibleWeb.Live.VerificationTest do end defp get_lv(conn, site, qs \\ nil) do - {:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification#{qs}") + {:ok, lv, html} = + conn |> no_slowdown() |> as_live() |> live(verification_path(site, qs)) + {lv, html} end defp kick_off_live_verification(conn, site, qs \\ nil) do {:ok, lv, _html} = - conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification#{qs}") + conn |> no_slowdown() |> no_delay() |> as_live() |> live(verification_path(site, qs)) {:ok, lv} end + # `PlausibleWeb.Live.Verification` is rendered via `live_render/3` from a + # plain controller-rendered page rather than a live router route, so it + # never carries the `:live_module` assign `Phoenix.LiveViewTest.live/2` + # looks for. Mirrors the same workaround used in other live_render-embedded + # LiveView tests (e.g. props_settings_test.exs). + defp as_live(conn), do: assign(conn, :live_module, PlausibleWeb.Live.Verification) + + defp verification_path(site, nil), do: "/#{site.domain}?verify_installation=true" + + defp verification_path(site, qs), + do: "/#{site.domain}?verify_installation=true&#{qs}" + defp no_slowdown(conn) do Plug.Conn.put_private(conn, :slowdown, 0) end From 86e09977df837556dcac6cc43ffaa6f29acd28ed Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Wed, 15 Jul 2026 13:13:57 +0100 Subject: [PATCH 02/21] move custom url input into the banner --- extra/lib/plausible_web/live/verification.ex | 36 +------------ .../live/components/verification.ex | 27 +++++++++- .../live/components/verification_test.exs | 35 ++++++++++++ test/plausible_web/live/verification_test.exs | 54 +++++++++++++------ 4 files changed, 100 insertions(+), 52 deletions(-) diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 21e4179aeb2a..7c1a2604f6a4 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -5,8 +5,6 @@ defmodule PlausibleWeb.Live.Verification do """ use PlausibleWeb, :live_view - import PlausibleWeb.Components.Generic - alias Plausible.InstallationSupport.{State, Verification} @component PlausibleWeb.Live.Components.Verification @@ -82,9 +80,7 @@ defmodule PlausibleWeb.Live.Verification do defp verification_content(assigns) do ~H""" - <.custom_url_form :if={@custom_url_input?} domain={@domain} /> <.live_component - :if={not @custom_url_input?} module={@component} installation_type={@installation_type} domain={@domain} @@ -93,6 +89,7 @@ defmodule PlausibleWeb.Live.Verification do flow={@flow} awaiting_first_pageview?={not @has_pageviews?} super_admin?={@super_admin?} + custom_url_input?={@custom_url_input?} /> """ end @@ -254,35 +251,4 @@ defmodule PlausibleWeb.Live.Verification do defp has_pageviews?(site) do Plausible.Stats.Clickhouse.has_pageviews?(site) end - - defp custom_url_form(assigns) do - ~H""" - <.notice title="Enter your custom URL" theme={:gray} class="mb-4"> - <:icon> - - -

    - Please enter the URL where your website with the Plausible script is located. -

    -
    - - - -
    - - """ - end end diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index a4fd779d3880..3692a7c2c75d 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -10,6 +10,7 @@ defmodule PlausibleWeb.Live.Components.Verification do alias Plausible.InstallationSupport.{State, Result} import PlausibleWeb.Components.Generic + import PlausibleWeb.Live.Components.Form @container_id "verification-ui" # Dismissing hides the banner immediately and strips `verify_installation` @@ -37,6 +38,7 @@ defmodule PlausibleWeb.Live.Components.Verification do attr(:flow, :string, default: "") attr(:installation_type, :string, default: nil) attr(:awaiting_first_pageview?, :boolean, default: false) + attr(:custom_url_input?, :boolean, default: false) def render(assigns) do assigns = @@ -71,6 +73,7 @@ defmodule PlausibleWeb.Live.Components.Verification do installation_type={@installation_type} super_admin?={@super_admin?} verification_state={@verification_state} + custom_url_input?={@custom_url_input?} />
    """ @@ -122,9 +125,29 @@ defmodule PlausibleWeb.Live.Components.Verification do /> <:actions> - <.button_link mt?={false} href="#" phx-click="retry" size="sm"> + <.button_link :if={not @custom_url_input?} mt?={false} href="#" phx-click="retry" size="sm"> Verify installation again +
    + <.input + type="url" + name="custom_url" + id="custom_url" + aria-label="Website URL" + required + mt?={false} + width="w-44" + placeholder={"https://#{@domain}"} + value={"https://#{@domain}"} + /> + <.button type="submit" mt?={false} size="sm"> + Verify installation again + +

    {List.first(@interpretation.recommendations).text}.  @@ -134,7 +157,7 @@ defmodule PlausibleWeb.Live.Components.Verification do

    Is your website located at a different URL? diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_test.exs index bf3d6a1a479d..f897ecbfbe7e 100644 --- a/test/plausible_web/live/components/verification_test.exs +++ b/test/plausible_web/live/components/verification_test.exs @@ -137,6 +137,41 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do assert element_exists?(html, ~s|a#verify-custom-url-link[phx-click="show-custom-url-form"]|) end + test "renders the custom URL input inline, retry button becomes the form's submit button, hides the prompt link" do + interpretation = + Verification.Checks.interpret_diagnostics(%State{ + url: "example.com", + diagnostics: %Verification.Diagnostics{ + plausible_is_on_window: false, + plausible_is_initialized: false, + service_error: %{code: :domain_not_found} + } + }) + + html = + render_component(@component, + domain: "example.com", + finished?: true, + success?: false, + interpretation: interpretation, + custom_url_input?: true + ) + + refute element_exists?(html, "#verify-custom-url-link") + refute element_exists?(html, ~s|a[phx-click="retry"]|) + + assert text_of_element(html, ~s|form[phx-submit="verify-custom-url"] button[type="submit"]|) =~ + "Verify installation again" + + assert element_exists?( + html, + ~s|form[phx-submit="verify-custom-url"] input[name="custom_url"]| + ) + + assert text_of_attr(html, ~s|form[phx-submit="verify-custom-url"] input|, "value") =~ + "https://example.com" + end + test "offers an installation-instructions escape path on failure, no more settings link" do html = render_component(@component, diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index bde788fa7ddc..841b2d7e6ab9 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -14,6 +14,8 @@ defmodule PlausibleWeb.Live.VerificationTest do @awaiting ~s|#verification-ui span#awaiting| @heading ~s|#verification-ui h3| + @in_progress_text "Verifying your installation" + describe "GET /:domain" do @tag :ee_only test "static verification banner renders on a freshly provisioned site", %{ @@ -29,7 +31,7 @@ defmodule PlausibleWeb.Live.VerificationTest do assert text_of_element(resp, @progress) =~ "We're visiting your site to ensure that everything is working" - assert resp =~ "Verifying your installation" + assert resp =~ @in_progress_text end @tag :ce_build_only @@ -52,41 +54,63 @@ defmodule PlausibleWeb.Live.VerificationTest do {_, html} = get_lv(conn, site) - assert html =~ "Verifying your installation" + assert html =~ @in_progress_text assert text_of_element(html, @progress) =~ "We're visiting your site to ensure that everything is working" end @tag :ee_only - test "clicking the custom URL link swaps in the form, submitting kicks off a new run", %{ - conn: conn, - site: site - } do + test "clicking the custom URL link reveals an inline form next to the retry button, submitting kicks off a new run", + %{ + conn: conn, + site: site + } do stub_dns() stub_lookup_a_records(site.domain) stub_verification_result(%{ - "completed" => false, - "error" => %{"message" => "Error"} + "completed" => true, + "trackerIsInHtml" => false, + "plausibleIsOnWindow" => false, + "plausibleIsInitialized" => false }) - {lv, _html} = get_lv(conn, site) + {:ok, lv} = kick_off_live_verification(conn, site) - verifying_installation_text = "Verifying your installation" + assert eventually(fn -> + html = render(lv) + + { + text_of_element(html, @heading) =~ "We couldn't detect Plausible on your site", + html + } + end) html = lv |> render_click("show-custom-url-form") - assert html =~ "Enter your custom URL" + refute html =~ @in_progress_text + + refute element_exists?(html, @retry_button) + refute element_exists?(html, "#verify-custom-url-link") + + assert element_exists?( + html, + ~s|form[phx-submit="verify-custom-url"] input[name="custom_url"]| + ) + assert html =~ ~s[value="https://#{site.domain}"] assert html =~ ~s[placeholder="https://#{site.domain}"] - refute html =~ verifying_installation_text - html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"}) + lv + |> element("form[phx-submit='verify-custom-url']") + |> render_submit(%{"custom_url" => "https://abc.de"}) - assert html =~ verifying_installation_text - refute html =~ "Enter your custom URL" + assert eventually(fn -> + html = render(lv) + {html =~ @in_progress_text, html} + end) end @tag :ee_only From 9f9d928a7e9254d01373c06e6b14b2fbd07a82c6 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Wed, 15 Jul 2026 14:01:05 +0100 Subject: [PATCH 03/21] improve notice styles --- extra/lib/plausible_web/live/verification.ex | 2 +- .../live/components/verification.ex | 100 ++++++++++-------- .../live/components/verification_test.exs | 12 +-- 3 files changed, 61 insertions(+), 53 deletions(-) diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 7c1a2604f6a4..38330c26fff8 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -8,7 +8,7 @@ defmodule PlausibleWeb.Live.Verification do alias Plausible.InstallationSupport.{State, Verification} @component PlausibleWeb.Live.Components.Verification - @slowdown_for_frequent_checking :timer.seconds(5) + @slowdown_for_frequent_checking :timer.seconds(0) @use_portal? Mix.env() not in [:test, :ce_test] def mount( diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index 3692a7c2c75d..466e23678f50 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -83,7 +83,9 @@ defmodule PlausibleWeb.Live.Components.Verification do ~H""" <.notice title="Verifying your installation" theme={:gray}> <:icon> -

    +
    +
    +

    {@message}

    @@ -116,17 +118,27 @@ defmodule PlausibleWeb.Live.Components.Verification do do: List.first(@interpretation.errors), else: "We couldn't verify your installation" } - theme={:red} + theme={:yellow} > <:icon> - + - <:actions> - <.button_link :if={not @custom_url_input?} mt?={false} href="#" phx-click="retry" size="sm"> - Verify installation again +

    + {List.first(@interpretation.recommendations).text}.  + <.styled_link href={List.first(@interpretation.recommendations).url} new_tab={true}> + Learn more + +

    +
    + <.button_link + :if={not @custom_url_input?} + mt?={false} + href="#" + phx-click="retry" + theme="secondary" + size="sm" + > + Check again
    - <.button type="submit" mt?={false} size="sm"> - Verify installation again + <.button type="submit" mt?={false} theme="secondary" size="sm"> + Check again
    - -

    - {List.first(@interpretation.recommendations).text}.  - <.styled_link href={List.first(@interpretation.recommendations).url} new_tab={true}> - Learn more - -

    -

    - - Is your website located at a different URL? - <.styled_link href="#" phx-click="show-custom-url-form" id="verify-custom-url-link"> - Click here - - - = 3}> - Need further help with your installation? - <.styled_link href="https://plausible.io/contact"> - Contact us - - - - Need to see installation instructions again? - <.styled_link href={ - Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, - flow: @flow, - installation_type: @installation_type - ) +

    +
    +
      +
    • - Click here - - -

      + Is your website located at a different URL? + <.styled_link href="#" phx-click="show-custom-url-form" id="verify-custom-url-link"> + Click here + +
    • +
    • = 3}> + Need further help with your installation? + <.styled_link href="https://plausible.io/contact"> + Contact us + +
    • +
    • + Need to see installation instructions again? + <.styled_link href={ + Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, + flow: @flow, + installation_type: @installation_type + ) + }> + Click here + +
    • +
    +
    <.super_admin_diagnostics :if={@super_admin? and not is_nil(@verification_state)} verification_state={@verification_state} @@ -194,7 +202,7 @@ defmodule PlausibleWeb.Live.Components.Verification do defp super_admin_diagnostics(assigns) do ~H"""
    diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_test.exs index f897ecbfbe7e..a9fda0c55af4 100644 --- a/test/plausible_web/live/components/verification_test.exs +++ b/test/plausible_web/live/components/verification_test.exs @@ -11,7 +11,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do @component PlausibleWeb.Live.Components.Verification @progress ~s|#verification-ui p#progress| - @pulsating_circle ~s|div#verification-ui div.pulsating-circle| + @loading_spinner ~s|div#verification-ui div.loading| @check_circle ~s|div#verification-ui #check-circle| @error_circle ~s|div#verification-ui #error-circle| @recommendations ~s|#recommendation| @@ -24,8 +24,8 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do assert text_of_element(html, @progress) == "We're visiting your site to ensure that everything is working" - assert element_exists?(html, @pulsating_circle) - refute class_of_element(html, @pulsating_circle) =~ "hidden" + assert element_exists?(html, @loading_spinner) + refute class_of_element(html, @loading_spinner) =~ "hidden" refute element_exists?(html, @recommendations) refute element_exists?(html, @check_circle) refute element_exists?(html, @super_admin_report) @@ -33,7 +33,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do test "renders error badge on error" do html = render_component(@component, domain: "example.com", success?: false, finished?: true) - refute element_exists?(html, @pulsating_circle) + refute element_exists?(html, @loading_spinner) refute element_exists?(html, @check_circle) refute element_exists?(html, @recommendations) assert element_exists?(html, @error_circle) @@ -92,7 +92,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do finished?: true ) - refute element_exists?(html, @pulsating_circle) + refute element_exists?(html, @loading_spinner) assert element_exists?(html, @check_circle) end @@ -161,7 +161,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do refute element_exists?(html, ~s|a[phx-click="retry"]|) assert text_of_element(html, ~s|form[phx-submit="verify-custom-url"] button[type="submit"]|) =~ - "Verify installation again" + "Check again" assert element_exists?( html, From af33d2fbe0b5ae94c3d645958ea319fed2140fbb Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 16 Jul 2026 13:23:38 +0100 Subject: [PATCH 04/21] expandable instructions --- extra/lib/plausible_web/live/verification.ex | 5 + lib/plausible_web/components/generic.ex | 32 +++ .../live/components/verification.ex | 230 +++++++++++++----- .../live/installation/instructions.ex | 102 ++++---- 4 files changed, 258 insertions(+), 111 deletions(-) diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 38330c26fff8..2fa2b5dd2392 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -36,6 +36,9 @@ defmodule PlausibleWeb.Live.Verification do super_admin? = Plausible.Auth.super_admin?(current_user) has_pageviews? = has_pageviews?(site) + tracker_script_configuration = + PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site) + socket = assign(socket, url_to_verify: nil, @@ -44,6 +47,7 @@ defmodule PlausibleWeb.Live.Verification do domain: domain, has_pageviews?: has_pageviews?, component: @component, + tracker_script_configuration: tracker_script_configuration, installation_type: get_installation_type(session["installation_type"], site), report_to: self(), delay: private[:delay] || 500, @@ -90,6 +94,7 @@ defmodule PlausibleWeb.Live.Verification do awaiting_first_pageview?={not @has_pageviews?} super_admin?={@super_admin?} custom_url_input?={@custom_url_input?} + tracker_script_configuration={@tracker_script_configuration} /> """ end diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex index ffdf5277c06a..2dd458386723 100644 --- a/lib/plausible_web/components/generic.ex +++ b/lib/plausible_web/components/generic.ex @@ -850,6 +850,38 @@ defmodule PlausibleWeb.Components.Generic do """ end + attr :id, :string, required: true + attr :text, :string, required: true + attr :rows, :integer, required: true + attr :resizable, :boolean, default: false + + def copyable_readonly_text_area(assigns) do + ~H""" +
    + + + + + + COPY + + +
    + """ + end + slot :title slot :subtitle slot :inner_block, required: true diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index 466e23678f50..f55c66a10a57 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -7,7 +7,10 @@ defmodule PlausibleWeb.Live.Components.Verification do use Plausible alias PlausibleWeb.Router.Helpers, as: Routes + alias PlausibleWeb.Components.Icons + alias PlausibleWeb.Live.Installation.Instructions alias Plausible.InstallationSupport.{State, Result} + alias Plausible.Site.TrackerScriptConfiguration import PlausibleWeb.Components.Generic import PlausibleWeb.Live.Components.Form @@ -39,6 +42,7 @@ defmodule PlausibleWeb.Live.Components.Verification do attr(:installation_type, :string, default: nil) attr(:awaiting_first_pageview?, :boolean, default: false) attr(:custom_url_input?, :boolean, default: false) + attr(:tracker_script_configuration, TrackerScriptConfiguration, default: nil) def render(assigns) do assigns = @@ -74,6 +78,7 @@ defmodule PlausibleWeb.Live.Components.Verification do super_admin?={@super_admin?} verification_state={@verification_state} custom_url_input?={@custom_url_input?} + tracker_script_configuration={@tracker_script_configuration} />
    """ @@ -111,6 +116,14 @@ defmodule PlausibleWeb.Live.Components.Verification do end defp render_failed(assigns) do + assigns = + assign( + assigns, + :expandable_instructions?, + assigns.installation_type in ["manual", "gtm"] and + not is_nil(assigns.tracker_script_configuration) + ) + ~H""" <.notice title={ @@ -129,68 +142,32 @@ defmodule PlausibleWeb.Live.Components.Verification do Learn more

    -
    - <.button_link - :if={not @custom_url_input?} - mt?={false} - href="#" - phx-click="retry" - theme="secondary" - size="sm" - > - Check again - -
    - <.input - type="url" - name="custom_url" - id="custom_url" - aria-label="Website URL" - required - mt?={false} - width="w-44" - placeholder={"https://#{@domain}"} - value={"https://#{@domain}"} +
    +
    + <.retry_form_or_button custom_url_input?={@custom_url_input?} domain={@domain} /> + <.expand_installation_instructions_button + :if={@expandable_instructions?} + installation_type={@installation_type} /> - <.button type="submit" mt?={false} theme="secondary" size="sm"> - Check again - - -
    -
    -
      -
    • - Is your website located at a different URL? - <.styled_link href="#" phx-click="show-custom-url-form" id="verify-custom-url-link"> - Click here - -
    • -
    • = 3}> - Need further help with your installation? - <.styled_link href="https://plausible.io/contact"> - Contact us - -
    • -
    • - Need to see installation instructions again? - <.styled_link href={ - Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, - flow: @flow, - installation_type: @installation_type - ) - }> - Click here - -
    • -
    + <.review_instructions_link + :if={@installation_type in ["wordpress", "npm"]} + installation_type={@installation_type} + /> +
    + <.expandable_installation_instructions + :if={@expandable_instructions?} + installation_type={@installation_type} + tracker_script_configuration={@tracker_script_configuration} + />
    + <.additional_help_links + custom_url_input?={@custom_url_input?} + interpretation={@interpretation} + attempts={@attempts} + domain={@domain} + flow={@flow} + installation_type={@installation_type} + /> <.super_admin_diagnostics :if={@super_admin? and not is_nil(@verification_state)} verification_state={@verification_state} @@ -199,6 +176,131 @@ defmodule PlausibleWeb.Live.Components.Verification do """ end + defp retry_form_or_button(%{custom_url_input?: true} = assigns) do + ~H""" +
    + <.input + type="url" + name="custom_url" + id="custom_url" + aria-label="Website URL" + required + mt?={false} + width="w-44" + placeholder={"https://#{@domain}"} + value={"https://#{@domain}"} + /> + <.button type="submit" mt?={false} theme="secondary" size="sm"> + Check again + +
    + """ + end + + defp retry_form_or_button(assigns) do + ~H""" + <.button_link + mt?={false} + href="#" + phx-click="retry" + theme="secondary" + size="sm" + > + Check again + + """ + end + + defp expand_installation_instructions_button(assigns) do + ~H""" + + <.button_link + mt?={false} + href="#" + theme="ghost" + size="sm" + class="hover:bg-gray-900/10 dark:hover:bg-white/10 hover:border-transparent dark:hover:border-transparent" + > + + {review_instructions_label(@installation_type)} + + + + + + + + + + """ + end + + defp review_instructions_link(assigns) do + ~H""" + <.button_link + mt?={false} + href={install_help_href(@installation_type)} + theme="ghost" + size="sm" + target="_blank" + rel="noopener noreferrer" + class="hover:bg-gray-900/10 dark:hover:bg-white/10 hover:border-transparent dark:hover:border-transparent" + > + Review instructions + + """ + end + + defp expandable_installation_instructions(assigns) do + ~H""" +
    + + +
    + """ + end + + defp additional_help_links(assigns) do + ~H""" +
    +
      +
    • + Is your website located at a different URL? + <.styled_link href="#" phx-click="show-custom-url-form" id="verify-custom-url-link"> + Click here + +
    • +
    • = 3}> + Need further help with your installation? + <.styled_link href="https://plausible.io/contact"> + Contact us + +
    • +
    • + Want to choose another installation method? + <.styled_link href={ + Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, + flow: @flow, + installation_type: @installation_type + ) + }> + Click here + +
    • +
    +
    + """ + end + defp super_admin_diagnostics(assigns) do ~H"""
    - <.snippet_form - text={render_snippet(@tracker_script_configuration_form.data)} - rows={6} - resizable={true} - /> + <.copy_snippet_box tracker_script_configuration={@tracker_script_configuration_form.data} /> <.h2 class="mt-8 text-sm font-medium">Optional measurements <.script_config_control field={@tracker_script_configuration_form[:outbound_links]} @@ -129,38 +127,56 @@ defmodule PlausibleWeb.Live.Installation.Instructions do Tag Manager installation
    - - We've detected your website is using Google Tag Manager. Here's how to integrate Plausible: - - - Using Google Tag Manager? Here's how to integrate Plausible: - -
    - <.focus_list> - <:item> - Copy your site's Script ID: - <.snippet_form - text={@tracker_script_configuration_form.data.id} - rows={1} - resizable={false} - /> - + <.gtm_instructions_content + recommended_installation_type={@recommended_installation_type} + tracker_script_configuration={@tracker_script_configuration_form.data} + /> +
    + """ + end - <:item> - <.styled_link href="https://plausible.io/gtm-template" new_tab={true}> - Install the Plausible template in GTM - - + attr :recommended_installation_type, :string, required: true + attr :tracker_script_configuration, TrackerScriptConfiguration, required: true - <:item> - Paste your Script ID into the template and click the button below to verify your installation. - - -
    + def gtm_instructions_content(assigns) do + ~H""" + + We've detected your website is using Google Tag Manager. Here's how to integrate Plausible: + + + Using Google Tag Manager? Here's how to integrate Plausible: + +
    + <.gtm_instructions_content_inner tracker_script_configuration={@tracker_script_configuration} />
    """ end + def gtm_instructions_content_inner(assigns) do + ~H""" + <.copyable_readonly_text_area + id="script-config-id" + text={@tracker_script_configuration.id} + rows={1} + /> + <.focus_list> + <:item> + Copy your site's Script ID from above + + + <:item> + <.styled_link href="https://plausible.io/gtm-template" new_tab={true}> + Install the Plausible template in GTM + + + + <:item> + Paste your Script ID into the template + + + """ + end + def npm_instructions(assigns) do ~H""" <.title class="my-4"> @@ -236,27 +252,11 @@ defmodule PlausibleWeb.Live.Installation.Instructions do """ end - defp snippet_form(assigns) do - ~H""" -
    - + def copy_snippet_box(assigns) do + assigns = assign(assigns, :text, render_snippet(assigns.tracker_script_configuration)) - - - - COPY - - -
    + ~H""" + <.copyable_readonly_text_area id="snippet" text={@text} rows={6} resizable={true} /> """ end From c60188ea9195900fa0fcb57e0874595a6076c686 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 16 Jul 2026 13:45:32 +0100 Subject: [PATCH 05/21] stop polling pageviews --- extra/lib/plausible_web/live/verification.ex | 34 ------- lib/plausible/stats/clickhouse.ex | 16 ---- .../live/components/verification.ex | 5 - test/plausible_web/live/verification_test.exs | 91 +------------------ 4 files changed, 1 insertion(+), 145 deletions(-) diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 2fa2b5dd2392..2f1b2bc13bbe 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -34,7 +34,6 @@ defmodule PlausibleWeb.Live.Verification do private = Map.get(socket.private.connect_info, :private, %{}) super_admin? = Plausible.Auth.super_admin?(current_user) - has_pageviews? = has_pageviews?(site) tracker_script_configuration = PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site) @@ -45,7 +44,6 @@ defmodule PlausibleWeb.Live.Verification do site: site, super_admin?: super_admin?, domain: domain, - has_pageviews?: has_pageviews?, component: @component, tracker_script_configuration: tracker_script_configuration, installation_type: get_installation_type(session["installation_type"], site), @@ -55,7 +53,6 @@ defmodule PlausibleWeb.Live.Verification do flow: session["flow"] || "", checks_pid: nil, attempts: 0, - polling_pageviews?: false, custom_url_input?: false ) @@ -91,7 +88,6 @@ defmodule PlausibleWeb.Live.Verification do id="verification-standalone" attempts={@attempts} flow={@flow} - awaiting_first_pageview?={not @has_pageviews?} super_admin?={@super_admin?} custom_url_input?={@custom_url_input?} tracker_script_configuration={@tracker_script_configuration} @@ -170,10 +166,6 @@ defmodule PlausibleWeb.Live.Verification do def handle_info({:all_checks_done, %State{} = state}, socket) do interpretation = Verification.Checks.interpret_diagnostics(state) - if not socket.assigns.has_pageviews? do - schedule_pageviews_check(socket) - end - update_component(socket, finished?: true, success?: interpretation.ok?, @@ -184,19 +176,6 @@ defmodule PlausibleWeb.Live.Verification do {:noreply, assign(socket, checks_pid: nil)} end - def handle_info(:check_pageviews, socket) do - if has_pageviews?(socket.assigns.site) do - {:noreply, assign(socket, has_pageviews?: true, polling_pageviews?: false)} - else - socket = - socket - |> assign(polling_pageviews?: false) - |> schedule_pageviews_check() - - {:noreply, socket} - end - end - @supported_installation_types_atoms PlausibleWeb.Tracker.supported_installation_types() |> Enum.map(&String.to_atom/1) defp get_installation_type(installation_type, site) do @@ -222,15 +201,6 @@ defmodule PlausibleWeb.Live.Verification do end end - defp schedule_pageviews_check(socket) do - if socket.assigns.polling_pageviews? do - socket - else - Process.send_after(self(), :check_pageviews, socket.assigns.delay * 2) - assign(socket, polling_pageviews?: true) - end - end - defp reset_component(socket) do update_component(socket, message: "We're visiting your site to ensure that everything is working", @@ -252,8 +222,4 @@ defmodule PlausibleWeb.Live.Verification do defp launch_delayed(socket) do Process.send_after(self(), {:start, socket.assigns.report_to}, socket.assigns.delay) end - - defp has_pageviews?(site) do - Plausible.Stats.Clickhouse.has_pageviews?(site) - end end diff --git a/lib/plausible/stats/clickhouse.ex b/lib/plausible/stats/clickhouse.ex index 375c9e9f47fc..fadd473f867d 100644 --- a/lib/plausible/stats/clickhouse.ex +++ b/lib/plausible/stats/clickhouse.ex @@ -191,20 +191,4 @@ defmodule Plausible.Stats.Clickhouse do def current_visitors_12h(site) do Stats.current_visitors(site, Duration.new!(hour: -12)) end - - def has_pageviews?(site) do - # This function is currently only used in installation verification - # which is not accessible for consolidated views. - true = Plausible.Sites.regular?(site) - - ClickhouseRepo.exists?( - from(e in "events_v2", - where: - e.site_id == ^site.id and - e.name == "pageview" and - e.timestamp >= - ^site.native_stats_start_at - ) - ) - end end diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index f55c66a10a57..c14dec4df100 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -40,7 +40,6 @@ defmodule PlausibleWeb.Live.Components.Verification do attr(:attempts, :integer, default: 0) attr(:flow, :string, default: "") attr(:installation_type, :string, default: nil) - attr(:awaiting_first_pageview?, :boolean, default: false) attr(:custom_url_input?, :boolean, default: false) attr(:tracker_script_configuration, TrackerScriptConfiguration, default: nil) @@ -63,7 +62,6 @@ defmodule PlausibleWeb.Live.Components.Verification do <.render_progress :if={not @finished?} message={@message} /> <.render_success :if={@finished? and @success?} - awaiting_first_pageview?={@awaiting_first_pageview?} domain={@domain} super_admin?={@super_admin?} verification_state={@verification_state} @@ -104,9 +102,6 @@ defmodule PlausibleWeb.Live.Components.Verification do Your installation is working and visitors are being counted accurately. - - Awaiting your first pageview... - <.super_admin_diagnostics :if={@super_admin? and not is_nil(@verification_state)} verification_state={@verification_state} diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index 841b2d7e6ab9..48134b7a42b9 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -11,7 +11,6 @@ defmodule PlausibleWeb.Live.VerificationTest do @retry_button ~s|a[phx-click="retry"]| @progress ~s|#verification-ui p#progress| - @awaiting ~s|#verification-ui span#awaiting| @heading ~s|#verification-ui h3| @in_progress_text "Verifying your installation" @@ -134,96 +133,8 @@ defmodule PlausibleWeb.Live.VerificationTest do assert eventually(fn -> html = render(lv) - - { - text_of_element(html, @awaiting) =~ - "Awaiting your first pageview", - html - } + {html =~ "Success!", html} end) - - html = render(lv) - assert html =~ "Success!" - assert html =~ "Awaiting your first pageview" - end - - @tag :ee_only - test "won't await first pageview if site has pageviews", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview) - ]) - - stub_dns() - - stub_verification_result(%{ - "completed" => true, - "trackerIsInHtml" => true, - "plausibleIsOnWindow" => true, - "plausibleIsInitialized" => true, - "testEvent" => %{ - "normalizedBody" => %{ - "domain" => site.domain - }, - "responseStatus" => 200 - } - }) - - {:ok, lv} = kick_off_live_verification(conn, site) - - assert eventually(fn -> - html = render(lv) - - { - text(html) =~ "Success", - html - } - end) - - html = render(lv) - - refute text_of_element(html, @awaiting) =~ "Awaiting your first pageview" - refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}") - end - - @tag :ee_only - test "shows success in place (no redirect) once first pageview arrives", %{ - conn: conn, - site: site - } do - stub_dns() - - stub_lookup_a_records(site.domain) - - stub_verification_result(%{ - "completed" => true, - "trackerIsInHtml" => true, - "plausibleIsOnWindow" => true, - "plausibleIsInitialized" => true, - "testEvent" => %{ - "normalizedBody" => %{ - "domain" => site.domain - }, - "responseStatus" => 200 - } - }) - - {:ok, lv} = kick_off_live_verification(conn, site) - - assert eventually(fn -> - html = render(lv) - {text(html) =~ "Awaiting", html} - end) - - populate_stats(site, [ - build(:pageview) - ]) - - assert eventually(fn -> - html = render(lv) - {not (text(html) =~ "Awaiting your first pageview"), html} - end) - - refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}") end for {installation_type_param, expected_text, saved_installation_type} <- [ From 2963c20526c6cb924f39d665155fc16c9bea5df7 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 16 Jul 2026 14:33:15 +0100 Subject: [PATCH 06/21] rely on tracker_script_config object for installation_method --- extra/lib/plausible_web/live/verification.ex | 28 ++----- .../controllers/stats_controller.ex | 3 +- .../live/components/verification.ex | 7 +- lib/plausible_web/live/installation.ex | 11 +-- .../live/components/verification_test.exs | 2 +- test/plausible_web/live/installation_test.exs | 6 +- test/plausible_web/live/verification_test.exs | 77 +++++++------------ 7 files changed, 41 insertions(+), 93 deletions(-) diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 2f1b2bc13bbe..e4ed5d8e91db 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -46,7 +46,6 @@ defmodule PlausibleWeb.Live.Verification do domain: domain, component: @component, tracker_script_configuration: tracker_script_configuration, - installation_type: get_installation_type(session["installation_type"], site), report_to: self(), delay: private[:delay] || 500, slowdown: private[:slowdown] || 500, @@ -83,7 +82,7 @@ defmodule PlausibleWeb.Live.Verification do ~H""" <.live_component module={@component} - installation_type={@installation_type} + installation_type={get_installation_type(@tracker_script_configuration)} domain={@domain} id="verification-standalone" attempts={@attempts} @@ -139,7 +138,7 @@ defmodule PlausibleWeb.Live.Verification do Verification.Checks.run( socket.assigns.url_to_verify, domain, - socket.assigns.installation_type, + get_installation_type(socket.assigns.tracker_script_configuration), report_to: report_to, slowdown: socket.assigns.slowdown ) @@ -178,26 +177,13 @@ defmodule PlausibleWeb.Live.Verification do @supported_installation_types_atoms PlausibleWeb.Tracker.supported_installation_types() |> Enum.map(&String.to_atom/1) - defp get_installation_type(installation_type, site) do - cond do - installation_type in PlausibleWeb.Tracker.supported_installation_types() -> - installation_type - - (saved_installation_type = get_saved_installation_type(site)) in @supported_installation_types_atoms -> - Atom.to_string(saved_installation_type) - - true -> - PlausibleWeb.Tracker.fallback_installation_type() - end - end - - defp get_saved_installation_type(site) do - case PlausibleWeb.Tracker.get_tracker_script_configuration(site) do - %{installation_type: installation_type} -> - installation_type + defp get_installation_type(tracker_script_configuration) do + case tracker_script_configuration.installation_type do + type when type in @supported_installation_types_atoms -> + Atom.to_string(type) _ -> - nil + PlausibleWeb.Tracker.fallback_installation_type() end end diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index f96cd2f94855..a0abc0f9665a 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -118,8 +118,7 @@ defmodule PlausibleWeb.StatsController do verify_installation?: verify_installation?, verification_session: %{ "domain" => site.domain, - "flow" => conn.params["flow"], - "installation_type" => conn.params["installation_type"] + "flow" => conn.params["flow"] } ) end diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index c14dec4df100..905539a2643d 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -22,7 +22,6 @@ defmodule PlausibleWeb.Live.Components.Verification do @dismiss_onclick "document.getElementById('#{@container_id}').classList.add('hidden');" <> "var u = new window.URL(window.location.href);" <> "u.searchParams.delete('verify_installation');" <> - "u.searchParams.delete('installation_type');" <> "u.searchParams.delete('flow');" <> "window.history.replaceState(null, '', u);" @@ -161,7 +160,6 @@ defmodule PlausibleWeb.Live.Components.Verification do attempts={@attempts} domain={@domain} flow={@flow} - installation_type={@installation_type} /> <.super_admin_diagnostics :if={@super_admin? and not is_nil(@verification_state)} @@ -283,10 +281,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
  • Want to choose another installation method? <.styled_link href={ - Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, - flow: @flow, - installation_type: @installation_type - ) + Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, flow: @flow) }> Click here diff --git a/lib/plausible_web/live/installation.ex b/lib/plausible_web/live/installation.ex index b34c6f11ae22..f88e6aaf1a79 100644 --- a/lib/plausible_web/live/installation.ex +++ b/lib/plausible_web/live/installation.ex @@ -321,12 +321,7 @@ defmodule PlausibleWeb.Live.Installation do end def handle_event("submit", %{"tracker_script_configuration" => params}, socket) do - config = - PlausibleWeb.Tracker.update_script_configuration!( - socket.assigns.site, - params, - :installation - ) + PlausibleWeb.Tracker.update_script_configuration!(socket.assigns.site, params, :installation) domain = socket.assigns.site.domain @@ -334,11 +329,9 @@ defmodule PlausibleWeb.Live.Installation do on_ee do Routes.stats_path(socket, :stats, domain, verify_installation: true, - flow: socket.assigns.flow, - installation_type: config.installation_type + flow: socket.assigns.flow ) else - _need_to_use_variable_on_ce = config Routes.stats_path(socket, :stats, domain, []) end diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_test.exs index a9fda0c55af4..da442e8de625 100644 --- a/test/plausible_web/live/components/verification_test.exs +++ b/test/plausible_web/live/components/verification_test.exs @@ -186,7 +186,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do assert element_exists?( html, - ~s|a[href="/example.com/installation?flow=review&installation_type=wordpress"]| + ~s|a[href="/example.com/installation?flow=review"]| ) end end diff --git a/test/plausible_web/live/installation_test.exs b/test/plausible_web/live/installation_test.exs index 30d7bf05d325..763b6dfdfac9 100644 --- a/test/plausible_web/live/installation_test.exs +++ b/test/plausible_web/live/installation_test.exs @@ -276,8 +276,7 @@ defmodule PlausibleWeb.Live.InstallationTest do lv, Routes.stats_path(conn, :stats, site.domain, verify_installation: true, - flow: "provisioning", - installation_type: unquote(type) + flow: "provisioning" ) ) end @@ -364,8 +363,7 @@ defmodule PlausibleWeb.Live.InstallationTest do lv, Routes.stats_path(conn, :stats, site.domain, verify_installation: true, - flow: "review", - installation_type: "manual" + flow: "review" ) ) end diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index 48134b7a42b9..7843fbb873ce 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -137,34 +137,21 @@ defmodule PlausibleWeb.Live.VerificationTest do end) end - for {installation_type_param, expected_text, saved_installation_type} <- [ - {"manual", - "Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", - nil}, - {"npm", - "Please make sure you've initialized Plausible on your site, or verify your installation manually.", - nil}, - {"gtm", - "Please make sure you've configured the GTM template correctly, or verify your installation manually.", - nil}, - {"wordpress", - "Please make sure you've enabled the plugin, or verify your installation manually.", - nil}, - # trusts param over saved installation type - {"wordpress", - "Please make sure you've enabled the plugin, or verify your installation manually.", + for {expected_text, saved_installation_type} <- [ + {"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", + "manual"}, + {"Please make sure you've initialized Plausible on your site, or verify your installation manually.", "npm"}, - # falls back to saved installation type if no param - {"", - "Please make sure you've initialized Plausible on your site, or verify your installation manually.", - "npm"}, - # falls back to manual if no param and no saved installation type - {"", - "Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", + {"Please make sure you've configured the GTM template correctly, or verify your installation manually.", + "gtm"}, + {"Please make sure you've enabled the plugin, or verify your installation manually.", + "wordpress"}, + # falls back to manual when there's no saved installation type + {"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", nil} ] do @tag :ee_only - test "eventually fails to verify installation (?installation_type=#{installation_type_param}) if saved installation type is #{inspect(saved_installation_type)}", + test "eventually fails to verify installation if saved installation type is #{inspect(saved_installation_type)}", %{ conn: conn, site: site @@ -184,24 +171,17 @@ defmodule PlausibleWeb.Live.VerificationTest do }) end - {:ok, lv} = - kick_off_live_verification( - conn, - site, - "installation_type=#{unquote(installation_type_param)}" - ) - - assert html = - eventually(fn -> - html = render(lv) - {html =~ "", html} - - { - text_of_element(html, @heading) =~ - "We couldn't detect Plausible on your site", - html - } - end) + {:ok, lv} = kick_off_live_verification(conn, site) + + html = + eventually(fn -> + html = render(lv) + + { + text_of_element(html, @heading) =~ "We couldn't detect Plausible on your site", + html + } + end) assert element_exists?(html, @retry_button) @@ -211,16 +191,16 @@ defmodule PlausibleWeb.Live.VerificationTest do end end - defp get_lv(conn, site, qs \\ nil) do + defp get_lv(conn, site) do {:ok, lv, html} = - conn |> no_slowdown() |> as_live() |> live(verification_path(site, qs)) + conn |> no_slowdown() |> as_live() |> live(verification_path(site)) {lv, html} end - defp kick_off_live_verification(conn, site, qs \\ nil) do + defp kick_off_live_verification(conn, site) do {:ok, lv, _html} = - conn |> no_slowdown() |> no_delay() |> as_live() |> live(verification_path(site, qs)) + conn |> no_slowdown() |> no_delay() |> as_live() |> live(verification_path(site)) {:ok, lv} end @@ -232,10 +212,7 @@ defmodule PlausibleWeb.Live.VerificationTest do # LiveView tests (e.g. props_settings_test.exs). defp as_live(conn), do: assign(conn, :live_module, PlausibleWeb.Live.Verification) - defp verification_path(site, nil), do: "/#{site.domain}?verify_installation=true" - - defp verification_path(site, qs), - do: "/#{site.domain}?verify_installation=true&#{qs}" + defp verification_path(site), do: "/#{site.domain}?verify_installation=true" defp no_slowdown(conn) do Plug.Conn.put_private(conn, :slowdown, 0) From e57569e3530513fd1c56864213bc58087fa1dc2f Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 20 Jul 2026 09:46:18 +0100 Subject: [PATCH 07/21] proper dismiss logic --- .../js/dashboard/verification/portal.test.tsx | 75 +++++++++++++++++++ assets/js/dashboard/verification/portal.tsx | 55 ++++++++++++-- assets/js/liveview/live_socket.js | 11 +++ extra/lib/plausible_web/live/verification.ex | 14 +++- .../controllers/stats_controller.ex | 8 +- .../live/components/verification.ex | 63 +++++++++++----- test/plausible_web/live/verification_test.exs | 63 ++++++++++++++++ 7 files changed, 258 insertions(+), 31 deletions(-) create mode 100644 assets/js/dashboard/verification/portal.test.tsx diff --git a/assets/js/dashboard/verification/portal.test.tsx b/assets/js/dashboard/verification/portal.test.tsx new file mode 100644 index 000000000000..48dca2a3cf01 --- /dev/null +++ b/assets/js/dashboard/verification/portal.test.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { act, render, screen } from '@testing-library/react' +import { useLocation } from 'react-router-dom' +import { TestContextProviders } from '../../../test-utils/app-context-providers' +import { + VERIFICATION_FINISHED_EVENT, + VerificationLiveViewPortal +} from './portal' + +function LocationDisplay() { + const location = useLocation() + return
    {location.pathname + location.search}
    +} + +function renderWithInitialEntry(initialEntry: string) { + render( + <> + + + , + { + wrapper: (props) => ( + + ) + } + ) +} + +function dispatchVerificationFinished(queryParams: string[]) { + act(() => { + window.dispatchEvent( + new CustomEvent(VERIFICATION_FINISHED_EVENT, { + detail: { queryParams } + }) + ) + }) +} + +test('drops exactly the query params named in the event detail, leaving every other param untouched', () => { + renderWithInitialEntry( + '/some-domain?f=contains,os,a&f=contains,page,/&verify_installation=true&flow=provisioning&comparison=year_over_year' + ) + + dispatchVerificationFinished(['verify_installation', 'flow']) + + expect(screen.getByTestId('location').textContent).toBe( + '/?f=contains,os,a&f=contains,page,/&comparison=year_over_year' + ) +}) + +test('does nothing when none of the named params are present', () => { + renderWithInitialEntry('/some-domain?comparison=year_over_year') + + dispatchVerificationFinished(['verify_installation', 'flow']) + + expect(screen.getByTestId('location').textContent).toBe( + '/?comparison=year_over_year' + ) +}) + +test('drops only verify_installation, keeping a real param that happens to be a prefix of it', () => { + renderWithInitialEntry( + '/some-domain?verify_installation=true&verify_installation_extra=keep-me' + ) + + dispatchVerificationFinished(['verify_installation']) + + expect(screen.getByTestId('location').textContent).toBe( + '/?verify_installation_extra=keep-me' + ) +}) diff --git a/assets/js/dashboard/verification/portal.tsx b/assets/js/dashboard/verification/portal.tsx index 8e9a54858276..f32aba92c2a6 100644 --- a/assets/js/dashboard/verification/portal.tsx +++ b/assets/js/dashboard/verification/portal.tsx @@ -1,9 +1,50 @@ -import React from 'react' +import React, { useEffect } from 'react' +import { useAppNavigate } from '../navigation/use-app-navigate' -export const VerificationLiveViewPortal = React.memo( - () => { - return
    +type VerificationFinishedDetail = { + /** + * Exact query param names to drop from the URL when verification banner + * disappears. See: PlausibleWeb.Live.Components.Verification.query_params/0 + */ + queryParams: string[] +} - }, - () => true -) +export const VERIFICATION_FINISHED_EVENT = 'verification-finished' + +/** + * Renders the portal target into which the verification LiveView (see + * lib/plausible_web/live/components/verification.ex) gets teleported. + * Also helps that LiveView out with cleaning up after itself: clearing + * its one-time query params through React Router. + */ +export const VerificationLiveViewPortal = React.memo(() => { + const navigate = useAppNavigate() + + useEffect(() => { + function handleVerificationFinished(event: Event) { + const { queryParams } = (event as CustomEvent) + .detail + + navigate({ + search: (search) => { + const nextSearch = { ...search } + queryParams.forEach((param) => delete nextSearch[param]) + return nextSearch + } + }) + } + + window.addEventListener( + VERIFICATION_FINISHED_EVENT, + handleVerificationFinished + ) + + return () => + window.removeEventListener( + VERIFICATION_FINISHED_EVENT, + handleVerificationFinished + ) + }, [navigate]) + + return
    +}) diff --git a/assets/js/liveview/live_socket.js b/assets/js/liveview/live_socket.js index 2379bb596e6d..b68184de75f3 100644 --- a/assets/js/liveview/live_socket.js +++ b/assets/js/liveview/live_socket.js @@ -17,6 +17,17 @@ let csrfToken = document.querySelector("meta[name='csrf-token']") let websocketUrl = document.querySelector("meta[name='websocket-url']") if (csrfToken && websocketUrl) { let Hooks = { Modal, Dropdown } + + // Lets a LiveView tell the client to tear down the websocket connection + // once it's done with it (e.g. PlausibleWeb.Live.Verification, once its + // banner has been dismissed) - the server-side process then terminates + // gracefully. + Hooks.DisconnectSocket = { + mounted() { + this.handleEvent('disconnect-liveview', () => liveSocket.disconnect()) + } + } + let Uploaders = {} Uploaders.S3 = function (entries, onViewError) { entries.forEach((entry) => { diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index e4ed5d8e91db..cfe347b186c3 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -52,7 +52,8 @@ defmodule PlausibleWeb.Live.Verification do flow: session["flow"] || "", checks_pid: nil, attempts: 0, - custom_url_input?: false + custom_url_input?: false, + dismissed?: false ) if connected?(socket) do @@ -66,7 +67,7 @@ defmodule PlausibleWeb.Live.Verification do assigns = assign(assigns, :use_portal?, @use_portal?) ~H""" -
    +
    <%= if @use_portal? do %> <.portal id="verification-portal-source" target="#verification-portal-target"> <.verification_content {assigns} /> @@ -90,6 +91,7 @@ defmodule PlausibleWeb.Live.Verification do super_admin?={@super_admin?} custom_url_input?={@custom_url_input?} tracker_script_configuration={@tracker_script_configuration} + dismissed?={@dismissed?} /> """ end @@ -108,6 +110,14 @@ defmodule PlausibleWeb.Live.Verification do {:noreply, assign(socket, custom_url_input?: true)} end + # Once dismissed, this LiveView has nothing left to do - and since it's + # the only LiveView on the dashboard page, there's no reason to keep + # the websocket connection open for the rest of the browsing session. + def handle_event("dismiss", _, socket) do + update_component(socket, dismissed?: true) + {:noreply, socket |> assign(dismissed?: true) |> push_event("disconnect-liveview", %{})} + end + def handle_event("verify-custom-url", %{"custom_url" => custom_url}, socket) do socket = socket diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index a0abc0f9665a..f30053664b24 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -116,10 +116,10 @@ defmodule PlausibleWeb.StatsController do limited_to_segment_id: nil, connect_live_socket: verify_installation?, verify_installation?: verify_installation?, - verification_session: %{ - "domain" => site.domain, - "flow" => conn.params["flow"] - } + verification_session: + PlausibleWeb.Live.Components.Verification.query_params() + |> Map.new(&{&1, conn.params[&1]}) + |> Map.put("domain", site.domain) ) end end diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification.ex index 905539a2643d..74f27c706a02 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification.ex @@ -16,14 +16,11 @@ defmodule PlausibleWeb.Live.Components.Verification do import PlausibleWeb.Live.Components.Form @container_id "verification-ui" - # Dismissing hides the banner immediately and strips `verify_installation` - # from the URL (the same param that got it rendered in the first place - - # see PlausibleWeb.StatsController), so a refresh doesn't bring it back. - @dismiss_onclick "document.getElementById('#{@container_id}').classList.add('hidden');" <> - "var u = new window.URL(window.location.href);" <> - "u.searchParams.delete('verify_installation');" <> - "u.searchParams.delete('flow');" <> - "window.history.replaceState(null, '', u);" + + # All query params the verification LiveView needs must be listed here, so + # they can be cleaned up from the URL once verification finishes. + @query_params ~w(verify_installation flow) + def query_params, do: @query_params attr(:domain, :string, required: true) @@ -41,23 +38,17 @@ defmodule PlausibleWeb.Live.Components.Verification do attr(:installation_type, :string, default: nil) attr(:custom_url_input?, :boolean, default: false) attr(:tracker_script_configuration, TrackerScriptConfiguration, default: nil) + attr(:dismissed?, :boolean, default: false) def render(assigns) do assigns = assigns - |> assign(:dismiss_onclick, @dismiss_onclick) |> assign(:container_id, @container_id) + |> assign(:query_params, @query_params) ~H""" -
    - +
    + <.dismiss_button container_id={@container_id} query_params={@query_params} /> <.render_progress :if={not @finished?} message={@message} /> <.render_success :if={@finished? and @success?} @@ -81,6 +72,42 @@ defmodule PlausibleWeb.Live.Components.Verification do """ end + # The action of dismissing the verification banner consists of 4 + # independent things: + # + # 1. Client-side: the inlined `onclick` instantly adds the `hidden` + # class straight to the container div. + # + # 2. Client-side: instantly dispatches a `verification-finished` + # window event so React router can clean up query params that are + # no longer needed (see assets/js/dashboard/verification/portal.tsx). + # Also makes sure that a refresh won't bring verification back. + # + # 3. Server-side (phx-click="dismiss"): sets `dismissed?` on this + # component's assigns, so it stays hidden even if a later + # `send_update` (e.g. :all_checks_done) re-renders it. + # + # 4. Server-side, same handler: tells the client to close the websocket + # connection, since the LiveView has nothing left to do. + defp dismiss_button(assigns) do + ~H""" + + """ + end + + defp dismiss_onclick(container_id, query_params) do + "document.getElementById('#{container_id}').classList.add('hidden');" <> + "window.dispatchEvent(new CustomEvent('verification-finished', { detail: { queryParams: #{Jason.encode!(query_params)} } }));" + end + defp render_progress(assigns) do ~H""" <.notice title="Verifying your installation" theme={:gray}> diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index 7843fbb873ce..e20fe187a2c4 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -12,6 +12,7 @@ defmodule PlausibleWeb.Live.VerificationTest do @retry_button ~s|a[phx-click="retry"]| @progress ~s|#verification-ui p#progress| @heading ~s|#verification-ui h3| + @banner ~s|#verification-ui| @in_progress_text "Verifying your installation" @@ -137,6 +138,68 @@ defmodule PlausibleWeb.Live.VerificationTest do end) end + @tag :ee_only + test "the dismissed flag keeps the banner hidden even if a late update arrives while still connected", + %{conn: conn, site: site} do + stub_lookup_a_records(site.domain) + + stub_verification_result(%{ + "completed" => true, + "trackerIsInHtml" => true, + "plausibleIsOnWindow" => true, + "plausibleIsInitialized" => true, + "testEvent" => %{ + "normalizedBody" => %{ + "domain" => site.domain + }, + "responseStatus" => 200 + } + }) + + {:ok, lv} = kick_off_live_verification(conn, site) + + html = render(lv) + assert html =~ @in_progress_text + refute class_of_element(html, @banner) =~ "hidden" + + html = render_click(lv, "dismiss") + assert class_of_element(html, @banner) =~ "hidden" + + # This might look a bit counter-intuitive -- dismissing the banner + # closes the websocket connection and the LV process would normally + # die before the component gets notified of success. + + # However, `Phoenix.LiveViewTest` can't simulate a real socket closing, + # so the process here just stays alive regardless. What this guards is + # the defensive `dismissed?` gate itself: if this process is ever still + # around when a late update arrives, for whatever reason, the banner + # must stay hidden. + assert eventually(fn -> + html = render(lv) + {html =~ "Success!", html} + end) + + html = render(lv) + assert class_of_element(html, @banner) =~ "hidden" + end + + @tag :ee_only + test "dismissing tells the client to close the websocket connection", + %{conn: conn, site: site} do + stub_lookup_a_records(site.domain) + + stub_verification_result(%{ + "completed" => false, + "error" => %{"message" => "Error"} + }) + + {:ok, lv} = kick_off_live_verification(conn, site) + + render_click(lv, "dismiss") + + assert_push_event(lv, "disconnect-liveview", %{}) + end + for {expected_text, saved_installation_type} <- [ {"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", "manual"}, From fa889810a05d830ac6d9b49f5324fb6e8184e5f5 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 20 Jul 2026 09:49:11 +0100 Subject: [PATCH 08/21] [revert me] debugging UI scenarios --- .../verification/checks.ex | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/extra/lib/plausible/installation_support/verification/checks.ex b/extra/lib/plausible/installation_support/verification/checks.ex index 2a3328d21bcb..6fe66711e03d 100644 --- a/extra/lib/plausible/installation_support/verification/checks.ex +++ b/extra/lib/plausible/installation_support/verification/checks.ex @@ -11,6 +11,24 @@ defmodule Plausible.InstallationSupport.Verification.Checks do @verify_installation_check_timeout 20_000 + # Local UI debugging only - set to one of the keys below to make every + # verification run return that canned interpretation, regardless of what + # the real check pipeline actually found. Handy for iterating on + # PlausibleWeb.Live.Verification's banner UI states. Must be `nil` on commit. + @debug_scenario nil + + @debug_scenarios %{ + 0 => :success, + 1 => %Verification.Diagnostics{}, + 2 => %Verification.Diagnostics{selected_installation_type: "wordpress"}, + 3 => %Verification.Diagnostics{ + plausible_is_on_window: false, + plausible_is_initialized: false, + service_error: %{code: :domain_not_found} + }, + 4 => %Verification.Diagnostics{disallowed_by_csp: true} + } + @spec run(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, pid()} | State.t() def run(url, data_domain, installation_type, opts \\ []) do # Timeout option for testing purposes @@ -59,6 +77,7 @@ defmodule Plausible.InstallationSupport.Verification.Checks do opts \\ [] ) do telemetry? = Keyword.get(opts, :telemetry?, true) + {diagnostics, url} = debug_override(diagnostics, data_domain, url) result = Verification.Diagnostics.interpret( @@ -97,4 +116,28 @@ defmodule Plausible.InstallationSupport.Verification.Checks do result end + + # Also overrides `url` to a clean, query-string-free one - otherwise the + # real check pipeline's cache-busting query param (?plausible_verification=...) + # leaks into canned error messages like "We couldn't find your website at ...". + defp debug_override(diagnostics, data_domain, url) do + case Map.get(@debug_scenarios, @debug_scenario) do + nil -> + {diagnostics, url} + + :success -> + { + %Verification.Diagnostics{ + test_event: %{ + "normalizedBody" => %{"domain" => data_domain}, + "responseStatus" => 200 + } + }, + "https://#{data_domain}" + } + + %Verification.Diagnostics{} = debug -> + {debug, "https://#{data_domain}"} + end + end end From 0add0ec9f859d2702e802cbb00b516beaf6842ba Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 20 Jul 2026 10:01:08 +0100 Subject: [PATCH 09/21] rename verification (component) to verification_banner --- assets/js/dashboard/verification/portal.tsx | 2 +- extra/lib/plausible_web/live/verification.ex | 2 +- lib/plausible_web/controllers/stats_controller.ex | 2 +- .../components/{verification.ex => verification_banner.ex} | 2 +- .../{verification_test.exs => verification_banner_test.exs} | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename lib/plausible_web/live/components/{verification.ex => verification_banner.ex} (99%) rename test/plausible_web/live/components/{verification_test.exs => verification_banner_test.exs} (98%) diff --git a/assets/js/dashboard/verification/portal.tsx b/assets/js/dashboard/verification/portal.tsx index f32aba92c2a6..19d482ecb111 100644 --- a/assets/js/dashboard/verification/portal.tsx +++ b/assets/js/dashboard/verification/portal.tsx @@ -4,7 +4,7 @@ import { useAppNavigate } from '../navigation/use-app-navigate' type VerificationFinishedDetail = { /** * Exact query param names to drop from the URL when verification banner - * disappears. See: PlausibleWeb.Live.Components.Verification.query_params/0 + * disappears. See: PlausibleWeb.Live.Components.VerificationBanner.query_params/0 */ queryParams: string[] } diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index cfe347b186c3..57611d11f436 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -7,7 +7,7 @@ defmodule PlausibleWeb.Live.Verification do alias Plausible.InstallationSupport.{State, Verification} - @component PlausibleWeb.Live.Components.Verification + @component PlausibleWeb.Live.Components.VerificationBanner @slowdown_for_frequent_checking :timer.seconds(0) @use_portal? Mix.env() not in [:test, :ce_test] diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index f30053664b24..d1cabbde2a1c 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -117,7 +117,7 @@ defmodule PlausibleWeb.StatsController do connect_live_socket: verify_installation?, verify_installation?: verify_installation?, verification_session: - PlausibleWeb.Live.Components.Verification.query_params() + PlausibleWeb.Live.Components.VerificationBanner.query_params() |> Map.new(&{&1, conn.params[&1]}) |> Map.put("domain", site.domain) ) diff --git a/lib/plausible_web/live/components/verification.ex b/lib/plausible_web/live/components/verification_banner.ex similarity index 99% rename from lib/plausible_web/live/components/verification.ex rename to lib/plausible_web/live/components/verification_banner.ex index 74f27c706a02..eecca8992d88 100644 --- a/lib/plausible_web/live/components/verification.ex +++ b/lib/plausible_web/live/components/verification_banner.ex @@ -1,4 +1,4 @@ -defmodule PlausibleWeb.Live.Components.Verification do +defmodule PlausibleWeb.Live.Components.VerificationBanner do @moduledoc """ This component is responsible for rendering the verification progress and diagnostics as a compact banner on top of the dashboard. diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_banner_test.exs similarity index 98% rename from test/plausible_web/live/components/verification_test.exs rename to test/plausible_web/live/components/verification_banner_test.exs index da442e8de625..61e1a34af423 100644 --- a/test/plausible_web/live/components/verification_test.exs +++ b/test/plausible_web/live/components/verification_banner_test.exs @@ -1,4 +1,4 @@ -defmodule PlausibleWeb.Live.Components.VerificationTest do +defmodule PlausibleWeb.Live.Components.VerificationBannerTest do use PlausibleWeb.ConnCase, async: true on_ee do @@ -8,7 +8,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do @moduletag :capture_log - @component PlausibleWeb.Live.Components.Verification + @component PlausibleWeb.Live.Components.VerificationBanner @progress ~s|#verification-ui p#progress| @loading_spinner ~s|div#verification-ui div.loading| From aa645030db772a01249b862979dbe8489104b033 Mon Sep 17 00:00:00 2001 From: Sanne de Vries <65487235+sanne-san@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:21:10 +0200 Subject: [PATCH 10/21] Refine verification banner UI and copy (#6525) * Refine verification banner UI and copy - Simplify failure CTAs: keep "Check again" as primary, add ghost "Review installation" (or "Try another URL" when a custom URL retry is offered); remove installation-guide, change-method, and view-snippet expandable logic - Render inline "verify your installation manually" / "review your installation" links inside recommendation text (offer_custom_url_input scenarios) via safe HTML helpers, avoiding HEEx whitespace pitfalls - Tighten diagnostics title and body copy - Move the "Setup pending" pill in the sites list to replace the percentage indicator; use the generic pill component - Extend the notice component with title_class override and a spinner slot; swap success icon to solid check-circle * improve templating logic --------- Co-authored-by: Robert Joonas --- .../plausible/installation_support/result.ex | 2 +- .../verification/diagnostics.ex | 129 +++++---- extra/lib/plausible_web/live/verification.ex | 2 - lib/plausible_web/components/generic.ex | 30 +- .../live/components/verification_banner.ex | 261 ++++++++---------- lib/plausible_web/live/sites.ex | 14 +- .../verification/checks_test.exs | 122 ++++---- .../verification/diagnostics_test.exs | 46 +++ .../components/verification_banner_test.exs | 88 ++++-- test/plausible_web/live/verification_test.exs | 16 +- 10 files changed, 415 insertions(+), 295 deletions(-) create mode 100644 test/plausible/installation_support/verification/diagnostics_test.exs diff --git a/extra/lib/plausible/installation_support/result.ex b/extra/lib/plausible/installation_support/result.ex index baa08d645ddd..a1fad54ca56b 100644 --- a/extra/lib/plausible/installation_support/result.ex +++ b/extra/lib/plausible/installation_support/result.ex @@ -6,7 +6,7 @@ defmodule Plausible.InstallationSupport.Result do ok?: false, data: nil, errors: [error.message], - recommendations: [%{text: error.recommendation, url: error.url}] + recommendations: [%{text: error.recommendation, inline_links: error.inline_links}] ok?: true, data: %{}, diff --git a/extra/lib/plausible/installation_support/verification/diagnostics.ex b/extra/lib/plausible/installation_support/verification/diagnostics.ex index 0773c07550f8..d464a0155fda 100644 --- a/extra/lib/plausible/installation_support/verification/diagnostics.ex +++ b/extra/lib/plausible/installation_support/verification/diagnostics.ex @@ -33,35 +33,56 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do """ @enforce_keys [:message, :recommendation] - defstruct [:message, :recommendation, :url] + defstruct [:message, :recommendation, inline_links: []] + + @required_link_prefix "https://plausible.io/" def new!(attrs) do message = Map.fetch!(attrs, :message) + recommendation = Map.fetch!(attrs, :recommendation) + inline_links = Map.get(attrs, :inline_links, []) if String.ends_with?(message, ".") do raise ArgumentError, "Error message must not end with a period: #{inspect(message)}" end - if String.ends_with?(attrs[:recommendation], ".") do + if String.ends_with?(recommendation, ".") do raise ArgumentError, - "Error recommendation must not end with a period: #{inspect(attrs[:recommendation])}" + "Error recommendation must not end with a period: #{inspect(recommendation)}" end - if is_binary(attrs[:url]) and not String.starts_with?(attrs[:url], "https://plausible.io") do - raise ArgumentError, - "Recommendation url must start with 'https://plausible.io': #{inspect(attrs[:url])}" + for %{text: text, href: href} <- inline_links do + if length(String.split(recommendation, text)) - 1 != 1 do + raise ArgumentError, + "Recommendation inline_links text #{inspect(text)} must appear exactly once in: #{inspect(recommendation)}" + end + + if not String.starts_with?(href, @required_link_prefix) do + raise ArgumentError, + "Recommendation inline_links href must start with '#{@required_link_prefix}': #{inspect(href)}" + end end struct!(__MODULE__, attrs) end end + @verify_manually_inline_link %{ + text: "verify your installation manually", + href: @verify_manually_url + } + @error_succeeds_only_after_cache_bust Error.new!(%{ message: "We detected an issue with your site's cache", recommendation: - "Please clear the cache for your site to ensure that your visitors will load the latest version of your site that has Plausible correctly installed", - url: - "https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site" + "Clear the cache for your site to ensure your visitors load the latest version of your site with Plausible correctly installed. Learn more", + inline_links: [ + %{ + text: "Learn more", + href: + "https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site" + } + ] }) @spec interpret(t(), String.t(), String.t()) :: Result.t() @@ -121,17 +142,21 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do end @error_proxy_network_error Error.new!(%{ - message: - "We got an unexpected response from the proxy you are using for Plausible", + message: "We couldn't verify your proxied installation", recommendation: - "Please check that you've configured the proxied /event route correctly", - url: "https://plausible.io/docs/proxy/introduction" + "We received an unexpected response from your proxy. Check that you've configured the proxied /event route correctly. Learn more", + inline_links: [ + %{ + text: "Learn more", + href: "https://plausible.io/docs/proxy/introduction" + } + ] }) @error_plausible_network_error Error.new!(%{ message: "We couldn't verify your website", recommendation: "Please try verifying again in a few minutes, or verify your installation manually", - url: @verify_manually_url + inline_links: [@verify_manually_inline_link] }) def interpret( @@ -174,11 +199,16 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do @error_csp_disallowed Error.new!(%{ message: - "We encountered an issue with your site's Content Security Policy (CSP)", + "Your site's Content Security Policy (CSP) is blocking Plausible", recommendation: - "Please add plausible.io domain specifically to the allowed list of domains in your site's CSP", - url: - "https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp" + "Add plausible.io to the list of allowed domains in your site's Content Security Policy to allow Plausible to collect analytics. Learn more", + inline_links: [ + %{ + text: "Learn more", + href: + "https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp" + } + ] }) def interpret( %__MODULE__{ @@ -191,10 +221,10 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do do: handled_error(@error_csp_disallowed) @error_domain_not_found Error.new!(%{ - message: "We couldn't find your website at <%= @attempted_url %>", + message: "We couldn't reach <%= @attempted_url %>", recommendation: - "Please check that the domain you entered is correct and reachable publicly. If it's intentionally private, you'll need to verify that Plausible works manually", - url: @verify_manually_url + "Check that the URL is correct and publicly accessible. If your site is intentionally private, you'll need to verify your installation manually", + inline_links: [@verify_manually_inline_link] }) def interpret(%__MODULE__{service_error: %{code: code}}, expected_domain, url) @@ -207,11 +237,10 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do end @error_browserless_network Error.new!(%{ - message: - "We couldn't verify your website at <%= @attempted_url %>", + message: "We couldn't verify <%= @attempted_url %>", recommendation: - "Accessing the website resulted in a network error. Please verify your installation manually", - url: @verify_manually_url + "We encountered a network error while trying to access your website. You can verify your installation manually", + inline_links: [@verify_manually_inline_link] }) def interpret( @@ -228,11 +257,10 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do end @error_browserless_temporary Error.new!(%{ - message: - "Our verification tool encountered a temporary service error", + message: "Our verification service is temporarily unavailable", recommendation: "Please try again in a few minutes or verify your installation manually", - url: @verify_manually_url + inline_links: [@verify_manually_inline_link] }) def interpret(%__MODULE__{service_error: %{code: code}}, _expected_domain, _url) @@ -241,11 +269,10 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do end @error_unexpected_page_response Error.new!(%{ - message: - "We couldn't verify your website at <%= @attempted_url %>", + message: "We couldn't verify <%= @attempted_url %>", recommendation: - "Accessing the website resulted in an unexpected status code <%= @page_response_status %>. Please check for anything that might be blocking us from reaching your site, like a firewall, authentication requirements, or CDN rules. If you'd prefer, you can skip this and verify your installation manually", - url: @verify_manually_url + "Accessing your website returned an unexpected status code (<%= @page_response_status %>). Check for anything that might be blocking our access to your site, such as a firewall, authentication requirements, or CDN rules. You can also verify your installation manually", + inline_links: [@verify_manually_inline_link] }) def interpret( @@ -290,26 +317,26 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do @error_plausible_not_found_for_manual Error.new!(%{ message: @message_plausible_not_found, recommendation: - "Please make sure you've copied the snippet to the head of your site, or verify your installation manually", - url: @verify_manually_url + "Make sure you've copied the snippet to the head of your site, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) @error_plausible_not_found_for_npm Error.new!(%{ message: @message_plausible_not_found, recommendation: - "Please make sure you've initialized Plausible on your site, or verify your installation manually", - url: @verify_manually_url + "Make sure you've initialized Plausible on your site, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) @error_plausible_not_found_for_gtm Error.new!(%{ message: @message_plausible_not_found, recommendation: - "Please make sure you've configured the GTM template correctly, or verify your installation manually", - url: @verify_manually_url + "Make sure you've configured the GTM template correctly, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) @error_plausible_not_found_for_wordpress Error.new!(%{ message: @message_plausible_not_found, recommendation: - "Please make sure you've enabled the plugin, or verify your installation manually", - url: @verify_manually_url + "Make sure you've enabled the WordPress plugin, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) defp error_plausible_not_found(selected_installation_type) do case selected_installation_type do @@ -320,33 +347,33 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do end end - @unexpected_domain_message "Plausible test event is not for this site" + @unexpected_domain_message "Your Plausible snippet is configured for a different domain" @error_unexpected_domain_for_manual Error.new!(%{ message: @unexpected_domain_message, recommendation: - "Please check that the snippet on your site matches the installation instructions exactly, or verify your installation manually", - url: @verify_manually_url + "Check that the snippet on your site matches the one shown in the installation instructions, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) @error_unexpected_domain_for_npm Error.new!(%{ message: @unexpected_domain_message, recommendation: - "Please check that you've initialized Plausible with the correct domain, or verify your installation manually", - url: @verify_manually_url + "Check you've initialized Plausible with the correct domain, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) @error_unexpected_domain_for_gtm Error.new!(%{ message: @unexpected_domain_message, recommendation: - "Please check that you've entered the ID in the GTM template correctly, or verify your installation manually", - url: @verify_manually_url + "Check you've entered the ID in the GTM template correctly, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) @error_unexpected_domain_for_wordpress Error.new!(%{ message: @unexpected_domain_message, recommendation: - "Please check that you've installed the WordPress plugin correctly, or verify your installation manually", - url: @verify_manually_url + "Check you've installed the WordPress plugin correctly, or verify your installation manually", + inline_links: [@verify_manually_inline_link] }) defp error_unexpected_domain(selected_installation_type) do case selected_installation_type do @@ -372,7 +399,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do %Result{ ok?: false, errors: [message], - recommendations: [%{text: recommendation, url: error.url}] + recommendations: [%{text: recommendation, inline_links: error.inline_links}] } end @@ -383,7 +410,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do ok?: false, data: %{unhandled: true, browserless_issue: browserless_issue}, errors: [error.message], - recommendations: [%{text: error.recommendation, url: error.url}] + recommendations: [%{text: error.recommendation, inline_links: error.inline_links}] } end end diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index 57611d11f436..ff4742bf3a56 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -83,14 +83,12 @@ defmodule PlausibleWeb.Live.Verification do ~H""" <.live_component module={@component} - installation_type={get_installation_type(@tracker_script_configuration)} domain={@domain} id="verification-standalone" attempts={@attempts} flow={@flow} super_admin?={@super_admin?} custom_url_input?={@custom_url_input?} - tracker_script_configuration={@tracker_script_configuration} dismissed?={@dismissed?} /> """ diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex index 2dd458386723..173e6a6a6f88 100644 --- a/lib/plausible_web/components/generic.ex +++ b/lib/plausible_web/components/generic.ex @@ -12,25 +12,37 @@ defmodule PlausibleWeb.Components.Generic do gray: %{ bg: "bg-gray-100 dark:bg-gray-800", icon: "text-gray-600 dark:text-gray-300", - title_text: "text-sm text-gray-900 dark:text-gray-100", + title_text: "text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-800 dark:text-gray-200 leading-5" }, + indigo: %{ + bg: "bg-indigo-100/60 dark:bg-indigo-900/40", + icon: "text-indigo-500", + title_text: "text-gray-900 dark:text-gray-100", + body_text: "text-sm text-gray-600 dark:text-gray-100/60 leading-5" + }, + green: %{ + bg: "bg-green-100/60 dark:bg-green-900/40", + icon: "text-green-500", + title_text: "text-gray-900 dark:text-gray-100", + body_text: "text-sm text-gray-600 dark:text-gray-100/60 leading-5" + }, yellow: %{ bg: "bg-yellow-100/60 dark:bg-yellow-900/40", icon: "text-yellow-500", - title_text: "text-sm text-gray-900 dark:text-gray-100", + title_text: "text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-600 dark:text-gray-100/60 leading-5" }, red: %{ bg: "bg-red-100 dark:bg-red-900/30", icon: "text-red-600 dark:text-red-500", - title_text: "text-sm text-gray-900 dark:text-gray-100", + title_text: "text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-600 dark:text-gray-100/60 leading-5" }, white: %{ bg: "bg-white dark:bg-gray-900 shadow-sm dark:shadow-none", icon: "text-gray-600 dark:text-gray-400", - title_text: "text-sm text-gray-900 dark:text-gray-100", + title_text: "text-gray-900 dark:text-gray-100", body_text: "text-sm text-gray-600 dark:text-gray-300 leading-5" } } @@ -222,6 +234,7 @@ defmodule PlausibleWeb.Components.Generic do attr(:show_icon, :boolean, default: true) attr(:class, :string, default: "") attr(:icon_class, :string, default: "") + attr(:title_class, :string, default: "") attr(:rest, :global) slot(:inner_block) slot(:actions) @@ -253,7 +266,14 @@ defmodule PlausibleWeb.Components.Generic do <% end %>
    -

    +

    {@title}

    diff --git a/lib/plausible_web/live/components/verification_banner.ex b/lib/plausible_web/live/components/verification_banner.ex index eecca8992d88..b40731c01db9 100644 --- a/lib/plausible_web/live/components/verification_banner.ex +++ b/lib/plausible_web/live/components/verification_banner.ex @@ -7,10 +7,7 @@ defmodule PlausibleWeb.Live.Components.VerificationBanner do use Plausible alias PlausibleWeb.Router.Helpers, as: Routes - alias PlausibleWeb.Components.Icons - alias PlausibleWeb.Live.Installation.Instructions alias Plausible.InstallationSupport.{State, Result} - alias Plausible.Site.TrackerScriptConfiguration import PlausibleWeb.Components.Generic import PlausibleWeb.Live.Components.Form @@ -35,9 +32,7 @@ defmodule PlausibleWeb.Live.Components.VerificationBanner do attr(:interpretation, Result, default: nil) attr(:attempts, :integer, default: 0) attr(:flow, :string, default: "") - attr(:installation_type, :string, default: nil) attr(:custom_url_input?, :boolean, default: false) - attr(:tracker_script_configuration, TrackerScriptConfiguration, default: nil) attr(:dismissed?, :boolean, default: false) def render(assigns) do @@ -62,11 +57,9 @@ defmodule PlausibleWeb.Live.Components.VerificationBanner do attempts={@attempts} domain={@domain} flow={@flow} - installation_type={@installation_type} super_admin?={@super_admin?} verification_state={@verification_state} custom_url_input?={@custom_url_input?} - tracker_script_configuration={@tracker_script_configuration} />
    """ @@ -94,7 +87,7 @@ defmodule PlausibleWeb.Live.Components.VerificationBanner do
    - <.site_stats sparkline={@sparkline} /> + <.site_stats sparkline={@sparkline} needs_verification?={@needs_verification?} />
    @@ -680,6 +674,7 @@ defmodule PlausibleWeb.Live.Sites do end attr(:sparkline, :any, required: true) + attr(:needs_verification?, :boolean, default: false) def site_stats(assigns) do ~H""" @@ -708,7 +703,10 @@ defmodule PlausibleWeb.Live.Sites do

    - <.percentage_change change={@sparkline.visitors_change} /> + <.pill :if={@needs_verification?} color={:yellow}> + Setup pending + + <.percentage_change :if={not @needs_verification?} change={@sparkline.visitors_change} />
  • diff --git a/test/plausible/installation_support/verification/checks_test.exs b/test/plausible/installation_support/verification/checks_test.exs index 01e01e4d7910..f3541ae1900e 100644 --- a/test/plausible/installation_support/verification/checks_test.exs +++ b/test/plausible/installation_support/verification/checks_test.exs @@ -13,6 +13,11 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do @expected_domain "example.com" @url_to_verify "https://#{@expected_domain}" + @verify_manually_url "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + @verify_manually_inline_link %{ + text: "verify your installation manually", + href: @verify_manually_url + } describe "URL check" do test "returns error when DNS check fails with domain not found error, offers custom URL input" do @@ -22,14 +27,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do ok?: false, data: %{offer_custom_url_input: true}, errors: [ - ^any(:string, ~r/We couldn't find your website at #{@url_to_verify}$/) + ^any(:string, ~r/We couldn't reach #{@url_to_verify}$/) ], recommendations: [ %{ text: - "Please check that the domain you entered is correct and reachable publicly. If it's intentionally private, you'll need to verify that Plausible works manually", - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + "Check that the URL is correct and publicly accessible. If your site is intentionally private, you can verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = @@ -49,14 +53,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do ok?: false, data: %{offer_custom_url_input: true}, errors: [ - ^any(:string, ~r/We couldn't find your website at #{url_to_verify}$/) + ^any(:string, ~r/We couldn't reach #{url_to_verify}$/) ], recommendations: [ %{ text: - "Please check that the domain you entered is correct and reachable publicly. If it's intentionally private, you'll need to verify that Plausible works manually", - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + "Check that the URL is correct and publicly accessible. If your site is intentionally private, you can verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = @@ -94,13 +97,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do for {installation_type, expected_recommendation} <- [ {"wordpress", - "Please check that you've installed the WordPress plugin correctly, or verify your installation manually"}, + "Check you've installed the WordPress plugin correctly, or verify your installation manually"}, {"gtm", - "Please check that you've entered the ID in the GTM template correctly, or verify your installation manually"}, + "Check you've entered the ID in the GTM template correctly, or verify your installation manually"}, {"npm", - "Please check that you've initialized Plausible with the correct domain, or verify your installation manually"}, + "Check you've initialized Plausible with the correct domain, or verify your installation manually"}, {"manual", - "Please check that the snippet on your site matches the installation instructions exactly, or verify your installation manually"} + "Check that the snippet on your site matches the one shown in the installation instructions, or verify your installation manually"} ] do test "returns error when test event domain doesn't match the expected domain, with recommendation for installation type: #{installation_type}" do verification_stub = @@ -119,12 +122,11 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, - errors: ["Plausible test event is not for this site"], + errors: ["Your Plausible snippet is configured for a different domain"], recommendations: [ %{ text: unquote(expected_recommendation), - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + inline_links: [^@verify_manually_inline_link] } ] } = @@ -154,11 +156,16 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, - errors: [^any(:string, ~r/.*proxy.*/)], + errors: [^any(:string, ~r/.*proxied.*/)], recommendations: [ %{ text: ^any(:string, ~r/.*proxied.*/), - url: "https://plausible.io/docs/proxy/introduction" + inline_links: [ + %{ + text: "Learn more", + href: "https://plausible.io/docs/proxy/introduction" + } + ] } ] } = @@ -187,9 +194,9 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do errors: [^any(:string, ~r/.*couldn't verify.*/)], recommendations: [ %{ - text: ^any(:string, ~r/.*try verifying again.*/), - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + text: + "Please try verifying again in a few minutes, or verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = @@ -213,9 +220,8 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do recommendations: [ %{ text: - "Please make sure you've copied the snippet to the head of your site, or verify your installation manually", - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + "Make sure you've copied the snippet to the head of your site, or verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = @@ -236,14 +242,19 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, errors: [ - "We encountered an issue with your site's Content Security Policy (CSP)" + "Your site's Content Security Policy (CSP) is blocking Plausible" ], recommendations: [ %{ text: - "Please add plausible.io domain specifically to the allowed list of domains in your site's CSP", - url: - "https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp" + "Add plausible.io to the list of allowed domains in your site's Content Security Policy to allow Plausible to collect analytics. Learn more", + inline_links: [ + %{ + text: "Learn more", + href: + "https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp" + } + ] } ] } = @@ -261,15 +272,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, data: %{offer_custom_url_input: true}, - errors: [ - "We couldn't verify your website at https://example.com" - ], + errors: ["We couldn't verify https://example.com"], recommendations: [ %{ text: - "Accessing the website resulted in a network error. Please verify your installation manually", - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + "We encountered a network error while trying to access your website. You can verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = @@ -291,15 +299,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, data: %{offer_custom_url_input: true}, - errors: [ - "We couldn't verify your website at https://example.com" - ], + errors: ["We couldn't verify https://example.com"], recommendations: [ %{ text: - "Accessing the website resulted in an unexpected status code 403. Please check for anything that might be blocking us from reaching your site, like a firewall, authentication requirements, or CDN rules. If you'd prefer, you can skip this and verify your installation manually", - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + "Accessing your website returned an unexpected status code (403). Check for anything that might be blocking our access to your site, such as a firewall, authentication requirements, or CDN rules. You can also verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = @@ -309,13 +314,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do for {installation_type, expected_recommendation} <- [ {"wordpress", - "Please make sure you've enabled the plugin, or verify your installation manually"}, + "Make sure you've enabled the WordPress plugin, or verify your installation manually"}, {"gtm", - "Please make sure you've configured the GTM template correctly, or verify your installation manually"}, + "Make sure you've configured the GTM template correctly, or verify your installation manually"}, {"npm", - "Please make sure you've initialized Plausible on your site, or verify your installation manually"}, + "Make sure you've initialized Plausible on your site, or verify your installation manually"}, {"manual", - "Please make sure you've copied the snippet to the head of your site, or verify your installation manually"} + "Make sure you've copied the snippet to the head of your site, or verify your installation manually"} ] do test "returns error \"We couldn't detect Plausible on your site\" when plausible_is_on_window is false (with best guess recommendation for installation type: #{installation_type})" do verification_stub = @@ -334,8 +339,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do recommendations: [ %{ text: unquote(expected_recommendation), - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + inline_links: [^@verify_manually_inline_link] } ] } = @@ -363,8 +367,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do recommendations: [ %{ text: unquote(expected_recommendation), - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + inline_links: [^@verify_manually_inline_link] } ] } = @@ -426,8 +429,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do recommendations: [ %{ text: ^any(:string, ~r/.*cache.*/), - url: - "https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site" + inline_links: [ + %{ + text: "Learn more", + href: + "https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site" + } + ] } ] } = run_checks(verification_stub) |> Checks.interpret_diagnostics() @@ -484,12 +492,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, - errors: [^any(:string, ~r/.*temporary service error.*/)], + errors: [^any(:string, ~r/.*temporarily unavailable.*/)], recommendations: [ %{ - text: ^any(:string, ~r/.*in a few minutes.*/), - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + text: + "Please try again in a few minutes or verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = Checks.interpret_diagnostics(state) @@ -514,12 +522,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do assert_matches %Result{ ok?: false, - errors: [^any(:string, ~r/.*temporary service error.*/)], + errors: [^any(:string, ~r/.*temporarily unavailable.*/)], recommendations: [ %{ - text: ^any(:string, ~r/.*in a few minutes.*/), - url: - "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration" + text: + "Please try again in a few minutes or verify your installation manually", + inline_links: [^@verify_manually_inline_link] } ] } = Checks.interpret_diagnostics(state) diff --git a/test/plausible/installation_support/verification/diagnostics_test.exs b/test/plausible/installation_support/verification/diagnostics_test.exs new file mode 100644 index 000000000000..bead8dfa1df0 --- /dev/null +++ b/test/plausible/installation_support/verification/diagnostics_test.exs @@ -0,0 +1,46 @@ +defmodule Plausible.InstallationSupport.Verification.DiagnosticsTest do + use ExUnit.Case, async: true + + alias Plausible.InstallationSupport.Verification.Diagnostics.Error + + describe "Error.new!/1" do + test "accepts a recommendation whose inline_links text appears exactly once" do + assert %Error{} = + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the docs for more info", + inline_links: [%{text: "docs", href: "https://plausible.io/docs"}] + }) + end + + test "raises when inline_links text isn't found in the recommendation" do + assert_raise ArgumentError, ~r/must appear exactly once/, fn -> + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the manual for more info", + inline_links: [%{text: "docs", href: "https://plausible.io/docs"}] + }) + end + end + + test "raises when inline_links text appears more than once in the recommendation" do + assert_raise ArgumentError, ~r/must appear exactly once/, fn -> + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the docs, or check the docs again", + inline_links: [%{text: "the docs", href: "https://plausible.io/docs"}] + }) + end + end + + test "raises when inline_links href doesn't point at plausible.io" do + assert_raise ArgumentError, ~r/must start with/, fn -> + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the docs for more info", + inline_links: [%{text: "docs", href: "https://example.com/docs"}] + }) + end + end + end +end diff --git a/test/plausible_web/live/components/verification_banner_test.exs b/test/plausible_web/live/components/verification_banner_test.exs index 61e1a34af423..0ecdf1bc8a58 100644 --- a/test/plausible_web/live/components/verification_banner_test.exs +++ b/test/plausible_web/live/components/verification_banner_test.exs @@ -11,9 +11,8 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do @component PlausibleWeb.Live.Components.VerificationBanner @progress ~s|#verification-ui p#progress| - @loading_spinner ~s|div#verification-ui div.loading| - @check_circle ~s|div#verification-ui #check-circle| - @error_circle ~s|div#verification-ui #error-circle| + @loading_spinner ~s|#verification-ui svg.animate-spin| + @check_circle ~s|#verification-ui #check-circle| @recommendations ~s|#recommendation| @super_admin_report ~s|#super-admin-report| @@ -22,24 +21,23 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do assert element_exists?(html, @progress) assert text_of_element(html, @progress) == - "We're visiting your site to ensure that everything is working" + "We're visiting your site to ensure that everything is working..." assert element_exists?(html, @loading_spinner) - refute class_of_element(html, @loading_spinner) =~ "hidden" refute element_exists?(html, @recommendations) refute element_exists?(html, @check_circle) refute element_exists?(html, @super_admin_report) end - test "renders error badge on error" do + test "renders failed state without progress spinner" do html = render_component(@component, domain: "example.com", success?: false, finished?: true) refute element_exists?(html, @loading_spinner) refute element_exists?(html, @check_circle) refute element_exists?(html, @recommendations) - assert element_exists?(html, @error_circle) + assert html =~ "We couldn't verify your installation" end - test "renders diagnostic interpretation" do + test "renders diagnostic interpretation with inline verify link and standalone review-installation sentence" do interpretation = Verification.Checks.interpret_diagnostics(%State{ url: "https://example.com", @@ -56,11 +54,60 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do ) assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1) - assert recommendation =~ "check that the domain you entered is correct" + assert recommendation =~ "Check that the URL is correct and publicly accessible" + assert recommendation =~ "verify your installation manually" + refute recommendation =~ "review your installation" + assert recommendation =~ "See your installation instructions again here" + + assert element_exists?( + html, + ~s|#recommendation a[href="https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"]| + ) + + assert element_exists?( + html, + ~s|#recommendation a[href="/example.com/installation?flow="]| + ) refute element_exists?(html, @super_admin_report) end + test "renders inline verify-manually link when the recommendation mentions it (no custom URL retry)" do + interpretation = + Verification.Checks.interpret_diagnostics(%State{ + url: "https://example.com", + data_domain: "example.com", + diagnostics: %Verification.Diagnostics{ + plausible_is_on_window: false, + selected_installation_type: "manual" + } + }) + + refute Map.get(interpretation.data || %{}, :offer_custom_url_input) == true + + html = + render_component(@component, + domain: "example.com", + success?: false, + finished?: true, + interpretation: interpretation + ) + + assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1) + assert recommendation =~ "Make sure you've copied the snippet" + assert recommendation =~ "verify your installation manually" + refute recommendation =~ "review your installation" + refute recommendation =~ "Learn more" + refute recommendation =~ "See your installation instructions again here" + + assert element_exists?( + html, + ~s|#recommendation a[href="https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"]| + ) + + refute element_exists?(html, ~s|#recommendation a[href^="/example.com/installation"]|) + end + test "renders super-admin report" do state = %State{ url: "https://example.com", @@ -99,20 +146,20 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do test "renders a progress message" do html = render_component(@component, domain: "example.com", message: "Arbitrary message") - assert text_of_element(html, @progress) == "Arbitrary message" + assert text_of_element(html, @progress) == "Arbitrary message..." end - test "renders contact link on >3 attempts" do + 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 installation?" + refute html =~ "Need help?" 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 installation?" + assert html =~ "Need help?" assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|) end - test "renders a click-to-show-form link to verify installation at a different URL" do + test "renders a Try another URL ghost button when a custom URL retry is offered" do interpretation = Verification.Checks.interpret_diagnostics(%State{ url: "example.com", @@ -133,11 +180,12 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do interpretation: interpretation ) - assert text_of_element(html, "#verify-custom-url-link") =~ "Click here" + assert text_of_element(html, "#verify-custom-url-link") =~ "Try another URL" assert element_exists?(html, ~s|a#verify-custom-url-link[phx-click="show-custom-url-form"]|) + refute html =~ "Review installation" end - test "renders the custom URL input inline, retry button becomes the form's submit button, hides the prompt link" do + test "renders the custom URL input inline, replacing Check again with the Verify URL submit button, and hides the secondary action" do interpretation = Verification.Checks.interpret_diagnostics(%State{ url: "example.com", @@ -159,9 +207,10 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do refute element_exists?(html, "#verify-custom-url-link") refute element_exists?(html, ~s|a[phx-click="retry"]|) + refute html =~ "Review installation" assert text_of_element(html, ~s|form[phx-submit="verify-custom-url"] button[type="submit"]|) =~ - "Check again" + "Verify URL" assert element_exists?( html, @@ -172,13 +221,12 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do "https://example.com" end - test "offers an installation-instructions escape path on failure, no more settings link" do + test "offers a Review installation ghost button on failure by default" do html = render_component(@component, domain: "example.com", success?: false, finished?: true, - installation_type: "wordpress", flow: PlausibleWeb.Flows.review() ) @@ -188,6 +236,8 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do html, ~s|a[href="/example.com/installation?flow=review"]| ) + + assert html =~ "Review installation" end end end diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index e20fe187a2c4..7ee23fcc3a03 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -134,7 +134,7 @@ defmodule PlausibleWeb.Live.VerificationTest do assert eventually(fn -> html = render(lv) - {html =~ "Success!", html} + {html =~ "Tracking is active on your site", html} end) end @@ -176,7 +176,7 @@ defmodule PlausibleWeb.Live.VerificationTest do # must stay hidden. assert eventually(fn -> html = render(lv) - {html =~ "Success!", html} + {html =~ "Tracking is active on your site", html} end) html = render(lv) @@ -201,16 +201,16 @@ defmodule PlausibleWeb.Live.VerificationTest do end for {expected_text, saved_installation_type} <- [ - {"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", + {"Make sure you've copied the snippet to the head of your site, or verify your installation manually.", "manual"}, - {"Please make sure you've initialized Plausible on your site, or verify your installation manually.", + {"Make sure you've initialized Plausible on your site, or verify your installation manually.", "npm"}, - {"Please make sure you've configured the GTM template correctly, or verify your installation manually.", + {"Make sure you've configured the GTM template correctly, or verify your installation manually.", "gtm"}, - {"Please make sure you've enabled the plugin, or verify your installation manually.", + {"Make sure you've enabled the WordPress plugin, or verify your installation manually.", "wordpress"}, # falls back to manual when there's no saved installation type - {"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.", + {"Make sure you've copied the snippet to the head of your site, or verify your installation manually.", nil} ] do @tag :ee_only @@ -248,7 +248,7 @@ defmodule PlausibleWeb.Live.VerificationTest do assert element_exists?(html, @retry_button) - assert html =~ htmlize_quotes(unquote(expected_text)) + assert text_of_element(html, "#recommendation") =~ unquote(expected_text) refute element_exists?(html, "#super-admin-report") end end From 131b4dbbed2f67c71e7ee78be9b4b95337314f33 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 23 Jul 2026 10:45:20 +0100 Subject: [PATCH 11/21] emails CTA banner --- .../js/dashboard/email-reports-cta-banner.tsx | 124 ++++++++++++++++++ assets/js/dashboard/index.tsx | 2 + .../first_dashboard_launch_banner.ex | 52 -------- .../controllers/site_controller.ex | 6 +- lib/plausible_web/live/installation.ex | 2 - .../templates/stats/stats.html.heex | 2 - .../controllers/site_controller_test.exs | 12 +- 7 files changed, 133 insertions(+), 67 deletions(-) create mode 100644 assets/js/dashboard/email-reports-cta-banner.tsx delete mode 100644 lib/plausible_web/components/first_dashboard_launch_banner.ex diff --git a/assets/js/dashboard/email-reports-cta-banner.tsx b/assets/js/dashboard/email-reports-cta-banner.tsx new file mode 100644 index 000000000000..8defbd306b23 --- /dev/null +++ b/assets/js/dashboard/email-reports-cta-banner.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useRef, useState } from 'react' +import { XMarkIcon } from '@heroicons/react/24/outline' +import { useSiteContext } from './site-context' +import { useCurrentVisitorsContext } from './current-visitors-context' + +type CTAStorageState = 'pending' | 'visible' + +function getStorageKey(domain: string) { + return `email_reports_cta_${domain}` +} + +// CTA for configuring weekly email reports +// +// Renders only once, as soon as the first pageview lands. This can happen: +// +// 1. Automatically, when the dashboard stays open -- relying on the value +// of current-visitors changing to something other than 0. +// +// 2. Dashboard is refreshed and showing data for the very first time. +// +// Case 2 is the tricky one. By the time of the refresh, `site.statsBegin` +// is already set, so that value alone can't distinguish "stats just +// started" from "this site has always had stats". +// +// The sessionStorage entry closes that gap -- it's stamped 'pending' the +// moment stats are still absent, so a later reload can still recognize the +// transition. It is only ever stamped while stats are absent, so established +// sites never pick it up and can't retrigger the CTA. +// +// Once shown, the same entry is stamped 'visible', so a refresh mid-display +// resumes the CTA instead of re-deciding from scratch -- but only for three +// seconds -- past that, the sessionStorage entry clears itself out and a +// refresh won't bring the CTA back. +export function EmailReportsCTABanner() { + const site = useSiteContext() + const currentVisitors = useCurrentVisitorsContext() + const hasStats = !!site.statsBegin + const storageKey = getStorageKey(site.domain) + + const hasTriggeredRef = useRef(false) + const [visible, setVisible] = useState(false) + + useEffect(() => { + if (!hasStats && sessionStorage.getItem(storageKey) !== 'visible') { + const state: CTAStorageState = 'pending' + sessionStorage.setItem(storageKey, state) + } + }, [hasStats, storageKey]) + + useEffect(() => { + if (hasTriggeredRef.current) { + return + } + + const storedState = sessionStorage.getItem(storageKey) + + if (storedState === 'visible') { + hasTriggeredRef.current = true + setVisible(true) + return + } + + const firstPageviewJustLanded = hasStats + ? storedState === 'pending' + : !!currentVisitors + + if (!firstPageviewJustLanded) { + return + } + + hasTriggeredRef.current = true + const state: CTAStorageState = 'visible' + sessionStorage.setItem(storageKey, state) + setVisible(true) + }, [hasStats, currentVisitors, storageKey]) + + useEffect(() => { + if (!visible) { + return + } + + const timeout = setTimeout(() => { + sessionStorage.removeItem(storageKey) + }, 3000) + + return () => clearTimeout(timeout) + }, [visible, storageKey]) + + if (!visible) { + return null + } + + function dismiss() { + sessionStorage.removeItem(storageKey) + setVisible(false) + } + + return ( + + ) +} diff --git a/assets/js/dashboard/index.tsx b/assets/js/dashboard/index.tsx index b4c8e0c1cb15..bb2f7ac6be1f 100644 --- a/assets/js/dashboard/index.tsx +++ b/assets/js/dashboard/index.tsx @@ -12,6 +12,7 @@ import { GraphIntervalProvider } from './stats/graph/graph-interval-context' import { ImportsIncludedProvider } from './stats/graph/imports-included-context' import { CurrentVisitorsProvider } from './current-visitors-context' import { VerificationLiveViewPortal } from './verification/portal' +import { EmailReportsCTABanner } from './email-reports-cta-banner' function DashboardStats({ importedDataInView, @@ -23,6 +24,7 @@ function DashboardStats({ return ( <>
    +
    diff --git a/lib/plausible_web/components/first_dashboard_launch_banner.ex b/lib/plausible_web/components/first_dashboard_launch_banner.ex deleted file mode 100644 index 7646368a6fdd..000000000000 --- a/lib/plausible_web/components/first_dashboard_launch_banner.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule PlausibleWeb.Components.FirstDashboardLaunchBanner do - @moduledoc """ - A banner that appears on the first dashboard launch - """ - - use PlausibleWeb, :component - - attr(:site, Plausible.Site, required: true) - - def set(assigns) do - ~H""" - - """ - end - - attr(:site, Plausible.Site, required: true) - - def render(assigns) do - ~H""" - - """ - end - - defp x_data(site) do - "{show: !!sessionStorage.getItem('#{storage_key(site)}')}" - end - - defp x_init(site) do - "setTimeout(() => sessionStorage.removeItem('#{storage_key(site)}'), 3000)" - end - - defp storage_key(site) do - "dashboard_seen_#{site.domain}" - end -end diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 7413111898eb..108891f486e1 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -50,11 +50,7 @@ defmodule PlausibleWeb.SiteController do end redirect(conn, - to: - Routes.site_path(conn, :installation, site.domain, - site_created: true, - flow: flow - ) + to: Routes.site_path(conn, :installation, site.domain, flow: flow) ) {:error, _, :permission_denied, _} -> diff --git a/lib/plausible_web/live/installation.ex b/lib/plausible_web/live/installation.ex index f88e6aaf1a79..769949868796 100644 --- a/lib/plausible_web/live/installation.ex +++ b/lib/plausible_web/live/installation.ex @@ -82,7 +82,6 @@ defmodule PlausibleWeb.Live.Installation do {:ok, assign(socket, site: site, - site_created?: params["site_created"] == "true", flow: flow )} end @@ -104,7 +103,6 @@ defmodule PlausibleWeb.Live.Installation do def render(assigns) do ~H"""
    - <.focus_box> diff --git a/lib/plausible_web/templates/stats/stats.html.heex b/lib/plausible_web/templates/stats/stats.html.heex index aceb45a07ff2..cbd6602bd48b 100644 --- a/lib/plausible_web/templates/stats/stats.html.heex +++ b/lib/plausible_web/templates/stats/stats.html.heex @@ -1,6 +1,4 @@
    - - <%= if Plausible.Teams.locked?(@site.team) do %>
    %Verification.Diagnostics{ - plausible_is_on_window: false, - plausible_is_initialized: false, - service_error: %{code: :domain_not_found} - }, - 4 => %Verification.Diagnostics{disallowed_by_csp: true} - } - @spec run(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, pid()} | State.t() def run(url, data_domain, installation_type, opts \\ []) do # Timeout option for testing purposes @@ -77,7 +59,6 @@ defmodule Plausible.InstallationSupport.Verification.Checks do opts \\ [] ) do telemetry? = Keyword.get(opts, :telemetry?, true) - {diagnostics, url} = debug_override(diagnostics, data_domain, url) result = Verification.Diagnostics.interpret( @@ -116,28 +97,4 @@ defmodule Plausible.InstallationSupport.Verification.Checks do result end - - # Also overrides `url` to a clean, query-string-free one - otherwise the - # real check pipeline's cache-busting query param (?plausible_verification=...) - # leaks into canned error messages like "We couldn't find your website at ...". - defp debug_override(diagnostics, data_domain, url) do - case Map.get(@debug_scenarios, @debug_scenario) do - nil -> - {diagnostics, url} - - :success -> - { - %Verification.Diagnostics{ - test_event: %{ - "normalizedBody" => %{"domain" => data_domain}, - "responseStatus" => 200 - } - }, - "https://#{data_domain}" - } - - %Verification.Diagnostics{} = debug -> - {debug, "https://#{data_domain}"} - end - end end diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index ff4742bf3a56..c0ea7b5ce793 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -8,7 +8,7 @@ defmodule PlausibleWeb.Live.Verification do alias Plausible.InstallationSupport.{State, Verification} @component PlausibleWeb.Live.Components.VerificationBanner - @slowdown_for_frequent_checking :timer.seconds(0) + @slowdown_for_frequent_checking :timer.seconds(5) @use_portal? Mix.env() not in [:test, :ce_test] def mount( From 87e438bc58918c4eb3eeb386d891c2f52708e9a6 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 23 Jul 2026 14:24:47 +0100 Subject: [PATCH 13/21] fix test after rebase --- .../installation_support/verification/checks_test.exs | 4 ++-- test/plausible_web/live/installation_test.exs | 2 -- test/plausible_web/live/verification_test.exs | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/plausible/installation_support/verification/checks_test.exs b/test/plausible/installation_support/verification/checks_test.exs index f3541ae1900e..7b8118c9aad7 100644 --- a/test/plausible/installation_support/verification/checks_test.exs +++ b/test/plausible/installation_support/verification/checks_test.exs @@ -32,7 +32,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do recommendations: [ %{ text: - "Check that the URL is correct and publicly accessible. If your site is intentionally private, you can verify your installation manually", + "Check that the URL is correct and publicly accessible. If your site is intentionally private, you'll need to verify your installation manually", inline_links: [^@verify_manually_inline_link] } ] @@ -58,7 +58,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do recommendations: [ %{ text: - "Check that the URL is correct and publicly accessible. If your site is intentionally private, you can verify your installation manually", + "Check that the URL is correct and publicly accessible. If your site is intentionally private, you'll need to verify your installation manually", inline_links: [^@verify_manually_inline_link] } ] diff --git a/test/plausible_web/live/installation_test.exs b/test/plausible_web/live/installation_test.exs index 763b6dfdfac9..8b199583ae72 100644 --- a/test/plausible_web/live/installation_test.exs +++ b/test/plausible_web/live/installation_test.exs @@ -253,8 +253,6 @@ defmodule PlausibleWeb.Live.InstallationTest do } do stub_dns() - stub_lookup_a_records(site.domain) - stub_detection_manual() {lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}") diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs index 7ee23fcc3a03..664d4ce5c021 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -68,8 +68,6 @@ defmodule PlausibleWeb.Live.VerificationTest do } do stub_dns() - stub_lookup_a_records(site.domain) - stub_verification_result(%{ "completed" => true, "trackerIsInHtml" => false, @@ -141,7 +139,7 @@ defmodule PlausibleWeb.Live.VerificationTest do @tag :ee_only test "the dismissed flag keeps the banner hidden even if a late update arrives while still connected", %{conn: conn, site: site} do - stub_lookup_a_records(site.domain) + stub_dns() stub_verification_result(%{ "completed" => true, @@ -186,7 +184,7 @@ defmodule PlausibleWeb.Live.VerificationTest do @tag :ee_only test "dismissing tells the client to close the websocket connection", %{conn: conn, site: site} do - stub_lookup_a_records(site.domain) + stub_dns() stub_verification_result(%{ "completed" => false, From 7908c7116a4c5d94911edd8584935b8392acc0d5 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 23 Jul 2026 17:16:54 +0100 Subject: [PATCH 14/21] fix CE tests --- .../verification/diagnostics_test.exs | 69 ++++++++++--------- .../controllers/stats_controller_test.exs | 24 ++++--- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/test/plausible/installation_support/verification/diagnostics_test.exs b/test/plausible/installation_support/verification/diagnostics_test.exs index bead8dfa1df0..21a065654faa 100644 --- a/test/plausible/installation_support/verification/diagnostics_test.exs +++ b/test/plausible/installation_support/verification/diagnostics_test.exs @@ -1,45 +1,48 @@ defmodule Plausible.InstallationSupport.Verification.DiagnosticsTest do use ExUnit.Case, async: true + use Plausible - alias Plausible.InstallationSupport.Verification.Diagnostics.Error + on_ee do + alias Plausible.InstallationSupport.Verification.Diagnostics.Error - describe "Error.new!/1" do - test "accepts a recommendation whose inline_links text appears exactly once" do - assert %Error{} = - Error.new!(%{ - message: "Something went wrong", - recommendation: "Check the docs for more info", - inline_links: [%{text: "docs", href: "https://plausible.io/docs"}] - }) - end + describe "Error.new!/1" do + test "accepts a recommendation whose inline_links text appears exactly once" do + assert %Error{} = + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the docs for more info", + inline_links: [%{text: "docs", href: "https://plausible.io/docs"}] + }) + end - test "raises when inline_links text isn't found in the recommendation" do - assert_raise ArgumentError, ~r/must appear exactly once/, fn -> - Error.new!(%{ - message: "Something went wrong", - recommendation: "Check the manual for more info", - inline_links: [%{text: "docs", href: "https://plausible.io/docs"}] - }) + test "raises when inline_links text isn't found in the recommendation" do + assert_raise ArgumentError, ~r/must appear exactly once/, fn -> + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the manual for more info", + inline_links: [%{text: "docs", href: "https://plausible.io/docs"}] + }) + end end - end - test "raises when inline_links text appears more than once in the recommendation" do - assert_raise ArgumentError, ~r/must appear exactly once/, fn -> - Error.new!(%{ - message: "Something went wrong", - recommendation: "Check the docs, or check the docs again", - inline_links: [%{text: "the docs", href: "https://plausible.io/docs"}] - }) + test "raises when inline_links text appears more than once in the recommendation" do + assert_raise ArgumentError, ~r/must appear exactly once/, fn -> + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the docs, or check the docs again", + inline_links: [%{text: "the docs", href: "https://plausible.io/docs"}] + }) + end end - end - test "raises when inline_links href doesn't point at plausible.io" do - assert_raise ArgumentError, ~r/must start with/, fn -> - Error.new!(%{ - message: "Something went wrong", - recommendation: "Check the docs for more info", - inline_links: [%{text: "docs", href: "https://example.com/docs"}] - }) + test "raises when inline_links href doesn't point at plausible.io" do + assert_raise ArgumentError, ~r/must start with/, fn -> + Error.new!(%{ + message: "Something went wrong", + recommendation: "Check the docs for more info", + inline_links: [%{text: "docs", href: "https://example.com/docs"}] + }) + end end end end diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index fab0eef0ba75..77c4c7004c4b 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -147,18 +147,20 @@ defmodule PlausibleWeb.StatsControllerTest do assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" end - test "can view stats of a website I've created; verification banner only shows with the explicit param", - %{ - conn: conn, - site: site - } do - resp = get(conn, "/" <> site.domain) |> html_response(200) - assert text_of_attr(resp, @react_container, "data-logged-in") == "true" - refute resp =~ "Verifying your installation" + on_ee do + test "verification banner only shows with the explicit param", + %{ + conn: conn, + site: site + } do + resp = get(conn, "/" <> site.domain) |> html_response(200) + refute resp =~ "Verifying your installation" - resp = conn |> get("/" <> site.domain <> "?verify_installation=true") |> html_response(200) - assert text_of_attr(resp, @react_container, "data-logged-in") == "true" - assert resp =~ "Verifying your installation" + resp = + conn |> get("/" <> site.domain <> "?verify_installation=true") |> html_response(200) + + assert resp =~ "Verifying your installation" + end end on_ee do From 29f19a8e7aea1fde9fa76d7976d8d795e1b6b5a2 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 23 Jul 2026 17:43:14 +0100 Subject: [PATCH 15/21] improve stats_controller_test.exs --- .../controllers/stats_controller_test.exs | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 77c4c7004c4b..dd0ac8d8601b 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -3,6 +3,7 @@ defmodule PlausibleWeb.StatsControllerTest do use Plausible.Repo @react_container "div#stats-react-container" + @verification_banner "#verification-ui" describe "GET /:domain - anonymous user" do test "public site - shows site stats", %{conn: conn} do @@ -112,7 +113,7 @@ defmodule PlausibleWeb.StatsControllerTest do resp = get(conn, "/some-other-public-site.io") |> html_response(200) - refute resp =~ "Verifying your installation" + refute element_exists?(resp, @verification_banner) end test "public site - anonymous visitors never see the verification banner, even with the param", @@ -125,7 +126,7 @@ defmodule PlausibleWeb.StatsControllerTest do get(conn, "/some-other-public-site.io?verify_installation=true") |> html_response(200) assert text_of_attr(resp, @react_container, "data-logged-in") == "false" - refute resp =~ "Verifying your installation" + refute element_exists?(resp, @verification_banner) end test "can not view stats of a private website", %{conn: conn} do @@ -153,13 +154,12 @@ defmodule PlausibleWeb.StatsControllerTest do conn: conn, site: site } do - resp = get(conn, "/" <> site.domain) |> html_response(200) - refute resp =~ "Verifying your installation" + resp = get(conn, "/#{site.domain}") |> html_response(200) + refute element_exists?(resp, @verification_banner) - resp = - conn |> get("/" <> site.domain <> "?verify_installation=true") |> html_response(200) + resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) - assert resp =~ "Verifying your installation" + assert element_exists?(resp, @verification_banner) end end @@ -234,21 +234,22 @@ defmodule PlausibleWeb.StatsControllerTest do assert cv.native_stats_start_at == twenty_days_ago end - test "does not redirect consolidated views to verification", %{ - conn: conn, - user: user - } do + test "does not show verification banner for consolidated views even with the explicit param", + %{ + conn: conn, + user: user + } do new_site(owner: user) new_site(owner: user) cv = user |> team_of() |> new_consolidated_view() - conn = get(conn, "/" <> cv.domain) - resp = html_response(conn, 200) + resp = get(conn, "/#{cv.domain}?verify_installation=true") |> html_response(200) assert text_of_attr(resp, @react_container, "data-domain") == cv.domain assert text_of_attr(resp, @react_container, "data-logged-in") == "true" assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner" assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" + refute element_exists?(resp, @verification_banner) end test "redirects to /sites if for some reason ineligible anymore", %{ @@ -392,11 +393,15 @@ defmodule PlausibleWeb.StatsControllerTest do assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}" end - test "can enter verification when site is without stats", %{conn: conn} do - site = new_site() + test "can enter verification regardless of whether the site has stats or not", %{conn: conn} do + site_without_stats = new_site() + site_with_stats = new_site() + populate_stats(site_with_stats, [build(:pageview)]) - resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) - assert resp =~ "Verifying your installation" + for site <- [site_without_stats, site_with_stats] do + resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) + assert element_exists?(resp, @verification_banner) + end end test "can view a private locked dashboard with stats", %{conn: conn} do @@ -410,13 +415,19 @@ defmodule PlausibleWeb.StatsControllerTest do assert resp =~ "This dashboard is actually locked" end - test "can view private locked verification without stats", %{conn: conn} do - user = new_user() - site = new_site(owner: user) - site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() + test "can trigger verification on a locked private dashboard regardless of whether the site has stats or not", + %{conn: conn} do + site_without_stats = new_site(owner: new_user()) + site_without_stats.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() - resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) - assert resp =~ "Verifying your installation" + site_with_stats = new_site(owner: new_user()) + populate_stats(site_with_stats, [build(:pageview)]) + site_with_stats.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() + + for site <- [site_without_stats, site_with_stats] do + resp = get(conn, "/#{site.domain}?verify_installation=true") |> html_response(200) + assert element_exists?(resp, @verification_banner) + end end test "can view a locked public dashboard", %{conn: conn} do From 334765dab660b2f953cf1481be1749d0c1aa967c Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 23 Jul 2026 18:39:50 +0100 Subject: [PATCH 16/21] E2E: named verification results + add genserver for mock scenarios --- .../verification/diagnostics.ex | 113 ++++++++++++++---- .../verification/mock_scenarios.ex | 51 ++++++++ lib/plausible/application.ex | 5 + .../verification/mock_scenarios_test.exs | 72 +++++++++++ 4 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 extra/lib/plausible/installation_support/verification/mock_scenarios.ex create mode 100644 test/plausible/installation_support/verification/mock_scenarios_test.exs diff --git a/extra/lib/plausible/installation_support/verification/diagnostics.ex b/extra/lib/plausible/installation_support/verification/diagnostics.ex index d464a0155fda..2e6eb1872404 100644 --- a/extra/lib/plausible/installation_support/verification/diagnostics.ex +++ b/extra/lib/plausible/installation_support/verification/diagnostics.ex @@ -102,7 +102,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do ) when response_status in [200, 202] and domain == expected_domain, - do: handled_error(@error_succeeds_only_after_cache_bust) + do: named_result!(:succeeds_only_after_cache_bust) def interpret( %__MODULE__{ @@ -119,7 +119,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do ) when response_status in [200, 202] and domain == expected_domain, - do: success() + do: named_result!(:success) def interpret( %__MODULE__{ @@ -137,8 +137,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do ) when response_status in [200, 202] and domain != expected_domain do - error_unexpected_domain(selected_installation_type) - |> handled_error() + named_result!(:unexpected_domain, installation_type: selected_installation_type) end @error_proxy_network_error Error.new!(%{ @@ -174,9 +173,9 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do proxying? = not String.starts_with?(request_url, PlausibleWeb.Endpoint.url()) if proxying? do - handled_error(@error_proxy_network_error) + named_result!(:proxy_network_error) else - handled_error(@error_plausible_network_error) + named_result!(:plausible_network_error) end end @@ -193,8 +192,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do ) when plausible_is_on_window != true and plausible_is_initialized != true do - error_plausible_not_found("manual") - |> handled_error() + named_result!(:plausible_not_found, installation_type: "manual") end @error_csp_disallowed Error.new!(%{ @@ -218,7 +216,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do _expected_domain, _url ), - do: handled_error(@error_csp_disallowed) + do: named_result!(:csp_disallowed) @error_domain_not_found Error.new!(%{ message: "We couldn't reach <%= @attempted_url %>", @@ -231,9 +229,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do when code in [:domain_not_found, :invalid_url] do attempted_url = if url, do: url, else: "https://#{expected_domain}" - @error_domain_not_found - |> handled_error(attempted_url: attempted_url) - |> struct!(data: %{offer_custom_url_input: true}) + named_result!(:domain_not_found, attempted_url: attempted_url) end @error_browserless_network Error.new!(%{ @@ -251,9 +247,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do when is_binary(url) do attempted_url = shorten_url(url) - @error_browserless_network - |> handled_error(attempted_url: attempted_url) - |> struct!(data: %{offer_custom_url_input: true}) + named_result!(:browserless_network_error, attempted_url: attempted_url) end @error_browserless_temporary Error.new!(%{ @@ -265,7 +259,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do def interpret(%__MODULE__{service_error: %{code: code}}, _expected_domain, _url) when code in [:bad_browserless_response, :browserless_timeout, :internal_check_timeout] do - unhandled_error(@error_browserless_temporary, browserless_issue: true) + named_result!(:browserless_temporary) end @error_unexpected_page_response Error.new!(%{ @@ -290,9 +284,10 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do plausible_is_initialized != true do attempted_url = shorten_url(url) - @error_unexpected_page_response - |> handled_error(attempted_url: attempted_url, page_response_status: page_response_status) - |> struct!(data: %{offer_custom_url_input: true}) + named_result!(:unexpected_page_response, + attempted_url: attempted_url, + page_response_status: page_response_status + ) end def interpret( @@ -304,13 +299,13 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do _expected_domain, _url ) do - error_plausible_not_found(selected_installation_type) - |> handled_error() + named_result!(:plausible_not_found, installation_type: selected_installation_type) end def interpret(%__MODULE__{} = diagnostics, _expected_domain, _url) do - error_plausible_not_found(diagnostics.selected_installation_type) - |> unhandled_error() + named_result!(:plausible_not_found_unhandled, + installation_type: diagnostics.selected_installation_type + ) end @message_plausible_not_found "We couldn't detect Plausible on your site" @@ -413,4 +408,76 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do recommendations: [%{text: error.recommendation, inline_links: error.inline_links}] } end + + @doc """ + Looks up a named interpretation result, optionally built from the given + assigns (e.g. `attempted_url`, `installation_type`) - keys that don't need + any just ignore them. + + Every result `interpret/3` can produce is named here, so that verification + can be mocked (see `Plausible.InstallationSupport.Verification.ChecksMock`) + by referring to the exact same result-construction code `interpret/3` + itself uses - a scenario name can never silently drift from what real + verification would have interpreted. + """ + @spec named_result!(atom()) :: Result.t() + def named_result!(key), do: named_result!(key, []) + + @spec named_result!(atom(), Keyword.t()) :: Result.t() + def named_result!(:success, _assigns), do: success() + + def named_result!(:succeeds_only_after_cache_bust, _assigns), + do: handled_error(@error_succeeds_only_after_cache_bust) + + def named_result!(:csp_disallowed, _assigns), do: handled_error(@error_csp_disallowed) + def named_result!(:proxy_network_error, _assigns), do: handled_error(@error_proxy_network_error) + + def named_result!(:plausible_network_error, _assigns), + do: handled_error(@error_plausible_network_error) + + def named_result!(:browserless_temporary, _assigns), + do: unhandled_error(@error_browserless_temporary, browserless_issue: true) + + def named_result!(:unexpected_domain, assigns) do + Keyword.fetch!(assigns, :installation_type) + |> error_unexpected_domain() + |> handled_error() + end + + def named_result!(:plausible_not_found, assigns) do + Keyword.fetch!(assigns, :installation_type) + |> error_plausible_not_found() + |> handled_error() + end + + def named_result!(:plausible_not_found_unhandled, assigns) do + Keyword.fetch!(assigns, :installation_type) + |> error_plausible_not_found() + |> unhandled_error() + end + + def named_result!(:domain_not_found, assigns) do + @error_domain_not_found + |> handled_error(attempted_url: Keyword.fetch!(assigns, :attempted_url)) + |> struct!(data: %{offer_custom_url_input: true}) + end + + def named_result!(:browserless_network_error, assigns) do + @error_browserless_network + |> handled_error(attempted_url: Keyword.fetch!(assigns, :attempted_url)) + |> struct!(data: %{offer_custom_url_input: true}) + end + + def named_result!(:unexpected_page_response, assigns) do + @error_unexpected_page_response + |> handled_error( + attempted_url: Keyword.fetch!(assigns, :attempted_url), + page_response_status: Keyword.fetch!(assigns, :page_response_status) + ) + |> struct!(data: %{offer_custom_url_input: true}) + end + + def named_result!(key, _assigns) do + raise ArgumentError, "No interpretation result named #{inspect(key)}" + end end diff --git a/extra/lib/plausible/installation_support/verification/mock_scenarios.ex b/extra/lib/plausible/installation_support/verification/mock_scenarios.ex new file mode 100644 index 000000000000..1c648cf03d28 --- /dev/null +++ b/extra/lib/plausible/installation_support/verification/mock_scenarios.ex @@ -0,0 +1,51 @@ +defmodule Plausible.InstallationSupport.Verification.MockScenarios do + @moduledoc """ + Per-domain registry of forced verification outcomes. + + Used to bypass the real DNS lookup and browserless check when iterating + on `PlausibleWeb.Live.Verification`'s banner UI locally, or when driving + it from Playwright e2e specs. + """ + + use GenServer + + @type scenario :: %{interpretation_result: atom(), slowdown: non_neg_integer() | nil} + + def start_link(_opts) do + GenServer.start_link(__MODULE__, %{}, name: __MODULE__) + end + + @doc """ + Registers a mock verification for `domain`. + + The `key` must be an atom that's recognized by + `Plausible.InstallationSupport.Verification.Diagnostics.named_result!/2`. + + ### Opts + + * `:slowdown` - overrides the check pipeline's default per-check delay + """ + @spec put(String.t(), atom(), Keyword.t()) :: :ok + def put(domain, key, opts \\ []) when is_binary(domain) and is_atom(key) do + scenario = %{interpretation_result: key, slowdown: Keyword.get(opts, :slowdown)} + GenServer.call(__MODULE__, {:put, domain, scenario}) + end + + @doc "Returns the scenario registered for `domain`, or `nil` if none was set." + @spec get(String.t()) :: scenario() | nil + def get(domain) when is_binary(domain) do + GenServer.call(__MODULE__, {:get, domain}) + end + + @impl true + def init(state), do: {:ok, state} + + @impl true + def handle_call({:put, domain, scenario}, _from, state) do + {:reply, :ok, Map.put(state, domain, scenario)} + end + + def handle_call({:get, domain}, _from, state) do + {:reply, Map.get(state, domain), state} + end +end diff --git a/lib/plausible/application.ex b/lib/plausible/application.ex index 20191fc70189..86de147f3b6a 100644 --- a/lib/plausible/application.ex +++ b/lib/plausible/application.ex @@ -194,6 +194,11 @@ defmodule Plausible.Application do end, Plausible.Ingestion.Counters, Plausible.Session.Salts, + on_ee do + if Mix.env() in [:dev, :e2e_test, :test] do + Plausible.InstallationSupport.Verification.MockScenarios + end + end, Supervisor.child_spec(Plausible.Event.WriteBuffer, id: Plausible.Event.WriteBuffer), Supervisor.child_spec(Plausible.Session.WriteBuffer, id: Plausible.Session.WriteBuffer), ReferrerBlocklist, diff --git a/test/plausible/installation_support/verification/mock_scenarios_test.exs b/test/plausible/installation_support/verification/mock_scenarios_test.exs new file mode 100644 index 000000000000..bcf7f2d93a3a --- /dev/null +++ b/test/plausible/installation_support/verification/mock_scenarios_test.exs @@ -0,0 +1,72 @@ +defmodule Plausible.InstallationSupport.Verification.MockScenariosTest do + use Plausible.DataCase, async: true + + on_ee do + alias Plausible.InstallationSupport.Verification.MockScenarios + + test "get/1 returns nil for a domain with no registered scenario" do + site = insert(:site) + + assert MockScenarios.get(site.domain) == nil + end + + test "put/3 registers a scenario, get/1 returns it" do + site = insert(:site) + + :ok = MockScenarios.put(site.domain, :success, []) + + assert MockScenarios.get(site.domain) == %{ + interpretation_result: :success, + slowdown: nil + } + end + + test "put/3 stores a slowdown opt alongside the interpretation result" do + site = insert(:site) + + :ok = MockScenarios.put(site.domain, :domain_not_found, slowdown: 2000) + + assert MockScenarios.get(site.domain) == %{ + interpretation_result: :domain_not_found, + slowdown: 2000 + } + end + + test "put/3 overwrites a previously registered scenario for the same domain" do + site = insert(:site) + + :ok = MockScenarios.put(site.domain, :success, []) + :ok = MockScenarios.put(site.domain, :csp_disallowed, []) + + assert MockScenarios.get(site.domain) == %{ + interpretation_result: :csp_disallowed, + slowdown: nil + } + end + + test "scenarios registered for one domain never leak into another domain on the same registry" do + site_a = insert(:site) + site_b = insert(:site) + + :ok = MockScenarios.put(site_a.domain, :success, []) + :ok = MockScenarios.put(site_b.domain, :domain_not_found, slowdown: 500) + + assert MockScenarios.get(site_a.domain) == %{ + interpretation_result: :success, + slowdown: nil + } + + assert MockScenarios.get(site_b.domain) == %{ + interpretation_result: :domain_not_found, + slowdown: 500 + } + + :ok = MockScenarios.put(site_a.domain, :csp_disallowed, []) + + assert MockScenarios.get(site_b.domain) == %{ + interpretation_result: :domain_not_found, + slowdown: 500 + } + end + end +end From 511af2cc926c1d247959b057f1cdc38f0b1b82b0 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Thu, 23 Jul 2026 20:09:41 +0100 Subject: [PATCH 17/21] E2E: checks_mock module --- .../verification/checks_mock.ex | 121 ++++++++++++++++++ .../verification/checks_mock_test.exs | 115 +++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 extra/lib/plausible/installation_support/verification/checks_mock.ex create mode 100644 test/plausible/installation_support/verification/checks_mock_test.exs diff --git a/extra/lib/plausible/installation_support/verification/checks_mock.ex b/extra/lib/plausible/installation_support/verification/checks_mock.ex new file mode 100644 index 000000000000..bcec4ec67550 --- /dev/null +++ b/extra/lib/plausible/installation_support/verification/checks_mock.ex @@ -0,0 +1,121 @@ +defmodule Plausible.InstallationSupport.Verification.ChecksMock do + @moduledoc """ + Drop-in replacement for `Plausible.InstallationSupport.Verification.Checks` + that never performs a real DNS lookup or browserless check for a domain + with a registered mock scenario. Used locally (`:dev`) and in Playwright + e2e specs (`:e2e_test`) to deterministically drive + `PlausibleWeb.Live.Verification`'s banner UI - see + `Plausible.InstallationSupport.verification_checks_mod/0`. + + When no scenario is registered for a domain (see + `Plausible.InstallationSupport.Verification.MockScenarios.put/3`): + + * in `:dev`, falls back to the real `Checks` module - casually loading a + site with `?verify_installation=true` still verifies for real unless + you've deliberately mocked that domain. + + * everywhere else (`:e2e_test`, and `:test` for this module's own + tests), raises - every e2e spec that drives verification is expected + to register a scenario before triggering it, and it shouldn't + silently fall back to a real, slow, non-deterministic check. + """ + + alias Plausible.InstallationSupport.{State, CheckRunner, Checks} + alias Plausible.InstallationSupport.Verification.{Diagnostics, MockScenarios} + alias Plausible.InstallationSupport.Verification.Checks, as: RealChecks + + defmodule FakeUrlCheck do + @moduledoc false + use Plausible.InstallationSupport.Check + + @impl true + def report_progress_as, do: Checks.Url.report_progress_as() + + @impl true + def perform(state, _opts), do: state + end + + defmodule FakeVerifyInstallationCheck do + @moduledoc false + use Plausible.InstallationSupport.Check + + @impl true + def report_progress_as, do: Checks.VerifyInstallation.report_progress_as() + + @impl true + def perform(state, _opts), do: state + end + + defmodule FakeVerifyInstallationCacheBustCheck do + @moduledoc false + use Plausible.InstallationSupport.Check + + @impl true + def report_progress_as, do: Checks.VerifyInstallationCacheBust.report_progress_as() + + @impl true + def perform(state, _opts), do: state + end + + @spec run(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, pid()} | State.t() + def run(url, data_domain, installation_type, opts \\ []) do + case MockScenarios.get(data_domain) do + nil -> + raise_unless_dev_env!(data_domain) + RealChecks.run(url, data_domain, installation_type, opts) + + scenario -> + run_mocked(url, data_domain, installation_type, opts, scenario) + end + end + + defp run_mocked(url, data_domain, installation_type, opts, scenario) do + report_to = Keyword.get(opts, :report_to, self()) + async? = Keyword.get(opts, :async?, true) + slowdown = scenario.slowdown || Keyword.get(opts, :slowdown, 500) + + init_state = %State{ + url: url || "https://#{data_domain}", + data_domain: data_domain, + report_to: report_to, + diagnostics: %Diagnostics{selected_installation_type: installation_type} + } + + checks = [ + {FakeUrlCheck, []}, + {FakeVerifyInstallationCheck, []}, + {FakeVerifyInstallationCacheBustCheck, []} + ] + + CheckRunner.run(init_state, checks, + async?: async?, + report_to: report_to, + slowdown: slowdown + ) + end + + @spec interpret_diagnostics(State.t()) :: Plausible.InstallationSupport.Result.t() + def interpret_diagnostics(%State{data_domain: data_domain} = state) do + case MockScenarios.get(data_domain) do + nil -> + raise_unless_dev_env!(data_domain) + RealChecks.interpret_diagnostics(state) + + scenario -> + Diagnostics.named_result!(scenario.interpretation_result, + installation_type: state.diagnostics.selected_installation_type, + attempted_url: state.url, + page_response_status: 500 + ) + end + end + + defp raise_unless_dev_env!(data_domain) do + if Mix.env() != :dev do + raise """ + ChecksMock was used to verify #{inspect(data_domain)}, but no scenario \ + is registered for it. Call MockScenarios.put/3 first. + """ + end + end +end diff --git a/test/plausible/installation_support/verification/checks_mock_test.exs b/test/plausible/installation_support/verification/checks_mock_test.exs new file mode 100644 index 000000000000..979d4ae91942 --- /dev/null +++ b/test/plausible/installation_support/verification/checks_mock_test.exs @@ -0,0 +1,115 @@ +defmodule Plausible.InstallationSupport.Verification.ChecksMockTest do + use Plausible.DataCase, async: true + + on_ee do + alias Plausible.InstallationSupport.{Checks, Result} + alias Plausible.InstallationSupport.Verification.{ChecksMock, Diagnostics, MockScenarios} + + @url "https://example.com" + + describe "run/4" do + test "raises when no scenario is registered for the domain" do + domain = insert(:site).domain + + assert_raise RuntimeError, ~r/no scenario is registered/, fn -> + ChecksMock.run(@url, domain, "manual", async?: false, slowdown: 0, report_to: nil) + end + end + + test "runs synchronously, keeping the given installation_type in the resulting state" do + domain = insert(:site).domain + :ok = MockScenarios.put(domain, :success) + + state = + ChecksMock.run(@url, domain, "wordpress", async?: false, slowdown: 0, report_to: nil) + + assert state.url == @url + assert state.data_domain == domain + assert state.diagnostics.selected_installation_type == "wordpress" + end + + test "notifies check_start for all 3 checks with the same messages as real verification, then all_checks_done" do + domain = insert(:site).domain + :ok = MockScenarios.put(domain, :success) + + ChecksMock.run(@url, domain, "manual", async?: false, slowdown: 0, report_to: self()) + + assert_received {:check_start, {ChecksMock.FakeUrlCheck, _state}} + assert_received {:check_start, {ChecksMock.FakeVerifyInstallationCheck, _state}} + assert_received {:check_start, {ChecksMock.FakeVerifyInstallationCacheBustCheck, _state}} + assert_received {:all_checks_done, %{data_domain: ^domain}} + + assert ChecksMock.FakeUrlCheck.report_progress_as() == + Checks.Url.report_progress_as() + + assert ChecksMock.FakeVerifyInstallationCheck.report_progress_as() == + Checks.VerifyInstallation.report_progress_as() + + assert ChecksMock.FakeVerifyInstallationCacheBustCheck.report_progress_as() == + Checks.VerifyInstallationCacheBust.report_progress_as() + end + + test "defaults state.url from data_domain when called with url: nil, mirroring the real Url check" do + domain = insert(:site).domain + :ok = MockScenarios.put(domain, :success) + + state = ChecksMock.run(nil, domain, "manual", async?: false, slowdown: 0, report_to: nil) + + assert state.url == "https://#{domain}" + end + end + + describe "interpret_diagnostics/1" do + test "returns the named result for the registered scenario" do + domain = insert(:site).domain + :ok = MockScenarios.put(domain, :success) + + state = ChecksMock.run(@url, domain, "manual", async?: false, slowdown: 0, report_to: nil) + + assert %Result{ok?: true} = ChecksMock.interpret_diagnostics(state) + end + + test "returns interpretation based on installation type" do + domain = insert(:site).domain + :ok = MockScenarios.put(domain, :plausible_not_found) + + state = + ChecksMock.run(@url, domain, "wordpress", async?: false, slowdown: 0, report_to: nil) + + assert %Result{ + ok?: false, + recommendations: [%{text: recommendation}] + } = ChecksMock.interpret_diagnostics(state) + + assert recommendation =~ "WordPress plugin" + end + + test "uses state.url (e.g. a custom retry URL) as attempted_url, not just the bare domain" do + domain = insert(:site).domain + :ok = MockScenarios.put(domain, :domain_not_found) + + custom_url = "https://abc.de" + + state = + ChecksMock.run(custom_url, domain, "manual", async?: false, slowdown: 0, report_to: nil) + + assert %Result{errors: [error]} = ChecksMock.interpret_diagnostics(state) + assert error =~ custom_url + end + + test "raises when no scenario is registered for the domain" do + domain = insert(:site).domain + + state = %Plausible.InstallationSupport.State{ + url: @url, + data_domain: domain, + diagnostics: %Diagnostics{selected_installation_type: "manual"} + } + + assert_raise RuntimeError, ~r/no scenario is registered/, fn -> + ChecksMock.interpret_diagnostics(state) + end + end + end + end +end From e4e61faeefb20f2580b4fc1321785bf3baae002d Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 27 Jul 2026 08:01:41 +0100 Subject: [PATCH 18/21] E2E: plug in the ChecksMock module --- .../installation_support/installation_support.ex | 8 ++++++++ extra/lib/plausible_web/live/verification.ex | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/extra/lib/plausible/installation_support/installation_support.ex b/extra/lib/plausible/installation_support/installation_support.ex index f724a96d28ea..c1320a62aa4a 100644 --- a/extra/lib/plausible/installation_support/installation_support.ex +++ b/extra/lib/plausible/installation_support/installation_support.ex @@ -12,6 +12,14 @@ defmodule Plausible.InstallationSupport do def user_agent() do "Plausible Verification Agent - if abused, contact support@plausible.io" end + + def verification_checks_mod do + if Mix.env() in [:dev, :e2e_test] do + Plausible.InstallationSupport.Verification.ChecksMock + else + Plausible.InstallationSupport.Verification.Checks + end + end else def user_agent() do "Plausible Community Edition" diff --git a/extra/lib/plausible_web/live/verification.ex b/extra/lib/plausible_web/live/verification.ex index c0ea7b5ce793..1d9550a6b5da 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -5,7 +5,8 @@ defmodule PlausibleWeb.Live.Verification do """ use PlausibleWeb, :live_view - alias Plausible.InstallationSupport.{State, Verification} + alias Plausible.InstallationSupport + alias Plausible.InstallationSupport.State @component PlausibleWeb.Live.Components.VerificationBanner @slowdown_for_frequent_checking :timer.seconds(5) @@ -143,7 +144,7 @@ defmodule PlausibleWeb.Live.Verification do end {:ok, pid} = - Verification.Checks.run( + InstallationSupport.verification_checks_mod().run( socket.assigns.url_to_verify, domain, get_installation_type(socket.assigns.tracker_script_configuration), @@ -171,7 +172,7 @@ defmodule PlausibleWeb.Live.Verification do end def handle_info({:all_checks_done, %State{} = state}, socket) do - interpretation = Verification.Checks.interpret_diagnostics(state) + interpretation = InstallationSupport.verification_checks_mod().interpret_diagnostics(state) update_component(socket, finished?: true, From 7fa2c43797890ef5bc211825df967a069a162ca1 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 27 Jul 2026 08:27:34 +0100 Subject: [PATCH 19/21] E2E: put_verification_scenario endpoint --- e2e/tests/fixtures.ts | 22 +++++++++++++++++++ lib/plausible_web/router.ex | 1 + .../support/dev/controllers/e2e_controller.ex | 10 +++++++++ 3 files changed, 33 insertions(+) diff --git a/e2e/tests/fixtures.ts b/e2e/tests/fixtures.ts index 55bb6a99f2c8..2d4c03d9b226 100644 --- a/e2e/tests/fixtures.ts +++ b/e2e/tests/fixtures.ts @@ -241,6 +241,28 @@ export async function populateStats({ expect(response.ok()).toBeTruthy() } +export async function setVerificationScenario({ + request, + domain, + scenario, + options +}: { + request: APIRequestContext + domain: string + scenario: string + options?: { slowdown?: number } +}) { + const response = await request.put('/e2e-tests/verification', { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + data: { domain: domain, scenario: scenario, options: options } + }) + + expect(response.ok()).toBeTruthy() +} + export async function addGoal({ request, domain, diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index b88211a93809..e867512f66e4 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -176,6 +176,7 @@ defmodule PlausibleWeb.Router do post "/stats", E2EController, :populate_stats post "/funnel", E2EController, :create_funnel post "/goal", E2EController, :create_goal + put "/verification", E2EController, :put_verification_scenario end end end diff --git a/test/support/dev/controllers/e2e_controller.ex b/test/support/dev/controllers/e2e_controller.ex index bd61fdfbf2e1..fa1ed838de98 100644 --- a/test/support/dev/controllers/e2e_controller.ex +++ b/test/support/dev/controllers/e2e_controller.ex @@ -96,6 +96,16 @@ defmodule PlausibleWeb.E2EController do send_resp(conn, 200, Jason.encode!(%{"ok" => true})) end + def put_verification_scenario(conn, %{"domain" => domain, "scenario" => scenario} = params) do + key = String.to_existing_atom(scenario) + + opts = [slowdown: params["options"]["slowdown"] || 0] + + :ok = Plausible.InstallationSupport.Verification.MockScenarios.put(domain, key, opts) + + send_resp(conn, 200, Jason.encode!(%{"ok" => true})) + end + defp get_goal(site, name) do Plausible.Repo.get_by!(Plausible.Goal, site_id: site.id, display_name: name) end From fd3c872172eb23ab7f07b9873a51393af0fce710 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 27 Jul 2026 09:08:47 +0100 Subject: [PATCH 20/21] e2e test for verification success --- e2e/tests/dashboard/verification.spec.ts | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 e2e/tests/dashboard/verification.spec.ts diff --git a/e2e/tests/dashboard/verification.spec.ts b/e2e/tests/dashboard/verification.spec.ts new file mode 100644 index 000000000000..00a7168b24cb --- /dev/null +++ b/e2e/tests/dashboard/verification.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test' +import { setupSite, setVerificationScenario } from '../fixtures' +import { expectLiveViewConnected } from '../test-utils' + +const VERIFICATION_BANNER_SELECTOR = '#verification-ui' +const PROGRESS_MSG_SELECTOR = '#progress' + +const SUCCESS_MESSAGE = 'Tracking is active on your site' +const LOADING_STATE_TITLE = 'Verifying your installation' +const LOADING_STATE_CYCLED_MESSAGES = [ + /We're visiting your site to ensure that everything is working/, + /We're trying to reach your website/, + /We're verifying that your visitors are being counted correctly/ +] + +test('verification success', async ({ page, request }) => { + const { domain } = await setupSite({ page, request }) + + await setVerificationScenario({ + request, + domain, + scenario: 'success', + options: { slowdown: 500 } + }) + + await page.goto(`/${domain}?verify_installation=true`, { waitUntil: 'commit' }) + await expectLiveViewConnected(page) + + const banner = page.locator(VERIFICATION_BANNER_SELECTOR) + const progress = banner.locator(PROGRESS_MSG_SELECTOR) + + await expect(banner).toContainText(LOADING_STATE_TITLE) + + for (const msg of LOADING_STATE_CYCLED_MESSAGES) { + await expect(progress).toHaveText(msg) + } + + await expect(banner).toContainText(SUCCESS_MESSAGE) + + await banner.getByRole('button', { name: 'Dismiss' }).click() + + await expect(banner).toBeHidden() + await expect(page).not.toHaveURL(/verify_installation/) + + await page.reload({ waitUntil: 'commit' }) + + await expect(page.locator(VERIFICATION_BANNER_SELECTOR)).toBeHidden() +}) From 45547c409d53f20bafaa8a5a4d998e7ac17e77e8 Mon Sep 17 00:00:00 2001 From: Robert Joonas Date: Mon, 27 Jul 2026 09:21:48 +0100 Subject: [PATCH 21/21] get CI green --- assets/js/dashboard/email-reports-cta-banner.tsx | 4 ++-- .../live/components/verification_banner_test.exs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/assets/js/dashboard/email-reports-cta-banner.tsx b/assets/js/dashboard/email-reports-cta-banner.tsx index 8defbd306b23..6a69fb9d0c39 100644 --- a/assets/js/dashboard/email-reports-cta-banner.tsx +++ b/assets/js/dashboard/email-reports-cta-banner.tsx @@ -21,12 +21,12 @@ function getStorageKey(domain: string) { // Case 2 is the tricky one. By the time of the refresh, `site.statsBegin` // is already set, so that value alone can't distinguish "stats just // started" from "this site has always had stats". -// +// // The sessionStorage entry closes that gap -- it's stamped 'pending' the // moment stats are still absent, so a later reload can still recognize the // transition. It is only ever stamped while stats are absent, so established // sites never pick it up and can't retrigger the CTA. -// +// // Once shown, the same entry is stamped 'visible', so a refresh mid-display // resumes the CTA instead of re-deciding from scratch -- but only for three // seconds -- past that, the sessionStorage entry clears itself out and a diff --git a/test/plausible_web/live/components/verification_banner_test.exs b/test/plausible_web/live/components/verification_banner_test.exs index 0ecdf1bc8a58..22d638c9d794 100644 --- a/test/plausible_web/live/components/verification_banner_test.exs +++ b/test/plausible_web/live/components/verification_banner_test.exs @@ -9,10 +9,11 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do @moduletag :capture_log @component PlausibleWeb.Live.Components.VerificationBanner + @banner "#verification-ui" @progress ~s|#verification-ui p#progress| - @loading_spinner ~s|#verification-ui svg.animate-spin| - @check_circle ~s|#verification-ui #check-circle| + @loading_spinner ~s|#{@banner} svg.animate-spin| + @check_circle ~s|#{@banner} #check-circle| @recommendations ~s|#recommendation| @super_admin_report ~s|#super-admin-report| @@ -34,7 +35,7 @@ defmodule PlausibleWeb.Live.Components.VerificationBannerTest do refute element_exists?(html, @loading_spinner) refute element_exists?(html, @check_circle) refute element_exists?(html, @recommendations) - assert html =~ "We couldn't verify your installation" + assert text_of_element(html, @banner) =~ "We couldn't verify your installation" end test "renders diagnostic interpretation with inline verify link and standalone review-installation sentence" do