defmodule Honeybadger.Utils do
@moduledoc """
Assorted helper functions used through out the Honeybadger package.
"""
@doc """
Internally all modules are prefixed with Elixir. This function removes the
`Elixir` prefix from the module when it is converted to a string.
# Example
iex> Honeybadger.Utils.module_to_string(Honeybadger.Utils)
"Honeybadger.Utils"
"""
def module_to_string(module) do
module
|> Module.split()
|> Enum.join(".")
end
@doc """
Transform value into a consistently cased string representation
# Example
iex> Honeybadger.Utils.canonicalize(:User_SSN)
"user_ssn"
"""
def canonicalize(val) do
val
|> to_string()
|> String.downcase()
end
@doc """
Configurable data sanitization. This currently:
- recursively truncates deep structures (to a depth of 20)
- constrains large string values (to 64k)
- filters out any map keys that might contain sensitive information.
"""
@depth_token "[DEPTH]"
@truncated_token "[TRUNCATED]"
@filtered_token "[FILTERED]"
# 64k with enough space to concat truncated_token
@default_max_string_size 64 * 1024 - 11
@default_max_depth 20
def sanitize(value, opts \\ []) do
base = %{
max_depth: @default_max_depth,
max_string_size: @default_max_string_size,
filter_keys: Honeybadger.get_env(:filter_keys)
}
opts =
Enum.into(opts, base)
|> Map.update!(:filter_keys, fn v -> MapSet.new(v, &canonicalize/1) end)
sanitize_val(value, Map.put(opts, :depth, 0))
end
defp sanitize_val(v, %{depth: depth, max_depth: depth}) when is_map(v) or is_list(v) do
@depth_token
end
defp sanitize_val(%{__struct__: _} = struct, opts) do
sanitize_val(Map.from_struct(struct), opts)
end
defp sanitize_val(v, %{depth: depth, filter_keys: filter_keys} = opts) when is_map(v) do
for {key, val} <- v, into: %{} do
if MapSet.member?(filter_keys, canonicalize(key)) do
{key, @filtered_token}
else
{key, sanitize_val(val, Map.put(opts, :depth, depth + 1))}
end
end
end
defp sanitize_val(v, %{depth: depth} = opts) when is_list(v) do
Enum.map(v, &sanitize_val(&1, Map.put(opts, :depth, depth + 1)))
end
defp sanitize_val(v, %{max_string_size: max_string_size}) when is_binary(v) do
if String.valid?(v) and String.length(v) > max_string_size do
String.slice(v, 0, max_string_size) <> @truncated_token
else
v
end
end
defp sanitize_val(v, _), do: v
end