better_auth-telemetry 0.8.0 → 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f696d840e8f9fb96751e211c5ad327bf255c3460b99c7adfccab3cbb9c10a1d
4
- data.tar.gz: 3b513054876f4c07d8ecfd2bca1705a94b3406226c3270815b084fa5450429fe
3
+ metadata.gz: c6795c847c337c646d429b2864c222edeadd7e5562e8bd475e6036da5795096d
4
+ data.tar.gz: 8892aaff5026e90b664a99793e77470d4aa6f8e4dfeef12cb2e4d67268938b8b
5
5
  SHA512:
6
- metadata.gz: b17c3cebcaae59c8a9cc0d4b936ec2f793fa4004cadf6599aacdaa806b407e6fcd1631036d0039c1746f4dcb43641731a8221deb226eaf6e31fc6dfecb6b52c7
7
- data.tar.gz: 437bd20b0f121eb0fa8d9f884545e2f0b1978a4c3f73c4bf6d1de9261d1f8d2cf3d410c188a03f1967f17d3c22c687315bcb97fd65300258a80b846ca42faaff
6
+ metadata.gz: 99a180b67dfa37a42eb2870b36ac371bca99badcfd6c159687160296ed2e17bc0550950280041b237d45f867e4ee82d66cea7eb8e52618c3ad80938c1685aee4
7
+ data.tar.gz: aad2e4e3b2eea17b81bd1d34c6ff7dc4acec635b54f1d3e0a6e39574e79aca5f899ed5dde67e3994c8b63f844bd762a714eda89c03b98120133caccfd099c3e3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ - Improved database detection, including MongoDB adapter metadata.
6
+ - Updated telemetry docs for the current package set.
7
+
3
8
  ## 0.8.0
4
9
 
5
10
  - Initial release. Ports the upstream `@better-auth/telemetry` package
data/README.md CHANGED
@@ -71,8 +71,9 @@ export BETTER_AUTH_TELEMETRY_ENDPOINT=https://telemetry.example.com/ingest
71
71
 
72
72
  ## Test environment skip
73
73
 
74
- When `RACK_ENV`, `RAILS_ENV`, or `APP_ENV` equals `"test"`, telemetry is skipped
75
- even if it is otherwise opted in. Bypass this skip by setting
74
+ When `RACK_ENV`, `RAILS_ENV`, or `APP_ENV` equals `"test"`, or `TEST` is set to
75
+ a truthy value, telemetry is skipped even if it is otherwise opted in. Bypass
76
+ this skip by setting
76
77
  `context[:skip_test_check] = true`. `skip_test_check` only bypasses the test
77
78
  gate; it does not opt telemetry in on its own.
78
79
 
@@ -100,11 +101,13 @@ auth = BetterAuth.auth(
100
101
  ```
101
102
 
102
103
  When neither debug mode nor `custom_track` is configured and an endpoint is
103
- set, the publisher starts a short-lived background thread that POSTs JSON
104
- events to the endpoint via `Net::HTTP` with a 5-second open + read timeout.
105
- HTTP telemetry is fire-and-forget, so constructing `BetterAuth.auth` is not
106
- blocked by a slow or unavailable endpoint. Any `StandardError` raised during
107
- HTTP delivery is rescued and logged at error level rather than propagated.
104
+ set, the publisher enqueues events into a bounded in-process dispatcher. A
105
+ single short-lived worker POSTs JSON events to the endpoint via `Net::HTTP` with
106
+ 5-second open, read, and write timeouts. HTTP telemetry is fire-and-forget, so
107
+ constructing `BetterAuth.auth` and request-time `#publish` calls are not blocked
108
+ by a slow or unavailable endpoint. If the queue is full, the event is dropped
109
+ and a payload-free error is logged. Any `StandardError` raised during HTTP
110
+ delivery is rescued and logged at error level rather than propagated.
108
111
 
109
112
  ## The `custom_track` injection seam
110
113
 
@@ -174,10 +177,27 @@ The intentional Ruby-specific deviations are:
174
177
  to `Gem.loaded_specs` for `sequel`, `pg`, `mysql2`, `sqlite3`,
175
178
  `activerecord`, `mongoid`, `mongo`, `rom-sql` (in that order) when no
176
179
  context override or `BetterAuth::Adapters::*` adapter class match is found.
177
- - **Standard library only HTTP.** HTTP delivery uses `Net::HTTP` with a
178
- 5-second open + read timeout inside a short-lived background thread. No
180
+ Known Better Auth adapter classes are reported as `memory`, `postgres`,
181
+ `mysql`, `sqlite`, `mssql`, or `mongodb`. When core passes the generic
182
+ `"adapter"` database marker for an external adapter, telemetry refines it
183
+ from `context.adapter` only when the adapter class is known; unknown
184
+ namespaced adapters remain the generic `"adapter"` marker.
185
+ - **Telemetry tests validate metadata only.** This package does not boot real
186
+ Rails, Sinatra, Hanami, or database-backed applications, and it does not run
187
+ rate-limit behavior against every storage backend. Those behaviors belong to
188
+ the framework, adapter, and core packages. Telemetry coverage is intentionally
189
+ limited to detector precedence, redaction shape, opt-in decisions, and
190
+ delivery behavior.
191
+ - **Standard library only HTTP.** HTTP delivery uses `Net::HTTP` with 5-second
192
+ open, read, and write timeouts behind a bounded single-worker dispatcher. No
179
193
  external HTTP-client gem is required at runtime, and HTTP delivery does not
180
- block `BetterAuth.auth` construction.
194
+ block `BetterAuth.auth` construction or request-time `#publish` calls.
195
+ - **Safer Ruby telemetry redaction.** Ruby object values that JavaScript would
196
+ omit or stringify unsafely are reduced before delivery. Field maps,
197
+ additional-field maps, trusted-provider lists, custom cookie/header lists,
198
+ `advanced.database.generateId`, `onAPIError.errorURL`, and unknown namespaced
199
+ adapter class names are emitted as counts, booleans, or the generic
200
+ `"adapter"` marker instead of raw app-specific values.
181
201
  - **Explicit false is a strong opt-out.** `telemetry: { enabled: false }`
182
202
  disables telemetry even when `BETTER_AUTH_TELEMETRY` or `OPEN_AUTH_TELEMETRY`
183
203
  is truthy. This is intentionally stricter than upstream so application
@@ -191,6 +211,10 @@ The intentional Ruby-specific deviations are:
191
211
  `BetterAuth::Telemetry.project_id` to derive the `anonymousId` but is
192
212
  intentionally not emitted as a payload field, since it can be
193
213
  user-identifying.
214
+ - **Project IDs are keyed by derivation input.** A process hosting multiple
215
+ auth instances can derive distinct anonymous IDs for distinct
216
+ `app_name`/`base_url` pairs. When no app name is configured, the Ruby fallback
217
+ uses the Bundler root directory name rather than the first locked dependency.
194
218
  - **Public `BetterAuth::Telemetry.reset_project_id!` testing helper.** A
195
219
  module-level helper is exposed for resetting the memoized
196
220
  `anonymous_id` between tests. It has no effect on production behavior and
@@ -4,6 +4,7 @@ require "json"
4
4
 
5
5
  require_relative "env"
6
6
  require_relative "http_client"
7
+ require_relative "http_dispatcher"
7
8
  require_relative "noop_publisher"
8
9
  require_relative "options"
9
10
  require_relative "project_id"
@@ -161,7 +162,7 @@ module BetterAuth
161
162
 
162
163
  # @api private
163
164
  def self.in_test_env?
164
- TEST_ENV_VARS.any? { |k| ENV[k] == "test" }
165
+ TEST_ENV_VARS.any? { |k| ENV[k] == "test" } || Env.truthy?(ENV["TEST"])
165
166
  end
166
167
 
167
168
  # Decide whether debug mode is active. The option-layer flag wins
@@ -184,9 +185,9 @@ module BetterAuth
184
185
  # requiring `BETTER_AUTH_TELEMETRY_ENDPOINT` to be set.
185
186
  # 2. Debug mode active — log the JSON-pretty event via
186
187
  # `logger.info(...)` and skip HTTP entirely (Requirement 5.9).
187
- # 3. Default — fire-and-forget JSON `POST` through a short-lived
188
- # background thread calling {HttpClient.post_json}, which
189
- # already swallows transport errors.
188
+ # 3. Default — fire-and-forget JSON `POST` through a bounded
189
+ # {HttpDispatcher}, which calls {HttpClient.post_json} from a
190
+ # single short-lived worker.
190
191
  #
191
192
  # Every branch wraps its dispatch in a `rescue StandardError` that
192
193
  # routes the failure through `logger.error(...)`, so callable /
@@ -217,12 +218,9 @@ module BetterAuth
217
218
  nil
218
219
  end
219
220
  else
221
+ dispatcher = HttpDispatcher.new(endpoint: endpoint, logger: logger)
220
222
  lambda do |event|
221
- Thread.new do
222
- HttpClient.post_json(endpoint, event, logger: logger)
223
- rescue => e
224
- logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.message}")
225
- end
223
+ dispatcher.call(event)
226
224
  nil
227
225
  rescue => e
228
226
  logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.message}")
@@ -77,7 +77,7 @@ module BetterAuth
77
77
  def call(options, context)
78
78
  {
79
79
  database: context_value(context, :database),
80
- adapter: context_value(context, :adapter),
80
+ adapter: sanitize_adapter(context_value(context, :adapter)),
81
81
  emailVerification: redact_email_verification(options),
82
82
  emailAndPassword: redact_email_and_password(options),
83
83
  socialProviders: redact_social_providers(options),
@@ -147,6 +147,13 @@ module BetterAuth
147
147
  Array(array).length
148
148
  end
149
149
 
150
+ def count_metadata(value)
151
+ return nil if value.nil?
152
+ return value.length if value.is_a?(Hash) || value.is_a?(Array)
153
+
154
+ raw(value)
155
+ end
156
+
150
157
  # ------------------------------------------------------------------
151
158
  # Unified accessor
152
159
  # ------------------------------------------------------------------
@@ -335,8 +342,8 @@ module BetterAuth
335
342
  def redact_user(opts)
336
343
  {
337
344
  modelName: raw(fetch_path(opts, [:user, :model_name])),
338
- fields: raw(fetch_path(opts, [:user, :fields])),
339
- additionalFields: raw(fetch_path(opts, [:user, :additional_fields])),
345
+ fields: count_metadata(fetch_path(opts, [:user, :fields])),
346
+ additionalFields: count_metadata(fetch_path(opts, [:user, :additional_fields])),
340
347
  changeEmail: {
341
348
  enabled: raw(fetch_path(opts, [:user, :change_email, :enabled])),
342
349
  sendChangeEmailConfirmation: bool(fetch_path(opts, [:user, :change_email, :send_change_email_confirmation]))
@@ -353,7 +360,7 @@ module BetterAuth
353
360
  {
354
361
  modelName: raw(fetch_path(opts, [:verification, :model_name])),
355
362
  disableCleanup: raw(fetch_path(opts, [:verification, :disable_cleanup])),
356
- fields: raw(fetch_path(opts, [:verification, :fields]))
363
+ fields: count_metadata(fetch_path(opts, [:verification, :fields]))
357
364
  }
358
365
  end
359
366
 
@@ -367,7 +374,7 @@ module BetterAuth
367
374
  def redact_session(opts)
368
375
  {
369
376
  modelName: raw(fetch_path(opts, [:session, :model_name])),
370
- additionalFields: raw(fetch_path(opts, [:session, :additional_fields])),
377
+ additionalFields: count_metadata(fetch_path(opts, [:session, :additional_fields])),
371
378
  cookieCache: {
372
379
  enabled: raw(fetch_path(opts, [:session, :cookie_cache, :enabled])),
373
380
  maxAge: raw(fetch_path(opts, [:session, :cookie_cache, :max_age])),
@@ -375,7 +382,7 @@ module BetterAuth
375
382
  },
376
383
  disableSessionRefresh: raw(fetch_path(opts, [:session, :disable_session_refresh])),
377
384
  expiresIn: raw(fetch_path(opts, [:session, :expires_in])),
378
- fields: raw(fetch_path(opts, [:session, :fields])),
385
+ fields: count_metadata(fetch_path(opts, [:session, :fields])),
379
386
  freshAge: raw(fetch_path(opts, [:session, :fresh_age])),
380
387
  preserveSessionInDatabase: raw(fetch_path(opts, [:session, :preserve_session_in_database])),
381
388
  storeSessionInDatabase: raw(fetch_path(opts, [:session, :store_session_in_database])),
@@ -393,12 +400,12 @@ module BetterAuth
393
400
  def redact_account(opts)
394
401
  {
395
402
  modelName: raw(fetch_path(opts, [:account, :model_name])),
396
- fields: raw(fetch_path(opts, [:account, :fields])),
403
+ fields: count_metadata(fetch_path(opts, [:account, :fields])),
397
404
  encryptOAuthTokens: raw(fetch_path(opts, [:account, :encrypt_oauth_tokens])),
398
405
  updateAccountOnSignIn: raw(fetch_path(opts, [:account, :update_account_on_sign_in])),
399
406
  accountLinking: {
400
407
  enabled: raw(fetch_path(opts, [:account, :account_linking, :enabled])),
401
- trustedProviders: raw(fetch_path(opts, [:account, :account_linking, :trusted_providers])),
408
+ trustedProviders: count_metadata(fetch_path(opts, [:account, :account_linking, :trusted_providers])),
402
409
  updateUserInfoOnLink: raw(fetch_path(opts, [:account, :account_linking, :update_user_info_on_link])),
403
410
  allowUnlinkingAll: raw(fetch_path(opts, [:account, :account_linking, :allow_unlinking_all]))
404
411
  }
@@ -464,16 +471,16 @@ module BetterAuth
464
471
  crossSubDomainCookies: {
465
472
  domain: bool(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :domain])),
466
473
  enabled: raw(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :enabled])),
467
- additionalCookies: raw(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :additional_cookies]))
474
+ additionalCookies: count_metadata(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :additional_cookies]))
468
475
  },
469
476
  database: {
470
- generateId: raw(fetch_path(opts, [:advanced, :database, :generate_id])),
477
+ generateId: bool(fetch_path(opts, [:advanced, :database, :generate_id])),
471
478
  defaultFindManyLimit: raw(fetch_path(opts, [:advanced, :database, :default_find_many_limit]))
472
479
  },
473
480
  useSecureCookies: raw(fetch_path(opts, [:advanced, :use_secure_cookies])),
474
481
  ipAddress: {
475
482
  disableIpTracking: raw(fetch_path(opts, [:advanced, :ip_address, :disable_ip_tracking])),
476
- ipAddressHeaders: raw(fetch_path(opts, [:advanced, :ip_address, :ip_address_headers]))
483
+ ipAddressHeaders: count_metadata(fetch_path(opts, [:advanced, :ip_address, :ip_address_headers]))
477
484
  },
478
485
  disableCSRFCheck: raw(fetch_path(opts, [:advanced, :disable_csrf_check])),
479
486
  cookieAttributes: {
@@ -540,7 +547,7 @@ module BetterAuth
540
547
  # @return [Hash{Symbol => Object}]
541
548
  def redact_on_api_error(opts)
542
549
  {
543
- errorURL: raw(fetch_path(opts, [:on_api_error, :error_url])),
550
+ errorURL: bool_present(fetch_path(opts, [:on_api_error, :error_url])),
544
551
  onError: bool(fetch_path(opts, [:on_api_error, :on_error])),
545
552
  throw: raw(fetch_path(opts, [:on_api_error, :throw]))
546
553
  }
@@ -621,6 +628,14 @@ module BetterAuth
621
628
  nil
622
629
  end
623
630
 
631
+ def sanitize_adapter(value)
632
+ return "adapter" if value.is_a?(String) && value.include?("::")
633
+
634
+ value
635
+ rescue
636
+ nil
637
+ end
638
+
624
639
  # Read the root (first segment) of a `fetch_path` lookup.
625
640
  # For a {BetterAuth::Configuration} we call the snake_case
626
641
  # reader; for a Hash we look up the key under both symbol
@@ -21,8 +21,9 @@ module BetterAuth
21
21
  #
22
22
  # 1. **Context override** — when the caller supplied a non-empty
23
23
  # `context.database` string, return it verbatim with
24
- # `version: nil`. This is the upstream `context.database`
25
- # seam.
24
+ # `version: nil`. The generic `"adapter"` marker is refined
25
+ # from `context.adapter` when it names a known
26
+ # `BetterAuth::Adapters::*` class.
26
27
  # 2. **Configuration adapter** — when `options` is a
27
28
  # {BetterAuth::Configuration} (or a hash with a `:database`
28
29
  # key) and the value is a known adapter symbol
@@ -62,7 +63,8 @@ module BetterAuth
62
63
  "BetterAuth::Adapters::MySQL" => "mysql",
63
64
  "BetterAuth::Adapters::SQLite" => "sqlite",
64
65
  "BetterAuth::Adapters::MSSQL" => "mssql",
65
- "BetterAuth::Adapters::Memory" => "memory"
66
+ "BetterAuth::Adapters::Memory" => "memory",
67
+ "BetterAuth::Adapters::MongoDB" => "mongodb"
66
68
  }.freeze
67
69
 
68
70
  # Map from a known {BetterAuth::Configuration#database} symbol
@@ -108,8 +110,10 @@ module BetterAuth
108
110
 
109
111
  # Read `database` from a {NormalizedContext}-like or hash-like
110
112
  # context. Returns the raw string when present and non-empty,
111
- # otherwise `nil`. Non-string values (e.g. a symbol set
112
- # accidentally) are ignored to keep the wire shape stable.
113
+ # otherwise `nil`. The generic `"adapter"` marker is refined
114
+ # from a known `context.adapter` class name when possible.
115
+ # Non-string values (e.g. a symbol set accidentally) are
116
+ # ignored to keep the wire shape stable.
113
117
  #
114
118
  # @param context [#database, Hash, nil]
115
119
  # @return [String, nil]
@@ -126,9 +130,33 @@ module BetterAuth
126
130
  return nil unless value.is_a?(String)
127
131
  return nil if value.empty?
128
132
 
133
+ return context_adapter_identifier(context) || value if value == "adapter"
134
+
129
135
  value
130
136
  end
131
137
 
138
+ # Read `adapter` from a {NormalizedContext}-like or hash-like
139
+ # context and map known `BetterAuth::Adapters::*` class names to
140
+ # their database identifiers. Unknown names stay generic by
141
+ # returning `nil` so the caller can preserve `"adapter"`.
142
+ #
143
+ # @param context [#adapter, Hash, nil]
144
+ # @return [String, nil]
145
+ def context_adapter_identifier(context)
146
+ return nil if context.nil?
147
+
148
+ value =
149
+ if context.respond_to?(:adapter)
150
+ context.adapter
151
+ elsif context.respond_to?(:[])
152
+ context[:adapter] || context["adapter"]
153
+ end
154
+
155
+ return nil unless value.is_a?(String)
156
+
157
+ ADAPTER_CLASS_MAP[value]
158
+ end
159
+
132
160
  # Translate the configuration's `database` value into a short
133
161
  # identifier when it matches a known adapter symbol or a known
134
162
  # `BetterAuth::Adapters::*` class.
@@ -170,7 +198,19 @@ module BetterAuth
170
198
  return ADAPTER_SYMBOLS[value]
171
199
  end
172
200
 
173
- ADAPTER_CLASS_MAP[value.class.name]
201
+ ADAPTER_CLASS_MAP[adapter_class_name(value)]
202
+ end
203
+
204
+ # Resolve an object's class name without requiring the class
205
+ # constant to exist. Some tests and external adapters expose a
206
+ # class-like singleton object whose `#name` carries the adapter
207
+ # identifier.
208
+ #
209
+ # @param value [Object]
210
+ # @return [String, nil]
211
+ def adapter_class_name(value)
212
+ klass = value.class
213
+ klass.name if klass.respond_to?(:name)
174
214
  end
175
215
 
176
216
  # Walk {GEM_FALLBACKS} in order and return the first
@@ -60,6 +60,10 @@ module BetterAuth
60
60
  # Bounded `read_timeout` for `Net::HTTP.start`. See Requirement 5.8.
61
61
  READ_TIMEOUT_SECONDS = 5
62
62
 
63
+ # Bounded `write_timeout` for request-body writes when supported by
64
+ # the active Ruby runtime.
65
+ WRITE_TIMEOUT_SECONDS = 5
66
+
63
67
  # Issue a synchronous JSON `POST` to `url`. Always returns `nil` and
64
68
  # never raises a `StandardError`.
65
69
  #
@@ -84,9 +88,15 @@ module BetterAuth
84
88
  uri.port,
85
89
  use_ssl: uri.scheme == "https",
86
90
  open_timeout: OPEN_TIMEOUT_SECONDS,
87
- read_timeout: READ_TIMEOUT_SECONDS
91
+ read_timeout: READ_TIMEOUT_SECONDS,
92
+ write_timeout: WRITE_TIMEOUT_SECONDS
88
93
  ) do |http|
89
- http.request(request)
94
+ response = http.request(request)
95
+ unless response.is_a?(Net::HTTPSuccess)
96
+ logger.error(
97
+ "[better-auth.telemetry] http delivery failed: HTTP #{response.code} #{response.message}"
98
+ )
99
+ end
90
100
  end
91
101
 
92
102
  nil
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Telemetry
5
+ class HttpDispatcher
6
+ DEFAULT_QUEUE_SIZE = 100
7
+ IDLE_TIMEOUT_SECONDS = 1.0
8
+ EMPTY_SLEEP_SECONDS = 0.01
9
+
10
+ def initialize(endpoint:, logger:, queue_size: DEFAULT_QUEUE_SIZE)
11
+ @endpoint = endpoint
12
+ @logger = logger
13
+ @queue = SizedQueue.new(queue_size)
14
+ @mutex = Mutex.new
15
+ @worker = nil
16
+ end
17
+
18
+ def call(event)
19
+ enqueue(event)
20
+ start_worker
21
+ nil
22
+ rescue => e
23
+ @logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.message}")
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ def enqueue(event)
30
+ @queue.push(event, true)
31
+ rescue ThreadError
32
+ @logger.error("[better-auth.telemetry] http dispatch dropped: queue full")
33
+ end
34
+
35
+ def start_worker
36
+ @mutex.synchronize do
37
+ return if @worker&.alive?
38
+
39
+ @worker = Thread.new { worker_loop }
40
+ @worker.report_on_exception = false if @worker.respond_to?(:report_on_exception=)
41
+ end
42
+ end
43
+
44
+ def worker_loop
45
+ idle_deadline = monotonic_now + IDLE_TIMEOUT_SECONDS
46
+
47
+ loop do
48
+ event = pop_nonblocking
49
+ if event
50
+ idle_deadline = monotonic_now + IDLE_TIMEOUT_SECONDS
51
+ deliver(event)
52
+ elsif monotonic_now >= idle_deadline
53
+ break
54
+ else
55
+ sleep EMPTY_SLEEP_SECONDS
56
+ end
57
+ end
58
+ ensure
59
+ restart = false
60
+ @mutex.synchronize do
61
+ @worker = nil if @worker == Thread.current
62
+ restart = !@queue.empty?
63
+ end
64
+ start_worker if restart
65
+ end
66
+
67
+ def pop_nonblocking
68
+ @queue.pop(true)
69
+ rescue ThreadError
70
+ nil
71
+ end
72
+
73
+ def deliver(event)
74
+ HttpClient.post_json(@endpoint, event, logger: @logger)
75
+ rescue => e
76
+ @logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.message}")
77
+ end
78
+
79
+ def monotonic_now
80
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -16,7 +16,7 @@ module BetterAuth
16
16
  # `BetterAuth::Telemetry.create` sets `app_name` for the duration of
17
17
  # an init flow via {.with_app_name}; outside of that scope the reader
18
18
  # returns `nil` and the project-name resolver falls through to the
19
- # next rule in the chain (Bundler.locked_gems → Bundler.root).
19
+ # Bundler root directory name.
20
20
  #
21
21
  # The store is per-thread so concurrent `create` calls in different
22
22
  # threads don't clobber each other.
@@ -59,10 +59,8 @@ module BetterAuth
59
59
  #
60
60
  # 1. {CurrentOptions.app_name} — when set and not the default
61
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.
62
+ # 2. `File.basename(Bundler.root)` — the directory name of the
63
+ # Gemfile root.
66
64
  #
67
65
  # Every fallback is wrapped in `rescue StandardError; nil` so that a
68
66
  # missing Bundler load, an unreadable lockfile, or any unrelated
@@ -78,7 +76,7 @@ module BetterAuth
78
76
  # @return [String, nil] the resolved project name, or `nil` when
79
77
  # no rule produced a non-empty string.
80
78
  def resolve_project_name
81
- from_app_name || from_locked_gems || from_bundler_root
79
+ from_app_name || from_bundler_root
82
80
  rescue
83
81
  nil
84
82
  end
@@ -100,10 +98,9 @@ module BetterAuth
100
98
  nil
101
99
  end
102
100
 
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.
101
+ # Legacy helper retained as a test seam for older specs. The
102
+ # resolver no longer uses the first locked dependency as project
103
+ # identity because that can collide across unrelated apps.
107
104
  #
108
105
  # @return [String, nil]
109
106
  def from_locked_gems
@@ -142,15 +139,14 @@ module BetterAuth
142
139
  end
143
140
  end
144
141
 
145
- @project_id_cache = nil
142
+ @project_id_cache = {}
146
143
  @project_id_mutex = Mutex.new
147
144
 
148
145
  # Resolve a stable, anonymous project id for telemetry.
149
146
  #
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.
147
+ # The id is memoized by normalized `(base_url, project_name)` input
148
+ # so multi-app Ruby processes do not collapse every auth instance
149
+ # into the first derived anonymous id.
154
150
  #
155
151
  # ## Derivation chain (Requirements 14.2 – 14.5)
156
152
  #
@@ -163,22 +159,26 @@ module BetterAuth
163
159
  # 4. Otherwise: a random 32-character `[a-zA-Z0-9]` id from
164
160
  # `SecureRandom`, matching upstream `generateId(32)`.
165
161
  #
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.
162
+ # The Bundler probe inside {ProjectId.resolve_project_name} never
163
+ # raises out of this method; a failed probe collapses to "no project
164
+ # name" and the chain continues at rule 3 or rule 4.
170
165
  #
171
166
  # @param base_url [String, nil] the host's configured base URL.
172
167
  # @return [String] the memoized anonymous project id.
173
168
  def self.project_id(base_url)
174
- cached = @project_id_cache
169
+ url = normalize_base_url(base_url)
170
+ name = ProjectId.resolve_project_name
171
+ name = nil if name.is_a?(String) && name.empty?
172
+ cache_key = [url, name]
173
+
174
+ cached = @project_id_cache[cache_key]
175
175
  return cached if cached
176
176
 
177
177
  @project_id_mutex.synchronize do
178
- cached = @project_id_cache
178
+ cached = @project_id_cache[cache_key]
179
179
  return cached if cached
180
180
 
181
- @project_id_cache = derive_project_id(base_url)
181
+ @project_id_cache[cache_key] = derive_project_id(url, name)
182
182
  end
183
183
  end
184
184
 
@@ -192,19 +192,13 @@ module BetterAuth
192
192
  # @return [nil]
193
193
  def self.reset_project_id!
194
194
  @project_id_mutex.synchronize do
195
- @project_id_cache = nil
195
+ @project_id_cache = {}
196
196
  end
197
197
  nil
198
198
  end
199
199
 
200
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
-
201
+ def self.derive_project_id(url, name)
208
202
  if name && url
209
203
  hash_to_base64(url + name)
210
204
  elsif name
@@ -216,6 +210,12 @@ module BetterAuth
216
210
  end
217
211
  end
218
212
 
213
+ # @api private
214
+ def self.normalize_base_url(base_url)
215
+ url = base_url.is_a?(String) ? base_url : nil
216
+ (url && url.empty?) ? nil : url
217
+ end
218
+
219
219
  # @api private
220
220
  def self.hash_to_base64(input)
221
221
  Base64.strict_encode64(Digest::SHA256.digest(input.to_s))
@@ -31,9 +31,9 @@ module BetterAuth
31
31
  # never escape `#publish`.
32
32
  #
33
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.
34
+ # `anonymous_id`: delivery mode is owned by the supplied `track`
35
+ # lambda. The HTTP track implementation enqueues into a bounded
36
+ # background dispatcher.
37
37
  #
38
38
  # @example wiring with a `RecordingTrack` (test seam)
39
39
  # recorder = BetterAuth::Telemetry::Test::RecordingTrack.new
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Telemetry
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
@@ -45,6 +45,7 @@ require_relative "telemetry/logger_adapter"
45
45
  require_relative "telemetry/options"
46
46
  require_relative "telemetry/env"
47
47
  require_relative "telemetry/http_client"
48
+ require_relative "telemetry/http_dispatcher"
48
49
  require_relative "telemetry/project_id"
49
50
  require_relative "telemetry/publisher"
50
51
  require_relative "telemetry/create"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-telemetry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -103,20 +103,21 @@ files:
103
103
  - lib/better_auth/telemetry/detectors/system_info.rb
104
104
  - lib/better_auth/telemetry/env.rb
105
105
  - lib/better_auth/telemetry/http_client.rb
106
+ - lib/better_auth/telemetry/http_dispatcher.rb
106
107
  - lib/better_auth/telemetry/logger_adapter.rb
107
108
  - lib/better_auth/telemetry/noop_publisher.rb
108
109
  - lib/better_auth/telemetry/options.rb
109
110
  - lib/better_auth/telemetry/project_id.rb
110
111
  - lib/better_auth/telemetry/publisher.rb
111
112
  - lib/better_auth/telemetry/version.rb
112
- homepage: https://github.com/sebasxsala/better-auth
113
+ homepage: https://github.com/sebasxsala/better-auth-rb
113
114
  licenses:
114
115
  - MIT
115
116
  metadata:
116
- homepage_uri: https://github.com/sebasxsala/better-auth
117
- source_code_uri: https://github.com/sebasxsala/better-auth
118
- changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-telemetry/CHANGELOG.md
119
- bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
117
+ homepage_uri: https://github.com/sebasxsala/better-auth-rb
118
+ source_code_uri: https://github.com/sebasxsala/better-auth-rb
119
+ changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-telemetry/CHANGELOG.md
120
+ bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
120
121
  rdoc_options: []
121
122
  require_paths:
122
123
  - lib