From 07a3436fa298dfd7e659aa40bd62ad49cd1ec1f5 Mon Sep 17 00:00:00 2001 From: Karl-Aksel Puulmann Date: Mon, 18 Nov 2024 10:00:18 +0200 Subject: [PATCH 01/13] Follow-up to Solve noisy warnings about a negative range was inferred for Date.range/2 (#4816) * Clamp dates on both directions We could still get warnings about negative date ranges when the date being queried is in the future. This could happen if the users local time is in the future for some reason or they manually edit the url. * Update cond --- lib/plausible/stats/query.ex | 10 +++++++--- test/plausible/stats/query_test.exs | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/plausible/stats/query.ex b/lib/plausible/stats/query.ex index f5bd669a4..c4e18a01e 100644 --- a/lib/plausible/stats/query.ex +++ b/lib/plausible/stats/query.ex @@ -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 diff --git a/test/plausible/stats/query_test.exs b/test/plausible/stats/query_test.exs index fae5524c3..ce5af98cd 100644 --- a/test/plausible/stats/query_test.exs +++ b/test/plausible/stats/query_test.exs @@ -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 From c2a95a142d428fbc2a9c3cc4f347935a74ebdece Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Mon, 18 Nov 2024 09:52:03 +0100 Subject: [PATCH 02/13] Make feature toggle work with teams schema and adjust affected tests (#4825) --- lib/plausible/billing/feature.ex | 71 ++++---------- lib/plausible/teams/adapter.ex | 8 -- lib/plausible/teams/adapter/read/billing.ex | 63 +++++++++++- lib/plausible/teams/adapter/read/ownership.ex | 20 +++- .../controllers/api/internal_controller.ex | 4 +- .../controllers/site_controller.ex | 2 +- test/plausible/auth/auth_test.exs | 15 +-- test/plausible/billing/feature_test.exs | 95 +++++++++---------- test/plausible/billing/quota_test.exs | 10 +- test/plausible/funnels_test.exs | 3 +- test/plausible/goals_test.exs | 47 ++++----- test/plausible/google/api_test.exs | 2 +- test/plausible/imported/buffer_test.exs | 2 +- test/plausible/imported/csv_importer_test.exs | 4 +- .../imported/google_analytics4_test.exs | 2 +- test/plausible/props_test.exs | 39 ++++---- test/plausible/site/cache_test.exs | 5 +- test/plausible/stats/comparisons_test.exs | 2 +- test/plausible/stats/query_parser_test.exs | 18 +--- .../components/billing/notice_test.exs | 25 +++-- .../api/external_sites_controller_test.exs | 10 +- .../aggregate_test.exs | 9 +- .../external_stats_controller/auth_test.exs | 5 +- .../breakdown_test.exs | 15 ++- .../query_comparisons_test.exs | 2 +- .../query_goal_dimension_test.exs | 2 +- .../query_imported_test.exs | 2 +- .../query_special_metrics_test.exs | 2 +- .../external_stats_controller/query_test.exs | 2 +- .../query_validations_test.exs | 18 ++-- .../timeseries_test.exs | 9 +- .../internal_controller/docs_query_test.exs | 4 +- .../api/stats_controller/browsers_test.exs | 4 +- .../api/stats_controller/cities_test.exs | 2 +- .../api/stats_controller/conversions_test.exs | 15 +-- .../api/stats_controller/countries_test.exs | 2 +- .../custom_prop_breakdown_test.exs | 13 +-- .../api/stats_controller/funnels_test.exs | 7 +- .../api/stats_controller/imported_test.exs | 2 +- .../api/stats_controller/main_graph_test.exs | 24 ++--- .../operating_systems_test.exs | 4 +- .../api/stats_controller/pages_test.exs | 6 +- .../api/stats_controller/regions_test.exs | 2 +- .../stats_controller/screen_sizes_test.exs | 2 +- .../api/stats_controller/sources_test.exs | 20 ++-- .../api/stats_controller/suggestions_test.exs | 10 +- .../api/stats_controller/top_stats_test.exs | 17 ++-- .../controllers/auth_controller_test.exs | 2 +- .../controllers/billing_controller_test.exs | 8 +- .../google_analytics_controller_test.exs | 8 +- .../controllers/site_controller_test.exs | 4 +- .../controllers/stats_controller_test.exs | 17 ++-- test/plausible_web/live/choose_plan_test.exs | 11 ++- .../live/funnel_settings_test.exs | 1 + .../plausible_web/live/goal_settings_test.exs | 1 + .../live/props_settings_test.exs | 1 + .../api/controllers/capabilities_test.exs | 3 +- .../api/controllers/custom_props_test.exs | 5 +- .../plugins/api/controllers/funnels_test.exs | 3 +- .../plugins/api/controllers/goals_test.exs | 5 +- .../plugs/authorize_public_api_test.exs | 9 +- test/support/plugins_api_case.ex | 3 +- test/support/teams/test.ex | 9 +- test/support/test_utils.ex | 14 +-- 64 files changed, 391 insertions(+), 360 deletions(-) diff --git a/lib/plausible/billing/feature.ex b/lib/plausible/billing/feature.ex index ea699b5e8..78df7c23d 100644 --- a/lib/plausible/billing/feature.ex +++ b/lib/plausible/billing/feature.ex @@ -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 diff --git a/lib/plausible/teams/adapter.ex b/lib/plausible/teams/adapter.ex index 63333f5e7..3a394e86e 100644 --- a/lib/plausible/teams/adapter.ex +++ b/lib/plausible/teams/adapter.ex @@ -11,14 +11,6 @@ defmodule Plausible.Teams.Adapter do end end - def team_or_user(user) do - switch( - user, - team_fn: &Function.identity/1, - user_fn: &Function.identity/1 - ) - end - def switch(user, opts \\ []) do team_fn = Keyword.fetch!(opts, :team_fn) user_fn = Keyword.fetch!(opts, :user_fn) diff --git a/lib/plausible/teams/adapter/read/billing.ex b/lib/plausible/teams/adapter/read/billing.ex index 6a58bd842..c70ddcad0 100644 --- a/lib/plausible/teams/adapter/read/billing.ex +++ b/lib/plausible/teams/adapter/read/billing.ex @@ -1,6 +1,6 @@ defmodule Plausible.Teams.Adapter.Read.Billing do @moduledoc """ - Transition adapter for new schema reads + Transition adapter for new schema reads """ use Plausible.Teams.Adapter @@ -34,4 +34,65 @@ 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 end diff --git a/lib/plausible/teams/adapter/read/ownership.ex b/lib/plausible/teams/adapter/read/ownership.ex index 35f691e31..38b06b8e0 100644 --- a/lib/plausible/teams/adapter/read/ownership.ex +++ b/lib/plausible/teams/adapter/read/ownership.ex @@ -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,20 @@ defmodule Plausible.Teams.Adapter.Read.Ownership do alias Plausible.Auth alias Plausible.Site.Memberships.Invitations + 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 +53,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 diff --git a/lib/plausible_web/controllers/api/internal_controller.ex b/lib/plausible_web/controllers/api/internal_controller.ex index 6ef04e55e..686eb99b6 100644 --- a/lib/plausible_web/controllers/api/internal_controller.ex +++ b/lib/plausible_web/controllers/api/internal_controller.ex @@ -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} -> diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 15a634d54..4105ddc47 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -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 diff --git a/test/plausible/auth/auth_test.exs b/test/plausible/auth/auth_test.exs index 79589261d..6b9dc27ca 100644 --- a/test/plausible/auth/auth_test.exs +++ b/test/plausible/auth/auth_test.exs @@ -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 @@ -52,14 +53,14 @@ defmodule Plausible.AuthTest do 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 +79,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 +101,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) diff --git a/test/plausible/billing/feature_test.exs b/test/plausible/billing/feature_test.exs index 6e37acaa6..f2ebdb8de 100644 --- a/test/plausible/billing/feature_test.exs +++ b/test/plausible/billing/feature_test.exs @@ -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 diff --git a/test/plausible/billing/quota_test.exs b/test/plausible/billing/quota_test.exs index c4d87d6e5..e0190e303 100644 --- a/test/plausible/billing/quota_test.exs +++ b/test/plausible/billing/quota_test.exs @@ -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") diff --git a/test/plausible/funnels_test.exs b/test/plausible/funnels_test.exs index 479f1a8ec..c59d3bbb4 100644 --- a/test/plausible/funnels_test.exs +++ b/test/plausible/funnels_test.exs @@ -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"}) diff --git a/test/plausible/goals_test.exs b/test/plausible/goals_test.exs index 552cef622..ede9287ba 100644 --- a/test/plausible/goals_test.exs +++ b/test/plausible/goals_test.exs @@ -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"}) diff --git a/test/plausible/google/api_test.exs b/test/plausible/google/api_test.exs index 4724d360a..357a3fd44 100644 --- a/test/plausible/google/api_test.exs +++ b/test/plausible/google/api_test.exs @@ -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 diff --git a/test/plausible/imported/buffer_test.exs b/test/plausible/imported/buffer_test.exs index 473079d7c..eb407950e 100644 --- a/test/plausible/imported/buffer_test.exs +++ b/test/plausible/imported/buffer_test.exs @@ -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) diff --git a/test/plausible/imported/csv_importer_test.exs b/test/plausible/imported/csv_importer_test.exs index bb3c759d4..cd5962672 100644 --- a/test/plausible/imported/csv_importer_test.exs +++ b/test/plausible/imported/csv_importer_test.exs @@ -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 diff --git a/test/plausible/imported/google_analytics4_test.exs b/test/plausible/imported/google_analytics4_test.exs index 231840299..81b857d1f 100644 --- a/test/plausible/imported/google_analytics4_test.exs +++ b/test/plausible/imported/google_analytics4_test.exs @@ -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) diff --git a/test/plausible/props_test.exs b/test/plausible/props_test.exs index 8c18bf88b..0d83a3f50 100644 --- a/test/plausible/props_test.exs +++ b/test/plausible/props_test.exs @@ -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") diff --git a/test/plausible/site/cache_test.exs b/test/plausible/site/cache_test.exs index 6d8d5fd38..a8467a773 100644 --- a/test/plausible/site/cache_test.exs +++ b/test/plausible/site/cache_test.exs @@ -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}, diff --git a/test/plausible/stats/comparisons_test.exs b/test/plausible/stats/comparisons_test.exs index 739f46801..f291cfd6b 100644 --- a/test/plausible/stats/comparisons_test.exs +++ b/test/plausible/stats/comparisons_test.exs @@ -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) diff --git a/test/plausible/stats/query_parser_test.exs b/test/plausible/stats/query_parser_test.exs index a12395c3f..5da17b28f 100644 --- a/test/plausible/stats/query_parser_test.exs +++ b/test/plausible/stats/query_parser_test.exs @@ -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, @@ -1523,10 +1518,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, diff --git a/test/plausible_web/components/billing/notice_test.exs b/test/plausible_web/components/billing/notice_test.exs index 843172103..d3d66abd1 100644 --- a/test/plausible_web/components/billing/notice_test.exs +++ b/test/plausible_web/components/billing/notice_test.exs @@ -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, diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index 1307049d3..6a33bdcb1 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -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) @@ -439,7 +439,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 +624,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 +687,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 +832,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 diff --git a/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs b/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs index 8f4264aa1..e81a7d1d3 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs @@ -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", %{ diff --git a/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs b/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs index 9107fd381..30d13f0b8 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs @@ -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) diff --git a/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs b/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs index 5c8b7fe16..7b0fef62c 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs @@ -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", %{ diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs index bf9e746d1..9280aa8d1 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs @@ -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, [ diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_goal_dimension_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_goal_dimension_test.exs index fb152d7cf..64b517995 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_goal_dimension_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_goal_dimension_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_imported_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_imported_test.exs index d179a9cf4..3c7c1a41c 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_imported_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_imported_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_special_metrics_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_special_metrics_test.exs index dc137b810..a2db1ca9e 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_special_metrics_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_special_metrics_test.exs @@ -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, diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_test.exs index c92480dec..0a33e8df6 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_test.exs @@ -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, [ diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_validations_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_validations_test.exs index af5f32286..1c805c08f 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_validations_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_validations_test.exs @@ -1,8 +1,10 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryValidationsTest do use PlausibleWeb.ConnCase + use Plausible.Teams.Test + 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] describe "feature access" do test "cannot break down by a custom prop without access to the props feature", %{ @@ -10,8 +12,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryValidationsTest 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 = post(conn, "/api/v2/query", %{ @@ -30,8 +31,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryValidationsTest 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 = post(conn, "/api/v2/query", %{ @@ -49,10 +49,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryValidationsTest 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 = post(conn, "/api/v2/query", %{ @@ -72,8 +69,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryValidationsTest 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 = post(conn, "/api/v2/query", %{ diff --git a/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs b/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs index 7c84ebadd..d1378171e 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs @@ -1,8 +1,9 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do use PlausibleWeb.ConnCase + use Plausible.Teams.Test 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] describe "feature access" do test "cannot filter by a custom prop without access to the props feature", %{ @@ -10,8 +11,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest 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/timeseries", %{ @@ -30,8 +30,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest 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/timeseries", %{ diff --git a/test/plausible_web/controllers/api/internal_controller/docs_query_test.exs b/test/plausible_web/controllers/api/internal_controller/docs_query_test.exs index 5d9694280..03cd0af46 100644 --- a/test/plausible_web/controllers/api/internal_controller/docs_query_test.exs +++ b/test/plausible_web/controllers/api/internal_controller/docs_query_test.exs @@ -4,7 +4,7 @@ defmodule PlausibleWeb.Api.InternalController.DocsQueryTest do @user_id Enum.random(1000..9999) describe "POST /api/docs/query not logged in" do - setup [:create_user, :create_new_site] + setup [:create_user, :create_site] test "rejects request when not logged in", %{conn: conn, site: site} do populate_stats(site, [ @@ -25,7 +25,7 @@ defmodule PlausibleWeb.Api.InternalController.DocsQueryTest do end describe "POST /api/docs/query logged in" do - setup [:create_user, :create_new_site, :log_in] + setup [:create_user, :create_site, :log_in] test "rejects when accessing any other site", %{conn: conn} do conn = diff --git a/test/plausible_web/controllers/api/stats_controller/browsers_test.exs b/test/plausible_web/controllers/api/stats_controller/browsers_test.exs index def994fea..60d1fa4a2 100644 --- a/test/plausible_web/controllers/api/stats_controller/browsers_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/browsers_test.exs @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do use PlausibleWeb.ConnCase describe "GET /api/stats/:domain/browsers" do - setup [:create_user, :log_in, :create_new_site, :create_site_import] + setup [:create_user, :log_in, :create_site, :create_site_import] test "returns top browsers by unique visitors", %{conn: conn, site: site} do populate_stats(site, [ @@ -274,7 +274,7 @@ defmodule PlausibleWeb.Api.StatsController.BrowsersTest do end describe "GET /api/stats/:domain/browser-versions" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns correct conversion_rate when browser_version clashes across browsers", %{ conn: conn, diff --git a/test/plausible_web/controllers/api/stats_controller/cities_test.exs b/test/plausible_web/controllers/api/stats_controller/cities_test.exs index 0f97188f2..b431739ea 100644 --- a/test/plausible_web/controllers/api/stats_controller/cities_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/cities_test.exs @@ -32,7 +32,7 @@ defmodule PlausibleWeb.Api.StatsController.CitiesTest do ]) end - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import, :seed] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import, :seed] test "returns top cities by new visitors", %{conn: conn, site: site} do conn = get(conn, "/api/stats/#{site.domain}/cities?period=day") diff --git a/test/plausible_web/controllers/api/stats_controller/conversions_test.exs b/test/plausible_web/controllers/api/stats_controller/conversions_test.exs index a58687eaf..0c423b025 100644 --- a/test/plausible_web/controllers/api/stats_controller/conversions_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/conversions_test.exs @@ -4,7 +4,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do @user_id Enum.random(1000..9999) describe "GET /api/stats/:domain/conversions" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns mixed conversions in ordered by count", %{conn: conn, site: site} do populate_stats(site, [ @@ -324,9 +324,12 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do site: site, user: user } do - user - |> Plausible.Auth.User.end_trial() - |> Plausible.Repo.update!() + user = + user + |> Plausible.Auth.User.end_trial() + |> Plausible.Repo.update!() + + Plausible.Teams.sync_team(user) populate_stats(site, [ build(:event, @@ -438,7 +441,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do end describe "GET /api/stats/:domain/conversions - with goal filter" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "does not consider custom event pathname as a pageview goal completion", %{ conn: conn, @@ -625,7 +628,7 @@ defmodule PlausibleWeb.Api.StatsController.ConversionsTest do end describe "GET /api/stats/:domain/conversions - with goal and prop=(none) filter" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns only the conversion that is filtered for", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/countries_test.exs b/test/plausible_web/controllers/api/stats_controller/countries_test.exs index 4f400fe5e..db3b2f069 100644 --- a/test/plausible_web/controllers/api/stats_controller/countries_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/countries_test.exs @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do use PlausibleWeb.ConnCase describe "GET /api/stats/:domain/countries" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top countries by new visitors", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs b/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs index 33921a905..dbf16a679 100644 --- a/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/custom_prop_breakdown_test.exs @@ -1,8 +1,9 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do use PlausibleWeb.ConnCase + use Plausible.Teams.Test describe "GET /api/stats/:domain/custom-prop-values/:prop_key" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns breakdown by a custom property", %{conn: conn, site: site} do prop_key = "parim_s6ber" @@ -151,7 +152,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do end describe "GET /api/stats/:domain/custom-prop-values/:prop_key - with goal filter" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns property breakdown for goal", %{conn: conn, site: site} do populate_stats(site, [ @@ -906,7 +907,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do end describe "GET /api/stats/:domain/custom-prop-values/:prop_key - other filters" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns prop-breakdown with a page filter", %{conn: conn, site: site} do prop_key = "parim_s6ber" @@ -1104,10 +1105,10 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do end describe "GET /api/stats/:domain/custom-prop-values/:prop_key - for a Growth subscription" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] setup %{user: user, site: site} do - insert(:growth_subscription, user: user) + subscribe_to_growth_plan(user) populate_stats(site, [ build(:pageview, @@ -1144,7 +1145,7 @@ defmodule PlausibleWeb.Api.StatsController.CustomPropBreakdownTest do end describe "with imported data" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "gracefully ignores unsupported WP Search Queries goal for imported data", %{ conn: conn, diff --git a/test/plausible_web/controllers/api/stats_controller/funnels_test.exs b/test/plausible_web/controllers/api/stats_controller/funnels_test.exs index e598e8d5e..727adbe3c 100644 --- a/test/plausible_web/controllers/api/stats_controller/funnels_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/funnels_test.exs @@ -1,6 +1,7 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do use PlausibleWeb.ConnCase, async: true use Plausible + use Plausible.Teams.Test @moduletag :ee_only on_ee do @@ -15,7 +16,7 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do ] describe "GET /api/stats/funnel - default" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "computes funnel for a day", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) @@ -229,7 +230,7 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do site: site } do {:ok, funnel} = setup_funnel(site, @build_funnel_with) - insert(:growth_subscription, user: user) + subscribe_to_growth_plan(user) resp = conn @@ -244,7 +245,7 @@ defmodule PlausibleWeb.Api.StatsController.FunnelsTest do end describe "GET /api/stats/funnel - disallowed filters" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "event:page", %{conn: conn, site: site} do {:ok, funnel} = setup_funnel(site, @build_funnel_with) diff --git a/test/plausible_web/controllers/api/stats_controller/imported_test.exs b/test/plausible_web/controllers/api/stats_controller/imported_test.exs index 7224fbd36..4e36ba8ef 100644 --- a/test/plausible_web/controllers/api/stats_controller/imported_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/imported_test.exs @@ -12,7 +12,7 @@ defmodule PlausibleWeb.Api.StatsController.ImportedTest do for import_type <- [:new_and_legacy, :new] do describe "Parse and import third party data fetched from Google Analytics as #{import_type} import" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] setup %{user: user, site: site} do import_params = diff --git a/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs b/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs index 8c3385fd9..1b2353c76 100644 --- a/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs @@ -4,7 +4,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do @user_id Enum.random(1000..9999) describe "GET /api/stats/main-graph - plot" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "displays pageviews for the last 30 minutes in realtime graph", %{conn: conn, site: site} do populate_stats(site, [ @@ -389,7 +389,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - pageviews plot" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "displays pageviews for a month", %{conn: conn, site: site} do populate_stats(site, [ @@ -455,7 +455,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - visitors plot" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "displays visitors per hour with short visits", %{ conn: conn, @@ -585,7 +585,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - conversion_rate plot" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns 400 when conversion rate is queried without a goal filter", %{ conn: conn, @@ -630,7 +630,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - events (total conversions) plot" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns 400 when the `events` metric is queried without a goal filter", %{ conn: conn, @@ -737,7 +737,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - bounce_rate plot" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "displays bounce_rate for a month", %{conn: conn, site: site} do populate_stats(site, [ @@ -801,7 +801,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - visit_duration plot" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "displays visit_duration for a month", %{conn: conn, site: site} do populate_stats(site, [ @@ -868,7 +868,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - varying intervals" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "displays visitors for 6mo on a day scale", %{conn: conn, site: site} do populate_stats(site, [ @@ -1118,7 +1118,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "GET /api/stats/main-graph - comparisons" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns past month stats when period=30d and comparison=previous_period", %{ conn: conn, @@ -1292,7 +1292,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do describe "GET /api/stats/main-graph - total_revenue plot" do @describetag :ee_only - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "plots total_revenue for a month", %{conn: conn, site: site} do insert(:goal, site: site, event_name: "Payment", currency: "USD") @@ -1427,7 +1427,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do describe "GET /api/stats/main-graph - average_revenue plot" do @describetag :ee_only - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "plots total_revenue for a month", %{conn: conn, site: site} do insert(:goal, site: site, event_name: "Payment", currency: "USD") @@ -1567,7 +1567,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end describe "present_index" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "exists for a date range that includes the current day", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs b/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs index 491e393c5..e805ef4e6 100644 --- a/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/operating_systems_test.exs @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do use PlausibleWeb.ConnCase describe "GET /api/stats/:domain/operating-systems" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns operating systems by unique visitors", %{conn: conn, site: site} do populate_stats(site, [ @@ -215,7 +215,7 @@ defmodule PlausibleWeb.Api.StatsController.OperatingSystemsTest do end describe "GET /api/stats/:domain/operating-system-versions" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top OS versions by unique visitors", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/pages_test.exs b/test/plausible_web/controllers/api/stats_controller/pages_test.exs index b1db5ca63..498132f41 100644 --- a/test/plausible_web/controllers/api/stats_controller/pages_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/pages_test.exs @@ -4,7 +4,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do @user_id Enum.random(1000..9999) describe "GET /api/stats/:domain/pages" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top pages by visitors", %{conn: conn, site: site} do populate_stats(site, [ @@ -1454,7 +1454,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do end describe "GET /api/stats/:domain/entry-pages" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top entry pages by visitors", %{conn: conn, site: site} do populate_stats(site, [ @@ -1861,7 +1861,7 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do end describe "GET /api/stats/:domain/exit-pages" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top exit pages by visitors", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/regions_test.exs b/test/plausible_web/controllers/api/stats_controller/regions_test.exs index 711ae957f..48f59feb7 100644 --- a/test/plausible_web/controllers/api/stats_controller/regions_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/regions_test.exs @@ -32,7 +32,7 @@ defmodule PlausibleWeb.Api.StatsController.RegionsTest do ]) end - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import, :seed] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import, :seed] test "returns top cities by new visitors", %{conn: conn, site: site} do conn = get(conn, "/api/stats/#{site.domain}/regions?period=day") diff --git a/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs b/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs index 9da575fa5..de4f1ac07 100644 --- a/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/screen_sizes_test.exs @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Api.StatsController.ScreenSizesTest do use PlausibleWeb.ConnCase describe "GET /api/stats/:domain/screen-sizes" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns screen sizes by new visitors", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/sources_test.exs b/test/plausible_web/controllers/api/stats_controller/sources_test.exs index 44584c6dc..db5153eae 100644 --- a/test/plausible_web/controllers/api/stats_controller/sources_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/sources_test.exs @@ -4,7 +4,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do @user_id Enum.random(1000..9999) describe "GET /api/stats/:domain/sources" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top sources by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -676,7 +676,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "UTM parameters with hostname filter" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] for {resource, attr} <- [ utm_campaigns: :utm_campaign, @@ -722,7 +722,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/channels" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns top channels by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -772,7 +772,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/utm_mediums" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top utm_mediums by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -924,7 +924,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/utm_campaigns" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top utm_campaigns by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -1084,7 +1084,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/utm_sources" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top utm_sources by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -1132,7 +1132,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/utm_terms" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top utm_terms by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -1292,7 +1292,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/utm_contents" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns top utm_contents by unique user ids", %{conn: conn, site: site} do populate_stats(site, [ @@ -1452,7 +1452,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/sources - with goal filter" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns top referrers for a custom goal including conversion_rate", %{ conn: conn, @@ -1718,7 +1718,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do end describe "GET /api/stats/:domain/referrer-drilldown" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns top referrers for a particular source", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs b/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs index 3aed02080..ead86adcc 100644 --- a/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs @@ -112,7 +112,7 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do end test "returns suggestions for regions", %{conn: conn, user: user} do - {:ok, [site: site]} = create_new_site(%{user: user}) + {:ok, [site: site]} = create_site(%{user: user}) populate_stats(site, [ build(:pageview, country_code: "EE", subdivision1_code: "EE-37"), @@ -129,7 +129,7 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do end test "returns suggestions for cities", %{conn: conn, user: user} do - {:ok, [site: site]} = create_new_site(%{user: user}) + {:ok, [site: site]} = create_site(%{user: user}) populate_stats(site, [ build(:pageview, @@ -256,7 +256,7 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do end test "returns suggestions for hostnames", %{conn: conn1, user: user} do - {:ok, [site: site]} = create_new_site(%{user: user}) + {:ok, [site: site]} = create_site(%{user: user}) populate_stats(site, [ build(:pageview, @@ -299,7 +299,7 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do end test "returns suggestions for hostnames limited by shields", %{conn: conn1, user: user} do - {:ok, [site: site]} = create_new_site(%{user: user}) + {:ok, [site: site]} = create_site(%{user: user}) Plausible.Shields.add_hostname_rule(site, %{"hostname" => "*.example.com"}) Plausible.Shields.add_hostname_rule(site, %{"hostname" => "erin.rogue.com"}) @@ -378,7 +378,7 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do end describe "suggestions for props" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns suggestions for prop key ordered by count", %{conn: conn, site: site} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs b/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs index e72087906..d7b2cd5cb 100644 --- a/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs @@ -1,10 +1,11 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do use PlausibleWeb.ConnCase + use Plausible.Teams.Test @user_id Enum.random(1000..9999) describe "GET /api/stats/top-stats - default" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns graph_metric key for graphable top stats", %{conn: conn, site: site} do [visitors, visits, pageviews, views_per_visit, bounce_rate, visit_duration] = @@ -477,7 +478,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do end describe "GET /api/stats/top-stats - with imported data" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "returns divisible metrics as 0 when no stats exist", %{ site: site, @@ -643,7 +644,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do end describe "GET /api/stats/top-stats - with_imported_switch info" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] setup context do insert(:site_import, @@ -777,7 +778,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do end describe "GET /api/stats/top-stats - realtime" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "shows current visitors (last 5 minutes)", %{conn: conn, site: site} do populate_stats(site, [ @@ -883,7 +884,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do end describe "GET /api/stats/top-stats - filters" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns graph_metric key for top stats with a page filter", %{ conn: conn, @@ -1257,7 +1258,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do end describe "GET /api/stats/top-stats - filtered for goal" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "returns total unique visitors", %{conn: conn, site: site} do populate_stats(site, [ @@ -1599,7 +1600,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do test "does not return average and total when site owner is on a growth plan", %{conn: conn, site: site, user: user} do - insert(:growth_subscription, user: user) + subscribe_to_growth_plan(user) insert(:goal, site: site, event_name: "Payment", currency: "USD") populate_stats(site, [ @@ -1621,7 +1622,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do end describe "GET /api/stats/top-stats - with comparisons" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "does not return comparisons by default", %{site: site, conn: conn} do populate_stats(site, [ diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs index 8bfb49518..be53fbe9e 100644 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -542,7 +542,7 @@ defmodule PlausibleWeb.AuthControllerTest do end describe "DELETE /me" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] use Plausible.Repo test "deletes the user", %{conn: conn, user: user, site: site} do diff --git a/test/plausible_web/controllers/billing_controller_test.exs b/test/plausible_web/controllers/billing_controller_test.exs index f5afa895b..ab7b507e0 100644 --- a/test/plausible_web/controllers/billing_controller_test.exs +++ b/test/plausible_web/controllers/billing_controller_test.exs @@ -145,6 +145,8 @@ defmodule PlausibleWeb.BillingControllerTest do test "data-product attribute on the checkout link is the paddle_plan_id of the enterprise plan", %{conn: conn, user: user} do + {:ok, team} = Plausible.Teams.get_by_owner(user) + doc = conn |> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan)) @@ -153,7 +155,7 @@ defmodule PlausibleWeb.BillingControllerTest do assert %{ "disableLogout" => true, "email" => user.email, - "passthrough" => user.id, + "passthrough" => "user:#{user.id};team:#{team.id}", "product" => @configured_enterprise_plan_paddle_plan_id, "success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), "theme" => "none" @@ -316,6 +318,8 @@ defmodule PlausibleWeb.BillingControllerTest do test "renders paddle button with the correct checkout params", %{conn: conn, user: user} do + {:ok, team} = Plausible.Teams.get_by_owner(user) + doc = conn |> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan)) @@ -324,7 +328,7 @@ defmodule PlausibleWeb.BillingControllerTest do assert %{ "disableLogout" => true, "email" => user.email, - "passthrough" => user.id, + "passthrough" => "user:#{user.id};team:#{team.id}", "product" => @configured_enterprise_plan_paddle_plan_id, "success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), "theme" => "none" diff --git a/test/plausible_web/controllers/google_analytics_controller_test.exs b/test/plausible_web/controllers/google_analytics_controller_test.exs index e3f77e8ff..6d66ca53e 100644 --- a/test/plausible_web/controllers/google_analytics_controller_test.exs +++ b/test/plausible_web/controllers/google_analytics_controller_test.exs @@ -17,7 +17,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do setup :verify_on_exit! describe "GET /:domain/import/google-analytics/property" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "lists Google Analytics properties", %{conn: conn, site: site} do expect( @@ -207,7 +207,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do end describe "POST /:domain/import/google-analytics/property" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "redirects to confirmation", %{conn: conn, site: site} do expect( @@ -505,7 +505,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do end describe "GET /:domain/import/google-analytics/confirm" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "renders confirmation form for Google Analytics 4 import", %{conn: conn, site: site} do expect( @@ -730,7 +730,7 @@ defmodule PlausibleWeb.GoogleAnalyticsControllerTest do end describe "POST /:domain/settings/google-import" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "creates Google Analytics 4 site import instance", %{conn: conn, site: site} do conn = diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index d972d723c..d14798430 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -1532,7 +1532,7 @@ defmodule PlausibleWeb.SiteControllerTest do end describe "DELETE /:domain/settings/:forget_import/:import_id" do - setup [:create_user, :log_in, :create_new_site, :create_legacy_site_import] + setup [:create_user, :log_in, :create_site, :create_legacy_site_import] test "removes site import, associated data and cancels oban job for a particular import", %{ conn: conn, @@ -1610,7 +1610,7 @@ defmodule PlausibleWeb.SiteControllerTest do end describe "DELETE /:domain/settings/forget_imported" do - setup [:create_user, :log_in, :create_new_site] + setup [:create_user, :log_in, :create_site] test "removes actual imported data from Clickhouse", %{conn: conn, user: user, site: site} do Plausible.Imported.NoopImporter.new_import( diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 92fe8eb87..5ba8255a8 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -1,13 +1,14 @@ defmodule PlausibleWeb.StatsControllerTest do use PlausibleWeb.ConnCase, async: false use Plausible.Repo + use Plausible.Teams.Test import Plausible.Test.Support.HTML @react_container "div#stats-react-container" describe "GET /:domain - anonymous user" do test "public site - shows site stats", %{conn: conn} do - site = insert(:site, public: true) + site = new_site(public: true) populate_stats(site, [build(:pageview)]) conn = get(conn, "/#{site.domain}") @@ -182,7 +183,7 @@ defmodule PlausibleWeb.StatsControllerTest do end describe "GET /:domain/export" do - setup [:create_user, :create_new_site, :log_in] + setup [:create_user, :create_site, :log_in] test "exports all the necessary CSV files", %{conn: conn, site: site} do conn = get(conn, "/" <> site.domain <> "/export") @@ -223,7 +224,7 @@ defmodule PlausibleWeb.StatsControllerTest do {:ok, site} = Plausible.Props.allow(site, ["author"]) site = Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) populate_stats(site, [ build(:pageview, "meta.key": ["author"], "meta.value": ["a"]), @@ -252,7 +253,7 @@ defmodule PlausibleWeb.StatsControllerTest do {:ok, site} = Plausible.Props.allow(site, ["author"]) site = Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) populate_stats(site, [ build(:pageview, "meta.key": ["author"], "meta.value": ["a"]) @@ -612,7 +613,7 @@ defmodule PlausibleWeb.StatsControllerTest do end describe "GET /:domain/export - for past 6 months" do - setup [:create_user, :create_new_site, :log_in] + setup [:create_user, :create_site, :log_in] test "exports 6 months of data in zipped csvs", %{conn: conn, site: site} do populate_exported_stats(site) @@ -622,7 +623,7 @@ defmodule PlausibleWeb.StatsControllerTest do end describe "GET /:domain/export - with path filter" do - setup [:create_user, :create_new_site, :log_in] + setup [:create_user, :create_site, :log_in] test "exports filtered data in zipped csvs", %{conn: conn, site: site} do populate_exported_stats(site) @@ -634,7 +635,7 @@ defmodule PlausibleWeb.StatsControllerTest do end describe "GET /:domain/export - with a custom prop filter" do - setup [:create_user, :create_new_site, :log_in] + setup [:create_user, :create_site, :log_in] test "custom-props.csv only returns the prop and its value in filter", %{ conn: conn, @@ -763,7 +764,7 @@ defmodule PlausibleWeb.StatsControllerTest do end describe "GET /:domain/export - with goal filter" do - setup [:create_user, :create_new_site, :log_in] + setup [:create_user, :create_site, :log_in] test "exports goal-filtered data in zipped csvs", %{conn: conn, site: site} do populate_exported_stats(site) diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs index c25777fab..e8c31c7ae 100644 --- a/test/plausible_web/live/choose_plan_test.exs +++ b/test/plausible_web/live/choose_plan_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do use PlausibleWeb.ConnCase, async: true + use Plausible.Teams.Test @moduletag :ee_only import Phoenix.LiveViewTest @@ -193,6 +194,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do user: user } do {:ok, lv, _doc} = get_liveview(conn) + {:ok, team} = Plausible.Teams.get_by_owner(user) set_slider(lv, "200k") doc = element(lv, @yearly_interval_button) |> render_click() @@ -200,7 +202,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert %{ "disableLogout" => true, "email" => user.email, - "passthrough" => user.id, + "passthrough" => "user:#{user.id};team:#{team.id}", "product" => @v4_growth_200k_yearly_plan_id, "success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), "theme" => "none" @@ -500,8 +502,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do conn: conn, user: user } do - another_user = insert(:user) - pending_site = insert(:site, members: [another_user]) + another_user = new_user() + pending_site = new_site(owner: another_user) Plausible.Props.allow(pending_site, ["author"]) @@ -1065,6 +1067,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do test "renders Paddle upgrade buttons", %{conn: conn, user: user} do {:ok, lv, _doc} = get_liveview(conn) + {:ok, team} = Plausible.Teams.get_by_owner(user) set_slider(lv, "200k") doc = element(lv, @yearly_interval_button) |> render_click() @@ -1072,7 +1075,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert %{ "disableLogout" => true, "email" => user.email, - "passthrough" => user.id, + "passthrough" => "user:#{user.id};team:#{team.id}", "product" => @v4_growth_200k_yearly_plan_id, "success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), "theme" => "none" diff --git a/test/plausible_web/live/funnel_settings_test.exs b/test/plausible_web/live/funnel_settings_test.exs index 245cae475..1e27ace00 100644 --- a/test/plausible_web/live/funnel_settings_test.exs +++ b/test/plausible_web/live/funnel_settings_test.exs @@ -15,6 +15,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do user |> Plausible.Auth.User.end_trial() |> Plausible.Repo.update!() + |> Plausible.Teams.sync_team() conn = get(conn, "/#{site.domain}/settings/funnels") resp = conn |> html_response(200) |> text() diff --git a/test/plausible_web/live/goal_settings_test.exs b/test/plausible_web/live/goal_settings_test.exs index 268631b49..653ddd44e 100644 --- a/test/plausible_web/live/goal_settings_test.exs +++ b/test/plausible_web/live/goal_settings_test.exs @@ -33,6 +33,7 @@ defmodule PlausibleWeb.Live.GoalSettingsTest do user |> Plausible.Auth.User.end_trial() |> Plausible.Repo.update!() + |> Plausible.Teams.sync_team() conn = get(conn, "/#{site.domain}/settings/goals") diff --git a/test/plausible_web/live/props_settings_test.exs b/test/plausible_web/live/props_settings_test.exs index 8154e38a7..b993dacba 100644 --- a/test/plausible_web/live/props_settings_test.exs +++ b/test/plausible_web/live/props_settings_test.exs @@ -11,6 +11,7 @@ defmodule PlausibleWeb.Live.PropsSettingsTest do user |> Plausible.Auth.User.end_trial() |> Plausible.Repo.update!() + |> Plausible.Teams.sync_team() conn = get(conn, "/#{site.domain}/settings/properties") resp = conn |> html_response(200) |> text() diff --git a/test/plausible_web/plugins/api/controllers/capabilities_test.exs b/test/plausible_web/plugins/api/controllers/capabilities_test.exs index 250ff809f..4c46621ba 100644 --- a/test/plausible_web/plugins/api/controllers/capabilities_test.exs +++ b/test/plausible_web/plugins/api/controllers/capabilities_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do use PlausibleWeb.PluginsAPICase, async: true + use Plausible.Teams.Test alias PlausibleWeb.Plugins.API.Schemas describe "examples" do @@ -88,7 +89,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do @tag :ee_only test "growth", %{conn: conn, site: site, token: token} do site = Plausible.Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) resp = conn diff --git a/test/plausible_web/plugins/api/controllers/custom_props_test.exs b/test/plausible_web/plugins/api/controllers/custom_props_test.exs index d2e911c20..e7b461e59 100644 --- a/test/plausible_web/plugins/api/controllers/custom_props_test.exs +++ b/test/plausible_web/plugins/api/controllers/custom_props_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CustomPropsTest do use PlausibleWeb.PluginsAPICase, async: true + use Plausible.Teams.Test alias PlausibleWeb.Plugins.API.Schemas describe "examples" do @@ -42,7 +43,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CustomPropsTest do conn: conn } do site = Plausible.Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) url = Routes.plugins_api_custom_props_url(PlausibleWeb.Endpoint, :enable) @@ -66,7 +67,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CustomPropsTest do conn: conn } do site = Plausible.Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) url = Routes.plugins_api_custom_props_url(PlausibleWeb.Endpoint, :enable) diff --git a/test/plausible_web/plugins/api/controllers/funnels_test.exs b/test/plausible_web/plugins/api/controllers/funnels_test.exs index ac09eddbc..cba0f6267 100644 --- a/test/plausible_web/plugins/api/controllers/funnels_test.exs +++ b/test/plausible_web/plugins/api/controllers/funnels_test.exs @@ -1,6 +1,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.FunnelsTest do use PlausibleWeb.PluginsAPICase, async: true use Plausible + use Plausible.Teams.Test @moduletag :ee_only @@ -249,7 +250,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.FunnelsTest do test "fails for insufficient plan", %{conn: conn, token: token, site: site} do site = Plausible.Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) diff --git a/test/plausible_web/plugins/api/controllers/goals_test.exs b/test/plausible_web/plugins/api/controllers/goals_test.exs index ab55f474a..aea1053dd 100644 --- a/test/plausible_web/plugins/api/controllers/goals_test.exs +++ b/test/plausible_web/plugins/api/controllers/goals_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do use PlausibleWeb.PluginsAPICase, async: true + use Plausible.Teams.Test alias PlausibleWeb.Plugins.API.Schemas describe "examples" do @@ -53,7 +54,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do conn: conn } do site = Plausible.Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create) @@ -79,7 +80,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do conn: conn } do site = Plausible.Repo.preload(site, :owner) - insert(:growth_subscription, user: site.owner) + subscribe_to_growth_plan(site.owner) url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create) diff --git a/test/plausible_web/plugs/authorize_public_api_test.exs b/test/plausible_web/plugs/authorize_public_api_test.exs index a7501e89c..d4c343588 100644 --- a/test/plausible_web/plugs/authorize_public_api_test.exs +++ b/test/plausible_web/plugs/authorize_public_api_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPITest do use PlausibleWeb.ConnCase, async: false + use Plausible.Teams.Test alias PlausibleWeb.Plugs.AuthorizePublicAPI @@ -115,8 +116,8 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPITest do test "halts with error when API key owner does not have access to the requested site", %{ conn: conn } do - user = insert(:user) - site = insert(:site) + user = new_user() + site = new_site() api_key = insert(:api_key, user: user) conn = @@ -183,8 +184,8 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPITest do test "passes and sets current user and site when valid API key and site ID provided", %{ conn: conn } do - user = insert(:user) - site = insert(:site, members: [user]) + user = new_user() + site = new_site(owner: user) api_key = insert(:api_key, user: user) conn = diff --git a/test/support/plugins_api_case.ex b/test/support/plugins_api_case.ex index 246bece30..4c6b6cfab 100644 --- a/test/support/plugins_api_case.ex +++ b/test/support/plugins_api_case.ex @@ -34,7 +34,6 @@ defmodule PlausibleWeb.PluginsAPICase do end setup %{test: test} = tags do - import Plausible.Factory :ok = Ecto.Adapters.SQL.Sandbox.checkout(Plausible.Repo) unless tags[:async] do @@ -43,7 +42,7 @@ defmodule PlausibleWeb.PluginsAPICase do conn = Phoenix.ConnTest.build_conn() - site = insert(:site) + site = Plausible.Teams.Test.new_site() {:ok, _token, raw_token} = Plausible.Plugins.API.Tokens.create(site, Atom.to_string(test)) {:ok, conn: conn, site: site, token: raw_token} diff --git a/test/support/teams/test.ex b/test/support/teams/test.ex index b5755625a..a53ad8cea 100644 --- a/test/support/teams/test.ex +++ b/test/support/teams/test.ex @@ -142,6 +142,13 @@ defmodule Plausible.Teams.Test do user end + def subscribe_to_business_plan(user) do + {:ok, team} = Teams.get_or_create(user) + + insert(:business_subscription, user: user, team: team) + user + end + def subscribe_to_plan(user, paddle_plan_id) do {:ok, team} = Teams.get_or_create(user) @@ -149,7 +156,7 @@ defmodule Plausible.Teams.Test do user end - def subscribe_to_enterprise_plan(user, attrs) do + def subscribe_to_enterprise_plan(user, attrs \\ []) do {:ok, team} = Teams.get_or_create(user) {subscription?, attrs} = Keyword.pop(attrs, :subscription?, true) diff --git a/test/support/test_utils.ex b/test/support/test_utils.ex index 153dec3ab..52693066c 100644 --- a/test/support/test_utils.ex +++ b/test/support/test_utils.ex @@ -35,16 +35,11 @@ defmodule Plausible.TestUtils do end def create_user(_) do - {:ok, user: Factory.insert(:user)} + {:ok, user: Plausible.Teams.Test.new_user()} end def create_site(%{user: user}) do - site = - Factory.insert(:site, - members: [user] - ) - - {:ok, site: site} + {:ok, site: Plausible.Teams.Test.new_site(owner: user)} end def create_legacy_site_import(%{site: site}) do @@ -64,11 +59,6 @@ defmodule Plausible.TestUtils do {:ok, site_import: site_import} end - def create_new_site(%{user: user}) do - site = Factory.insert(:site, members: [user]) - {:ok, site: site} - end - def create_api_key(%{user: user}) do api_key = Factory.insert(:api_key, user: user) From 916c2bb4c5ea667c28a07f3a1ddf0b5b1cce5371 Mon Sep 17 00:00:00 2001 From: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:16:33 +0100 Subject: [PATCH 03/13] Stop suggesting pageleave as a custom event goal name (#4829) * stop suggestions pageleave as a custom event goal name * add missing test --- lib/plausible/stats/goal_suggestions.ex | 2 +- .../plausible/stats/goal_suggestions_test.exs | 8 ++++++-- .../api/stats_controller/suggestions_test.exs | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/plausible/stats/goal_suggestions.ex b/lib/plausible/stats/goal_suggestions.ex index 93ee2cd57..f0ca5a5eb 100644 --- a/lib/plausible/stats/goal_suggestions.ex +++ b/lib/plausible/stats/goal_suggestions.ex @@ -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, diff --git a/test/plausible/stats/goal_suggestions_test.exs b/test/plausible/stats/goal_suggestions_test.exs index 42a087b88..21d379f65 100644 --- a/test/plausible/stats/goal_suggestions_test.exs +++ b/test/plausible/stats/goal_suggestions_test.exs @@ -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(:event, name: "pageleave", user_id: 1, timestamp: NaiveDateTime.utc_now()) ]) assert GoalSuggestions.suggest_event_names(site, "") == ["Signup"] diff --git a/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs b/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs index ead86adcc..ed9ec0349 100644 --- a/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/suggestions_test.exs @@ -50,11 +50,28 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do end test "returns suggestions for goals", %{conn: conn, site: site} do - conn = get(conn, "/api/stats/#{site.domain}/suggestions/goal?period=month&date=2019-01-01") + conn = + get(conn, "/api/stats/#{site.domain}/suggestions/goal?period=month&date=2019-01-01&q=") assert json_response(conn, 200) == [] end + test "returns suggestions for configured site goals but not all event names", %{ + conn: conn, + site: site + } do + insert(:goal, site: site, event_name: "Signup") + + populate_stats(site, [ + build(:event, name: "Signup", timestamp: ~N[2019-01-01 00:00:00]), + build(:event, name: "another", timestamp: ~N[2019-01-01 00:00:00]) + ]) + + conn = get(conn, "/api/stats/#{site.domain}/suggestions/goal?period=day&date=2019-01-01&q=") + + assert json_response(conn, 200) == [%{"label" => "Signup", "value" => "Signup"}] + end + test "returns suggestions for sources", %{conn: conn, site: site} do populate_stats(site, [ build(:pageview, timestamp: ~N[2019-01-01 23:00:00], referrer_source: "Bing"), From 73166774e0b1729e30643dbbe0dfa0e01a719cb6 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Mon, 18 Nov 2024 13:19:11 +0200 Subject: [PATCH 04/13] Infer medium from click id if not present (#4817) --- lib/plausible/ingestion/event.ex | 13 +++++ .../api/external_controller_test.exs | 58 ++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/lib/plausible/ingestion/event.ex b/lib/plausible/ingestion/event.ex index 1027e1075..f94c81bc4 100644 --- a/lib/plausible/ingestion/event.ex +++ b/lib/plausible/ingestion/event.ex @@ -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, @@ -269,6 +270,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" -> diff --git a/test/plausible_web/controllers/api/external_controller_test.exs b/test/plausible_web/controllers/api/external_controller_test.exs index 81180b392..a5244488e 100644 --- a/test/plausible_web/controllers/api/external_controller_test.exs +++ b/test/plausible_web/controllers/api/external_controller_test.exs @@ -1374,6 +1374,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 +1398,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 +1443,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 +1453,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 +1466,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", From 9af498833e381d48d8c5b371ec6e5659612cde52 Mon Sep 17 00:00:00 2001 From: Karl-Aksel Puulmann Date: Tue, 19 Nov 2024 10:12:39 +0200 Subject: [PATCH 05/13] Channels: backfill utm_medium based on click_param_id (#4833) * Backfill utm_medium Follow-up to https://github.com/plausible/analytics/pull/4817 * Update backfill --- CHANGELOG.md | 1 + ...238_backfill_utm_medium_click_id_param.exs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 priv/ingest_repo/migrations/20241118112238_backfill_utm_medium_click_id_param.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7090b49ff..dc0f3056f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/priv/ingest_repo/migrations/20241118112238_backfill_utm_medium_click_id_param.exs b/priv/ingest_repo/migrations/20241118112238_backfill_utm_medium_click_id_param.exs new file mode 100644 index 000000000..df0c470a2 --- /dev/null +++ b/priv/ingest_repo/migrations/20241118112238_backfill_utm_medium_click_id_param.exs @@ -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 From 4ff2a66548e8ae15c325350ffdec6bb9bf927ad7 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 19 Nov 2024 10:49:37 +0100 Subject: [PATCH 06/13] Switch on team schema in site settings controller actions and LVs (#4834) * Populate `current_team` to site's team and make site and subscription preloads consistent * Accept only full `User` struct in `Users.get_for_user(!)` * Make all uses of `Sites.get_for_user(!)` switch on team schema * Remove redundant preloads for funnel/props settings * Use adapter transitions in subscription settings * Use team's schema subscription when listing invoices * Fix typespec * Turn owned site IDs into a specific query * Add clauses for when FF is on but no team has been created * Fix formatting --------- Co-authored-by: Adam Rutkowski --- .../api/external_sites_controller.ex | 2 +- .../lib/plausible_web/live/funnel_settings.ex | 14 ++- .../live/funnel_settings/form.ex | 8 +- lib/plausible/billing/paddle_api.ex | 2 +- lib/plausible/billing/qouta/limits.ex | 2 +- lib/plausible/sites.ex | 55 +---------- lib/plausible/teams.ex | 36 +++++-- lib/plausible/teams/adapter.ex | 10 ++ lib/plausible/teams/adapter/read/billing.ex | 36 +++++++ lib/plausible/teams/adapter/read/sites.ex | 71 +++++++++++++- lib/plausible/teams/billing.ex | 54 +++++++++- lib/plausible/users.ex | 2 +- .../controllers/admin_controller.ex | 2 +- .../controllers/settings_controller.ex | 23 ++--- .../controllers/site/membership_controller.ex | 21 ++-- .../controllers/site_controller.ex | 15 --- lib/plausible_web/live/goal_settings.ex | 4 +- .../live/imports_exports_settings.ex | 7 +- lib/plausible_web/live/installation.ex | 2 +- .../live/plugins/api/settings.ex | 7 +- .../live/plugins/api/token_form.ex | 7 +- lib/plausible_web/live/props_settings.ex | 6 +- lib/plausible_web/live/props_settings/form.ex | 6 +- lib/plausible_web/live/shields/countries.ex | 7 +- lib/plausible_web/live/shields/hostnames.ex | 7 +- .../live/shields/ip_addresses.ex | 7 +- lib/plausible_web/live/shields/pages.ex | 7 +- lib/plausible_web/live/verification.ex | 2 +- .../plugs/authorize_site_access.ex | 16 ++- lib/plausible_web/user_auth.ex | 10 +- lib/workers/check_usage.ex | 4 +- test/plausible/site/sites_test.exs | 30 +++--- .../api/external_sites_controller_test.exs | 20 ++-- .../controllers/settings_controller_test.exs | 98 +++++++------------ .../site/membership_controller_test.exs | 46 ++++----- .../controllers/site_controller_test.exs | 8 +- test/support/teams/test.ex | 8 +- 37 files changed, 401 insertions(+), 261 deletions(-) diff --git a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex index 447fa0e54..4900d6c8b 100644 --- a/extra/lib/plausible_web/controllers/api/external_sites_controller.ex +++ b/extra/lib/plausible_web/controllers/api/external_sites_controller.ex @@ -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 diff --git a/extra/lib/plausible_web/live/funnel_settings.ex b/extra/lib/plausible_web/live/funnel_settings.ex index 1a5a801bd..b6282abc9 100644 --- a/extra/lib/plausible_web/live/funnel_settings.ex +++ b/extra/lib/plausible_web/live/funnel_settings.ex @@ -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) diff --git a/extra/lib/plausible_web/live/funnel_settings/form.ex b/extra/lib/plausible_web/live/funnel_settings/form.ex index 4a98bc23d..e0d24220d 100644 --- a/extra/lib/plausible_web/live/funnel_settings/form.ex +++ b/extra/lib/plausible_web/live/funnel_settings/form.ex @@ -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{} diff --git a/lib/plausible/billing/paddle_api.ex b/lib/plausible/billing/paddle_api.ex index 71796cbe8..3c1ebfb8d 100644 --- a/lib/plausible/billing/paddle_api.ex +++ b/lib/plausible/billing/paddle_api.ex @@ -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} diff --git a/lib/plausible/billing/qouta/limits.ex b/lib/plausible/billing/qouta/limits.ex index 1d11505d9..93918cb72 100644 --- a/lib/plausible/billing/qouta/limits.ex +++ b/lib/plausible/billing/qouta/limits.ex @@ -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) diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index d6be09a27..f79b8687b 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -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, diff --git a/lib/plausible/teams.ex b/lib/plausible/teams.ex index f34fec535..32894bd88 100644 --- a/lib/plausible/teams.ex +++ b/lib/plausible/teams.ex @@ -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 diff --git a/lib/plausible/teams/adapter.ex b/lib/plausible/teams/adapter.ex index 3a394e86e..b93496138 100644 --- a/lib/plausible/teams/adapter.ex +++ b/lib/plausible/teams/adapter.ex @@ -11,6 +11,13 @@ defmodule Plausible.Teams.Adapter do end end + def user_or_team(user) do + switch(user, + team_fn: &Function.identity/1, + user_fn: &Function.identity/1 + ) + end + def switch(user, opts \\ []) do team_fn = Keyword.fetch!(opts, :team_fn) user_fn = Keyword.fetch!(opts, :user_fn) @@ -22,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 diff --git a/lib/plausible/teams/adapter/read/billing.ex b/lib/plausible/teams/adapter/read/billing.ex index c70ddcad0..9e38bf136 100644 --- a/lib/plausible/teams/adapter/read/billing.ex +++ b/lib/plausible/teams/adapter/read/billing.ex @@ -4,6 +4,42 @@ defmodule Plausible.Teams.Adapter.Read.Billing do """ use Plausible.Teams.Adapter + 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, diff --git a/lib/plausible/teams/adapter/read/sites.ex b/lib/plausible/teams/adapter/read/sites.ex index b38e9eeb4..34a4e4df7 100644 --- a/lib/plausible/teams/adapter/read/sites.ex +++ b/lib/plausible/teams/adapter/read/sites.ex @@ -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}%")) diff --git a/lib/plausible/teams/billing.ex b/lib/plausible/teams/billing.ex index 3db3f8dcb..17b6ebaa4 100644 --- a/lib/plausible/teams/billing.ex +++ b/lib/plausible/teams/billing.ex @@ -85,6 +85,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 +100,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) @@ -110,7 +118,7 @@ defmodule Plausible.Teams.Billing 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 +137,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) diff --git a/lib/plausible/users.ex b/lib/plausible/users.ex index ec9d99be5..a53121b7b 100644 --- a/lib/plausible/users.ex +++ b/lib/plausible/users.ex @@ -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 diff --git a/lib/plausible_web/controllers/admin_controller.ex b/lib/plausible_web/controllers/admin_controller.ex index fd68c75bd..13b0b5b3f 100644 --- a/lib/plausible_web/controllers/admin_controller.ex +++ b/lib/plausible_web/controllers/admin_controller.ex @@ -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) } diff --git a/lib/plausible_web/controllers/settings_controller.ex b/lib/plausible_web/controllers/settings_controller.ex index 0109e5597..015696fbc 100644 --- a/lib/plausible_web/controllers/settings_controller.ex +++ b/lib/plausible_web/controllers/settings_controller.ex @@ -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 diff --git a/lib/plausible_web/controllers/site/membership_controller.ex b/lib/plausible_web/controllers/site/membership_controller.ex index d17849f77..33aea074b 100644 --- a/lib/plausible_web/controllers/site/membership_controller.ex +++ b/lib/plausible_web/controllers/site/membership_controller.ex @@ -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} -> diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 4105ddc47..f4d39f6ba 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -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 diff --git a/lib/plausible_web/live/goal_settings.ex b/lib/plausible_web/live/goal_settings.ex index 1284436ac..19c26662e 100644 --- a/lib/plausible_web/live/goal_settings.ex +++ b/lib/plausible_web/live/goal_settings.ex @@ -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} -> diff --git a/lib/plausible_web/live/imports_exports_settings.ex b/lib/plausible_web/live/imports_exports_settings.ex index aefcd5982..94a19c5b6 100644 --- a/lib/plausible_web/live/imports_exports_settings.ex +++ b/lib/plausible_web/live/imports_exports_settings.ex @@ -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 diff --git a/lib/plausible_web/live/installation.ex b/lib/plausible_web/live/installation.ex index 19b2be8fd..b70e22574 100644 --- a/lib/plausible_web/live/installation.ex +++ b/lib/plausible_web/live/installation.ex @@ -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, diff --git a/lib/plausible_web/live/plugins/api/settings.ex b/lib/plausible_web/live/plugins/api/settings.ex index 48b20c2e3..582ab3c27 100644 --- a/lib/plausible_web/live/plugins/api/settings.ex +++ b/lib/plausible_web/live/plugins/api/settings.ex @@ -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) diff --git a/lib/plausible_web/live/plugins/api/token_form.ex b/lib/plausible_web/live/plugins/api/token_form.ex index 26d4bc7bc..e2fed6458 100644 --- a/lib/plausible_web/live/plugins/api/token_form.ex +++ b/lib/plausible_web/live/plugins/api/token_form.ex @@ -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() diff --git a/lib/plausible_web/live/props_settings.ex b/lib/plausible_web/live/props_settings.ex index 5e16d16f7..05cb1bc76 100644 --- a/lib/plausible_web/live/props_settings.ex +++ b/lib/plausible_web/live/props_settings.ex @@ -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 || [] diff --git a/lib/plausible_web/live/props_settings/form.ex b/lib/plausible_web/live/props_settings/form.ex index e2888c2f4..ada245135 100644 --- a/lib/plausible_web/live/props_settings/form.ex +++ b/lib/plausible_web/live/props_settings/form.ex @@ -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) diff --git a/lib/plausible_web/live/shields/countries.ex b/lib/plausible_web/live/shields/countries.ex index dc00a76cd..400496389 100644 --- a/lib/plausible_web/live/shields/countries.ex +++ b/lib/plausible_web/live/shields/countries.ex @@ -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) diff --git a/lib/plausible_web/live/shields/hostnames.ex b/lib/plausible_web/live/shields/hostnames.ex index af6887f66..8ec12623f 100644 --- a/lib/plausible_web/live/shields/hostnames.ex +++ b/lib/plausible_web/live/shields/hostnames.ex @@ -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) diff --git a/lib/plausible_web/live/shields/ip_addresses.ex b/lib/plausible_web/live/shields/ip_addresses.ex index 6f1979f27..f7cb9dfee 100644 --- a/lib/plausible_web/live/shields/ip_addresses.ex +++ b/lib/plausible_web/live/shields/ip_addresses.ex @@ -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) diff --git a/lib/plausible_web/live/shields/pages.ex b/lib/plausible_web/live/shields/pages.ex index 23b62a7af..ff368a2ac 100644 --- a/lib/plausible_web/live/shields/pages.ex +++ b/lib/plausible_web/live/shields/pages.ex @@ -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) diff --git a/lib/plausible_web/live/verification.ex b/lib/plausible_web/live/verification.ex index 44cbaad57..9308a98f0 100644 --- a/lib/plausible_web/live/verification.ex +++ b/lib/plausible_web/live/verification.ex @@ -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, diff --git a/lib/plausible_web/plugs/authorize_site_access.ex b/lib/plausible_web/plugs/authorize_site_access.ex index 651eda6e4..5abcc9ebb 100644 --- a/lib/plausible_web/plugs/authorize_site_access.ex +++ b/lib/plausible_web/plugs/authorize_site_access.ex @@ -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 diff --git a/lib/plausible_web/user_auth.ex b/lib/plausible_web/user_auth.ex index 58e3bf362..13a7f3c3e 100644 --- a/lib/plausible_web/user_auth.ex +++ b/lib/plausible_web/user_auth.ex @@ -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 diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex index a66d6d670..007892eb4 100644 --- a/lib/workers/check_usage.ex +++ b/lib/workers/check_usage.ex @@ -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} diff --git a/test/plausible/site/sites_test.exs b/test/plausible/site/sites_test.exs index e1699cbd1..9376ff7dd 100644 --- a/test/plausible/site/sites_test.exs +++ b/test/plausible/site/sites_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index 6a33bdcb1..dd19aa07b 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -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", %{ diff --git a/test/plausible_web/controllers/settings_controller_test.exs b/test/plausible_web/controllers/settings_controller_test.exs index fbfadd088..e2454a853 100644 --- a/test/plausible_web/controllers/settings_controller_test.exs +++ b/test/plausible_web/controllers/settings_controller_test.exs @@ -2,6 +2,7 @@ defmodule PlausibleWeb.SettingsControllerTest do use PlausibleWeb.ConnCase, async: true use Bamboo.Test use Plausible.Repo + use Plausible.Teams.Test import Mox import Plausible.Test.Support.HTML @@ -23,7 +24,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "shows subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "558018", user: user) + subscribe_to_plan(user, "558018") conn = get(conn, Routes.settings_path(conn, :subscription)) assert html_response(conn, 200) =~ "10k pageviews" assert html_response(conn, 200) =~ "monthly billing" @@ -31,7 +32,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "shows yearly subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "590752", user: user) + subscribe_to_plan(user, "590752") conn = get(conn, Routes.settings_path(conn, :subscription)) assert html_response(conn, 200) =~ "100k pageviews" assert html_response(conn, 200) =~ "yearly billing" @@ -39,7 +40,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "shows free subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "free_10k", user: user) + subscribe_to_plan(user, "free_10k") conn = get(conn, Routes.settings_path(conn, :subscription)) assert html_response(conn, 200) =~ "10k pageviews" assert html_response(conn, 200) =~ "N/A billing" @@ -47,8 +48,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "shows enterprise plan subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "123", user: user) - + subscribe_to_plan(user, "123") configure_enterprise_plan(user) conn = get(conn, Routes.settings_path(conn, :subscription)) @@ -61,20 +61,15 @@ defmodule PlausibleWeb.SettingsControllerTest do conn: conn, user: user } do - insert(:subscription, - paddle_plan_id: @configured_enterprise_plan_paddle_plan_id, - user: user - ) - - insert(:enterprise_plan, - paddle_plan_id: "1234", - user: user, - monthly_pageview_limit: 10_000_000, - billing_interval: :yearly - ) - configure_enterprise_plan(user) + subscribe_to_enterprise_plan(user, + paddle_plan_id: "1234", + monthly_pageview_limit: 10_000_000, + billing_interval: :yearly, + subscription?: false + ) + conn = get(conn, Routes.settings_path(conn, :subscription)) assert html_response(conn, 200) =~ "20M pageviews" assert html_response(conn, 200) =~ "yearly billing" @@ -102,7 +97,7 @@ defmodule PlausibleWeb.SettingsControllerTest do conn: conn, user: user } do - insert(:subscription, paddle_plan_id: @v3_plan_id, user: user) + subscribe_to_plan(user, @v3_plan_id) doc = conn @@ -161,7 +156,12 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "renders two links to '/billing/choose-plan' with the text 'Upgrade' for a configured enterprise plan", %{conn: conn, user: user} do - configure_enterprise_plan(user) + subscribe_to_enterprise_plan(user, + paddle_plan_id: @configured_enterprise_plan_paddle_plan_id, + monthly_pageview_limit: 20_000_000, + billing_interval: :yearly, + subscription?: false + ) doc = conn @@ -282,7 +282,7 @@ defmodule PlausibleWeb.SettingsControllerTest do conn: conn, user: user } do - site = insert(:site, members: [user]) + site = new_site(owner: user) populate_stats(site, [ build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)), @@ -293,12 +293,7 @@ defmodule PlausibleWeb.SettingsControllerTest do last_bill_date = Timex.shift(Timex.today(), days: -10) - insert(:subscription, - paddle_plan_id: @v4_plan_id, - user: user, - status: :deleted, - last_bill_date: last_bill_date - ) + subscribe_to_plan(user, @v4_plan_id, last_bill_date: last_bill_date, status: :deleted) html = conn @@ -358,14 +353,11 @@ defmodule PlausibleWeb.SettingsControllerTest do assert element_exists?(doc, "#total_pageviews_penultimate_cycle") end - # for an active subscription subscription = - insert(:subscription, - paddle_plan_id: @v4_plan_id, - user: user, + subscribe_to_plan(user, @v4_plan_id, status: :active, last_bill_date: Timex.shift(Timex.now(), months: -6) - ) + ).subscription get(conn, Routes.settings_path(conn, :subscription)) |> html_response(200) @@ -398,7 +390,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "penultimate cycle is disabled if there's no usage", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) populate_stats(site, [ build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)), @@ -407,11 +399,7 @@ defmodule PlausibleWeb.SettingsControllerTest do last_bill_date = Timex.shift(Timex.today(), days: -10) - insert(:subscription, - paddle_plan_id: @v4_plan_id, - user: user, - last_bill_date: last_bill_date - ) + subscribe_to_plan(user, @v4_plan_id, last_bill_date: last_bill_date) html = conn @@ -429,11 +417,7 @@ defmodule PlausibleWeb.SettingsControllerTest do conn: conn, user: user } do - insert(:subscription, - paddle_plan_id: @v4_plan_id, - user: user, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) + subscribe_to_plan(user, @v4_plan_id, last_bill_date: Timex.shift(Timex.today(), days: -1)) html = conn @@ -450,7 +434,7 @@ defmodule PlausibleWeb.SettingsControllerTest do conn: conn, user: user } do - site = insert(:site, members: [user]) + site = new_site(owner: user) populate_stats(site, [ build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -1)), @@ -474,15 +458,12 @@ defmodule PlausibleWeb.SettingsControllerTest do |> html_response(200) |> assert_usage.() - # for an expired subscription subscription = - insert(:subscription, - paddle_plan_id: @v4_plan_id, - user: user, + subscribe_to_plan(user, @v4_plan_id, status: :deleted, last_bill_date: ~D[2022-01-01], next_bill_date: ~D[2022-02-01] - ) + ).subscription conn |> get(Routes.settings_path(conn, :subscription)) @@ -514,8 +495,8 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "renders sites usage and limit", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: @v3_plan_id, user: user) - insert(:site, members: [user]) + subscribe_to_plan(user, @v3_plan_id) + new_site(owner: user) site_usage_row_text = conn @@ -541,7 +522,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "renders team member usage without limit if it's unlimited", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: @v3_plan_id, user: user) + subscribe_to_plan(user, @v3_plan_id) team_member_usage_row_text = conn @@ -570,11 +551,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "shows invoices for subscribed user", %{conn: conn, user: user} do - insert(:subscription, - paddle_plan_id: "558018", - paddle_subscription_id: "redundant", - user: user - ) + subscribe_to_plan(user, "558018") html = conn @@ -589,11 +566,7 @@ defmodule PlausibleWeb.SettingsControllerTest do @tag :ee_only test "shows message on failed invoice request'", %{conn: conn, user: user} do - insert(:subscription, - paddle_plan_id: "558018", - paddle_subscription_id: "invalid_subscription_id", - user: user - ) + subscribe_to_plan(user, "558018", paddle_subscription_id: "invalid_subscription_id") html = conn @@ -1125,9 +1098,8 @@ defmodule PlausibleWeb.SettingsControllerTest do end defp configure_enterprise_plan(user) do - insert(:enterprise_plan, + subscribe_to_enterprise_plan(user, paddle_plan_id: @configured_enterprise_plan_paddle_plan_id, - user: user, monthly_pageview_limit: 20_000_000, billing_interval: :yearly ) diff --git a/test/plausible_web/controllers/site/membership_controller_test.exs b/test/plausible_web/controllers/site/membership_controller_test.exs index ba4f761fa..fa531388e 100644 --- a/test/plausible_web/controllers/site/membership_controller_test.exs +++ b/test/plausible_web/controllers/site/membership_controller_test.exs @@ -13,7 +13,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do describe "GET /sites/:domain/memberships/invite" do test "shows invite form", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) html = conn @@ -27,11 +27,11 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do @tag :ee_only test "display a notice when is over limit", %{conn: conn, user: user} do - memberships = [ - build(:site_membership, user: user, role: :owner) | build_list(5, :site_membership) - ] + site = new_site(owner: user) - site = insert(:site, memberships: memberships) + for _ <- 1..5 do + add_guest(site, role: :viewer) + end html = conn @@ -44,7 +44,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do describe "POST /sites/:domain/memberships/invite" do test "creates invitation", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ @@ -60,11 +60,11 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do @tag :ee_only test "fails to create invitation when is over limit", %{conn: conn, user: user} do - memberships = [ - build(:site_membership, user: user, role: :owner) | build_list(5, :site_membership) - ] + site = new_site(owner: user) - site = insert(:site, memberships: memberships) + for _ <- 1..5 do + add_guest(site, role: :viewer) + end conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ @@ -110,7 +110,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do end test "sends invitation email for new user", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "john.doe@example.com", @@ -125,7 +125,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do test "sends invitation email for existing user", %{conn: conn, user: user} do existing_user = insert(:user) - site = insert(:site, members: [user]) + site = new_site(owner: user) post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: existing_user.email, @@ -139,14 +139,10 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do end test "renders form with error if the invitee is already a member", %{conn: conn, user: user} do - second_member = insert(:user) + site = new_site(owner: user) - memberships = [ - build(:site_membership, user: user, role: :owner), - build(:site_membership, user: second_member) - ] - - site = insert(:site, memberships: memberships) + second_member = new_user() + add_guest(site, user: second_member, role: :viewer) conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ @@ -162,7 +158,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do conn: conn, user: user } do - site = insert(:site, members: [user]) + site = new_site(owner: user) _req1 = post(conn, "/sites/#{site.domain}/memberships/invite", %{ @@ -202,7 +198,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do describe "GET /sites/:domain/transfer-ownership" do test "shows ownership transfer form", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) conn = get(conn, "/sites/#{site.domain}/transfer-ownership") @@ -212,7 +208,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do describe "POST /sites/:domain/transfer-ownership" do test "creates invitation with :owner role", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) conn = post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: "john.doe@example.com"}) @@ -224,7 +220,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do end test "sends ownership transfer email for new user", %{conn: conn, user: user} do - site = insert(:site, members: [user]) + site = new_site(owner: user) post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: "john.doe@example.com"}) @@ -236,7 +232,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do test "sends invitation email for existing user", %{conn: conn, user: user} do existing_user = insert(:user) - site = insert(:site, members: [user]) + site = new_site(owner: user) post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: existing_user.email}) @@ -262,7 +258,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do test "fails to transfer ownership to invited user with proper error message", ctx do %{conn: conn, user: user} = ctx - site = insert(:site, members: [user]) + site = new_site(owner: user) invited = "john.doe@example.com" # invite a user but don't join diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index d14798430..f8d9b857e 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -444,13 +444,7 @@ defmodule PlausibleWeb.SiteControllerTest do conn: conn, user: user } do - :site - |> insert( - domain: "example.com", - memberships: [ - build(:site_membership, user: user, role: :owner) - ] - ) + new_site(domain: "example.com", owner: user) |> Plausible.Site.Domain.change("new.example.com") conn = diff --git a/test/support/teams/test.ex b/test/support/teams/test.ex index a53ad8cea..bf44e2cab 100644 --- a/test/support/teams/test.ex +++ b/test/support/teams/test.ex @@ -149,11 +149,11 @@ defmodule Plausible.Teams.Test do user end - def subscribe_to_plan(user, paddle_plan_id) do + def subscribe_to_plan(user, paddle_plan_id, attrs \\ []) do {:ok, team} = Teams.get_or_create(user) - - insert(:subscription, user: user, team: team, paddle_plan_id: paddle_plan_id) - user + attrs = Keyword.merge([user: user, team: team, paddle_plan_id: paddle_plan_id], attrs) + subscription = insert(:subscription, attrs) + %{user | subscription: subscription} end def subscribe_to_enterprise_plan(user, attrs \\ []) do From e93c97de1edef2c4149816f09dd83921fa0351f0 Mon Sep 17 00:00:00 2001 From: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:59:23 +0100 Subject: [PATCH 07/13] migration: add scroll_depth to events_v2 (#4827) --- .../20241028142653_add_scroll_depth_to_events.exs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 priv/ingest_repo/migrations/20241028142653_add_scroll_depth_to_events.exs diff --git a/priv/ingest_repo/migrations/20241028142653_add_scroll_depth_to_events.exs b/priv/ingest_repo/migrations/20241028142653_add_scroll_depth_to_events.exs new file mode 100644 index 000000000..0904db362 --- /dev/null +++ b/priv/ingest_repo/migrations/20241028142653_add_scroll_depth_to_events.exs @@ -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 From 7f783920f364fd2a86171e844f02f705827d3124 Mon Sep 17 00:00:00 2001 From: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:38:34 +0100 Subject: [PATCH 08/13] Average Scroll Depth Metric: extracted ingestion changes (#4828) * migration: add scroll_depth to events_v2 * (cherry-pick) ingest scroll depth * replace convoluted test with more concise ones --- lib/plausible/clickhouse_event_v2.ex | 2 + lib/plausible/ingestion/event.ex | 3 +- lib/plausible/ingestion/request.ex | 17 ++++++ test/plausible/ingestion/request_test.exs | 3 +- .../api/external_controller_test.exs | 58 +++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/lib/plausible/clickhouse_event_v2.ex b/lib/plausible/clickhouse_event_v2.ex index eb5d309b9..e8a0141d6 100644 --- a/lib/plausible/clickhouse_event_v2.ex +++ b/lib/plausible/clickhouse_event_v2.ex @@ -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, diff --git a/lib/plausible/ingestion/event.ex b/lib/plausible/ingestion/event.ex index f94c81bc4..ad04a639a 100644 --- a/lib/plausible/ingestion/event.ex +++ b/lib/plausible/ingestion/event.ex @@ -246,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 diff --git a/lib/plausible/ingestion/request.ex b/lib/plausible/ingestion/request.ex index 407b4c032..7e48ae7e5 100644 --- a/lib/plausible/ingestion/request.ex +++ b/lib/plausible/ingestion/request.ex @@ -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) -> diff --git a/test/plausible/ingestion/request_test.exs b/test/plausible/ingestion/request_test.exs index cfe592938..23fd6c4d1 100644 --- a/test/plausible/ingestion/request_test.exs +++ b/test/plausible/ingestion/request_test.exs @@ -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"]) diff --git a/test/plausible_web/controllers/api/external_controller_test.exs b/test/plausible_web/controllers/api/external_controller_test.exs index a5244488e..f866df67e 100644 --- a/test/plausible_web/controllers/api/external_controller_test.exs +++ b/test/plausible_web/controllers/api/external_controller_test.exs @@ -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) From 7b49dd33c75e19a0dfacedfbd8a5d17c46cc40c0 Mon Sep 17 00:00:00 2001 From: hq1 Date: Wed, 20 Nov 2024 09:20:35 +0100 Subject: [PATCH 09/13] Remove `DebugReplayInfo` (#4842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have now better tools to debug queries 😈 --- lib/plausible/debug_replay_info.ex | 51 ----------------------- lib/plausible/stats.ex | 11 ----- test/plausible/debug_replay_info_test.exs | 49 ---------------------- 3 files changed, 111 deletions(-) delete mode 100644 lib/plausible/debug_replay_info.ex delete mode 100644 test/plausible/debug_replay_info_test.exs diff --git a/lib/plausible/debug_replay_info.ex b/lib/plausible/debug_replay_info.ex deleted file mode 100644 index ed1b90fbf..000000000 --- a/lib/plausible/debug_replay_info.ex +++ /dev/null @@ -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 diff --git a/lib/plausible/stats.ex b/lib/plausible/stats.ex index 8c3fac9a5..495c02049 100644 --- a/lib/plausible/stats.ex +++ b/lib/plausible/stats.ex @@ -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 diff --git a/test/plausible/debug_replay_info_test.exs b/test/plausible/debug_replay_info_test.exs deleted file mode 100644 index b0379aa15..000000000 --- a/test/plausible/debug_replay_info_test.exs +++ /dev/null @@ -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 From 380dc00d1a8a26acfa013efbd0384b0dd29cf39a Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 20 Nov 2024 09:35:18 +0100 Subject: [PATCH 10/13] Switch on teams schema in choose plan view behind FF (#4838) * Switch on teams schema in choose plan view behind FF * Proxy via Read adapter where applicable for billing context * Proxy remaining plan-related functions * Switch enterprise_configured?/1 tests to use the adapter * Format * Update SiteLocker tests * Actually use `has_active_subscription?/1` billing adapter --------- Co-authored-by: Adam Rutkowski --- lib/plausible/billing/billing.ex | 18 +-- lib/plausible/billing/plans.ex | 38 +++---- lib/plausible/billing/site_locker.ex | 5 +- lib/plausible/teams/adapter/read/billing.ex | 21 ++++ lib/plausible/teams/adapter/read/ownership.ex | 7 ++ lib/plausible/teams/billing.ex | 27 +++++ lib/plausible/teams/memberships.ex | 24 ++++ .../components/billing/billing.ex | 2 +- .../controllers/billing_controller.ex | 10 +- lib/plausible_web/live/choose_plan.ex | 20 ++-- .../templates/layout/_notice.html.heex | 4 +- test/plausible/auth/auth_test.exs | 18 ++- test/plausible/billing/billing_test.exs | 12 +- test/plausible/billing/plans_test.exs | 100 ++++++++--------- test/plausible/billing/site_locker_test.exs | 58 +++------- .../controllers/billing_controller_test.exs | 4 +- test/plausible_web/live/choose_plan_test.exs | 106 ++++++------------ .../workers/send_trial_notifications_test.exs | 6 +- 18 files changed, 251 insertions(+), 229 deletions(-) diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex index 9f5071e51..38ca3e111 100644 --- a/lib/plausible/billing/billing.ex +++ b/lib/plausible/billing/billing.ex @@ -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 = @@ -283,9 +283,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 ) diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex index 15f56e496..ace9f7510 100644 --- a/lib/plausible/billing/plans.ex +++ b/lib/plausible/billing/plans.ex @@ -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 diff --git a/lib/plausible/billing/site_locker.ex b/lib/plausible/billing/site_locker.ex index ea0d22df3..3db164050 100644 --- a/lib/plausible/billing/site_locker.ex +++ b/lib/plausible/billing/site_locker.ex @@ -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 diff --git a/lib/plausible/teams/adapter/read/billing.ex b/lib/plausible/teams/adapter/read/billing.ex index 9e38bf136..3c25186f2 100644 --- a/lib/plausible/teams/adapter/read/billing.ex +++ b/lib/plausible/teams/adapter/read/billing.ex @@ -4,6 +4,22 @@ defmodule Plausible.Teams.Adapter.Read.Billing do """ use Plausible.Teams.Adapter + 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 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 get_subscription(user) do case user_or_team(user) do %{subscription: subscription} -> subscription @@ -131,4 +147,9 @@ defmodule Plausible.Teams.Adapter.Read.Billing do 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 diff --git a/lib/plausible/teams/adapter/read/ownership.ex b/lib/plausible/teams/adapter/read/ownership.ex index 38b06b8e0..9801d326f 100644 --- a/lib/plausible/teams/adapter/read/ownership.ex +++ b/lib/plausible/teams/adapter/read/ownership.ex @@ -8,6 +8,13 @@ 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 -> diff --git a/lib/plausible/teams/billing.ex b/lib/plausible/teams/billing.ex index 17b6ebaa4..a9fe643c5 100644 --- a/lib/plausible/teams/billing.ex +++ b/lib/plausible/teams/billing.ex @@ -12,10 +12,28 @@ 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 enterprise_configured?(nil), do: false + + def enterprise_configured?(%Teams.Team{} = team) do + team + |> Ecto.assoc(:enterprise_plan) + |> Repo.exists?() + end + + def has_active_subscription?(nil), do: false + + def has_active_subscription?(team) do + team + |> active_subscription_query() + |> Repo.exists?() + end + def check_needs_to_upgrade(nil), do: {:needs_to_upgrade, :no_trial} def check_needs_to_upgrade(team) do @@ -350,4 +368,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 diff --git a/lib/plausible/teams/memberships.ex b/lib/plausible/teams/memberships.ex index 13927e483..d470fb0e9 100644 --- a/lib/plausible/teams/memberships.ex +++ b/lib/plausible/teams/memberships.ex @@ -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 diff --git a/lib/plausible_web/components/billing/billing.ex b/lib/plausible_web/components/billing/billing.ex index 6f51713bd..01b2b0a97 100644 --- a/lib/plausible_web/components/billing/billing.ex +++ b/lib/plausible_web/components/billing/billing.ex @@ -189,7 +189,7 @@ defmodule PlausibleWeb.Components.Billing do <.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" diff --git a/lib/plausible_web/controllers/billing_controller.ex b/lib/plausible_web/controllers/billing_controller.ex index cdc2c708f..a3160a20d 100644 --- a/lib/plausible_web/controllers/billing_controller.ex +++ b/lib/plausible_web/controllers/billing_controller.ex @@ -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", @@ -135,8 +137,8 @@ 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(user, new_plan_id) do + subscription = Billing.active_subscription_for(user) if subscription do with {:ok, preview_info} <- Billing.change_plan_preview(subscription, new_plan_id) do diff --git a/lib/plausible_web/live/choose_plan.ex b/lib/plausible_web/live/choose_plan.ex index 65b2d0098..3a8fd9f5f 100644 --- a/lib/plausible_web/live/choose_plan.ex +++ b/lib/plausible_web/live/choose_plan.ex @@ -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)} /> - - + +

diff --git a/lib/plausible_web/templates/layout/_notice.html.heex b/lib/plausible_web/templates/layout/_notice.html.heex index cb55e60f9..63854c29d 100644 --- a/lib/plausible_web/templates/layout/_notice.html.heex +++ b/lib/plausible_web/templates/layout/_notice.html.heex @@ -6,7 +6,9 @@

diff --git a/test/plausible/auth/auth_test.exs b/test/plausible/auth/auth_test.exs index 6b9dc27ca..68c783073 100644 --- a/test/plausible/auth/auth_test.exs +++ b/test/plausible/auth/auth_test.exs @@ -43,12 +43,20 @@ 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 diff --git a/test/plausible/billing/billing_test.exs b/test/plausible/billing/billing_test.exs index 206a4f33e..a96c61fc3 100644 --- a/test/plausible/billing/billing_test.exs +++ b/test/plausible/billing/billing_test.exs @@ -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 diff --git a/test/plausible/billing/plans_test.exs b/test/plausible/billing/plans_test.exs index f3e387d7d..b0576e2ed 100644 --- a/test/plausible/billing/plans_test.exs +++ b/test/plausible/billing/plans_test.exs @@ -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 diff --git a/test/plausible/billing/site_locker_test.exs b/test/plausible/billing/site_locker_test.exs index 7f98b4c14..677fe3856 100644 --- a/test/plausible/billing/site_locker_test.exs +++ b/test/plausible/billing/site_locker_test.exs @@ -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} diff --git a/test/plausible_web/controllers/billing_controller_test.exs b/test/plausible_web/controllers/billing_controller_test.exs index ab7b507e0..d140dfdd6 100644 --- a/test/plausible_web/controllers/billing_controller_test.exs +++ b/test/plausible_web/controllers/billing_controller_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.BillingControllerTest do use PlausibleWeb.ConnCase, async: true + use Plausible.Teams.Test import Plausible.Test.Support.HTML require Plausible.Billing.Subscription.Status alias Plausible.Billing.Subscription @@ -12,7 +13,8 @@ defmodule PlausibleWeb.BillingControllerTest do test "redirects to enterprise upgrade page if user has an enterprise plan configured", %{conn: conn, user: user} do - insert(:enterprise_plan, user: user, paddle_plan_id: "123") + subscribe_to_enterprise_plan(user, paddle_plan_id: "123") + conn = get(conn, Routes.billing_path(conn, :choose_plan)) assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan) end diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs index e8c31c7ae..afe179771 100644 --- a/test/plausible_web/live/choose_plan_test.exs +++ b/test/plausible_web/live/choose_plan_test.exs @@ -245,11 +245,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do user: user } do previous_owner = insert(:user) - site = insert(:site, members: [previous_owner]) + site = new_site(owner: previous_owner) insert(:goal, site: site, currency: :USD, event_name: "Purchase") - insert(:invitation, email: user.email, inviter: previous_owner, role: :owner, site: site) + invite_transfer(site, user, inviter: previous_owner) {:ok, _lv, doc} = get_liveview(conn) @@ -259,31 +259,16 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do test "recommends Business when team member limit for Growth exceeded due to pending ownerships", %{conn: conn, user: user} do - _owned_site = - insert(:site, - memberships: [ - build(:site_membership, role: :owner, user: user), - build(:site_membership, role: :admin, user: insert(:user)), - build(:site_membership, role: :admin, user: insert(:user)) - ] - ) + owned_site = new_site(owner: user) + add_guest(owned_site, role: :editor) + add_guest(owned_site, role: :editor) - previous_owner = insert(:user) + previous_owner = new_user() - pending_ownership_site = - insert(:site, - memberships: [ - build(:site_membership, role: :owner, user: previous_owner), - build(:site_membership, role: :viewer, user: insert(:user)) - ] - ) + pending_ownership_site = new_site(owner: previous_owner) + add_guest(pending_ownership_site, role: :viewer) - insert(:invitation, - email: user.email, - inviter: previous_owner, - role: :owner, - site: pending_ownership_site - ) + invite_transfer(pending_ownership_site, user, inviter: previous_owner) {:ok, _lv, doc} = get_liveview(conn) @@ -298,15 +283,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do insert_list(9, :site, members: [user]) assert 10 = Plausible.Billing.Quota.Usage.site_usage(user) - another_user = insert(:user) - pending_ownership_site = insert(:site, members: [another_user]) + another_user = new_user() + pending_ownership_site = new_site(owner: another_user) - insert(:invitation, - email: user.email, - site: pending_ownership_site, - role: :owner, - inviter: another_user - ) + invite_transfer(pending_ownership_site, user, inviter: another_user) {:ok, _lv, doc} = get_liveview(conn) @@ -463,28 +443,18 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do build(:pageview, timestamp: yesterday) ]) - another_user = insert(:user) + another_user = new_user() - pending_site = - insert(:site, - memberships: [ - build(:site_membership, role: :owner, user: another_user), - build(:site_membership, role: :admin, user: build(:user)), - build(:site_membership, role: :viewer, user: build(:user)), - build(:site_membership, role: :viewer, user: build(:user)) - ] - ) + pending_site = new_site(owner: another_user) + add_guest(pending_site, role: :editor) + add_guest(pending_site, role: :viewer) + add_guest(pending_site, role: :viewer) populate_stats(pending_site, [ build(:pageview, timestamp: yesterday) ]) - insert(:invitation, - site: pending_site, - inviter: another_user, - email: user.email, - role: :owner - ) + invite_transfer(pending_site, user, inviter: another_user) {:ok, _lv, doc} = get_liveview(conn) @@ -507,12 +477,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do Plausible.Props.allow(pending_site, ["author"]) - insert(:invitation, - site: pending_site, - inviter: another_user, - email: user.email, - role: :owner - ) + invite_transfer(pending_site, user, inviter: another_user) {:ok, _lv, doc} = get_liveview(conn) @@ -644,18 +609,16 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do conn: conn, user: user } do - insert_list(49, :site, members: [user]) + for _ <- 1..49 do + new_site(owner: user) + end + assert 50 = Plausible.Billing.Quota.Usage.site_usage(user) - another_user = insert(:user) - pending_ownership_site = insert(:site, members: [another_user]) + another_user = new_user() + pending_ownership_site = new_site(owner: another_user) - insert(:invitation, - email: user.email, - site: pending_ownership_site, - role: :owner, - inviter: another_user - ) + invite_transfer(pending_ownership_site, user, inviter: another_user) {:ok, _lv, doc} = get_liveview(conn) @@ -1102,9 +1065,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do conn: conn, user: user } do - old_owner = insert(:user) - site = insert(:site, members: [old_owner]) - insert(:invitation, site_id: site.id, inviter: old_owner, email: user.email, role: :owner) + old_owner = new_user() + site = new_site(owner: old_owner) + invite_transfer(site, user, inviter: old_owner) {:ok, _lv, doc} = get_liveview(conn) @@ -1146,15 +1109,14 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do ) end - defp create_subscription_for(user, subscription_options) do - insert(:subscription, Keyword.put(subscription_options, :user, user)) - {:ok, user: Plausible.Users.with_subscription(user)} + defp create_subscription_for(user, subscription_opts) do + {paddle_plan_id, subscription_opts} = Keyword.pop(subscription_opts, :paddle_plan_id) + user = subscribe_to_plan(user, paddle_plan_id, subscription_opts) + {:ok, user: user} end defp subscribe_free_10k(%{user: user}) do - Plausible.Billing.Subscription.free(%{user_id: user.id}) - |> Repo.insert!() - + user = subscribe_to_plan(user, "free_10k") {:ok, user: user} end diff --git a/test/workers/send_trial_notifications_test.exs b/test/workers/send_trial_notifications_test.exs index 9c9701ce1..bf111f9b5 100644 --- a/test/workers/send_trial_notifications_test.exs +++ b/test/workers/send_trial_notifications_test.exs @@ -2,6 +2,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do use Plausible.DataCase use Bamboo.Test use Oban.Testing, repo: Plausible.Repo + use Plausible.Teams.Test alias Plausible.Workers.SendTrialNotifications test "does not send a notification if user didn't create a site" do @@ -220,10 +221,9 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do end test "does not suggest a plan when user is switching to an enterprise plan" do - user = insert(:user) + user = new_user() usage = %{total: 10_000, custom_events: 0} - - insert(:enterprise_plan, user: user, paddle_plan_id: "enterprise-plan-id") + subscribe_to_enterprise_plan(user, paddle_plan_id: "enterprise-plan-id") email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage) assert email.html_body =~ "please reply back to this email to get a quote for your volume" From a29eb3d3cabdb60eb3841c4a64d2adc938d035e5 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Wed, 20 Nov 2024 11:10:54 +0100 Subject: [PATCH 11/13] Switch billing controller action to teams schema behind FF (#4843) * Switch upgrade to enterprise plan view to teams schema behind FF * Switch change plan preview action to teams schema behind FF * Switch remaining billing controller actions to teams schema behind FF --- lib/plausible/billing/billing.ex | 3 +- lib/plausible/teams/adapter/read/billing.ex | 21 ++++++++++ lib/plausible/teams/billing.ex | 39 +++++++++++++++++- .../controllers/billing_controller.ex | 24 +++++------ .../controllers/billing_controller_test.exs | 41 +++++++++---------- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex index 38ca3e111..89db62cf9 100644 --- a/lib/plausible/billing/billing.ex +++ b/lib/plausible/billing/billing.ex @@ -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 diff --git a/lib/plausible/teams/adapter/read/billing.ex b/lib/plausible/teams/adapter/read/billing.ex index 3c25186f2..1f33a0f70 100644 --- a/lib/plausible/teams/adapter/read/billing.ex +++ b/lib/plausible/teams/adapter/read/billing.ex @@ -4,6 +4,13 @@ defmodule Plausible.Teams.Adapter.Read.Billing do """ 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 @@ -13,6 +20,13 @@ defmodule Plausible.Teams.Adapter.Read.Billing do ) 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, @@ -20,6 +34,13 @@ defmodule Plausible.Teams.Adapter.Read.Billing do ) 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 diff --git a/lib/plausible/teams/billing.ex b/lib/plausible/teams/billing.ex index a9fe643c5..f2c773787 100644 --- a/lib/plausible/teams/billing.ex +++ b/lib/plausible/teams/billing.ex @@ -18,6 +18,24 @@ defmodule Plausible.Teams.Billing do @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 @@ -26,6 +44,19 @@ defmodule Plausible.Teams.Billing do |> 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 @@ -34,6 +65,12 @@ defmodule Plausible.Teams.Billing do |> 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 @@ -132,7 +169,7 @@ 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, []) diff --git a/lib/plausible_web/controllers/billing_controller.ex b/lib/plausible_web/controllers/billing_controller.ex index a3160a20d..630727bdc 100644 --- a/lib/plausible_web/controllers/billing_controller.ex +++ b/lib/plausible_web/controllers/billing_controller.ex @@ -30,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() ]) -> @@ -68,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), @@ -99,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") @@ -137,15 +139,11 @@ defmodule PlausibleWeb.BillingController do end end - defp preview_subscription(user, new_plan_id) do - subscription = Billing.active_subscription_for(user) + 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 diff --git a/test/plausible_web/controllers/billing_controller_test.exs b/test/plausible_web/controllers/billing_controller_test.exs index d140dfdd6..862f7f403 100644 --- a/test/plausible_web/controllers/billing_controller_test.exs +++ b/test/plausible_web/controllers/billing_controller_test.exs @@ -24,17 +24,13 @@ defmodule PlausibleWeb.BillingControllerTest do setup [:create_user, :log_in] test "errors if usage exceeds team member limit on the new plan", %{conn: conn, user: user} do - insert(:subscription, user: user, paddle_plan_id: "123123") + subscribe_to_plan(user, "123123") - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner), - build(:site_membership, user: build(:user)), - build(:site_membership, user: build(:user)), - build(:site_membership, user: build(:user)), - build(:site_membership, user: build(:user)) - ] - ) + site = new_site(owner: user) + + for _ <- 1..4 do + add_guest(site, role: :viewer) + end conn = post(conn, Routes.billing_path(conn, :change_plan, @v4_growth_plan)) @@ -46,9 +42,9 @@ defmodule PlausibleWeb.BillingControllerTest do conn: conn, user: user } do - insert(:subscription, user: user, paddle_plan_id: "123123") + subscribe_to_plan(user, "123123") - for _ <- 1..11, do: insert(:site, members: [user]) + for _ <- 1..11, do: new_site(owner: user) Plausible.Users.allow_next_upgrade_override(user) @@ -64,8 +60,8 @@ defmodule PlausibleWeb.BillingControllerTest do conn: conn, user: user } do - insert(:subscription, user: user, paddle_plan_id: "123123") - site = insert(:site, members: [user]) + subscribe_to_plan(user, "123123") + site = new_site(owner: user) now = NaiveDateTime.utc_now() generate_usage_for(site, 11_000, Timex.shift(now, days: -5)) @@ -91,7 +87,7 @@ defmodule PlausibleWeb.BillingControllerTest do end test "calls Paddle API to update subscription", %{conn: conn, user: user} do - insert(:subscription, user: user) + subscribe_to_plan(user, "321321") post(conn, Routes.billing_path(conn, :change_plan, "123123")) @@ -218,7 +214,7 @@ defmodule PlausibleWeb.BillingControllerTest do setup [:create_user, :log_in] test "renders preview information about the plan change", %{conn: conn, user: user} do - insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan) + subscribe_to_plan(user, @v4_growth_plan) html_response = conn @@ -339,29 +335,30 @@ defmodule PlausibleWeb.BillingControllerTest do end defp configure_enterprise_plan(%{user: user}) do - insert(:enterprise_plan, - user_id: user.id, + subscribe_to_enterprise_plan(user, paddle_plan_id: "123", billing_interval: :yearly, monthly_pageview_limit: 50_000_000, site_limit: 20_000, hourly_api_request_limit: 5000, - inserted_at: Timex.now() |> Timex.shift(hours: 1) + inserted_at: NaiveDateTime.utc_now() |> NaiveDateTime.shift(hour: 1), + subscription?: false ) :ok end defp subscribe_enterprise(%{user: user}, opts \\ []) do + {paddle_plan_id, opts} = Keyword.pop(opts, :paddle_plan_id, "321") + opts = opts |> Keyword.put(:user, user) - |> Keyword.put_new(:paddle_plan_id, "321") |> Keyword.put_new(:status, Subscription.Status.active()) - insert(:subscription, opts) + user = subscribe_to_plan(user, paddle_plan_id, opts) - {:ok, user: Plausible.Users.with_subscription(user)} + {:ok, user: user} end defp get_paddle_checkout_params(element) do From 6822b290168742be49ce64fadb4e6fcade6fbb9b Mon Sep 17 00:00:00 2001 From: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:13:04 +0100 Subject: [PATCH 12/13] Average Scroll Depth Metric: put scroll depth on the dashboard under a feature flag (#4832) * migration: add scroll_depth to events_v2 * (cherry-pick) ingest scroll depth * replace convoluted test with more concise ones * QueryParser: parse internal scroll_depth metric + validation * turn QueryComparisonsTest into QueryInternalTest * rename file * (cherry pick) query scroll depth 15b14d3 ...and move the tests into `internal_query_test.exs` * review feedback * Get rid of unnecessary separation between aggregate and group scroll depth * Drop irrelevant other metrics in tests * add test ensuring scroll depth unavailable in Stats API v1 * Put scroll depth on the dashboard * Top Stats * Main Graph * Top Pages > Details * feature flag for dashboard scroll depth access * ignore credo warning * enable scroll_depth flag in tests * remove duplication * write timestamps explicitly in a test * revert moving tests around * Add query_comparisons_test back * Move scroll_depth tests into query_test * Delete query_internal_test * rename setup util (got updated on master) * use pageleave_factory where applicable * Use the correct generated query-api.d.ts * npm format --- assets/js/dashboard/stats/graph/graph-util.js | 3 +- assets/js/dashboard/stats/modals/pages.js | 4 +- .../stats/reports/metric-formatter.ts | 2 + assets/js/dashboard/stats/reports/metrics.js | 15 +- assets/js/types/query-api.d.ts | 3 +- lib/plausible/stats/filters/query_parser.ex | 11 + lib/plausible/stats/metrics.ex | 3 +- lib/plausible/stats/sql/expression.ex | 1 + lib/plausible/stats/sql/query_builder.ex | 2 +- lib/plausible/stats/sql/special_metrics.ex | 50 +++ lib/plausible/stats/table_decider.ex | 1 + lib/plausible/stats/timeseries.ex | 1 + .../controllers/api/stats_controller.ex | 63 +++- .../controllers/stats_controller.ex | 2 +- priv/json-schemas/query-api-schema.json | 4 + test/plausible/billing/quota_test.exs | 2 +- .../plausible/stats/goal_suggestions_test.exs | 2 +- test/plausible/stats/query_parser_test.exs | 75 +++++ .../aggregate_test.exs | 21 +- .../breakdown_test.exs | 7 +- .../external_stats_controller/query_test.exs | 298 +++++++++++++++++- .../api/stats_controller/main_graph_test.exs | 50 +++ .../api/stats_controller/pages_test.exs | 125 ++++++-- .../api/stats_controller/top_stats_test.exs | 28 +- test/support/factory.ex | 4 + test/test_helper.exs | 1 + 26 files changed, 711 insertions(+), 67 deletions(-) diff --git a/assets/js/dashboard/stats/graph/graph-util.js b/assets/js/dashboard/stats/graph/graph-util.js index a8029845a..ee5b6a83d 100644 --- a/assets/js/dashboard/stats/graph/graph-util.js +++ b/assets/js/dashboard/stats/graph/graph-util.js @@ -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"] } diff --git a/assets/js/dashboard/stats/modals/pages.js b/assets/js/dashboard/stats/modals/pages.js index 533880d5a..26bf287c6 100644 --- a/assets/js/dashboard/stats/modals/pages.js +++ b/assets/js/dashboard/stats/modals/pages.js @@ -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 ( diff --git a/assets/js/dashboard/stats/reports/metric-formatter.ts b/assets/js/dashboard/stats/reports/metric-formatter.ts index 60e011dc2..a9c5413d8 100644 --- a/assets/js/dashboard/stats/reports/metric-formatter.ts +++ b/assets/js/dashboard/stats/reports/metric-formatter.ts @@ -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, diff --git a/assets/js/dashboard/stats/reports/metrics.js b/assets/js/dashboard/stats/reports/metrics.js index 5ea1a227e..a8fc6609d 100644 --- a/assets/js/dashboard/stats/reports/metrics.js +++ b/assets/js/dashboard/stats/reports/metrics.js @@ -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 + }) +} diff --git a/assets/js/types/query-api.d.ts b/assets/js/types/query-api.d.ts index c33eae9c8..06121a816 100644 --- a/assets/js/types/query-api.d.ts +++ b/assets/js/types/query-api.d.ts @@ -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 diff --git a/lib/plausible/stats/filters/query_parser.ex b/lib/plausible/stats/filters/query_parser.ex index 8b4a7445a..b7410964b 100644 --- a/lib/plausible/stats/filters/query_parser.ex +++ b/lib/plausible/stats/filters/query_parser.ex @@ -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") -> diff --git a/lib/plausible/stats/metrics.ex b/lib/plausible/stats/metrics.ex index 29319d07a..2b11dcd65 100644 --- a/lib/plausible/stats/metrics.ex +++ b/lib/plausible/stats/metrics.ex @@ -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) diff --git a/lib/plausible/stats/sql/expression.ex b/lib/plausible/stats/sql/expression.ex index 50abda9c5..34311dcb7 100644 --- a/lib/plausible/stats/sql/expression.ex +++ b/lib/plausible/stats/sql/expression.ex @@ -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: %{} diff --git a/lib/plausible/stats/sql/query_builder.ex b/lib/plausible/stats/sql/query_builder.ex index 0a4e42409..5aedeb5f7 100644 --- a/lib/plausible/stats/sql/query_builder.ex +++ b/lib/plausible/stats/sql/query_builder.ex @@ -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 diff --git a/lib/plausible/stats/sql/special_metrics.ex b/lib/plausible/stats/sql/special_metrics.ex index 8f18ae24c..1bf77d50b 100644 --- a/lib/plausible/stats/sql/special_metrics.ex +++ b/lib/plausible/stats/sql/special_metrics.ex @@ -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. diff --git a/lib/plausible/stats/table_decider.ex b/lib/plausible/stats/table_decider.ex index 5af91af42..6820f7c60 100644 --- a/lib/plausible/stats/table_decider.ex +++ b/lib/plausible/stats/table_decider.ex @@ -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 diff --git a/lib/plausible/stats/timeseries.ex b/lib/plausible/stats/timeseries.ex index 6fc8ac89d..1b45c9548 100644 --- a/lib/plausible/stats/timeseries.ex +++ b/lib/plausible/stats/timeseries.ex @@ -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}) diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 92345e536..89b73d5ca 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -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 diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index df89b56cc..c040f5c6a 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -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) diff --git a/priv/json-schemas/query-api-schema.json b/priv/json-schemas/query-api-schema.json index 69bcbce97..e715c5957 100644 --- a/priv/json-schemas/query-api-schema.json +++ b/priv/json-schemas/query-api-schema.json @@ -269,6 +269,10 @@ { "const": "average_revenue", "$comment": "only :internal" + }, + { + "const": "scroll_depth", + "$comment": "only :internal" } ] }, diff --git a/test/plausible/billing/quota_test.exs b/test/plausible/billing/quota_test.exs index e0190e303..aaae39cd8 100644 --- a/test/plausible/billing/quota_test.exs +++ b/test/plausible/billing/quota_test.exs @@ -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 %{ diff --git a/test/plausible/stats/goal_suggestions_test.exs b/test/plausible/stats/goal_suggestions_test.exs index 21d379f65..0f280f9e0 100644 --- a/test/plausible/stats/goal_suggestions_test.exs +++ b/test/plausible/stats/goal_suggestions_test.exs @@ -63,7 +63,7 @@ defmodule Plausible.Stats.GoalSuggestionsTest do user_id: 1, timestamp: NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :minute) ), - build(:event, name: "pageleave", user_id: 1, timestamp: NaiveDateTime.utc_now()) + build(:pageleave, user_id: 1, timestamp: NaiveDateTime.utc_now()) ]) assert GoalSuggestions.suggest_event_names(site, "") == ["Signup"] diff --git a/test/plausible/stats/query_parser_test.exs b/test/plausible/stats/query_parser_test.exs index 5da17b28f..53e998d23 100644 --- a/test/plausible/stats/query_parser_test.exs +++ b/test/plausible/stats/query_parser_test.exs @@ -1416,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"}) diff --git a/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs b/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs index e81a7d1d3..8e9c779e4 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs @@ -126,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, @@ -1628,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 = diff --git a/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs b/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs index 7b0fef62c..f6ebe7166 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs @@ -2604,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 = diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_test.exs index 0a33e8df6..087004aa7 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_test.exs @@ -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 diff --git a/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs b/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs index 1b2353c76..b1ecaca80 100644 --- a/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/main_graph_test.exs @@ -584,6 +584,56 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do end end + describe "GET /api/stats/main-graph - scroll_depth plot" do + setup [:create_user, :log_in, :create_site] + + test "returns 400 when scroll_depth is queried without a page filter", %{ + conn: conn, + site: site + } do + conn = + get( + conn, + "/api/stats/#{site.domain}/main-graph?period=month&date=2021-01-01&metric=scroll_depth" + ) + + assert %{"error" => error} = json_response(conn, 400) + assert error =~ "can only be queried with a page filter" + end + + test "returns scroll depth per day", %{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 + ) + ]) + + filters = Jason.encode!(%{page: "/"}) + + conn = + get( + conn, + "/api/stats/#{site.domain}/main-graph?period=7d&date=2020-01-07&metric=scroll_depth&filters=#{filters}" + ) + + assert %{"plot" => plot} = json_response(conn, 200) + + assert plot == [40, 20, 0, 0, 0, 0, 0] + end + end + describe "GET /api/stats/main-graph - conversion_rate plot" do setup [:create_user, :log_in, :create_site] diff --git a/test/plausible_web/controllers/api/stats_controller/pages_test.exs b/test/plausible_web/controllers/api/stats_controller/pages_test.exs index 498132f41..c1a5aff85 100644 --- a/test/plausible_web/controllers/api/stats_controller/pages_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/pages_test.exs @@ -272,14 +272,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 2, "bounce_rate" => 0, - "time_on_page" => 600 + "time_on_page" => 600, + "scroll_depth" => 0 }, %{ "name" => "/blog/john-1", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 } ] end @@ -334,14 +336,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 2, "bounce_rate" => 0, - "time_on_page" => 120.0 + "time_on_page" => 120.0, + "scroll_depth" => 0 }, %{ "name" => "/blog/other-post", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 } ] end @@ -386,14 +390,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 2, "bounce_rate" => 50, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 }, %{ "name" => "/blog/other-post", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 } ] end @@ -442,14 +448,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 2, "bounce_rate" => 100, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 }, %{ "name" => "/blog/john-1", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 } ] end @@ -584,7 +592,49 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 3, "bounce_rate" => 50, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 + } + ] + end + + test "calculates scroll_depth", %{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 = get(conn, "/api/stats/#{site.domain}/pages?period=day&date=2020-01-01&detailed=true") + + assert json_response(conn, 200)["results"] == [ + %{ + "name" => "/blog", + "visitors" => 3, + "pageviews" => 4, + "bounce_rate" => 33, + "time_on_page" => 60, + "scroll_depth" => 60 + }, + %{ + "name" => "/another", + "visitors" => 2, + "pageviews" => 2, + "bounce_rate" => 0, + "time_on_page" => 60, + "scroll_depth" => 25 } ] end @@ -631,14 +681,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 3, "bounce_rate" => 50, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 }, %{ "name" => "/about", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 100, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 } ] end @@ -685,7 +737,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 3, "bounce_rate" => 50, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 } ] end @@ -731,21 +784,24 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 2, "bounce_rate" => 100, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 }, %{ "name" => "/blog/post-1", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 }, %{ "name" => "/blog/post-2", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 } ] end @@ -783,14 +839,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => 60 + "time_on_page" => 60, + "scroll_depth" => 0 }, %{ "name" => "/blog/(/post-2", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 } ] end @@ -836,14 +894,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "visitors" => 2, "pageviews" => 2, "bounce_rate" => 50, - "time_on_page" => 600 + "time_on_page" => 600, + "scroll_depth" => 0 }, %{ "name" => "/about", "visitors" => 1, "pageviews" => 1, "bounce_rate" => 0, - "time_on_page" => nil + "time_on_page" => nil, + "scroll_depth" => 0 } ] end @@ -937,14 +997,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "time_on_page" => 900.0, "visitors" => 2, "pageviews" => 2, - "name" => "/" + "name" => "/", + "scroll_depth" => 0 }, %{ "bounce_rate" => 0, "time_on_page" => nil, "visitors" => 1, "pageviews" => 1, - "name" => "/some-other-page" + "name" => "/some-other-page", + "scroll_depth" => 0 } ] end @@ -984,7 +1046,8 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "name" => "/about", "pageviews" => 2, "time_on_page" => nil, - "visitors" => 2 + "visitors" => 2, + "scroll_depth" => 0 } ] end @@ -1063,14 +1126,16 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "name" => "/about-blog", "pageviews" => 3, "time_on_page" => 1140.0, - "visitors" => 2 + "visitors" => 2, + "scroll_depth" => 0 }, %{ "bounce_rate" => 0, "name" => "/exit-blog", "pageviews" => 1, "time_on_page" => nil, - "visitors" => 1 + "visitors" => 1, + "scroll_depth" => 0 } ] end @@ -1418,17 +1483,20 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "pageviews" => 0, "time_on_page" => 0, "visitors" => 0, + "scroll_depth" => 0, "change" => %{ "bounce_rate" => nil, "pageviews" => 100, "time_on_page" => nil, - "visitors" => 100 + "visitors" => 100, + "scroll_depth" => 0 } }, "name" => "/page2", "pageviews" => 2, "time_on_page" => nil, - "visitors" => 2 + "visitors" => 2, + "scroll_depth" => 0 }, %{ "bounce_rate" => 100, @@ -1436,16 +1504,19 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do "pageviews" => 1, "time_on_page" => nil, "visitors" => 1, + "scroll_depth" => 0, "comparison" => %{ "bounce_rate" => 100, "pageviews" => 1, "time_on_page" => nil, "visitors" => 1, + "scroll_depth" => 0, "change" => %{ "bounce_rate" => 0, "pageviews" => 0, "time_on_page" => nil, - "visitors" => 0 + "visitors" => 0, + "scroll_depth" => 0 } } } diff --git a/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs b/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs index d7b2cd5cb..438373b8a 100644 --- a/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs +++ b/test/plausible_web/controllers/api/stats_controller/top_stats_test.exs @@ -892,7 +892,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do } do filters = Jason.encode!(%{page: "/A"}) - [visitors, visits, pageviews, bounce_rate, time_on_page] = + [visitors, visits, pageviews, bounce_rate, time_on_page, scroll_depth] = conn |> get("/api/stats/#{site.domain}/top-stats?filters=#{filters}") |> json_response(200) @@ -903,6 +903,7 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do assert %{"graph_metric" => "pageviews"} = pageviews assert %{"graph_metric" => "bounce_rate"} = bounce_rate assert %{"graph_metric" => "time_on_page"} = time_on_page + assert %{"graph_metric" => "scroll_depth"} = scroll_depth end test "returns graph_metric key for top stats with a goal filter", %{ @@ -960,6 +961,31 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do ] end + test "returns scroll_depth 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) + ]) + + filters = Jason.encode!(%{page: "/"}) + + conn = + get( + conn, + "/api/stats/#{site.domain}/top-stats?period=day&date=2021-01-01&filters=#{filters}" + ) + + res = json_response(conn, 200) + + assert %{"name" => "Scroll depth", "value" => 70, "graph_metric" => "scroll_depth"} in res[ + "top_stats" + ] + end + test "page glob filter", %{conn: conn, site: site} do populate_stats(site, [ build(:pageview, pathname: "/index"), diff --git a/test/support/factory.ex b/test/support/factory.ex index 2e2158412..62de00560 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -140,6 +140,10 @@ defmodule Plausible.Factory do Map.put(event_factory(attrs), :name, "pageview") end + def pageleave_factory(attrs) do + Map.put(event_factory(attrs), :name, "pageleave") + end + def event_factory(attrs) do if Map.get(attrs, :acquisition_channel) do raise "Acquisition channel cannot be written directly since it's a materialized column." diff --git a/test/test_helper.exs b/test/test_helper.exs index aec1fc2a1..87d50bcf4 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -7,6 +7,7 @@ Mox.defmock(Plausible.HTTPClient.Mock, for: Plausible.HTTPClient.Interface) Application.ensure_all_started(:double) FunWithFlags.enable(:channels) +FunWithFlags.enable(:scroll_depth) # Temporary flag to test `read_team_schemas` flag on all tests. if System.get_env("TEST_READ_TEAM_SCHEMAS") == "1" do IO.puts("READS TEAM SCHEMAS") From 5072613c4f7e9c745ca86c4378b210a7f2983f55 Mon Sep 17 00:00:00 2001 From: Marko Saric <34340819+metmarkosaric@users.noreply.github.com> Date: Thu, 21 Nov 2024 06:28:28 +0100 Subject: [PATCH 13/13] Change the copy when switching a plan (#4796) * Update plan_box.ex * Fix test assertions --------- Co-authored-by: Adam Rutkowski --- lib/plausible_web/components/billing/plan_box.ex | 2 +- test/plausible_web/live/choose_plan_test.exs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/plausible_web/components/billing/plan_box.ex b/lib/plausible_web/components/billing/plan_box.ex index e1a90414a..c389f6b1e 100644 --- a/lib/plausible_web/components/billing/plan_box.ex +++ b/lib/plausible_web/components/billing/plan_box.ex @@ -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 diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs index afe179771..808937578 100644 --- a/test/plausible_web/live/choose_plan_test.exs +++ b/test/plausible_web/live/choose_plan_test.exs @@ -221,7 +221,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do {:ok, _lv, doc} = get_liveview(conn) assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ - "if (confirm(\"This plan does not support Custom Properties, which you are currently using. Please note that by subscribing to this plan you will lose access to this feature.\")) {Paddle.Checkout.open" + "if (confirm(\"This plan does not support Custom Properties, which you have been using. By subscribing to this plan, you will not have access to this feature.\")) {Paddle.Checkout.open" end test "recommends Growth tier when no premium features were used", %{conn: conn} do @@ -484,7 +484,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert doc =~ "Your account has been invited to become the owner of a site" assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ - "if (confirm(\"This plan does not support Custom Properties, which you are currently using. Please note that by subscribing to this plan you will lose access to this feature.\")) {window.location = " + "if (confirm(\"This plan does not support Custom Properties, which you have been using. By subscribing to this plan, you will not have access to this feature.\")) {window.location =" assert text_of_element(doc, @business_highlight_pill) == "Recommended" refute element_exists?(doc, @growth_highlight_pill) @@ -740,7 +740,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do {:ok, _lv, doc} = get_liveview(conn) assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ - "if (confirm(\"This plan does not support Custom Properties, Revenue Goals and Stats API, which you are currently using. Please note that by subscribing to this plan you will lose access to these features.\")) {window.location = " + "if (confirm(\"This plan does not support Custom Properties, Revenue Goals and Stats API, which you have been using. By subscribing to this plan, you will not have access to these features.\")) {window.location = " end end