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 """ """ 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