Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LiveView hook #722

Merged
merged 10 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[
import_deps: [:plug],
import_deps: [:plug, :phoenix, :phoenix_live_view],
inputs: [
"lib/**/*.ex",
"config/*.exs",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
otp: '25.3'

# Oldest supported Elixir/Erlang pair.
- elixir: '1.11.4'
otp: '21.3'
- elixir: '1.13.4-otp-22'
otp: '22.3.4'

steps:
- name: Check out this repository
Expand Down
155 changes: 155 additions & 0 deletions lib/sentry/live_view_hook.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
if Code.ensure_loaded?(Phoenix.LiveView) do
defmodule Sentry.LiveViewHook do
@moduledoc """
A module that provides a `Phoenix.LiveView` hook to add Sentry context and breadcrumbs.

*Available since v10.5.0.*

This module sets context and breadcrumbs for the live view process through
`Sentry.Context`. It sets things like:

* The request URL
* The user agent and user's IP address
* Breadcrumbs for events that happen within LiveView

To make this module work best, you'll need to fetch information from the LiveView's
WebSocket. You can do that when calling the `socket/3` macro in your Phoenix endpoint.
For example:

socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [:peer_data, :uri, :user_agent]]

## Examples

defmodule MyApp.UserLive do
use Phoenix.LiveView

on_mount Sentry.LiveViewHook

# ...
end

You can do the same at the router level:

live_session :default, on_mounbt: Sentry.LiveViewHook do
scope "..." do
# ...
end
end

You can also set this in your `MyAppWeb` module, so that all LiveViews that
`use MyAppWeb, :live_view` will have this hook.
"""

@moduledoc since: "10.5.0"

import Phoenix.LiveView, only: [attach_hook: 4, get_connect_info: 2]

alias Sentry.Context

require Logger

# See also:
# https://develop.sentry.dev/sdk/event-payloads/request/

@doc false
@spec on_mount(:default, map(), map(), struct()) :: {:cont, struct()}
def on_mount(:default, params, _session, socket), do: on_mount(params, socket)

## Helpers

defp on_mount(params, %Phoenix.LiveView.Socket{} = socket) do
Context.set_extra_context(%{socket_id: socket.id})
Context.set_request_context(%{url: socket.host_uri})

Context.add_breadcrumb(%{
category: "web.live_view.mount",
message: "Mounted live view",
data: params
})

if uri = get_connect_info(socket, :uri) do
Context.set_request_context(%{url: URI.to_string(uri)})
end

if user_agent = get_connect_info(socket, :user_agent) do
Context.set_extra_context(%{user_agent: user_agent})
end

# :peer_data returns t:Plug.Conn.Adapter.peer_data/0.
# https://hexdocs.pm/plug/Plug.Conn.Adapter.html#t:peer_data/0
if ip_address = socket |> get_connect_info(:peer_data) |> get_safe_ip_address() do
Context.set_user_context(%{ip_address: ip_address})
end

socket
|> maybe_attach_hook_handle_params()
|> attach_hook(__MODULE__, :handle_event, &handle_event_hook/3)
|> attach_hook(__MODULE__, :handle_info, &handle_info_hook/2)
catch
# We must NEVER raise an error in a hook, as it will crash the LiveView process
# and we don't want Sentry to be responsible for that.
kind, reason ->
Logger.error(
"Sentry.LiveView.on_mount hook errored out: #{Exception.format(kind, reason)}",
event_source: :logger
)

{:cont, socket}
else
socket -> {:cont, socket}
end

defp handle_event_hook(event, params, socket) do
Context.add_breadcrumb(%{
category: "web.live_view.event",
message: inspect(event),
data: %{event: event, params: params}
})

{:cont, socket}
end

defp handle_info_hook(message, socket) do
Context.add_breadcrumb(%{
category: "web.live_view.info",
message: inspect(message, pretty: true)
})

{:cont, socket}
end

defp handle_params_hook(params, uri, socket) do
Context.set_extra_context(%{socket_id: socket.id})
Context.set_request_context(%{url: uri})

Context.add_breadcrumb(%{
category: "web.live_view.params",
message: "#{uri}",
data: %{params: params, uri: uri}
})

{:cont, socket}
end

defp maybe_attach_hook_handle_params(socket) do
case socket.parent_pid do
nil -> attach_hook(socket, __MODULE__, :handle_params, &handle_params_hook/3)
pid when is_pid(pid) -> socket
end
end

defp get_safe_ip_address(%{ip_address: ip} = _peer_data) do
case :inet.ntoa(ip) do
ip_address when is_list(ip_address) -> List.to_string(ip_address)
{:error, _reason} -> nil
end
catch
_kind, _reason -> nil
end

defp get_safe_ip_address(_peer_data) do
nil
end
end
end
30 changes: 8 additions & 22 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ defmodule Sentry.Mixfile do
"Upgrade Guides": [~r{^pages/upgrade}]
],
groups_for_modules: [
"Plug and Phoenix": [Sentry.PlugCapture, Sentry.PlugContext],
"Plug and Phoenix": [Sentry.PlugCapture, Sentry.PlugContext, Sentry.LiveViewHook],
Loggers: [Sentry.LoggerBackend, Sentry.LoggerHandler],
"Data Structures": [Sentry.Attachment, Sentry.CheckIn],
HTTP: [Sentry.HTTPClient, Sentry.HackneyClient],
Expand Down Expand Up @@ -91,6 +91,8 @@ defmodule Sentry.Mixfile do
# Optional dependencies
{:hackney, "~> 1.8", optional: true},
{:jason, "~> 1.1", optional: true},
{:phoenix, "~> 1.6", optional: true},
{:phoenix_live_view, "~> 0.20", optional: true},
{:plug, "~> 1.6", optional: true},
{:telemetry, "~> 0.4 or ~> 1.0", optional: true},

Expand All @@ -99,27 +101,11 @@ defmodule Sentry.Mixfile do
{:dialyxir, "~> 1.0", only: [:test, :dev], runtime: false},
{:ex_doc, "~> 0.29.0", only: :dev},
{:excoveralls, "~> 0.17.1", only: [:test]},
{:phoenix, "~> 1.5", only: [:test]},
{:phoenix_html, "~> 2.0", only: [:test]}
] ++ maybe_oban_optional_dependency() ++ maybe_quantum_optional_dependency()
end

# TODO: Remove this once we drop support for Elixir < 1.13.
defp maybe_oban_optional_dependency do
if Version.match?(System.version(), "~> 1.13") do
[{:oban, "~> 2.17 and >= 2.17.6", only: [:test]}]
else
[]
end
end

# TODO: Remove this once we drop support for Elixir < 1.12.
defp maybe_quantum_optional_dependency do
if Version.match?(System.version(), "~> 1.12") do
[{:quantum, "~> 3.0", only: [:test]}]
else
[]
end
# Required by Phoenix.LiveView's testing
{:floki, ">= 0.30.0", only: :test},
{:oban, "~> 2.17 and >= 2.17.6", only: [:test]},
{:quantum, "~> 3.0", only: [:test]}
]
end

defp package do
Expand Down
Loading