Merge branch 'master' into log-mailer-errors-in-ce

This commit is contained in:
ruslandoga 2024-11-21 16:26:52 +07:00 committed by GitHub
commit 71e33bba0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
140 changed files with 2064 additions and 1071 deletions

View File

@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
### Added
- Dashboard shows comparisons for all reports
- UTM Medium report and API shows (gclid) and (msclkid) for paid searches when no explicit utm medium present.
### Removed
### Changed

View File

@ -15,7 +15,8 @@ export function getGraphableMetrics(query, site) {
} else if (isGoalFilter) {
return ["visitors", "events", "conversion_rate"]
} else if (isPageFilter) {
return ["visitors", "visits", "pageviews", "bounce_rate"]
const pageFilterMetrics = ["visitors", "visits", "pageviews", "bounce_rate"]
return site.flags.scroll_depth ? [...pageFilterMetrics, "scroll_depth"] : pageFilterMetrics
} else {
return ["visitors", "visits", "pageviews", "views_per_visit", "bounce_rate", "visit_duration"]
}

View File

@ -47,12 +47,14 @@ function PagesModal() {
]
}
return [
const defaultMetrics = [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
metrics.createPageviews(),
metrics.createBounceRate(),
metrics.createTimeOnPage()
]
return site.flags.scroll_depth ? [...defaultMetrics, metrics.createScrollDepth()] : defaultMetrics
}
return (

View File

@ -38,6 +38,7 @@ export const MetricFormatterShort: Record<
bounce_rate: percentageFormatter,
conversion_rate: percentageFormatter,
scroll_depth: percentageFormatter,
exit_rate: percentageFormatter,
group_conversion_rate: percentageFormatter,
percentage: percentageFormatter,
@ -65,6 +66,7 @@ export const MetricFormatterLong: Record<
bounce_rate: percentageFormatter,
conversion_rate: percentageFormatter,
scroll_depth: percentageFormatter,
exit_rate: percentageFormatter,
group_conversion_rate: percentageFormatter,
percentage: percentageFormatter,

View File

@ -172,7 +172,7 @@ export const createVisitDuration = (props) => {
export const createBounceRate = (props) => {
const renderLabel = (_query) => 'Bounce Rate'
return new Metric({
width: 'w-32',
width: 'w-28',
...props,
key: 'bounce_rate',
renderLabel,
@ -194,7 +194,7 @@ export const createPageviews = (props) => {
export const createTimeOnPage = (props) => {
const renderLabel = (_query) => 'Time on Page'
return new Metric({
width: 'w-32',
width: 'w-28',
...props,
key: 'time_on_page',
renderLabel,
@ -212,3 +212,14 @@ export const createExitRate = (props) => {
sortable: false
})
}
export const createScrollDepth = (props) => {
const renderLabel = (_query) => 'Scroll Depth'
return new Metric({
width: 'w-28',
...props,
key: 'scroll_depth',
renderLabel,
sortable: false
})
}

View File

@ -18,7 +18,8 @@ export type Metric =
| "group_conversion_rate"
| "time_on_page"
| "total_revenue"
| "average_revenue";
| "average_revenue"
| "scroll_depth";
export type DateRangeShorthand = "30m" | "realtime" | "all" | "day" | "7d" | "30d" | "month" | "6mo" | "12mo" | "year";
/**
* @minItems 2

View File

@ -206,7 +206,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
end
defp get_site(user, site_id, roles) do
case Sites.get_for_user(user.id, site_id, roles) do
case Plausible.Teams.Adapter.Read.Sites.get_for_user(user, site_id, roles) do
nil -> {:error, :site_not_found}
site -> {:ok, site}
end

View File

@ -6,7 +6,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
use Plausible.Funnel
alias Plausible.{Sites, Goals, Funnels}
alias Plausible.{Goals, Funnels}
def mount(
_params,
@ -16,7 +16,11 @@ defmodule PlausibleWeb.Live.FunnelSettings do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:all_funnels, fn %{site: %{id: ^site_id} = site} ->
Funnels.list(site)
@ -102,7 +106,11 @@ defmodule PlausibleWeb.Live.FunnelSettings do
def handle_event("delete-funnel", %{"funnel-id" => id}, socket) do
site =
Sites.get_for_user!(socket.assigns.current_user, socket.assigns.domain, [:owner, :admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(
socket.assigns.current_user,
socket.assigns.domain,
[:owner, :admin]
)
id = String.to_integer(id)
:ok = Funnels.delete(site, id)

View File

@ -9,11 +9,15 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
use Plausible.Funnel
import PlausibleWeb.Live.Components.Form
alias Plausible.{Sites, Goals, Funnels}
alias Plausible.{Goals, Funnels}
def mount(_params, %{"domain" => domain} = session, socket) do
site =
Sites.get_for_user!(socket.assigns.current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin
])
# We'll have the options trimmed to only the data we care about, to keep
# it minimal at the socket assigns, yet, we want to retain specific %Goal{}

View File

@ -6,14 +6,14 @@ defmodule Plausible.Billing do
alias Plausible.Billing.{Subscription, Plans, Quota}
alias Plausible.Auth.User
@spec active_subscription_for(integer()) :: Subscription.t() | nil
def active_subscription_for(user_id) do
user_id |> active_subscription_query() |> Repo.one()
@spec active_subscription_for(User.t()) :: Subscription.t() | nil
def active_subscription_for(user) do
user |> active_subscription_query() |> Repo.one()
end
@spec has_active_subscription?(integer()) :: boolean()
def has_active_subscription?(user_id) do
user_id |> active_subscription_query() |> Repo.exists?()
@spec has_active_subscription?(User.t()) :: boolean()
def has_active_subscription?(user) do
user |> active_subscription_query() |> Repo.exists?()
end
def subscription_created(params) do
@ -41,7 +41,7 @@ defmodule Plausible.Billing do
end
def change_plan(user, new_plan_id) do
subscription = active_subscription_for(user.id)
subscription = active_subscription_for(user)
plan = Plans.find(new_plan_id)
limit_checking_opts =
@ -55,7 +55,8 @@ defmodule Plausible.Billing do
do: do_change_plan(subscription, new_plan_id)
end
defp do_change_plan(subscription, new_plan_id) do
@doc false
def do_change_plan(subscription, new_plan_id) do
res =
paddle_api().update_subscription(subscription.paddle_subscription_id, %{
plan_id: new_plan_id
@ -283,9 +284,9 @@ defmodule Plausible.Billing do
"subscription_cancelled__#{user.id}"
end
defp active_subscription_query(user_id) do
defp active_subscription_query(user) do
from(s in Subscription,
where: s.user_id == ^user_id and s.status == ^Subscription.Status.active(),
where: s.user_id == ^user.id and s.status == ^Subscription.Status.active(),
order_by: [desc: s.inserted_at],
limit: 1
)

View File

@ -48,7 +48,8 @@ defmodule Plausible.Billing.Feature do
`{:error, :upgrade_required}` when toggling a feature the site owner does not
have access to.
"""
@callback toggle(Plausible.Site.t(), Keyword.t()) :: :ok | {:error, :upgrade_required}
@callback toggle(Plausible.Site.t(), Plausible.Auth.User.t(), Keyword.t()) ::
:ok | {:error, :upgrade_required}
@doc """
Checks whether a feature is enabled or not. Returns false when the feature is
@ -130,31 +131,20 @@ defmodule Plausible.Billing.Feature do
@impl true
def check_availability(%Plausible.Auth.User{} = user) do
cond do
free?() -> :ok
__MODULE__ in Quota.Limits.allowed_features_for(user) -> :ok
true -> {:error, :upgrade_required}
end
end
def check_availability(team_or_nil) do
cond do
free?() -> :ok
__MODULE__ in Plausible.Teams.Billing.allowed_features_for(team_or_nil) -> :ok
true -> {:error, :upgrade_required}
end
Plausible.Teams.Adapter.Read.Billing.check_feature_availability(__MODULE__, user)
end
@impl true
def toggle(%Plausible.Site{} = site, opts \\ []) do
if toggle_field(), do: do_toggle(site, opts), else: :ok
def toggle(%Plausible.Site{} = site, %Plausible.Auth.User{} = user, opts \\ []) do
if toggle_field(), do: do_toggle(site, user, opts), else: :ok
end
defp do_toggle(%Plausible.Site{} = site, opts) do
site = Plausible.Repo.preload(site, :owner)
defp do_toggle(%Plausible.Site{} = site, user, opts) do
owner = Plausible.Teams.Adapter.Read.Ownership.get_owner(site, user)
override = Keyword.get(opts, :override)
toggle = if is_boolean(override), do: override, else: !Map.fetch!(site, toggle_field())
availability = if toggle, do: check_availability(site.owner), else: :ok
availability = if toggle, do: check_availability(owner), else: :ok
case availability do
:ok ->
@ -210,38 +200,17 @@ defmodule Plausible.Billing.Feature.StatsAPI do
name: :stats_api,
display_name: "Stats API"
if Plausible.ee?() do
@impl true
@doc """
Checks whether the user has access to Stats API or not.
@impl true
@doc """
Checks whether the user has access to Stats API or not.
Before the business tier, users who had not yet started their trial had
access to Stats API. With the business tier work, access is blocked and they
must either start their trial or subscribe to a plan. This is common when a
site owner invites a new user. In such cases, using the owner's API key is
recommended.
"""
def check_availability(%Plausible.Auth.User{} = user) do
user = Plausible.Users.with_subscription(user)
unlimited_trial? = is_nil(user.trial_expiry_date)
subscription? = Plausible.Billing.Subscriptions.active?(user.subscription)
pre_business_tier_account? =
NaiveDateTime.before?(user.inserted_at, Plausible.Billing.Plans.business_tier_launch())
cond do
!subscription? && unlimited_trial? && pre_business_tier_account? ->
:ok
!subscription? && unlimited_trial? && !pre_business_tier_account? ->
{:error, :upgrade_required}
true ->
super(user)
end
end
else
@impl true
def check_availability(_user), do: :ok
Before the business tier, users who had not yet started their trial had
access to Stats API. With the business tier work, access is blocked and they
must either start their trial or subscribe to a plan. This is common when a
site owner invites a new user. In such cases, using the owner's API key is
recommended.
"""
def check_availability(%Plausible.Auth.User{} = user) do
Plausible.Teams.Adapter.Read.Billing.check_feature_availability_for_stats_api(user)
end
end

View File

@ -83,7 +83,7 @@ defmodule Plausible.Billing.PaddleApi do
end
end
@spec get_invoices(Plausible.Billing.Subscription.t()) ::
@spec get_invoices(Plausible.Billing.Subscription.t() | nil) ::
{:ok, list()}
| {:error, :request_failed}
| {:error, :no_invoices}

View File

@ -28,21 +28,20 @@ defmodule Plausible.Billing.Plans do
@business_tier_launch ~N[2023-11-08 12:00:00]
def business_tier_launch, do: @business_tier_launch
@spec growth_plans_for(User.t()) :: [Plan.t()]
@spec growth_plans_for(Subscription.t()) :: [Plan.t()]
@doc """
Returns a list of growth plans available for the user to choose.
Returns a list of growth plans available for the subscription to choose.
As new versions of plans are introduced, users who were on old plans can
As new versions of plans are introduced, subscriptions which were on old plans can
still choose from old plans.
"""
def growth_plans_for(%User{} = user) do
user = Plausible.Users.with_subscription(user)
owned_plan = get_regular_plan(user.subscription)
def growth_plans_for(subscription) do
owned_plan = get_regular_plan(subscription)
cond do
Application.get_env(:plausible, :environment) in ["dev", "staging"] -> @sandbox_plans
is_nil(owned_plan) -> @plans_v4
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
subscription && Subscriptions.expired?(subscription) -> @plans_v4
owned_plan.kind == :business -> @plans_v4
owned_plan.generation == 1 -> @plans_v1 |> drop_high_plans(owned_plan)
owned_plan.generation == 2 -> @plans_v2 |> drop_high_plans(owned_plan)
@ -52,21 +51,20 @@ defmodule Plausible.Billing.Plans do
|> Enum.filter(&(&1.kind == :growth))
end
def business_plans_for(%User{} = user) do
user = Plausible.Users.with_subscription(user)
owned_plan = get_regular_plan(user.subscription)
def business_plans_for(subscription) do
owned_plan = get_regular_plan(subscription)
cond do
Application.get_env(:plausible, :environment) in ["dev", "staging"] -> @sandbox_plans
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
subscription && Subscriptions.expired?(subscription) -> @plans_v4
owned_plan && owned_plan.generation < 4 -> @plans_v3
true -> @plans_v4
end
|> Enum.filter(&(&1.kind == :business))
end
def available_plans_for(%User{} = user, opts \\ []) do
plans = growth_plans_for(user) ++ business_plans_for(user)
def available_plans_for(subscription, opts \\ []) do
plans = growth_plans_for(subscription) ++ business_plans_for(subscription)
plans =
if Keyword.get(opts, :with_prices) do
@ -220,18 +218,16 @@ defmodule Plausible.Billing.Plans do
def suggest(user, usage_during_cycle) do
cond do
usage_during_cycle > @enterprise_level_usage -> :enterprise
Plausible.Auth.enterprise_configured?(user) -> :enterprise
true -> suggest_by_usage(user, usage_during_cycle)
Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user) -> :enterprise
true -> Plausible.Teams.Adapter.Read.Billing.suggest_by_usage(user, usage_during_cycle)
end
end
defp suggest_by_usage(user, usage_during_cycle) do
user = Plausible.Users.with_subscription(user)
def suggest_by_usage(subscription, usage_during_cycle) do
available_plans =
if business_tier?(user.subscription),
do: business_plans_for(user),
else: growth_plans_for(user)
if business_tier?(subscription),
do: business_plans_for(subscription),
else: growth_plans_for(subscription)
Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit))
end

View File

@ -58,7 +58,7 @@ defmodule Plausible.Billing.Quota.Limits do
@monthly_pageview_limit_for_free_10k 10_000
@monthly_pageview_limit_for_trials :unlimited
@spec monthly_pageview_limit(User.t() | Subscription.t()) ::
@spec monthly_pageview_limit(User.t() | Subscription.t() | nil) ::
non_neg_integer() | :unlimited
def monthly_pageview_limit(%User{} = user) do
user = Users.with_subscription(user)

View File

@ -67,10 +67,11 @@ defmodule Plausible.Billing.SiteLocker do
@spec send_grace_period_end_email(Plausible.Auth.User.t()) :: Plausible.Mailer.result()
def send_grace_period_end_email(user) do
usage = Plausible.Billing.Quota.Usage.monthly_pageview_usage(user)
usage = Plausible.Teams.Adapter.Read.Billing.monthly_pageview_usage(user)
suggested_plan = Plausible.Billing.Plans.suggest(user, usage.last_cycle.total)
PlausibleWeb.Email.dashboard_locked(user, usage, suggested_plan)
user
|> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan)
|> Plausible.Mailer.send()
end
end

View File

@ -17,6 +17,7 @@ defmodule Plausible.ClickhouseEventV2 do
field :"meta.key", {:array, :string}
field :"meta.value", {:array, :string}
field :scroll_depth, Ch, type: "UInt8"
field :revenue_source_amount, Ch, type: "Nullable(Decimal64(3))"
field :revenue_source_currency, Ch, type: "FixedString(3)"
@ -60,6 +61,7 @@ defmodule Plausible.ClickhouseEventV2 do
:timestamp,
:"meta.key",
:"meta.value",
:scroll_depth,
:revenue_source_amount,
:revenue_source_currency,
:revenue_reporting_amount,

View File

@ -1,51 +0,0 @@
defmodule Plausible.DebugReplayInfo do
@moduledoc """
Function execution context (with arguments) to Sentry reports.
"""
require Logger
defmacro __using__(_) do
quote do
require Plausible.DebugReplayInfo
import Plausible.DebugReplayInfo, only: [include_sentry_replay_info: 0]
end
end
defmacro include_sentry_replay_info() do
module = __CALLER__.module
{function, arity} = __CALLER__.function
f = Function.capture(module, function, arity)
quote bind_quoted: [f: f] do
replay_info =
{f, binding()}
|> :erlang.term_to_iovec([:compressed])
|> IO.iodata_to_binary()
|> Base.encode64()
payload_size = byte_size(replay_info)
if payload_size <= 10_000 do
Sentry.Context.set_extra_context(%{
debug_replay_info: replay_info,
debug_replay_info_size: payload_size
})
else
Sentry.Context.set_extra_context(%{
debug_replay_info: :too_large,
debug_replay_info_size: payload_size
})
end
:ok
end
end
@spec deserialize(String.t()) :: any()
def deserialize(replay_info) do
replay_info
|> Base.decode64!()
|> :erlang.binary_to_term()
end
end

View File

@ -119,6 +119,7 @@ defmodule Plausible.Ingestion.Event do
put_user_agent: &put_user_agent/2,
put_basic_info: &put_basic_info/2,
put_source_info: &put_source_info/2,
maybe_infer_medium: &maybe_infer_medium/2,
put_props: &put_props/2,
put_revenue: &put_revenue/2,
put_salts: &put_salts/2,
@ -245,7 +246,8 @@ defmodule Plausible.Ingestion.Event do
timestamp: event.request.timestamp,
name: event.request.event_name,
hostname: event.request.hostname,
pathname: event.request.pathname
pathname: event.request.pathname,
scroll_depth: event.request.scroll_depth
})
end
@ -269,6 +271,18 @@ defmodule Plausible.Ingestion.Event do
})
end
defp maybe_infer_medium(%__MODULE__{} = event, _context) do
inferred_medium =
case event.clickhouse_session_attrs do
%{utm_medium: medium} when is_binary(medium) -> medium
%{utm_medium: nil, referrer_source: "Google", click_id_param: "gclid"} -> "(gclid)"
%{utm_medium: nil, referrer_source: "Bing", click_id_param: "msclkid"} -> "(msclkid)"
_ -> nil
end
update_session_attrs(event, %{utm_medium: inferred_medium})
end
defp put_geolocation(%__MODULE__{} = event, _context) do
case event.request.ip_classification do
"anonymous_vpn_ip" ->

View File

@ -41,6 +41,7 @@ defmodule Plausible.Ingestion.Request do
field :hash_mode, :integer
field :pathname, :string
field :props, :map
field :scroll_depth, :integer
on_ee do
field :revenue_source, :map
@ -77,6 +78,7 @@ defmodule Plausible.Ingestion.Request do
|> put_request_params(request_body)
|> put_referrer(request_body)
|> put_props(request_body)
|> put_scroll_depth(request_body)
|> put_pathname()
|> put_query_params()
|> put_revenue_source(request_body)
@ -245,6 +247,21 @@ defmodule Plausible.Ingestion.Request do
end
end
defp put_scroll_depth(changeset, %{} = request_body) do
if Changeset.get_field(changeset, :event_name) == "pageleave" do
scroll_depth =
case request_body["sd"] do
sd when is_integer(sd) and sd >= 0 and sd <= 100 -> sd
sd when is_integer(sd) and sd > 100 -> 100
_ -> 0
end
Changeset.put_change(changeset, :scroll_depth, scroll_depth)
else
changeset
end
end
defp put_query_params(changeset) do
case Changeset.get_field(changeset, :uri) do
%{query: query} when is_binary(query) ->

View File

@ -55,7 +55,7 @@ defmodule Plausible.Sites do
@spec set_option(Auth.User.t(), Site.t(), atom(), any()) :: Site.UserPreference.t()
def set_option(user, site, option, value) when option in Site.UserPreference.options() do
get_for_user!(user.id, site.domain)
Plausible.Teams.Adapter.Read.Sites.get_for_user!(user, site.domain)
user
|> Site.UserPreference.changeset(site, %{option => value})
@ -91,7 +91,7 @@ defmodule Plausible.Sites do
end)
|> Ecto.Multi.run(:clear_changed_from, fn
_repo, %{site_changeset: %{changes: %{domain: domain}}} ->
case get_for_user(user.id, domain, [:owner]) do
case Plausible.Teams.Adapter.Read.Sites.get_for_user(user, domain, [:owner]) do
%Site{domain_changed_from: ^domain} = site ->
site
|> Ecto.Changeset.change()
@ -204,46 +204,6 @@ defmodule Plausible.Sites do
base <> domain <> "?auth=" <> link.slug
end
@spec get_for_user!(Auth.User.t() | pos_integer(), String.t(), [
:super_admin | :owner | :admin | :viewer
]) ::
Site.t()
def get_for_user!(user, domain, roles \\ [:owner, :admin, :viewer])
def get_for_user!(%Auth.User{id: user_id}, domain, roles) do
get_for_user!(user_id, domain, roles)
end
def get_for_user!(user_id, domain, roles) do
if :super_admin in roles and Auth.is_super_admin?(user_id) do
get_by_domain!(domain)
else
user_id
|> get_for_user_q(domain, List.delete(roles, :super_admin))
|> Repo.one!()
end
end
@spec get_for_user(Auth.User.t() | pos_integer(), String.t(), [
:super_admin | :owner | :admin | :viewer
]) ::
Site.t() | nil
def get_for_user(user, domain, roles \\ [:owner, :admin, :viewer])
def get_for_user(%Auth.User{id: user_id}, domain, roles) do
get_for_user(user_id, domain, roles)
end
def get_for_user(user_id, domain, roles) do
if :super_admin in roles and Auth.is_super_admin?(user_id) do
get_by_domain(domain)
else
user_id
|> get_for_user_q(domain, List.delete(roles, :super_admin))
|> Repo.one()
end
end
def update_installation_meta!(site, meta) do
site
|> Ecto.Changeset.change()
@ -251,17 +211,6 @@ defmodule Plausible.Sites do
|> Repo.update!()
end
defp get_for_user_q(user_id, domain, roles) do
from(s in Site,
join: sm in Site.Membership,
on: sm.site_id == s.id,
where: sm.user_id == ^user_id,
where: sm.role in ^roles,
where: s.domain == ^domain or s.domain_changed_from == ^domain,
select: s
)
end
def has_goals?(site) do
Repo.exists?(
from(g in Plausible.Goal,

View File

@ -11,48 +11,37 @@ defmodule Plausible.Stats do
QueryRunner
}
use Plausible.DebugReplayInfo
def query(site, query) do
include_sentry_replay_info()
QueryRunner.run(site, query)
end
def breakdown(site, query, metrics, pagination) do
include_sentry_replay_info()
Breakdown.breakdown(site, query, metrics, pagination)
end
def aggregate(site, query, metrics) do
include_sentry_replay_info()
Aggregate.aggregate(site, query, metrics)
end
def timeseries(site, query, metrics) do
include_sentry_replay_info()
Timeseries.timeseries(site, query, metrics)
end
def current_visitors(site, duration \\ Duration.new!(minute: -5)) do
include_sentry_replay_info()
CurrentVisitors.current_visitors(site, duration)
end
on_ee do
def funnel(site, query, funnel) do
include_sentry_replay_info()
Plausible.Stats.Funnel.funnel(site, query, funnel)
end
end
def filter_suggestions(site, query, filter_name, filter_search) do
include_sentry_replay_info()
FilterSuggestions.filter_suggestions(site, query, filter_name, filter_search)
end
def custom_prop_value_filter_suggestions(site, query, prop_key, filter_search) do
include_sentry_replay_info()
FilterSuggestions.custom_prop_value_filter_suggestions(site, query, prop_key, filter_search)
end
end

View File

@ -541,6 +541,17 @@ defmodule Plausible.Stats.Filters.QueryParser do
end
end
defp validate_metric(:scroll_depth = metric, query) do
page_dimension? = Enum.member?(query.dimensions, "event:page")
toplevel_page_filter? = not is_nil(Filters.get_toplevel_filter(query, "event:page"))
if page_dimension? or toplevel_page_filter? do
:ok
else
{:error, "Metric `#{metric}` can only be queried with event:page filters or dimensions."}
end
end
defp validate_metric(:views_per_visit = metric, query) do
cond do
Filters.filtering_on_dimension?(query, "event:page") ->

View File

@ -44,7 +44,7 @@ defmodule Plausible.Stats.GoalSuggestions do
native_q =
from(e in base_event_query(site, query),
where: fragment("? ilike ?", e.name, ^matches),
where: e.name != "pageview",
where: e.name not in ["pageview", "pageleave"],
where: fragment("trim(?)", e.name) != "",
where: e.name == fragment("trim(?)", e.name),
where: e.name not in ^excluded,

View File

@ -18,7 +18,8 @@ defmodule Plausible.Stats.Metrics do
:conversion_rate,
:group_conversion_rate,
:time_on_page,
:percentage
:percentage,
:scroll_depth
] ++ on_ee(do: Plausible.Stats.Goal.Revenue.revenue_metrics(), else: [])
@metric_mappings Enum.into(@all_metrics, %{}, fn metric -> {to_string(metric), metric} end)

View File

@ -57,15 +57,19 @@ defmodule Plausible.Stats.Query do
Date.range(
date_range.first,
earliest(date_range.last, today)
clamp(today, date_range)
)
else
date_range
end
end
defp earliest(a, b) do
if Date.compare(a, b) in [:eq, :lt], do: a, else: b
defp clamp(date, date_range) do
cond do
date in date_range -> date
Date.before?(date, date_range.first) -> date_range.first
Date.after?(date, date_range.last) -> date_range.last
end
end
def set(query, keywords) do

View File

@ -245,6 +245,7 @@ defmodule Plausible.Stats.SQL.Expression do
def event_metric(:percentage), do: %{}
def event_metric(:conversion_rate), do: %{}
def event_metric(:scroll_depth), do: %{}
def event_metric(:group_conversion_rate), do: %{}
def event_metric(:total_visitors), do: %{}

View File

@ -126,7 +126,7 @@ defmodule Plausible.Stats.SQL.QueryBuilder do
|> Enum.reduce(%{}, &Map.merge/2)
end
defp build_group_by(q, table, query) do
def build_group_by(q, table, query) do
Enum.reduce(query.dimensions, q, &dimension_group_by(&2, table, query, &1))
end

View File

@ -16,6 +16,7 @@ defmodule Plausible.Stats.SQL.SpecialMetrics do
|> maybe_add_percentage_metric(site, query)
|> maybe_add_global_conversion_rate(site, query)
|> maybe_add_group_conversion_rate(site, query)
|> maybe_add_scroll_depth(site, query)
end
defp maybe_add_percentage_metric(q, site, query) do
@ -121,6 +122,55 @@ defmodule Plausible.Stats.SQL.SpecialMetrics do
end
end
def maybe_add_scroll_depth(q, site, query) do
if :scroll_depth in query.metrics do
max_per_visitor_q =
Base.base_event_query(site, query)
|> where([e], e.name == "pageleave")
|> select([e], %{
user_id: e.user_id,
max_scroll_depth: max(e.scroll_depth)
})
|> SQL.QueryBuilder.build_group_by(:events, query)
|> group_by([e], e.user_id)
dim_shortnames = Enum.map(query.dimensions, fn dim -> shortname(query, dim) end)
dim_select =
dim_shortnames
|> Enum.map(fn dim -> {dim, dynamic([p], field(p, ^dim))} end)
|> Map.new()
dim_group_by =
dim_shortnames
|> Enum.map(fn dim -> dynamic([p], field(p, ^dim)) end)
scroll_depth_q =
subquery(max_per_visitor_q)
|> select([p], %{
scroll_depth: fragment("toUInt8(round(ifNotFinite(avg(?), 0)))", p.max_scroll_depth)
})
|> select_merge(^dim_select)
|> group_by(^dim_group_by)
join_on_dim_condition =
if dim_shortnames == [] do
true
else
dim_shortnames
|> Enum.map(fn dim -> dynamic([_e, ..., s], selected_as(^dim) == field(s, ^dim)) end)
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|> Enum.reduce(fn condition, acc -> dynamic([], ^acc and ^condition) end)
end
q
|> join(:left, [e], s in subquery(scroll_depth_q), on: ^join_on_dim_condition)
|> select_merge_as([_e, ..., s], %{scroll_depth: fragment("any(?)", s.scroll_depth)})
else
q
end
end
# `total_visitors_subquery` returns a subquery which selects `total_visitors` -
# the number used as the denominator in the calculation of `conversion_rate` and
# `percentage` metrics.

View File

@ -74,6 +74,7 @@ defmodule Plausible.Stats.TableDecider do
defp metric_partitioner(_, :average_revenue), do: :event
defp metric_partitioner(_, :total_revenue), do: :event
defp metric_partitioner(_, :scroll_depth), do: :event
defp metric_partitioner(_, :pageviews), do: :event
defp metric_partitioner(_, :events), do: :event
defp metric_partitioner(_, :bounce_rate), do: :session

View File

@ -87,6 +87,7 @@ defmodule Plausible.Stats.Timeseries do
:views_per_visit -> Map.merge(row, %{views_per_visit: 0.0})
:conversion_rate -> Map.merge(row, %{conversion_rate: 0.0})
:group_conversion_rate -> Map.merge(row, %{group_conversion_rate: 0.0})
:scroll_depth -> Map.merge(row, %{scroll_depth: 0})
:bounce_rate -> Map.merge(row, %{bounce_rate: 0.0})
:visit_duration -> Map.merge(row, %{visit_duration: nil})
:average_revenue -> Map.merge(row, %{average_revenue: nil})

View File

@ -9,8 +9,9 @@ defmodule Plausible.Teams do
alias Plausible.Repo
use Plausible
@spec on_trial?(Teams.Team.t()) :: boolean()
@spec on_trial?(Teams.Team.t() | nil) :: boolean()
on_ee do
def on_trial?(nil), do: false
def on_trial?(%Teams.Team{trial_expiry_date: nil}), do: false
def on_trial?(team) do
@ -38,6 +39,19 @@ defmodule Plausible.Teams do
Repo.preload(team, :sites).sites
end
def owned_sites_ids(nil) do
[]
end
def owned_sites_ids(team) do
Repo.all(
from s in Plausible.Site,
where: s.team_id == ^team.id,
select: s.id,
order_by: [desc: s.id]
)
end
@doc """
Create (when necessary) and load team relation for provided site.
@ -110,6 +124,19 @@ defmodule Plausible.Teams do
end
end
def last_subscription_join_query() do
from(subscription in last_subscription_query(),
where: subscription.team_id == parent_as(:team).id
)
end
def last_subscription_query() do
from(subscription in Plausible.Billing.Subscription,
order_by: [desc: subscription.inserted_at, desc: subscription.id],
limit: 1
)
end
defp create_my_team(user) do
team =
"My Team"
@ -135,11 +162,4 @@ defmodule Plausible.Teams do
{:error, :exists_already}
end
end
defp last_subscription_query() do
from(subscription in Plausible.Billing.Subscription,
order_by: [desc: subscription.inserted_at, desc: subscription.id],
limit: 1
)
end
end

View File

@ -11,9 +11,8 @@ defmodule Plausible.Teams.Adapter do
end
end
def team_or_user(user) do
switch(
user,
def user_or_team(user) do
switch(user,
team_fn: &Function.identity/1,
user_fn: &Function.identity/1
)
@ -30,8 +29,11 @@ defmodule Plausible.Teams.Adapter do
{:error, _} -> nil
end
team = Plausible.Teams.with_subscription(team)
team_fn.(team)
else
user = Plausible.Users.with_subscription(user)
user_fn.(user)
end
end

View File

@ -1,9 +1,82 @@
defmodule Plausible.Teams.Adapter.Read.Billing do
@moduledoc """
Transition adapter for new schema reads
Transition adapter for new schema reads
"""
use Plausible.Teams.Adapter
def change_plan(user, new_plan_id) do
switch(user,
team_fn: &Plausible.Teams.Billing.change_plan(&1, new_plan_id),
user_fn: &Plausible.Billing.change_plan(&1, new_plan_id)
)
end
def enterprise_configured?(nil), do: false
def enterprise_configured?(user) do
switch(user,
team_fn: &Plausible.Teams.Billing.enterprise_configured?/1,
user_fn: &Plausible.Auth.enterprise_configured?/1
)
end
def latest_enterprise_plan_with_prices(user, customer_ip) do
switch(user,
team_fn: &Plausible.Teams.Billing.latest_enterprise_plan_with_price(&1, customer_ip),
user_fn: &Plausible.Billing.Plans.latest_enterprise_plan_with_price(&1, customer_ip)
)
end
def has_active_subscription?(user) do
switch(user,
team_fn: &Plausible.Teams.Billing.has_active_subscription?/1,
user_fn: &Plausible.Billing.has_active_subscription?/1
)
end
def active_subscription_for(user) do
switch(user,
team_fn: &Plausible.Teams.Billing.active_subscription_for/1,
user_fn: &Plausible.Billing.active_subscription_for/1
)
end
def get_subscription(user) do
case user_or_team(user) do
%{subscription: subscription} -> subscription
_ -> nil
end
end
def team_member_limit(user) do
switch(user,
team_fn: &Teams.Billing.team_member_limit/1,
user_fn: &Plausible.Billing.Quota.Limits.team_member_limit/1
)
end
def team_member_usage(user, opts \\ []) do
switch(user,
team_fn: &Teams.Billing.team_member_usage(&1, opts),
user_fn: &Plausible.Billing.Quota.Usage.team_member_usage(&1, opts)
)
end
def monthly_pageview_limit(user) do
switch(user,
team_fn: &Teams.Billing.monthly_pageview_limit/1,
user_fn: &Plausible.Billing.Quota.Limits.monthly_pageview_limit/1
)
end
def monthly_pageview_usage(user, site_ids \\ nil) do
switch(
user,
team_fn: &Teams.Billing.monthly_pageview_usage(&1, site_ids),
user_fn: &Plausible.Billing.Quota.Usage.monthly_pageview_usage(&1, site_ids)
)
end
def check_needs_to_upgrade(user) do
switch(
user,
@ -34,4 +107,70 @@ defmodule Plausible.Teams.Adapter.Read.Billing do
user_fn: &Plausible.Billing.Quota.Usage.site_usage/1
)
end
use Plausible
on_ee do
def check_feature_availability_for_stats_api(user) do
{unlimited_trial?, subscription?} =
switch(user,
team_fn: fn team ->
team = Plausible.Teams.with_subscription(team)
unlimited_trial? = is_nil(team) or is_nil(team.trial_expiry_date)
subscription? =
not is_nil(team) and Plausible.Billing.Subscriptions.active?(team.subscription)
{unlimited_trial?, subscription?}
end,
user_fn: fn user ->
user = Plausible.Users.with_subscription(user)
unlimited_trial? = is_nil(user.trial_expiry_date)
subscription? = Plausible.Billing.Subscriptions.active?(user.subscription)
{unlimited_trial?, subscription?}
end
)
pre_business_tier_account? =
NaiveDateTime.before?(user.inserted_at, Plausible.Billing.Plans.business_tier_launch())
cond do
!subscription? && unlimited_trial? && pre_business_tier_account? ->
:ok
!subscription? && unlimited_trial? && !pre_business_tier_account? ->
{:error, :upgrade_required}
true ->
check_feature_availability(Plausible.Billing.Feature.StatsAPI, user)
end
end
else
def check_feature_availability_for_stats_api(_user), do: :ok
end
def check_feature_availability(feature, user) do
switch(user,
team_fn: fn team_or_nil ->
cond do
feature.free?() -> :ok
feature in Teams.Billing.allowed_features_for(team_or_nil) -> :ok
true -> {:error, :upgrade_required}
end
end,
user_fn: fn user ->
cond do
feature.free?() -> :ok
feature in Plausible.Billing.Quota.Limits.allowed_features_for(user) -> :ok
true -> {:error, :upgrade_required}
end
end
)
end
def suggest_by_usage(user, usage_during_cycle) do
subscription = get_subscription(user)
Plausible.Billing.Plans.suggest_by_usage(subscription, usage_during_cycle)
end
end

View File

@ -1,6 +1,6 @@
defmodule Plausible.Teams.Adapter.Read.Ownership do
@moduledoc """
Transition adapter for new schema reads
Transition adapter for new schema reads
"""
use Plausible
use Plausible.Teams.Adapter
@ -8,6 +8,27 @@ defmodule Plausible.Teams.Adapter.Read.Ownership do
alias Plausible.Auth
alias Plausible.Site.Memberships.Invitations
def all_pending_site_transfers(email, user) do
switch(user,
team_fn: fn _ -> Plausible.Teams.Memberships.all_pending_site_transfers(email) end,
user_fn: fn _ -> Plausible.Site.Memberships.all_pending_ownerships(email) end
)
end
def get_owner(site, user) do
switch(user,
team_fn: fn team ->
case Teams.Sites.get_owner(team) do
{:ok, user} -> user
_ -> nil
end
end,
user_fn: fn _ ->
Plausible.Repo.preload(site, :owner).owner
end
)
end
def ensure_can_take_ownership(site, user) do
switch(
user,
@ -39,11 +60,9 @@ defmodule Plausible.Teams.Adapter.Read.Ownership do
on_ee do
def check_feature_access(site, new_owner) do
team_or_user = team_or_user(new_owner)
missing_features =
Plausible.Billing.Quota.Usage.features_usage(nil, [site.id])
|> Enum.filter(&(&1.check_availability(team_or_user) != :ok))
|> Enum.filter(&(&1.check_availability(new_owner) != :ok))
if missing_features == [] do
:ok

View File

@ -3,11 +3,13 @@ defmodule Plausible.Teams.Adapter.Read.Sites do
Transition adapter for new schema reads
"""
use Plausible.Teams.Adapter
import Ecto.Query
alias Plausible.Repo
alias Plausible.Site
use Plausible.Teams.Adapter
alias Plausible.Teams
def list(user, pagination_params, opts \\ []) do
switch(
@ -182,6 +184,73 @@ defmodule Plausible.Teams.Adapter.Read.Sites do
end
end
def get_for_user!(user, domain, roles \\ [:owner, :admin, :viewer]) do
{query_fn, roles} = for_user_query_and_roles(user, roles)
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
Plausible.Sites.get_by_domain!(domain)
else
user.id
|> query_fn.(domain, List.delete(roles, :super_admin))
|> Repo.one!()
end
end
def get_for_user(user, domain, roles \\ [:owner, :admin, :viewer]) do
{query_fn, roles} = for_user_query_and_roles(user, roles)
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
Plausible.Sites.get_by_domain(domain)
else
user.id
|> query_fn.(domain, List.delete(roles, :super_admin))
|> Repo.one()
end
end
defp for_user_query_and_roles(user, roles) do
switch(
user,
team_fn: fn _ ->
translated_roles =
Enum.map(roles, fn
:admin -> :editor
other -> other
end)
{&new_get_for_user_query/3, translated_roles}
end,
user_fn: fn _ ->
{&old_get_for_user_query/3, roles}
end
)
end
defp old_get_for_user_query(user_id, domain, roles) do
from(s in Plausible.Site,
join: sm in Plausible.Site.Membership,
on: sm.site_id == s.id,
where: sm.user_id == ^user_id,
where: sm.role in ^roles,
where: s.domain == ^domain or s.domain_changed_from == ^domain,
select: s
)
end
defp new_get_for_user_query(user_id, domain, roles) do
roles = Enum.map(roles, &to_string/1)
from(s in Plausible.Site,
join: t in assoc(s, :team),
join: tm in assoc(t, :team_memberships),
left_join: gm in assoc(tm, :guest_memberships),
where: tm.user_id == ^user_id,
where: coalesce(gm.role, tm.role) in ^roles,
where: s.domain == ^domain or s.domain_changed_from == ^domain,
select: s
)
end
defp maybe_filter_by_domain(query, domain)
when byte_size(domain) >= 1 and byte_size(domain) <= 64 do
where(query, [s], ilike(s.domain, ^"%#{domain}%"))

View File

@ -12,10 +12,65 @@ defmodule Plausible.Teams.Billing do
alias Plausible.Billing.{Plan, Plans, EnterprisePlan, Feature}
alias Plausible.Billing.Feature.{Goals, Props, StatsAPI}
require Plausible.Billing.Subscription.Status
@team_member_limit_for_trials 3
@limit_sites_since ~D[2021-05-05]
@site_limit_for_trials 10
def change_plan(team, new_plan_id) do
subscription = active_subscription_for(team)
plan = Plausible.Billing.Plans.find(new_plan_id)
limit_checking_opts =
if team.allow_next_upgrade_override do
[ignore_pageview_limit: true]
else
[]
end
usage = quota_usage(team)
with :ok <-
Plausible.Billing.Quota.ensure_within_plan_limits(usage, plan, limit_checking_opts),
do: Plausible.Billing.do_change_plan(subscription, new_plan_id)
end
def enterprise_configured?(nil), do: false
def enterprise_configured?(%Teams.Team{} = team) do
team
|> Ecto.assoc(:enterprise_plan)
|> Repo.exists?()
end
def latest_enterprise_plan_with_price(team, customer_ip) do
enterprise_plan =
Repo.one!(
from(e in EnterprisePlan,
where: e.team_id == ^team.id,
order_by: [desc: e.inserted_at],
limit: 1
)
)
{enterprise_plan, Plausible.Billing.Plans.get_price_for(enterprise_plan, customer_ip)}
end
def has_active_subscription?(nil), do: false
def has_active_subscription?(team) do
team
|> active_subscription_query()
|> Repo.exists?()
end
def active_subscription_for(team) do
team
|> active_subscription_query()
|> Repo.one()
end
def check_needs_to_upgrade(nil), do: {:needs_to_upgrade, :no_trial}
def check_needs_to_upgrade(team) do
@ -85,6 +140,10 @@ defmodule Plausible.Teams.Billing do
|> length()
end
defp get_site_limit_from_plan(nil) do
@site_limit_for_trials
end
defp get_site_limit_from_plan(team) do
team =
Teams.with_subscription(team)
@ -96,6 +155,10 @@ defmodule Plausible.Teams.Billing do
end
end
def team_member_limit(nil) do
@team_member_limit_for_trials
end
def team_member_limit(team) do
team = Teams.with_subscription(team)
@ -106,11 +169,11 @@ defmodule Plausible.Teams.Billing do
end
end
def quota_usage(team, opts) do
def quota_usage(team, opts \\ []) do
team = Teams.with_subscription(team)
with_features? = Keyword.get(opts, :with_features, false)
pending_site_ids = Keyword.get(opts, :pending_ownership_site_ids, [])
team_site_ids = team |> Teams.owned_sites() |> Enum.map(& &1.id)
team_site_ids = Teams.owned_sites_ids(team)
all_site_ids = pending_site_ids ++ team_site_ids
monthly_pageviews = monthly_pageview_usage(team, all_site_ids)
@ -129,6 +192,50 @@ defmodule Plausible.Teams.Billing do
end
end
@monthly_pageview_limit_for_free_10k 10_000
@monthly_pageview_limit_for_trials :unlimited
def monthly_pageview_limit(nil) do
@monthly_pageview_limit_for_trials
end
def monthly_pageview_limit(%Teams.Team{} = team) do
team = Teams.with_subscription(team)
monthly_pageview_limit(team.subscription)
end
def monthly_pageview_limit(subscription) do
case Plans.get_subscription_plan(subscription) do
%EnterprisePlan{monthly_pageview_limit: limit} ->
limit
%Plan{monthly_pageview_limit: limit} ->
limit
:free_10k ->
@monthly_pageview_limit_for_free_10k
_any ->
if subscription do
Sentry.capture_message("Unknown monthly pageview limit for plan",
extra: %{paddle_plan_id: subscription.paddle_plan_id}
)
end
@monthly_pageview_limit_for_trials
end
end
def monthly_pageview_usage(team, site_ids \\ nil)
def monthly_pageview_usage(team, nil) do
monthly_pageview_usage(team, Teams.owned_sites_ids(team))
end
def monthly_pageview_usage(nil, _site_ids) do
%{last_30_days: usage_cycle(nil, :last_30_days, [])}
end
def monthly_pageview_usage(team, site_ids) do
team = Teams.with_subscription(team)
active_subscription? = Subscriptions.active?(team.subscription)
@ -298,4 +405,13 @@ defmodule Plausible.Teams.Billing do
end
end
end
defp active_subscription_query(team) do
from(s in Plausible.Billing.Subscription,
where:
s.team_id == ^team.id and s.status == ^Plausible.Billing.Subscription.Status.active(),
order_by: [desc: s.inserted_at],
limit: 1
)
end
end

View File

@ -7,6 +7,26 @@ defmodule Plausible.Teams.Memberships do
alias Plausible.Repo
alias Plausible.Teams
def all_pending_site_transfers(email) do
email
|> pending_site_transfers_query()
|> Repo.all()
|> Enum.map(fn transfer ->
%Plausible.Auth.Invitation{
site_id: transfer.site_id,
email: transfer.email,
invitation_id: transfer.transfer_id,
role: :owner
}
end)
end
def any_pending_site_transfers?(email) do
email
|> pending_site_transfers_query()
|> Repo.exists?()
end
def get(team, user) do
result =
from(tm in Teams.Membership,
@ -127,4 +147,8 @@ defmodule Plausible.Teams.Memberships do
membership -> {:ok, membership}
end
end
defp pending_site_transfers_query(email) do
from st in Teams.SiteTransfer, where: st.email == ^email
end
end

View File

@ -191,7 +191,7 @@ defmodule Plausible.Users do
user
end
defp last_subscription_query() do
def last_subscription_query() do
from(subscription in Subscription,
order_by: [desc: subscription.inserted_at],
limit: 1

View File

@ -189,7 +189,7 @@ defmodule PlausibleWeb.Components.Billing do
</div>
<.styled_link
:if={
not (Plausible.Auth.enterprise_configured?(@user) &&
not (Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(@user) &&
Subscriptions.halted?(@subscription))
}
id="#upgrade-or-change-plan-link"

View File

@ -357,7 +357,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
|> Enum.map(fn feature_mod -> feature_mod.display_name() end)
|> PlausibleWeb.TextHelpers.pretty_join()
"This plan does not support #{features_list_str}, which you are currently using. Please note that by subscribing to this plan you will lose access to #{if length(features) == 1, do: "this feature", else: "these features"}."
"This plan does not support #{features_list_str}, which you have been using. By subscribing to this plan, you will not have access to #{if length(features) == 1, do: "this feature", else: "these features"}."
end
defp contact_button(assigns) do

View File

@ -12,7 +12,7 @@ defmodule PlausibleWeb.AdminController do
usage = Quota.Usage.usage(user, with_features: true)
limits = %{
monthly_pageviews: Quota.Limits.monthly_pageview_limit(user),
monthly_pageviews: Quota.Limits.monthly_pageview_limit(user.subscription),
sites: Quota.Limits.site_limit(user),
team_members: Quota.Limits.team_member_limit(user)
}

View File

@ -25,11 +25,11 @@ defmodule PlausibleWeb.Api.InternalController do
"conversions" => Plausible.Billing.Feature.Goals
}
def disable_feature(conn, %{"domain" => domain, "feature" => feature}) do
with %User{id: user_id} <- conn.assigns[:current_user],
with %User{id: user_id} = user <- conn.assigns[:current_user],
site <- Sites.get_by_domain(domain),
true <- Sites.has_admin_access?(user_id, site) || Auth.is_super_admin?(user_id),
{:ok, mod} <- Map.fetch(@features, feature),
{:ok, _site} <- mod.toggle(site, override: false) do
{:ok, _site} <- mod.toggle(site, user, override: false) do
json(conn, "ok")
else
{:error, :upgrade_required} ->

View File

@ -193,12 +193,13 @@ defmodule PlausibleWeb.Api.StatsController do
def top_stats(conn, params) do
site = conn.assigns[:site]
current_user = conn.assigns[:current_user]
params = realtime_period_to_30m(params)
query = Query.from(site, params, debug_metadata(conn))
{top_stats, sample_percent} = fetch_top_stats(site, query)
{top_stats, sample_percent} = fetch_top_stats(site, query, current_user)
comparison_query = comparison_query(query)
json(conn, %{
@ -293,7 +294,7 @@ defmodule PlausibleWeb.Api.StatsController do
end
end
defp fetch_top_stats(site, query) do
defp fetch_top_stats(site, query, current_user) do
goal_filter? = Filters.filtering_on_dimension?(query, "event:goal")
cond do
@ -307,7 +308,7 @@ defmodule PlausibleWeb.Api.StatsController do
fetch_goal_top_stats(site, query)
true ->
fetch_other_top_stats(site, query)
fetch_other_top_stats(site, query, current_user)
end
end
@ -391,16 +392,24 @@ defmodule PlausibleWeb.Api.StatsController do
|> then(&{&1, 100})
end
defp fetch_other_top_stats(site, query) do
defp fetch_other_top_stats(site, query, current_user) do
page_filter? = Filters.filtering_on_dimension?(query, "event:page")
metrics = [:visitors, :visits, :pageviews, :sample_percent]
metrics =
cond do
page_filter? && query.include_imported -> metrics
page_filter? -> metrics ++ [:bounce_rate, :time_on_page]
true -> metrics ++ [:views_per_visit, :bounce_rate, :visit_duration]
page_filter? && query.include_imported ->
metrics
page_filter? && scroll_depth_enabled?(site, current_user) ->
metrics ++ [:bounce_rate, :scroll_depth, :time_on_page]
page_filter? ->
metrics ++ [:bounce_rate, :time_on_page]
true ->
metrics ++ [:views_per_visit, :bounce_rate, :visit_duration]
end
current_results = Stats.aggregate(site, query, metrics)
@ -418,7 +427,8 @@ defmodule PlausibleWeb.Api.StatsController do
nil -> 0
value -> value
end
)
),
top_stats_entry(current_results, "Scroll depth", :scroll_depth)
]
|> Enum.filter(& &1)
@ -819,13 +829,22 @@ defmodule PlausibleWeb.Api.StatsController do
def pages(conn, params) do
site = conn.assigns[:site]
current_user = conn.assigns[:current_user]
params = Map.put(params, "property", "event:page")
query = Query.from(site, params, debug_metadata(conn))
extra_metrics =
if params["detailed"],
do: [:pageviews, :bounce_rate, :time_on_page],
else: []
cond do
params["detailed"] && !query.include_imported && scroll_depth_enabled?(site, current_user) ->
[:pageviews, :bounce_rate, :time_on_page, :scroll_depth]
params["detailed"] ->
[:pageviews, :bounce_rate, :time_on_page]
true ->
[]
end
metrics = breakdown_metrics(query, extra_metrics)
pagination = parse_pagination(params)
@ -1532,11 +1551,20 @@ defmodule PlausibleWeb.Api.StatsController do
end
requires_goal_filter? = metric in [:conversion_rate, :events]
has_goal_filter? = Filters.filtering_on_dimension?(query, "event:goal")
if requires_goal_filter? and !Filters.filtering_on_dimension?(query, "event:goal") do
{:error, "Metric `#{metric}` can only be queried with a goal filter"}
else
{:ok, metric}
requires_page_filter? = metric == :scroll_depth
has_page_filter? = Filters.filtering_on_dimension?(query, "event:page")
cond do
requires_goal_filter? and not has_goal_filter? ->
{:error, "Metric `#{metric}` can only be queried with a goal filter"}
requires_page_filter? and not has_page_filter? ->
{:error, "Metric `#{metric}` can only be queried with a page filter"}
true ->
{:ok, metric}
end
end
@ -1588,4 +1616,9 @@ defmodule PlausibleWeb.Api.StatsController do
end
defp realtime_period_to_30m(params), do: params
defp scroll_depth_enabled?(site, user) do
FunWithFlags.enabled?(:scroll_depth, for: user) ||
FunWithFlags.enabled?(:scroll_depth, for: site)
end
end

View File

@ -9,14 +9,16 @@ defmodule PlausibleWeb.BillingController do
plug PlausibleWeb.RequireAccountPlug
def ping_subscription(%Plug.Conn{} = conn, _params) do
subscribed? = Billing.has_active_subscription?(conn.assigns.current_user.id)
subscribed? =
Plausible.Teams.Adapter.Read.Billing.has_active_subscription?(conn.assigns.current_user)
json(conn, %{is_subscribed: subscribed?})
end
def choose_plan(conn, _params) do
current_user = conn.assigns.current_user
if Plausible.Auth.enterprise_configured?(current_user) do
if Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(current_user) do
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
else
render(conn, "choose_plan.html",
@ -28,19 +30,20 @@ defmodule PlausibleWeb.BillingController do
def upgrade_to_enterprise_plan(conn, _params) do
current_user = conn.assigns.current_user
subscription = Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
{latest_enterprise_plan, price} =
Plans.latest_enterprise_plan_with_price(current_user, PlausibleWeb.RemoteIP.get(conn))
subscription_resumable? =
Plausible.Billing.Subscriptions.resumable?(current_user.subscription)
Plausible.Billing.Subscriptions.resumable?(subscription)
subscribed_to_latest? =
subscription_resumable? &&
current_user.subscription.paddle_plan_id == latest_enterprise_plan.paddle_plan_id
subscription.paddle_plan_id == latest_enterprise_plan.paddle_plan_id
cond do
Subscription.Status.in?(current_user.subscription, [
Subscription.Status.in?(subscription, [
Subscription.Status.past_due(),
Subscription.Status.paused()
]) ->
@ -66,8 +69,9 @@ defmodule PlausibleWeb.BillingController do
def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do
current_user = conn.assigns.current_user
subscription = Plausible.Teams.Adapter.Read.Billing.active_subscription_for(current_user)
case preview_subscription(current_user, new_plan_id) do
case preview_subscription(subscription, new_plan_id) do
{:ok, {subscription, preview_info}} ->
render(conn, "change_plan_preview.html",
back_link: Routes.billing_path(conn, :choose_plan),
@ -97,7 +101,7 @@ defmodule PlausibleWeb.BillingController do
def change_plan(conn, %{"new_plan_id" => new_plan_id}) do
current_user = conn.assigns.current_user
case Billing.change_plan(current_user, new_plan_id) do
case Plausible.Teams.Adapter.Read.Billing.change_plan(current_user, new_plan_id) do
{:ok, _subscription} ->
conn
|> put_flash(:success, "Plan changed successfully")
@ -135,15 +139,11 @@ defmodule PlausibleWeb.BillingController do
end
end
defp preview_subscription(%{id: user_id}, new_plan_id) do
subscription = Billing.active_subscription_for(user_id)
defp preview_subscription(nil, _new_plan_id), do: {:error, :no_subscription}
if subscription do
with {:ok, preview_info} <- Billing.change_plan_preview(subscription, new_plan_id) do
{:ok, {subscription, preview_info}}
end
else
{:error, :no_subscription}
defp preview_subscription(subscription, new_plan_id) do
with {:ok, preview_info} <- Billing.change_plan_preview(subscription, new_plan_id) do
{:ok, {subscription, preview_info}}
end
end
end

View File

@ -5,8 +5,6 @@ defmodule PlausibleWeb.SettingsController do
alias Plausible.Auth
alias PlausibleWeb.UserAuth
alias Plausible.Billing.Quota
require Logger
def index(conn, _params) do
@ -23,22 +21,25 @@ defmodule PlausibleWeb.SettingsController do
def subscription(conn, _params) do
current_user = conn.assigns.current_user
subscription = Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
render(conn, :subscription,
layout: {PlausibleWeb.LayoutView, :settings},
subscription: current_user.subscription,
pageview_limit: Quota.Limits.monthly_pageview_limit(current_user),
pageview_usage: Quota.Usage.monthly_pageview_usage(current_user),
site_usage: Quota.Usage.site_usage(current_user),
site_limit: Quota.Limits.site_limit(current_user),
team_member_limit: Quota.Limits.team_member_limit(current_user),
team_member_usage: Quota.Usage.team_member_usage(current_user)
subscription: subscription,
pageview_limit: Plausible.Teams.Adapter.Read.Billing.monthly_pageview_limit(current_user),
pageview_usage: Plausible.Teams.Adapter.Read.Billing.monthly_pageview_usage(current_user),
site_usage: Plausible.Teams.Adapter.Read.Billing.site_usage(current_user),
site_limit: Plausible.Teams.Adapter.Read.Billing.site_limit(current_user),
team_member_limit: Plausible.Teams.Adapter.Read.Billing.team_member_limit(current_user),
team_member_usage: Plausible.Teams.Adapter.Read.Billing.team_member_usage(current_user)
)
end
def invoices(conn, _params) do
current_user = conn.assigns.current_user
invoices = Plausible.Billing.paddle_api().get_invoices(current_user.subscription)
subscription =
Plausible.Teams.Adapter.Read.Billing.get_subscription(conn.assigns.current_user)
invoices = Plausible.Billing.paddle_api().get_invoices(subscription)
render(conn, :invoices, layout: {PlausibleWeb.LayoutView, :settings}, invoices: invoices)
end

View File

@ -13,7 +13,6 @@ defmodule PlausibleWeb.Site.MembershipController do
use PlausibleWeb, :controller
use Plausible.Repo
use Plausible
alias Plausible.Sites
alias Plausible.Site.{Membership, Memberships}
@only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership]
@ -26,8 +25,8 @@ defmodule PlausibleWeb.Site.MembershipController do
def invite_member_form(conn, _params) do
site =
conn.assigns.current_user.id
|> Sites.get_for_user!(conn.assigns.site.domain)
conn.assigns.current_user
|> Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.site.domain)
|> Plausible.Repo.preload(:owner)
limit = Plausible.Billing.Quota.Limits.team_member_limit(site.owner)
@ -45,10 +44,10 @@ defmodule PlausibleWeb.Site.MembershipController do
end
def invite_member(conn, %{"email" => email, "role" => role}) do
site_domain = conn.assigns[:site].domain
site_domain = conn.assigns.site.domain
site =
Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|> Plausible.Repo.preload(:owner)
case Memberships.create_invitation(site, conn.assigns.current_user, email, role) do
@ -94,8 +93,10 @@ defmodule PlausibleWeb.Site.MembershipController do
end
def transfer_ownership_form(conn, _params) do
site_domain = conn.assigns[:site].domain
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
site_domain = conn.assigns.site.domain
site =
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
render(
conn,
@ -106,8 +107,10 @@ defmodule PlausibleWeb.Site.MembershipController do
end
def transfer_ownership(conn, %{"email" => email}) do
site_domain = conn.assigns[:site].domain
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
site_domain = conn.assigns.site.domain
site =
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
case Memberships.create_invitation(site, conn.assigns.current_user, email, :owner) do
{:ok, _invitation} ->

View File

@ -81,7 +81,7 @@ defmodule PlausibleWeb.SiteController do
feature_mod =
Enum.find(Plausible.Billing.Feature.list(), &(&1.toggle_field() == toggle_field))
case feature_mod.toggle(site, override: value == "true") do
case feature_mod.toggle(site, conn.assigns.current_user, override: value == "true") do
{:ok, updated_site} ->
message =
if Map.fetch!(updated_site, toggle_field) do
@ -152,13 +152,8 @@ defmodule PlausibleWeb.SiteController do
end
def settings_goals(conn, _params) do
site = Repo.preload(conn.assigns[:site], [:owner])
owner = Plausible.Users.with_subscription(site.owner)
site = Map.put(site, :owner, owner)
conn
|> render("settings_goals.html",
site: site,
dogfood_page_path: "/:dashboard/settings/goals",
connect_live_socket: true,
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
@ -166,13 +161,8 @@ defmodule PlausibleWeb.SiteController do
end
def settings_funnels(conn, _params) do
site = Repo.preload(conn.assigns[:site], [:owner])
owner = Plausible.Users.with_subscription(site.owner)
site = Map.put(site, :owner, owner)
conn
|> render("settings_funnels.html",
site: site,
dogfood_page_path: "/:dashboard/settings/funnels",
connect_live_socket: true,
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
@ -180,13 +170,8 @@ defmodule PlausibleWeb.SiteController do
end
def settings_props(conn, _params) do
site = Repo.preload(conn.assigns[:site], [:owner])
owner = Plausible.Users.with_subscription(site.owner)
site = Map.put(site, :owner, owner)
conn
|> render("settings_props.html",
site: site,
dogfood_page_path: "/:dashboard/settings/properties",
layout: {PlausibleWeb.LayoutView, "site_settings.html"},
connect_live_socket: true

View File

@ -375,7 +375,7 @@ defmodule PlausibleWeb.StatsController do
defp get_flags(user, site),
do:
[:channels, :saved_segments]
[:channels, :saved_segments, :scroll_depth]
|> Enum.map(fn flag ->
{flag, FunWithFlags.enabled?(flag, for: user) || FunWithFlags.enabled?(flag, for: site)}
end)

View File

@ -8,7 +8,6 @@ defmodule PlausibleWeb.Live.ChoosePlan do
require Plausible.Billing.Subscription.Status
alias PlausibleWeb.Components.Billing.{PlanBox, PlanBenefits, Notice, PageviewSlider}
alias Plausible.Site
alias Plausible.Billing.{Plans, Quota}
@contact_link "https://plausible.io/contact"
@ -19,7 +18,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
socket
|> assign_new(:pending_ownership_site_ids, fn %{current_user: current_user} ->
current_user.email
|> Site.Memberships.all_pending_ownerships()
|> Plausible.Teams.Adapter.Read.Ownership.all_pending_site_transfers(current_user)
|> Enum.map(& &1.site_id)
end)
|> assign_new(:usage, fn %{
@ -31,17 +30,20 @@ defmodule PlausibleWeb.Live.ChoosePlan do
pending_ownership_site_ids: pending_ownership_site_ids
)
end)
|> assign_new(:owned_plan, fn %{current_user: %{subscription: subscription}} ->
|> assign_new(:subscription, fn %{current_user: current_user} ->
Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
end)
|> assign_new(:owned_plan, fn %{subscription: subscription} ->
Plans.get_regular_plan(subscription, only_non_expired: true)
end)
|> assign_new(:owned_tier, fn %{owned_plan: owned_plan} ->
if owned_plan, do: Map.get(owned_plan, :kind), else: nil
end)
|> assign_new(:current_interval, fn %{current_user: current_user} ->
current_user_subscription_interval(current_user.subscription)
|> assign_new(:current_interval, fn %{subscription: subscription} ->
current_user_subscription_interval(subscription)
end)
|> assign_new(:available_plans, fn %{current_user: current_user} ->
Plans.available_plans_for(current_user, with_prices: true, customer_ip: remote_ip)
|> assign_new(:available_plans, fn %{subscription: subscription} ->
Plans.available_plans_for(subscription, with_prices: true, customer_ip: remote_ip)
end)
|> assign_new(:recommended_tier, fn %{usage: usage, available_plans: available_plans} ->
highest_growth_plan = List.last(available_plans.growth)
@ -102,8 +104,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
class="pb-6"
pending_ownership_count={length(@pending_ownership_site_ids)}
/>
<Notice.subscription_past_due class="pb-6" subscription={@current_user.subscription} />
<Notice.subscription_paused class="pb-6" subscription={@current_user.subscription} />
<Notice.subscription_past_due class="pb-6" subscription={@subscription} />
<Notice.subscription_paused class="pb-6" subscription={@subscription} />
<Notice.upgrade_ineligible :if={not Quota.eligible_for_upgrade?(@usage)} />
<div class="mx-auto max-w-4xl text-center">
<p class="text-4xl font-bold tracking-tight lg:text-5xl">

View File

@ -4,7 +4,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
"""
use PlausibleWeb, :live_view
alias Plausible.{Sites, Goals}
alias Plausible.Goals
alias PlausibleWeb.Live.Components.Modal
def mount(
@ -16,7 +16,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
socket
|> assign_new(:site, fn %{current_user: current_user} ->
current_user
|> Sites.get_for_user!(domain, [:owner, :admin, :super_admin])
|> Plausible.Teams.Adapter.Read.Sites.get_for_user!(domain, [:owner, :admin, :super_admin])
|> Plausible.Imported.load_import_data()
end)
|> assign_new(:all_goals, fn %{site: site} ->

View File

@ -8,7 +8,6 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
alias Plausible.Imported
alias Plausible.Imported.SiteImport
alias Plausible.Sites
require Plausible.Imported.SiteImport
@ -16,7 +15,11 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:site_imports, fn %{site: site} ->
site

View File

@ -32,7 +32,7 @@ defmodule PlausibleWeb.Live.Installation do
socket
) do
site =
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin,

View File

@ -5,14 +5,17 @@ defmodule PlausibleWeb.Live.Plugins.API.Settings do
use PlausibleWeb, :live_view
alias Plausible.Sites
alias Plausible.Plugins.API.Tokens
def mount(_params, %{"domain" => domain} = session, socket) do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:displayed_tokens, fn %{site: site} ->
Tokens.list(site)

View File

@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
use PlausibleWeb, live_view: :no_sentry_context
import PlausibleWeb.Live.Components.Form
alias Plausible.Sites
alias Plausible.Plugins.API.{Token, Tokens}
def mount(
@ -20,7 +19,11 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
token = Token.generate()

View File

@ -11,7 +11,11 @@ defmodule PlausibleWeb.Live.PropsSettings do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Plausible.Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:all_props, fn %{site: site} ->
site.allowed_event_props || []

View File

@ -18,7 +18,11 @@ defmodule PlausibleWeb.Live.PropsSettings.Form do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Plausible.Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:form, fn %{site: site} ->
new_form(site)

View File

@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.Shields.Countries do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(
_params,
@ -15,7 +14,11 @@ defmodule PlausibleWeb.Live.Shields.Countries do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:country_rules_count, fn %{site: site} ->
Shields.count_country_rules(site)

View File

@ -5,13 +5,16 @@ defmodule PlausibleWeb.Live.Shields.Hostnames do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(_params, %{"domain" => domain}, socket) do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:hostname_rules_count, fn %{site: site} ->
Shields.count_hostname_rules(site)

View File

@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.Shields.IPAddresses do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(
_params,
@ -18,7 +17,11 @@ defmodule PlausibleWeb.Live.Shields.IPAddresses do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:ip_rules_count, fn %{site: site} ->
Shields.count_ip_rules(site)

View File

@ -5,13 +5,16 @@ defmodule PlausibleWeb.Live.Shields.Pages do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(_params, %{"domain" => domain}, socket) do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:page_rules_count, fn %{site: site} ->
Shields.count_page_rules(site)

View File

@ -18,7 +18,7 @@ defmodule PlausibleWeb.Live.Verification do
socket
) do
site =
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin,

View File

@ -107,9 +107,21 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
Sentry.Context.set_extra_context(%{site_id: site.id, domain: site.domain})
Plausible.OpenTelemetry.add_site_attributes(site)
site = Plausible.Imported.load_import_data(site)
site =
site
|> Plausible.Imported.load_import_data()
|> Repo.preload(
team: [subscription: Plausible.Teams.last_subscription_query()],
owner: [subscription: Plausible.Users.last_subscription_query()]
)
merge_assigns(conn, site: site, current_user_role: role)
conn = merge_assigns(conn, site: site, current_user_role: role)
if not is_nil(current_user) and role not in [:public, nil] do
assign(conn, :current_team, site.team)
else
conn
end
else
error_not_found(conn)
end

View File

@ -6,7 +6,9 @@
<div class="flex flex-col gap-y-2">
<Notice.active_grace_period
:if={Plausible.Auth.GracePeriod.active?(@conn.assigns.current_user)}
enterprise?={Plausible.Auth.enterprise_configured?(@conn.assigns.current_user)}
enterprise?={
Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(@conn.assigns.current_user)
}
grace_period_end={grace_period_end(@conn.assigns.current_user)}
/>

View File

@ -123,7 +123,8 @@ defmodule PlausibleWeb.UserAuth do
defp get_session_by_token(token) do
now = NaiveDateTime.utc_now(:second)
last_subscription_query = Plausible.Users.last_subscription_join_query()
last_user_subscription_query = Plausible.Users.last_subscription_join_query()
last_team_subscription_query = Plausible.Teams.last_subscription_join_query()
token_query =
from(us in Auth.UserSession,
@ -132,10 +133,13 @@ defmodule PlausibleWeb.UserAuth do
left_join: tm in assoc(u, :team_memberships),
on: tm.role != :guest,
left_join: t in assoc(tm, :team),
left_lateral_join: s in subquery(last_subscription_query),
as: :team,
left_lateral_join: ts in subquery(last_team_subscription_query),
on: true,
left_lateral_join: s in subquery(last_user_subscription_query),
on: true,
where: us.token == ^token and us.timeout_at > ^now,
preload: [user: {u, subscription: s, team_memberships: {tm, team: t}}]
preload: [user: {u, subscription: s, team_memberships: {tm, team: {t, subscription: ts}}}]
)
case Repo.one(token_query) do

View File

@ -150,7 +150,7 @@ defmodule Plausible.Workers.CheckUsage do
defp check_pageview_usage_two_cycles(subscriber, usage_mod) do
usage = usage_mod.monthly_pageview_usage(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber.subscription)
if Quota.exceeds_last_two_usage_cycles?(usage, limit) do
{:over_limit, usage}
@ -161,7 +161,7 @@ defmodule Plausible.Workers.CheckUsage do
defp check_pageview_usage_last_cycle(subscriber, usage_mod) do
usage = usage_mod.monthly_pageview_usage(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber.subscription)
if :last_cycle in Quota.exceeded_cycles(usage, limit) do
{:over_limit, usage}

View File

@ -0,0 +1,9 @@
defmodule Plausible.IngestRepo.Migrations.AddScrollDepthToEvents do
use Ecto.Migration
def change do
alter table(:events_v2) do
add :scroll_depth, :UInt8
end
end
end

View File

@ -0,0 +1,27 @@
defmodule Plausible.IngestRepo.Migrations.BackfillUtmMediumClickIdParam do
@moduledoc """
Backfills utm_medium based on referrer_source and click_id_param
"""
use Ecto.Migration
def up do
execute(fn -> repo().query!(update_query("events_v2")) end)
execute(fn -> repo().query!(update_query("sessions_v2")) end)
end
def down do
raise "irreversible"
end
defp update_query(table) do
"""
ALTER TABLE #{table}
UPDATE utm_medium = multiIf(
referrer_source = 'Google' AND click_id_param = 'gclid', '(gclid)',
referrer_source = 'Bing' AND click_id_param = 'msclkid', '(msclkid)',
utm_medium
)
WHERE empty(utm_medium) AND NOT empty(click_id_param)
"""
end
end

View File

@ -269,6 +269,10 @@
{
"const": "average_revenue",
"$comment": "only :internal"
},
{
"const": "scroll_depth",
"$comment": "only :internal"
}
]
},

View File

@ -1,5 +1,6 @@
defmodule Plausible.AuthTest do
use Plausible.DataCase, async: true
use Plausible.Teams.Test
alias Plausible.Auth
describe "user_completed_setup?" do
@ -42,24 +43,32 @@ defmodule Plausible.AuthTest do
end
test "enterprise_configured?/1 returns whether the user has an enterprise plan" do
user_without_plan = insert(:user)
user_with_plan = insert(:user, enterprise_plan: build(:enterprise_plan))
user_without_plan = new_user()
user_with_plan = new_user() |> subscribe_to_enterprise_plan()
assert Auth.enterprise_configured?(user_with_plan)
refute Auth.enterprise_configured?(user_without_plan)
refute Auth.enterprise_configured?(nil)
user_with_plan_no_subscription =
new_user() |> subscribe_to_enterprise_plan(subscription?: false)
assert Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user_with_plan)
assert Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(
user_with_plan_no_subscription
)
refute Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user_without_plan)
refute Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(nil)
end
describe "create_api_key/3" do
test "creates a new api key" do
user = insert(:user)
user = new_user()
key = Ecto.UUID.generate()
assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(user, "my new key", key)
end
@tag :ee_only
test "defaults to 600 requests per hour limit in EE" do
user = insert(:user)
user = new_user()
{:ok, %Auth.ApiKey{hourly_request_limit: hourly_request_limit}} =
Auth.create_api_key(user, "my new EE key", Ecto.UUID.generate())
@ -78,8 +87,8 @@ defmodule Plausible.AuthTest do
end
test "errors when key already exists" do
u1 = insert(:user)
u2 = insert(:user)
u1 = new_user()
u2 = new_user()
key = Ecto.UUID.generate()
assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(u1, "my new key", key)
assert {:error, changeset} = Auth.create_api_key(u2, "my other key", key)
@ -100,16 +109,16 @@ defmodule Plausible.AuthTest do
describe "delete_api_key/2" do
test "deletes the record" do
user = insert(:user)
user = new_user()
assert {:ok, api_key} = Auth.create_api_key(user, "my new key", Ecto.UUID.generate())
assert :ok = Auth.delete_api_key(user, api_key.id)
refute Plausible.Repo.reload(api_key)
end
test "returns error when api key does not exist or does not belong to user" do
me = insert(:user)
me = new_user()
other_user = insert(:user)
other_user = new_user()
{:ok, other_api_key} = Auth.create_api_key(other_user, "my new key", Ecto.UUID.generate())
assert {:error, :not_found} = Auth.delete_api_key(me, other_api_key.id)

View File

@ -531,9 +531,9 @@ defmodule Plausible.BillingTest do
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
user_without_subscription = insert(:user)
assert Billing.active_subscription_for(active.user_id).id == active.id
assert Billing.active_subscription_for(paused.user_id) == nil
assert Billing.active_subscription_for(user_without_subscription.id) == nil
assert Billing.active_subscription_for(active.user).id == active.id
assert Billing.active_subscription_for(paused.user) == nil
assert Billing.active_subscription_for(user_without_subscription) == nil
end
test "has_active_subscription?/1 returns whether the user has an active subscription" do
@ -541,8 +541,8 @@ defmodule Plausible.BillingTest do
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
user_without_subscription = insert(:user)
assert Billing.has_active_subscription?(active.user_id)
refute Billing.has_active_subscription?(paused.user_id)
refute Billing.has_active_subscription?(user_without_subscription.id)
assert Billing.has_active_subscription?(active.user)
refute Billing.has_active_subscription?(paused.user)
refute Billing.has_active_subscription?(user_without_subscription)
end
end

View File

@ -1,28 +1,25 @@
defmodule Plausible.Billing.FeatureTest do
use Plausible.DataCase
use Plausible.Teams.Test
@v1_plan_id "558018"
for mod <- [Plausible.Billing.Feature.Funnels, Plausible.Billing.Feature.RevenueGoals] do
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan" do
user =
insert(:user,
enterprise_plan:
build(:enterprise_plan, paddle_plan_id: "123321", features: [unquote(mod)]),
subscription: build(:subscription, paddle_plan_id: "123321")
)
new_user()
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
assert :ok == unquote(mod).check_availability(user)
end
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
user = insert(:user, subscription: build(:business_subscription))
user = new_user() |> subscribe_to_business_plan()
assert :ok == unquote(mod).check_availability(user)
end
test "#{mod}.check_availability/1 returns error when site owner is on a growth plan" do
user = insert(:user, subscription: build(:growth_subscription))
user = new_user() |> subscribe_to_growth_plan()
assert {:error, :upgrade_required} == unquote(mod).check_availability(user)
end
@ -33,29 +30,26 @@ defmodule Plausible.Billing.FeatureTest do
end
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on a business plan" do
user = insert(:user, subscription: build(:business_subscription))
user = new_user() |> subscribe_to_business_plan()
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an old plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
user = new_user() |> subscribe_to_plan(@v1_plan_id)
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on trial" do
user = insert(:user)
user = new_user()
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an enterprise plan" do
user =
insert(:user,
enterprise_plan:
build(:enterprise_plan,
paddle_plan_id: "123321",
features: [Plausible.Billing.Feature.StatsAPI]
),
subscription: build(:subscription, paddle_plan_id: "123321")
new_user()
|> subscribe_to_enterprise_plan(
paddle_plan_id: "123321",
features: [Plausible.Billing.Feature.StatsAPI]
)
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
@ -63,46 +57,40 @@ defmodule Plausible.Billing.FeatureTest do
@tag :ee_only
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user is on a growth plan" do
user = insert(:user, subscription: build(:growth_subscription))
user = new_user() |> subscribe_to_growth_plan()
assert {:error, :upgrade_required} ==
Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user trial hasn't started and was created before the business tier launch" do
user = insert(:user, inserted_at: ~N[2020-01-01T00:00:00], trial_expiry_date: nil)
user = new_user(inserted_at: ~N[2020-01-01T00:00:00], trial_expiry_date: nil)
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok if user is subscribed and account was created after business tier launch" do
user = insert(:user, trial_expiry_date: nil, subscription: build(:business_subscription))
user = new_user(trial_expiry_date: nil) |> subscribe_to_business_plan()
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
@tag :ee_only
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user trial hasn't started and was created after the business tier launch" do
user = insert(:user, trial_expiry_date: nil)
user = new_user(trial_expiry_date: nil)
assert {:error, :upgrade_required} ==
Plausible.Billing.Feature.StatsAPI.check_availability(user)
end
test "Plausible.Billing.Feature.Props.check_availability/1 applies grandfathering to old plans" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
user = new_user() |> subscribe_to_plan(@v1_plan_id)
assert :ok == Plausible.Billing.Feature.Props.check_availability(user)
end
test "Plausible.Billing.Feature.Goals.check_availability/2 always returns :ok" do
u1 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
u2 = insert(:user, subscription: build(:growth_subscription))
u3 = insert(:user, subscription: build(:business_subscription))
u4 =
insert(:user,
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
subscription: build(:subscription, paddle_plan_id: "123321")
)
u1 = new_user() |> subscribe_to_plan(@v1_plan_id)
u2 = new_user() |> subscribe_to_growth_plan()
u3 = new_user() |> subscribe_to_business_plan()
u4 = new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321")
assert :ok == Plausible.Billing.Feature.Goals.check_availability(u1)
assert :ok == Plausible.Billing.Feature.Goals.check_availability(u2)
@ -115,51 +103,56 @@ defmodule Plausible.Billing.FeatureTest do
{Plausible.Billing.Feature.Props, :props_enabled}
] do
test "#{mod}.toggle/2 toggles #{property} on and off" do
site = insert(:site, [{:members, [build(:user)]}, {unquote(property), false}])
user = new_user()
site = new_site([{:owner, user}, {unquote(property), false}])
{:ok, site} = unquote(mod).toggle(site)
{:ok, site} = unquote(mod).toggle(site, user)
assert Map.get(site, unquote(property))
assert unquote(mod).enabled?(site)
refute unquote(mod).opted_out?(site)
{:ok, site} = unquote(mod).toggle(site)
{:ok, site} = unquote(mod).toggle(site, user)
refute Map.get(site, unquote(property))
refute unquote(mod).enabled?(site)
assert unquote(mod).opted_out?(site)
end
test "#{mod}.toggle/2 accepts an override option" do
site = insert(:site, [{:members, [build(:user)]}, {unquote(property), false}])
user = new_user()
site = new_site([{:owner, user}, {unquote(property), false}])
{:ok, site} = unquote(mod).toggle(site, override: false)
{:ok, site} = unquote(mod).toggle(site, user, override: false)
refute Map.get(site, unquote(property))
refute unquote(mod).enabled?(site)
end
test "#{mod}.toggle/2 errors when enabling a feature the site owner does not have access to the feature" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, [{:members, [user]}, {unquote(property), false}])
{:error, :upgrade_required} = unquote(mod).toggle(site)
user = new_user() |> subscribe_to_growth_plan()
site = new_site([{:owner, user}, {unquote(property), false}])
{:error, :upgrade_required} = unquote(mod).toggle(site, user)
refute unquote(mod).enabled?(site)
end
test "#{mod}.toggle/2 does not error when disabling a feature the site owner does not have access to" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, [{:members, [user]}, {unquote(property), true}])
{:ok, site} = unquote(mod).toggle(site)
user = new_user() |> subscribe_to_growth_plan()
site = new_site([{:owner, user}, {unquote(property), true}])
{:ok, site} = unquote(mod).toggle(site, user)
assert unquote(mod).opted_out?(site)
end
end
test "Plausible.Billing.Feature.Goals.toggle/2 toggles conversions_enabled on and off" do
site = insert(:site, [{:members, [build(:user)]}, {:conversions_enabled, false}])
user = new_user()
site = new_site(owner: user, conversions_enabled: false)
{:ok, site} = Plausible.Billing.Feature.Goals.toggle(site)
{:ok, site} = Plausible.Billing.Feature.Goals.toggle(site, user)
assert Map.get(site, :conversions_enabled)
assert Plausible.Billing.Feature.Goals.enabled?(site)
refute Plausible.Billing.Feature.Goals.opted_out?(site)
{:ok, site} = Plausible.Billing.Feature.Goals.toggle(site)
{:ok, site} = Plausible.Billing.Feature.Goals.toggle(site, user)
refute Map.get(site, :conversions_enabled)
refute Plausible.Billing.Feature.Goals.enabled?(site)
assert Plausible.Billing.Feature.Goals.opted_out?(site)
@ -167,14 +160,14 @@ defmodule Plausible.Billing.FeatureTest do
for mod <- [Plausible.Billing.Feature.Funnels, Plausible.Billing.Feature.Props] do
test "#{mod}.enabled?/1 returns false when user does not have access to the feature even when enabled" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, [{:members, [user]}, {unquote(mod).toggle_field(), true}])
user = new_user() |> subscribe_to_growth_plan()
site = new_site([{:owner, user}, {unquote(mod).toggle_field(), true}])
refute unquote(mod).enabled?(site)
end
test "#{mod}.opted_out?/1 returns false when feature toggle is enabled even when user does not have access to the feature" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, [{:members, [user]}, {unquote(mod).toggle_field(), true}])
user = new_user() |> subscribe_to_growth_plan()
site = new_site([{:owner, user}, {unquote(mod).toggle_field(), true}])
refute unquote(mod).opted_out?(site)
end
end

View File

@ -1,5 +1,6 @@
defmodule Plausible.Billing.PlansTest do
use Plausible.DataCase, async: true
use Plausible.Teams.Test
alias Plausible.Billing.Plans
@legacy_plan_id "558746"
@ -9,56 +10,49 @@ defmodule Plausible.Billing.PlansTest do
describe "getting subscription plans for user" do
test "growth_plans_for/1 returns v1 plans for a user on a legacy plan" do
insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
new_user()
|> subscribe_to_plan(@legacy_plan_id)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(1)
end
test "growth_plans_for/1 returns v1 plans for users who are already on v1 pricing" do
insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
new_user()
|> subscribe_to_plan(@v1_plan_id)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(1)
end
test "growth_plans_for/1 returns v2 plans for users who are already on v2 pricing" do
insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
new_user()
|> subscribe_to_plan(@v2_plan_id)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(2)
end
test "growth_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
insert(:user, trial_expiry_date: nil)
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 returns v4 plans for users whose trial started after the business tiers release" do
insert(:user, trial_expiry_date: ~D[2023-12-24])
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 returns v4 plans for expired legacy subscriptions" do
subscription =
build(:subscription,
paddle_plan_id: @v1_plan_id,
status: :deleted,
next_bill_date: ~D[2023-11-10]
)
insert(:user, subscription: subscription)
new_user()
|> subscribe_to_plan(@v1_plan_id, status: :deleted, next_bill_date: ~D[2023-11-10])
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 shows v4 plans for everyone else" do
insert(:user)
new_user()
|> Repo.preload(:subscription)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 does not return business plans" do
insert(:user)
new_user()
|> Repo.preload(:subscription)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> Enum.each(fn plan ->
assert plan.kind != :business
@ -66,64 +60,61 @@ defmodule Plausible.Billing.PlansTest do
end
test "growth_plans_for/1 returns the latest generation of growth plans for a user with a business subscription" do
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
new_user()
|> subscribe_to_plan(@v3_business_plan_id)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v3 business plans for a user on a legacy plan" do
insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
new_user()
|> subscribe_to_plan(@legacy_plan_id)
|> Map.fetch!(:subscription)
|> Plans.business_plans_for()
|> assert_generation(3)
end
test "business_plans_for/1 returns v3 business plans for a v2 subscriber" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user = new_user() |> subscribe_to_plan(@v2_plan_id)
business_plans = Plans.business_plans_for(user)
business_plans = Plans.business_plans_for(user.subscription)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert_generation(business_plans, 3)
end
test "business_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
insert(:user, trial_expiry_date: nil)
|> Plans.business_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v4 plans for users whose trial started after the business tiers release" do
insert(:user, trial_expiry_date: ~D[2023-12-24])
new_user(trial_expiry_date: nil)
|> Repo.preload(:subscription)
|> Map.fetch!(:subscription)
|> Plans.business_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v4 plans for expired legacy subscriptions" do
subscription =
build(:subscription,
paddle_plan_id: @v2_plan_id,
status: :deleted,
next_bill_date: ~D[2023-11-10]
)
user =
new_user()
|> subscribe_to_plan(@v2_plan_id, status: :deleted, next_bill_date: ~D[2023-11-10])
insert(:user, subscription: subscription)
user.subscription
|> Plans.business_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v4 business plans for everyone else" do
user = insert(:user)
business_plans = Plans.business_plans_for(user)
user = new_user() |> Repo.preload(:subscription)
business_plans = Plans.business_plans_for(user.subscription)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert_generation(business_plans, 4)
end
test "available_plans returns all plans for user with prices when asked for" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user = new_user() |> subscribe_to_plan(@v2_plan_id)
%{growth: growth_plans, business: business_plans} =
Plans.available_plans_for(user, with_prices: true, customer_ip: "127.0.0.1")
Plans.available_plans_for(user.subscription, with_prices: true, customer_ip: "127.0.0.1")
assert Enum.find(growth_plans, fn plan ->
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v2_plan_id
@ -135,9 +126,9 @@ defmodule Plausible.Billing.PlansTest do
end
test "available_plans returns all plans without prices by default" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user = new_user() |> subscribe_to_plan(@v2_plan_id)
assert %{growth: [_ | _], business: [_ | _]} = Plans.available_plans_for(user)
assert %{growth: [_ | _], business: [_ | _]} = Plans.available_plans_for(user.subscription)
end
test "latest_enterprise_plan_with_price/1" do
@ -190,7 +181,7 @@ defmodule Plausible.Billing.PlansTest do
describe "suggested_plan/2" do
test "returns suggested plan based on usage" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
user = new_user() |> subscribe_to_plan(@v1_plan_id)
assert %Plausible.Billing.Plan{
monthly_pageview_limit: 100_000,
@ -212,13 +203,16 @@ defmodule Plausible.Billing.PlansTest do
end
test "returns nil when user has enterprise-level usage" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
user = new_user() |> subscribe_to_plan(@v1_plan_id)
assert :enterprise == Plans.suggest(user, 100_000_000)
end
test "returns nil when user is on an enterprise plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
_enterprise_plan = insert(:enterprise_plan, user_id: user.id, billing_interval: :yearly)
user =
new_user()
|> subscribe_to_plan(@v1_plan_id)
|> subscribe_to_enterprise_plan(billing_interval: :yearly, subscription?: false)
assert :enterprise == Plans.suggest(user, 10_000)
end
end

View File

@ -510,8 +510,8 @@ defmodule Plausible.Billing.QuotaTest do
on_ee do
test "returns [Funnels] when user/site uses funnels" do
user = insert(:user)
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
user = new_user()
site = new_site(owner: user)
goals = insert_list(3, :goal, site: site, event_name: fn -> Ecto.UUID.generate() end)
steps = Enum.map(goals, &%{"goal_id" => &1.id})
@ -550,13 +550,13 @@ defmodule Plausible.Billing.QuotaTest do
on_ee do
test "returns multiple features used by the user" do
user = insert(:user)
user = new_user()
insert(:api_key, user: user)
site =
insert(:site,
new_site(
allowed_event_props: ["dummy"],
memberships: [build(:site_membership, user: user, role: :owner)]
owner: user
)
insert(:goal, currency: :USD, site: site, event_name: "Purchase")
@ -721,7 +721,7 @@ defmodule Plausible.Billing.QuotaTest do
populate_stats(site, [
build(:event, timestamp: Timex.shift(now, days: -8), name: "custom"),
build(:pageview, user_id: 199, timestamp: Timex.shift(now, days: -5, minutes: -2)),
build(:event, user_id: 199, timestamp: Timex.shift(now, days: -5), name: "pageleave")
build(:pageleave, user_id: 199, timestamp: Timex.shift(now, days: -5))
])
assert %{

View File

@ -112,16 +112,9 @@ defmodule Plausible.Billing.SiteLockerTest do
test "locks all sites if user has active subscription but grace period has ended" do
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
user = insert(:user, grace_period: grace_period)
insert(:subscription, status: Subscription.Status.active(), user: user)
site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
user = new_user(grace_period: grace_period)
subscribe_to_plan(user, "123")
site = new_site(owner: user)
assert SiteLocker.update_sites_for(user) == {:locked, :grace_period_ended_now}
@ -131,15 +124,9 @@ defmodule Plausible.Billing.SiteLockerTest do
@tag :teams
test "syncs grace period end with teams" do
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
user = insert(:user, grace_period: grace_period)
insert(:subscription, status: Subscription.Status.active(), user: user)
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
user = new_user(grace_period: grace_period)
subscribe_to_plan(user, "123")
new_site(owner: user)
assert SiteLocker.update_sites_for(user) == {:locked, :grace_period_ended_now}
@ -151,15 +138,9 @@ defmodule Plausible.Billing.SiteLockerTest do
test "sends email if grace period has ended" do
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
user = insert(:user, grace_period: grace_period)
insert(:subscription, status: Subscription.Status.active(), user: user)
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
user = new_user(grace_period: grace_period)
subscribe_to_plan(user, "123")
new_site(owner: user)
assert SiteLocker.update_sites_for(user) == {:locked, :grace_period_ended_now}
@ -170,21 +151,14 @@ defmodule Plausible.Billing.SiteLockerTest do
end
test "does not send grace period email if site is already locked" do
user =
insert(:user,
grace_period: %Plausible.Auth.GracePeriod{
end_date: Timex.shift(Timex.today(), days: -1),
is_over: false
}
)
grace_period = %Plausible.Auth.GracePeriod{
end_date: Timex.shift(Timex.today(), days: -1),
is_over: false
}
insert(:subscription, status: Subscription.Status.active(), user: user)
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
user = new_user(grace_period: grace_period)
subscribe_to_plan(user, "123")
new_site(owner: user)
assert SiteLocker.update_sites_for(user) == {:locked, :grace_period_ended_now}

View File

@ -1,49 +0,0 @@
defmodule Plausible.DebugReplayInfoTest do
use Plausible.DataCase, async: true
defmodule SampleModule do
use Plausible.DebugReplayInfo
def task(site, query, report_to) do
include_sentry_replay_info()
send(report_to, {:task_done, Sentry.Context.get_all()})
{:ok, {site, query}}
end
end
@tag :slow
test "adds replayable sentry context" do
site = insert(:site)
query = Plausible.Stats.Query.from(site, %{"period" => "day"})
{:ok, {^site, ^query}} = SampleModule.task(site, query, self())
assert_receive {:task_done, context}
assert is_integer(context.extra.debug_replay_info_size)
assert info = context.extra.debug_replay_info
{function, input} = Plausible.DebugReplayInfo.deserialize(info)
assert function == (&SampleModule.task/3)
assert input[:site] == site
assert input[:query] == query
assert input[:report_to] == self()
assert apply(function, [input[:site], input[:query], input[:report_to]])
assert_receive {:task_done, ^context}
end
test "won't add replay info, if serialized input too large" do
{:ok, _} =
SampleModule.task(
:crypto.strong_rand_bytes(10_000),
:crypto.strong_rand_bytes(10_000),
self()
)
assert_receive {:task_done, context}
assert context.extra.debug_replay_info == :too_large
assert context.extra.debug_replay_info_size > 10_000
end
end

View File

@ -3,6 +3,7 @@ defmodule Plausible.FunnelsTest do
@moduletag :ee_only
use Plausible
use Plausible.Teams.Test
on_ee do
alias Plausible.Goals
@ -10,7 +11,7 @@ defmodule Plausible.FunnelsTest do
alias Plausible.Stats
setup do
site = insert(:site)
site = new_site()
{:ok, g1} = Goals.create(site, %{"page_path" => "/go/to/blog/**"})
{:ok, g2} = Goals.create(site, %{"event_name" => "Signup"})

View File

@ -1,10 +1,11 @@
defmodule Plausible.GoalsTest do
use Plausible.DataCase
use Plausible
use Plausible.Teams.Test
alias Plausible.Goals
test "create/2 creates goals and trims input" do
site = insert(:site)
site = new_site()
{:ok, goal} = Goals.create(site, %{"page_path" => "/foo bar "})
assert goal.page_path == "/foo bar"
assert goal.display_name == "Visit /foo bar"
@ -20,33 +21,33 @@ defmodule Plausible.GoalsTest do
end
test "create/2 creates pageview goal and adds a leading slash if missing" do
site = insert(:site)
site = new_site()
{:ok, goal} = Goals.create(site, %{"page_path" => "foo bar"})
assert goal.page_path == "/foo bar"
end
test "create/2 validates goal name is at most 120 chars" do
site = insert(:site)
site = new_site()
assert {:error, changeset} = Goals.create(site, %{"event_name" => String.duplicate("a", 130)})
assert {"should be at most %{count} character(s)", _} = changeset.errors[:event_name]
end
test "create/2 fails to create the same pageview goal twice" do
site = insert(:site)
site = new_site()
{:ok, _} = Goals.create(site, %{"page_path" => "foo bar"})
assert {:error, changeset} = Goals.create(site, %{"page_path" => "foo bar"})
assert {"has already been taken", _} = changeset.errors[:page_path]
end
test "create/2 fails to create the same custom event goal twice" do
site = insert(:site)
site = new_site()
{:ok, _} = Goals.create(site, %{"event_name" => "foo bar"})
assert {:error, changeset} = Goals.create(site, %{"event_name" => "foo bar"})
assert {"has already been taken", _} = changeset.errors[:event_name]
end
test "create/2 fails to create the same currency goal twice" do
site = insert(:site)
site = new_site()
{:ok, _} = Goals.create(site, %{"event_name" => "foo bar", "currency" => "EUR"})
assert {:error, changeset} =
@ -56,7 +57,7 @@ defmodule Plausible.GoalsTest do
end
test "create/2 fails to create a goal with 'pageleave' as event_name (reserved)" do
site = insert(:site)
site = new_site()
assert {:error, changeset} = Goals.create(site, %{"event_name" => "pageleave"})
assert {"The event name 'pageleave' is reserved and cannot be used as a goal", _} =
@ -65,14 +66,14 @@ defmodule Plausible.GoalsTest do
@tag :ee_only
test "create/2 sets site.updated_at for revenue goal" do
site_1 = insert(:site, updated_at: DateTime.add(DateTime.utc_now(), -3600))
site_1 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
{:ok, _goal_1} = Goals.create(site_1, %{"event_name" => "Checkout", "currency" => "BRL"})
assert NaiveDateTime.compare(site_1.updated_at, Plausible.Repo.reload!(site_1).updated_at) ==
:lt
site_2 = insert(:site, updated_at: DateTime.add(DateTime.utc_now(), -3600))
site_2 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))
{:ok, _goal_2} = Goals.create(site_2, %{"event_name" => "Read Article", "currency" => nil})
assert NaiveDateTime.compare(site_2.updated_at, Plausible.Repo.reload!(site_2).updated_at) ==
@ -81,7 +82,7 @@ defmodule Plausible.GoalsTest do
@tag :ee_only
test "create/2 creates revenue goal" do
site = insert(:site)
site = new_site()
{:ok, goal} = Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
assert goal.event_name == "Purchase"
assert goal.page_path == nil
@ -90,8 +91,8 @@ defmodule Plausible.GoalsTest do
@tag :ee_only
test "create/2 returns error when site does not have access to revenue goals" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, members: [user])
user = new_user() |> subscribe_to_growth_plan()
site = new_site(owner: user)
{:error, :upgrade_required} =
Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
@ -99,7 +100,7 @@ defmodule Plausible.GoalsTest do
@tag :ee_only
test "create/2 fails for unknown currency code" do
site = insert(:site)
site = new_site()
assert {:error, changeset} =
Goals.create(site, %{"event_name" => "Purchase", "currency" => "Euro"})
@ -108,7 +109,7 @@ defmodule Plausible.GoalsTest do
end
test "update/2 updates a goal" do
site = insert(:site)
site = new_site()
{:ok, goal1} = Goals.create(site, %{"page_path" => "/foo bar "})
{:ok, goal2} = Goals.update(goal1, %{"page_path" => "/", "display_name" => "Homepage"})
assert goal1.id == goal2.id
@ -118,7 +119,7 @@ defmodule Plausible.GoalsTest do
@tag :ee_only
test "list_revenue_goals/1 lists event_names and currencies for each revenue goal" do
site = insert(:site)
site = new_site()
Goals.create(site, %{"event_name" => "One", "currency" => "EUR"})
Goals.create(site, %{"event_name" => "Two", "currency" => "EUR"})
@ -135,7 +136,7 @@ defmodule Plausible.GoalsTest do
end
test "create/2 clears currency for pageview goals" do
site = insert(:site)
site = new_site()
{:ok, goal} = Goals.create(site, %{"page_path" => "/purchase", "currency" => "EUR"})
assert goal.event_name == nil
assert goal.page_path == "/purchase"
@ -143,7 +144,7 @@ defmodule Plausible.GoalsTest do
end
test "for_site/1 returns trimmed input even if it was saved with trailing whitespace" do
site = insert(:site)
site = new_site()
insert(:goal, %{site: site, event_name: " Signup "})
insert(:goal, %{site: site, page_path: " /Signup "})
@ -153,7 +154,7 @@ defmodule Plausible.GoalsTest do
end
test "goals are present after domain change" do
site = insert(:site)
site = new_site()
insert(:goal, %{site: site, event_name: " Signup "})
insert(:goal, %{site: site, page_path: " /Signup "})
@ -163,7 +164,7 @@ defmodule Plausible.GoalsTest do
end
test "goals are removed when site is deleted" do
site = insert(:site)
site = new_site()
insert(:goal, %{site: site, event_name: " Signup "})
insert(:goal, %{site: site, page_path: " /Signup "})
@ -173,7 +174,7 @@ defmodule Plausible.GoalsTest do
end
test "goals can be deleted" do
site = insert(:site)
site = new_site()
goal = insert(:goal, %{site: site, event_name: " Signup "})
:ok = Goals.delete(goal.id, site)
assert [] = Goals.for_site(site)
@ -181,7 +182,7 @@ defmodule Plausible.GoalsTest do
on_ee do
test "goals can be fetched with funnel count preloaded" do
site = insert(:site)
site = new_site()
goals =
Enum.map(1..4, fn i ->
@ -218,7 +219,7 @@ defmodule Plausible.GoalsTest do
end
test "deleting goals with funnels triggers funnel reduction" do
site = insert(:site)
site = new_site()
{:ok, g1} = Goals.create(site, %{"page_path" => "/1"})
{:ok, g2} = Goals.create(site, %{"page_path" => "/2"})
{:ok, g3} = Goals.create(site, %{"page_path" => "/3"})
@ -257,7 +258,7 @@ defmodule Plausible.GoalsTest do
end
test "must be either page_path or event_name" do
site = insert(:site)
site = new_site()
assert {:error, changeset} =
Goals.create(site, %{"page_path" => "/foo", "event_name" => "/foo"})

View File

@ -9,7 +9,7 @@ defmodule Plausible.Google.APITest do
import Mox
setup :verify_on_exit!
setup [:create_user, :create_new_site]
setup [:create_user, :create_site]
describe "fetch_stats/3 errors" do
setup %{user: user, site: site} do

View File

@ -4,7 +4,7 @@ defmodule Plausible.Imported.BufferTest do
import Ecto.Query
alias Plausible.Imported.Buffer
setup [:create_user, :create_new_site, :set_buffer_size]
setup [:create_user, :create_site, :set_buffer_size]
defp set_buffer_size(_setup_args) do
imported_setting = Application.fetch_env!(:plausible, :imported)

View File

@ -13,7 +13,7 @@ defmodule Plausible.Imported.CSVImporterTest do
end
describe "new_import/3 and parse_args/1" do
setup [:create_user, :create_new_site]
setup [:create_user, :create_site]
test "parses job args properly", %{user: user, site: site} do
tables = [
@ -81,7 +81,7 @@ defmodule Plausible.Imported.CSVImporterTest do
end
describe "import_data/2" do
setup [:create_user, :create_new_site, :clean_buckets]
setup [:create_user, :create_site, :clean_buckets]
@describetag :tmp_dir

View File

@ -33,7 +33,7 @@ defmodule Plausible.Imported.GoogleAnalytics4Test do
setup :verify_on_exit!
describe "parse_args/1 and import_data/2" do
setup [:create_user, :create_new_site]
setup [:create_user, :create_site]
test "imports data returned from GA4 Data API", %{conn: conn, user: user, site: site} do
past = DateTime.add(DateTime.utc_now(), -3600, :second)

View File

@ -464,7 +464,8 @@ defmodule Plausible.Ingestion.RequestTest do
"revenue_source" => %{"amount" => "12.3", "currency" => "USD"},
"uri" => "https://dummy.site/pictures/index.html?foo=bar&baz=bam",
"user_agent" => "Mozilla",
"ip_classification" => nil
"ip_classification" => nil,
"scroll_depth" => nil
}
assert %NaiveDateTime{} = NaiveDateTime.from_iso8601!(request["timestamp"])

View File

@ -1,16 +1,17 @@
defmodule Plausible.PropsTest do
use Plausible.DataCase
use Plausible.Teams.Test
test "allow/2 returns error when user plan does not include props" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, members: [user])
user = new_user() |> subscribe_to_growth_plan()
site = new_site(owner: user)
assert {:error, :upgrade_required} = Plausible.Props.allow(site, "my-prop-1")
assert %Plausible.Site{allowed_event_props: nil} = Plausible.Repo.reload!(site)
end
test "allow/2 adds props to the array" do
site = insert(:site)
site = new_site()
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-1")
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-2")
@ -20,7 +21,7 @@ defmodule Plausible.PropsTest do
end
test "allow/2 takes a single prop or multiple" do
site = insert(:site)
site = new_site()
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-1")
assert {:ok, site} = Plausible.Props.allow(site, ["my-prop-3", "my-prop-2"])
@ -30,14 +31,14 @@ defmodule Plausible.PropsTest do
end
test "allow/2 trims trailing whitespaces" do
site = insert(:site)
site = new_site()
assert {:ok, site} = Plausible.Props.allow(site, " my-prop-1 ")
assert %Plausible.Site{allowed_event_props: ["my-prop-1"]} = Plausible.Repo.reload!(site)
end
test "allow/2 fails when prop list is too long" do
site = insert(:site)
site = new_site()
props = for i <- 1..300, do: "my-prop-#{i}"
assert {:ok, site} = Plausible.Props.allow(site, props)
@ -49,7 +50,7 @@ defmodule Plausible.PropsTest do
end
test "allow/2 fails when prop key is too long" do
site = insert(:site)
site = new_site()
long_prop = String.duplicate("a", 301)
assert {:error, changeset} = Plausible.Props.allow(site, long_prop)
@ -57,7 +58,7 @@ defmodule Plausible.PropsTest do
end
test "allow/2 fails when prop key is empty" do
site = insert(:site)
site = new_site()
assert {:error, changeset} = Plausible.Props.allow(site, "")
assert {"must be between 1 and 300 characters", []} == changeset.errors[:allowed_event_props]
@ -67,7 +68,7 @@ defmodule Plausible.PropsTest do
end
test "allow/2 does not fail when prop key is already in the list" do
site = insert(:site)
site = new_site()
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-1")
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-1")
@ -75,7 +76,7 @@ defmodule Plausible.PropsTest do
end
test "disallow/2 removes the prop from the array" do
site = insert(:site)
site = new_site()
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-1")
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-2")
@ -84,7 +85,7 @@ defmodule Plausible.PropsTest do
end
test "disallow/2 does not fail when prop is not in the list" do
site = insert(:site)
site = new_site()
assert {:ok, site} = Plausible.Props.allow(site, "my-prop-1")
assert {:ok, site} = Plausible.Props.disallow(site, "my-prop-2")
@ -92,8 +93,8 @@ defmodule Plausible.PropsTest do
end
test "allow_existing_props/2 returns error when user plan does not include props" do
user = insert(:user, subscription: build(:growth_subscription))
site = insert(:site, members: [user])
user = new_user() |> subscribe_to_growth_plan()
site = new_site(owner: user)
populate_stats(site, [
build(:event,
@ -118,7 +119,7 @@ defmodule Plausible.PropsTest do
end
test "allow_existing_props/1 saves the most frequent prop keys" do
site = insert(:site)
site = new_site()
populate_stats(site, [
build(:event,
@ -145,7 +146,7 @@ defmodule Plausible.PropsTest do
end
test "allow_existing_props/1 skips invalid keys" do
site = insert(:site)
site = new_site()
populate_stats(site, [
build(:event,
@ -172,7 +173,7 @@ defmodule Plausible.PropsTest do
end
test "allow_existing_props/1 can be run multiple times" do
site = insert(:site)
site = new_site()
populate_stats(site, [
build(:event,
@ -227,7 +228,7 @@ defmodule Plausible.PropsTest do
end
test "suggest_keys_to_allow/2 returns prop keys from events" do
site = insert(:site)
site = new_site()
populate_stats(site, [
build(:event,
@ -252,7 +253,7 @@ defmodule Plausible.PropsTest do
end
test "suggest_keys_to_allow/2 does not return internal prop keys from special event types" do
site = insert(:site)
site = new_site()
populate_stats(site, [
build(:event,
@ -282,7 +283,7 @@ defmodule Plausible.PropsTest do
end
test "configured?/1 returns whether the site has allow at least one prop" do
site = insert(:site)
site = new_site()
refute Plausible.Props.configured?(site)
{:ok, site} = Plausible.Props.allow(site, "hello-world")

View File

@ -1,5 +1,6 @@
defmodule Plausible.Site.CacheTest do
use Plausible.DataCase, async: true
use Plausible.Teams.Test
alias Plausible.{Site, Goal}
alias Plausible.Site.Cache
@ -60,7 +61,7 @@ defmodule Plausible.Site.CacheTest do
name: :"cache_supervisor_#{test}"
)
%{id: site_id} = site = insert(:site, domain: "site1.example.com")
%{id: site_id} = site = new_site(domain: "site1.example.com")
{:ok, _goal} =
Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => :BRL})
@ -95,7 +96,7 @@ defmodule Plausible.Site.CacheTest do
yesterday = DateTime.utc_now() |> DateTime.add(-1 * 60 * 60 * 24)
# the site was added yesterday so full refresh will pick it up
%{id: site_id} = site = insert(:site, domain: "site1.example.com", updated_at: yesterday)
%{id: site_id} = site = new_site(domain: "site1.example.com", updated_at: yesterday)
# the goal was added yesterday so full refresh will pick it up
Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => :BRL},

View File

@ -157,16 +157,20 @@ defmodule Plausible.SitesTest do
describe "get_for_user/2" do
@tag :ee_only
test "get site for super_admin" do
user1 = insert(:user)
user2 = insert(:user)
user1 = new_user()
user2 = new_user()
patch_env(:super_admin_user_ids, [user2.id])
%{id: site_id, domain: domain} = insert(:site, members: [user1])
assert %{id: ^site_id} = Sites.get_for_user(user1.id, domain)
assert %{id: ^site_id} = Sites.get_for_user(user1.id, domain, [:owner])
%{id: site_id, domain: domain} = new_site(owner: user1)
assert %{id: ^site_id} = Plausible.Teams.Adapter.Read.Sites.get_for_user(user1, domain)
assert is_nil(Sites.get_for_user(user2.id, domain))
assert %{id: ^site_id} = Sites.get_for_user(user2.id, domain, [:super_admin])
assert %{id: ^site_id} =
Plausible.Teams.Adapter.Read.Sites.get_for_user(user1, domain, [:owner])
assert is_nil(Plausible.Teams.Adapter.Read.Sites.get_for_user(user2, domain))
assert %{id: ^site_id} =
Plausible.Teams.Adapter.Read.Sites.get_for_user(user2, domain, [:super_admin])
end
end
@ -487,8 +491,8 @@ defmodule Plausible.SitesTest do
describe "set_option/4" do
test "allows setting option multiple times" do
user = insert(:user)
site = insert(:site, members: [user])
user = new_user()
site = new_site(owner: user)
assert prefs =
%{pinned_at: %NaiveDateTime{}} =
@ -538,8 +542,8 @@ defmodule Plausible.SitesTest do
describe "toggle_pin/2" do
test "allows pinning and unpinning site" do
user = insert(:user)
site = insert(:site, members: [user])
user = new_user()
site = new_site(owner: user)
site = %{site | pinned_at: nil}
assert {:ok, prefs} = Sites.toggle_pin(user, site)
@ -567,10 +571,10 @@ defmodule Plausible.SitesTest do
end
test "returns error when pins limit hit" do
user = insert(:user)
user = new_user()
for _ <- 1..9 do
site = insert(:site, members: [user])
site = new_site(owner: user)
assert {:ok, _} = Sites.toggle_pin(user, site)
end

View File

@ -3,7 +3,7 @@ defmodule Plausible.Stats.ComparisonsTest do
alias Plausible.Stats.{DateTimeRange, Query, Comparisons}
import Plausible.TestUtils
setup [:create_user, :create_new_site]
setup [:create_user, :create_site]
def build_query(site, params, now) do
query = Query.from(site, params)

View File

@ -56,10 +56,14 @@ defmodule Plausible.Stats.GoalSuggestionsTest do
]
end
test "ignores the 'pageview' event name", %{site: site} do
test "ignores 'pageview' and 'pageleave' event names", %{site: site} do
populate_stats(site, [
build(:event, name: "Signup"),
build(:pageview)
build(:pageview,
user_id: 1,
timestamp: NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :minute)
),
build(:pageleave, user_id: 1, timestamp: NaiveDateTime.utc_now())
])
assert GoalSuggestions.suggest_event_names(site, "") == ["Signup"]

View File

@ -1,11 +1,12 @@
defmodule Plausible.Stats.Filters.QueryParserTest do
use Plausible.DataCase
use Plausible.Teams.Test
alias Plausible.Stats.DateTimeRange
alias Plausible.Stats.Filters
import Plausible.Stats.Filters.QueryParser
setup [:create_user, :create_new_site]
setup [:create_user, :create_site]
@now DateTime.new!(~D[2021-05-05], ~T[12:30:00], "Etc/UTC")
@date_range_realtime %DateTimeRange{
@ -1289,10 +1290,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
describe "custom props access" do
test "filters - no access", %{site: site, user: user} do
ep =
insert(:enterprise_plan, features: [Plausible.Billing.Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
%{
"site_id" => site.domain,
@ -1307,10 +1305,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
end
test "dimensions - no access", %{site: site, user: user} do
ep =
insert(:enterprise_plan, features: [Plausible.Billing.Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
%{
"site_id" => site.domain,
@ -1421,6 +1416,81 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
end
end
describe "scroll_depth metric" do
test "fails validation on its own", %{site: site} do
%{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all"
}
|> check_error(
site,
"Metric `scroll_depth` can only be queried with event:page filters or dimensions.",
:internal
)
end
test "fails with only a non-top-level event:page filter", %{site: site} do
%{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"filters" => [["not", ["is", "event:page", ["/"]]]]
}
|> check_error(
site,
"Metric `scroll_depth` can only be queried with event:page filters or dimensions.",
:internal
)
end
test "succeeds with top-level event:page filter", %{site: site} do
%{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"filters" => [["is", "event:page", ["/"]]]
}
|> check_success(
site,
%{
metrics: [:scroll_depth],
utc_time_range: @date_range_day,
filters: [[:is, "event:page", ["/"]]],
dimensions: [],
order_by: nil,
timezone: site.timezone,
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
pagination: %{limit: 10_000, offset: 0}
},
:internal
)
end
test "succeeds with event:page dimension", %{site: site} do
%{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"dimensions" => ["event:page"]
}
|> check_success(
site,
%{
metrics: [:scroll_depth],
utc_time_range: @date_range_day,
filters: [],
dimensions: ["event:page"],
order_by: nil,
timezone: site.timezone,
include: %{imports: false, time_labels: false, total_rows: false, comparisons: nil},
pagination: %{limit: 10_000, offset: 0}
},
:internal
)
end
end
describe "views_per_visit metric" do
test "succeeds with normal filters", %{site: site} do
insert(:goal, %{site: site, event_name: "Signup"})
@ -1523,10 +1593,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
test "no access", %{site: site, user: user, subscription: subscription} do
Repo.delete!(subscription)
plan =
insert(:enterprise_plan, features: [Plausible.Billing.Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: plan.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.StatsAPI])
%{
"site_id" => site.domain,

View File

@ -266,6 +266,13 @@ defmodule Plausible.Stats.QueryTest do
~U[2024-05-07 07:00:00Z],
trim_trailing: true
) == Date.range(~D[2024-05-05], ~D[2024-05-06])
assert date_range(
{~U[2024-05-05 12:00:00Z], ~U[2024-05-08 11:59:59Z]},
"Etc/GMT+12",
~U[2024-05-03 07:00:00Z],
trim_trailing: true
) == Date.range(~D[2024-05-05], ~D[2024-05-05])
end
end

View File

@ -1,10 +1,11 @@
defmodule PlausibleWeb.Components.Billing.NoticeTest do
use Plausible.DataCase
use Plausible.Teams.Test
import Plausible.LiveViewTest, only: [render_component: 2]
alias PlausibleWeb.Components.Billing.Notice
test "premium_feature/1 does not render a notice when user is on trial" do
me = insert(:user)
me = new_user()
assert render_component(&Notice.premium_feature/1,
billable_user: me,
@ -14,7 +15,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
end
test "premium_feature/1 renders an upgrade link when user is the site owner and does not have access to the feature" do
me = insert(:user, subscription: build(:growth_subscription))
me = new_user() |> subscribe_to_growth_plan()
rendered =
render_component(&Notice.premium_feature/1,
@ -29,8 +30,8 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
end
test "premium_feature/1 does not render an upgrade link when user is not the site owner" do
me = insert(:user)
owner = insert(:user, subscription: build(:growth_subscription))
me = new_user() |> subscribe_to_growth_plan()
owner = new_user() |> subscribe_to_growth_plan()
rendered =
render_component(&Notice.premium_feature/1,
@ -44,7 +45,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
end
test "premium_feature/1 does not render a notice when the user has access to the feature" do
me = insert(:user, subscription: build(:business_subscription))
me = new_user() |> subscribe_to_business_plan()
rendered =
render_component(&Notice.premium_feature/1,
@ -57,7 +58,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
end
test "limit_exceeded/1 when billable user is on growth displays upgrade link" do
me = insert(:user, subscription: build(:growth_subscription))
me = new_user() |> subscribe_to_growth_plan()
rendered =
render_component(&Notice.limit_exceeded/1,
@ -73,7 +74,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
end
test "limit_exceeded/1 when billable user is on growth but is not current user does not display upgrade link" do
me = insert(:user, subscription: build(:growth_subscription))
me = new_user() |> subscribe_to_growth_plan()
rendered =
render_component(&Notice.limit_exceeded/1,
@ -89,7 +90,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
@tag :ee_only
test "limit_exceeded/1 when billable user is on trial displays upgrade link" do
me = insert(:user)
me = new_user()
rendered =
render_component(&Notice.limit_exceeded/1,
@ -106,11 +107,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
@tag :ee_only
test "limit_exceeded/1 when billable user is on an enterprise plan displays support email" do
me =
insert(:user,
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
subscription: build(:subscription, paddle_plan_id: "123321")
)
me = new_user() |> subscribe_to_enterprise_plan()
rendered =
render_component(&Notice.limit_exceeded/1,
@ -128,7 +125,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
@tag :ee_only
test "limit_exceeded/1 when billable user is on a business plan displays support email" do
me = insert(:user, subscription: build(:business_subscription))
me = new_user() |> subscribe_to_business_plan()
rendered =
render_component(&Notice.limit_exceeded/1,

View File

@ -1253,6 +1253,64 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
end
end
describe "scroll depth tests" do
setup do
site = insert(:site)
{:ok, site: site}
end
test "ingests scroll_depth as 0 when sd not in params", %{conn: conn, site: site} do
post(conn, "/api/event", %{n: "pageview", u: "https://test.com", d: site.domain})
post(conn, "/api/event", %{n: "pageleave", u: "https://test.com", d: site.domain})
post(conn, "/api/event", %{n: "custom", u: "https://test.com", d: site.domain})
assert [%{scroll_depth: 0}, %{scroll_depth: 0}, %{scroll_depth: 0}] = get_events(site)
end
test "sd field is ignored if name is not pageleave", %{conn: conn, site: site} do
post(conn, "/api/event", %{n: "pageview", u: "https://test.com", d: site.domain, sd: 10})
post(conn, "/api/event", %{n: "custom_e", u: "https://test.com", d: site.domain, sd: 10})
assert [%{scroll_depth: 0}, %{scroll_depth: 0}] = get_events(site)
end
test "ingests valid scroll_depth for a pageleave", %{conn: conn, site: site} do
post(conn, "/api/event", %{n: "pageview", u: "https://test.com", d: site.domain})
post(conn, "/api/event", %{n: "pageleave", u: "https://test.com", d: site.domain, sd: 25})
pageleave = get_events(site) |> Enum.find(&(&1.name == "pageleave"))
assert pageleave.scroll_depth == 25
end
test "ingests scroll_depth as 100 when sd > 100", %{conn: conn, site: site} do
post(conn, "/api/event", %{n: "pageview", u: "https://test.com", d: site.domain})
post(conn, "/api/event", %{n: "pageleave", u: "https://test.com", d: site.domain, sd: 101})
pageleave = get_events(site) |> Enum.find(&(&1.name == "pageleave"))
assert pageleave.scroll_depth == 100
end
test "ingests scroll_depth as 0 when sd is a string", %{conn: conn, site: site} do
post(conn, "/api/event", %{n: "pageview", u: "https://test.com", d: site.domain})
post(conn, "/api/event", %{n: "pageleave", u: "https://test.com", d: site.domain, sd: "1"})
pageleave = get_events(site) |> Enum.find(&(&1.name == "pageleave"))
assert pageleave.scroll_depth == 0
end
test "ingests scroll_depth as 0 when sd is a negative integer", %{conn: conn, site: site} do
post(conn, "/api/event", %{n: "pageview", u: "https://test.com", d: site.domain})
post(conn, "/api/event", %{n: "pageleave", u: "https://test.com", d: site.domain, sd: -1})
pageleave = get_events(site) |> Enum.find(&(&1.name == "pageleave"))
assert pageleave.scroll_depth == 0
end
end
describe "acquisition channel tests" do
setup do
site = insert(:site)
@ -1374,6 +1432,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
assert response(conn, 202) == "ok"
assert session.acquisition_channel == "Paid Search"
assert session.utm_medium == "(gclid)"
assert session.click_id_param == "gclid"
end
@ -1397,6 +1456,31 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
assert response(conn, 202) == "ok"
assert session.acquisition_channel == "Organic Search"
assert session.utm_medium == ""
assert session.click_id_param == "gclid"
end
test "does not override utm_medium with (gclid) if link is already tagged", %{
conn: conn,
site: site
} do
params = %{
name: "pageview",
url: "http://example.com?gclid=123identifier&utm_medium=paidads",
referrer: "https://google.com",
domain: site.domain
}
conn =
conn
|> put_req_header("user-agent", @user_agent)
|> post("/api/event", params)
session = get_created_session(site)
assert response(conn, 202) == "ok"
assert session.acquisition_channel == "Paid Search"
assert session.utm_medium == "paidads"
assert session.click_id_param == "gclid"
end
@ -1417,6 +1501,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
assert response(conn, 202) == "ok"
assert session.acquisition_channel == "Paid Search"
assert session.utm_medium == "(msclkid)"
assert session.click_id_param == "msclkid"
end
@ -1426,8 +1511,8 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
} do
params = %{
name: "pageview",
url: "http://example.com?msclkid=123identifier",
referrer: "https://duckduckgo.com",
url: "http://example.com?msclkid=123identifier&utm_medium=cpc",
referrer: "https://bing.com",
domain: site.domain
}
@ -1439,10 +1524,35 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
session = get_created_session(site)
assert response(conn, 202) == "ok"
assert session.acquisition_channel == "Organic Search"
assert session.acquisition_channel == "Paid Search"
assert session.utm_medium == "cpc"
assert session.click_id_param == "msclkid"
end
test "does not override utm_medium with (msclkid) if link is already tagged", %{
conn: conn,
site: site
} do
params = %{
name: "pageview",
url: "http://example.com?gclid=123identifier&utm_medium=paidads",
referrer: "https://google.com",
domain: site.domain
}
conn =
conn
|> put_req_header("user-agent", @user_agent)
|> post("/api/event", params)
session = get_created_session(site)
assert response(conn, 202) == "ok"
assert session.acquisition_channel == "Paid Search"
assert session.utm_medium == "paidads"
assert session.click_id_param == "gclid"
end
test "parses paid search channel based on utm_source and medium", %{conn: conn, site: site} do
params = %{
name: "pageview",

View File

@ -108,7 +108,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end
describe "DELETE /api/v1/sites/:site_id" do
setup :create_new_site
setup :create_site
test "delete a site by its domain", %{conn: conn, site: site} do
conn = delete(conn, "/api/v1/sites/" <> site.domain)
@ -232,15 +232,11 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
test "returns 404 when api key owner does not have permissions to create a shared link", %{
conn: conn,
site: site,
user: user
} do
Repo.update_all(
from(sm in Plausible.Site.Membership,
where: sm.site_id == ^site.id and sm.user_id == ^user.id
),
set: [role: :viewer]
)
site = new_site()
add_guest(site, user: user, role: :viewer)
conn =
put(conn, "/api/v1/sites/shared-links", %{
@ -383,15 +379,11 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
test "returns 404 when api key owner does not have permissions to create a goal", %{
conn: conn,
site: site,
user: user
} do
Repo.update_all(
from(sm in Plausible.Site.Membership,
where: sm.site_id == ^site.id and sm.user_id == ^user.id
),
set: [role: :viewer]
)
site = new_site()
add_guest(site, user: user, role: :viewer)
conn =
put(conn, "/api/v1/sites/goals", %{
@ -439,7 +431,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end
describe "DELETE /api/v1/sites/goals/:goal_id" do
setup :create_new_site
setup :create_site
test "delete a goal by its id", %{conn: conn, site: site} do
conn =
@ -624,7 +616,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end
describe "GET /api/v1/sites/:site_id" do
setup :create_new_site
setup :create_site
test "get a site by its domain", %{conn: conn, site: site} do
site =
@ -687,7 +679,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end
describe "GET /api/v1/goals" do
setup :create_new_site
setup :create_site
test "returns empty when there are no goals for site", %{conn: conn, site: site} do
conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain)
@ -832,7 +824,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end
describe "PUT /api/v1/sites/:site_id" do
setup :create_new_site
setup :create_site
test "can change domain name", %{conn: conn, site: site} do
old_domain = site.domain

View File

@ -1,9 +1,10 @@
defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
use PlausibleWeb.ConnCase
use Plausible.Teams.Test
import Plausible.TestUtils
alias Plausible.Billing.Feature
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
setup [:create_user, :create_site, :create_api_key, :use_api_key]
@user_id Enum.random(1000..9999)
describe "feature access" do
@ -12,8 +13,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
user: user,
site: site
} do
ep = insert(:enterprise_plan, features: [Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Feature.StatsAPI])
conn =
get(conn, "/api/v1/stats/aggregate", %{
@ -30,8 +30,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
user: user,
site: site
} do
ep = insert(:enterprise_plan, features: [Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Feature.StatsAPI])
conn =
get(conn, "/api/v1/stats/aggregate", %{
@ -127,6 +126,20 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
}
end
test "scroll depth metric is not recognized in the legacy API v1", %{conn: conn, site: site} do
conn =
get(conn, "/api/v1/stats/aggregate", %{
"site_id" => site.domain,
"period" => "30d",
"metrics" => "scroll_depth"
})
assert json_response(conn, 400) == %{
"error" =>
"The metric `scroll_depth` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#metrics"
}
end
for property <- ["event:name", "event:goal", "event:props:custom_prop"] do
test "validates that session metrics cannot be used with #{property} filter", %{
conn: conn,
@ -1629,12 +1642,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
populate_stats(site, [
build(:pageview, user_id: 1234, timestamp: ~N[2021-01-01 12:00:00], pathname: "/1"),
build(:pageview, user_id: 1234, timestamp: ~N[2021-01-01 12:00:05], pathname: "/2"),
build(:event,
name: "pageleave",
user_id: 1234,
timestamp: ~N[2021-01-01 12:01:00],
pathname: "/1"
)
build(:pageleave, user_id: 1234, timestamp: ~N[2021-01-01 12:01:00], pathname: "/1")
])
conn =

View File

@ -1,5 +1,6 @@
defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
use PlausibleWeb.ConnCase
use Plausible.Teams.Test
setup [:create_user, :create_api_key]
@ -156,8 +157,8 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
user: user,
api_key: api_key
} do
insert(:growth_subscription, user: user)
site = insert(:site, members: [user])
subscribe_to_growth_plan(user)
site = new_site(owner: user)
conn
|> with_api_key(api_key)

View File

@ -1,10 +1,12 @@
defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
use PlausibleWeb.ConnCase
use Plausible.Teams.Test
alias Plausible.Billing.Feature
@user_id Enum.random(1000..9999)
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
setup [:create_user, :create_site, :create_api_key, :use_api_key]
describe "feature access" do
test "cannot break down by a custom prop without access to the props feature", %{
@ -12,8 +14,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
user: user,
site: site
} do
ep = insert(:enterprise_plan, features: [Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Feature.StatsAPI])
conn =
get(conn, "/api/v1/stats/breakdown", %{
@ -30,8 +31,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
user: user,
site: site
} do
ep = insert(:enterprise_plan, features: [Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Feature.StatsAPI])
conn =
get(conn, "/api/v1/stats/breakdown", %{
@ -47,10 +47,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
user: user,
site: site
} do
ep =
insert(:enterprise_plan, features: [Feature.StatsAPI], user_id: user.id)
insert(:subscription, user: user, paddle_plan_id: ep.paddle_plan_id)
subscribe_to_enterprise_plan(user, features: [Feature.StatsAPI])
conn =
get(conn, "/api/v1/stats/breakdown", %{
@ -2607,12 +2604,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do
populate_stats(site, [
build(:pageview, user_id: 1234, timestamp: ~N[2021-01-01 12:00:00], pathname: "/1"),
build(:pageview, user_id: 1234, timestamp: ~N[2021-01-01 12:00:05], pathname: "/2"),
build(:event,
name: "pageleave",
user_id: 1234,
timestamp: ~N[2021-01-01 12:01:00],
pathname: "/1"
)
build(:pageleave, user_id: 1234, timestamp: ~N[2021-01-01 12:01:00], pathname: "/1")
])
conn =

View File

@ -1,7 +1,7 @@
defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do
use PlausibleWeb.ConnCase
setup [:create_user, :create_new_site, :create_api_key, :use_api_key, :create_site_import]
setup [:create_user, :create_site, :create_api_key, :use_api_key, :create_site_import]
test "aggregates a single metric", %{conn: conn, site: site} do
populate_stats(site, [

View File

@ -3,7 +3,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryGoalDimensionTest do
@user_id Enum.random(1000..9999)
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
setup [:create_user, :create_site, :create_api_key, :use_api_key]
describe "breakdown by event:goal" do
test "returns custom event goals and pageview goals", %{conn: conn, site: site} do

View File

@ -3,7 +3,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryImportedTest do
@user_id Enum.random(1000..9999)
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
setup [:create_user, :create_site, :create_api_key, :use_api_key]
describe "aggregation with imported data" do
setup :create_site_import

View File

@ -1,7 +1,7 @@
defmodule PlausibleWeb.Api.ExternalStatsController.QuerySpecialMetricsTest do
use PlausibleWeb.ConnCase
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
setup [:create_user, :create_site, :create_api_key, :use_api_key]
test "returns conversion_rate in a goal filtered custom prop breakdown", %{
conn: conn,

View File

@ -3,7 +3,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do
@user_id Enum.random(1000..9999)
setup [:create_user, :create_new_site, :create_api_key, :use_api_key]
setup [:create_user, :create_site, :create_api_key, :use_api_key]
test "aggregates a single metric", %{conn: conn, site: site} do
populate_stats(site, [
@ -105,7 +105,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do
%{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 234, timestamp: ~N[2021-01-01 00:00:00]),
build(:event, user_id: 234, name: "pageleave", timestamp: ~N[2021-01-01 00:00:01])
build(:pageleave, user_id: 234, timestamp: ~N[2021-01-01 00:00:01])
])
conn =
@ -126,7 +126,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do
} do
populate_stats(site, [
build(:pageview, user_id: 123, timestamp: ~N[2021-01-01 00:00:00]),
build(:event, user_id: 123, name: "pageleave", timestamp: ~N[2021-01-01 00:00:03])
build(:pageleave, user_id: 123, timestamp: ~N[2021-01-01 00:00:03])
])
conn =
@ -3426,4 +3426,298 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do
assert json_response(conn4, 200)["results"] == []
end
end
describe "scroll_depth" do
setup [:create_user, :create_site, :create_api_key, :use_api_key]
test "scroll depth is (not yet) available in public API", %{conn: conn, site: site} do
conn =
post(conn, "/api/v2/query", %{
"site_id" => site.domain,
"filters" => [["is", "event:page", ["/"]]],
"date_range" => "all",
"metrics" => ["scroll_depth"]
})
assert json_response(conn, 400)["error"] =~ "Invalid metric \"scroll_depth\""
end
test "can query scroll_depth metric with a page filter", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 123, timestamp: ~N[2021-01-01 00:00:00]),
build(:pageleave, user_id: 123, timestamp: ~N[2021-01-01 00:00:10], scroll_depth: 40),
build(:pageview, user_id: 123, timestamp: ~N[2021-01-01 00:00:10]),
build(:pageleave, user_id: 123, timestamp: ~N[2021-01-01 00:00:20], scroll_depth: 60),
build(:pageview, user_id: 456, timestamp: ~N[2021-01-01 00:00:00]),
build(:pageleave, user_id: 456, timestamp: ~N[2021-01-01 00:00:10], scroll_depth: 80)
])
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"filters" => [["is", "event:page", ["/"]]],
"date_range" => "all",
"metrics" => ["scroll_depth"]
})
assert json_response(conn, 200)["results"] == [
%{"metrics" => [70], "dimensions" => []}
]
end
test "scroll depth is 0 when no pageleave data in range", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, timestamp: ~N[2021-01-01 00:00:00])
])
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"filters" => [["is", "event:page", ["/"]]],
"date_range" => "all",
"metrics" => ["visitors", "scroll_depth"]
})
assert json_response(conn, 200)["results"] == [
%{"metrics" => [1, 0], "dimensions" => []}
]
end
test "scroll depth is 0 when no data at all in range", %{conn: conn, site: site} do
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"filters" => [["is", "event:page", ["/"]]],
"date_range" => "all",
"metrics" => ["scroll_depth"]
})
assert json_response(conn, 200)["results"] == [
%{"metrics" => [0], "dimensions" => []}
]
end
test "scroll_depth metric in a time:day breakdown", %{conn: conn, site: site} do
t0 = ~N[2020-01-01 00:00:00]
[t1, t2, t3] = for i <- 1..3, do: NaiveDateTime.add(t0, i, :minute)
populate_stats(site, [
build(:pageview, user_id: 12, timestamp: t0),
build(:pageleave, user_id: 12, timestamp: t1, scroll_depth: 20),
build(:pageview, user_id: 34, timestamp: t0),
build(:pageleave, user_id: 34, timestamp: t1, scroll_depth: 17),
build(:pageview, user_id: 34, timestamp: t2),
build(:pageleave, user_id: 34, timestamp: t3, scroll_depth: 60),
build(:pageview, user_id: 56, timestamp: NaiveDateTime.add(t0, 1, :day)),
build(:pageleave,
user_id: 56,
timestamp: NaiveDateTime.add(t1, 1, :day),
scroll_depth: 20
)
])
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"dimensions" => ["time:day"],
"filters" => [["is", "event:page", ["/"]]]
})
assert json_response(conn, 200)["results"] == [
%{"dimensions" => ["2020-01-01"], "metrics" => [40]},
%{"dimensions" => ["2020-01-02"], "metrics" => [20]}
]
end
test "breakdown by event:page with scroll_depth metric", %{conn: conn, site: site} do
t0 = ~N[2020-01-01 00:00:00]
[t1, t2, t3] = for i <- 1..3, do: NaiveDateTime.add(t0, i, :minute)
populate_stats(site, [
build(:pageview, user_id: 12, pathname: "/blog", timestamp: t0),
build(:pageleave, user_id: 12, pathname: "/blog", timestamp: t1, scroll_depth: 20),
build(:pageview, user_id: 12, pathname: "/another", timestamp: t1),
build(:pageleave, user_id: 12, pathname: "/another", timestamp: t2, scroll_depth: 24),
build(:pageview, user_id: 34, pathname: "/blog", timestamp: t0),
build(:pageleave, user_id: 34, pathname: "/blog", timestamp: t1, scroll_depth: 17),
build(:pageview, user_id: 34, pathname: "/another", timestamp: t1),
build(:pageleave, user_id: 34, pathname: "/another", timestamp: t2, scroll_depth: 26),
build(:pageview, user_id: 34, pathname: "/blog", timestamp: t2),
build(:pageleave, user_id: 34, pathname: "/blog", timestamp: t3, scroll_depth: 60),
build(:pageview, user_id: 56, pathname: "/blog", timestamp: t0),
build(:pageleave, user_id: 56, pathname: "/blog", timestamp: t1, scroll_depth: 100)
])
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"dimensions" => ["event:page"]
})
assert json_response(conn, 200)["results"] == [
%{"dimensions" => ["/blog"], "metrics" => [60]},
%{"dimensions" => ["/another"], "metrics" => [25]}
]
end
test "breakdown by event:page + visit:source with scroll_depth metric", %{
conn: conn,
site: site
} do
populate_stats(site, [
build(:pageview,
referrer_source: "Google",
user_id: 12,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00]
),
build(:pageleave,
referrer_source: "Google",
user_id: 12,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(1, :minute),
scroll_depth: 20
),
build(:pageview,
referrer_source: "Google",
user_id: 34,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00]
),
build(:pageleave,
referrer_source: "Google",
user_id: 34,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(1, :minute),
scroll_depth: 17
),
build(:pageview,
referrer_source: "Google",
user_id: 34,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(2, :minute)
),
build(:pageleave,
referrer_source: "Google",
user_id: 34,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(3, :minute),
scroll_depth: 60
),
build(:pageview,
referrer_source: "Twitter",
user_id: 56,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00]
),
build(:pageleave,
referrer_source: "Twitter",
user_id: 56,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(1, :minute),
scroll_depth: 20
),
build(:pageview,
referrer_source: "Twitter",
user_id: 56,
pathname: "/another",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(1, :minute)
),
build(:pageleave,
referrer_source: "Twitter",
user_id: 56,
pathname: "/another",
timestamp: ~N[2020-01-01 00:00:00] |> NaiveDateTime.add(2, :minute),
scroll_depth: 24
)
])
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"dimensions" => ["event:page", "visit:source"]
})
assert json_response(conn, 200)["results"] == [
%{"dimensions" => ["/blog", "Google"], "metrics" => [40]},
%{"dimensions" => ["/another", "Twitter"], "metrics" => [24]},
%{"dimensions" => ["/blog", "Twitter"], "metrics" => [20]}
]
end
test "breakdown by event:page + time:day with scroll_depth metric", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: 12, pathname: "/blog", timestamp: ~N[2020-01-01 00:00:00]),
build(:pageleave,
user_id: 12,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:01:00],
scroll_depth: 20
),
build(:pageview, user_id: 12, pathname: "/another", timestamp: ~N[2020-01-01 00:01:00]),
build(:pageleave,
user_id: 12,
pathname: "/another",
timestamp: ~N[2020-01-01 00:02:00],
scroll_depth: 24
),
build(:pageview, user_id: 34, pathname: "/blog", timestamp: ~N[2020-01-01 00:00:00]),
build(:pageleave,
user_id: 34,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:01:00],
scroll_depth: 17
),
build(:pageview, user_id: 34, pathname: "/another", timestamp: ~N[2020-01-01 00:01:00]),
build(:pageleave,
user_id: 34,
pathname: "/another",
timestamp: ~N[2020-01-01 00:02:00],
scroll_depth: 26
),
build(:pageview, user_id: 34, pathname: "/blog", timestamp: ~N[2020-01-01 00:02:00]),
build(:pageleave,
user_id: 34,
pathname: "/blog",
timestamp: ~N[2020-01-01 00:03:00],
scroll_depth: 60
),
build(:pageview, user_id: 56, pathname: "/blog", timestamp: ~N[2020-01-02 00:00:00]),
build(:pageleave,
user_id: 56,
pathname: "/blog",
timestamp: ~N[2020-01-02 00:01:00],
scroll_depth: 20
),
build(:pageview, user_id: 56, pathname: "/another", timestamp: ~N[2020-01-02 00:01:00]),
build(:pageleave,
user_id: 56,
pathname: "/another",
timestamp: ~N[2020-01-02 00:02:00],
scroll_depth: 24
)
])
conn =
post(conn, "/api/v2/query-internal-test", %{
"site_id" => site.domain,
"metrics" => ["scroll_depth"],
"date_range" => "all",
"dimensions" => ["event:page", "time:day"]
})
assert json_response(conn, 200)["results"] == [
%{"dimensions" => ["/blog", "2020-01-01"], "metrics" => [40]},
%{"dimensions" => ["/another", "2020-01-01"], "metrics" => [25]},
%{"dimensions" => ["/another", "2020-01-02"], "metrics" => [24]},
%{"dimensions" => ["/blog", "2020-01-02"], "metrics" => [20]}
]
end
end
end

Some files were not shown because too many files have changed in this diff Show More