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

Issue:
Similar errors from different stacktraces are grouped together
While reviewing errors collected by `error-tracker`, I stumbled upon the following error:

```
(Ecto.NoResultsError) expected at least one result but got none in query:
```

After looking through the error occurrences, I noticed two things:

1. actual Ecto query which results in the exception above - can be different,
2. the error may be coming from different unrelated places in the code

This makes it hard to look for similar errors, since I need to look through all occurrences. I think it would be nice if `error-tracker` collected errors more precisely, for example taking into account both exception message and stacktrace when calculating fingerprint.

Pull request:
Fix error grouping issue
The errors are being grouped in a wrong way because a type mismatch comparison was done between the application name and the stacktrace application name.

The fix is to convert the configured application name to a string and be able to compare it with the stacktrace application name.

You can test it putting this script on the root project:

<details>

```elixir
Mix.install([
  {:ecto_sql, "~> 3.0"},
  {:postgrex, "~> 0.1"},
  {:error_tracker, path: "."},
  {:bandit, "~> 1.0"}
])

### ErrorLogger config

Application.put_env(:error_tracker, :repo, Repo)
Application.put_env(:error_tracker, :otp_app, :myapp)
Application.put_env(:error_tracker, :enabled, true)

defmodule AddErrorTracker do
  use Ecto.Migration

  def up, do: ErrorTracker.Migration.up(version: 4)

  def down, do: ErrorTracker.Migration.down(version: 1)
end

### Common config

Application.put_env(:myapp, Repo, database: "mix_install_examples")

defmodule Repo do
  use Ecto.Repo, adapter: Ecto.Adapters.Postgres, otp_app: :myapp
end

defmodule Migration0 do
  use Ecto.Migration

  def change do
    create table("posts") do
      add(:title, :string)
      timestamps(type: :utc_datetime_usec)
    end
  end
end

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    field(:title, :string)
    timestamps(type: :utc_datetime_usec)
  end
end

defmodule Main do
  import Ecto.Query, warn: false

  def migrate do
    Repo.__adapter__().storage_down(Repo.config())
    :ok = Repo.__adapter__().storage_up(Repo.config())
    {:ok, _} = Repo.start_link([])
    Ecto.Migrator.run(Repo, [{0, Migration0}, {1, AddErrorTracker}], :up, all: true, log_migrations_sql: :info)
  end

  def register_app_and_modules do
    :application.load({:application, :myapp, [
      {:description, ~c"My sample application"},
      {:vsn, ~c"0.1.0"},
      {:modules, [Router]},
      {:registered, [Router]},
      {:applications, [:kernel, :stdlib]}
    ]})
  end

  def run_http_server do
    Supervisor.start_link([{Bandit, [plug: Router, port: 1234]}], strategy: :one_for_one)
  end

  def make_requests do
    :inets.start()
    :httpc.request("http://localhost:1234/route1")
    :httpc.request("http://localhost:1234/route2")
  end

  def check_what_was_collected do
    ErrorTracker.Error
    |> Repo.all()
    |> IO.inspect()

    ErrorTracker.Occurrence
    |> Repo.all()
    |> Enum.map(&Enum.take(&1.stacktrace.lines, 2))
    |> IO.inspect()
  end
end

defmodule Router do
  use Plug.Router
  use ErrorTracker.Integrations.Plug

  plug(:match)
  plug(:dispatch)

  get "/route1" do
    Repo.get!(Post, 1)
    send_resp(conn, 200, "Anything")
  end

  get "/route2" do
    Repo.get!(Post, 2)
    send_resp(conn, 200, "Anything")
  end
end

Main.migrate()
Main.register_app_and_modules()
Main.run_http_server()
Main.make_requests()
Main.check_what_was_collected() # <- demonstrates the issue

Process.sleep(:infinity)
```

</details>

This closes #132 

Patch:
diff --git a/lib/error_tracker/schemas/stacktrace.ex b/lib/error_tracker/schemas/stacktrace.ex
index 24e8bb0..eb3162d 100644
--- a/lib/error_tracker/schemas/stacktrace.ex
+++ b/lib/error_tracker/schemas/stacktrace.ex
@@ -55,7 +55,7 @@ defmodule ErrorTracker.Stacktrace do
   application, just the first line.
   """
   def source(stack = %__MODULE__{}) do
-    client_app = Application.fetch_env!(:error_tracker, :otp_app)
+    client_app = Application.fetch_env!(:error_tracker, :otp_app) |> to_string()
 
     Enum.find(stack.lines, &(&1.application == client_app)) || List.first(stack.lines)
   end


Test patch:
diff --git a/test/error_tracker_test.exs b/test/error_tracker_test.exs
index 00538fe..b5121ee 100644
--- a/test/error_tracker_test.exs
+++ b/test/error_tracker_test.exs
@@ -4,7 +4,10 @@ defmodule ErrorTrackerTest do
   alias ErrorTracker.Error
   alias ErrorTracker.Occurrence
 
-  @relative_file_path Path.relative_to(__ENV__.file, File.cwd!())
+  # We use this file path because for some reason the test scripts are not
+  # handled as part of the application, so the last line of the app executed is
+  # on the case module.
+  @relative_file_path "test/support/case.ex"
 
   describe inspect(&ErrorTracker.report/3) do
     setup context do
@@ -29,7 +32,7 @@ defmodule ErrorTrackerTest do
     test "reports badarith errors" do
       string_var = to_string(1)
 
-      %Occurrence{error: error = %Error{}} =
+      %Occurrence{error: error = %Error{}, stacktrace: %{lines: [last_line | _]}} =
         report_error(fn -> 1 + string_var end)
 
       assert error.kind == to_string(ArithmeticError)
@@ -37,10 +40,17 @@ defmodule ErrorTrackerTest do
 
       # Elixir 1.17.0 reports these errors differently than previous versions
       if Version.compare(System.version(), "1.17.0") == :lt do
-        assert error.source_line =~ @relative_file_path
+        assert last_line.module == "ErrorTrackerTest"
+        assert last_line.function =~ "&ErrorTracker.report/3 reports badarith errors"
+        assert last_line.arity == 1
+        assert last_line.file
+        assert last_line.line
       else
-        assert error.source_function == "erlang.+/2"
-        assert error.source_line == "(nofile)"
+        assert last_line.module == "erlang"
+        assert last_line.function == "+"
+        assert last_line.arity == 2
+        refute last_line.file
+        refute last_line.line
       end
     end