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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.md +20 -0
- data/README.md +202 -0
- data/lib/better_auth/plugins/telemetry.rb +11 -0
- data/lib/better_auth/telemetry/create.rb +293 -0
- data/lib/better_auth/telemetry/detectors/auth_config.rb +662 -0
- data/lib/better_auth/telemetry/detectors/database.rb +194 -0
- data/lib/better_auth/telemetry/detectors/environment.rb +86 -0
- data/lib/better_auth/telemetry/detectors/framework.rb +80 -0
- data/lib/better_auth/telemetry/detectors/project_info.rb +84 -0
- data/lib/better_auth/telemetry/detectors/runtime.rb +45 -0
- data/lib/better_auth/telemetry/detectors/system_info.rb +320 -0
- data/lib/better_auth/telemetry/env.rb +77 -0
- data/lib/better_auth/telemetry/http_client.rb +99 -0
- data/lib/better_auth/telemetry/logger_adapter.rb +118 -0
- data/lib/better_auth/telemetry/noop_publisher.rb +33 -0
- data/lib/better_auth/telemetry/options.rb +240 -0
- data/lib/better_auth/telemetry/project_id.rb +234 -0
- data/lib/better_auth/telemetry/publisher.rb +111 -0
- data/lib/better_auth/telemetry/version.rb +7 -0
- data/lib/better_auth/telemetry.rb +68 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7f696d840e8f9fb96751e211c5ad327bf255c3460b99c7adfccab3cbb9c10a1d
|
|
4
|
+
data.tar.gz: 3b513054876f4c07d8ecfd2bca1705a94b3406226c3270815b084fa5450429fe
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b17c3cebcaae59c8a9cc0d4b936ec2f793fa4004cadf6599aacdaa806b407e6fcd1631036d0039c1746f4dcb43641731a8221deb226eaf6e31fc6dfecb6b52c7
|
|
7
|
+
data.tar.gz: 437bd20b0f121eb0fa8d9f884545e2f0b1978a4c3f73c4bf6d1de9261d1f8d2cf3d410c188a03f1967f17d3c22c687315bcb97fd65300258a80b846ca42faaff
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- Initial release. Ports the upstream `@better-auth/telemetry` package
|
|
6
|
+
(vendored at `upstream/better-auth/1.6.9/packages/telemetry/`) into the
|
|
7
|
+
Ruby monorepo as the canonical `better_auth-telemetry` gem with a paired
|
|
8
|
+
`openauth-telemetry` alias.
|
|
9
|
+
- Opt-in only. Telemetry is disabled by default and skipped under
|
|
10
|
+
`RACK_ENV=test` / `RAILS_ENV=test` / `APP_ENV=test` unless
|
|
11
|
+
`context[:skip_test_check]` bypasses the gate.
|
|
12
|
+
- Supports both the `BETTER_AUTH_*` and `OPEN_AUTH_*` environment-variable
|
|
13
|
+
prefixes for `BETTER_AUTH_TELEMETRY`, `BETTER_AUTH_TELEMETRY_DEBUG`, and
|
|
14
|
+
`BETTER_AUTH_TELEMETRY_ENDPOINT` via `BetterAuth::Env.get`.
|
|
15
|
+
- HTTP delivery uses Ruby's standard library (`Net::HTTP`) with a 5-second
|
|
16
|
+
open + read timeout. No external HTTP-client gem is required at runtime.
|
|
17
|
+
- Soft-loaded by `BetterAuth::Auth#initialize`: when bundled, `auth.telemetry`
|
|
18
|
+
returns a publisher; when not bundled, it returns a noop publisher whose
|
|
19
|
+
`#publish` is a safe no-op.
|
|
20
|
+
- Mirrors upstream redaction rules and camelCase wire-format keys for
|
|
21
|
+
`payload.config`. Ruby-specific deviations (single Ruby implementation,
|
|
22
|
+
`runtime.engine` extra key, `cpuSpeed` omitted, `cpuModel` always `nil`,
|
|
23
|
+
`packageManager` reflects Bundler, framework/database probe lists,
|
|
24
|
+
`appName` not emitted) are documented in the README.
|
|
25
|
+
- No file under `upstream/better-auth/1.6.9/` is modified.
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2024 - present
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
6
|
+
the Software without restriction, including without limitation the rights to
|
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
8
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
16
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
19
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
20
|
+
DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# better_auth-telemetry
|
|
2
|
+
|
|
3
|
+
Opt-in telemetry package for Better Auth Ruby. Ports the upstream
|
|
4
|
+
`@better-auth/telemetry` package (vendored at
|
|
5
|
+
`upstream/better-auth/1.6.9/packages/telemetry/`) using only Ruby's standard
|
|
6
|
+
library.
|
|
7
|
+
|
|
8
|
+
Telemetry is **disabled by default**. The package never sends data unless an
|
|
9
|
+
operator explicitly opts in, and it is automatically skipped when the host
|
|
10
|
+
process is running under `RACK_ENV=test`, `RAILS_ENV=test`, or `APP_ENV=test`.
|
|
11
|
+
It is not configured through `plugins: [...]`; it is an optional gem that core
|
|
12
|
+
soft-loads when available.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add the gem:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "better_auth-telemetry"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When the gem is bundled, `BetterAuth::Auth#initialize` automatically wires
|
|
23
|
+
`auth.telemetry` to a publisher. When the gem is **not** bundled, `auth.telemetry`
|
|
24
|
+
is still safe to call: it returns a noop publisher whose `#publish` is a no-op.
|
|
25
|
+
Core's behavior is unchanged either way.
|
|
26
|
+
|
|
27
|
+
Require `better_auth/telemetry` only when using the telemetry API directly:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "better_auth/telemetry"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Opting in
|
|
34
|
+
|
|
35
|
+
Two equivalent ways to opt in. Either is sufficient on its own.
|
|
36
|
+
|
|
37
|
+
### Via options
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
auth = BetterAuth.auth(
|
|
41
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
42
|
+
database: :postgres,
|
|
43
|
+
telemetry: { enabled: true }
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
An explicit `telemetry: { enabled: false }` always wins over the env var:
|
|
48
|
+
setting `options[:telemetry][:enabled] = false` disables telemetry even when
|
|
49
|
+
`BETTER_AUTH_TELEMETRY=1` is set.
|
|
50
|
+
|
|
51
|
+
### Via environment variables
|
|
52
|
+
|
|
53
|
+
The package reads every variable through `BetterAuth::Env.get`, which honors
|
|
54
|
+
both the `BETTER_AUTH_*` and `OPEN_AUTH_*` prefixes. The `OPEN_AUTH_*` form
|
|
55
|
+
takes precedence over the `BETTER_AUTH_*` form when both are set.
|
|
56
|
+
|
|
57
|
+
| Purpose | `BETTER_AUTH_*` form | `OPEN_AUTH_*` form |
|
|
58
|
+
|--------------|--------------------------------|------------------------------|
|
|
59
|
+
| Opt in | `BETTER_AUTH_TELEMETRY` | `OPEN_AUTH_TELEMETRY` |
|
|
60
|
+
| Debug mode | `BETTER_AUTH_TELEMETRY_DEBUG` | `OPEN_AUTH_TELEMETRY_DEBUG` |
|
|
61
|
+
| Endpoint URL | `BETTER_AUTH_TELEMETRY_ENDPOINT` | `OPEN_AUTH_TELEMETRY_ENDPOINT` |
|
|
62
|
+
|
|
63
|
+
A value is treated as truthy when it is non-empty, not equal to `"0"`, and not
|
|
64
|
+
equal to (case-insensitive) `"false"`. Unset and empty are both treated as
|
|
65
|
+
absent. No other telemetry environment variables are recognized.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export BETTER_AUTH_TELEMETRY=1
|
|
69
|
+
export BETTER_AUTH_TELEMETRY_ENDPOINT=https://telemetry.example.com/ingest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Test environment skip
|
|
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
|
|
76
|
+
`context[:skip_test_check] = true`. `skip_test_check` only bypasses the test
|
|
77
|
+
gate; it does not opt telemetry in on its own.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
BetterAuth::Telemetry.create(
|
|
81
|
+
options,
|
|
82
|
+
{ skip_test_check: true } # opt-in still required via options or env
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Debug mode
|
|
87
|
+
|
|
88
|
+
When debug mode is on (`options[:telemetry][:debug] = true` or
|
|
89
|
+
`BETTER_AUTH_TELEMETRY_DEBUG` set to a truthy value), every event is logged via
|
|
90
|
+
the configured logger using `logger.info(JSON.pretty_generate(event))` and
|
|
91
|
+
**no HTTP request is made**. This is the recommended mode for inspecting what
|
|
92
|
+
the package would send before pointing it at a real endpoint.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
auth = BetterAuth.auth(
|
|
96
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
97
|
+
database: :postgres,
|
|
98
|
+
telemetry: { enabled: true, debug: true }
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
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.
|
|
108
|
+
|
|
109
|
+
## The `custom_track` injection seam
|
|
110
|
+
|
|
111
|
+
`context[:custom_track]` is a callable (typically a `Proc` or lambda) that
|
|
112
|
+
receives every event in lieu of HTTP delivery. It is the testing seam used by
|
|
113
|
+
the gem's own test suite, and it is also useful in production to forward
|
|
114
|
+
events to an in-process queue, an audit log, or a custom collector.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
recorder = []
|
|
118
|
+
custom_track = ->(event) { recorder << event }
|
|
119
|
+
|
|
120
|
+
publisher = BetterAuth::Telemetry.create(
|
|
121
|
+
{ secret: "x", database: :memory, telemetry: { enabled: true } },
|
|
122
|
+
{ custom_track: custom_track, skip_test_check: true }
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
publisher.publish(type: "ping", payload: { hello: "world" })
|
|
126
|
+
|
|
127
|
+
# recorder now contains the init event plus { type: "ping", payload: { hello: "world" }, anonymousId: "..." }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If `custom_track` raises, the exception is rescued, logged at error level, and
|
|
131
|
+
swallowed; `#publish` always returns `nil`. The `anonymousId` on every event
|
|
132
|
+
emitted by a single publisher is the same string, derived from
|
|
133
|
+
`BetterAuth::Telemetry.project_id(base_url)`.
|
|
134
|
+
|
|
135
|
+
The package accepts both snake_case and camelCase keys on the context for
|
|
136
|
+
parity with callers mirroring upstream type definitions: `:custom_track` and
|
|
137
|
+
`:customTrack` are equivalent, as are `:skip_test_check` and `:skipTestCheck`.
|
|
138
|
+
|
|
139
|
+
## Differences from upstream
|
|
140
|
+
|
|
141
|
+
The upstream `@better-auth/telemetry` package targets multiple JavaScript
|
|
142
|
+
runtimes (Node, Bun, Deno, edge) and ships two build entrypoints. This Ruby
|
|
143
|
+
port collapses both upstream variants into a single server-side Ruby
|
|
144
|
+
implementation and adapts every detector to idiomatic Ruby. The wire format
|
|
145
|
+
preserves upstream camelCase keys and redaction rules so existing telemetry
|
|
146
|
+
consumers can ingest events from Ruby projects without schema branching.
|
|
147
|
+
|
|
148
|
+
The intentional Ruby-specific deviations are:
|
|
149
|
+
|
|
150
|
+
- **Single Ruby implementation.** No Node, Bun, Deno, or edge runtime
|
|
151
|
+
branches. Detectors do not probe for `npm_config_user_agent`, do not walk
|
|
152
|
+
`node_modules`, and do not classify against JavaScript-only runtimes.
|
|
153
|
+
- **`runtime.engine` extra key.** The runtime payload includes an `:engine`
|
|
154
|
+
key (`"ruby"`, `"jruby"`, `"truffleruby"`) sourced from `RUBY_ENGINE` so
|
|
155
|
+
consumers can distinguish Ruby implementations. Upstream emits only `name`
|
|
156
|
+
and `version`.
|
|
157
|
+
- **`cpuSpeed` omitted.** Upstream's `systemInfo.cpuSpeed` field is not
|
|
158
|
+
emitted at all on the Ruby side. There is no portable Ruby standard-library
|
|
159
|
+
API for CPU speed, and emitting `nil` would invite consumers to assume the
|
|
160
|
+
field can ever be populated.
|
|
161
|
+
- **`cpuModel` always `nil`.** The `systemInfo.cpuModel` key is present in the
|
|
162
|
+
payload (so the schema matches upstream) but is always `nil`. Ruby has no
|
|
163
|
+
portable standard-library API for the CPU model string.
|
|
164
|
+
- **`packageManager` reflects Bundler, not npm.** When Bundler is loadable
|
|
165
|
+
and a Gemfile is locatable, `payload.packageManager` is
|
|
166
|
+
`{ name: "bundler", version: Bundler::VERSION }`. Otherwise the field is
|
|
167
|
+
`nil`. Upstream's `npm_config_user_agent` parsing has no Ruby analogue.
|
|
168
|
+
- **Framework probe list is Ruby-specific.** The framework detector inspects
|
|
169
|
+
`Gem.loaded_specs` for `rails`, `sinatra`, `hanami`, `hanami-router`,
|
|
170
|
+
`roda`, `grape`, `rack` (in that order). Node-only frameworks (`next`,
|
|
171
|
+
`nuxt`, `astro`, `sveltekit`, `solid-start`, `tanstack-start`, `hono`,
|
|
172
|
+
`express`, `elysia`, `expo`) are intentionally not probed.
|
|
173
|
+
- **Database probe list is Ruby-specific.** The database detector falls back
|
|
174
|
+
to `Gem.loaded_specs` for `sequel`, `pg`, `mysql2`, `sqlite3`,
|
|
175
|
+
`activerecord`, `mongoid`, `mongo`, `rom-sql` (in that order) when no
|
|
176
|
+
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
|
|
179
|
+
external HTTP-client gem is required at runtime, and HTTP delivery does not
|
|
180
|
+
block `BetterAuth.auth` construction.
|
|
181
|
+
- **Explicit false is a strong opt-out.** `telemetry: { enabled: false }`
|
|
182
|
+
disables telemetry even when `BETTER_AUTH_TELEMETRY` or `OPEN_AUTH_TELEMETRY`
|
|
183
|
+
is truthy. This is intentionally stricter than upstream so application
|
|
184
|
+
configuration can override process-wide env vars.
|
|
185
|
+
- **snake_case canonical context keys, with camelCase synonyms accepted.**
|
|
186
|
+
The Ruby-canonical context keys are `:custom_track`, `:database`,
|
|
187
|
+
`:adapter`, `:skip_test_check`. The package also accepts the camelCase
|
|
188
|
+
variants (`:customTrack`, `:skipTestCheck`) for parity with callers
|
|
189
|
+
mirroring upstream type definitions.
|
|
190
|
+
- **`appName` is not emitted.** The `app_name` value is used internally by
|
|
191
|
+
`BetterAuth::Telemetry.project_id` to derive the `anonymousId` but is
|
|
192
|
+
intentionally not emitted as a payload field, since it can be
|
|
193
|
+
user-identifying.
|
|
194
|
+
- **Public `BetterAuth::Telemetry.reset_project_id!` testing helper.** A
|
|
195
|
+
module-level helper is exposed for resetting the memoized
|
|
196
|
+
`anonymous_id` between tests. It has no effect on production behavior and
|
|
197
|
+
exists solely so test suites can assert deterministic project_id derivation
|
|
198
|
+
across opt-in / opt-out cycles.
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Soft-load probe shim for `better_auth-telemetry`.
|
|
4
|
+
#
|
|
5
|
+
# The core package soft-loads `require "better_auth/telemetry"` when building
|
|
6
|
+
# `auth.telemetry`. This shim keeps the plugin-style path loadable for callers
|
|
7
|
+
# that still require it directly, then delegates to the canonical public entry
|
|
8
|
+
# point.
|
|
9
|
+
#
|
|
10
|
+
# Implements Requirements 16.1 and 16.2.
|
|
11
|
+
require "better_auth/telemetry"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "env"
|
|
6
|
+
require_relative "http_client"
|
|
7
|
+
require_relative "noop_publisher"
|
|
8
|
+
require_relative "options"
|
|
9
|
+
require_relative "project_id"
|
|
10
|
+
require_relative "publisher"
|
|
11
|
+
|
|
12
|
+
require_relative "detectors/auth_config"
|
|
13
|
+
require_relative "detectors/database"
|
|
14
|
+
require_relative "detectors/environment"
|
|
15
|
+
require_relative "detectors/framework"
|
|
16
|
+
require_relative "detectors/project_info"
|
|
17
|
+
require_relative "detectors/runtime"
|
|
18
|
+
require_relative "detectors/system_info"
|
|
19
|
+
|
|
20
|
+
module BetterAuth
|
|
21
|
+
module Telemetry
|
|
22
|
+
# Process-environment variables that mark the host as running inside a
|
|
23
|
+
# test suite. Mirrors {BetterAuth::Configuration#test_environment?}
|
|
24
|
+
# without taking a hard dependency on a `Configuration` instance — the
|
|
25
|
+
# `create` entry point also accepts raw hashes and `nil`.
|
|
26
|
+
TEST_ENV_VARS = %w[RACK_ENV RAILS_ENV APP_ENV].freeze
|
|
27
|
+
|
|
28
|
+
# Public entry point used by `BetterAuth::Auth#initialize` (and by
|
|
29
|
+
# tests that exercise the publisher in isolation) to build a
|
|
30
|
+
# publisher tailored to the host's opt-in state.
|
|
31
|
+
#
|
|
32
|
+
# ## Pipeline
|
|
33
|
+
#
|
|
34
|
+
# 1. Normalize the heterogeneous `options` and `context` arguments
|
|
35
|
+
# into {NormalizedOptions} / {NormalizedContext} value objects so
|
|
36
|
+
# the rest of the pipeline does not have to repeatedly do
|
|
37
|
+
# snake/camelCase key lookups.
|
|
38
|
+
# 2. Resolve `endpoint = Env.get("BETTER_AUTH_TELEMETRY_ENDPOINT")`,
|
|
39
|
+
# honoring the `OPEN_AUTH_*` alias prefix.
|
|
40
|
+
# 3. **Short-circuit**: when both the endpoint and `custom_track`
|
|
41
|
+
# are absent there is no delivery channel and the publisher
|
|
42
|
+
# cannot do useful work, so we hand back a {NoopPublisher} and
|
|
43
|
+
# bypass the rest of the pipeline (Requirement 5.1).
|
|
44
|
+
# 4. **Decision table** (Property 3 / Requirements 4.1–4.7):
|
|
45
|
+
# compute `enabled` from `(options_enabled, env_truthy,
|
|
46
|
+
# in_test_env, skip_test_check)` using
|
|
47
|
+
#
|
|
48
|
+
# opt_in = options_enabled == true || (options_enabled.nil? && env_truthy)
|
|
49
|
+
# overridden = options_enabled == false # explicit false beats env truthy
|
|
50
|
+
# in_test_gate = in_test_env && !skip_test_check
|
|
51
|
+
# enabled = opt_in && !overridden && !in_test_gate
|
|
52
|
+
#
|
|
53
|
+
# 5. When enabled, build the delivery `track` lambda via
|
|
54
|
+
# {.build_track}: `custom_track` wins, then debug-mode logging,
|
|
55
|
+
# then HTTP delivery (Requirements 5.2–5.4, 5.7, 5.9). Each
|
|
56
|
+
# branch is wrapped in a `rescue StandardError` that routes the
|
|
57
|
+
# failure through the configured logger (Requirements 21.1,
|
|
58
|
+
# 21.2) so a misbehaving sink never propagates out of the track
|
|
59
|
+
# callable.
|
|
60
|
+
# 6. **Compose and emit the init event** (Requirement 6): resolve a
|
|
61
|
+
# stable {.project_id} for the host (scoped to the
|
|
62
|
+
# {CurrentOptions.with_app_name} block so the `from_app_name`
|
|
63
|
+
# rule sees the configured `app_name`), invoke each detector
|
|
64
|
+
# inside {.safely} so a single misbehaving probe degrades to
|
|
65
|
+
# `nil` instead of aborting the init event, build the
|
|
66
|
+
# upstream-shaped `{type: "init", anonymousId:, payload: {...}}`
|
|
67
|
+
# event with camelCase keys, and fire it through the track
|
|
68
|
+
# lambda exactly once. Errors raised by the dispatch itself
|
|
69
|
+
# surface through the rescue inside the track lambda.
|
|
70
|
+
# 7. Return a fully-initialized {Publisher} that closes over the
|
|
71
|
+
# same `track` / `anonymous_id` / `enabled` state so subsequent
|
|
72
|
+
# `#publish` calls reuse the already-resolved id (Requirement
|
|
73
|
+
# 6.10).
|
|
74
|
+
#
|
|
75
|
+
# The method itself never raises: detectors are wrapped in
|
|
76
|
+
# {.safely}, the track lambda swallows transport failures, and the
|
|
77
|
+
# decision-layer logic is plain hash lookups and env reads.
|
|
78
|
+
#
|
|
79
|
+
# @param options [BetterAuth::Configuration, Hash, nil] the host's
|
|
80
|
+
# options. `nil` is equivalent to `{}`. When a `Hash`, both
|
|
81
|
+
# snake_case and camelCase keys are accepted.
|
|
82
|
+
# @param context [Hash, nil] optional caller-supplied context with
|
|
83
|
+
# `custom_track` / `database` / `adapter` / `skip_test_check`
|
|
84
|
+
# keys (snake_case or camelCase).
|
|
85
|
+
# @return [NoopPublisher, Publisher] a noop publisher when telemetry
|
|
86
|
+
# has no delivery channel or is disabled, otherwise a fully-formed
|
|
87
|
+
# {Publisher}.
|
|
88
|
+
def self.create(options, context = nil)
|
|
89
|
+
norm_opts = NormalizedOptions.from(options)
|
|
90
|
+
norm_ctx = NormalizedContext.from(context)
|
|
91
|
+
logger = norm_opts.logger
|
|
92
|
+
|
|
93
|
+
endpoint = Env.get("BETTER_AUTH_TELEMETRY_ENDPOINT")
|
|
94
|
+
|
|
95
|
+
# No delivery channel -> short-circuit to noop, regardless of opt-in.
|
|
96
|
+
return NoopPublisher.new if endpoint_absent?(endpoint) && norm_ctx.custom_track.nil?
|
|
97
|
+
|
|
98
|
+
enabled = compute_enabled(
|
|
99
|
+
options_enabled: norm_opts.telemetry_enabled,
|
|
100
|
+
env_truthy: Env.truthy?(Env.get("BETTER_AUTH_TELEMETRY")),
|
|
101
|
+
in_test_env: in_test_env?,
|
|
102
|
+
skip_test_check: norm_ctx.skip_test_check ? true : false
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return NoopPublisher.new unless enabled
|
|
106
|
+
|
|
107
|
+
track = build_track(
|
|
108
|
+
custom_track: norm_ctx.custom_track,
|
|
109
|
+
debug: debug_mode?(norm_opts),
|
|
110
|
+
endpoint: endpoint,
|
|
111
|
+
logger: logger
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Resolve the anonymous id under a `with_app_name` scope so the
|
|
115
|
+
# `from_app_name` rule in `ProjectId.resolve_project_name` reads
|
|
116
|
+
# the configured `app_name` even when the underlying
|
|
117
|
+
# `BetterAuth::Telemetry.project_id` cache is cold. Once cached
|
|
118
|
+
# the value is reused for the lifetime of the process; the scope
|
|
119
|
+
# only matters on the very first call.
|
|
120
|
+
anonymous_id = CurrentOptions.with_app_name(norm_opts.app_name) do
|
|
121
|
+
BetterAuth::Telemetry.project_id(norm_opts.base_url)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
init_event = compose_init_event(
|
|
125
|
+
options: options,
|
|
126
|
+
norm_ctx: norm_ctx,
|
|
127
|
+
anonymous_id: anonymous_id
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
track.call(init_event)
|
|
131
|
+
|
|
132
|
+
Publisher.new(
|
|
133
|
+
enabled: true,
|
|
134
|
+
anonymous_id: anonymous_id,
|
|
135
|
+
track: track,
|
|
136
|
+
base_url: norm_opts.base_url,
|
|
137
|
+
logger: logger
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Apply the Property 3 decision table.
|
|
142
|
+
#
|
|
143
|
+
# @api private
|
|
144
|
+
# @param options_enabled [Boolean, nil]
|
|
145
|
+
# @param env_truthy [Boolean]
|
|
146
|
+
# @param in_test_env [Boolean]
|
|
147
|
+
# @param skip_test_check [Boolean]
|
|
148
|
+
# @return [Boolean]
|
|
149
|
+
def self.compute_enabled(options_enabled:, env_truthy:, in_test_env:, skip_test_check:)
|
|
150
|
+
opt_in = options_enabled == true || (options_enabled.nil? && env_truthy)
|
|
151
|
+
overridden = options_enabled == false
|
|
152
|
+
in_test_gate = in_test_env && !skip_test_check
|
|
153
|
+
|
|
154
|
+
opt_in && !overridden && !in_test_gate
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @api private
|
|
158
|
+
def self.endpoint_absent?(endpoint)
|
|
159
|
+
endpoint.nil? || (endpoint.respond_to?(:empty?) && endpoint.empty?)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @api private
|
|
163
|
+
def self.in_test_env?
|
|
164
|
+
TEST_ENV_VARS.any? { |k| ENV[k] == "test" }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Decide whether debug mode is active. The option-layer flag wins
|
|
168
|
+
# when explicitly `true`; otherwise we defer to the env classifier
|
|
169
|
+
# via {Env.truthy?} on `BETTER_AUTH_TELEMETRY_DEBUG` (which honors
|
|
170
|
+
# the `OPEN_AUTH_*` alias prefix as well). Mirrors Requirement 5.4.
|
|
171
|
+
#
|
|
172
|
+
# @api private
|
|
173
|
+
# @param norm_opts [NormalizedOptions]
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
def self.debug_mode?(norm_opts)
|
|
176
|
+
norm_opts.telemetry_debug == true || Env.truthy?(Env.get("BETTER_AUTH_TELEMETRY_DEBUG"))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Build the delivery `track` lambda. Three branches, in priority
|
|
180
|
+
# order (Requirements 5.2 → 5.4):
|
|
181
|
+
#
|
|
182
|
+
# 1. `custom_track` present — invoke `custom_track.call(event)`.
|
|
183
|
+
# Primary testing seam and the only branch that runs without
|
|
184
|
+
# requiring `BETTER_AUTH_TELEMETRY_ENDPOINT` to be set.
|
|
185
|
+
# 2. Debug mode active — log the JSON-pretty event via
|
|
186
|
+
# `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.
|
|
190
|
+
#
|
|
191
|
+
# Every branch wraps its dispatch in a `rescue StandardError` that
|
|
192
|
+
# routes the failure through `logger.error(...)`, so callable /
|
|
193
|
+
# logger-encoding / HTTP failures never propagate out of the track
|
|
194
|
+
# lambda. The lambda always returns `nil`.
|
|
195
|
+
#
|
|
196
|
+
# @api private
|
|
197
|
+
# @param custom_track [#call, nil]
|
|
198
|
+
# @param debug [Boolean]
|
|
199
|
+
# @param endpoint [String, nil]
|
|
200
|
+
# @param logger [LoggerAdapter]
|
|
201
|
+
# @return [Proc] a one-arg lambda accepting a normalized event hash.
|
|
202
|
+
def self.build_track(custom_track:, debug:, endpoint:, logger:)
|
|
203
|
+
if custom_track
|
|
204
|
+
lambda do |event|
|
|
205
|
+
custom_track.call(event)
|
|
206
|
+
nil
|
|
207
|
+
rescue => e
|
|
208
|
+
logger.error("[better-auth.telemetry] custom_track failed: #{e.class}: #{e.message}")
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
elsif debug
|
|
212
|
+
lambda do |event|
|
|
213
|
+
logger.info(JSON.pretty_generate(event))
|
|
214
|
+
nil
|
|
215
|
+
rescue => e
|
|
216
|
+
logger.error("[better-auth.telemetry] debug log failed: #{e.class}: #{e.message}")
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
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
|
|
226
|
+
nil
|
|
227
|
+
rescue => e
|
|
228
|
+
logger.error("[better-auth.telemetry] http dispatch failed: #{e.class}: #{e.message}")
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Compose the init event hash emitted at create time.
|
|
235
|
+
#
|
|
236
|
+
# Each detector is invoked through {.safely} so a single failing
|
|
237
|
+
# probe degrades that field to `nil` rather than aborting the
|
|
238
|
+
# whole event composition (Requirement 6.4 / 9.11). The output
|
|
239
|
+
# matches the upstream wire shape: top-level `type`,
|
|
240
|
+
# `anonymousId`, and a `payload` hash with the seven camelCase
|
|
241
|
+
# keys `config`, `runtime`, `database`, `framework`,
|
|
242
|
+
# `environment`, `systemInfo`, `packageManager`
|
|
243
|
+
# (Requirements 6.1, 6.3).
|
|
244
|
+
#
|
|
245
|
+
# `AuthConfig.call` and `Database.call` are passed the original
|
|
246
|
+
# `options` argument (not the {NormalizedOptions} wrapper) because
|
|
247
|
+
# both detectors transparently accept either a
|
|
248
|
+
# {BetterAuth::Configuration} or a raw hash; the normalized view
|
|
249
|
+
# is only consumed by the decision/track-building layer.
|
|
250
|
+
#
|
|
251
|
+
# @api private
|
|
252
|
+
# @param options [BetterAuth::Configuration, Hash, nil]
|
|
253
|
+
# @param norm_ctx [NormalizedContext]
|
|
254
|
+
# @param anonymous_id [String]
|
|
255
|
+
# @return [Hash{Symbol => Object}]
|
|
256
|
+
def self.compose_init_event(options:, norm_ctx:, anonymous_id:)
|
|
257
|
+
payload = {
|
|
258
|
+
config: safely { Detectors::AuthConfig.call(options, norm_ctx) },
|
|
259
|
+
runtime: safely { Detectors::Runtime.call },
|
|
260
|
+
database: safely { Detectors::Database.call(options, norm_ctx) },
|
|
261
|
+
framework: safely { Detectors::Framework.call },
|
|
262
|
+
environment: safely { Detectors::Environment.call },
|
|
263
|
+
systemInfo: safely { Detectors::SystemInfo.call },
|
|
264
|
+
packageManager: safely { Detectors::ProjectInfo.call }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
{
|
|
268
|
+
type: "init",
|
|
269
|
+
anonymousId: anonymous_id,
|
|
270
|
+
payload: payload
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Run `block` and rescue any `StandardError` to `nil`. Used to
|
|
275
|
+
# bound each detector invocation in {.compose_init_event} so a
|
|
276
|
+
# raising probe degrades only that field rather than aborting the
|
|
277
|
+
# whole init event.
|
|
278
|
+
#
|
|
279
|
+
# Non-`StandardError` exceptions (`Interrupt`, `SystemExit`,
|
|
280
|
+
# `SignalException`, `NoMemoryError`) are intentionally allowed to
|
|
281
|
+
# propagate.
|
|
282
|
+
#
|
|
283
|
+
# @api private
|
|
284
|
+
# @yield the probe to run.
|
|
285
|
+
# @return [Object, nil] whatever the block returns, or `nil` if
|
|
286
|
+
# the block raised a `StandardError`.
|
|
287
|
+
def self.safely
|
|
288
|
+
yield
|
|
289
|
+
rescue
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|