lib/stacktrace_cleaner.ex

defmodule StacktraceCleaner do
  @moduledoc """
  Stacktraces often include many lines that are not relevant for the context under review.
  This makes it hard to find the signal amongst many noise, and adds debugging time.
  StacktraceCleaner is a module to remove those noises and make them easier to see.
  """
  @noise_paths ["process", "ex_unit", ".erl"]
  @type type_stacktrace :: {atom(), atom(), integer, [file: charlist(), line: integer()]}

  @doc """
  Extracts first stacktrace from `StacktraceCleaner.current_stacktraces`
    * match_path: If you want to extract more strictly, you can specify it with an argument.
  """
  @spec current_stacktrace(String.t() | nil) :: type_stacktrace
  def current_stacktrace(match_path \\ nil) do
    [stacktrace | _] = current_stacktraces(match_path)
    stacktrace
  end

  @doc """
  Gets cleaned stacktraces.
    * match_path: If you want to extract more strictly, you can specify it with an argument.
  """
  @spec current_stacktraces(String.t() | nil) :: list(type_stacktrace)
  def current_stacktraces(match_path \\ nil) do
    Process.info(self(), :current_stacktrace)
    |> elem(1)
    |> clean(match_path)
  end

  @doc """
  Cleans stacktraces.
    * match_path: If you want to extract more strictly, you can specify it with an argument.
  """
  @spec clean(list(type_stacktrace), String.t() | nil) :: list(type_stacktrace)
  def clean(stacktraces, match_path \\ nil) do
    deps_app_regexes = create_deps_app_regexes()

    cleaned =
      stacktraces
      |> Enum.reject(fn stacktrace ->
        stacktrace_path = stacktrace |> elem(3) |> Keyword.get(:file) |> to_string()
        deps_app_regexes |> Enum.find(&(stacktrace_path =~ &1))
      end)

    if match_path do
      cleaned
      |> Enum.filter(fn stacktrace ->
        stacktrace |> elem(3) |> Keyword.get(:file) |> to_string() =~ Regex.compile!(match_path)
      end)
    else
      cleaned
    end
  end

  defp create_deps_app_regexes do
    (Mix.Project.deps_apps() ++ @noise_paths)
    |> Enum.filter(&(!is_nil(&1)))
    |> Enum.map(fn app ->
      app
      |> to_string()
      |> Regex.compile!()
    end)
  end
end