Packages
phoenix_echo
0.1.0
Forward browser console logs and errors to Phoenix server logs via WebSocket
Current section
Files
Jump to
Current section
Files
lib/phoenix_echo.ex
defmodule PhoenixEcho do
@moduledoc """
Forward browser console logs and errors to Phoenix server logs.
## Installation
1. Add to your dependencies in `mix.exs`:
```elixir
{:phoenix_echo, "~> 0.1.0"}
```
2. Add to your endpoint (`lib/my_app_web/endpoint.ex`), before the router plug:
```elixir
use PhoenixEcho.Endpoint
plug MyAppWeb.Router
```
3. Configure in `config/dev.exs`:
```elixir
config :phoenix_echo,
enabled: true,
prefix: "[Browser]",
log_level: :debug
```
## Manual Setup (alternative)
If you prefer manual control, instead of step 2 above:
```elixir
socket "/__phoenix_echo", PhoenixEcho.Socket,
websocket: true,
longpoll: false
plug PhoenixEcho.Plug
```
## Configuration Options
- `:enabled` - Enable/disable logging (default: `true`)
- `:prefix` - Prefix for log messages (default: `"[Browser]"`)
- `:log_level` - Minimum log level to display (default: `:debug`)
- `:include_stacktrace` - Include stacktrace for errors (default: `true`)
"""
require Logger
@default_prefix "[Browser]"
@default_log_level :debug
@type log_level :: String.t() | :error | :warning | :warn | :info | :debug
@doc """
Format and log a browser message.
"""
@spec log(String.t(), list() | term(), map()) :: :ok | nil
def log(level, args, metadata \\ %{}) do
if enabled?() do
prefix = config(:prefix, @default_prefix)
formatted = format_args(args)
stacktrace = Map.get(metadata, "stack", nil)
message =
if stacktrace && config(:include_stacktrace, true) do
"#{prefix} #{formatted}\n #{format_stacktrace(stacktrace)}"
else
"#{prefix} #{formatted}"
end
log_with_level(level, message)
end
end
@doc """
Check if browser logging is enabled.
"""
@spec enabled?() :: boolean()
def enabled? do
config(:enabled, true)
end
@doc """
Get the JavaScript client code.
"""
@spec js_client() :: String.t()
def js_client do
priv_dir = :code.priv_dir(:phoenix_echo)
Path.join(priv_dir, "static/phoenix_echo.js")
|> File.read!()
end
@doc """
Get the script tag to inject into HTML.
"""
@spec script_tag() :: String.t()
def script_tag do
"""
<script>
#{js_client()}
</script>
"""
end
# Private functions
defp config(key, default) do
Application.get_env(:phoenix_echo, key, default)
end
defp format_args(args) when is_list(args) do
args
|> Enum.map(&format_arg/1)
|> Enum.join(" ")
end
defp format_args(arg), do: format_arg(arg)
defp format_arg(arg) when is_binary(arg), do: arg
defp format_arg(arg) when is_number(arg), do: to_string(arg)
defp format_arg(arg) when is_boolean(arg), do: to_string(arg)
defp format_arg(nil), do: "null"
defp format_arg(arg) when is_map(arg) or is_list(arg) do
case Jason.encode(arg, pretty: false) do
{:ok, json} -> json
{:error, _} -> inspect(arg)
end
end
defp format_arg(arg), do: inspect(arg)
defp format_stacktrace(stack) when is_binary(stack) do
stack
|> String.split("\n")
|> Enum.map(&(" " <> &1))
|> Enum.join("\n")
end
defp format_stacktrace(_), do: ""
defp log_with_level(level, message) do
min_level = config(:log_level, @default_log_level)
if should_log?(level, min_level) do
case level do
"error" -> Logger.error(message)
"warn" -> Logger.warning(message)
"info" -> Logger.info(message)
"debug" -> Logger.debug(message)
"log" -> Logger.info(message)
_ -> Logger.debug(message)
end
end
end
defp should_log?(level, min_level) do
level_priority(level) >= level_priority(min_level)
end
defp level_priority("error"), do: 4
defp level_priority(:error), do: 4
defp level_priority("warn"), do: 3
defp level_priority(:warning), do: 3
defp level_priority(:warn), do: 3
defp level_priority("info"), do: 2
defp level_priority(:info), do: 2
defp level_priority("log"), do: 2
defp level_priority(_), do: 1
end