Script v2: Maybe load installation type from DB (#5717)

* Maybe load installation type from DB

* Swap get_or_create... with get_tracker_script_configuration

* Fix getting saved_installation_type

* Add utility to htmlize quotes

* Update manual snippet not found error text

* Prevent custom URL input if scriptv2 not true

* Test verification v2 flows

* Fix import for CE test
This commit is contained in:
Artur Pata 2025-09-16 08:28:36 +03:00 committed by GitHub
parent 1531386b76
commit a0e5f801ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 438 additions and 50 deletions

View File

@ -56,7 +56,7 @@ defmodule Plausible.InstallationSupport.Detection.Diagnostics do
} = diagnostics,
_url
) do
get_result("manual", diagnostics)
get_result(PlausibleWeb.Tracker.fallback_installation_type(), diagnostics)
end
def interpret(

View File

@ -15,6 +15,7 @@ defmodule Plausible.InstallationSupport.Verification.Checks do
Checks.InstallationV2CacheBust
]
@spec run(String.t(), String.t(), String.t(), Keyword.t()) :: :ok
def run(url, data_domain, installation_type, opts \\ []) do
report_to = Keyword.get(opts, :report_to, self())
async? = Keyword.get(opts, :async?, true)

View File

@ -157,7 +157,7 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do
def interpret(
%__MODULE__{
tracker_is_in_html: false,
selected_installation_type: selected_installation_type,
selected_installation_type: "manual",
plausible_is_on_window: plausible_is_on_window,
plausible_is_initialized: plausible_is_initialized,
service_error: nil
@ -165,9 +165,9 @@ defmodule Plausible.InstallationSupport.Verification.Diagnostics do
_expected_domain,
_url
)
when selected_installation_type in ["manual", nil] and plausible_is_on_window != true and
when plausible_is_on_window != true and
plausible_is_initialized != true,
do: error_plausible_not_found(selected_installation_type)
do: error_plausible_not_found("manual")
@error_csp_disallowed Error.new!(%{
message:
@ -275,7 +275,7 @@ 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 snippet to the head of your site, or verify your installation manually",
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually",
url: @verify_manually_url
})
@error_plausible_not_found_for_npm Error.new!(%{

View File

@ -43,7 +43,14 @@ defmodule PlausibleWeb.Live.Installation do
if PlausibleWeb.Tracker.scriptv2?(site) do
{:ok,
redirect(socket,
to: Routes.site_path(socket, :installation_v2, site.domain, flow: params["flow"])
to:
Routes.site_path(
socket,
:installation_v2,
site.domain,
[flow: params["flow"], type: params["installation_type"]]
|> Keyword.filter(fn {_k, v} -> not is_nil(v) and v != "" end)
)
)}
else
flow = params["flow"]

View File

@ -15,10 +15,6 @@ defmodule PlausibleWeb.Live.InstallationV2 do
on_ee do
alias Plausible.InstallationSupport.{Detection, Result}
@installation_methods ["manual", "wordpress", "gtm", "npm"]
else
@installation_methods ["manual", "wordpress", "npm"]
end
def mount(
@ -93,7 +89,7 @@ defmodule PlausibleWeb.Live.InstallationV2 do
def handle_params(params, _url, socket) do
socket =
if connected?(socket) && socket.assigns.recommended_installation_type.result &&
params["type"] in @installation_methods do
params["type"] in PlausibleWeb.Tracker.supported_installation_types() do
assign(socket,
installation_type: %AsyncResult{result: params["type"]}
)
@ -217,12 +213,12 @@ defmodule PlausibleWeb.Live.InstallationV2 do
Detection.Checks.interpret_diagnostics(detection_result) do
{data.suggested_technology, data.v1_detected}
else
_ -> {"manual", false}
_ -> {PlausibleWeb.Tracker.fallback_installation_type(), false}
end
end
else
defp detect_recommended_installation_type(_flow, _site) do
{"manual", false}
{PlausibleWeb.Tracker.fallback_installation_type(), false}
end
end
@ -326,7 +322,7 @@ defmodule PlausibleWeb.Live.InstallationV2 do
selected_installation_type =
cond do
params["type"] in @installation_methods ->
params["type"] in PlausibleWeb.Tracker.supported_installation_types() ->
params["type"]
flow == Flows.review() and

View File

@ -19,8 +19,10 @@ defmodule PlausibleWeb.Live.Verification do
_session,
socket
) do
current_user = socket.assigns.current_user
site =
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
Plausible.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:editor,
@ -30,9 +32,11 @@ defmodule PlausibleWeb.Live.Verification do
private = Map.get(socket.private.connect_info, :private, %{})
super_admin? = Plausible.Auth.is_super_admin?(socket.assigns.current_user)
super_admin? = Plausible.Auth.is_super_admin?(current_user)
has_pageviews? = has_pageviews?(site)
custom_url_input? = params["custom_url"] == "true"
custom_url_input? =
PlausibleWeb.Tracker.scriptv2?(site, current_user) and params["custom_url"] == "true"
socket =
assign(socket,
@ -42,7 +46,7 @@ defmodule PlausibleWeb.Live.Verification do
domain: domain,
has_pageviews?: has_pageviews?,
component: @component,
installation_type: params["installation_type"],
installation_type: get_installation_type(params, site, current_user),
report_to: self(),
delay: private[:delay] || 500,
slowdown: private[:slowdown] || 500,
@ -136,11 +140,14 @@ defmodule PlausibleWeb.Live.Verification do
end
def handle_info({:start, report_to}, socket) do
if is_pid(socket.assigns.checks_pid) and Process.alive?(socket.assigns.checks_pid) do
domain = socket.assigns.domain
checks_pid = socket.assigns.checks_pid
if is_pid(checks_pid) and Process.alive?(checks_pid) do
{:noreply, socket}
else
case Plausible.RateLimit.check_rate(
"site_verification:#{socket.assigns.domain}",
"site_verification:#{domain}",
:timer.minutes(60),
3
) do
@ -148,18 +155,18 @@ defmodule PlausibleWeb.Live.Verification do
{:deny, _} -> :timer.sleep(@slowdown_for_frequent_checking)
end
domain = socket.assigns.domain
installation_type = socket.assigns.installation_type
{:ok, pid} =
if PlausibleWeb.Tracker.scriptv2?(socket.assigns.site, socket.assigns.current_user) do
Verification.Checks.run(socket.assigns.url_to_verify, domain, installation_type,
Verification.Checks.run(
socket.assigns.url_to_verify,
domain,
socket.assigns.installation_type,
report_to: report_to,
slowdown: socket.assigns.slowdown
)
else
LegacyVerification.Checks.run(
"https://#{socket.assigns.domain}",
"https://#{domain}",
domain,
report_to: report_to,
slowdown: socket.assigns.slowdown
@ -220,6 +227,35 @@ defmodule PlausibleWeb.Live.Verification do
{:noreply, socket}
end
@supported_installation_types_atoms PlausibleWeb.Tracker.supported_installation_types()
|> Enum.map(&String.to_atom/1)
defp get_installation_type(params, site, current_user) do
if PlausibleWeb.Tracker.scriptv2?(site, current_user) do
cond do
params["installation_type"] in PlausibleWeb.Tracker.supported_installation_types() ->
params["installation_type"]
(saved_installation_type = get_saved_installation_type(site)) in @supported_installation_types_atoms ->
Atom.to_string(saved_installation_type)
true ->
PlausibleWeb.Tracker.fallback_installation_type()
end
else
params["installation_type"]
end
end
defp get_saved_installation_type(site) do
case PlausibleWeb.Tracker.get_tracker_script_configuration(site) do
%{installation_type: installation_type} ->
installation_type
_ ->
nil
end
end
defp schedule_pageviews_check(socket) do
if socket.assigns.polling_pageviews? do
socket

View File

@ -27,7 +27,7 @@ defmodule PlausibleWeb.Tracker do
#
# Note that EE is relying on CDN caching the script
if PlausibleWeb.TrackerScriptCache.get(id, cache_opts) do
get_tracker_script_configuration(id)
get_tracker_script_configuration_by_id(id)
|> build_script()
end
else
@ -109,13 +109,17 @@ defmodule PlausibleWeb.Tracker do
def purge_tracker_script_cache!(_site), do: nil
end
def get_tracker_script_configuration(site) do
Repo.get_by(TrackerScriptConfiguration, site_id: site.id)
end
def update_script_configuration!(site, config_update, changeset_type) do
{:ok, updated_config} = update_script_configuration(site, config_update, changeset_type)
updated_config
end
def get_or_create_tracker_script_configuration(site, params \\ %{}) do
configuration = Repo.get_by(TrackerScriptConfiguration, site_id: site.id)
configuration = get_tracker_script_configuration(site)
if configuration do
{:ok, configuration}
@ -140,10 +144,24 @@ defmodule PlausibleWeb.Tracker do
config
end
on_ee do
def supported_installation_types do
["manual", "wordpress", "gtm", "npm"]
end
else
def supported_installation_types do
["manual", "wordpress", "npm"]
end
end
def fallback_installation_type do
"manual"
end
on_ee do
import Ecto.Query
defp get_tracker_script_configuration(id) do
defp get_tracker_script_configuration_by_id(id) do
from(t in TrackerScriptConfiguration,
where: t.id == ^id,
join: s in assoc(t, :site),

View File

@ -254,7 +254,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
recommendations: [
%{
text:
"Please make sure you've copied snippet to the head of your site, or verify your installation manually",
"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"
}
@ -448,7 +448,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
{"npm",
"Please make sure you've initialized Plausible on your site, or verify your installation manually"},
{"manual",
"Please make sure you've copied snippet to the head of your site, or verify your installation manually"}
"Please 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
expected_domain = "example.com"

View File

@ -1008,7 +1008,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
"user" => %{"password" => password, "email" => user.email}
})
assert html_response(conn, 200) =~ "can't be the same"
assert html_response(conn, 200) =~ htmlize_quotes("can't be the same")
end
end

View File

@ -254,7 +254,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
assert html_response(conn, 200) =~ "can't be blank"
assert html_response(conn, 200) =~ htmlize_quotes("can't be blank")
end
test "fails to create site when not allowed to in selected team", %{conn: conn, user: user} do
@ -404,7 +404,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
assert html_response(conn, 200) =~ "can't be blank"
assert html_response(conn, 200) =~ htmlize_quotes("can't be blank")
end
test "only alphanumeric characters and slash allowed in domain", %{conn: conn} do

View File

@ -133,7 +133,7 @@ defmodule PlausibleWeb.Live.ChangeDomainV2Test do
|> element("form")
|> render_submit(%{site: %{domain: ""}})
assert html =~ "can't be blank"
assert html =~ htmlize_quotes("can't be blank")
end
test "form validation shows error for invalid domain format", %{conn: conn, site: site} do

View File

@ -147,7 +147,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
domain: "example.com",
success?: false,
finished?: true,
installation_type: "WordPress",
installation_type: "wordpress",
flow: PlausibleWeb.Flows.review()
)
@ -155,7 +155,7 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
assert element_exists?(
html,
~s|a[href="/example.com/installation?flow=review&installation_type=WordPress"]|
~s|a[href="/example.com/installation?flow=review&installation_type=wordpress"]|
)
end
end

View File

@ -59,25 +59,29 @@ defmodule PlausibleWeb.Live.VerificationTest do
end
@tag :ee_only
test "from custom URL input form to verification", %{conn: conn, site: site} do
test "ignores v2 verification custom URL input", %{conn: conn, site: site} do
stub_fetch_body(200, source(site.domain))
stub_installation()
# Get liveview with ?custom_url=true query param
{:ok, lv, html} =
conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true")
verifying_installation_text = "Verifying your installation"
# Assert form is rendered instead of kicking off verification automatically
assert html =~ "Enter Your Custom URL"
assert html =~ ~s[value="https://#{site.domain}"]
assert html =~ ~s[placeholder="https://#{site.domain}"]
refute html =~ verifying_installation_text
# Submit custom URL form
html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"})
# Should now show verification progress and hide custom URL form
assert html =~ verifying_installation_text
refute html =~ "Enter Your Custom URL"
assert eventually(fn ->
html = render(lv)
{
text_of_element(html, @awaiting) =~
"Awaiting your first pageview",
html
}
end)
html = render(lv)
assert html =~ "Success!"
assert html =~ "Awaiting your first pageview"
end
@tag :ee_only

View File

@ -0,0 +1,322 @@
defmodule PlausibleWeb.Live.VerificationTest do
use PlausibleWeb.ConnCase, async: true
use Plausible.Test.Support.DNS
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
@moduletag :capture_log
setup [:create_user, :log_in, :create_site]
# @verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
@retry_button ~s|a[phx-click="retry"]|
# @go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]|
@progress ~s|#verification-ui p#progress|
@awaiting ~s|#verification-ui span#awaiting|
@heading ~s|#verification-ui h2|
setup %{site: site} do
FunWithFlags.enable(:scriptv2, for_actor: site)
:ok
end
describe "GET /:domain" do
@tag :ee_only
test "static verification screen renders", %{conn: conn, site: site} do
resp =
get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
|> html_response(200)
assert text_of_element(resp, @progress) =~
"We're visiting your site to ensure that everything is working"
assert resp =~ "Verifying your installation"
end
@tag :ce_build_only
test "static verification screen renders (ce)", %{conn: conn, site: site} do
resp =
get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
|> html_response(200)
assert resp =~ "Awaiting your first pageview …"
end
end
describe "LiveView" do
@tag :ee_only
test "LiveView mounts", %{conn: conn, site: site} do
stub_lookup_a_records(site.domain)
stub_verification_result(%{
"completed" => false,
"error" => %{"message" => "Error"}
})
{_, html} = get_lv(conn, site)
assert html =~ "Verifying your installation"
assert text_of_element(html, @progress) =~
"We're visiting your site to ensure that everything is working"
end
@tag :ce_build_only
test "LiveView mounts (ce)", %{conn: conn, site: site} do
{_, html} = get_lv(conn, site)
assert html =~ "Awaiting your first pageview …"
end
@tag :ee_only
test "from custom URL input form to verification", %{conn: conn, site: site} do
stub_lookup_a_records(site.domain)
stub_verification_result(%{
"completed" => false,
"error" => %{"message" => "Error"}
})
# Get liveview with ?custom_url=true query param
{:ok, lv, html} =
conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true")
verifying_installation_text = "Verifying your installation"
# Assert form is rendered instead of kicking off verification automatically
assert html =~ "Enter Your Custom URL"
assert html =~ ~s[value="https://#{site.domain}"]
assert html =~ ~s[placeholder="https://#{site.domain}"]
refute html =~ verifying_installation_text
# Submit custom URL form
html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"})
# Should now show verification progress and hide custom URL form
assert html =~ verifying_installation_text
refute html =~ "Enter Your Custom URL"
end
@tag :ee_only
test "eventually verifies installation", %{conn: conn, site: site} do
stub_lookup_a_records(site.domain)
stub_verification_result(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{
"normalizedBody" => %{
"domain" => site.domain
},
"responseStatus" => 200
}
})
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
{
text_of_element(html, @awaiting) =~
"Awaiting your first pageview",
html
}
end)
html = render(lv)
assert html =~ "Success!"
assert html =~ "Awaiting your first pageview"
end
@tag :ee_only
test "won't await first pageview if site has pageviews", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview)
])
stub_lookup_a_records(site.domain)
stub_verification_result(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{
"normalizedBody" => %{
"domain" => site.domain
},
"responseStatus" => 200
}
})
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
{
text(html) =~ "Success",
html
}
end)
html = render(lv)
refute text_of_element(html, @awaiting) =~ "Awaiting your first pageview"
refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}/")
end
test "will redirect when first pageview arrives", %{conn: conn, site: site} do
stub_lookup_a_records(site.domain)
stub_verification_result(%{
"completed" => true,
"trackerIsInHtml" => true,
"plausibleIsOnWindow" => true,
"plausibleIsInitialized" => true,
"testEvent" => %{
"normalizedBody" => %{
"domain" => site.domain
},
"responseStatus" => 200
}
})
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
{
text(html) =~ "Awaiting",
html
}
end)
populate_stats(site, [
build(:pageview)
])
assert_redirect(lv, "/#{URI.encode_www_form(site.domain)}/")
end
@tag :ce_build_only
test "will redirect when first pageview arrives (ce)", %{conn: conn, site: site} do
{:ok, lv} = kick_off_live_verification(conn, site)
html = render(lv)
assert text(html) =~ "Awaiting your first pageview …"
populate_stats(site, [build(:pageview)])
assert_redirect(lv, "/#{URI.encode_www_form(site.domain)}/")
end
for {installation_type_param, expected_text, saved_installation_type} <- [
{"manual",
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
nil},
{"npm",
"Please make sure you've initialized Plausible on your site, or verify your installation manually.",
nil},
{"gtm",
"Please make sure you've configured the GTM template correctly, or verify your installation manually.",
nil},
{"wordpress",
"Please make sure you've enabled the plugin, or verify your installation manually.",
nil},
# trusts param over saved installation type
{"wordpress",
"Please make sure you've enabled the plugin, or verify your installation manually.",
"npm"},
# falls back to saved installation type if no param
{"",
"Please make sure you've initialized Plausible on your site, or verify your installation manually.",
"npm"},
# falls back to manual if no param and no saved installation type
{"",
"Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
nil}
] do
@tag :ee_only
test "eventually fails to verify installation (?installation_type=#{installation_type_param}) if saved installation type is #{inspect(saved_installation_type)}",
%{
conn: conn,
site: site
} do
stub_lookup_a_records(site.domain)
stub_verification_result(%{
"completed" => true,
"trackerIsInHtml" => false,
"plausibleIsOnWindow" => false,
"plausibleIsInitialized" => false
})
if unquote(saved_installation_type) do
PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{
"installation_type" => unquote(saved_installation_type)
})
end
{:ok, lv} =
kick_off_live_verification(
conn,
site,
"?installation_type=#{unquote(installation_type_param)}"
)
assert html =
eventually(fn ->
html = render(lv)
{html =~ "", html}
{
text_of_element(html, @heading) =~
"We couldn't detect Plausible on your site",
html
}
end)
assert element_exists?(html, @retry_button)
assert html =~ htmlize_quotes(unquote(expected_text))
refute element_exists?(html, "#super-admin-report")
end
end
end
defp get_lv(conn, site, qs \\ nil) do
{:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification#{qs}")
{lv, html}
end
defp kick_off_live_verification(conn, site, qs \\ nil) do
{:ok, lv, _html} =
conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification#{qs}")
{:ok, lv}
end
defp no_slowdown(conn) do
Plug.Conn.put_private(conn, :slowdown, 0)
end
defp no_delay(conn) do
Plug.Conn.put_private(conn, :delay, 0)
end
defp stub_verification_result(js_data) do
Req.Test.stub(Plausible.InstallationSupport.Checks.InstallationV2, fn conn ->
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(%{"data" => js_data}))
end)
end
end

View File

@ -267,6 +267,10 @@ defmodule Plausible.TestUtils do
Enum.map_join(1..4, ".", fn _ -> Enum.random(1..254) end)
end
def htmlize_quotes(string) do
String.replace(string, "'", "&#39;")
end
def minio_running? do
%{host: host, port: port} = ExAws.Config.new(:s3)
healthcheck_req = Finch.build(:head, "http://#{host}:#{port}")