better_auth-telemetry
Opt-in telemetry package for Better Auth Ruby. Ports the upstream
@better-auth/telemetry package (vendored at
upstream/better-auth/1.6.9/packages/telemetry/) using only Ruby's standard
library.
Telemetry is disabled by default. The package never sends data unless an
operator explicitly opts in, and it is automatically skipped when the host
process is running under RACK_ENV=test, RAILS_ENV=test, or APP_ENV=test.
It is not configured through plugins: [...]; it is an optional gem that core
soft-loads when available.
Installation
Add the gem:
gem "better_auth-telemetry"
When the gem is bundled, BetterAuth::Auth#initialize automatically wires
auth.telemetry to a publisher. When the gem is not bundled, auth.telemetry
is still safe to call: it returns a noop publisher whose #publish is a no-op.
Core's behavior is unchanged either way.
Require better_auth/telemetry only when using the telemetry API directly:
require "better_auth/telemetry"
Opting in
Two equivalent ways to opt in. Either is sufficient on its own.
Via options
auth = BetterAuth.auth(
secret: ENV.fetch("BETTER_AUTH_SECRET"),
database: :postgres,
telemetry: { enabled: true }
)
An explicit telemetry: { enabled: false } always wins over the env var:
setting options[:telemetry][:enabled] = false disables telemetry even when
BETTER_AUTH_TELEMETRY=1 is set.
Via environment variables
The package reads every variable through BetterAuth::Env.get, which honors
both the BETTER_AUTH_* and OPEN_AUTH_* prefixes. The OPEN_AUTH_* form
takes precedence over the BETTER_AUTH_* form when both are set.
| Purpose | BETTER_AUTH_* form |
OPEN_AUTH_* form |
|---|---|---|
| Opt in | BETTER_AUTH_TELEMETRY |
OPEN_AUTH_TELEMETRY |
| Debug mode | BETTER_AUTH_TELEMETRY_DEBUG |
OPEN_AUTH_TELEMETRY_DEBUG |
| Endpoint URL | BETTER_AUTH_TELEMETRY_ENDPOINT |
OPEN_AUTH_TELEMETRY_ENDPOINT |
A value is treated as truthy when it is non-empty, not equal to "0", and not
equal to (case-insensitive) "false". Unset and empty are both treated as
absent. No other telemetry environment variables are recognized.
export BETTER_AUTH_TELEMETRY=1
export BETTER_AUTH_TELEMETRY_ENDPOINT=https://telemetry.example.com/ingest
Test environment skip
When RACK_ENV, RAILS_ENV, or APP_ENV equals "test", telemetry is skipped
even if it is otherwise opted in. Bypass this skip by setting
context[:skip_test_check] = true. skip_test_check only bypasses the test
gate; it does not opt telemetry in on its own.
BetterAuth::Telemetry.create(
,
{ skip_test_check: true } # opt-in still required via options or env
)
Debug mode
When debug mode is on (options[:telemetry][:debug] = true or
BETTER_AUTH_TELEMETRY_DEBUG set to a truthy value), every event is logged via
the configured logger using logger.info(JSON.pretty_generate(event)) and
no HTTP request is made. This is the recommended mode for inspecting what
the package would send before pointing it at a real endpoint.
auth = BetterAuth.auth(
secret: ENV.fetch("BETTER_AUTH_SECRET"),
database: :postgres,
telemetry: { enabled: true, debug: true }
)
When neither debug mode nor custom_track is configured and an endpoint is
set, the publisher starts a short-lived background thread that POSTs JSON
events to the endpoint via Net::HTTP with a 5-second open + read timeout.
HTTP telemetry is fire-and-forget, so constructing BetterAuth.auth is not
blocked by a slow or unavailable endpoint. Any StandardError raised during
HTTP delivery is rescued and logged at error level rather than propagated.
The custom_track injection seam
context[:custom_track] is a callable (typically a Proc or lambda) that
receives every event in lieu of HTTP delivery. It is the testing seam used by
the gem's own test suite, and it is also useful in production to forward
events to an in-process queue, an audit log, or a custom collector.
recorder = []
custom_track = ->(event) { recorder << event }
publisher = BetterAuth::Telemetry.create(
{ secret: "x", database: :memory, telemetry: { enabled: true } },
{ custom_track: custom_track, skip_test_check: true }
)
publisher.publish(type: "ping", payload: { hello: "world" })
# recorder now contains the init event plus { type: "ping", payload: { hello: "world" }, anonymousId: "..." }
If custom_track raises, the exception is rescued, logged at error level, and
swallowed; #publish always returns nil. The anonymousId on every event
emitted by a single publisher is the same string, derived from
BetterAuth::Telemetry.project_id(base_url).
The package accepts both snake_case and camelCase keys on the context for
parity with callers mirroring upstream type definitions: :custom_track and
:customTrack are equivalent, as are :skip_test_check and :skipTestCheck.
Differences from upstream
The upstream @better-auth/telemetry package targets multiple JavaScript
runtimes (Node, Bun, Deno, edge) and ships two build entrypoints. This Ruby
port collapses both upstream variants into a single server-side Ruby
implementation and adapts every detector to idiomatic Ruby. The wire format
preserves upstream camelCase keys and redaction rules so existing telemetry
consumers can ingest events from Ruby projects without schema branching.
The intentional Ruby-specific deviations are:
- Single Ruby implementation. No Node, Bun, Deno, or edge runtime
branches. Detectors do not probe for
npm_config_user_agent, do not walknode_modules, and do not classify against JavaScript-only runtimes. runtime.engineextra key. The runtime payload includes an:enginekey ("ruby","jruby","truffleruby") sourced fromRUBY_ENGINEso consumers can distinguish Ruby implementations. Upstream emits onlynameandversion.cpuSpeedomitted. Upstream'ssystemInfo.cpuSpeedfield is not emitted at all on the Ruby side. There is no portable Ruby standard-library API for CPU speed, and emittingnilwould invite consumers to assume the field can ever be populated.cpuModelalwaysnil. ThesystemInfo.cpuModelkey is present in the payload (so the schema matches upstream) but is alwaysnil. Ruby has no portable standard-library API for the CPU model string.packageManagerreflects Bundler, not npm. When Bundler is loadable and a Gemfile is locatable,payload.packageManageris{ name: "bundler", version: Bundler::VERSION }. Otherwise the field isnil. Upstream'snpm_config_user_agentparsing has no Ruby analogue.- Framework probe list is Ruby-specific. The framework detector inspects
Gem.loaded_specsforrails,sinatra,hanami,hanami-router,roda,grape,rack(in that order). Node-only frameworks (next,nuxt,astro,sveltekit,solid-start,tanstack-start,hono,express,elysia,expo) are intentionally not probed. - Database probe list is Ruby-specific. The database detector falls back
to
Gem.loaded_specsforsequel,pg,mysql2,sqlite3,activerecord,mongoid,mongo,rom-sql(in that order) when no context override orBetterAuth::Adapters::*adapter class match is found. - Standard library only HTTP. HTTP delivery uses
Net::HTTPwith a 5-second open + read timeout inside a short-lived background thread. No external HTTP-client gem is required at runtime, and HTTP delivery does not blockBetterAuth.authconstruction. - Explicit false is a strong opt-out.
telemetry: { enabled: false }disables telemetry even whenBETTER_AUTH_TELEMETRYorOPEN_AUTH_TELEMETRYis truthy. This is intentionally stricter than upstream so application configuration can override process-wide env vars. - snake_case canonical context keys, with camelCase synonyms accepted.
The Ruby-canonical context keys are
:custom_track,:database,:adapter,:skip_test_check. The package also accepts the camelCase variants (:customTrack,:skipTestCheck) for parity with callers mirroring upstream type definitions. appNameis not emitted. Theapp_namevalue is used internally byBetterAuth::Telemetry.project_idto derive theanonymousIdbut is intentionally not emitted as a payload field, since it can be user-identifying.- Public
BetterAuth::Telemetry.reset_project_id!testing helper. A module-level helper is exposed for resetting the memoizedanonymous_idbetween tests. It has no effect on production behavior and exists solely so test suites can assert deterministic project_id derivation across opt-in / opt-out cycles.
License
MIT