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 ( +
+ + 🎉 + + Your first pageview has landed! + {' '} + + Get weekly traffic reports by email → + +
+ ) +} diff --git a/assets/js/dashboard/index.tsx b/assets/js/dashboard/index.tsx index 1736f963d227..bb2f7ac6be1f 100644 --- a/assets/js/dashboard/index.tsx +++ b/assets/js/dashboard/index.tsx @@ -11,6 +11,8 @@ 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' +import { EmailReportsCTABanner } from './email-reports-cta-banner' function DashboardStats({ importedDataInView, @@ -21,7 +23,11 @@ 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 ( -
+
<>
{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 new file mode 100644 index 000000000000..19d482ecb111 --- /dev/null +++ b/assets/js/dashboard/verification/portal.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react' +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.VerificationBanner.query_params/0 + */ + queryParams: string[] +} + +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 7df1ef9984c7..8cccfde07821 100644 --- a/assets/js/liveview/live_socket.js +++ b/assets/js/liveview/live_socket.js @@ -25,6 +25,15 @@ if (csrfToken && websocketUrl) { }) } } + // 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/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/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 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 0fc148d3a2b0..ff4742bf3a56 100644 --- a/extra/lib/plausible_web/live/verification.ex +++ b/extra/lib/plausible_web/live/verification.ex @@ -1,21 +1,19 @@ 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 - import PlausibleWeb.Components.Generic - alias Plausible.InstallationSupport.{State, Verification} - @component PlausibleWeb.Live.Components.Verification - @slowdown_for_frequent_checking :timer.seconds(5) + @component PlausibleWeb.Live.Components.VerificationBanner + @slowdown_for_frequent_checking :timer.seconds(0) + @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 @@ -36,9 +34,9 @@ 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) - custom_url_input? = params["custom_url"] == "true" + tracker_script_configuration = + PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site) socket = assign(socket, @@ -46,20 +44,19 @@ defmodule PlausibleWeb.Live.Verification do site: site, super_admin?: super_admin?, domain: domain, - has_pageviews?: has_pageviews?, component: @component, - installation_type: get_installation_type(params, site), + tracker_script_configuration: tracker_script_configuration, 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, + dismissed?: false ) - if connected?(socket) and not custom_url_input? do + if connected?(socket) do launch_delayed(socket) end @@ -67,19 +64,32 @@ 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?} module={@component} - installation_type={@installation_type} domain={@domain} id="verification-standalone" attempts={@attempts} flow={@flow} - awaiting_first_pageview?={not @has_pageviews?} super_admin?={@super_admin?} + custom_url_input?={@custom_url_input?} + dismissed?={@dismissed?} /> """ end @@ -94,6 +104,18 @@ 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 + + # 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 @@ -124,7 +146,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 ) @@ -151,10 +173,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?, @@ -165,58 +183,18 @@ defmodule PlausibleWeb.Live.Verification do {:noreply, assign(socket, checks_pid: nil)} 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 - @supported_installation_types_atoms PlausibleWeb.Tracker.supported_installation_types() |> Enum.map(&String.to_atom/1) - defp get_installation_type(params, site) do - cond do - params["installation_type"] in PlausibleWeb.Tracker.supported_installation_types() -> - params["installation_type"] - - (saved_installation_type = get_saved_installation_type(site)) in @supported_installation_types_atoms -> - Atom.to_string(saved_installation_type) - - 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 - 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) + PlausibleWeb.Tracker.fallback_installation_type() 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", @@ -238,51 +216,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 - - 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. -

-
-
- - -
- -
-
- - """ - 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/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/components/generic.ex b/lib/plausible_web/components/generic.ex index ffdf5277c06a..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}

@@ -850,6 +870,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/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/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 38e9169f66f9..d1cabbde2a1c 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,14 @@ 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: + PlausibleWeb.Live.Components.VerificationBanner.query_params() + |> Map.new(&{&1, conn.params[&1]}) + |> Map.put("domain", site.domain) ) - - !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 +428,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 deleted file mode 100644 index ef8113d0a0a8..000000000000 --- a/lib/plausible_web/live/components/verification.ex +++ /dev/null @@ -1,205 +0,0 @@ -defmodule PlausibleWeb.Live.Components.Verification do - @moduledoc """ - This component is responsible for rendering the verification progress - and diagnostics. - """ - use Phoenix.LiveComponent - use Plausible - - alias PlausibleWeb.Router.Helpers, as: Routes - alias Plausible.InstallationSupport.{State, Result} - - import PlausibleWeb.Components.Generic - - attr(:domain, :string, required: true) - - attr(:message, :string, - default: "We're visiting your site to ensure that everything is working" - ) - - attr(:super_admin?, :boolean, default: false) - attr(:finished?, :boolean, default: false) - attr(:success?, :boolean, default: false) - attr(:verification_state, State, default: nil) - attr(:interpretation, Result, default: nil) - attr(:attempts, :integer, default: 0) - attr(:flow, :string, default: "") - attr(:installation_type, :string, default: nil) - attr(:awaiting_first_pageview?, :boolean, default: false) - - def render(assigns) do - ~H""" -
- <.render_progress :if={not @finished?} message={@message} /> - <.render_success - :if={@finished? and @success?} - awaiting_first_pageview?={@awaiting_first_pageview?} - domain={@domain} - /> - <.render_failed - :if={@finished? and not @success?} - interpretation={@interpretation} - attempts={@attempts} - domain={@domain} - flow={@flow} - installation_type={@installation_type} - /> - <.render_super_admin_diagnostics - :if={not is_nil(@verification_state) && @super_admin? && @finished?} - verification_state={@verification_state} - /> -
- """ - end - - defp render_progress(assigns) do - ~H""" - <.focus_box> -
-
-
-
- <.title>Verifying your installation -

{@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 - - - """ - 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"> - Verify installation again - -
- <:footer> - <.focus_list> - <:item :if={ - @interpretation && is_map(@interpretation.data) && - @interpretation.data[:offer_custom_url_input] - }> - - 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 - - - - - - """ - end - - defp render_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)} - - - -
-
- - """ - end - - defp to_string_value(value) when is_binary(value), do: value - defp to_string_value(value), do: inspect(value) -end diff --git a/lib/plausible_web/live/components/verification_banner.ex b/lib/plausible_web/live/components/verification_banner.ex new file mode 100644 index 000000000000..b40731c01db9 --- /dev/null +++ b/lib/plausible_web/live/components/verification_banner.ex @@ -0,0 +1,333 @@ +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. + """ + use Phoenix.LiveComponent + use Plausible + + alias PlausibleWeb.Router.Helpers, as: Routes + alias Plausible.InstallationSupport.{State, Result} + + import PlausibleWeb.Components.Generic + import PlausibleWeb.Live.Components.Form + + @container_id "verification-ui" + + # 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) + + attr(:message, :string, + default: "We're visiting your site to ensure that everything is working" + ) + + attr(:super_admin?, :boolean, default: false) + attr(:finished?, :boolean, default: false) + attr(:success?, :boolean, default: false) + attr(:verification_state, State, default: nil) + attr(:interpretation, Result, default: nil) + attr(:attempts, :integer, default: 0) + attr(:flow, :string, default: "") + attr(:custom_url_input?, :boolean, default: false) + attr(:dismissed?, :boolean, default: false) + + def render(assigns) do + assigns = + assigns + |> 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?} + domain={@domain} + super_admin?={@super_admin?} + verification_state={@verification_state} + /> + <.render_failed + :if={@finished? and not @success?} + interpretation={@interpretation} + attempts={@attempts} + domain={@domain} + flow={@flow} + super_admin?={@super_admin?} + verification_state={@verification_state} + custom_url_input?={@custom_url_input?} + /> +
+ """ + 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={:indigo} + title_class="text-base font-semibold text-gray-900 dark:text-gray-100" + > + <:icon> + <.spinner class="mt-0.5 size-4.5" /> + +

+ {@message}... +

+ + """ + end + + defp render_success(assigns) do + ~H""" + <.notice + title="Tracking is active on your site" + theme={:green} + title_class="text-base font-semibold text-green-800 dark:text-green-300" + icon_class="!mt-0.5 size-5 text-green-800 dark:text-green-300" + > + <:icon> + + +

+ Your dashboard is ready. Data will appear here as soon as visitors start arriving. +

+ <.super_admin_diagnostics + :if={@super_admin? and not is_nil(@verification_state)} + verification_state={@verification_state} + /> + + """ + end + + defp render_failed(assigns) do + assigns = + assign( + assigns, + :offer_custom_url_input?, + offer_custom_url_input?(assigns.interpretation) + ) + + ~H""" + <.notice + title={ + if @interpretation, + do: List.first(@interpretation.errors), + else: "We couldn't verify your installation" + } + theme={:yellow} + show_icon={false} + title_class="text-base font-semibold text-yellow-800 dark:text-yellow-400" + > + <.recommendation + :if={@interpretation} + interpretation={@interpretation} + offer_custom_url_input?={@offer_custom_url_input?} + domain={@domain} + flow={@flow} + /> +
+ <.retry_form_or_button custom_url_input?={@custom_url_input?} domain={@domain} /> + <.button_link + :if={not @custom_url_input? and @offer_custom_url_input?} + mt?={false} + href="#" + phx-click="show-custom-url-form" + id="verify-custom-url-link" + theme="ghost" + size="sm" + class="hover:bg-gray-600/10 dark:hover:bg-white/10 hover:border-transparent dark:hover:border-transparent" + > + Try another URL + + <.button_link + :if={not @custom_url_input? and not @offer_custom_url_input?} + mt?={false} + href={Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain, flow: @flow)} + theme="ghost" + size="sm" + class="hover:bg-gray-600/10 dark:hover:bg-white/10 hover:border-transparent dark:hover:border-transparent" + > + Review installation + +
+ <.contact_us_link :if={ee?() and @attempts >= 3} /> + <.super_admin_diagnostics + :if={@super_admin? and not is_nil(@verification_state)} + verification_state={@verification_state} + /> + + """ + end + + defp offer_custom_url_input?(interpretation) do + match?(%{data: %{offer_custom_url_input: true}}, interpretation) + end + + defp recommendation(assigns) do + recommendation = List.first(assigns.interpretation.recommendations) + main = render_recommendation(recommendation.text, recommendation.inline_links) + + body = + if assigns.offer_custom_url_input? do + [main, ". ", review_installation_link_sentence(assigns), "."] + else + [main, "."] + end + + assigns = assign(assigns, :body, body) + + ~H""" +

+ {@body} +

+ """ + end + + defp render_recommendation(text, inline_links) do + html = + Enum.reduce(inline_links, html_escape(text), fn %{text: link_text, href: href}, acc -> + String.replace(acc, link_text, link_markup(link_text, href), global: false) + end) + + Phoenix.HTML.raw(html) + end + + defp review_installation_link_sentence(assigns) do + review_installation_url = + Routes.site_path(PlausibleWeb.Endpoint, :installation, assigns.domain, flow: assigns.flow) + + render_recommendation("See your installation instructions again here", [ + %{text: "here", href: review_installation_url} + ]) + end + + defp link_markup(text, href) do + external_attrs = + if String.starts_with?(href, "http"), + do: ~s| target="_blank" rel="noopener noreferrer"|, + else: "" + + ~s|#{html_escape(text)}| + end + + defp html_escape(value) do + value |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() + 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-64 h-[38px] dark:bg-white/15 dark:border-transparent" + placeholder={"https://#{@domain}"} + value={"https://#{@domain}"} + /> + <.button type="submit" mt?={false} theme="primary" size="sm"> + Verify URL + +
+ """ + end + + defp retry_form_or_button(assigns) do + ~H""" + <.button_link + mt?={false} + href="#" + phx-click="retry" + theme="secondary" + size="sm" + class="dark:bg-white/15 dark:hover:bg-white/20 dark:border-transparent" + > + Check again + + """ + end + + defp contact_us_link(assigns) do + ~H""" +

+ Need help? {Phoenix.HTML.raw(link_markup("Contact us", "https://plausible.io/contact"))} +

+ """ + end + + defp super_admin_diagnostics(assigns) do + ~H""" +
+

+ + 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 + + defp to_string_value(value) when is_binary(value), do: value + defp to_string_value(value), do: inspect(value) +end diff --git a/lib/plausible_web/live/installation.ex b/lib/plausible_web/live/installation.ex index 4e865013b9ff..e1b38e645664 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> @@ -317,21 +315,21 @@ 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) - {: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 + ) + else + 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/installation/instructions.ex b/lib/plausible_web/live/installation/instructions.ex index 31d7292c301c..9716d01ea8e7 100644 --- a/lib/plausible_web/live/installation/instructions.ex +++ b/lib/plausible_web/live/installation/instructions.ex @@ -3,6 +3,8 @@ defmodule PlausibleWeb.Live.Installation.Instructions do Instruction forms and components for the Installation module """ use PlausibleWeb, :component + import PlausibleWeb.Components.Generic + alias Plausible.Site.TrackerScriptConfiguration attr :tracker_script_configuration_form, :map, required: true @@ -21,11 +23,7 @@ defmodule PlausibleWeb.Live.Installation.Instructions do Once done, click the button below to verify your installation.
- <.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 diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index 11217bd370a1..ed6b72daa1e7 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" >
    @@ -567,7 +584,7 @@ defmodule PlausibleWeb.Live.Sites do
  • - <.site_stats sparkline={@sparkline} /> + <.site_stats sparkline={@sparkline} needs_verification?={@needs_verification?} />
    @@ -657,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""" @@ -685,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/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..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 %>
    + <%= 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/installation_support/verification/checks_test.exs b/test/plausible/installation_support/verification/checks_test.exs index a0b7ef39a8dd..b9e0bde8e134 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/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index bb76d271a853..22407b15979e 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -362,7 +362,7 @@ defmodule PlausibleWeb.SiteControllerTest do }) assert redirected_to(conn) == - "/#{URI.encode_www_form("éxample.com")}/installation?site_created=true&flow=" + "/#{URI.encode_www_form("éxample.com")}/installation?flow=" assert site = Repo.get_by(Plausible.Site, domain: "éxample.com") assert site.timezone == "Europe/London" @@ -480,7 +480,7 @@ defmodule PlausibleWeb.SiteControllerTest do } }) - assert redirected_to(conn) == "/example.com/installation?site_created=true&flow=" + assert redirected_to(conn) == "/example.com/installation?flow=" assert Repo.get_by(Plausible.Site, domain: "example.com") end @@ -501,7 +501,7 @@ defmodule PlausibleWeb.SiteControllerTest do } }) - assert redirected_to(conn) == "/example.com/installation?site_created=true&flow=" + assert redirected_to(conn) == "/example.com/installation?flow=" assert Plausible.Teams.Billing.site_usage(team) == 3 end @@ -515,7 +515,7 @@ defmodule PlausibleWeb.SiteControllerTest do } }) - assert redirected_to(conn) == "/example.com/installation?site_created=true&flow=" + assert redirected_to(conn) == "/example.com/installation?flow=" assert Repo.get_by(Plausible.Site, domain: "example.com") end end @@ -555,7 +555,7 @@ defmodule PlausibleWeb.SiteControllerTest do }) assert redirected_to(conn) == - "/example.com%2Fsome_blog_site/installation?site_created=true&flow=" + "/example.com%2Fsome_blog_site/installation?flow=" end test "renders form again when it is a duplicate domain", %{conn: conn} do @@ -618,7 +618,7 @@ defmodule PlausibleWeb.SiteControllerTest do }) assert redirected_to(conn) == - "/example.com/installation?site_created=true&flow=" + "/example.com/installation?flow=" end for role <- [:owner, :admin, :editor] 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_banner_test.exs b/test/plausible_web/live/components/verification_banner_test.exs new file mode 100644 index 000000000000..0ecdf1bc8a58 --- /dev/null +++ b/test/plausible_web/live/components/verification_banner_test.exs @@ -0,0 +1,243 @@ +defmodule PlausibleWeb.Live.Components.VerificationBannerTest do + use PlausibleWeb.ConnCase, async: true + + on_ee do + import Phoenix.LiveViewTest, only: [render_component: 2] + + alias Plausible.InstallationSupport.{State, Verification} + + @moduletag :capture_log + + @component PlausibleWeb.Live.Components.VerificationBanner + @progress ~s|#verification-ui p#progress| + + @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| + + test "renders initial state" do + html = render_component(@component, domain: "example.com") + assert element_exists?(html, @progress) + + assert text_of_element(html, @progress) == + "We're visiting your site to ensure that everything is working..." + + assert element_exists?(html, @loading_spinner) + refute element_exists?(html, @recommendations) + refute element_exists?(html, @check_circle) + refute element_exists?(html, @super_admin_report) + end + + 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 html =~ "We couldn't verify your installation" + end + + test "renders diagnostic interpretation with inline verify link and standalone review-installation sentence" do + interpretation = + Verification.Checks.interpret_diagnostics(%State{ + url: "https://example.com", + data_domain: "example.com", + diagnostics: %Verification.Diagnostics{service_error: %{code: :domain_not_found}} + }) + + html = + render_component(@component, + domain: "example.com", + success?: false, + finished?: true, + interpretation: interpretation + ) + + assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1) + 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", + data_domain: "example.com", + diagnostics: %Verification.Diagnostics{} + } + + interpretation = Verification.Checks.interpret_diagnostics(state) + + html = + render_component(@component, + domain: "example.com", + success?: false, + finished?: true, + interpretation: interpretation, + verification_state: state, + super_admin?: true + ) + + assert element_exists?(html, @super_admin_report) + assert text_of_element(html, @super_admin_report) =~ "Plausible is on window: nil" + end + + test "hides pulsating circle when finished, shows check circle" do + html = + render_component(@component, + domain: "example.com", + success?: true, + finished?: true + ) + + refute element_exists?(html, @loading_spinner) + assert element_exists?(html, @check_circle) + end + + test "renders a progress message" do + html = render_component(@component, domain: "example.com", message: "Arbitrary message") + + assert text_of_element(html, @progress) == "Arbitrary message..." + end + + test "renders contact link on >=3 attempts" do + html = render_component(@component, domain: "example.com", attempts: 2, finished?: true) + 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 help?" + assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|) + end + + 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", + diagnostics: %Verification.Diagnostics{ + plausible_is_on_window: false, + plausible_is_initialized: false, + service_error: %{code: :domain_not_found} + } + }) + + assert interpretation.data.offer_custom_url_input == true + + html = + render_component(@component, + domain: "example.com", + finished?: true, + success?: false, + interpretation: interpretation + ) + + 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, replacing Check again with the Verify URL submit button, and hides the secondary action" 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"]|) + refute html =~ "Review installation" + + assert text_of_element(html, ~s|form[phx-submit="verify-custom-url"] button[type="submit"]|) =~ + "Verify URL" + + 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 a Review installation ghost button on failure by default" do + html = + render_component(@component, + domain: "example.com", + success?: false, + finished?: true, + flow: PlausibleWeb.Flows.review() + ) + + refute element_exists?(html, ~s|a[href="/example.com/settings/general"]|) + + assert element_exists?( + html, + ~s|a[href="/example.com/installation?flow=review"]| + ) + + assert html =~ "Review installation" + end + end +end diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_test.exs deleted file mode 100644 index 2645de32cc4e..000000000000 --- a/test/plausible_web/live/components/verification_test.exs +++ /dev/null @@ -1,162 +0,0 @@ -defmodule PlausibleWeb.Live.Components.VerificationTest do - use PlausibleWeb.ConnCase, async: true - - on_ee do - import Phoenix.LiveViewTest, only: [render_component: 2] - - alias Plausible.InstallationSupport.{State, Verification} - - @moduletag :capture_log - - @component PlausibleWeb.Live.Components.Verification - @progress ~s|#verification-ui p#progress| - - @pulsating_circle ~s|div#verification-ui div.pulsating-circle| - @check_circle ~s|div#verification-ui #check-circle| - @error_circle ~s|div#verification-ui #error-circle| - @recommendations ~s|#recommendation| - @super_admin_report ~s|#super-admin-report| - - test "renders initial state" do - html = render_component(@component, domain: "example.com") - assert element_exists?(html, @progress) - - assert text_of_element(html, @progress) == - "We're visiting your site to ensure that everything is working" - - assert element_exists?(html, @pulsating_circle) - refute class_of_element(html, @pulsating_circle) =~ "hidden" - refute element_exists?(html, @recommendations) - refute element_exists?(html, @check_circle) - refute element_exists?(html, @super_admin_report) - end - - 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, @check_circle) - refute element_exists?(html, @recommendations) - assert element_exists?(html, @error_circle) - end - - test "renders diagnostic interpretation" do - interpretation = - Verification.Checks.interpret_diagnostics(%State{ - url: "https://example.com", - data_domain: "example.com", - diagnostics: %Verification.Diagnostics{service_error: %{code: :domain_not_found}} - }) - - html = - render_component(@component, - domain: "example.com", - success?: false, - finished?: true, - interpretation: interpretation - ) - - assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1) - assert recommendation =~ "check that the domain you entered is correct" - - refute element_exists?(html, @super_admin_report) - end - - test "renders super-admin report" do - state = %State{ - url: "https://example.com", - data_domain: "example.com", - diagnostics: %Verification.Diagnostics{} - } - - interpretation = Verification.Checks.interpret_diagnostics(state) - - html = - render_component(@component, - domain: "example.com", - success?: false, - finished?: true, - interpretation: interpretation, - verification_state: state, - super_admin?: true - ) - - assert element_exists?(html, @super_admin_report) - assert text_of_element(html, @super_admin_report) =~ "Plausible is on window: nil" - end - - test "hides pulsating circle when finished, shows check circle" do - html = - render_component(@component, - domain: "example.com", - success?: true, - finished?: true - ) - - refute element_exists?(html, @pulsating_circle) - assert element_exists?(html, @check_circle) - end - - test "renders a progress message" do - html = render_component(@component, domain: "example.com", message: "Arbitrary message") - - assert text_of_element(html, @progress) == "Arbitrary message" - end - - 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 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 element_exists?(html, ~s|a[href="https://plausible.io/contact"]|) - end - - test "renders link to verify installation at a different URL" 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} - } - }) - - 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", - finished?: true, - success?: false, - 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" - end - - test "offers escape paths: settings and installation instructions on failure" do - html = - render_component(@component, - domain: "example.com", - success?: false, - finished?: true, - installation_type: "wordpress", - flow: PlausibleWeb.Flows.review() - ) - - assert element_exists?(html, ~s|a[href="/example.com/settings/general"]|) - - assert element_exists?( - html, - ~s|a[href="/example.com/installation?flow=review&installation_type=wordpress"]| - ) - end - end -end diff --git a/test/plausible_web/live/installation_test.exs b/test/plausible_web/live/installation_test.exs index a3ac3aae5f06..7c7adf0fe169 100644 --- a/test/plausible_web/live/installation_test.exs +++ b/test/plausible_web/live/installation_test.exs @@ -246,10 +246,11 @@ 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_lookup_a_records(site.domain) stub_detection_manual() {lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}") @@ -270,9 +271,9 @@ defmodule PlausibleWeb.Live.InstallationTest do assert_redirect( lv, - Routes.site_path(conn, :verification, site.domain, - flow: "provisioning", - installation_type: unquote(type) + Routes.stats_path(conn, :stats, site.domain, + verify_installation: true, + flow: "provisioning" ) ) end @@ -280,7 +281,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 +298,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 +329,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_lookup_a_records(site.domain) stub_detection_manual() @@ -356,13 +355,19 @@ 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" + ) ) - ) + 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 4580e932e96b..4ba6ea3f823c 100644 --- a/test/plausible_web/live/verification_test.exs +++ b/test/plausible_web/live/verification_test.exs @@ -9,33 +9,36 @@ 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| + @banner ~s|#verification-ui| + + @in_progress_text "Verifying your installation" 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) =~ "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 - 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 @@ -51,45 +54,61 @@ 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 :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 reveals an inline form next to the retry button, submitting kicks off a new run", + %{ + conn: conn, + site: site + } do stub_lookup_a_records(site.domain) stub_verification_result(%{ - "completed" => false, - "error" => %{"message" => "Error"} + "completed" => true, + "trackerIsInHtml" => false, + "plausibleIsOnWindow" => false, + "plausibleIsInitialized" => false }) - # Get liveview with ?custom_url=true query param - {:ok, lv, html} = - conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true") + {:ok, lv} = kick_off_live_verification(conn, site) + + 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") + + refute html =~ @in_progress_text + + refute element_exists?(html, @retry_button) + refute element_exists?(html, "#verify-custom-url-link") - verifying_installation_text = "Verifying your installation" + assert element_exists?( + html, + ~s|form[phx-submit="verify-custom-url"] input[name="custom_url"]| + ) - # Assert form is rendered instead of kicking off verification automatically - assert html =~ "Enter Your Custom URL" assert html =~ ~s[value="https://#{site.domain}"] assert html =~ ~s[placeholder="https://#{site.domain}"] - refute html =~ verifying_installation_text - # Submit custom URL form - html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"}) + lv + |> element("form[phx-submit='verify-custom-url']") + |> render_submit(%{"custom_url" => "https://abc.de"}) - # Should now show verification progress and hide custom URL form - assert html =~ verifying_installation_text - refute html =~ "Enter Your Custom URL" + assert eventually(fn -> + html = render(lv) + {html =~ @in_progress_text, html} + end) end @tag :ee_only @@ -113,25 +132,13 @@ defmodule PlausibleWeb.Live.VerificationTest do assert eventually(fn -> html = render(lv) - - { - text_of_element(html, @awaiting) =~ - "Awaiting your first pageview", - html - } + {html =~ "Tracking is active on your site", 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) - ]) - + 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(%{ @@ -149,95 +156,63 @@ defmodule PlausibleWeb.Live.VerificationTest do {: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) - - { - text(html) =~ "Success", - html - } + {html =~ "Tracking is active on your site", html} end) html = render(lv) - - refute text_of_element(html, @awaiting) =~ "Awaiting your first pageview" - refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}/") + assert class_of_element(html, @banner) =~ "hidden" end - test "will redirect when first pageview arrives", %{conn: conn, site: site} 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_verification_result(%{ - "completed" => true, - "trackerIsInHtml" => true, - "plausibleIsOnWindow" => true, - "plausibleIsInitialized" => true, - "testEvent" => %{ - "normalizedBody" => %{ - "domain" => site.domain - }, - "responseStatus" => 200 - } + "completed" => false, + "error" => %{"message" => "Error"} }) {:ok, lv} = kick_off_live_verification(conn, site) - assert eventually(fn -> - html = render(lv) - - { - text(html) =~ "Awaiting", - html - } - end) - - populate_stats(site, [ - build(:pageview) - ]) - - assert_redirect(lv, 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)]) + render_click(lv, "dismiss") - assert_redirect(lv, Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain)) + assert_push_event(lv, "disconnect-liveview", %{}) 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} <- [ + {"Make sure you've copied the snippet to the head of your site, or verify your installation manually.", + "manual"}, + {"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.", + {"Make sure you've configured the GTM template correctly, or verify your installation manually.", + "gtm"}, + {"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 + {"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 @@ -257,45 +232,49 @@ 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) - 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 end - defp get_lv(conn, site, qs \\ nil) do - {:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification#{qs}") + defp get_lv(conn, site) do + {:ok, lv, html} = + 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() |> live("/#{site.domain}/verification#{qs}") + conn |> no_slowdown() |> no_delay() |> as_live() |> live(verification_path(site)) {: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), do: "/#{site.domain}?verify_installation=true" + defp no_slowdown(conn) do Plug.Conn.put_private(conn, :slowdown, 0) end