better_auth-telemetry 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "logger_adapter"
4
+
5
+ module BetterAuth
6
+ module Telemetry
7
+ # Value objects that normalize the heterogeneous shapes the
8
+ # `BetterAuth::Telemetry.create(options, context)` entry point accepts
9
+ # into the small, well-typed surface the rest of the pipeline depends
10
+ # on.
11
+ #
12
+ # Two normalizers ship together:
13
+ #
14
+ # - {NormalizedOptions} wraps the host-supplied `options`. The argument
15
+ # may be a {BetterAuth::Configuration}, a raw `Hash`, or `nil`.
16
+ # - {NormalizedContext} wraps the optional `context` hash that callers
17
+ # use to override telemetry-side detection (`custom_track`,
18
+ # `database`, `adapter`, `skip_test_check`).
19
+ #
20
+ # Both accept either snake_case (`:custom_track`, `:skip_test_check`,
21
+ # `:database`, `:adapter`) or camelCase (`:customTrack`,
22
+ # `:skipTestCheck`) keys, in either symbol or string form, so callers
23
+ # mirroring the upstream TypeScript API do not have to translate keys
24
+ # by hand.
25
+ #
26
+ # Neither value object raises on missing or `nil` input. Missing keys
27
+ # surface as `nil` readers (or `false` for the boolean-defaulting
28
+ # `skip_test_check`).
29
+ module Options
30
+ end
31
+
32
+ # Normalized view of the host `options` argument supplied to
33
+ # {BetterAuth::Telemetry.create}.
34
+ #
35
+ # `NormalizedOptions.from(options)` accepts:
36
+ #
37
+ # - a {BetterAuth::Configuration} instance (production path: the value
38
+ # `BetterAuth::Auth#initialize` passes in),
39
+ # - a `Hash` with snake_case or camelCase keys (mirrors the upstream
40
+ # `BetterAuthOptions` shape and the common test seam),
41
+ # - or `nil` (every reader returns `nil` / a default-fallback logger).
42
+ #
43
+ # ## Telemetry opt-in precedence
44
+ #
45
+ # `telemetry_enabled` and `telemetry_debug` use the upstream
46
+ # `nil`/`true`/`false` precedence semantics:
47
+ #
48
+ # - `nil` means "not configured at the option layer" (the env layer
49
+ # may still opt the process in via `BETTER_AUTH_TELEMETRY`).
50
+ # - `true` is an explicit opt-in (subject to the test-environment skip
51
+ # unless `skip_test_check` overrides it).
52
+ # - `false` is an explicit opt-out that overrides every env opt-in.
53
+ #
54
+ # The readers resolve `telemetry[:enabled]` and `telemetry[:debug]`
55
+ # from either a {BetterAuth::Configuration} or a raw Hash.
56
+ #
57
+ # ## Logger
58
+ #
59
+ # The {#logger} reader always returns a usable {LoggerAdapter}.
60
+ # When the host supplies no logger we fall back to
61
+ # `BetterAuth::Logger.create` via {LoggerAdapter.from} so callers
62
+ # never have to nil-check.
63
+ class NormalizedOptions
64
+ # @return [BetterAuth::Configuration, nil] the raw configuration
65
+ # instance when the host passed one, otherwise `nil`. Useful for
66
+ # detectors that want to read additional fields without going
67
+ # through this value object.
68
+ attr_reader :configuration
69
+
70
+ # @return [String, nil] the resolved `app_name` from the
71
+ # configuration or hash, or `nil` when not configured.
72
+ attr_reader :app_name
73
+
74
+ # @return [String, nil] the resolved `base_url` from the
75
+ # configuration or hash, or `nil` when not configured.
76
+ attr_reader :base_url
77
+
78
+ # @return [Boolean, nil] explicit option-layer opt-in / opt-out for
79
+ # telemetry. `nil` defers to env. See class docs for precedence.
80
+ attr_reader :telemetry_enabled
81
+
82
+ # @return [Boolean, nil] explicit option-layer toggle for debug
83
+ # mode. `nil` defers to `BETTER_AUTH_TELEMETRY_DEBUG`.
84
+ attr_reader :telemetry_debug
85
+
86
+ # @return [LoggerAdapter] always-usable logger adapter. Falls back
87
+ # to the default {BetterAuth::Logger} when no logger was supplied.
88
+ attr_reader :logger
89
+
90
+ # Build a {NormalizedOptions} from a {BetterAuth::Configuration},
91
+ # a `Hash`, or `nil`.
92
+ #
93
+ # @param options [BetterAuth::Configuration, Hash, nil]
94
+ # @return [NormalizedOptions]
95
+ def self.from(options)
96
+ if options.is_a?(::BetterAuth::Configuration)
97
+ from_configuration(options)
98
+ elsif options.is_a?(Hash)
99
+ from_hash(options)
100
+ else
101
+ new(
102
+ configuration: nil,
103
+ app_name: nil,
104
+ base_url: nil,
105
+ telemetry_enabled: nil,
106
+ telemetry_debug: nil,
107
+ raw_logger: nil
108
+ )
109
+ end
110
+ end
111
+
112
+ # @api private
113
+ def self.from_configuration(configuration)
114
+ telemetry =
115
+ if configuration.respond_to?(:telemetry) && configuration.telemetry.is_a?(Hash)
116
+ configuration.telemetry
117
+ else
118
+ {}
119
+ end
120
+ new(
121
+ configuration: configuration,
122
+ app_name: configuration.app_name,
123
+ base_url: configuration.base_url,
124
+ telemetry_enabled: Options.fetch_key(telemetry, :enabled, :enabled),
125
+ telemetry_debug: Options.fetch_key(telemetry, :debug, :debug),
126
+ raw_logger: configuration.logger
127
+ )
128
+ end
129
+
130
+ # @api private
131
+ def self.from_hash(hash)
132
+ telemetry = Options.fetch_key(hash, :telemetry, :telemetry)
133
+ telemetry = telemetry.is_a?(Hash) ? telemetry : {}
134
+
135
+ new(
136
+ configuration: nil,
137
+ app_name: Options.fetch_key(hash, :app_name, :appName),
138
+ base_url: Options.fetch_key(hash, :base_url, :baseURL),
139
+ telemetry_enabled: Options.fetch_key(telemetry, :enabled, :enabled),
140
+ telemetry_debug: Options.fetch_key(telemetry, :debug, :debug),
141
+ raw_logger: Options.fetch_key(hash, :logger, :logger)
142
+ )
143
+ end
144
+
145
+ # @api private
146
+ def initialize(configuration:, app_name:, base_url:, telemetry_enabled:, telemetry_debug:, raw_logger:)
147
+ @configuration = configuration
148
+ @app_name = app_name
149
+ @base_url = base_url
150
+ @telemetry_enabled = telemetry_enabled
151
+ @telemetry_debug = telemetry_debug
152
+ @logger = LoggerAdapter.from(raw_logger)
153
+ end
154
+ end
155
+
156
+ # Normalized view of the optional `context` argument supplied to
157
+ # {BetterAuth::Telemetry.create}.
158
+ #
159
+ # `NormalizedContext.from(context)` accepts:
160
+ #
161
+ # - a `Hash` with snake_case or camelCase keys (`:custom_track` /
162
+ # `:customTrack`, `:skip_test_check` / `:skipTestCheck`,
163
+ # `:database`, `:adapter`),
164
+ # - or `nil` (every reader returns its default).
165
+ #
166
+ # Defaults:
167
+ #
168
+ # - {#custom_track} — `nil` when missing.
169
+ # - {#database} — `nil` when missing.
170
+ # - {#adapter} — `nil` when missing.
171
+ # - {#skip_test_check} — `false` when missing or `nil`. Any other
172
+ # value is preserved as-is so the decision layer can apply its own
173
+ # truthiness check.
174
+ class NormalizedContext
175
+ # @return [#call, nil] caller-supplied tracker. When present, every
176
+ # event is delivered to `custom_track.call(event)` instead of via
177
+ # HTTP. The primary testing seam.
178
+ attr_reader :custom_track
179
+
180
+ # @return [String, nil] override for the database name reported in
181
+ # the init event. Bypasses the {Detectors::Database} chain when
182
+ # present.
183
+ attr_reader :database
184
+
185
+ # @return [String, nil] adapter class name, populated by
186
+ # `BetterAuth::Auth#initialize`. Pass-through into the auth-config
187
+ # payload's `adapter` key.
188
+ attr_reader :adapter
189
+
190
+ # @return [Boolean] whether to bypass the
191
+ # `RACK_ENV/RAILS_ENV/APP_ENV == "test"` skip. Does NOT
192
+ # force-enable telemetry on its own; the opt-in from
193
+ # {NormalizedOptions} or env still has to be in place.
194
+ attr_reader :skip_test_check
195
+
196
+ # Build a {NormalizedContext} from a `Hash` or `nil`.
197
+ #
198
+ # @param context [Hash, nil]
199
+ # @return [NormalizedContext]
200
+ def self.from(context)
201
+ hash = context.is_a?(Hash) ? context : {}
202
+ skip = Options.fetch_key(hash, :skip_test_check, :skipTestCheck)
203
+ new(
204
+ custom_track: Options.fetch_key(hash, :custom_track, :customTrack),
205
+ database: Options.fetch_key(hash, :database, :database),
206
+ adapter: Options.fetch_key(hash, :adapter, :adapter),
207
+ skip_test_check: skip.nil? ? false : skip
208
+ )
209
+ end
210
+
211
+ # @api private
212
+ def initialize(custom_track:, database:, adapter:, skip_test_check:)
213
+ @custom_track = custom_track
214
+ @database = database
215
+ @adapter = adapter
216
+ @skip_test_check = skip_test_check
217
+ end
218
+ end
219
+
220
+ module Options
221
+ # Look up a key in a hash, accepting symbol and string forms of both
222
+ # the snake_case and camelCase variants. Returns `nil` when nothing
223
+ # matches; an explicit `nil` value also returns `nil`.
224
+ #
225
+ # @param hash [Hash]
226
+ # @param snake [Symbol] snake_case key (canonical Ruby form).
227
+ # @param camel [Symbol] camelCase key (upstream form).
228
+ # @return [Object, nil]
229
+ def self.fetch_key(hash, snake, camel)
230
+ return nil unless hash.is_a?(Hash)
231
+
232
+ keys = [snake, camel, snake.to_s, camel.to_s].uniq
233
+ keys.each do |key|
234
+ return hash[key] if hash.key?(key)
235
+ end
236
+ nil
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "digest"
5
+ require "securerandom"
6
+
7
+ require_relative "version"
8
+
9
+ module BetterAuth
10
+ module Telemetry
11
+ # Thread-local registry that lets {ProjectId.resolve_project_name}
12
+ # discover the host's `app_name` without changing the public method
13
+ # signature `BetterAuth::Telemetry.project_id(base_url)` (Requirement
14
+ # 14.1).
15
+ #
16
+ # `BetterAuth::Telemetry.create` sets `app_name` for the duration of
17
+ # an init flow via {.with_app_name}; outside of that scope the reader
18
+ # returns `nil` and the project-name resolver falls through to the
19
+ # next rule in the chain (Bundler.locked_gems → Bundler.root).
20
+ #
21
+ # The store is per-thread so concurrent `create` calls in different
22
+ # threads don't clobber each other.
23
+ module CurrentOptions
24
+ KEY = :better_auth_telemetry_current_options_app_name
25
+
26
+ module_function
27
+
28
+ # @return [String, nil] the app name set by the most recent
29
+ # {.with_app_name} block on the current thread, or `nil`.
30
+ def app_name
31
+ Thread.current[KEY]
32
+ end
33
+
34
+ # @param value [String, nil]
35
+ # @return [String, nil] the value just stored.
36
+ def app_name=(value)
37
+ Thread.current[KEY] = value
38
+ end
39
+
40
+ # Run `block` with `app_name` set to `value`, restoring the prior
41
+ # value (typically `nil`) on the way out — even when the block
42
+ # raises.
43
+ #
44
+ # @param value [String, nil]
45
+ # @yield with the thread-local app name temporarily set.
46
+ # @return [Object] whatever the block returns.
47
+ def with_app_name(value)
48
+ prior = Thread.current[KEY]
49
+ Thread.current[KEY] = value
50
+ yield
51
+ ensure
52
+ Thread.current[KEY] = prior
53
+ end
54
+ end
55
+
56
+ # Project-name resolver used by {BetterAuth::Telemetry.project_id}.
57
+ #
58
+ # The chain (Requirement 14.7) is:
59
+ #
60
+ # 1. {CurrentOptions.app_name} — when set and not the default
61
+ # `"Better Auth"`.
62
+ # 2. The first entry of `Bundler.locked_gems.specs` — the
63
+ # Gemfile.lock-pinned name of the current project.
64
+ # 3. `File.basename(Bundler.root)` — the directory name of the
65
+ # Gemfile root, used when the lockfile yields nothing useful.
66
+ #
67
+ # Every fallback is wrapped in `rescue StandardError; nil` so that a
68
+ # missing Bundler load, an unreadable lockfile, or any unrelated
69
+ # error in one rule degrades to the next rule rather than escaping
70
+ # to the caller (Requirement 14.8).
71
+ module ProjectId
72
+ # Upstream sentinel: the `Better Auth` literal is treated as "not
73
+ # configured" so the chain falls through to the Bundler signals.
74
+ DEFAULT_APP_NAME = "Better Auth"
75
+
76
+ module_function
77
+
78
+ # @return [String, nil] the resolved project name, or `nil` when
79
+ # no rule produced a non-empty string.
80
+ def resolve_project_name
81
+ from_app_name || from_locked_gems || from_bundler_root
82
+ rescue
83
+ nil
84
+ end
85
+
86
+ # Read the host's `app_name` from {CurrentOptions}. Treats the
87
+ # literal `"Better Auth"` (the upstream default) as "not
88
+ # configured" so it never wins over the Bundler-derived rules.
89
+ #
90
+ # @return [String, nil]
91
+ def from_app_name
92
+ name = CurrentOptions.app_name
93
+ return nil if name.nil?
94
+ return nil unless name.is_a?(String)
95
+ return nil if name.empty?
96
+ return nil if name == DEFAULT_APP_NAME
97
+
98
+ name
99
+ rescue
100
+ nil
101
+ end
102
+
103
+ # First gemspec in `Bundler.locked_gems.specs`. Mirrors the
104
+ # upstream `package.json#name` lookup. Returns `nil` when Bundler
105
+ # is not loaded, no lockfile is locatable, or the spec list is
106
+ # empty.
107
+ #
108
+ # @return [String, nil]
109
+ def from_locked_gems
110
+ return nil unless defined?(::Bundler)
111
+
112
+ locked = ::Bundler.locked_gems
113
+ return nil if locked.nil?
114
+
115
+ spec = locked.specs&.first
116
+ return nil if spec.nil?
117
+
118
+ name = spec.name
119
+ return nil if name.nil? || name.empty?
120
+
121
+ name
122
+ rescue
123
+ nil
124
+ end
125
+
126
+ # Directory name of `Bundler.root`. The closest Ruby analog to
127
+ # upstream's "directory containing package.json" fallback.
128
+ #
129
+ # @return [String, nil]
130
+ def from_bundler_root
131
+ return nil unless defined?(::Bundler)
132
+
133
+ root = ::Bundler.root
134
+ return nil if root.nil?
135
+
136
+ name = File.basename(root.to_s)
137
+ return nil if name.nil? || name.empty?
138
+
139
+ name
140
+ rescue
141
+ nil
142
+ end
143
+ end
144
+
145
+ @project_id_cache = nil
146
+ @project_id_mutex = Mutex.new
147
+
148
+ # Resolve a stable, anonymous project id for telemetry.
149
+ #
150
+ # The id is derived once per process and memoized; subsequent calls
151
+ # — regardless of the `base_url` they pass — return the cached
152
+ # value (Requirement 14.6). This mirrors the upstream
153
+ # `projectIdCached` module-scope variable.
154
+ #
155
+ # ## Derivation chain (Requirements 14.2 – 14.5)
156
+ #
157
+ # 1. Project name resolvable AND `base_url` non-empty:
158
+ # `Base64(SHA-256(base_url + name))`.
159
+ # 2. Project name resolvable AND `base_url` nil/empty:
160
+ # `Base64(SHA-256(name))`.
161
+ # 3. No project name AND `base_url` non-empty:
162
+ # `Base64(SHA-256(base_url))`.
163
+ # 4. Otherwise: a random 32-character `[a-zA-Z0-9]` id from
164
+ # `SecureRandom`, matching upstream `generateId(32)`.
165
+ #
166
+ # The Bundler/lockfile probes inside {ProjectId.resolve_project_name}
167
+ # never raise out of this method (Requirement 14.8); a failed probe
168
+ # collapses to "no project name" and the chain continues at rule 3
169
+ # or rule 4.
170
+ #
171
+ # @param base_url [String, nil] the host's configured base URL.
172
+ # @return [String] the memoized anonymous project id.
173
+ def self.project_id(base_url)
174
+ cached = @project_id_cache
175
+ return cached if cached
176
+
177
+ @project_id_mutex.synchronize do
178
+ cached = @project_id_cache
179
+ return cached if cached
180
+
181
+ @project_id_cache = derive_project_id(base_url)
182
+ end
183
+ end
184
+
185
+ # Test-only hook that clears the memoized project id cache.
186
+ #
187
+ # Wired here in task 3.6 to clear the `@project_id_cache` ivar that
188
+ # backs {.project_id}. Tests use this between cases that exercise
189
+ # different derivation rules (e.g. with vs. without a project name)
190
+ # so each call goes through the full chain again.
191
+ #
192
+ # @return [nil]
193
+ def self.reset_project_id!
194
+ @project_id_mutex.synchronize do
195
+ @project_id_cache = nil
196
+ end
197
+ nil
198
+ end
199
+
200
+ # @api private
201
+ def self.derive_project_id(base_url)
202
+ url = base_url.is_a?(String) ? base_url : nil
203
+ url = nil if url && url.empty?
204
+
205
+ name = ProjectId.resolve_project_name
206
+ name = nil if name.is_a?(String) && name.empty?
207
+
208
+ if name && url
209
+ hash_to_base64(url + name)
210
+ elsif name
211
+ hash_to_base64(name)
212
+ elsif url
213
+ hash_to_base64(url)
214
+ else
215
+ random_id_32
216
+ end
217
+ end
218
+
219
+ # @api private
220
+ def self.hash_to_base64(input)
221
+ Base64.strict_encode64(Digest::SHA256.digest(input.to_s))
222
+ end
223
+
224
+ # @api private
225
+ PROJECT_ID_ALPHABET = (
226
+ ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
227
+ ).freeze
228
+
229
+ # @api private
230
+ def self.random_id_32
231
+ Array.new(32) { PROJECT_ID_ALPHABET[SecureRandom.random_number(PROJECT_ID_ALPHABET.length)] }.join
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "project_id"
4
+
5
+ module BetterAuth
6
+ module Telemetry
7
+ # Publisher returned from {BetterAuth::Telemetry.create} when telemetry is
8
+ # opted-in. The publisher is delivery-agnostic: it does not know whether
9
+ # the configured `track` callable forwards events over HTTP, hands them to
10
+ # a host-supplied `custom_track`, or routes them through the debug logger.
11
+ # All of that branching is built into the `track` lambda once at
12
+ # `create`-time, and the `Publisher` simply normalizes each event and
13
+ # forwards it through (Requirements 5.6, 5.7, 6.10, 15.1, 15.2).
14
+ #
15
+ # ## Responsibilities
16
+ #
17
+ # 1. Short-circuit to `nil` when `enabled` is `false`, so a disabled
18
+ # publisher is a noop. `#enabled?` reports the flag verbatim.
19
+ # 2. Lazily resolve `anonymous_id` on the first `#publish` call by
20
+ # delegating to {BetterAuth::Telemetry.project_id} when the publisher
21
+ # was constructed without one. The result is cached on the instance,
22
+ # so subsequent `#publish` calls reuse the same `anonymousId`.
23
+ # 3. Normalize each event hash to symbol keys with the upstream wire
24
+ # shape (`type`, `payload`, `anonymousId`). Both `:type`/`:payload`
25
+ # and `"type"`/`"payload"` input keys are accepted; missing
26
+ # `:payload` falls back to `{}`.
27
+ # 4. Forward the normalized event through `track.call(event)` and
28
+ # rescue any `StandardError` raised by the callable, routing the
29
+ # failure through `logger.error(...)` and returning `nil`. Errors in
30
+ # HTTP delivery, custom_track callbacks, or JSON encoding therefore
31
+ # never escape `#publish`.
32
+ #
33
+ # The publisher is intentionally stateless beyond the cached
34
+ # `anonymous_id`: there is no internal queue and no batching. It calls
35
+ # the supplied `track` lambda synchronously; the HTTP track implementation
36
+ # may then hand the actual POST to a short-lived background thread.
37
+ #
38
+ # @example wiring with a `RecordingTrack` (test seam)
39
+ # recorder = BetterAuth::Telemetry::Test::RecordingTrack.new
40
+ # publisher = BetterAuth::Telemetry::Publisher.new(
41
+ # enabled: true,
42
+ # anonymous_id: nil,
43
+ # track: recorder,
44
+ # base_url: "https://example.com",
45
+ # logger: BetterAuth::Telemetry::LoggerAdapter.from(nil)
46
+ # )
47
+ # publisher.publish(type: "ping", payload: {})
48
+ # recorder.last # => { type: :ping, payload: {}, anonymousId: "..." }
49
+ class Publisher
50
+ # @param enabled [Boolean] whether the publisher should forward events.
51
+ # When `false`, every `#publish` call is a noop returning `nil`.
52
+ # @param anonymous_id [String, nil] the resolved anonymous project id,
53
+ # or `nil` to defer resolution to the first `#publish` call.
54
+ # @param track [#call] callable that receives the normalized event
55
+ # hash. Built once by `BetterAuth::Telemetry.create` and closes over
56
+ # the chosen delivery mode (custom_track, debug, or http).
57
+ # @param base_url [String, nil] the host's base URL, forwarded to
58
+ # {BetterAuth::Telemetry.project_id} when lazy-resolving
59
+ # `anonymous_id`.
60
+ # @param logger [#error] log adapter used to surface delivery
61
+ # failures (`StandardError`) raised by `track`.
62
+ def initialize(enabled:, anonymous_id:, track:, base_url:, logger:)
63
+ @enabled = enabled
64
+ @anonymous_id = anonymous_id
65
+ @track = track
66
+ @base_url = base_url
67
+ @logger = logger
68
+ end
69
+
70
+ # Forward an event through the configured `track` callable.
71
+ #
72
+ # Returns `nil` when the publisher is disabled. Otherwise normalizes
73
+ # the input event to `{type:, payload:, anonymousId:}` (symbol keys),
74
+ # lazy-resolves `anonymous_id` on first use, and dispatches via
75
+ # `track.call(event_to_emit)`. Any `StandardError` raised by `track`
76
+ # is rescued and logged at error level; the call still returns `nil`.
77
+ #
78
+ # The `event` argument may carry either symbol (`:type`, `:payload`)
79
+ # or string (`"type"`, `"payload"`) keys; missing `:payload` defaults
80
+ # to `{}`. Output keys are always symbols, matching the upstream wire
81
+ # format.
82
+ #
83
+ # @param event [Hash] the event to publish; accepts symbol or string
84
+ # `:type`/`:payload` keys.
85
+ # @return [nil]
86
+ def publish(event)
87
+ return nil unless @enabled
88
+
89
+ @anonymous_id ||= BetterAuth::Telemetry.project_id(@base_url)
90
+
91
+ event_to_emit = {
92
+ type: event[:type] || event["type"],
93
+ payload: event[:payload] || event["payload"] || {},
94
+ anonymousId: @anonymous_id
95
+ }
96
+
97
+ @track.call(event_to_emit)
98
+ nil
99
+ rescue => e
100
+ @logger.error("[better-auth.telemetry] publish failed: #{e.class}: #{e.message}")
101
+ nil
102
+ end
103
+
104
+ # @return [Boolean] whether this publisher is opted-in. `false` means
105
+ # `#publish` is a noop.
106
+ def enabled?
107
+ @enabled
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Telemetry
5
+ VERSION = "0.8.0"
6
+ end
7
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public entry point for the `better_auth-telemetry` gem.
4
+ #
5
+ # Requiring this file pulls in every internal component the telemetry
6
+ # pipeline depends on so callers only need a single
7
+ # `require "better_auth/telemetry"` to access the full public surface:
8
+ #
9
+ # - {BetterAuth::Telemetry.create} — build a publisher tailored to the
10
+ # host's opt-in state.
11
+ # - {BetterAuth::Telemetry.project_id} /
12
+ # {BetterAuth::Telemetry.reset_project_id!} — anonymous project id
13
+ # resolution and the test-only cache reset hook.
14
+ # - {BetterAuth::Telemetry::Publisher} /
15
+ # {BetterAuth::Telemetry::NoopPublisher} — the two publisher shapes
16
+ # `create` returns.
17
+ # - {BetterAuth::Telemetry::Detectors} — the seven detector modules
18
+ # (`Runtime`, `Environment`, `SystemInfo`, `Database`, `Framework`,
19
+ # `ProjectInfo`, `AuthConfig`).
20
+ # - Supporting value objects and helpers
21
+ # ({BetterAuth::Telemetry::NormalizedOptions},
22
+ # {BetterAuth::Telemetry::NormalizedContext},
23
+ # {BetterAuth::Telemetry::CurrentOptions},
24
+ # {BetterAuth::Telemetry::Env},
25
+ # {BetterAuth::Telemetry::HttpClient},
26
+ # {BetterAuth::Telemetry::LoggerAdapter}).
27
+ #
28
+ # The standard-library requires below are listed once at the entry
29
+ # point so individual internal files can rely on them being loaded.
30
+ # Every internal file additionally requires what it directly depends
31
+ # on, so any single file is independently loadable.
32
+
33
+ require "better_auth"
34
+
35
+ require "base64"
36
+ require "digest"
37
+ require "json"
38
+ require "net/http"
39
+ require "securerandom"
40
+ require "uri"
41
+
42
+ require_relative "telemetry/version"
43
+ require_relative "telemetry/noop_publisher"
44
+ require_relative "telemetry/logger_adapter"
45
+ require_relative "telemetry/options"
46
+ require_relative "telemetry/env"
47
+ require_relative "telemetry/http_client"
48
+ require_relative "telemetry/project_id"
49
+ require_relative "telemetry/publisher"
50
+ require_relative "telemetry/create"
51
+
52
+ require_relative "telemetry/detectors/runtime"
53
+ require_relative "telemetry/detectors/environment"
54
+ require_relative "telemetry/detectors/system_info"
55
+ require_relative "telemetry/detectors/database"
56
+ require_relative "telemetry/detectors/framework"
57
+ require_relative "telemetry/detectors/project_info"
58
+ require_relative "telemetry/detectors/auth_config"
59
+
60
+ module BetterAuth
61
+ # Top-level namespace for the `better_auth-telemetry` gem.
62
+ #
63
+ # See `BetterAuth::Telemetry.create` for the entry point used by
64
+ # `BetterAuth::Auth#initialize` and by tests that exercise the
65
+ # publisher in isolation.
66
+ module Telemetry
67
+ end
68
+ end