PhoenixDevToolbar.Truncate (PhoenixDevToolbar v0.0.2)

Copy Markdown View Source

Bounds payloads before they are stored (ADR 03): plugins use these helpers in PhoenixDevToolbar.Plugin.collect/3, which runs inside host processes. Every function is pure, cheap on small inputs and bounded on large ones. redact/2 hides the values of sensitive keys (config :phoenix_dev_toolbar, redact: [...], by default keys containing "password", "token" or "secret").

Summary

Functions

Formats an exception, throw or exit as a bounded one line banner. Exception.format_banner/2 inspects throw and exit reasons without limits.

Inspects a value with the toolbar limits.

Redacted, bounded inspect of any value, at most bytes long.

Replaces the value of sensitive keys with "[REDACTED]".

Caps a SQL string at 8192 bytes.

Formats the first 10 stacktrace frames that do not belong to the toolbar, to :telemetry or to the extra skip modules and namespaces (an Elixir module skips itself and every module nested under it).

Caps any text (exception messages, banners) at limit bytes, 2048 by default.

Types

changes()

@type changes() :: %{
  keys: [term()],
  values: %{required(term()) => String.t()},
  omitted: non_neg_integer()
}

Functions

banner(kind, reason)

@spec banner(:error | :exit | :throw, term()) :: String.t()

Formats an exception, throw or exit as a bounded one line banner. Exception.format_banner/2 inspects throw and exit reasons without limits.

iex> PhoenixDevToolbar.Truncate.banner(:error, %RuntimeError{message: "bad"})
"** (RuntimeError) bad"

iex> PhoenixDevToolbar.Truncate.banner(:throw, :oops)
"** (throw) :oops"

inspect_value(value, opts \\ [])

@spec inspect_value(term(), keyword()) :: String.t()

Inspects a value with the toolbar limits.

iex> PhoenixDevToolbar.Truncate.inspect_value(Enum.to_list(1..60)) |> String.ends_with?("...]")
true

preview(value, bytes \\ 360, redact \\ lowered_redact_keys())

@spec preview(term(), pos_integer(), [String.t()]) :: String.t()

Redacted, bounded inspect of any value, at most bytes long.

Unlike redact/2, the cost does not depend on the value: at most 200 nodes and 20 items per container are walked, and whatever is not walked is replaced by "…", so a sensitive key deep in a large value is cut, never shown.

iex> PhoenixDevToolbar.Truncate.preview(%{user: %{password: "hunter2"}})
~s(%{user: %{password: "[REDACTED]"}})

iex> PhoenixDevToolbar.Truncate.preview(Enum.to_list(1..30))
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...]"

redact(value, keys \\ redact_keys())

@spec redact(term(), [String.t() | atom()]) :: term()

Replaces the value of sensitive keys with "[REDACTED]".

A key is sensitive when its name (atom or string, case insensitive, the first 256 bytes of it) contains one of keys. Walks maps, structs, keyword lists, lists and tuples, at most 50 entries per container and 1000 nodes in total, so the cost does not depend on the value.

Contract: a sensitive key's value is "[REDACTED]"; anything the walk does not reach is replaced by "…", never returned as is. The rest of a list or tuple becomes a final "…" element (so does the tail of an improper list), the rest of a map becomes a "…" => "…" entry, and a struct with more than 50 fields becomes a plain map with that entry. Scalars, including large binaries, are returned as they are (they are cheap; inspect_value/2 bounds their size). Key matching only sees map, struct and keyword keys: a bare {"password", "x"} tuple outside a list, a charlist key and a secret inside a string value are not recognised.

iex> PhoenixDevToolbar.Truncate.redact(%{"user" => %{"password" => "hunter2", "name" => "Ada"}})
%{"user" => %{"password" => "[REDACTED]", "name" => "Ada"}}

iex> PhoenixDevToolbar.Truncate.redact([api_token: "abc", page: 1])
[api_token: "[REDACTED]", page: 1]

iex> PhoenixDevToolbar.Truncate.redact({:login, %{password: "hunter2"}})
{:login, %{password: "[REDACTED]"}}

iex> PhoenixDevToolbar.Truncate.redact([%{password: "hunter2"} | :tail])
[%{password: "[REDACTED]"}, "…"]

sql(sql)

@spec sql(String.t() | nil) :: String.t() | nil

Caps a SQL string at 8192 bytes.

iex> PhoenixDevToolbar.Truncate.sql("SELECT 1")
"SELECT 1"

iex> PhoenixDevToolbar.Truncate.sql(nil)
nil

stacktrace(frames, skip \\ [])

@spec stacktrace(Exception.stacktrace() | nil, [module()]) :: [String.t()]

Formats the first 10 stacktrace frames that do not belong to the toolbar, to :telemetry or to the extra skip modules and namespaces (an Elixir module skips itself and every module nested under it).

iex> PhoenixDevToolbar.Truncate.stacktrace([{MyApp.Users, :list, 0, [file: ~c"lib/users.ex", line: 3]}])
["lib/users.ex:3: MyApp.Users.list/0"]

iex> PhoenixDevToolbar.Truncate.stacktrace([{Ecto.Repo.Queryable, :all, 3, []}], [Ecto])
[]

iex> PhoenixDevToolbar.Truncate.stacktrace(nil)
[]

string(text, limit \\ 2048)

@spec string(String.t(), pos_integer()) :: String.t()

Caps any text (exception messages, banners) at limit bytes, 2048 by default.

iex> PhoenixDevToolbar.Truncate.string("boom")
"boom"

iex> "x" |> String.duplicate(3000) |> PhoenixDevToolbar.Truncate.string() |> byte_size()
2060