From 714c0367570727bbebaf79ab5cf8dd7f680b78cd Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Mon, 30 Sep 2024 13:03:08 +0200 Subject: [PATCH 01/18] Implement listing user sessions in user settings (#4588) * Implement listing user sessions in user settings * Make copy adjustments (h/t @metmarkosaric) * Make warning button text color more consistent across user settings * Add tests for `UserAuth.revoke_user_session/2` * Test and improve `Auth.UserSessions` * Test and improve controller actions * Update CHANGELOG.md --- CHANGELOG.md | 10 +++ lib/plausible/auth/user_sessions.ex | 33 ++++++++ .../controllers/auth_controller.ex | 13 ++++ lib/plausible_web/router.ex | 1 + .../templates/auth/user_settings.html.heex | 77 ++++++++++++++++++- lib/plausible_web/user_auth.ex | 20 +++++ test/plausible/auth/user_sessions_test.exs | 64 +++++++++++++++ .../controllers/auth_controller_test.exs | 53 +++++++++++++ test/plausible_web/user_auth_test.exs | 43 +++++++++++ 9 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 lib/plausible/auth/user_sessions.ex create mode 100644 test/plausible/auth/user_sessions_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a93b1fa2bf..6f061a0ab650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. ## Unreleased +### Added + +- Add ability to review and revoke particular logged in user sessions + +### Removed + +### Changed + +### Fixed + ## v2.1.3 - 2024-09-26 ### Fixed diff --git a/lib/plausible/auth/user_sessions.ex b/lib/plausible/auth/user_sessions.ex new file mode 100644 index 000000000000..821f94a40e00 --- /dev/null +++ b/lib/plausible/auth/user_sessions.ex @@ -0,0 +1,33 @@ +defmodule Plausible.Auth.UserSessions do + @moduledoc """ + Functions for interacting with user sessions. + """ + + import Ecto.Query, only: [from: 2] + alias Plausible.Auth + alias Plausible.Repo + + @spec list_for_user(Auth.User.t(), NaiveDateTime.t()) :: [Auth.UserSession.t()] + def list_for_user(user, now \\ NaiveDateTime.utc_now(:second)) do + Repo.all( + from us in Auth.UserSession, + where: us.user_id == ^user.id, + where: us.timeout_at >= ^now, + order_by: [desc: us.last_used_at, desc: us.id] + ) + end + + @spec last_used_humanize(Auth.UserSession.t(), NaiveDateTime.t()) :: String.t() + def last_used_humanize(user_session, now \\ NaiveDateTime.utc_now(:second)) do + diff = NaiveDateTime.diff(now, user_session.last_used_at, :hour) + diff_days = NaiveDateTime.diff(now, user_session.last_used_at, :day) + + cond do + diff < 1 -> "Just recently" + diff == 1 -> "1 hour ago" + diff < 24 -> "#{diff} hours ago" + diff < 2 * 24 -> "Yesterday" + true -> "#{diff_days} days ago" + end + end +end diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index 3c9d296d3b9b..2226be3a48d3 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -33,6 +33,7 @@ defmodule PlausibleWeb.AuthController do :new_api_key, :create_api_key, :delete_api_key, + :delete_session, :delete_me, :activate_form, :activate, @@ -528,9 +529,11 @@ defmodule PlausibleWeb.AuthController do settings_changeset = Keyword.fetch!(opts, :settings_changeset) email_changeset = Keyword.fetch!(opts, :email_changeset) api_keys = Repo.preload(current_user, :api_keys).api_keys + user_sessions = Auth.UserSessions.list_for_user(current_user) render(conn, "user_settings.html", api_keys: api_keys, + user_sessions: user_sessions, settings_changeset: settings_changeset, email_changeset: email_changeset, subscription: current_user.subscription, @@ -584,6 +587,16 @@ defmodule PlausibleWeb.AuthController do logout(conn, params) end + def delete_session(conn, %{"id" => session_id}) do + current_user = conn.assigns.current_user + + :ok = UserAuth.revoke_user_session(current_user, session_id) + + conn + |> put_flash(:success, "Session logged out successfully") + |> redirect(to: "/settings#user-sessions") + end + def logout(conn, params) do redirect_to = Map.get(params, "redirect", "/") diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 633c8c85ed79..fe6ebbe3634a 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -334,6 +334,7 @@ defmodule PlausibleWeb.Router do get "/settings/api-keys/new", AuthController, :new_api_key post "/settings/api-keys", AuthController, :create_api_key delete "/settings/api-keys/:id", AuthController, :delete_api_key + delete "/settings/user-sessions/:id", AuthController, :delete_session get "/auth/google/callback", AuthController, :google_auth_callback diff --git a/lib/plausible_web/templates/auth/user_settings.html.heex b/lib/plausible_web/templates/auth/user_settings.html.heex index 636d75fd67e8..d631f4631bca 100644 --- a/lib/plausible_web/templates/auth/user_settings.html.heex +++ b/lib/plausible_web/templates/auth/user_settings.html.heex @@ -313,11 +313,82 @@ <%= submit("Change my email", class: - "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150" + "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-600 dark:text-red-500 bg-white dark:bg-gray-800 hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150" ) %> <% end %> +
+

Login Management

+ +
+ +

+ Log out of your account on other devices. Note that logged-in sessions automatically expire after 14 days of inactivity. +

+ +
+
+
+ <%= if Enum.any?(@user_sessions) do %> +
+ + + + + + + + + + <%= for session <- @user_sessions do %> + + + + + + <% end %> + +
+ Device + + Last seen + + Log Out +
+ <%= session.device %> + + <%= Plausible.Auth.UserSessions.last_used_humanize(session) %> + + <%= if @current_user_session.id == session.id do %> + + Current Session + + <% else %> + <%= button("Log Out", + to: "/settings/user-sessions/#{session.id}", + class: "text-red-600 hover:text-red-400 dark:text-red-500", + method: :delete, + "data-confirm": "Are you sure you want to log out this session?" + ) %> + <% end %> +
+
+ <% end %> +
+
+
+
+
<%= button("Revoke", to: "/settings/api-keys/#{api_key.id}", - class: "text-red-600 hover:text-red-900", + class: "text-red-600 hover:text-red-400 dark:text-red-500", method: :delete, "data-confirm": "Are you sure you want to revoke this key? This action cannot be reversed." @@ -415,7 +486,7 @@ <%= link("Delete my account", to: "/me", class: - "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", + "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 dark:text-red-500 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete", data: [ confirm: diff --git a/lib/plausible_web/user_auth.ex b/lib/plausible_web/user_auth.ex index 02cec2c645c8..e7fbf8e2e2dd 100644 --- a/lib/plausible_web/user_auth.ex +++ b/lib/plausible_web/user_auth.ex @@ -65,6 +65,26 @@ defmodule PlausibleWeb.UserAuth do end end + @spec revoke_user_session(Auth.User.t(), pos_integer()) :: :ok + def revoke_user_session(user, session_id) do + {_, tokens} = + Repo.delete_all( + from us in Auth.UserSession, + where: us.user_id == ^user.id and us.id == ^session_id, + select: us.token + ) + + case tokens do + [token] -> + PlausibleWeb.Endpoint.broadcast(live_socket_id(token), "disconnect", %{}) + + _ -> + :pass + end + + :ok + end + @spec revoke_all_user_sessions(Auth.User.t()) :: :ok def revoke_all_user_sessions(user) do {_count, tokens} = diff --git a/test/plausible/auth/user_sessions_test.exs b/test/plausible/auth/user_sessions_test.exs new file mode 100644 index 000000000000..673d2e795ffd --- /dev/null +++ b/test/plausible/auth/user_sessions_test.exs @@ -0,0 +1,64 @@ +defmodule Plausible.Auth.UserSessionsTest do + use Plausible.DataCase, async: true + + alias Plausible.Auth + alias Plausible.Auth.UserSessions + alias Plausible.Repo + + describe "list_for_user/2" do + test "lists user sessions" do + user = insert(:user) + + now = NaiveDateTime.utc_now(:second) + thirty_minutes_ago = NaiveDateTime.shift(now, minute: -30) + ten_hours_ago = NaiveDateTime.shift(now, hour: -10) + ten_days_ago = NaiveDateTime.shift(now, day: -10) + twenty_days_ago = NaiveDateTime.shift(now, day: -20) + + recent_session = insert_session(user, "Recent Device", thirty_minutes_ago) + old_session = insert_session(user, "Old Device", ten_hours_ago) + older_session = insert_session(user, "Older Device", ten_days_ago) + _expired_session = insert_session(user, "Expired Device", twenty_days_ago) + _rogue_session = insert_session(insert(:user), "Unrelated device", now) + + assert [session1, session2, session3] = UserSessions.list_for_user(user, now) + + assert session1.id == recent_session.id + assert session2.id == old_session.id + assert session3.id == older_session.id + end + end + + describe "last_used_humanize/2" do + test "returns humanized relative time" do + user = insert(:user) + now = NaiveDateTime.utc_now(:second) + thirty_minutes_ago = NaiveDateTime.shift(now, minute: -30) + ninety_minutes_ago = NaiveDateTime.shift(now, minute: -90) + ten_hours_ago = NaiveDateTime.shift(now, hour: -10) + twenty_seven_hours_ago = NaiveDateTime.shift(now, hour: -27) + fifty_hours_ago = NaiveDateTime.shift(now, hour: -50) + ten_days_ago = NaiveDateTime.shift(now, day: -10) + + assert last_used_humanize(user, now) == "Just recently" + assert last_used_humanize(user, thirty_minutes_ago) == "Just recently" + assert last_used_humanize(user, ninety_minutes_ago) == "1 hour ago" + assert last_used_humanize(user, ten_hours_ago) == "10 hours ago" + assert last_used_humanize(user, twenty_seven_hours_ago) == "Yesterday" + assert last_used_humanize(user, fifty_hours_ago) == "2 days ago" + assert last_used_humanize(user, ten_days_ago) == "10 days ago" + end + end + + defp last_used_humanize(user, dt) do + user + |> insert_session("Some Device", dt) + |> UserSessions.last_used_humanize() + end + + defp insert_session(user, device_name, now) do + user + |> Auth.UserSession.new_session(device_name, now) + |> Repo.insert!() + end +end diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs index 2b1a6d02fcab..19e749c78fa5 100644 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -1100,6 +1100,27 @@ defmodule PlausibleWeb.AuthControllerTest do assert html_response(conn, 200) =~ "Disable 2FA" end + + test "renders active user sessions with an option to revoke them", %{conn: conn, user: user} do + now = NaiveDateTime.utc_now(:second) + seventy_minutes_ago = NaiveDateTime.shift(now, minute: -70) + + another_session = + user + |> Auth.UserSession.new_session("Some Device", seventy_minutes_ago) + |> Repo.insert!() + + conn = get(conn, "/settings") + + assert html = html_response(conn, 200) + + assert html =~ "Unknown" + assert html =~ "Current Session" + assert html =~ "Just recently" + assert html =~ "Some Device" + assert html =~ "1 hour ago" + assert html =~ Routes.auth_path(conn, :delete_session, another_session.id) + end end describe "PUT /settings" do @@ -1451,6 +1472,38 @@ defmodule PlausibleWeb.AuthControllerTest do end end + describe "DELETE /settings/user-sessions/:id" do + setup [:create_user, :log_in] + + test "deletes session", %{conn: conn, user: user} do + another_session = + user + |> Auth.UserSession.new_session("Some Device") + |> Repo.insert!() + + conn = delete(conn, "/settings/user-sessions/#{another_session.id}") + + assert Phoenix.Flash.get(conn.assigns.flash, :success) == "Session logged out successfully" + + assert redirected_to(conn, 302) == + Routes.auth_path(conn, :user_settings) <> "#user-sessions" + + refute Repo.reload(another_session) + end + + test "refuses deletion when not logged in" do + another_session = + insert(:user) + |> Auth.UserSession.new_session("Some Device") + |> Repo.insert!() + + conn = delete(build_conn(), "/settings/user-sessions/#{another_session.id}") + + assert redirected_to(conn, 302) == Routes.auth_path(conn, :login_form) + assert Repo.reload(another_session) + end + end + describe "GET /auth/google/callback" do test "shows error and redirects back to settings when authentication fails", %{conn: conn} do site = insert(:site) diff --git a/test/plausible_web/user_auth_test.exs b/test/plausible_web/user_auth_test.exs index 60d0416e05bd..5d7f10b05825 100644 --- a/test/plausible_web/user_auth_test.exs +++ b/test/plausible_web/user_auth_test.exs @@ -223,6 +223,49 @@ defmodule PlausibleWeb.UserAuthTest do end end + describe "revoke_user_session/2" do + setup [:create_user, :log_in] + + test "deletes and disconnects user session", %{user: user} do + assert [active_session] = Repo.preload(user, :sessions).sessions + live_socket_id = "user_sessions:" <> Base.url_encode64(active_session.token) + Phoenix.PubSub.subscribe(Plausible.PubSub, live_socket_id) + + another_session = + user + |> Auth.UserSession.new_session("Some Device") + |> Repo.insert!() + + assert :ok = UserAuth.revoke_user_session(user, active_session.id) + assert [remaining_session] = Repo.preload(user, :sessions).sessions + assert_broadcast "disconnect", %{} + assert remaining_session.id == another_session.id + refute Repo.reload(active_session) + assert Repo.reload(another_session) + end + + test "does not delete session of another user", %{user: user} do + assert [active_session] = Repo.preload(user, :sessions).sessions + + other_session = + insert(:user) + |> Auth.UserSession.new_session("Some Device") + |> Repo.insert!() + + assert :ok = UserAuth.revoke_user_session(user, other_session.id) + + assert Repo.reload(active_session) + assert Repo.reload(other_session) + end + + test "executes gracefully when session does not exist", %{user: user} do + assert [active_session] = Repo.preload(user, :sessions).sessions + Repo.delete!(active_session) + + assert :ok = UserAuth.revoke_user_session(user, active_session.id) + end + end + describe "revoke_all_user_sessions/1" do setup [:create_user, :log_in] From 5c72de015584aad4a1013e07a25e2193b1596150 Mon Sep 17 00:00:00 2001 From: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:52:40 +0200 Subject: [PATCH 02/18] Experimental pageleave events (#4624) * add experimental pageleave script variant * also send pageleave events on SPA navigation * disallow goals with 'pageleave' event name * do not count pageleaves towards the event metric * remove duplication in test file * do not update sessions on pageleave events * ignore pageleaves in the current time_on_page implementation * make pageleave events not billable * rename function * Prevent multiple pageleaves being sent at the same time --- extra/lib/plausible_web/dogfood.ex | 4 +- lib/plausible/goal/schema.ex | 38 +++-- lib/plausible/ingestion/event.ex | 5 + lib/plausible/session/cache_store.ex | 21 ++- lib/plausible/stats/aggregate.ex | 1 + lib/plausible/stats/breakdown.ex | 1 + lib/plausible/stats/clickhouse.ex | 1 + lib/plausible/stats/sql/expression.ex | 4 +- lib/plausible_web/plugs/tracker.ex | 3 +- test/plausible/billing/quota_test.exs | 21 +++ test/plausible/goals_test.exs | 8 + test/plausible/ingestion/event_test.exs | 16 ++ test/plausible/session/cache_store_test.exs | 157 ++++++------------ .../aggregate_test.exs | 26 +++ .../breakdown_test.exs | 29 ++++ .../external_stats_controller/query_test.exs | 40 +++++ tracker/compile.js | 2 +- tracker/src/plausible.js | 79 ++++++++- 18 files changed, 331 insertions(+), 125 deletions(-) diff --git a/extra/lib/plausible_web/dogfood.ex b/extra/lib/plausible_web/dogfood.ex index df8fd01c4032..ee82b071ff75 100644 --- a/extra/lib/plausible_web/dogfood.ex +++ b/extra/lib/plausible_web/dogfood.ex @@ -15,9 +15,9 @@ defmodule PlausibleWeb.Dogfood do def script_url() do if Application.get_env(:plausible, :environment) in ["prod", "staging"] do - "#{PlausibleWeb.Endpoint.url()}/js/script.manual.pageview-props.tagged-events.js" + "#{PlausibleWeb.Endpoint.url()}/js/script.manual.pageview-props.tagged-events.pageleave.js" else - "#{PlausibleWeb.Endpoint.url()}/js/script.local.manual.pageview-props.tagged-events.js" + "#{PlausibleWeb.Endpoint.url()}/js/script.local.manual.pageview-props.tagged-events.pageleave.js" end end diff --git a/lib/plausible/goal/schema.ex b/lib/plausible/goal/schema.ex index e59a1e1e8341..8f2e29febe01 100644 --- a/lib/plausible/goal/schema.ex +++ b/lib/plausible/goal/schema.ex @@ -72,25 +72,43 @@ defmodule Plausible.Goal do end defp validate_event_name_and_page_path(changeset) do - if validate_page_path(changeset) || validate_event_name(changeset) do - changeset - |> update_change(:event_name, &String.trim/1) - |> update_change(:page_path, &String.trim/1) - else - changeset - |> add_error(:event_name, "this field is required and cannot be blank") - |> add_error(:page_path, "this field is required and must start with a /") + case {validate_page_path(changeset), validate_event_name(changeset)} do + {:ok, _} -> + update_change(changeset, :page_path, &String.trim/1) + + {_, :ok} -> + update_change(changeset, :event_name, &String.trim/1) + + {{:error, page_path_error}, {:error, event_name_error}} -> + changeset + |> add_error(:event_name, event_name_error) + |> add_error(:page_path, page_path_error) end end defp validate_page_path(changeset) do value = get_field(changeset, :page_path) - value && String.match?(value, ~r/^\/.*/) + + if value && String.match?(value, ~r/^\/.*/) do + :ok + else + {:error, "this field is required and must start with a /"} + end end defp validate_event_name(changeset) do value = get_field(changeset, :event_name) - value && String.match?(value, ~r/^.+/) + + cond do + value == "pageleave" -> + {:error, "The event name 'pageleave' is reserved and cannot be used as a goal"} + + value && String.match?(value, ~r/^.+/) -> + :ok + + true -> + {:error, "this field is required and cannot be blank"} + end end defp maybe_drop_currency(changeset) do diff --git a/lib/plausible/ingestion/event.ex b/lib/plausible/ingestion/event.ex index dcd5a010d40d..94e13d04c299 100644 --- a/lib/plausible/ingestion/event.ex +++ b/lib/plausible/ingestion/event.ex @@ -34,6 +34,8 @@ defmodule Plausible.Ingestion.Event do | :site_page_blocklist | :site_hostname_allowlist | :verification_agent + | :lock_timeout + | :no_session_for_pageleave @type t() :: %__MODULE__{ domain: String.t() | nil, @@ -376,6 +378,9 @@ defmodule Plausible.Ingestion.Event do | clickhouse_event: ClickhouseEventV2.merge_session(event.clickhouse_event, session) } + {:error, :no_session_for_pageleave} -> + drop(event, :no_session_for_pageleave) + {:error, :timeout} -> drop(event, :lock_timeout) end diff --git a/lib/plausible/session/cache_store.ex b/lib/plausible/session/cache_store.ex index bf43a7d29ba2..724e9a5d6d99 100644 --- a/lib/plausible/session/cache_store.ex +++ b/lib/plausible/session/cache_store.ex @@ -8,7 +8,26 @@ defmodule Plausible.Session.CacheStore do def lock_telemetry_event, do: @lock_telemetry_event - def on_event(event, session_attributes, prev_user_id, buffer_insert \\ &WriteBuffer.insert/1) do + def on_event(event, session_attributes, prev_user_id, buffer_insert \\ &WriteBuffer.insert/1) + + def on_event(%{name: "pageleave"} = event, _, prev_user_id, _) do + # The `pageleave` event is currently experimental. In a real use case we would + # probably want to update the session as well (e.g. `is_bounce` or `duration`). + + # However, for now we're only interested in finding out the success rate of + # pageleave events. So these events will simply be inserted into the events + # table with the session ID found from the cache. If there's no session, the + # event will be dropped. + found_session = find_session(event, event.user_id) || find_session(event, prev_user_id) + + if found_session do + {:ok, found_session} + else + {:error, :no_session_for_pageleave} + end + end + + def on_event(event, session_attributes, prev_user_id, buffer_insert) do lock_requested_at = System.monotonic_time() Plausible.Cache.Adapter.with_lock( diff --git a/lib/plausible/stats/aggregate.ex b/lib/plausible/stats/aggregate.ex index 8bce4103dd2c..2730ff44ec7a 100644 --- a/lib/plausible/stats/aggregate.ex +++ b/lib/plausible/stats/aggregate.ex @@ -50,6 +50,7 @@ defmodule Plausible.Stats.Aggregate do defp aggregate_time_on_page(site, query) do windowed_pages_q = from e in base_event_query(site, Query.remove_top_level_filters(query, ["event:page"])), + where: e.name != "pageleave", select: %{ next_timestamp: over(fragment("leadInFrame(?)", e.timestamp), :event_horizon), next_pathname: over(fragment("leadInFrame(?)", e.pathname), :event_horizon), diff --git a/lib/plausible/stats/breakdown.ex b/lib/plausible/stats/breakdown.ex index 41542d65ed1a..637609ebdd1e 100644 --- a/lib/plausible/stats/breakdown.ex +++ b/lib/plausible/stats/breakdown.ex @@ -93,6 +93,7 @@ defmodule Plausible.Stats.Breakdown do site, Query.remove_top_level_filters(query, ["event:page", "event:props"]) ), + where: e.name != "pageleave", select: %{ next_timestamp: over(fragment("leadInFrame(?)", e.timestamp), :event_horizon), next_pathname: over(fragment("leadInFrame(?)", e.pathname), :event_horizon), diff --git a/lib/plausible/stats/clickhouse.ex b/lib/plausible/stats/clickhouse.ex index 219051e686b0..a201d0c94ac2 100644 --- a/lib/plausible/stats/clickhouse.ex +++ b/lib/plausible/stats/clickhouse.ex @@ -56,6 +56,7 @@ defmodule Plausible.Stats.Clickhouse do ClickhouseRepo.one( from(e in "events_v2", where: e.site_id in ^site_ids, + where: e.name != "pageleave", where: fragment("toDate(?)", e.timestamp) >= ^date_range.first, where: fragment("toDate(?)", e.timestamp) <= ^date_range.last, select: { diff --git a/lib/plausible/stats/sql/expression.ex b/lib/plausible/stats/sql/expression.ex index 4b9d2f4a4b97..5ded014b47fa 100644 --- a/lib/plausible/stats/sql/expression.ex +++ b/lib/plausible/stats/sql/expression.ex @@ -197,8 +197,8 @@ defmodule Plausible.Stats.SQL.Expression do end def event_metric(:events) do - wrap_alias([], %{ - events: fragment("toUInt64(round(count(*) * any(_sample_factor)))") + wrap_alias([e], %{ + events: fragment("toUInt64(round(countIf(? != 'pageleave') * any(_sample_factor)))", e.name) }) end diff --git a/lib/plausible_web/plugs/tracker.ex b/lib/plausible_web/plugs/tracker.ex index cd9a89b4e65e..5b7387e65a34 100644 --- a/lib/plausible_web/plugs/tracker.ex +++ b/lib/plausible_web/plugs/tracker.ex @@ -12,7 +12,8 @@ defmodule PlausibleWeb.Tracker do "file-downloads", "pageview-props", "tagged-events", - "revenue" + "revenue", + "pageleave" ] # Generates Power Set of all variants diff --git a/test/plausible/billing/quota_test.exs b/test/plausible/billing/quota_test.exs index ceeb861eaf82..5b30719b9b6e 100644 --- a/test/plausible/billing/quota_test.exs +++ b/test/plausible/billing/quota_test.exs @@ -723,6 +723,27 @@ defmodule Plausible.Billing.QuotaTest do } = Quota.Usage.monthly_pageview_usage(user) end + test "pageleave events are not counted towards monthly pageview usage" do + user = insert(:user) |> Plausible.Users.with_subscription() + site = insert(:site, members: [user]) + now = NaiveDateTime.utc_now() + + 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") + ]) + + assert %{ + last_30_days: %{ + total: 2, + custom_events: 1, + pageviews: 1, + date_range: %{} + } + } = Quota.Usage.monthly_pageview_usage(user) + end + test "returns usage for user with subscription and a site" do today = Date.utc_today() diff --git a/test/plausible/goals_test.exs b/test/plausible/goals_test.exs index 2e8174fd5981..4ed0bdc070b0 100644 --- a/test/plausible/goals_test.exs +++ b/test/plausible/goals_test.exs @@ -55,6 +55,14 @@ defmodule Plausible.GoalsTest do assert {"has already been taken", _} = changeset.errors[:event_name] end + test "create/2 fails to create a goal with 'pageleave' as event_name (reserved)" do + site = insert(:site) + assert {:error, changeset} = Goals.create(site, %{"event_name" => "pageleave"}) + + assert {"The event name 'pageleave' is reserved and cannot be used as a goal", _} = + changeset.errors[:event_name] + end + @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)) diff --git a/test/plausible/ingestion/event_test.exs b/test/plausible/ingestion/event_test.exs index 345fe6884e68..bc2a63696a57 100644 --- a/test/plausible/ingestion/event_test.exs +++ b/test/plausible/ingestion/event_test.exs @@ -318,6 +318,22 @@ defmodule Plausible.Ingestion.EventTest do assert dropped.drop_reason == :lock_timeout end + test "drops pageleave event when no session found from cache" do + site = insert(:site) + + payload = %{ + name: "pageleave", + url: "https://#{site.domain}/123", + d: "#{site.domain}" + } + + conn = build_conn(:post, "/api/events", payload) + + assert {:ok, request} = Request.build(conn) + assert {:ok, %{buffered: [], dropped: [dropped]}} = Event.build_and_buffer(request) + assert dropped.drop_reason == :no_session_for_pageleave + end + @tag :ee_only test "saves revenue amount" do site = insert(:site) diff --git a/test/plausible/session/cache_store_test.exs b/test/plausible/session/cache_store_test.exs index 877c0ee47f43..33f3ef6386b9 100644 --- a/test/plausible/session/cache_store_test.exs +++ b/test/plausible/session/cache_store_test.exs @@ -3,6 +3,22 @@ defmodule Plausible.Session.CacheStoreTest do alias Plausible.Session.CacheStore + @session_params %{ + referrer: "ref", + referrer_source: "refsource", + utm_medium: "medium", + utm_source: "source", + utm_campaign: "campaign", + utm_content: "content", + utm_term: "term", + browser: "browser", + browser_version: "55", + country_code: "EE", + screen_size: "Desktop", + operating_system: "Mac", + operating_system_version: "11" + } + setup do current_pid = self() @@ -40,23 +56,7 @@ defmodule Plausible.Session.CacheStoreTest do event2 = build(:event, name: "pageview", user_id: event1.user_id, site_id: event1.site_id) event3 = build(:event, name: "pageview", user_id: event1.user_id, site_id: event1.site_id) - session_params = %{ - referrer: "ref", - referrer_source: "refsource", - utm_medium: "medium", - utm_source: "source", - utm_campaign: "campaign", - utm_content: "content", - utm_term: "term", - browser: "browser", - browser_version: "55", - country_code: "EE", - screen_size: "Desktop", - operating_system: "Mac", - operating_system_version: "11" - } - - CacheStore.on_event(event1, session_params, nil, buffer) + CacheStore.on_event(event1, @session_params, nil, buffer) assert_receive({:buffer, :insert, [[session1]]}) assert_receive({:telemetry_handled, duration}) @@ -65,7 +65,7 @@ defmodule Plausible.Session.CacheStoreTest do [event2, event3] |> Enum.map(fn e -> Task.async(fn -> - CacheStore.on_event(e, session_params, nil, slow_buffer) + CacheStore.on_event(e, @session_params, nil, slow_buffer) end) end) |> Task.await_many() @@ -120,25 +120,9 @@ defmodule Plausible.Session.CacheStoreTest do event2 = build(:event, name: "pageview", user_id: event1.user_id, site_id: event1.site_id) event3 = build(:event, name: "pageview", user_id: event1.user_id, site_id: event1.site_id) - session_params = %{ - referrer: "ref", - referrer_source: "refsource", - utm_medium: "medium", - utm_source: "source", - utm_campaign: "campaign", - utm_content: "content", - utm_term: "term", - browser: "browser", - browser_version: "55", - country_code: "EE", - screen_size: "Desktop", - operating_system: "Mac", - operating_system_version: "11" - } - async1 = Task.async(fn -> - CacheStore.on_event(event1, session_params, nil, very_slow_buffer) + CacheStore.on_event(event1, @session_params, nil, very_slow_buffer) end) # Ensure next events are executed after processing event1 starts @@ -146,12 +130,12 @@ defmodule Plausible.Session.CacheStoreTest do async2 = Task.async(fn -> - CacheStore.on_event(event2, session_params, nil, buffer) + CacheStore.on_event(event2, @session_params, nil, buffer) end) async3 = Task.async(fn -> - CacheStore.on_event(event3, session_params, nil, buffer) + CacheStore.on_event(event3, @session_params, nil, buffer) end) Task.await_many([async1, async2, async3]) @@ -174,25 +158,9 @@ defmodule Plausible.Session.CacheStoreTest do event2 = build(:event, name: "pageview") event3 = build(:event, name: "pageview", user_id: event2.user_id, site_id: event2.site_id) - session_params = %{ - referrer: "ref", - referrer_source: "refsource", - utm_medium: "medium", - utm_source: "source", - utm_campaign: "campaign", - utm_content: "content", - utm_term: "term", - browser: "browser", - browser_version: "55", - country_code: "EE", - screen_size: "Desktop", - operating_system: "Mac", - operating_system_version: "11" - } - async1 = Task.async(fn -> - CacheStore.on_event(event1, session_params, nil, very_slow_buffer) + CacheStore.on_event(event1, @session_params, nil, very_slow_buffer) end) # Ensure next events are executed after processing event1 starts @@ -200,14 +168,14 @@ defmodule Plausible.Session.CacheStoreTest do async2 = Task.async(fn -> - CacheStore.on_event(event2, session_params, nil, buffer) + CacheStore.on_event(event2, @session_params, nil, buffer) end) Process.sleep(100) async3 = Task.async(fn -> - CacheStore.on_event(event3, session_params, nil, buffer) + CacheStore.on_event(event3, @session_params, nil, buffer) end) Task.await_many([async1, async2, async3]) @@ -229,24 +197,8 @@ defmodule Plausible.Session.CacheStoreTest do event = build(:event, name: "pageview") - session_params = %{ - referrer: "ref", - referrer_source: "refsource", - utm_medium: "medium", - utm_source: "source", - utm_campaign: "campaign", - utm_term: "term", - utm_content: "content", - browser: "browser", - browser_version: "55", - country_code: "EE", - screen_size: "Desktop", - operating_system: "Mac", - operating_system_version: "11" - } - assert_raise RuntimeError, "boom", fn -> - CacheStore.on_event(event, session_params, nil, crashing_buffer) + CacheStore.on_event(event, @session_params, nil, crashing_buffer) end end @@ -258,23 +210,7 @@ defmodule Plausible.Session.CacheStoreTest do "meta.value": ["true", "false"] ) - session_params = %{ - referrer: "ref", - referrer_source: "refsource", - utm_medium: "medium", - utm_source: "source", - utm_campaign: "campaign", - utm_content: "content", - utm_term: "term", - browser: "browser", - browser_version: "55", - country_code: "EE", - screen_size: "Desktop", - operating_system: "Mac", - operating_system_version: "11" - } - - CacheStore.on_event(event, session_params, nil, buffer) + CacheStore.on_event(event, @session_params, nil, buffer) assert_receive({:buffer, :insert, [sessions]}) assert [session] = sessions @@ -289,19 +225,19 @@ defmodule Plausible.Session.CacheStoreTest do assert session.duration == 0 assert session.pageviews == 1 assert session.events == 1 - assert session.referrer == Map.get(session_params, :referrer) - assert session.referrer_source == Map.get(session_params, :referrer_source) - assert session.utm_medium == Map.get(session_params, :utm_medium) - assert session.utm_source == Map.get(session_params, :utm_source) - assert session.utm_campaign == Map.get(session_params, :utm_campaign) - assert session.utm_content == Map.get(session_params, :utm_content) - assert session.utm_term == Map.get(session_params, :utm_term) - assert session.country_code == Map.get(session_params, :country_code) - assert session.screen_size == Map.get(session_params, :screen_size) - assert session.operating_system == Map.get(session_params, :operating_system) - assert session.operating_system_version == Map.get(session_params, :operating_system_version) - assert session.browser == Map.get(session_params, :browser) - assert session.browser_version == Map.get(session_params, :browser_version) + assert session.referrer == Map.get(@session_params, :referrer) + assert session.referrer_source == Map.get(@session_params, :referrer_source) + assert session.utm_medium == Map.get(@session_params, :utm_medium) + assert session.utm_source == Map.get(@session_params, :utm_source) + assert session.utm_campaign == Map.get(@session_params, :utm_campaign) + assert session.utm_content == Map.get(@session_params, :utm_content) + assert session.utm_term == Map.get(@session_params, :utm_term) + assert session.country_code == Map.get(@session_params, :country_code) + assert session.screen_size == Map.get(@session_params, :screen_size) + assert session.operating_system == Map.get(@session_params, :operating_system) + assert session.operating_system_version == Map.get(@session_params, :operating_system_version) + assert session.browser == Map.get(@session_params, :browser) + assert session.browser_version == Map.get(@session_params, :browser_version) assert session.timestamp == event.timestamp assert session.start === event.timestamp # assert Map.get(session, :"entry.meta.key") == ["logged_in", "darkmode"] @@ -326,6 +262,21 @@ defmodule Plausible.Session.CacheStoreTest do assert session.events == 2 end + test "does not update session counters on pageleave event", %{buffer: buffer} do + now = Timex.now() + pageview = build(:pageview, timestamp: Timex.shift(now, seconds: -10)) + pageleave = %{pageview | name: "pageleave", timestamp: now} + + CacheStore.on_event(pageview, %{}, nil, buffer) + CacheStore.on_event(pageleave, %{}, nil, buffer) + assert_receive({:buffer, :insert, [[session]]}) + + assert session.is_bounce == true + assert session.duration == 0 + assert session.pageviews == 1 + assert session.events == 1 + end + describe "hostname-related attributes" do test "initial for non-pageview" do site_id = new_site_id() 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 f4c057d13538..8f4264aa1894 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 @@ -1625,6 +1625,32 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do assert json_response(conn, 200)["results"] == %{"time_on_page" => %{"value" => nil}} end + test "pageleave events are ignored when querying time on page", %{conn: conn, site: site} 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" + ) + ]) + + conn = + get(conn, "/api/v1/stats/aggregate", %{ + "site_id" => site.domain, + "metrics" => "time_on_page", + "filters" => "event:page==/2", + "period" => "day", + "date" => "2021-01-01" + }) + + assert json_response(conn, 200)["results"] == %{ + "time_on_page" => %{"value" => nil} + } + end + test "conversion_rate when goal filter is applied", %{conn: conn, site: site} do populate_stats(site, [ build(:event, name: "Signup"), 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 eabacae75444..c2d474f7727b 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,6 +2604,35 @@ defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do } end + test "pageleave events are ignored when querying time on page", %{conn: conn, site: site} 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" + ) + ]) + + conn = + get(conn, "/api/v1/stats/breakdown", %{ + "site_id" => site.domain, + "property" => "event:page", + "metrics" => "time_on_page", + "period" => "day", + "date" => "2021-01-01" + }) + + assert json_response(conn, 200) == %{ + "results" => [ + %{"page" => "/1", "time_on_page" => 5}, + %{"page" => "/2", "time_on_page" => nil} + ] + } + end + test "returns time_on_page as the only metric in an event:page breakdown", %{ conn: conn, site: site 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 e3ebc0265aea..95c6ae1d9491 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 @@ -101,6 +101,46 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do ] end + test "does not count pageleave events towards the events metric in a simple aggregate query", + %{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]) + ]) + + conn = + post(conn, "/api/v2/query", %{ + "site_id" => site.domain, + "date_range" => "all", + "metrics" => ["events"] + }) + + assert json_response(conn, 200)["results"] == [ + %{"metrics" => [1], "dimensions" => []} + ] + end + + test "pageleave events do not affect bounce rate and visit duration", %{ + conn: conn, + site: site + } 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]) + ]) + + conn = + post(conn, "/api/v2/query", %{ + "site_id" => site.domain, + "date_range" => "all", + "metrics" => ["bounce_rate", "visit_duration"] + }) + + assert json_response(conn, 200)["results"] == [ + %{"metrics" => [100, 0], "dimensions" => []} + ] + end + test "can filter by channel", %{conn: conn, site: site} do populate_stats(site, [ build(:pageview, diff --git a/tracker/compile.js b/tracker/compile.js index 5896759cf131..c4c5add65757 100644 --- a/tracker/compile.js +++ b/tracker/compile.js @@ -26,7 +26,7 @@ function compilefile(input, output, templateVars = {}) { } } -const base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events", "revenue"] +const base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events", "revenue", "pageleave"] const variants = [...g.clone.powerSet(base_variants)].filter(a => a.length > 0).map(a => a.sort()); compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js')) diff --git a/tracker/src/plausible.js b/tracker/src/plausible.js index 7701c5d43d11..aecb95786d36 100644 --- a/tracker/src/plausible.js +++ b/tracker/src/plausible.js @@ -10,6 +10,7 @@ var scriptEl = document.currentScript; {{/if}} var endpoint = scriptEl.getAttribute('data-api') || defaultEndpoint(scriptEl) + var dataDomain = scriptEl.getAttribute('data-domain') function onIgnoredEvent(reason, options) { if (reason) console.warn('Ignoring Event: ' + reason); @@ -27,6 +28,53 @@ {{/if}} } + {{#if pageleave}} + // :NOTE: Tracking pageleave events is currently experimental. + + // Multiple pageviews might be sent by the same script when the page + // uses client-side routing (e.g. hash or history-based). This flag + // prevents registering multiple listeners in those cases. + var listeningPageLeave = false + + // In SPA-s, multiple listeners that trigger the pageleave event + // might fire nearly at the same time. E.g. when navigating back + // in browser history while using hash-based routing - a popstate + // and hashchange will be fired in a very quick succession. This + // flag prevents sending multiple pageleaves in those cases. + var pageLeaveSending = false + + function triggerPageLeave(url) { + if (pageLeaveSending) {return} + pageLeaveSending = true + setTimeout(function () {pageLeaveSending = false}, 500) + + var payload = { + n: 'pageleave', + d: dataDomain, + u: url, + } + + {{#if hash}} + payload.h = 1 + {{/if}} + + if (navigator.sendBeacon) { + var blob = new Blob([JSON.stringify(payload)], { type: 'text/plain' }); + navigator.sendBeacon(endpoint, blob) + } + } + + function registerPageLeaveListener(url) { + if (listeningPageLeave) { return } + + window.addEventListener('pagehide', function () { + triggerPageLeave(url) + }) + + listeningPageLeave = true + } + {{/if}} + function trigger(eventName, options) { {{#unless local}} @@ -73,7 +121,7 @@ {{else}} payload.u = location.href {{/if}} - payload.d = scriptEl.getAttribute('data-domain') + payload.d = dataDomain payload.r = document.referrer || null if (options && options.meta) { payload.m = JSON.stringify(options.meta) @@ -115,6 +163,11 @@ request.onreadystatechange = function() { if (request.readyState === 4) { + {{#if pageleave}} + if (eventName === 'pageview') { + registerPageLeaveListener(payload.u) + } + {{/if}} options && options.callback && options.callback({status: request.status}) } } @@ -129,25 +182,41 @@ {{#unless manual}} var lastPage; - function page() { + {{#if pageleave}} + var lastUrl = location.href + + function pageLeaveSPA() { + triggerPageLeave(lastUrl); + lastUrl = location.href; + } + {{/if}} + + function page(isSPANavigation) { {{#unless hash}} if (lastPage === location.pathname) return; {{/unless}} + + {{#if pageleave}} + if (isSPANavigation) {pageLeaveSPA()} + {{/if}} + lastPage = location.pathname trigger('pageview') } + var onSPANavigation = function() {page(true)} + {{#if hash}} - window.addEventListener('hashchange', page) + window.addEventListener('hashchange', onSPANavigation) {{else}} var his = window.history if (his.pushState) { var originalPushState = his['pushState'] his.pushState = function() { originalPushState.apply(this, arguments) - page(); + onSPANavigation(); } - window.addEventListener('popstate', page) + window.addEventListener('popstate', onSPANavigation) } {{/if}} From 52437e8df00eb64f8a4f6680fa83a8e0d1a72f32 Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Mon, 30 Sep 2024 23:16:36 +0700 Subject: [PATCH 03/18] Handle cross-device file move (#4640) * handle cross-device file move * don't change tests * rm all the time * no need to rm if rename succeeds * don't raise in after --- lib/plausible/file.ex | 27 +++++++++++++++++++++++++++ lib/plausible_web/live/csv_import.ex | 2 +- lib/workers/export_analytics.ex | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 lib/plausible/file.ex diff --git a/lib/plausible/file.ex b/lib/plausible/file.ex new file mode 100644 index 000000000000..689134b9453c --- /dev/null +++ b/lib/plausible/file.ex @@ -0,0 +1,27 @@ +defmodule Plausible.File do + @moduledoc """ + File helpers for Plausible. + """ + + @doc """ + Moves a file from one location to another. + + Tries renaming first, and falls back to copying and deleting the original. + """ + @spec mv!(Path.t(), Path.t()) :: :ok + def mv!(source, destination) do + File.rename!(source, destination) + rescue + e in File.RenameError -> + try do + case e.reason do + # fallback to cp/rm for cross-device moves + # https://github.com/plausible/analytics/issues/4638 + :exdev -> File.cp!(source, destination) + _ -> reraise(e, __STACKTRACE__) + end + after + File.rm(source) + end + end +end diff --git a/lib/plausible_web/live/csv_import.ex b/lib/plausible_web/live/csv_import.ex index 5234ba1fe7f0..20751de4c736 100644 --- a/lib/plausible_web/live/csv_import.ex +++ b/lib/plausible_web/live/csv_import.ex @@ -45,7 +45,7 @@ defmodule PlausibleWeb.Live.CSVImport do fn meta, entry -> local_path = Path.join(local_dir, Path.basename(meta.path)) - File.rename!(meta.path, local_path) + Plausible.File.mv!(meta.path, local_path) {:ok, %{"local_path" => local_path, "filename" => entry.client_name}} end end diff --git a/lib/workers/export_analytics.ex b/lib/workers/export_analytics.ex index dd8a8aeb2086..51a1c0af06cb 100644 --- a/lib/workers/export_analytics.ex +++ b/lib/workers/export_analytics.ex @@ -101,7 +101,7 @@ defmodule Plausible.Workers.ExportAnalytics do File.mkdir_p!(Path.dirname(local_path)) if File.exists?(local_path), do: File.rm!(local_path) - File.rename!(tmp_path, local_path) + Plausible.File.mv!(tmp_path, local_path) end defp email_failure(args) do From 6822a4c54b3e9d3718271005f5813452d90b1c9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 06:50:31 +0000 Subject: [PATCH 04/18] Bump phoenix_live_reload from 1.4.1 to 1.5.3 (#4551) Bumps [phoenix_live_reload](https://github.com/phoenixframework/phoenix_live_reload) from 1.4.1 to 1.5.3. - [Changelog](https://github.com/phoenixframework/phoenix_live_reload/blob/main/CHANGELOG.md) - [Commits](https://github.com/phoenixframework/phoenix_live_reload/compare/v1.4.1...v1.5.3) --- updated-dependencies: - dependency-name: phoenix_live_reload dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: hq1 --- mix.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mix.lock b/mix.lock index 37bca1572026..597fc014849a 100644 --- a/mix.lock +++ b/mix.lock @@ -20,9 +20,9 @@ "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "con_cache": {:hex, :con_cache, "1.1.0", "45c7c6cd6dc216e47636232e8c683734b7fe293221fccd9454fa1757bc685044", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8655f2ae13a1e56c8aef304d250814c7ed929c12810f126fc423ecc8e871593b"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, - "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, @@ -52,7 +52,7 @@ "ex_money": {:hex, :ex_money, "5.17.0", "9064a30d877d85b3e3ec7ca52339542037473bc71fc5c5b9f6c31d86516002b9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.33", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:gringotts, "~> 1.1", [hex: :gringotts, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "226aa8906c85cb121f1d0ead0b108b259ba68d92c8fc79fa1758d520ff5c84c0"}, "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"}, "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, "fun_with_flags": {:hex, :fun_with_flags, "1.11.0", "a9019d0300e9755c53111cf5b2aba640d7f0de2a8a03a0bd0c593e943c3e9ec5", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}], "hexpm", "448ec640cd1ade4728979ae5b3e7592b0fc8b0f99cf40785d048515c27d09743"}, @@ -111,18 +111,18 @@ "paginator": {:git, "https://github.com/duffelhq/paginator.git", "3508d6ad77a95ac1faf15d5fd7f959fab3e17da2", []}, "parent": {:hex, :parent, "0.12.1", "495c4386f06de0df492e0a7a7199c10323a55e9e933b27222060dd86dccd6d62", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2ab589ef1f37bfcedbfb5ecfbab93354972fb7391201b8907a866dadd20b39d1"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_bakery": {:hex, :phoenix_bakery, "0.1.2", "ca57673caea1a98f1cc763f94032796a015774d27eaa3ce5feef172195470452", [:mix], [{:brotli, "~> 0.3.0", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "45cc8cecc5c3002b922447c16389761718c07c360432328b04680034e893ea5b"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.3", "8b6406bc0a451f295407d7acff7f234a6314be5bbe0b3f90ed82b07f50049878", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8e4385e05618b424779f894ed2df97d3c7518b7285fcd11979077ae6226466b"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "php_serializer": {:hex, :php_serializer, "2.0.0", "b43f31aca22ed7321f32da2b94fe2ddf9b6739a965cb51541969119e572e821d", [:mix], [], "hexpm", "61e402e99d9062c0225a3f4fcf7e43b4cba1b8654944c0e7c139c3ca9de481da"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"}, @@ -152,7 +152,7 @@ "ua_inspector": {:git, "https://github.com/plausible/ua_inspector.git", "25cba4c910e80d7c34bbb1bbb939372260d088e8", [branch: "sanitize-pre"]}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, "x509": {:hex, :x509, "0.8.9", "03c47e507171507d3d3028d802f48dd575206af2ef00f764a900789dfbe17476", [:mix], [], "hexpm", "ea3fb16a870a199cb2c45908a2c3e89cc934f0434173dc0c828136f878f11661"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "zstream": {:hex, :zstream, "0.6.4", "169ce887a443d4163085ee682ab1b0ad38db8fa45e843927b9b431a92f4b7d9e", [:mix], [], "hexpm", "acc6c35b6db9eb2cfe8b85e972cb9dc1b730f8efeb76c5bbe871216fe639d9a1"}, From de04f222fddff62f6f8b35f79e417a2b2930c6da Mon Sep 17 00:00:00 2001 From: hq1 Date: Tue, 1 Oct 2024 11:11:46 +0200 Subject: [PATCH 05/18] Update con_cache (#4642) --- mix.exs | 2 +- mix.lock | 2 +- test/test_helper.exs | 11 +---------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/mix.exs b/mix.exs index 54c0f3467cf2..ee9adbf52447 100644 --- a/mix.exs +++ b/mix.exs @@ -142,7 +142,7 @@ defmodule Plausible.MixProject do {:ex_aws_s3, "~> 2.5"}, {:sweet_xml, "~> 0.7.4"}, {:zstream, "~> 0.6.4"}, - {:con_cache, "~> 1.1.0"}, + {:con_cache, "~> 1.1.1"}, {:req, "~> 0.5.0"}, {:happy_tcp, github: "ruslandoga/happy_tcp", only: [:ce, :ce_dev, :ce_test]}, {:ex_json_schema, "~> 0.10.2"}, diff --git a/mix.lock b/mix.lock index 597fc014849a..bc915b942b49 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "combination": {:hex, :combination, "0.0.3", "746aedca63d833293ec6e835aa1f34974868829b1486b1e1cb0685f0b2ae1f41", [:mix], [], "hexpm", "72b099f463df42ef7dc6371d250c7070b57b6c5902853f69deb894f79eda18ca"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, - "con_cache": {:hex, :con_cache, "1.1.0", "45c7c6cd6dc216e47636232e8c683734b7fe293221fccd9454fa1757bc685044", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8655f2ae13a1e56c8aef304d250814c7ed929c12810f126fc423ecc8e871593b"}, + "con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, diff --git a/test/test_helper.exs b/test/test_helper.exs index c9006141c2cf..3906208d9329 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -22,18 +22,9 @@ end default_exclude = [:slow, :minio, :migrations] -# warm up ConCache until https://github.com/sasa1977/con_cache/pull/79 +# avoid slowdowns contacting the code server https://github.com/sasa1977/con_cache/pull/79 :code.ensure_loaded(ConCache.Lock.Resource) -for i <- 1..(System.schedulers_online() * 2) do - Plausible.Cache.Adapter.with_lock( - :sessions, - {i, i + 1}, - 500, - fn -> :warmup end - ) -end - if Mix.env() == :ce_test do IO.puts("Test mode: Community Edition") ExUnit.configure(exclude: [:ee_only | default_exclude]) From ec2a56001604148f43394ee348747a8a95b7977b Mon Sep 17 00:00:00 2001 From: hq1 Date: Wed, 2 Oct 2024 11:05:21 +0200 Subject: [PATCH 06/18] Rework settings UI (#4626) * Update generic components library * Import generic components via `PlausibleWeb` * Update Settings/Danger Zone * Update new shared link template and convert to heex * Update site settings layout * Update site settings sidebar tab layout * Update Settings/Email Reports * Update Funnels * Update ComboBox * Extend/update form components * Update Modal live component * Update Settings/Goals * Update Shields * Update Settings/Props * Update Settings/Import & Export * Update flow progress * Import Routes in settings * Update Billing components * Update Billing notice component * Update feature toggle component * Update 2fa component * Update verification markup * Update installation * Update Settings/Integrations/Plugins * Update domain change markup * Update Settings/General * Update Settings/Integrations * Update Settings/People * Update Settings/Integrations/GSC * Update Settings/Visiblity * ukuwip * ukuwip * Tables & paddings * Imports exports * Brighten disabled input text color for dark mode * Tune down table border/divider in dark mode * Format * Fix goal list on mobile * Fix IP Shields table on mobile * Fix country shields list on mobile * Fix country shield list on mobile * Fix page shields list on mobile * Fix import/export settings on mobile * Fix combobox dropdown background in dark mode * Fix filter bar search input on mobile * Revert @ukutaht's changes to goal list * Maybe maybe maybe * Revert the current prod goal list + fix mobile issues * Format * Revert tests change cc @ukutaht * Fix markup expectation in a test * Set autocomplete="off" again * Bring back `text-sm` where previously removed --------- Co-authored-by: Uku Taht --- .../live/funnel_settings/form.ex | 44 +- .../live/funnel_settings/list.ex | 107 ++--- lib/plausible_web.ex | 2 + .../components/billing/billing.ex | 11 +- .../components/billing/notice.ex | 2 - lib/plausible_web/components/flow_progress.ex | 4 +- lib/plausible_web/components/generic.ex | 394 +++++++++++++--- lib/plausible_web/components/settings.ex | 1 + lib/plausible_web/components/site/feature.ex | 54 +-- lib/plausible_web/components/two_factor.ex | 3 +- .../live/components/combo_box.ex | 4 +- lib/plausible_web/live/components/form.ex | 59 ++- lib/plausible_web/live/components/modal.ex | 8 +- .../live/components/verification.ex | 23 +- lib/plausible_web/live/csv_export.ex | 64 +-- lib/plausible_web/live/csv_import.ex | 15 +- lib/plausible_web/live/goal_settings/form.ex | 89 ++-- lib/plausible_web/live/goal_settings/list.ex | 183 +++----- .../live/imports_exports_settings.ex | 191 ++++---- lib/plausible_web/live/installation.ex | 12 +- .../live/plugins/api/settings.ex | 130 ++---- .../live/plugins/api/token_form.ex | 50 +- lib/plausible_web/live/props_settings/form.ex | 14 +- lib/plausible_web/live/props_settings/list.ex | 78 +--- .../live/shields/country_rules.ex | 220 ++++----- .../live/shields/hostname_rules.ex | 265 +++++------ lib/plausible_web/live/shields/ip_rules.ex | 285 +++++------- lib/plausible_web/live/shields/page_rules.ex | 245 +++++----- .../templates/layout/_settings_tab.html.heex | 11 +- .../templates/layout/site_settings.html.heex | 6 +- .../templates/site/change_domain.html.heex | 9 +- .../templates/site/csv_import.html.heex | 27 +- .../membership/invite_member_form.html.heex | 8 +- .../transfer_ownership_form.html.heex | 4 +- .../templates/site/new_shared_link.html.eex | 22 - .../templates/site/new_shared_link.html.heex | 26 ++ .../site/settings_danger_zone.html.eex | 47 -- .../site/settings_danger_zone.html.heex | 42 ++ .../site/settings_email_reports.html.heex | 434 +++++++++--------- .../templates/site/settings_funnels.html.heex | 44 +- .../templates/site/settings_general.html.heex | 137 ++---- .../templates/site/settings_goals.html.heex | 47 +- .../site/settings_imports_exports.html.heex | 57 ++- .../site/settings_integrations.html.heex | 34 +- .../templates/site/settings_people.html.heex | 404 ++++++++-------- .../templates/site/settings_props.html.heex | 57 +-- .../site/settings_search_console.html.heex | 104 ++--- .../site/settings_visibility.html.heex | 369 +++++---------- .../templates/site/traffic_change_form.heex | 133 ------ .../controllers/site_controller_test.exs | 21 +- .../live/funnel_settings_test.exs | 6 +- .../plausible_web/live/goal_settings_test.exs | 2 +- .../live/shields/countries_test.exs | 6 +- .../live/shields/hostnames_test.exs | 2 +- .../live/shields/ip_addresses_test.exs | 6 +- .../plausible_web/live/shields/pages_test.exs | 2 +- test/plausible_web/live/verification_test.exs | 12 +- 57 files changed, 2107 insertions(+), 2529 deletions(-) delete mode 100644 lib/plausible_web/templates/site/new_shared_link.html.eex create mode 100644 lib/plausible_web/templates/site/new_shared_link.html.heex delete mode 100644 lib/plausible_web/templates/site/settings_danger_zone.html.eex create mode 100644 lib/plausible_web/templates/site/settings_danger_zone.html.heex delete mode 100644 lib/plausible_web/templates/site/traffic_change_form.heex diff --git a/extra/lib/plausible_web/live/funnel_settings/form.ex b/extra/lib/plausible_web/live/funnel_settings/form.ex index 24346e5f27d1..ea794eb55a68 100644 --- a/extra/lib/plausible_web/live/funnel_settings/form.ex +++ b/extra/lib/plausible_web/live/funnel_settings/form.ex @@ -59,13 +59,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do onkeydown="return event.key != 'Enter';" class="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8" > -

+ <.title class="mb-6"> <%= if @funnel, do: "Edit", else: "Add" %> Funnel -

- - + <.input field={f[:name]} @@ -73,13 +69,13 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do autocomplete="off" placeholder="e.g. From Blog to Purchase" autofocus - class="w-full focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2" + label="Funnel Name" />
- +
@@ -123,27 +119,21 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do

<%= if @evaluation_result do %> Last month conversion rate: <%= List.last(@evaluation_result.steps).conversion_rate %>% - <% else %> - - Choose minimum <%= Funnel.min_steps() %> steps to evaluate funnel. - <% end %>

-
- map_size(@selections_made) - } - > - <%= if @funnel, do: "Update", else: "Add" %> Funnel → - -
+ map_size(@selections_made) + } + > + <%= if @funnel, do: "Update", else: "Add" %> Funnel +
@@ -200,7 +190,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do class="border-dotted border-b border-gray-400 " tooltip="Sample calculation for last month" > - <%= PlausibleWeb.StatsView.large_number_format(@result.entering_visitors) %> + <%= PlausibleWeb.StatsView.large_number_format(@result.entering_visitors) %> 0}> diff --git a/extra/lib/plausible_web/live/funnel_settings/list.ex b/extra/lib/plausible_web/live/funnel_settings/list.ex index b29f8ec96701..510304814550 100644 --- a/extra/lib/plausible_web/live/funnel_settings/list.ex +++ b/extra/lib/plausible_web/live/funnel_settings/list.ex @@ -9,94 +9,47 @@ defmodule PlausibleWeb.Live.FunnelSettings.List do """ use Phoenix.LiveComponent use Phoenix.HTML + import PlausibleWeb.Components.Generic def render(assigns) do ~H"""
-
-
-
-
-
- -
- -
+ <.filter_bar filter_text={@filter_text} placeholder="Search Funnels"> + <.button id="add-funnel-button" phx-click="add-funnel" mt?={false}> + Add Funnel + + - -
-
-
- - + Add Funnel - -
-
<%= if Enum.count(@funnels) > 0 do %> -
- <%= for funnel <- @funnels do %> -
- - <%= funnel.name %> - - <%= funnel.steps_count %>-step funnel - + <.table rows={@funnels}> + <:tbody :let={funnel}> + <.td truncate> + <%= funnel.name %> + + <.td hide_on_mobile> + + <%= funnel.steps_count %>-step funnel -
- - - - -
-
- <% end %> -
+ + <.td actions> + <.edit_button phx-click="edit-funnel" phx-value-funnel-id={funnel.id} /> + <.delete_button + id={"delete-funnel-#{funnel.id}"} + phx-click="delete-funnel" + phx-value-funnel-id={funnel.id} + class="text-sm text-red-600" + data-confirm={"Are you sure you want to remove funnel '#{funnel.name}'? This will just affect the UI, all of your analytics data will stay intact."} + /> + + + <% else %> -

+

No funnels found for this site. Please refine or - + <.styled_link phx-click="reset-filter-text" id="reset-filter-hint"> reset your search. - + No funnels configured for this site. diff --git a/lib/plausible_web.ex b/lib/plausible_web.ex index b4f75b69950f..3eefaad83b3f 100644 --- a/lib/plausible_web.ex +++ b/lib/plausible_web.ex @@ -13,6 +13,8 @@ defmodule PlausibleWeb do alias PlausibleWeb.Router.Helpers, as: Routes alias Phoenix.LiveView.JS + + import PlausibleWeb.Components.Generic end end diff --git a/lib/plausible_web/components/billing/billing.ex b/lib/plausible_web/components/billing/billing.ex index 90709745c845..65bd1e29e84c 100644 --- a/lib/plausible_web/components/billing/billing.ex +++ b/lib/plausible_web/components/billing/billing.ex @@ -79,14 +79,12 @@ defmodule PlausibleWeb.Components.Billing do pad title="Pageviews" usage={@usage.pageviews} - class="font-normal text-gray-500 dark:text-gray-400" /> <.usage_and_limits_row id={"custom_events_#{@period}"} pad title="Custom events" usage={@usage.custom_events} - class="font-normal text-gray-500 dark:text-gray-400" /> """ @@ -169,10 +167,10 @@ defmodule PlausibleWeb.Components.Billing do def usage_and_limits_row(assigns) do ~H""" - + <%= @title %> - + <%= Cldr.Number.to_string!(@usage) %> <%= if is_number(@limit), do: "/ #{Cldr.Number.to_string!(@limit)}" %> @@ -184,8 +182,7 @@ defmodule PlausibleWeb.Components.Billing do ~H"""

Monthly quota

@@ -245,7 +242,7 @@ defmodule PlausibleWeb.Components.Billing do id={@id} onclick={"if (#{@confirmed}) {Paddle.Checkout.open(#{Jason.encode!(%{product: @paddle_product_id, email: @user.email, disableLogout: true, passthrough: @user.id, success: Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), theme: "none"})})}"} class={[ - "w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white", + "text-sm w-full mt-6 block rounded-md py-2 px-3 text-center font-semibold leading-6 text-white", !@checkout_disabled && "bg-indigo-600 hover:bg-indigo-500", @checkout_disabled && "pointer-events-none bg-gray-400 dark:bg-gray-600" ]} diff --git a/lib/plausible_web/components/billing/notice.ex b/lib/plausible_web/components/billing/notice.ex index e794c22ecf76..2a4236e95da3 100644 --- a/lib/plausible_web/components/billing/notice.ex +++ b/lib/plausible_web/components/billing/notice.ex @@ -63,7 +63,6 @@ defmodule PlausibleWeb.Components.Billing.Notice do attr(:current_user, User, required: true) attr(:feature_mod, :atom, required: true, values: Feature.list()) attr(:grandfathered?, :boolean, default: false) - attr(:size, :atom, default: :sm) attr(:rest, :global) def premium_feature(assigns) do @@ -71,7 +70,6 @@ defmodule PlausibleWeb.Components.Billing.Notice do <.notice :if={@feature_mod.check_availability(@billable_user) !== :ok} class="rounded-t-md rounded-b-none" - size={@size} title="Notice" {@rest} > diff --git a/lib/plausible_web/components/flow_progress.ex b/lib/plausible_web/components/flow_progress.ex index 084a460c3ebc..300677c99c57 100644 --- a/lib/plausible_web/components/flow_progress.ex +++ b/lib/plausible_web/components/flow_progress.ex @@ -21,9 +21,9 @@ defmodule PlausibleWeb.Components.FlowProgress do ~H"""