Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 1 addition & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,7 @@ En Elixir based built-in error tracking solution.

## Configuration

Set up the repository:

```elixir
config :error_tracker,
repo: MyApp.Repo
```

And you are ready to go!

By default Phoenix and Oban integrations will start registering exceptions.

If you want to also catch exceptions before your Phoenix Router (in plugs used
on your Endpoint) or your application just use `Plug` but not `Phoenix`, you can
attach to those errors with:

```elixir
defmodule MyApp.Endpoint do
use ErrorTracker.Integrations.Plug
end
```
Take a look at the [Getting Started](/guides/Getting%20Started.md) guide.

## Development

Expand Down Expand Up @@ -61,5 +42,3 @@ To do so you can execute this task in a separate terminal:
```
mix assets.watch
```


12 changes: 7 additions & 5 deletions dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Application.put_env(:error_tracker, ErrorTrackerDevWeb.Endpoint,

# Setup up the ErrorTracker configuration
Application.put_env(:error_tracker, :repo, ErrorTrackerDev.Repo)
Application.put_env(:error_tracker, :application, :error_tracker_dev)
Application.put_env(:error_tracker, :otp_app, :error_tracker_dev)
Application.put_env(:error_tracker, :prefix, "private")

defmodule ErrorTrackerDevWeb.PageController do
Expand All @@ -54,7 +54,7 @@ defmodule ErrorTrackerDevWeb.PageController do
def call(conn, :index) do
content(conn, """
<h2>ErrorTracker Dev Server</h2>
<div><a href="/errors">Open ErrorTracker</a></div>
<div><a href="/dev/errors">Open ErrorTracker</a></div>
<div><a href="/plug-exception">Generate Plug exception</a></div>
<div><a href="/404">Generate Router 404</a></div>
<div><a href="/noroute">Raise NoRouteError from a controller</a></div>
Expand Down Expand Up @@ -108,7 +108,9 @@ defmodule ErrorTrackerDevWeb.Router do
get "/exception", ErrorTrackerDevWeb.PageController, :exception
get "/exit", ErrorTrackerDevWeb.PageController, :exit

error_tracker_dashboard("/errors")
scope "/dev" do
error_tracker_dashboard("/errors")
end
end
end

Expand Down Expand Up @@ -143,8 +145,8 @@ end
defmodule Migration0 do
use Ecto.Migration

def up, do: ErrorTracker.Migrations.up(prefix: "private")
def down, do: ErrorTracker.Migrations.down(prefix: "private")
def up, do: ErrorTracker.Migration.up(prefix: "private")
def down, do: ErrorTracker.Migration.down(prefix: "private")
end

Application.put_env(:phoenix, :serve_endpoints, true)
Expand Down
128 changes: 128 additions & 0 deletions guides/Getting Started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Getting Started

This guide is an introduction to the ErrorTracker, an Elixir based built-in error tracking solution. The ErrorTracker provides a basic and free error-tracking solution integrated in your own application. It is designed to be easy to install and easy to use so you can integrate it in your existing project with minimal changes. The only requirement is a relational database in which errors will be tracked.

In this guide we will learn how to install the ErrorTracker in an Elixir project so you can start reporting errors as soon as possible. We will also cover more advanced topics such as how to report custom errors and how to add extra context to reported errors.

**This guide requires you to have setup Ecto with PostgreSQL beforehand.**

## Installing the ErrorTracking as a dependency

The first step add the ErrorTracker to your application is to declare the package as a dependency in your `mix.exs` file:

```elixir
# mix.exs
defp deps do
[
{:error_tracker, "~> 1.0"}
]
end
```

Once the ErrorTracker is declared as a dependency of your application, you can install it with the following command:

```bash
mix deps.get
```

## Configuring the ErrorTracker

The ErrorTracker needs a few configuration options to work. This configuration should be added in your `config/config.exs` file:

```elixir
config :error_tracker,
repo: MyApp.Repo,
otp_app: :my_app
```

The `:repo` option specifies the repository that will be used by the ErrorTracker. You can use your regular application repository, or a different one if you prefer to keep errors in a different database.

The `:otp_app` option specifies your application name. When an error happens the ErrorTracker will use this information to understand which parts of the stacktrace belong to your application and which parts belong to third party dependencies. This allows you to filter in-app vs third-party frames when viewing errors.

## Setting up the database

Since the ErrorTracker stores errors in the database you must create a database migration to add the required tables:

```
mix ecto.gen.migration add_error_tracker
```

Open the generated migration and call the `up` and `down` functions on `ErrorTracker.Migration`:

```elixir
defmodule MyApp.Repo.Migrations.AddErrorTracker do
use Ecto.Migration

def up, do: ErrorTracker.Migration.up()
def down, do: ErrorTracker.Migration.down()
end
```

You can run the migration and perform the database changes with the following command:

```bash
mix ecto.migrate
```

For more information about how to handle migrations take a look at the `ErrorTracker.Migration` module docs.

## Automatic error tracking

At this point, the ErrorTracker is ready to track errors. It will automatically start when your application boots and track errors that happen in your Phoenix controllers, Phoenix LiveViews and Oban jobs. The `ErrorTracker.Integrations.Phoenix` and `ErrorTracker.Integrations.Oban` provide detailed information about how this works.

If your application uses Plug but not Phoenix, you will need to add the relevant integration in your `Plug.Builder` or `Plug.Router` module.

```elixir
defmodule MyApp.Router do
use Plug.Router
use ErrorTracker.Integrations.Plug

# Your code here
end
```

This is also required if you want to track errors that happen in your Phoenix endpoint, before the Phoenix router starts handling the request. Keep in mind that this won't be needed in most cases as endpoint errors are very infrequent.

```elixir
defmodule MyApp.Endpoint do
use Phoenix.Endpoint
use ErrorTracker.Integrations.Plug

# Your code here
end
```

You can learn more about this in the `ErrorTracker.Integrations.Plug` module documentation.

## Error context

The default integrations include some additional context when tracking errors. You can take a look at the relevant integration modules to see what is being tracked out of the box.

In certain cases you may want to include some additional information when tracking errors. For example it may be useful to track the user ID that was using the application when an error happened. Fortunately, the ErrorTracker allows you to enrich the default context with custom information.

The `ErrorTracker.set_context/1` function stores the given context in the current process so any errors that happen in that process (for example a Phoenix request or an Oban job) will include this given context along with the default integration context.

```elixir
ErrorTracker.set_context(%{user_id: conn.assigns.current_user.id})
```

## Manual error tracking

If you want to report custom errors that fall outside the default integrations scope you may use `ErrorTracker.report/2`. This allows you to report an exception by yourself:

```elixir
try do
# your code
catch
e ->
ErrorTracker.report(e, __STACKTRACE__)
end
```

You can also use `ErrorTracker.report/3` and set some custom context that will be included along with the reported error.

## Web UI

The ErrorTracker also provides a dashboard built with Phoenix LiveView that can be used to see and manage the recorded errors.

This is completely optional and you can find more information about it in the `ErrorTracker.Web` module documentation.
116 changes: 116 additions & 0 deletions lib/error_tracker.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
defmodule ErrorTracker do
@moduledoc """
En Elixir based built-in error tracking solution.

The main objectives behind this project are:

* Provide a basic free error tracking solution: because tracking errors on
your application should be a requirement to almost any project, and helps to
provide quality and maintenance to your project.

* Be easy to use: by providing plug and play integrations, documentation and a
simple UI to manage your errors.

* Be as minimalistic as possible: you just need a database to store errors and
an Phoenix application if you want to inspect them via web. That's all.

## Requirements

ErrorTracker requires Elixir 1.15+, Ecto 3.11+, Phoenix LiveView 0.19+ and PostgreSQL

## Integrations

We currently include integrations for what we consider the basic stack of
an application: Phoenix, Plug and Oban.

However, we may continue working in adding support for more systems and
libraries in the future if there is enough interest by the community.

If you want to manually report an error you can use the `ErrorTracker.report/3` function.

## Context

Aside from the information abot each exception (kind, message, stacktrace...)
we also store contexts.

Contexts are arbitrary maps that allow you to store extra information of an
exception to be able to reproduce it later.

Each integration includes a default context with the useful information they
can gather, but aside from that you can also add your own information. You can
do this in a per-process way or in a per-call way (or both).

**Per process**

This allows you to set general context for the current process such as a Phoenix
request or an Oban job. For example you could include the following code in your
authentication Plug to automatically include the user ID on any error that is
tracked during the Phoenix request handling.

```elixir
ErrorTracker.set_context(%{user_id: conn.assigns.current_user.id})
```

**Per call**

As we had seen before you can use `ErrorTracker.report/3` to manually report an
error. The third parameter of this function is optional and allows you to include
extra context that will be tracked along with that error.
"""

@typedoc """
Expand All @@ -11,6 +66,40 @@ defmodule ErrorTracker do
alias ErrorTracker.Error
alias ErrorTracker.Repo

@doc """
Report a exception to be stored.

Aside from the exception, it is expected to receive the stacktrace and,
optionally, a context map which will be merged with the current process
context.

Keep in mind that errors that happen in Phoenix controllers, Phoenix live views
and Oban jobs are automatically reported. You will need this function only if you
want to report custom errors.

```elixir
try do
# your code
catch
e ->
ErrorTracker.report(e, __STACKTRACE__)
end
```

## Exceptions

Exceptions passed can be in three different forms:

* An exception struct: the module of the exception is stored alongside with
the exception message.

* A `{kind, exception}` tuple in which the `exception` is an struct: it
behaves the same as when passing just the exception struct.

* A `{kind, reason}` tuple: it stores the kind and the message itself casted
to strings, as it is useful for some errors like EXIT signals or custom error
messages.
"""
def report(exception, stacktrace, given_context \\ %{}) do
{kind, reason} =
case exception do
Expand Down Expand Up @@ -40,18 +129,42 @@ defmodule ErrorTracker do
|> Repo.insert!()
end

@doc """
Marks an error as resolved.

If an error is marked as resolved and it happens again, it will automatically
appear as unresolved again.
"""
def resolve(error = %Error{status: :unresolved}) do
changeset = Ecto.Changeset.change(error, status: :resolved)

Repo.update(changeset)
end

@doc """
Marks an error as unresolved.
"""
def unresolve(error = %Error{status: :resolved}) do
changeset = Ecto.Changeset.change(error, status: :unresolved)

Repo.update(changeset)
end

@doc """
Sets current process context.

By default it will merge the current context with the new one received, taking
preference the new context's contents over the existsing ones if any key
matches.

## Depth of the context

You can store context on more than one level of depth, but take into account
that the merge operation is performed on the first level.

That means that any existing data on deep levels fot he current context will
be replaced if the first level key is received on the new contents.
"""
@spec set_context(context()) :: context()
def set_context(params) when is_map(params) do
current_context = Process.get(:error_tracker_context, %{})
Expand All @@ -61,6 +174,9 @@ defmodule ErrorTracker do
params
end

@doc """
Obtain the context of the current process.
"""
@spec get_context() :: context()
def get_context do
Process.get(:error_tracker_context, %{})
Expand Down
Loading