Condux SDK for Ruby
Report errors from a Ruby app to a Condux relay. Emits the Sentry "store" wire shape, so the relay normalizes it exactly like an official Sentry SDK — point it at a project DSN and it works.
Delivery is resilient (429 / 5xx / network failures retry with backoff, honoring Retry-After) and
never raises — a failed send returns a SendResult, it does not crash the caller. That holds even
if init was never called: capture warns once and drops the event, because the Rack middleware below
reports from inside a rescue and an SDK that raised there would replace your application's exception
with its own. A malformed DSN is refused by init instead, where it is a developer-time mistake.
Usage
require "condux"
Condux.init(
dsn: "https://<key>@ingest.condux.ai/<projectId>",
environment: "production",
release: "1.4.2",
)
begin
do_work
rescue => e
Condux.capture_exception(e) # captures the exception's backtrace
raise
end
# or a bare message
Condux.("cache miss storm", level: Condux::Level::WARNING)
Verify your setup
Silence is what a broken error monitor and a healthy app look like from the outside, so prove the pipeline once:
CONDUX_DSN="https://<key>@ingest.condux.ai/<projectId>" bundle exec condux test-event
Exit code 0 means delivered (the message appears as an info-level issue), 1 means delivery failed and prints why, 2 means the DSN was missing or malformed.
Enrichment
Attach the ambient facts triage always needs. Every subsequent event carries them, so nothing has to be threaded through capture calls:
Condux.set_user({ "id" => "1042", "email" => "dev@example.com" }) # nil clears it (sign-out)
Condux.set_tag("plan", "team") # nil removes the tag
Condux.set_context("job", { "queue" => "billing", "attempt" => 3 })
Condux.("charge.started", category: "billing")
The breadcrumb trail keeps the most recent 30 entries. Condux.clear_scope resets everything.
In a server, scope one request at a time
Those calls are process wide by default, which is right for facts about the deployment and wrong for
facts about one request: Puma serves requests concurrently, so a bare set_user in a controller can
attach that user to a different request's error. That is worse than reporting no user, because it is
confidently wrong.
Condux.request_scope isolates it. Anything set inside belongs to that request alone, layered over the
process-wide values:
Condux.request_scope do
Condux.set_user({ "id" => current_user.id }) # this request only
process(job)
end
The Rack middleware below does this for you, so a set_user in a Rails controller is already
isolated. Call it directly around background jobs, which have the same problem. It uses
Thread.current[], which is fiber-local in Ruby, so it isolates Puma's threads and Falcon's fibers
alike.
Detail belonging to a single event can skip the scope entirely:
Condux.capture_exception(error, request: { "url" => "/api/sync" }, tags: { "job" => "nightly" })
Rack and Rails
# config.ru
require "condux/rack"
use Condux::Rack::CaptureExceptions
# Rails (config/application.rb)
config.middleware.use "Condux::Rack::CaptureExceptions"
Uncaught exceptions are reported as unhandled and re-raised, so the app's own error handling still runs.
On Rails use config.middleware.use, and nothing else. use appends, which puts the middleware at
the bottom of the stack, inside ActionDispatch::ShowExceptions. That position is why it works:
ShowExceptions catches a controller exception and turns it into a 500, so anything above it never sees
the exception and reports nothing, with no error to tell you. insert_before, insert_after and
unshift all move it above and break it silently.
Develop
for f in test/*.rb; do ruby -Ilib -Itest "$f"; done
Zero runtime dependencies (standard library only). The transport, sleep, and clock are injectable
(Condux.init(... transport:, sleep:, clock:)), so the tests exercise the retry/backoff with no real
network or timers.