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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +34 -10
- data/lib/better_auth/telemetry/create.rb +7 -9
- data/lib/better_auth/telemetry/detectors/auth_config.rb +27 -12
- data/lib/better_auth/telemetry/detectors/database.rb +46 -6
- data/lib/better_auth/telemetry/http_client.rb +12 -2
- data/lib/better_auth/telemetry/http_dispatcher.rb +84 -0
- data/lib/better_auth/telemetry/project_id.rb +30 -30
- data/lib/better_auth/telemetry/publisher.rb +3 -3
- data/lib/better_auth/telemetry/version.rb +1 -1
- data/lib/better_auth/telemetry.rb +1 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6795c847c337c646d429b2864c222edeadd7e5562e8bd475e6036da5795096d
|
|
4
|
+
data.tar.gz: 8892aaff5026e90b664a99793e77470d4aa6f8e4dfeef12cb2e4d67268938b8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99a180b67dfa37a42eb2870b36ac371bca99badcfd6c159687160296ed2e17bc0550950280041b237d45f867e4ee82d66cea7eb8e52618c3ad80938c1685aee4
|
|
7
|
+
data.tar.gz: aad2e4e3b2eea17b81bd1d34c6ff7dc4acec635b54f1d3e0a6e39574e79aca5f899ed5dde67e3994c8b63f844bd762a714eda89c03b98120133caccfd099c3e3
|
data/CHANGELOG.md
CHANGED
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"`,
|
|
75
|
-
even if it is otherwise opted in. Bypass
|
|
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
|
|
104
|
-
events to the endpoint via `Net::HTTP` with
|
|
105
|
-
HTTP telemetry is fire-and-forget, so
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
188
|
-
#
|
|
189
|
-
#
|
|
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
|
-
|
|
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:
|
|
339
|
-
additionalFields:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
474
|
+
additionalCookies: count_metadata(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :additional_cookies]))
|
|
468
475
|
},
|
|
469
476
|
database: {
|
|
470
|
-
generateId:
|
|
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:
|
|
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:
|
|
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`.
|
|
25
|
-
#
|
|
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`.
|
|
112
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
63
|
-
# Gemfile
|
|
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 ||
|
|
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
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
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 =
|
|
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
|
|
151
|
-
#
|
|
152
|
-
#
|
|
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
|
|
167
|
-
#
|
|
168
|
-
#
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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`:
|
|
35
|
-
#
|
|
36
|
-
#
|
|
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
|
|
@@ -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.
|
|
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
|