Use sessionStorage for "dashboard first launch" banner tracking (#3892)

* Use sessionStorage for offer e-mail report banner tracking

Keeping it within the cookie is problematic, as the banners don't
expire and overflow the cookie with data when enough new sites
are added.

Ref https://github.com/plausible/analytics/issues/3762

* Update changelog

* Extract a component

* Make is_dbip evaluate to quoted boolean
This commit is contained in:
hq1 2024-03-26 09:49:15 +01:00 committed by GitHub
parent 7523abe93e
commit edf70d14b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 140 additions and 84 deletions

View File

@ -57,6 +57,7 @@ All notable changes to this project will be documented in this file.
- Validate metric isn't queried multiple times
### Fixed
- Creating many sites no longer leads to cookie overflow
- Ignore sessions without pageviews for `entry_page` and `exit_page` breakdowns
- Using `VersionedCollapsingMergeTree` to store visit data to avoid rare race conditions that led to wrong visit data being shown
- Fix `conversion_rate` metric in a `browser_versions` breakdown

View File

@ -0,0 +1,50 @@
defmodule PlausibleWeb.Components.FirstDashboardLaunchBanner do
@moduledoc """
A banner that appears on the first dashboard launch
"""
use Phoenix.Component
use Phoenix.HTML
attr(:site, Plausible.Site, required: true)
def set(assigns) do
~H"""
<script>
sessionStorage.setItem('<%= storage_key(@site) %>', false);
</script>
"""
end
attr(:site, Plausible.Site, required: true)
def render(assigns) do
~H"""
<div
x-cloak
x-data={x_data(@site)}
class="w-full px-4 text-sm font-bold text-center text-blue-900 bg-blue-200 rounded transition"
style="top: 91px"
role="alert"
x-bind:class="! show ? 'hidden' : ''"
x-init={x_init(@site)}
>
<%= link("Team members, email reports and GA import. Explore more →",
to: "/#{URI.encode_www_form(@site.domain)}/settings/email-reports",
class: "py-2 block"
) %>
</div>
"""
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

View File

@ -34,9 +34,9 @@ defmodule PlausibleWeb.SiteController do
|> Plausible.Mailer.send()
end
conn
|> put_session(site.domain <> "_offer_email_report", true)
|> redirect(external: Routes.site_path(conn, :add_snippet, site.domain))
redirect(conn,
external: Routes.site_path(conn, :add_snippet, site.domain, site_created: true)
)
{:error, {:over_limit, limit}} ->
render(conn, "new.html",

View File

@ -59,10 +59,7 @@ defmodule PlausibleWeb.StatsController do
cond do
stats_start_date && can_see_stats? ->
offer_email_report = get_session(conn, site.domain <> "_offer_email_report")
conn
|> remove_email_report_banner(site)
|> put_resp_header("x-robots-tag", "noindex, nofollow")
|> render("stats.html",
site: site,
@ -72,7 +69,6 @@ defmodule PlausibleWeb.StatsController do
stats_start_date: stats_start_date,
native_stats_start_date: NaiveDateTime.to_date(site.native_stats_start_at),
title: title(conn, site),
offer_email_report: offer_email_report,
demo: demo,
flags: get_flags(conn.assigns[:current_user]),
is_dbip: is_dbip(),
@ -328,7 +324,6 @@ defmodule PlausibleWeb.StatsController do
stats_start_date: stats_start_date,
native_stats_start_date: NaiveDateTime.to_date(shared_link.site.native_stats_start_at),
title: title(conn, shared_link.site),
offer_email_report: false,
demo: false,
dogfood_page_path: "/share/:dashboard",
shared_link_auth: shared_link.slug,
@ -351,14 +346,6 @@ defmodule PlausibleWeb.StatsController do
end
end
defp remove_email_report_banner(conn, site) do
if conn.assigns[:current_user] do
delete_session(conn, site.domain <> "_offer_email_report")
else
conn
end
end
defp shared_link_cookie_name(slug), do: "shared-link-" <> slug
defp get_flags(_user) do
@ -367,11 +354,12 @@ defmodule PlausibleWeb.StatsController do
defp is_dbip() do
on_full_build do
false
"false"
else
Plausible.Geo.database_type()
|> to_string()
|> String.starts_with?("DBIP")
|> to_string()
end
end

View File

@ -1,3 +1,8 @@
<PlausibleWeb.Components.FirstDashboardLaunchBanner.set
:if={@conn.params["site_created"] == "true"}
site={@site}
/>
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @conn, "/", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-bold dark:text-gray-100">Add JavaScript snippet</h2>

View File

@ -1,63 +0,0 @@
<div class="<%= stats_container_class(@conn) %>" data-site-domain="<%= @site.domain %>">
<%= if @offer_email_report do %>
<div class="w-full px-4 text-sm font-bold text-center text-blue-900 bg-blue-200 rounded transition" style="top: 91px" role="alert">
<%= link("Team members, email reports and GA import. Explore more →", to: "/#{URI.encode_www_form(@site.domain)}/settings/email-reports", class: "py-2 block") %>
</div>
<% end %>
<%= if @site.locked do %>
<div class="w-full px-4 py-4 text-sm font-bold text-center text-yellow-800 bg-yellow-100 rounded transition" style="top: 91px" role="alert">
<p>This dashboard is actually locked. You are viewing it with super-admin access</p>
</div>
<% end %>
<div class="pt-6"></div>
<div
id="stats-react-container"
style="overflow-anchor: none;"
data-domain="<%= @site.domain %>"
data-offset="<%= Plausible.Site.tz_offset(@site) %>"
data-has-goals="<%= @has_goals %>"
data-conversions-opted-out="<%= Plausible.Billing.Feature.Goals.opted_out?(@site) %>"
data-funnels-opted-out="<%= Plausible.Billing.Feature.Funnels.opted_out?(@site) %>"
data-props-opted-out="<%= Plausible.Billing.Feature.Props.opted_out?(@site) %>"
data-funnels-available="<%= Plausible.Billing.Feature.Funnels.check_availability(@site.owner) == :ok %>"
data-props-available="<%= Plausible.Billing.Feature.Props.check_availability(@site.owner) == :ok %>"
data-funnels="<%= Jason.encode!(@funnels) %>"
data-has-props="<%= @has_props %>"
data-logged-in="<%= !!@conn.assigns[:current_user] %>"
data-stats-begin="<%= @stats_start_date %>"
data-native-stats-begin="<%= @native_stats_start_date %>"
data-shared-link-auth="<%= assigns[:shared_link_auth] %>"
data-embedded="<%= @conn.assigns[:embedded] %>"
data-background="<%= @conn.assigns[:background] %>"
data-is-dbip="<%= @is_dbip %>"
data-current-user-role="<%= @conn.assigns[:current_user_role] %>"
data-flags="<%= Jason.encode!(@flags) %>"
data-valid-intervals-by-period="<%= Plausible.Stats.Interval.valid_by_period(site: @site) |> Jason.encode!() %>">
</div>
<div id="modal_root"></div>
<%= if !@conn.assigns[:current_user] && @conn.assigns[:demo] do %>
<div class="bg-gray-50 dark:bg-gray-850">
<div class="py-12 lg:py-16 lg:flex lg:items-center lg:justify-between">
<h2 class="text-3xl font-extrabold tracking-tight text-gray-900 leading-9 sm:text-4xl sm:leading-10 dark:text-gray-100">
Want these stats for your website?
<br />
<span class="text-indigo-600">Start your free trial today.</span>
</h2>
<div class="flex mt-8 lg:flex-shrink-0 lg:mt-0">
<div class="inline-flex shadow rounded-md">
<a href="/register" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-white bg-indigo-600 border border-transparent leading-6 rounded-md hover:bg-indigo-500 focus:outline-none focus:ring transition duration-150 ease-in-out">
Get started
</a>
</div>
<div class="inline-flex ml-3 shadow rounded-md">
<a href="/" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-indigo-600 bg-white border border-transparent leading-6 rounded-md dark:text-gray-100 dark:bg-gray-800 hover:text-indigo-500 dark:hover:text-indigo-500 focus:outline-none focus:ring transition duration-150 ease-in-out">
Learn more
</a>
</div>
</div>
</div>
</div>
<% end %>
</div>

View File

@ -0,0 +1,73 @@
<div class={stats_container_class(@conn)} data-site-domain={@site.domain}>
<PlausibleWeb.Components.FirstDashboardLaunchBanner.render site={@site} />
<%= if @site.locked do %>
<div
class="w-full px-4 py-4 text-sm font-bold text-center text-yellow-800 bg-yellow-100 rounded transition"
style="top: 91px"
role="alert"
>
<p>This dashboard is actually locked. You are viewing it with super-admin access</p>
</div>
<% end %>
<div class="pt-6"></div>
<div
id="stats-react-container"
style="overflow-anchor: none;"
data-domain={@site.domain}
data-offset={Plausible.Site.tz_offset(@site)}
data-has-goals={@has_goals}
data-conversions-opted-out={Plausible.Billing.Feature.Goals.opted_out?(@site)}
data-funnels-opted-out={Plausible.Billing.Feature.Funnels.opted_out?(@site)}
data-props-opted-out={Plausible.Billing.Feature.Props.opted_out?(@site)}
data-funnels-available={
Plausible.Billing.Feature.Funnels.check_availability(@site.owner) == :ok
}
data-props-available={Plausible.Billing.Feature.Props.check_availability(@site.owner) == :ok}
data-funnels={Jason.encode!(@funnels)}
data-has-props={@has_props}
data-logged-in={!!@conn.assigns[:current_user]}
data-stats-begin={@stats_start_date}
data-native-stats-begin={@native_stats_start_date}
data-shared-link-auth={assigns[:shared_link_auth]}
data-embedded={@conn.assigns[:embedded]}
data-background={@conn.assigns[:background]}
data-is-dbip={@is_dbip}
data-current-user-role={@conn.assigns[:current_user_role]}
data-flags={Jason.encode!(@flags)}
data-valid-intervals-by-period={
Plausible.Stats.Interval.valid_by_period(site: @site) |> Jason.encode!()
}
>
</div>
<div id="modal_root"></div>
<%= if !@conn.assigns[:current_user] && @conn.assigns[:demo] do %>
<div class="bg-gray-50 dark:bg-gray-850">
<div class="py-12 lg:py-16 lg:flex lg:items-center lg:justify-between">
<h2 class="text-3xl font-extrabold tracking-tight text-gray-900 leading-9 sm:text-4xl sm:leading-10 dark:text-gray-100">
Want these stats for your website? <br />
<span class="text-indigo-600">Start your free trial today.</span>
</h2>
<div class="flex mt-8 lg:flex-shrink-0 lg:mt-0">
<div class="inline-flex shadow rounded-md">
<a
href="/register"
class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-white bg-indigo-600 border border-transparent leading-6 rounded-md hover:bg-indigo-500 focus:outline-none focus:ring transition duration-150 ease-in-out"
>
Get started
</a>
</div>
<div class="inline-flex ml-3 shadow rounded-md">
<a
href="/"
class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-indigo-600 bg-white border border-transparent leading-6 rounded-md dark:text-gray-100 dark:bg-gray-800 hover:text-indigo-500 dark:hover:text-indigo-500 focus:outline-none focus:ring transition duration-150 ease-in-out"
>
Learn more
</a>
</div>
</div>
</div>
</div>
<% end %>
</div>

View File

@ -244,7 +244,9 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
assert redirected_to(conn) == "/#{URI.encode_www_form("éxample.com")}/snippet"
assert redirected_to(conn) ==
"/#{URI.encode_www_form("éxample.com")}/snippet?site_created=true"
assert site = Repo.get_by(Plausible.Site, domain: "éxample.com")
assert site.timezone == "Europe/London"
assert site.ingest_rate_limit_scale_seconds == 60
@ -341,7 +343,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
assert redirected_to(conn) == "/example.com/snippet"
assert redirected_to(conn) == "/example.com/snippet?site_created=true"
assert Repo.get_by(Plausible.Site, domain: "example.com")
end
@ -361,7 +363,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
assert redirected_to(conn) == "/example.com/snippet"
assert redirected_to(conn) == "/example.com/snippet?site_created=true"
assert Plausible.Billing.Quota.site_usage(user) == 3
end
@ -375,7 +377,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
assert redirected_to(conn) == "/example.com/snippet"
assert redirected_to(conn) == "/example.com/snippet?site_created=true"
assert Repo.get_by(Plausible.Site, domain: "example.com")
end
end