mirror of
https://github.com/plausible/analytics.git
synced 2024-11-22 02:27:57 +03:00
Merge branch 'master' into log-mailer-errors-in-ce
This commit is contained in:
commit
71e33bba0d
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
3
assets/js/types/query-api.d.ts
vendored
3
assets/js/types/query-api.d.ts
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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{}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
@ -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" ->
|
||||
|
@ -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) ->
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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") ->
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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: %{}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}%"))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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} ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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} ->
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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">
|
||||
|
@ -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} ->
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 || []
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
@ -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
|
@ -269,6 +269,10 @@
|
||||
{
|
||||
"const": "average_revenue",
|
||||
"$comment": "only :internal"
|
||||
},
|
||||
{
|
||||
"const": "scroll_depth",
|
||||
"$comment": "only :internal"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 %{
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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
|
@ -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"})
|
||||
|
@ -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"})
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"])
|
||||
|
@ -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")
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
|
@ -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 =
|
||||
|
@ -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, [
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user