Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { isRealTimeDashboard } from './util/filters'
import { GraphIntervalProvider } from './stats/graph/graph-interval-context'
import { ImportsIncludedProvider } from './stats/graph/imports-included-context'
import { CurrentVisitorsProvider } from './current-visitors-context'
import { VerificationLiveViewPortal } from './verification/portal'

function DashboardStats({
importedDataInView,
Expand All @@ -21,7 +22,10 @@ function DashboardStats({
}) {
return (
<>
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
<div className="col-span-full">
<VerificationLiveViewPortal />
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
</div>
<Sources />
<Pages />
<Locations />
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/graph/visitor-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function VisitorGraph({
!showFullLoader

return (
<div className="col-span-full relative w-full bg-white rounded-md shadow-sm dark:bg-gray-900">
<div className="relative w-full bg-white rounded-md shadow-sm dark:bg-gray-900">
<>
<div
id="top-stats-container"
Expand Down
75 changes: 75 additions & 0 deletions assets/js/dashboard/verification/portal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react'
import { act, render, screen } from '@testing-library/react'
import { useLocation } from 'react-router-dom'
import { TestContextProviders } from '../../../test-utils/app-context-providers'
import {
VERIFICATION_FINISHED_EVENT,
VerificationLiveViewPortal
} from './portal'

function LocationDisplay() {
const location = useLocation()
return <div data-testid="location">{location.pathname + location.search}</div>
}

function renderWithInitialEntry(initialEntry: string) {
render(
<>
<VerificationLiveViewPortal />
<LocationDisplay />
</>,
{
wrapper: (props) => (
<TestContextProviders
siteOptions={{ domain: 'some-domain' }}
routerProps={{ initialEntries: [initialEntry] }}
{...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'
)
})
50 changes: 50 additions & 0 deletions assets/js/dashboard/verification/portal.tsx
Original file line number Diff line number Diff line change
@@ -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<VerificationFinishedDetail>)
.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 <div id="verification-portal-target"></div>
})
9 changes: 9 additions & 0 deletions assets/js/liveview/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion extra/lib/plausible/installation_support/result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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: %{},
Expand Down
43 changes: 43 additions & 0 deletions extra/lib/plausible/installation_support/verification/checks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Loading
Loading