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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Telemetry
|
|
7
|
+
module Detectors
|
|
8
|
+
# Database detector. Returns a small hash describing the database
|
|
9
|
+
# backend the host application is using (or `nil` when no signal
|
|
10
|
+
# is available).
|
|
11
|
+
#
|
|
12
|
+
# This is the Ruby-specific replacement for upstream's
|
|
13
|
+
# `detect-database.ts`, which only walked the Node `package.json`
|
|
14
|
+
# for known SQL/ORM packages. The Ruby port adds two earlier
|
|
15
|
+
# precedence rules (a caller-supplied context override and a
|
|
16
|
+
# `BetterAuth::Configuration` adapter check) so an application
|
|
17
|
+
# with a configured Better Auth adapter does not fall through to
|
|
18
|
+
# the generic gem fallback.
|
|
19
|
+
#
|
|
20
|
+
# ## Precedence chain (Requirement 10)
|
|
21
|
+
#
|
|
22
|
+
# 1. **Context override** — when the caller supplied a non-empty
|
|
23
|
+
# `context.database` string, return it verbatim with
|
|
24
|
+
# `version: nil`. This is the upstream `context.database`
|
|
25
|
+
# seam.
|
|
26
|
+
# 2. **Configuration adapter** — when `options` is a
|
|
27
|
+
# {BetterAuth::Configuration} (or a hash with a `:database`
|
|
28
|
+
# key) and the value is a known adapter symbol
|
|
29
|
+
# ({ADAPTER_SYMBOLS}) or a `BetterAuth::Adapters::*` instance
|
|
30
|
+
# ({ADAPTER_CLASS_MAP}), return its short identifier with
|
|
31
|
+
# `version: nil`.
|
|
32
|
+
# 3. **Gem fallback** — when neither rule above matches, walk
|
|
33
|
+
# `Gem.loaded_specs` in {GEM_FALLBACKS} order and return the
|
|
34
|
+
# first match as `{name: <gem_name>, version: <spec.version.to_s>}`.
|
|
35
|
+
# 4. Otherwise — `nil`.
|
|
36
|
+
#
|
|
37
|
+
# ## Failure handling
|
|
38
|
+
#
|
|
39
|
+
# The whole call is wrapped in `rescue StandardError; nil` so a
|
|
40
|
+
# surprise from any branch (an exotic `context` shape, a
|
|
41
|
+
# `Configuration` reader that raises on a partially constructed
|
|
42
|
+
# instance, a `Gem.loaded_specs` mutation, …) degrades to `nil`
|
|
43
|
+
# rather than escaping out of the init payload composition in
|
|
44
|
+
# {BetterAuth::Telemetry.create}.
|
|
45
|
+
#
|
|
46
|
+
# @example Context override
|
|
47
|
+
# ctx = BetterAuth::Telemetry::NormalizedContext.from(database: "postgresql")
|
|
48
|
+
# BetterAuth::Telemetry::Detectors::Database.call(nil, ctx)
|
|
49
|
+
# # => {name: "postgresql", version: nil}
|
|
50
|
+
#
|
|
51
|
+
# @example Configuration symbol
|
|
52
|
+
# config = BetterAuth::Configuration.new(secret: "...", database: :memory)
|
|
53
|
+
# BetterAuth::Telemetry::Detectors::Database.call(config, nil)
|
|
54
|
+
# # => {name: "memory", version: nil}
|
|
55
|
+
module Database
|
|
56
|
+
# Map from `BetterAuth::Adapters::*` class name to the short
|
|
57
|
+
# identifier reported in the init event. Class names are
|
|
58
|
+
# matched as strings so loading the telemetry gem does not
|
|
59
|
+
# autoload every adapter constant.
|
|
60
|
+
ADAPTER_CLASS_MAP = {
|
|
61
|
+
"BetterAuth::Adapters::Postgres" => "postgres",
|
|
62
|
+
"BetterAuth::Adapters::MySQL" => "mysql",
|
|
63
|
+
"BetterAuth::Adapters::SQLite" => "sqlite",
|
|
64
|
+
"BetterAuth::Adapters::MSSQL" => "mssql",
|
|
65
|
+
"BetterAuth::Adapters::Memory" => "memory"
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
# Map from a known {BetterAuth::Configuration#database} symbol
|
|
69
|
+
# value to the short identifier reported in the init event.
|
|
70
|
+
ADAPTER_SYMBOLS = {
|
|
71
|
+
postgres: "postgres",
|
|
72
|
+
mysql: "mysql",
|
|
73
|
+
sqlite: "sqlite",
|
|
74
|
+
mssql: "mssql",
|
|
75
|
+
memory: "memory"
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
# Gems to probe in `Gem.loaded_specs`, in upstream-spec order.
|
|
79
|
+
# First match wins.
|
|
80
|
+
GEM_FALLBACKS = %w[sequel pg mysql2 sqlite3 activerecord mongoid mongo rom-sql].freeze
|
|
81
|
+
|
|
82
|
+
module_function
|
|
83
|
+
|
|
84
|
+
# Resolve the database signal for the host application.
|
|
85
|
+
#
|
|
86
|
+
# @param options [BetterAuth::Configuration, Hash, nil] the
|
|
87
|
+
# options passed to {BetterAuth::Telemetry.create}. May be a
|
|
88
|
+
# {BetterAuth::Configuration} (production path), a raw hash
|
|
89
|
+
# with a `:database` key, or `nil`.
|
|
90
|
+
# @param context [BetterAuth::Telemetry::NormalizedContext, Hash, nil]
|
|
91
|
+
# the optional context. When it responds to `:database` (or
|
|
92
|
+
# carries a `:database` / `"database"` key) and the value is
|
|
93
|
+
# a non-empty string, that string short-circuits the chain.
|
|
94
|
+
# @return [Hash{Symbol => String, nil}, nil] either
|
|
95
|
+
# `{name: String, version: String|nil}` or `nil` when nothing
|
|
96
|
+
# matches.
|
|
97
|
+
def call(options, context)
|
|
98
|
+
override = context_override(context)
|
|
99
|
+
return {name: override, version: nil} if override
|
|
100
|
+
|
|
101
|
+
identifier = identify_from_options(options)
|
|
102
|
+
return {name: identifier, version: nil} if identifier
|
|
103
|
+
|
|
104
|
+
detect_from_gems
|
|
105
|
+
rescue
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Read `database` from a {NormalizedContext}-like or hash-like
|
|
110
|
+
# 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
|
+
#
|
|
114
|
+
# @param context [#database, Hash, nil]
|
|
115
|
+
# @return [String, nil]
|
|
116
|
+
def context_override(context)
|
|
117
|
+
return nil if context.nil?
|
|
118
|
+
|
|
119
|
+
value =
|
|
120
|
+
if context.respond_to?(:database)
|
|
121
|
+
context.database
|
|
122
|
+
elsif context.respond_to?(:[])
|
|
123
|
+
context[:database] || context["database"]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
return nil unless value.is_a?(String)
|
|
127
|
+
return nil if value.empty?
|
|
128
|
+
|
|
129
|
+
value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Translate the configuration's `database` value into a short
|
|
133
|
+
# identifier when it matches a known adapter symbol or a known
|
|
134
|
+
# `BetterAuth::Adapters::*` class.
|
|
135
|
+
#
|
|
136
|
+
# @param options [BetterAuth::Configuration, Hash, nil]
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
def identify_from_options(options)
|
|
139
|
+
database = configuration_database(options)
|
|
140
|
+
return nil if database.nil?
|
|
141
|
+
|
|
142
|
+
identify_adapter(database)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Read `database` from a {BetterAuth::Configuration} or a raw
|
|
146
|
+
# hash. Returns `nil` for any other input shape.
|
|
147
|
+
#
|
|
148
|
+
# @param options [BetterAuth::Configuration, Hash, nil]
|
|
149
|
+
# @return [Object, nil]
|
|
150
|
+
def configuration_database(options)
|
|
151
|
+
return nil if options.nil?
|
|
152
|
+
|
|
153
|
+
if defined?(::BetterAuth::Configuration) && options.is_a?(::BetterAuth::Configuration)
|
|
154
|
+
return options.database
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
return options[:database] || options["database"] if options.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Map a known adapter symbol or a `BetterAuth::Adapters::*`
|
|
163
|
+
# instance to its short identifier. Returns `nil` when the
|
|
164
|
+
# value is neither a known symbol nor a known adapter class.
|
|
165
|
+
#
|
|
166
|
+
# @param value [Symbol, BetterAuth::Adapters::Base, Object]
|
|
167
|
+
# @return [String, nil]
|
|
168
|
+
def identify_adapter(value)
|
|
169
|
+
if value.is_a?(Symbol)
|
|
170
|
+
return ADAPTER_SYMBOLS[value]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
ADAPTER_CLASS_MAP[value.class.name]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Walk {GEM_FALLBACKS} in order and return the first
|
|
177
|
+
# `Gem.loaded_specs` match as `{name:, version:}`. Returns
|
|
178
|
+
# `nil` when no listed gem is loaded.
|
|
179
|
+
#
|
|
180
|
+
# @return [Hash{Symbol => String}, nil]
|
|
181
|
+
def detect_from_gems
|
|
182
|
+
GEM_FALLBACKS.each do |name|
|
|
183
|
+
spec = ::Gem.loaded_specs[name]
|
|
184
|
+
next if spec.nil?
|
|
185
|
+
|
|
186
|
+
version = spec.respond_to?(:version) ? spec.version : nil
|
|
187
|
+
return {name: name, version: version&.to_s}
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Telemetry
|
|
5
|
+
module Detectors
|
|
6
|
+
# Environment detector. Classifies the current process as
|
|
7
|
+
# `"production"`, `"ci"`, `"test"`, or `"development"`, mirroring
|
|
8
|
+
# the upstream `detect-runtime.ts:detectEnvironment` short-circuit
|
|
9
|
+
# chain.
|
|
10
|
+
#
|
|
11
|
+
# Precedence (top wins):
|
|
12
|
+
#
|
|
13
|
+
# 1. `"production"` — when any of `RACK_ENV`, `RAILS_ENV`,
|
|
14
|
+
# `APP_ENV` equals the literal string `"production"`.
|
|
15
|
+
# 2. `"ci"` — when any of the documented CI marker variables
|
|
16
|
+
# ({CI_VARS}) is set to a non-empty value that is not the
|
|
17
|
+
# case-insensitive string `"false"`.
|
|
18
|
+
# 3. `"test"` — when any of `RACK_ENV`, `RAILS_ENV`, `APP_ENV`
|
|
19
|
+
# equals the literal string `"test"`.
|
|
20
|
+
# 4. `"development"` — fallback when no rule above matches.
|
|
21
|
+
#
|
|
22
|
+
# The CI marker check intentionally skips the literal string
|
|
23
|
+
# `"false"` (case-insensitive) so a host that exports
|
|
24
|
+
# `CI=false` (a common pattern in non-CI shells where CI tooling
|
|
25
|
+
# has been opted out) is not misclassified. Empty values are also
|
|
26
|
+
# treated as unset.
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# BetterAuth::Telemetry::Detectors::Environment.call
|
|
30
|
+
# # => "development"
|
|
31
|
+
module Environment
|
|
32
|
+
# CI marker variables, in the upstream-defined order. Any
|
|
33
|
+
# non-empty / non-`"false"` value flips the classifier to
|
|
34
|
+
# `"ci"`.
|
|
35
|
+
CI_VARS = %w[
|
|
36
|
+
CI
|
|
37
|
+
BUILD_ID
|
|
38
|
+
BUILD_NUMBER
|
|
39
|
+
CI_APP_ID
|
|
40
|
+
CI_BUILD_ID
|
|
41
|
+
CI_BUILD_NUMBER
|
|
42
|
+
CI_NAME
|
|
43
|
+
CONTINUOUS_INTEGRATION
|
|
44
|
+
RUN_ID
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
# Test/production env variable names that get inspected for the
|
|
48
|
+
# literal `"production"` and `"test"` strings.
|
|
49
|
+
TEST_VARS = %w[RACK_ENV RAILS_ENV APP_ENV].freeze
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# @return [String] one of `"production"`, `"ci"`, `"test"`, or
|
|
54
|
+
# `"development"`.
|
|
55
|
+
def call
|
|
56
|
+
return "production" if any_env_eq?(TEST_VARS, "production")
|
|
57
|
+
return "ci" if ci?
|
|
58
|
+
return "test" if any_env_eq?(TEST_VARS, "test")
|
|
59
|
+
|
|
60
|
+
"development"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Boolean] true when at least one CI marker variable
|
|
64
|
+
# has a non-empty value that is not (case-insensitive)
|
|
65
|
+
# `"false"`.
|
|
66
|
+
def ci?
|
|
67
|
+
CI_VARS.any? do |key|
|
|
68
|
+
value = ENV[key]
|
|
69
|
+
next false if value.nil? || value.empty?
|
|
70
|
+
next false if value.casecmp("false").zero?
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param keys [Array<String>] env var names to inspect.
|
|
77
|
+
# @param value [String] expected exact value.
|
|
78
|
+
# @return [Boolean] true when at least one of the named vars
|
|
79
|
+
# equals `value`.
|
|
80
|
+
def any_env_eq?(keys, value)
|
|
81
|
+
keys.any? { |k| ENV[k] == value }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Telemetry
|
|
7
|
+
module Detectors
|
|
8
|
+
# Framework detector. Returns a small hash describing the Ruby
|
|
9
|
+
# web framework hosting the application (or `nil` when no
|
|
10
|
+
# supported framework gem is loaded).
|
|
11
|
+
#
|
|
12
|
+
# This is the Ruby-specific replacement for upstream's
|
|
13
|
+
# `detect-framework.ts`, which walked the Node `package.json`
|
|
14
|
+
# for known JavaScript frameworks. The Ruby port instead probes
|
|
15
|
+
# `Gem.loaded_specs` for the canonical Ruby web framework gems
|
|
16
|
+
# in declaration order; the first hit wins.
|
|
17
|
+
#
|
|
18
|
+
# ## Probe order (Requirement 11.1)
|
|
19
|
+
#
|
|
20
|
+
# 1. `rails`
|
|
21
|
+
# 2. `sinatra`
|
|
22
|
+
# 3. `hanami`
|
|
23
|
+
# 4. `hanami-router`
|
|
24
|
+
# 5. `roda`
|
|
25
|
+
# 6. `grape`
|
|
26
|
+
# 7. `rack`
|
|
27
|
+
#
|
|
28
|
+
# `rack` is intentionally last so a Rails or Sinatra app does
|
|
29
|
+
# not get reported as a "rack" app just because Rack is a
|
|
30
|
+
# transitive dependency.
|
|
31
|
+
#
|
|
32
|
+
# ## Failure handling
|
|
33
|
+
#
|
|
34
|
+
# The whole call is wrapped in `rescue StandardError; nil` so a
|
|
35
|
+
# surprise from `Gem.loaded_specs` (e.g. a mutated registry, a
|
|
36
|
+
# `respond_to?(:version)` shim that raises) degrades to `nil`
|
|
37
|
+
# rather than escaping out of the init payload composition in
|
|
38
|
+
# {BetterAuth::Telemetry.create}.
|
|
39
|
+
#
|
|
40
|
+
# Node-only frameworks (`next`, `nuxt`, `astro`, `sveltekit`,
|
|
41
|
+
# `solid-start`, `tanstack-start`, `hono`, `express`, `elysia`,
|
|
42
|
+
# `expo`) are intentionally not probed (Requirement 11.4).
|
|
43
|
+
#
|
|
44
|
+
# @example Rails app
|
|
45
|
+
# BetterAuth::Telemetry::Detectors::Framework.call
|
|
46
|
+
# # => {name: "rails", version: "7.1.3"}
|
|
47
|
+
#
|
|
48
|
+
# @example No supported framework loaded
|
|
49
|
+
# BetterAuth::Telemetry::Detectors::Framework.call
|
|
50
|
+
# # => nil
|
|
51
|
+
module Framework
|
|
52
|
+
# Gems to probe in `Gem.loaded_specs`, in upstream/spec order.
|
|
53
|
+
# First match wins.
|
|
54
|
+
GEMS = %w[rails sinatra hanami hanami-router roda grape rack].freeze
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
# Resolve the framework signal for the host application by
|
|
59
|
+
# walking {GEMS} in order against `Gem.loaded_specs`.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash{Symbol => String}, nil] either
|
|
62
|
+
# `{name: String, version: String}` for the first matching
|
|
63
|
+
# gem, or `nil` when none of the supported framework gems
|
|
64
|
+
# are loaded.
|
|
65
|
+
def call
|
|
66
|
+
GEMS.each do |name|
|
|
67
|
+
spec = ::Gem.loaded_specs[name]
|
|
68
|
+
next if spec.nil?
|
|
69
|
+
|
|
70
|
+
version = spec.respond_to?(:version) ? spec.version : nil
|
|
71
|
+
return {name: name, version: version&.to_s}
|
|
72
|
+
end
|
|
73
|
+
nil
|
|
74
|
+
rescue
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Telemetry
|
|
5
|
+
module Detectors
|
|
6
|
+
# ProjectInfo detector. Returns a small hash describing the
|
|
7
|
+
# project's "package manager" — for the Ruby port, this is
|
|
8
|
+
# always Bundler (or `nil` when Bundler is not available).
|
|
9
|
+
#
|
|
10
|
+
# This is the Ruby-specific replacement for upstream's
|
|
11
|
+
# `detect-project-info.ts`, which parsed the
|
|
12
|
+
# `npm_config_user_agent` env var to determine the npm/yarn/pnpm
|
|
13
|
+
# toolchain. There is no equivalent Ruby env var; Bundler is the
|
|
14
|
+
# closest semantic match.
|
|
15
|
+
#
|
|
16
|
+
# ## Detection rule (Requirements 12.1 / 12.2)
|
|
17
|
+
#
|
|
18
|
+
# 1. If `Bundler` is `defined?` AND `Bundler.default_gemfile`
|
|
19
|
+
# succeeds (the Gemfile is locatable), return
|
|
20
|
+
# `{name: "bundler", version: ::Bundler::VERSION}`.
|
|
21
|
+
# 2. Otherwise return `nil`.
|
|
22
|
+
#
|
|
23
|
+
# ## Failure handling
|
|
24
|
+
#
|
|
25
|
+
# The whole call is wrapped in `rescue StandardError; nil` so any
|
|
26
|
+
# surprise from probing Bundler (e.g. a stubbed/partially-loaded
|
|
27
|
+
# Bundler module) degrades to `nil` rather than escaping out of
|
|
28
|
+
# the init payload composition in {BetterAuth::Telemetry.create}.
|
|
29
|
+
#
|
|
30
|
+
# No `npm_config_user_agent` or other Node package-manager env
|
|
31
|
+
# var is read (Requirement 12.3); this Ruby-specific deviation
|
|
32
|
+
# is intentional.
|
|
33
|
+
#
|
|
34
|
+
# @example Inside a Bundler-managed app
|
|
35
|
+
# BetterAuth::Telemetry::Detectors::ProjectInfo.call
|
|
36
|
+
# # => {name: "bundler", version: "2.5.3"}
|
|
37
|
+
#
|
|
38
|
+
# @example Bundler not loaded
|
|
39
|
+
# BetterAuth::Telemetry::Detectors::ProjectInfo.call
|
|
40
|
+
# # => nil
|
|
41
|
+
module ProjectInfo
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# Resolve the project-info signal for the host application.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash{Symbol => String}, nil] either
|
|
47
|
+
# `{name: "bundler", version: <Bundler::VERSION>}` when
|
|
48
|
+
# Bundler is loaded and a Gemfile is locatable, otherwise
|
|
49
|
+
# `nil`.
|
|
50
|
+
def call
|
|
51
|
+
return nil unless bundler_loaded?
|
|
52
|
+
return nil unless default_gemfile_locatable?
|
|
53
|
+
|
|
54
|
+
{name: "bundler", version: ::Bundler::VERSION}
|
|
55
|
+
rescue
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether the `Bundler` constant is defined in the current
|
|
60
|
+
# process. Extracted as a stub seam so tests can simulate the
|
|
61
|
+
# Bundler-absent case without actually unloading Bundler.
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def bundler_loaded?
|
|
65
|
+
defined?(::Bundler) ? true : false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Whether `Bundler.default_gemfile` resolves successfully.
|
|
69
|
+
# Bundler raises `Bundler::GemfileNotFound` (a `StandardError`
|
|
70
|
+
# subclass) when no Gemfile is locatable, so we treat any
|
|
71
|
+
# raise as "not locatable" rather than letting it escape.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def default_gemfile_locatable?
|
|
75
|
+
return false unless ::Bundler.respond_to?(:default_gemfile)
|
|
76
|
+
|
|
77
|
+
!::Bundler.default_gemfile.nil?
|
|
78
|
+
rescue
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Telemetry
|
|
5
|
+
module Detectors
|
|
6
|
+
# Runtime detector. Returns a small hash describing the Ruby
|
|
7
|
+
# interpreter currently executing the host application.
|
|
8
|
+
#
|
|
9
|
+
# This is the Ruby-specific replacement for upstream's
|
|
10
|
+
# `detect-runtime.ts`, which classified Node, Deno, Bun, Cloudflare
|
|
11
|
+
# Workers, and other JavaScript runtimes. The Ruby port is
|
|
12
|
+
# server-only, so all of those branches collapse into a single
|
|
13
|
+
# `"ruby"` case. The `:engine` field preserves enough information
|
|
14
|
+
# for telemetry consumers to distinguish MRI, JRuby, TruffleRuby,
|
|
15
|
+
# etc.
|
|
16
|
+
#
|
|
17
|
+
# The whole detector is wrapped in `rescue StandardError` so a
|
|
18
|
+
# surprise from `RUBY_VERSION`/`RUBY_ENGINE` (very unlikely, but
|
|
19
|
+
# possible under exotic patched interpreters) cannot bubble out
|
|
20
|
+
# of the init payload composition in
|
|
21
|
+
# {BetterAuth::Telemetry.create}.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# BetterAuth::Telemetry::Detectors::Runtime.call
|
|
25
|
+
# # => {name: "ruby", version: "3.3.0", engine: "ruby"}
|
|
26
|
+
module Runtime
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# @return [Hash{Symbol => String, nil}] hash with `:name`,
|
|
30
|
+
# `:version`, and `:engine` keys. `:name` is always `"ruby"`.
|
|
31
|
+
# `:version` is `RUBY_VERSION`. `:engine` is `RUBY_ENGINE`
|
|
32
|
+
# when defined, otherwise the literal string `"ruby"`.
|
|
33
|
+
def call
|
|
34
|
+
{
|
|
35
|
+
name: "ruby",
|
|
36
|
+
version: RUBY_VERSION,
|
|
37
|
+
engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
|
|
38
|
+
}
|
|
39
|
+
rescue
|
|
40
|
+
{name: "ruby", version: nil, engine: nil}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|