Repository: elixir-error-tracker/error-tracker (elixir)

Issue:
Add typespecs for public functions
Public functions are already documented, but adding typespecs would make them even better and more comprehensive.

Pull request:
Add typespecs for public functions
This PR adds type specs to public functions as requested

Closes #84 

Patch:
diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex
index c737cde..ea9b643 100644
--- a/lib/error_tracker.ex
+++ b/lib/error_tracker.ex
@@ -67,6 +67,11 @@ defmodule ErrorTracker do
   """
   @type context :: %{String.t() => any()}
 
+  @typedoc """
+  An `Exception` or a `{kind, payload}` tuple compatible with `Exception.normalize/3`.
+  """
+  @type exception :: Exception.t() | {:error, any()} | {Exception.non_error_kind(), any()}
+
   import Ecto.Query
 
   alias ErrorTracker.Error
@@ -107,6 +112,8 @@ defmodule ErrorTracker do
   * A `{kind, exception}` tuple in which case the information is converted to
   an Elixir exception (if possible) and stored.
   """
+
+  @spec report(exception(), Exception.stacktrace(), context()) :: Occurrence.t() | :noop
   def report(exception, stacktrace, given_context \\ %{}) do
     {kind, reason} = normalize_exception(exception, stacktrace)
     {:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
@@ -127,6 +134,7 @@ defmodule ErrorTracker do
   If an error is marked as resolved and it happens again, it will automatically
   appear as unresolved again.
   """
+  @spec resolve(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
   def resolve(error = %Error{status: :unresolved}) do
     changeset = Ecto.Changeset.change(error, status: :resolved)
 
@@ -139,6 +147,7 @@ defmodule ErrorTracker do
   @doc """
   Marks an error as unresolved.
   """
+  @spec unresolve(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
   def unresolve(error = %Error{status: :resolved}) do
     changeset = Ecto.Changeset.change(error, status: :unresolved)
 
diff --git a/lib/error_tracker/integrations/oban.ex b/lib/error_tracker/integrations/oban.ex
index be637d5..0553ae2 100644
--- a/lib/error_tracker/integrations/oban.ex
+++ b/lib/error_tracker/integrations/oban.ex
@@ -12,6 +12,20 @@ defmodule ErrorTracker.Integrations.Oban do
   It works using Oban's Telemetry events, so you don't need to modify anything
   on your application.
 
+  > #### A note on errors grouping {: .warning}
+  >
+  > All errors reported using `:error` or `{:error, any()}` as the output of
+  > your `perform/2` worker function are going to be grouped together (one group
+  > of those of errors per worker).
+  >
+  > The reason of that behaviour is that those errors do not generate an exception,
+  > so no stack trace is detected and they are stored as happening in the same
+  > place.
+  >
+  > If you want errors of your workers to be grouped as you may expect on other
+  > integrations, you should raise exceptions to report errors instead of gracefully
+  > returning an error value.
+
   ### Default context
 
   By default we store some context for you on errors generated in an Oban
@@ -58,9 +72,14 @@ defmodule ErrorTracker.Integrations.Oban do
   end
 
   def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do
-    %{reason: exception, stacktrace: stacktrace} = metadata
+    %{reason: exception, stacktrace: stacktrace, job: job} = metadata
     state = Map.get(metadata, :state, :failure)
 
+    stacktrace =
+      if stacktrace == [],
+        do: [{String.to_existing_atom("Elixir." <> job.worker), :perform, 2, []}],
+        else: stacktrace
+
     ErrorTracker.report(exception, stacktrace, %{state: state})
   end
 end
diff --git a/lib/error_tracker/migration.ex b/lib/error_tracker/migration.ex
index e6fdc19..cc098c8 100644
--- a/lib/error_tracker/migration.ex
+++ b/lib/error_tracker/migration.ex
@@ -96,14 +96,17 @@ defmodule ErrorTracker.Migration do
   @callback down(Keyword.t()) :: :ok
   @callback current_version(Keyword.t()) :: non_neg_integer()
 
+  @spec up(Keyword.t()) :: :ok
   def up(opts \\ []) when is_list(opts) do
     migrator().up(opts)
   end
 
+  @spec down(Keyword.t()) :: :ok
   def down(opts \\ []) when is_list(opts) do
     migrator().down(opts)
   end
 
+  @spec migrated_version(Keyword.t()) :: non_neg_integer()
   def migrated_version(opts \\ []) when is_list(opts) do
     migrator().migrated_version(opts)
   end
diff --git a/lib/error_tracker/schemas/error.ex b/lib/error_tracker/schemas/error.ex
index 2c4ab45..de7efd9 100644
--- a/lib/error_tracker/schemas/error.ex
+++ b/lib/error_tracker/schemas/error.ex
@@ -12,6 +12,8 @@ defmodule ErrorTracker.Error do
 
   use Ecto.Schema
 
+  @type t :: %__MODULE__{}
+
   schema "error_tracker_errors" do
     field :kind, :string
     field :reason, :string
@@ -32,7 +34,7 @@ defmodule ErrorTracker.Error do
 
     {source_line, source_function} =
       if source do
-        source_line = if source.line, do: "#{source.file}:#{source.line}", else: "nofile"
+        source_line = if source.line, do: "#{source.file}:#{source.line}", else: "(nofile)"
         source_function = "#{source.module}.#{source.function}/#{source.arity}"
 
         {source_line, source_function}
diff --git a/lib/error_tracker/schemas/occurrence.ex b/lib/error_tracker/schemas/occurrence.ex
index 4729e2e..8cf0b5c 100644
--- a/lib/error_tracker/schemas/occurrence.ex
+++ b/lib/error_tracker/schemas/occurrence.ex
@@ -6,10 +6,13 @@ defmodule ErrorTracker.Occurrence do
   in which the exception raised.
   """
 
+  import Ecto.Changeset
+
   use Ecto.Schema
 
   require Logger
-  import Ecto.Changeset
+
+  @type t :: %__MODULE__{}
 
   schema "error_tracker_occurrences" do
     field :context, :map
diff --git a/lib/error_tracker/schemas/stacktrace.ex b/lib/error_tracker/schemas/stacktrace.ex
index cf22f31..24e8bb0 100644
--- a/lib/error_tracker/schemas/stacktrace.ex
+++ b/lib/error_tracker/schemas/stacktrace.ex
@@ -6,6 +6,8 @@ defmodule ErrorTracker.Stacktrace do
 
   use Ecto.Schema
 
+  @type t :: %__MODULE__{}
+
   @primary_key false
   embedded_schema do
     embeds_many :lines, Line, primary_key: false do
diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex
index 16ecb87..b14cda4 100644
--- a/lib/error_tracker/web/live/show.html.heex
+++ b/lib/error_tracker/web/live/show.html.heex
@@ -48,7 +48,7 @@
                 <td class="px-2 align-top"><pre>(<%= line.application || @app %>)</pre></td>
                 <td>
                   <pre><%= "#{sanitize_module(line.module)}.#{line.function}/#{line.arity}" %>
-                <%= if line.line, do: "#{line.file}:#{line.line}", else: "nofile" %></pre>
+                <%= if line.line, do: "#{line.file}:#{line.line}", else: "(nofile)" %></pre>
                 </td>
               </tr>
             </tbody>
diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex
index 7e6c7b0..58a2b44 100644
--- a/lib/error_tracker/web/router.ex
+++ b/lib/error_tracker/web/router.ex
@@ -34,7 +34,7 @@ defmodule ErrorTracker.Web.Router do
       # paths for navigating through the dashboard.
       scoped_path = Phoenix.Router.scoped_path(__MODULE__, path)
       # Generate the session name and session hooks.
-      {session_name, session_opts} = parse_options(opts, scoped_path)
+      {session_name, session_opts} = ErrorTracker.Web.Router.__parse_options__(opts, scoped_path)
 
       scope path, alias: false, as: false do
         import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]
@@ -49,7 +49,7 @@ defmodule ErrorTracker.Web.Router do
   end
 
   @doc false
-  def parse_options(opts, path) do
+  def __parse_options__(opts, path) do
     custom_on_mount = Keyword.get(opts, :on_mount, [])
     session_name = Keyword.get(opts, :as, :error_tracker_dashboard)
 


Test patch:
diff --git a/test/error_tracker_test.exs b/test/error_tracker_test.exs
index f8018b3..8d9ea79 100644
--- a/test/error_tracker_test.exs
+++ b/test/error_tracker_test.exs
@@ -40,7 +40,7 @@ defmodule ErrorTrackerTest do
         assert error.source_line =~ @relative_file_path
       else
         assert error.source_function == "erlang.+/2"
-        assert error.source_line == "nofile"
+        assert error.source_line == "(nofile)"
       end
     end
 
@@ -54,7 +54,7 @@ defmodule ErrorTrackerTest do
       assert error.kind == to_string(UndefinedFunctionError)
       assert error.reason =~ "is undefined or private"
       assert error.source_function == Exception.format_mfa(m, f, Enum.count(a))
-      assert error.source_line == "nofile"
+      assert error.source_line == "(nofile)"
     end
 
     test "reports throws" do