diff --git a/extra/lib/plausible_web/live/funnel_settings.ex b/extra/lib/plausible_web/live/funnel_settings.ex index cf98c3ddc..2a00a028b 100644 --- a/extra/lib/plausible_web/live/funnel_settings.ex +++ b/extra/lib/plausible_web/live/funnel_settings.ex @@ -2,9 +2,8 @@ defmodule PlausibleWeb.Live.FunnelSettings do @moduledoc """ LiveView allowing listing, creating and deleting funnels. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Phoenix.HTML - use PlausibleWeb.Live.Flash use Plausible.Funnel diff --git a/extra/lib/plausible_web/live/funnel_settings/form.ex b/extra/lib/plausible_web/live/funnel_settings/form.ex index be0159816..37e7525ed 100644 --- a/extra/lib/plausible_web/live/funnel_settings/form.ex +++ b/extra/lib/plausible_web/live/funnel_settings/form.ex @@ -5,7 +5,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do to allow building searchable funnel definitions out of list of goals available. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Plausible.Funnel import PlausibleWeb.Live.Components.Form diff --git a/lib/plausible_web.ex b/lib/plausible_web.ex index 4c2cb6b4b..649b33ee4 100644 --- a/lib/plausible_web.ex +++ b/lib/plausible_web.ex @@ -1,4 +1,19 @@ defmodule PlausibleWeb do + def live_view(opts \\ []) do + quote do + use Plausible + use Phoenix.LiveView, global_prefixes: ~w(x-) + use PlausibleWeb.Live.Flash + + unless :no_sentry_context in unquote(opts) do + use PlausibleWeb.Live.SentryContext + end + + alias PlausibleWeb.Router.Helpers, as: Routes + alias Phoenix.LiveView.JS + end + end + def controller do quote do use Phoenix.Controller, namespace: PlausibleWeb @@ -86,4 +101,8 @@ defmodule PlausibleWeb do defmacro __using__(which) when is_atom(which) do apply(__MODULE__, which, []) end + + defmacro __using__([{which, opts}]) when is_atom(which) do + apply(__MODULE__, which, [List.wrap(opts)]) + end end diff --git a/lib/plausible_web/endpoint.ex b/lib/plausible_web/endpoint.ex index c4a1fa247..5b6c6f930 100644 --- a/lib/plausible_web/endpoint.ex +++ b/lib/plausible_web/endpoint.ex @@ -17,7 +17,12 @@ defmodule PlausibleWeb.Endpoint do socket("/live", Phoenix.LiveView.Socket, websocket: [ check_origin: true, - connect_info: [session: {__MODULE__, :runtime_session_opts, []}] + connect_info: [ + :peer_data, + :uri, + :user_agent, + session: {__MODULE__, :runtime_session_opts, []} + ] ] ) diff --git a/lib/plausible_web/live/choose_plan.ex b/lib/plausible_web/live/choose_plan.ex index 4a8ee85a1..24dbc7494 100644 --- a/lib/plausible_web/live/choose_plan.ex +++ b/lib/plausible_web/live/choose_plan.ex @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do @moduledoc """ LiveView for upgrading to a plan, or changing an existing plan. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Phoenix.HTML require Plausible.Billing.Subscription.Status diff --git a/lib/plausible_web/live/goal_settings.ex b/lib/plausible_web/live/goal_settings.ex index 6a9bef179..081066f30 100644 --- a/lib/plausible_web/live/goal_settings.ex +++ b/lib/plausible_web/live/goal_settings.ex @@ -2,9 +2,8 @@ defmodule PlausibleWeb.Live.GoalSettings do @moduledoc """ LiveView allowing listing, creating and deleting goals. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Phoenix.HTML - use PlausibleWeb.Live.Flash alias Plausible.{Sites, Goals} diff --git a/lib/plausible_web/live/goal_settings/form.ex b/lib/plausible_web/live/goal_settings/form.ex index 285d1bd2d..c01613150 100644 --- a/lib/plausible_web/live/goal_settings/form.ex +++ b/lib/plausible_web/live/goal_settings/form.ex @@ -2,8 +2,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do @moduledoc """ Live view for the goal creation form """ - use Phoenix.LiveView - use Plausible + use PlausibleWeb, :live_view import PlausibleWeb.Live.Components.Form alias PlausibleWeb.Live.Components.ComboBox diff --git a/lib/plausible_web/live/plugins/api/settings.ex b/lib/plausible_web/live/plugins/api/settings.ex index 82b287ddd..53e46b078 100644 --- a/lib/plausible_web/live/plugins/api/settings.ex +++ b/lib/plausible_web/live/plugins/api/settings.ex @@ -2,9 +2,9 @@ defmodule PlausibleWeb.Live.Plugins.API.Settings do @moduledoc """ LiveView allowing listing, creating and revoking Plugins API tokens. """ - use Phoenix.LiveView + + use PlausibleWeb, :live_view use Phoenix.HTML - use PlausibleWeb.Live.Flash alias Plausible.Sites alias Plausible.Plugins.API.Tokens diff --git a/lib/plausible_web/live/plugins/api/token_form.ex b/lib/plausible_web/live/plugins/api/token_form.ex index 5180b177b..3e641fced 100644 --- a/lib/plausible_web/live/plugins/api/token_form.ex +++ b/lib/plausible_web/live/plugins/api/token_form.ex @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do @moduledoc """ Live view for the goal creation form """ - use Phoenix.LiveView + use PlausibleWeb, live_view: :no_sentry_context import PlausibleWeb.Live.Components.Form alias Plausible.Repo diff --git a/lib/plausible_web/live/props_settings.ex b/lib/plausible_web/live/props_settings.ex index d48c6fee3..b850d656b 100644 --- a/lib/plausible_web/live/props_settings.ex +++ b/lib/plausible_web/live/props_settings.ex @@ -3,9 +3,8 @@ defmodule PlausibleWeb.Live.PropsSettings do LiveView allowing listing, allowing and disallowing custom event properties. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Phoenix.HTML - use PlausibleWeb.Live.Flash alias PlausibleWeb.Live.Components.ComboBox diff --git a/lib/plausible_web/live/props_settings/form.ex b/lib/plausible_web/live/props_settings/form.ex index 8e715a1b7..a1cc47d6a 100644 --- a/lib/plausible_web/live/props_settings/form.ex +++ b/lib/plausible_web/live/props_settings/form.ex @@ -2,7 +2,7 @@ defmodule PlausibleWeb.Live.PropsSettings.Form do @moduledoc """ Live view for the custom props creation form """ - use Phoenix.LiveView + use PlausibleWeb, :live_view import PlausibleWeb.Live.Components.Form alias PlausibleWeb.Live.Components.ComboBox diff --git a/lib/plausible_web/live/register_form.ex b/lib/plausible_web/live/register_form.ex index 258c6b52a..619504318 100644 --- a/lib/plausible_web/live/register_form.ex +++ b/lib/plausible_web/live/register_form.ex @@ -3,17 +3,13 @@ defmodule PlausibleWeb.Live.RegisterForm do LiveView for registration form. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Phoenix.HTML - use Plausible - import PlausibleWeb.Live.Components.Form alias Plausible.Auth alias Plausible.Repo - alias PlausibleWeb.Router.Helpers, as: Routes - def mount(params, _session, socket) do socket = assign_new(socket, :invitation, fn -> diff --git a/lib/plausible_web/live/reset_password_form.ex b/lib/plausible_web/live/reset_password_form.ex index 49451afbc..6b757d4ec 100644 --- a/lib/plausible_web/live/reset_password_form.ex +++ b/lib/plausible_web/live/reset_password_form.ex @@ -3,7 +3,7 @@ defmodule PlausibleWeb.Live.ResetPasswordForm do LiveView for password reset form. """ - use Phoenix.LiveView + use PlausibleWeb, :live_view use Phoenix.HTML import PlausibleWeb.Live.Components.Form diff --git a/lib/plausible_web/live/sentry_context.ex b/lib/plausible_web/live/sentry_context.ex new file mode 100644 index 000000000..7511ddd80 --- /dev/null +++ b/lib/plausible_web/live/sentry_context.ex @@ -0,0 +1,76 @@ +defmodule PlausibleWeb.Live.SentryContext do + @moduledoc """ + This module tries to supply LiveViews with some common Sentry context + (without it, there is practically none). + + Use via `use PlausibleWeb.Live.SentryContext` in your LiveView module, + or preferably via `use PlausibleWeb, :live_view`. + + In case you have multiple LiveViews, there is `use PlausibleWeb, live_view: :no_sentry_context` + exposed that allows you to skip using this module. This is because + only the root LiveView has access to `connect_info` and an exception will be + thrown otherwise. + """ + + defmacro __using__(_) do + quote do + on_mount PlausibleWeb.Live.SentryContext + end + end + + def on_mount(:default, _params, session, socket) do + peer = Phoenix.LiveView.get_connect_info(socket, :peer_data) + uri = Phoenix.LiveView.get_connect_info(socket, :uri) + + user_agent = + Phoenix.LiveView.get_connect_info(socket, :user_agent) + + socket_host = + case socket.host_uri do + :not_mounted_at_router -> :not_mounted_at_router + %URI{host: host} -> host + end + + request_context = + %{ + host: socket_host, + env: %{ + "REMOTE_ADDR" => get_ip(peer), + "REMOTE_PORT" => peer && peer.port, + "SEVER_NAME" => uri && uri.host + } + } + + request_context = + if user_agent do + Map.merge(request_context, %{ + headers: %{ + "User-Agent" => user_agent + } + }) + else + request_context + end + + Sentry.Context.set_request_context(request_context) + + user_id = session["current_user_id"] + + if user_id do + Sentry.Context.set_user_context(%{ + id: user_id + }) + end + + {:cont, socket} + end + + defp get_ip(%{address: addr}) do + case :inet.ntoa(addr) do + {:error, _} -> "" + address -> to_string(address) + end + end + + defp get_ip(_), do: "" +end diff --git a/lib/plausible_web/live/sites.ex b/lib/plausible_web/live/sites.ex index b9e37a519..3ef7f2f19 100644 --- a/lib/plausible_web/live/sites.ex +++ b/lib/plausible_web/live/sites.ex @@ -3,11 +3,7 @@ defmodule PlausibleWeb.Live.Sites do LiveView for sites index. """ - use Phoenix.LiveView, global_prefixes: ~w(x-) - use PlausibleWeb.Live.Flash - use Plausible - - alias Phoenix.LiveView.JS + use PlausibleWeb, :live_view use Phoenix.HTML import PlausibleWeb.Components.Generic @@ -18,7 +14,6 @@ defmodule PlausibleWeb.Live.Sites do alias Plausible.Site alias Plausible.Sites alias Plausible.Site.Memberships.Invitations - alias PlausibleWeb.Router.Helpers, as: Routes def mount(params, %{"current_user_id" => user_id}, socket) do uri = diff --git a/test/plausible_web/live/sentry_context_test.exs b/test/plausible_web/live/sentry_context_test.exs new file mode 100644 index 000000000..feb0446bb --- /dev/null +++ b/test/plausible_web/live/sentry_context_test.exs @@ -0,0 +1,78 @@ +defmodule PlausibleWeb.Live.SentryContextTest do + use PlausibleWeb.ConnCase, async: true + import Phoenix.LiveViewTest + + defmodule SampleLV do + use PlausibleWeb, :live_view + + def mount(_params, %{"test" => test_pid}, socket) do + socket = assign(socket, test_pid: test_pid) + {:ok, socket} + end + + def render(assigns) do + ~H""" + ok computer + """ + end + + def handle_event("get_sentry_context", _params, socket) do + context = Sentry.Context.get_all() + send(socket.assigns.test_pid, {:context, context}) + {:noreply, socket} + end + end + + describe "sentry context" do + test "basic shape", %{conn: conn} do + context_hook(conn) + assert_receive {:context, context} + + assert %{ + extra: %{}, + request: %{ + env: %{ + "REMOTE_ADDR" => "127.0.0.1", + "REMOTE_PORT" => port, + "SEVER_NAME" => "www.example.com" + }, + host: :not_mounted_at_router + }, + user: %{}, + tags: %{}, + breadcrumbs: [] + } = context + + assert is_integer(port) + end + + test "user-agent is included", %{conn: conn} do + conn + |> put_req_header("user-agent", "Firefox") + |> context_hook() + + assert_receive {:context, context} + assert context.request.headers["User-Agent"] == "Firefox" + end + + test "user_id is included", %{conn: conn} do + context_hook(conn, %{"current_user_id" => 172}) + + assert_receive {:context, context} + assert context.user.id == 172 + end + end + + defp context_hook(conn, extra_session \\ %{}) do + lv = get_liveview(conn, extra_session) + assert render(lv) =~ "ok computer" + render_hook(lv, :get_sentry_context, %{}) + end + + defp get_liveview(conn, extra_session) do + {:ok, lv, _html} = + live_isolated(conn, SampleLV, session: Map.merge(%{"test" => self()}, extra_session)) + + lv + end +end