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). A from-scratch port of gems/forge_ops_tracker (the
Rails client) -- see that gem's README for the shared design rationale; this document only covers
what's Elixir/OTP-specific.
Installation
Not yet published to Hex -- point at this path directly, or a local checkout once split into its own repo:
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:
# config/config.exs
config :forge_ops_tracker,
dsn: "https://<api_key>@your-forgeops-host/api/v1/events",
environment: "production"
# 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, the same "fail loudly on a typo" behavior the Go/Rust clients'
own builder closures give.
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. This is a meaningfully stronger automatic-capture story than the single-thread/goroutine
uncaught-exception hooks every other client in this repo installs for its own language: 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):
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__) -- unlike languages
where an exception object carries its own trace, Elixir's stacktrace is only available via that
special form, and only for as long as nothing else has run since the rescue/catch.
Delivery runs through a supervised ForgeOpsTracker.DeliveryQueueGenServer: 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 Elixir has no filesystem convention as reliable as Rails'
Rails.root or a Go binary's build path to infer this automatically.
PII scrubbing
Same behavior as every other client in this repo: 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. The patterns
are unmodified from the Ruby original: Elixir's Regex is full PCRE (via Erlang's :re), unlike
this repo's C client, which had to adapt its own patterns to POSIX ERE.
To disable it:
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. The same "don't add a dependency the standard library already covers"
philosophy every client in this repo follows.
Running the tests
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, the same "test against something real" approach this repo's Go/Rust/Swift/Kotlin/C clients'
own hand-rolled test servers take.