Module: BetterAuth::Telemetry
- Defined in:
- lib/better_auth/telemetry.rb,
lib/better_auth/telemetry/env.rb,
lib/better_auth/telemetry/create.rb,
lib/better_auth/telemetry/options.rb,
lib/better_auth/telemetry/version.rb,
lib/better_auth/telemetry/publisher.rb,
lib/better_auth/telemetry/project_id.rb,
lib/better_auth/telemetry/http_client.rb,
lib/better_auth/telemetry/logger_adapter.rb,
lib/better_auth/telemetry/noop_publisher.rb,
lib/better_auth/telemetry/detectors/runtime.rb,
lib/better_auth/telemetry/detectors/database.rb,
lib/better_auth/telemetry/detectors/framework.rb,
lib/better_auth/telemetry/detectors/auth_config.rb,
lib/better_auth/telemetry/detectors/environment.rb,
lib/better_auth/telemetry/detectors/system_info.rb,
lib/better_auth/telemetry/detectors/project_info.rb
Overview
Top-level namespace for the ‘better_auth-telemetry` gem.
See ‘BetterAuth::Telemetry.create` for the entry point used by `BetterAuth::Auth#initialize` and by tests that exercise the publisher in isolation.
Defined Under Namespace
Modules: CurrentOptions, Detectors, Env, HttpClient, Options, ProjectId Classes: LoggerAdapter, NoopPublisher, NormalizedContext, NormalizedOptions, Publisher
Constant Summary collapse
- TEST_ENV_VARS =
Process-environment variables that mark the host as running inside a test suite. Mirrors Configuration#test_environment? without taking a hard dependency on a ‘Configuration` instance — the `create` entry point also accepts raw hashes and `nil`.
%w[RACK_ENV RAILS_ENV APP_ENV].freeze
- VERSION =
"0.8.0"- PROJECT_ID_ALPHABET =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
( ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a ).freeze
Class Method Summary collapse
-
.build_track(custom_track:, debug:, endpoint:, logger:) ⇒ Proc
private
Build the delivery ‘track` lambda.
-
.compose_init_event(options:, norm_ctx:, anonymous_id:) ⇒ Hash{Symbol => Object}
private
Compose the init event hash emitted at create time.
-
.compute_enabled(options_enabled:, env_truthy:, in_test_env:, skip_test_check:) ⇒ Boolean
private
Apply the Property 3 decision table.
-
.create(options, context = nil) ⇒ NoopPublisher, Publisher
Public entry point used by ‘BetterAuth::Auth#initialize` (and by tests that exercise the publisher in isolation) to build a publisher tailored to the host’s opt-in state.
-
.debug_mode?(norm_opts) ⇒ Boolean
private
Decide whether debug mode is active.
- .derive_project_id(base_url) ⇒ Object private
- .endpoint_absent?(endpoint) ⇒ Boolean private
- .hash_to_base64(input) ⇒ Object private
- .in_test_env? ⇒ Boolean private
-
.project_id(base_url) ⇒ String
Resolve a stable, anonymous project id for telemetry.
- .random_id_32 ⇒ Object private
-
.reset_project_id! ⇒ nil
Test-only hook that clears the memoized project id cache.
-
.safely { ... } ⇒ Object?
private
Run ‘block` and rescue any `StandardError` to `nil`.
Class Method Details
.build_track(custom_track:, debug:, endpoint:, logger:) ⇒ Proc
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build the delivery ‘track` lambda. Three branches, in priority order (Requirements 5.2 → 5.4):
-
‘custom_track` present — invoke `custom_track.call(event)`. Primary testing seam and the only branch that runs without requiring `BETTER_AUTH_TELEMETRY_ENDPOINT` to be set.
-
Debug mode active — log the JSON-pretty event via ‘logger.info(…)` and skip HTTP entirely (Requirement 5.9).
-
Default — fire-and-forget JSON ‘POST` through a short-lived background thread calling BetterAuth::Telemetry::HttpClient.post_json, which already swallows transport errors.
Every branch wraps its dispatch in a ‘rescue StandardError` that routes the failure through `logger.error(…)`, so callable / logger-encoding / HTTP failures never propagate out of the track lambda. The lambda always returns `nil`.
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/better_auth/telemetry/create.rb', line 202 def self.build_track(custom_track:, debug:, endpoint:, logger:) if custom_track lambda do |event| custom_track.call(event) nil rescue => e logger.error("[better-auth.telemetry] custom_track failed: #{e.class}: #{e.}") nil end elsif debug lambda do |event| logger.info(JSON.pretty_generate(event)) nil rescue => e logger.error("[better-auth.telemetry] debug log failed: #{e.class}: #{e.}") nil end else lambda do |event| Thread.new do HttpClient.post_json(endpoint, event, logger: logger) rescue => e logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.}") end nil rescue => e logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.}") nil end end end |
.compose_init_event(options:, norm_ctx:, anonymous_id:) ⇒ Hash{Symbol => Object}
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compose the init event hash emitted at create time.
Each detector is invoked through safely so a single failing probe degrades that field to ‘nil` rather than aborting the whole event composition (Requirement 6.4 / 9.11). The output matches the upstream wire shape: top-level `type`, `anonymousId`, and a `payload` hash with the seven camelCase keys `config`, `runtime`, `database`, `framework`, `environment`, `systemInfo`, `packageManager` (Requirements 6.1, 6.3).
‘AuthConfig.call` and `Database.call` are passed the original `options` argument (not the NormalizedOptions wrapper) because both detectors transparently accept either a Configuration or a raw hash; the normalized view is only consumed by the decision/track-building layer.
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/better_auth/telemetry/create.rb', line 256 def self.compose_init_event(options:, norm_ctx:, anonymous_id:) payload = { config: safely { Detectors::AuthConfig.call(, norm_ctx) }, runtime: safely { Detectors::Runtime.call }, database: safely { Detectors::Database.call(, norm_ctx) }, framework: safely { Detectors::Framework.call }, environment: safely { Detectors::Environment.call }, systemInfo: safely { Detectors::SystemInfo.call }, packageManager: safely { Detectors::ProjectInfo.call } } { type: "init", anonymousId: anonymous_id, payload: payload } end |
.compute_enabled(options_enabled:, env_truthy:, in_test_env:, skip_test_check:) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Apply the Property 3 decision table.
149 150 151 152 153 154 155 |
# File 'lib/better_auth/telemetry/create.rb', line 149 def self.compute_enabled(options_enabled:, env_truthy:, in_test_env:, skip_test_check:) opt_in = == true || (.nil? && env_truthy) overridden = == false in_test_gate = in_test_env && !skip_test_check opt_in && !overridden && !in_test_gate end |
.create(options, context = nil) ⇒ NoopPublisher, Publisher
Public entry point used by ‘BetterAuth::Auth#initialize` (and by tests that exercise the publisher in isolation) to build a publisher tailored to the host’s opt-in state.
## Pipeline
-
Normalize the heterogeneous ‘options` and `context` arguments into NormalizedOptions / NormalizedContext value objects so the rest of the pipeline does not have to repeatedly do snake/camelCase key lookups.
-
Resolve ‘endpoint = Env.get(“BETTER_AUTH_TELEMETRY_ENDPOINT”)`, honoring the `OPEN_AUTH_*` alias prefix.
-
Short-circuit: when both the endpoint and ‘custom_track` are absent there is no delivery channel and the publisher cannot do useful work, so we hand back a NoopPublisher and bypass the rest of the pipeline (Requirement 5.1).
-
**Decision table** (Property 3 / Requirements 4.1–4.7): compute ‘enabled` from `(options_enabled, env_truthy, in_test_env, skip_test_check)` using
opt_in = == true || (.nil? && env_truthy) overridden = == false # explicit false beats env truthy in_test_gate = in_test_env && !skip_test_check enabled = opt_in && !overridden && !in_test_gate -
When enabled, build the delivery ‘track` lambda via build_track: `custom_track` wins, then debug-mode logging, then HTTP delivery (Requirements 5.2–5.4, 5.7, 5.9). Each branch is wrapped in a `rescue StandardError` that routes the failure through the configured logger (Requirements 21.1, 21.2) so a misbehaving sink never propagates out of the track callable.
-
**Compose and emit the init event** (Requirement 6): resolve a stable project_id for the host (scoped to the BetterAuth::Telemetry::CurrentOptions.with_app_name block so the ‘from_app_name` rule sees the configured `app_name`), invoke each detector inside safely so a single misbehaving probe degrades to `nil` instead of aborting the init event, build the upstream-shaped `“init”, anonymousId:, payload: {…}` event with camelCase keys, and fire it through the track lambda exactly once. Errors raised by the dispatch itself surface through the rescue inside the track lambda.
-
Return a fully-initialized Publisher that closes over the same ‘track` / `anonymous_id` / `enabled` state so subsequent `#publish` calls reuse the already-resolved id (Requirement 6.10).
The method itself never raises: detectors are wrapped in safely, the track lambda swallows transport failures, and the decision-layer logic is plain hash lookups and env reads.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/better_auth/telemetry/create.rb', line 88 def self.create(, context = nil) norm_opts = NormalizedOptions.from() norm_ctx = NormalizedContext.from(context) logger = norm_opts.logger endpoint = Env.get("BETTER_AUTH_TELEMETRY_ENDPOINT") # No delivery channel -> short-circuit to noop, regardless of opt-in. return NoopPublisher.new if endpoint_absent?(endpoint) && norm_ctx.custom_track.nil? enabled = compute_enabled( options_enabled: norm_opts.telemetry_enabled, env_truthy: Env.truthy?(Env.get("BETTER_AUTH_TELEMETRY")), in_test_env: in_test_env?, skip_test_check: norm_ctx.skip_test_check ? true : false ) return NoopPublisher.new unless enabled track = build_track( custom_track: norm_ctx.custom_track, debug: debug_mode?(norm_opts), endpoint: endpoint, logger: logger ) # Resolve the anonymous id under a `with_app_name` scope so the # `from_app_name` rule in `ProjectId.resolve_project_name` reads # the configured `app_name` even when the underlying # `BetterAuth::Telemetry.project_id` cache is cold. Once cached # the value is reused for the lifetime of the process; the scope # only matters on the very first call. anonymous_id = CurrentOptions.with_app_name(norm_opts.app_name) do BetterAuth::Telemetry.project_id(norm_opts.base_url) end init_event = compose_init_event( options: , norm_ctx: norm_ctx, anonymous_id: anonymous_id ) track.call(init_event) Publisher.new( enabled: true, anonymous_id: anonymous_id, track: track, base_url: norm_opts.base_url, logger: logger ) end |
.debug_mode?(norm_opts) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Decide whether debug mode is active. The option-layer flag wins when explicitly ‘true`; otherwise we defer to the env classifier via BetterAuth::Telemetry::Env.truthy? on `BETTER_AUTH_TELEMETRY_DEBUG` (which honors the `OPEN_AUTH_*` alias prefix as well). Mirrors Requirement 5.4.
175 176 177 |
# File 'lib/better_auth/telemetry/create.rb', line 175 def self.debug_mode?(norm_opts) norm_opts.telemetry_debug == true || Env.truthy?(Env.get("BETTER_AUTH_TELEMETRY_DEBUG")) end |
.derive_project_id(base_url) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/better_auth/telemetry/project_id.rb', line 201 def self.derive_project_id(base_url) url = base_url.is_a?(String) ? base_url : nil url = nil if url && url.empty? name = ProjectId.resolve_project_name name = nil if name.is_a?(String) && name.empty? if name && url hash_to_base64(url + name) elsif name hash_to_base64(name) elsif url hash_to_base64(url) else random_id_32 end end |
.endpoint_absent?(endpoint) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
158 159 160 |
# File 'lib/better_auth/telemetry/create.rb', line 158 def self.endpoint_absent?(endpoint) endpoint.nil? || (endpoint.respond_to?(:empty?) && endpoint.empty?) end |
.hash_to_base64(input) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
220 221 222 |
# File 'lib/better_auth/telemetry/project_id.rb', line 220 def self.hash_to_base64(input) Base64.strict_encode64(Digest::SHA256.digest(input.to_s)) end |
.in_test_env? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
163 164 165 |
# File 'lib/better_auth/telemetry/create.rb', line 163 def self.in_test_env? TEST_ENV_VARS.any? { |k| ENV[k] == "test" } end |
.project_id(base_url) ⇒ String
Resolve a stable, anonymous project id for telemetry.
The id is derived once per process and memoized; subsequent calls — regardless of the ‘base_url` they pass — return the cached value (Requirement 14.6). This mirrors the upstream `projectIdCached` module-scope variable.
## Derivation chain (Requirements 14.2 – 14.5)
-
Project name resolvable AND ‘base_url` non-empty: `Base64(SHA-256(base_url + name))`.
-
Project name resolvable AND ‘base_url` nil/empty: `Base64(SHA-256(name))`.
-
No project name AND ‘base_url` non-empty: `Base64(SHA-256(base_url))`.
-
Otherwise: a random 32-character ‘[a-zA-Z0-9]` id from `SecureRandom`, matching upstream `generateId(32)`.
The Bundler/lockfile probes inside BetterAuth::Telemetry::ProjectId.resolve_project_name never raise out of this method (Requirement 14.8); a failed probe collapses to “no project name” and the chain continues at rule 3 or rule 4.
173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/better_auth/telemetry/project_id.rb', line 173 def self.project_id(base_url) cached = @project_id_cache return cached if cached @project_id_mutex.synchronize do cached = @project_id_cache return cached if cached @project_id_cache = derive_project_id(base_url) end end |
.random_id_32 ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
230 231 232 |
# File 'lib/better_auth/telemetry/project_id.rb', line 230 def self.random_id_32 Array.new(32) { PROJECT_ID_ALPHABET[SecureRandom.random_number(PROJECT_ID_ALPHABET.length)] }.join end |
.reset_project_id! ⇒ nil
Test-only hook that clears the memoized project id cache.
Wired here in task 3.6 to clear the ‘@project_id_cache` ivar that backs project_id. Tests use this between cases that exercise different derivation rules (e.g. with vs. without a project name) so each call goes through the full chain again.
193 194 195 196 197 198 |
# File 'lib/better_auth/telemetry/project_id.rb', line 193 def self.reset_project_id! @project_id_mutex.synchronize do @project_id_cache = nil end nil end |
.safely { ... } ⇒ Object?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Run ‘block` and rescue any `StandardError` to `nil`. Used to bound each detector invocation in compose_init_event so a raising probe degrades only that field rather than aborting the whole init event.
Non-‘StandardError` exceptions (`Interrupt`, `SystemExit`, `SignalException`, `NoMemoryError`) are intentionally allowed to propagate.
287 288 289 290 291 |
# File 'lib/better_auth/telemetry/create.rb', line 287 def self.safely yield rescue nil end |