Current section

Files

Jump to
Raw

README.md

# forge_ops_tracker (Elixir)
Elixir error reporting client for a private, self-hosted [ForgeOps](../../) tracker instance.
Requires Elixir 1.18+ and an OTP install with `:inets`/`:ssl` (bundled with every standard OTP
install). Reports exceptions and process crashes anywhere in the BEAM VM, using OTP's own
supervision and logging primitives rather than anything bolted on from outside.
## Installation
Not yet published to Hex -- point at this path directly, or a local checkout once split into its
own repo:
```elixir
def deps do
[
{:forge_ops_tracker, path: "/path/to/forge_ops/sdks/elixir"}
]
end
```
## Configuration
Set a DSN (from a project's settings page in ForgeOps), either via the `FORGE_OPS_DSN` environment
variable, `config.exs`, or an explicit `init/1` call:
```elixir
# config/config.exs
config :forge_ops_tracker,
dsn: "https://<api_key>@your-forgeops-host/api/v1/events",
environment: "production"
```
```elixir
# or, at application startup (application.ex's own start/2, before the rest of your
# supervision tree starts, is the natural place):
ForgeOpsTracker.init(dsn: "...", environment: "production")
ForgeOpsTracker.install_handlers()
```
`init/1` accepts the same keys as `ForgeOpsTracker.Configuration`'s struct fields and raises
`ArgumentError` for an unknown one, so a typo in a config key fails loudly at startup instead of
silently being ignored.
## What gets reported automatically, and what doesn't
`install_handlers/0` attaches a standard `:logger_handler` (see
`ForgeOpsTracker.LoggerHandler`'s own module doc) that reports **any process crash anywhere in
the whole BEAM VM, with zero further wiring needed** -- a `GenServer` callback that raises, an
unhandled message that trips `handle_info`'s catch-all, a linked process crashing another, any of
it. Erlang's own crash-reporting pipeline already forwards a structured `{reason, stacktrace}` for
every abnormal process exit into `:logger`, because that's simply how OTP's "let it crash"
supervision model already works. Attaching a handler observes that existing stream rather than
building a new one. An ordinary `Logger.error("something went wrong")` call from application code
(no crash attached) is deliberately left alone -- reporting every error-level log line, not just
real crashes, would be far noisier than this client should be by default.
For an exception you've already rescued yourself and want to report explicitly (and typically
re-raise):
```elixir
try do
charge_card(order)
rescue
e ->
ForgeOpsTracker.capture_exception(e, __STACKTRACE__, %{order_id: order.id})
reraise e, __STACKTRACE__
end
```
`stacktrace` has to come from the actual rescue/catch site (`__STACKTRACE__`) -- an Elixir
exception value carries no trace of its own, so the special form is the only place a stacktrace is
available, and only for as long as nothing else has run since the rescue/catch.
Delivery runs through a supervised `ForgeOpsTracker.DeliveryQueue` `GenServer`: each push is
bounded (`Configuration.queue_size`, drops and logs rather than blocking the caller once full),
and the actual HTTP call for each delivery runs in its own short-lived `Task` rather than inline
in the `GenServer`'s own callback, so a slow or unreachable tracker never makes the queue itself
unresponsive to new pushes. Every failure mode -- network errors, timeouts, a malformed DSN -- is
caught and logged rather than propagated, so a broken tracker can never take down the host app.
## `in_app` backtrace frames
A frame is marked `in_app` when its module name (via `inspect/1`, which strips the `Elixir.`
prefix every Erlang-level module atom carries) starts with `Configuration.app_module_prefix` --
e.g. `"MyApp"` for an app whose modules are all namespaced under `MyApp.*`. Unset by default (no
frame is marked `in_app`), since a compiled BEAM release carries no reliable filesystem convention
to infer an application's own module prefix automatically.
## PII scrubbing
The message, backtrace, and any context you attach are scanned for likely personal data -- email
addresses, formatted SSNs/credit cards, known API key/token formats, and anything under a
suspiciously-named key (`password`, `api_key`, `ssn`, and similar) -- and redacted before the
payload ever leaves this process. ForgeOps itself scrubs again on arrival regardless, so this is a
second, earlier layer, not the only one. Elixir's `Regex` is full PCRE (via Erlang's `:re`), so
every pattern (including `\b` word boundaries) is supported as written.
To disable it:
```elixir
ForgeOpsTracker.init(scrub_pii: false)
```
## Dependencies
Zero Hex dependencies. HTTP delivery uses `:httpc`/`:ssl` (bundled with every OTP install, not a
Hex package, with `ssl: [verify: :verify_peer, cacerts: :public_key.cacerts_get()]` for proper TLS
certificate verification); JSON encoding uses Elixir's own `JSON` module, part of the standard
library since 1.18 (this SDK's real version floor, not an arbitrary pin); PII scrubbing uses
Elixir's built-in `Regex`. Nothing here needs a dependency the standard library doesn't already
cover.
## Running the tests
```bash
cd sdks/elixir
mix deps.get # no-op today; there are no deps, but keeps the usual workflow intact
mix test
mix format --check-formatted
mix compile --warnings-as-errors
```
Client/DeliveryQueue/Reporter/LoggerHandler tests run against a real local HTTP server
(`test/support/test_http_server.ex`, a small hand-rolled `:gen_tcp`-based server) rather than a
mock, so delivery is verified against something real rather than an assumption about how the HTTP
client behaves.