API Reference forge_ops_tracker v#0.10.0

Copy Markdown View Source

Modules

Elixir error reporting client for ForgeOps

A bounded, in-order trail of what happened right before an exception: the automatic "controller"/"query"/"job" entries ForgeOpsTracker.Integrations.Phoenix/Ecto/Oban record (see each one's own module doc), plus anything added by hand via ForgeOpsTracker.add_breadcrumb/4. Ported from gems/forge_ops_tracker/lib/forge_ops_tracker/breadcrumb_buffer.rb: a ring buffer capped at Configuration.t/0's own max_breadcrumbs, oldest entry dropped once full.

Delivers one payload over HTTP via :httpc (Erlang's own HTTP client, part of the :inets application bundled with every OTP install: no Hex dependency needed, same reasoning as net/http in the Ruby gem and java.net.http.HttpClient in the Java/Kotlin clients). Every failure mode (DNS, connection, timeout, TLS, a non-2xx response) is caught here and turned into false rather than a raised exception, since a broken or unreachable tracker must never be able to break the host app.

Holds a single ForgeOps DSN plus everything else the client needs to build and deliver events. Mirrors gems/forge_ops_tracker's Configuration: a single DSN string carries both the ingestion URL and the project's api_key: https://<api_key>@host/api/v1/events.

A small bounded queue, processed one event at a time, so delivery never blocks the caller that raised the error. The BEAM equivalent of the Ruby/Python/Java clients' own background-thread- backed queue: except here the "queue manager" is an ordinary supervised GenServer (started once, see ForgeOpsTracker.Application, not spun up lazily on first push, since BEAM processes are cheap enough that there's no meaningful cost to keeping one alive idle) and each actual delivery runs in its own short-lived Task, not inline in the GenServer's own callback.

Turns an exception/reason plus an Elixir stacktrace into the payload shape the ingestion API expects. Unlike the Ruby/PHP/JS clients in this repo, backtrace parsing here is not a regex over a string: __STACKTRACE__ is already a structured list of {module, function, arity, location} tuples (location a keyword list with :file/:line), the same situation Python's traceback module and Java's Throwable.getStackTrace() put those clients in. Verified directly against a real raised-and-rescued exception's __STACKTRACE__ before relying on this shape, not assumed from documentation alone.

Buckets a single duration into one of a fixed set of latency-range labels, the building block ForgeOpsTracker.PerformanceFlusher uses to accumulate an approximate distribution (not just count/sum/max) alongside every {transaction_name, kind} bucket it already tallies. The server merges these counts across matching samples at read time and walks cumulative counts to approximate a percentile, accurate to the bucket width: this SDK never stores the raw duration list a true percentile would need. Ported from gems/forge_ops_tracker/lib/forge_ops_tracker/histogram_bucketer.rb.

Times every query a given Ecto.Repo runs, kind: "query". Call once at startup, passing the repo module itself

Times every Oban job, kind: "job", and reports a genuinely failed job (retries exhausted, or the worker explicitly gave up) the same way an unhandled exception anywhere else already is. Call once at startup

Times every matched Phoenix route, kind: "controller". Call once at startup, e.g. application.ex's own start/2, before the rest of the supervision tree starts

Automatically identifies the affected user for every error reported during this request, when conn.assigns[:current_user] is present: the de facto convention across Phoenix's own generated auth (mix phx.gen.auth), Pow, and most hand-rolled Guardian setups alike, since Phoenix itself (unlike Rails/Devise) has no single dominant auth library this could depend on directly.

Reports any process crash anywhere in the whole BEAM VM, with zero further wiring needed: attached via :logger.add_handler/3 (see ForgeOpsTracker.install_handlers/0), a standard :logger_handler behaviour implementation, the same technique other logger-integrated error trackers in the Elixir ecosystem use for the same purpose.

Collects individual ForgeOpsTracker.capture_metric/2 / capture_infrastructure_metric/3 calls and periodically flushes them as one batch, rather than one network call per capture. Unlike ForgeOpsTracker.PerformanceFlusher this keeps a list of individually meaningful entries instead of summing them into buckets: a customer's own signup or payment is exactly the kind of thing they will want a genuinely accurate count/sum of later, so the server stores one row per entry as-is. Ported from gems/forge_ops_tracker's metric_buffer.rb and infrastructure_metric_buffer.rb, which are the same class twice; here it is one GenServer started twice under ForgeOpsTracker.Application (:custom and :infrastructure), each registered under its own name.

Times every request/job/query in-process, bucketed by {transaction_name, kind} (kind is "controller"/"job"/"query", see the ForgeOpsTracker.Integrations.* modules, each a different telemetry source), and periodically flushes each distinct bucket as one small aggregate report, rather than one network call per event. Mirrors gems/forge_ops_tracker's own PerformanceFlusher exactly in shape (the aggregation itself: count + duration sum + max over a period), but started once as a supervised GenServer (a fourth child of ForgeOpsTracker.Application, alongside Configuration and DeliveryQueue) rather than lazily spawning a background thread on first use: Elixir has no per-request-process fork boundary to dodge the way Ruby's Puma/Passenger workers do (the actual reason that gem starts lazily), and BEAM processes are cheap enough that there's no cost to just always running one.

Redacts likely-sensitive content out of a payload before it ever leaves this process: the same patterns ForgeOps itself applies again on arrival (defense in depth: this layer keeps the data off the wire and out of any request logging in between; the server-side layer is what actually protects the database, and doesn't depend on every reporting app running an up-to-date version of this client). Ported from gems/forge_ops_tracker/lib/forge_ops_tracker/pii_scrubber.rb: same key list, same 8 regex patterns, unmodified from the Ruby original: Elixir's Regex is Erlang's :re module, itself a PCRE implementation, so unlike this repo's own C client (which had to adapt every pattern to POSIX Extended Regular Expressions: no \d/\s/\b), nothing here needed translating.

A low-cardinality bucket name for a database query, for the same reason every other SDK's own transaction_name never uses the raw SQL text directly: every slightly different query shape would become its own bucket, and raw SQL is a real, if usually parameterized, risk of leaking a literal value.

Ties Configuration, EventBuilder, and DeliveryQueue together into the one thing callers actually need: report an exception. Mirrors every other SDK's own Reporter/ErrorSubscriber: never raises. An error reporter that itself raises while reporting an error is the worst possible failure mode, so every path here is wrapped to guarantee this never propagates back into the caller (or, when called from ForgeOpsTracker.LoggerHandler, back into the :logger pipeline itself, which would be just as bad).

Finds the SQL behind a database error and reduces it to something safe to send: the names of the stored procedures, tables and views it touched, and (only if capture_sql_statement is on) the statement itself with every string and number replaced by ?. Ported from gems/forge_ops_tracker's SqlStatement, which is itself ported from the server's own SqlStatementMasker/SqlObjectExtractor: same rules everywhere, and the server applies them again on arrival, so a difference here can only ever mean less is masked client-side, never that something unmasked gets stored.

One trace's worth of spans (a request's own call tree), kept in the calling process's own process dictionary: the same isolation boundary ForgeOpsTracker.set_user/1 and ForgeOpsTracker.add_breadcrumb/4 already use, and for the same reason (Phoenix and Oban both run one unit of work in one process). Not something app code normally calls directly; see ForgeOpsTracker.span/4 and ForgeOpsTracker.record_span/5.