# `Faulty`
[🔗](https://github.com/Hermanverschooten/faulty/blob/main/lib/faulty.ex#L1)

En Elixir-based  error tracking solution.

The main objectives behind this project are:

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

## Requirements

Faulty requires Elixir 1.17+. Errors are sent as JSON with the built-in `JSON`
module, which needs Elixir 1.18+. On Elixir 1.17, add `:jason` to your
dependencies and set `config :faulty, json_library: Jason`.

## Integrations

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

The Phoenix and Plug integrations are only available when `:plug` is one of
your dependencies, which it always is in a Phoenix application.

If you want to manually report an error, you can use the `Faulty.message/2` function.

## Context

Aside from the information about each exception (kind, message, stack trace...)
we also store contexts.

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

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

There are some requirements on the type of data that can be included in the
context, so we recommend taking a look at `set_context/1` documentation.

**Per process**

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

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

**Per call**

As we had seen before, you can use `Faulty.message/2` to manually report an
error. The second parameter of this function is optional and allows you to include
extra context that will be tracked along with the error.

# `context`

```elixir
@type context() :: %{required(String.t() | atom()) =&gt; any()}
```

A map containing the relevant context for a particular error.

# `exception`

```elixir
@type exception() ::
  Exception.t() | {:error, any()} | {Exception.non_error_kind(), any()}
```

An `Exception` or a `{kind, payload}` tuple compatible with `Exception.normalize/3`.

# `clear_reported`

```elixir
@spec clear_reported() :: :ok
```

Clears the duplicate-report guard of the current process.

A process reports only one error until this is called, so that the same crash
is not stored twice when it is seen by more than one integration (for example
a Phoenix telemetry event and the `:logger` handler).

The integrations call this for you at the start of every Phoenix request,
LiveView mount and `handle_params`, Oban job and Quantum job. Call it yourself
at the start of each unit of work in a long-lived process that rescues and
reports errors, such as a `GenServer`:

```elixir
def handle_info(:tick, state) do
  Faulty.clear_reported()
  do_work(state)
rescue
  e -> Faulty.report(e, __STACKTRACE__)
end
```

# `get_context`

```elixir
@spec get_context() :: context()
```

Obtain the context of the current process.

# `message`

```elixir
@spec message(binary() | exception(), context()) :: :ok | :noop
```

Reports a message to be stored.

Returns `:ok` stored or `:noop` if the Faulty is disabled by
configuration the exception has not been stored.

This allows you to store a message or exception manually,
the stacktrace will be added automatically.

```elixir
Faulty.message("Invalid user or password", %{login: login, password: password})

Faulty.message({ArgumentError, "Invalid user or password"}, %{login: login, password: password})
```

## Exceptions

Exceptions can be passed in three different forms:

* A binary: This will be stored as an `ErlangError`.

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

* A `{kind, exception}` tuple in which case the information is converted to
an Elixir exception (if possible) and stored.

This function can also be used to test your setup.

# `report`

```elixir
@spec report(exception(), Exception.stacktrace(), context()) :: :ok | :noop
```

Report an exception to be stored.

Returns `:ok` stored or `:noop` if the Faulty is disabled by
configuration the exception has not been stored.

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

Keep in mind that errors that occur in Phoenix controllers, Phoenix LiveViews
and Quantum 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 ->
    Faulty.report(e, __STACKTRACE__)
end
```

## Exceptions

Exceptions can be passed as:

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

* A `{kind, exception}` tuple in which case the information is converted to
an Elixir exception (if possible) and stored.

# `set_context`

```elixir
@spec set_context(context()) :: context()
```

Sets the current process context.

The given context will be merged into the current process context. The given context
may override existing keys from the current process context.

## Context depth

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 for he current context will
be replaced if the first level key is received on the new contents.

## Content serialization

The content stored on the context must be serializable by the JSON library that
Faulty is configured to use (`:json_library`, the built-in `JSON` module by
default), so it is rather recommended to use primitive types (strings, numbers,
booleans...).

If you still need to pass more complex data types to your context, please test
that they can be encoded to JSON or the error will be dropped, with a warning in
your logs. That may require defining an encoder for that data type if not
included by default: `@derive JSON.Encoder` with the built-in `JSON` module,
or `@derive Jason.Encoder` with `Jason`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
