Module: BetterAuth::Telemetry::Detectors::SystemInfo

Defined in:
lib/better_auth/telemetry/detectors/system_info.rb

Overview

System info detector. Returns a hash describing the host platform, container/WSL state, deployment vendor, and a few cheap host-level signals (cpu count, memory, isTTY).

This is the Ruby-specific replacement for upstream’s ‘detect-system-info.ts` (and the Node-specific block inside `node.ts`). The Ruby port collapses both upstream variants into a single server-side detector that uses `RbConfig`, `Gem::Platform.local`, `Etc`, `IO.popen`, and a few `File.exist?` / `File.read` probes against well-known paths.

## Ruby-specific deviations from upstream

  • ‘cpuModel` is always `nil`. There is no portable Ruby stdlib API for the model string, and exposing a partial detection (e.g. parsing `/proc/cpuinfo`) would only work on Linux.

  • ‘cpuSpeed` (an upstream key) is **omitted entirely** from the returned hash, rather than emitted as `nil`. Including it would invite consumers to assume it can ever be populated by the Ruby implementation. The README documents this.

  • ‘memory` is read from `/proc/meminfo` on Linux and from `sysctl -n hw.memsize` on macOS via SystemInfo.read_sysctl_memsize under a 1s Timeout.timeout. On other platforms (and on read failures) it is `nil`.

## Failure handling (Requirement 9.11)

Every probe is invoked through SystemInfo.safely, which is just ‘yield rescue StandardError; nil`. A surprise from any single probe degrades that field to `nil` rather than escaping out of the init payload composition in BetterAuth::Telemetry.create.

Each helper probe (SystemInfo.detect_vendor, SystemInfo.platform, SystemInfo.release, SystemInfo.architecture, SystemInfo.cpu_count, SystemInfo.total_memory_bytes, SystemInfo.wsl?, SystemInfo.docker?, SystemInfo.tty?) is exposed as a ‘module_function` so it can be stubbed with `Minitest::Mock#stub` in the corresponding test, exercising the per-field rescue path.

Constant Summary collapse

VENDORS =

Vendor table. The list and order mirror upstream’s ‘getVendor` short-circuit chain in `upstream/better-auth/1.6.9/packages/telemetry/src/detectors/detect-system-info.ts`. First match wins; a missing match yields `nil`.

Each entry is ‘[vendor_name, [marker_env_var, …]]`. A vendor matches when any of its marker variables is set to a non-empty value.

[
  ["cloudflare", %w[CF_PAGES CF_PAGES_URL CF_ACCOUNT_ID]],
  ["vercel", %w[VERCEL VERCEL_URL VERCEL_ENV]],
  ["netlify", %w[NETLIFY NETLIFY_URL]],
  ["render", %w[RENDER RENDER_URL RENDER_INTERNAL_HOSTNAME RENDER_SERVICE_ID]],
  ["aws", %w[AWS_LAMBDA_FUNCTION_NAME AWS_EXECUTION_ENV LAMBDA_TASK_ROOT]],
  ["gcp", %w[GOOGLE_CLOUD_FUNCTION_NAME GOOGLE_CLOUD_PROJECT GCP_PROJECT K_SERVICE]],
  ["azure", %w[AZURE_FUNCTION_NAME FUNCTIONS_WORKER_RUNTIME WEBSITE_INSTANCE_ID WEBSITE_SITE_NAME]],
  ["deno-deploy", %w[DENO_DEPLOYMENT_ID DENO_REGION]],
  ["fly-io", %w[FLY_APP_NAME FLY_REGION FLY_ALLOC_ID]],
  ["railway", %w[RAILWAY_STATIC_URL RAILWAY_ENVIRONMENT_NAME]],
  ["heroku", %w[DYNO HEROKU_APP_NAME]],
  ["digitalocean", %w[DO_DEPLOYMENT_ID DO_APP_NAME DIGITALOCEAN]],
  ["koyeb", %w[KOYEB KOYEB_DEPLOYMENT_ID KOYEB_APP_NAME]]
].freeze
SYSCTL_TIMEOUT_SECONDS =

Cap on the ‘sysctl` subprocess reading macOS `hw.memsize`. A well-behaved `sysctl` returns essentially instantly; the cap only exists so a hung subprocess cannot block init.

1

Class Method Summary collapse

Class Method Details

.architectureString?

Short architecture identifier matching upstream ‘os.arch()` style.

Returns:

  • (String, nil)

    e.g. ‘“x64”`, `“arm64”`, `“ia32”`. Falls back to `Gem::Platform.local.cpu` (or the raw `host_cpu`) when the value does not match a known token.



186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 186

def architecture
  host_cpu = RbConfig::CONFIG["host_cpu"].to_s.downcase
  case host_cpu
  when "x86_64", "amd64", "x64" then "x64"
  when "aarch64", "arm64" then "arm64"
  when "i386", "i686", "x86" then "ia32"
  when /ppc64/ then "ppc64"
  when /ppc/ then "ppc"
  when /arm/ then "arm"
  else
    (Gem::Platform.local.cpu if defined?(::Gem::Platform)) || host_cpu
  end
end

.callHash{Symbol => Object, nil}

Compose the system-info hash emitted as ‘payload` in the init event.

Returns:

  • (Hash{Symbol => Object, nil})

    hash with keys ‘:deploymentVendor`, `:systemPlatform`, `:systemRelease`, `:systemArchitecture`, `:cpuCount`, `:cpuModel`, `:memory`, `:isWSL`, `:isDocker`, `:isTTY`. Any individual field may be `nil` when the underlying probe is unsupported on the host or raises. The key `:cpuSpeed` is intentionally absent.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 91

def call
  {
    deploymentVendor: safely { detect_vendor },
    systemPlatform: safely { platform },
    systemRelease: safely { release },
    systemArchitecture: safely { architecture },
    cpuCount: safely { cpu_count },
    cpuModel: nil,
    memory: safely { total_memory_bytes },
    isWSL: safely { wsl? },
    isDocker: safely { docker? },
    isTTY: safely { tty? }
  }
end

.cpu_countInteger?

Returns the value returned by ‘Etc.nprocessors`, reported verbatim including `0`. The outer `safely` wrapper in call maps an `Etc.nprocessors` raise to `nil`.

Returns:

  • (Integer, nil)

    the value returned by ‘Etc.nprocessors`, reported verbatim including `0`. The outer `safely` wrapper in call maps an `Etc.nprocessors` raise to `nil`.



204
205
206
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 204

def cpu_count
  ::Etc.nprocessors
end

.detect_vendorString?

Match the first vendor whose marker variables are present in ‘ENV`. Mirrors upstream’s ‘getVendor` short-circuit chain.

Returns:

  • (String, nil)

    the vendor name (‘“vercel”`, `“cloudflare”`, …) or `nil` when no vendor matches.



125
126
127
128
129
130
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 125

def detect_vendor
  VENDORS.each do |(name, keys)|
    return name if keys.any? { |k| has_env_marker?(k) }
  end
  nil
end

.docker?Boolean

Detect Docker via well-known sentinels.

Returns:

  • (Boolean)

    ‘true` when `/.dockerenv` exists OR `/proc/self/cgroup` exists and contains the literal substring `“docker”`; `false` otherwise.



257
258
259
260
261
262
263
264
265
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 257

def docker?
  return true if File.exist?("/.dockerenv")
  if File.exist?("/proc/self/cgroup")
    return true if File.read("/proc/self/cgroup").include?("docker")
  end
  false
rescue
  false
end

.has_env_marker?(key) ⇒ Boolean

Returns whether ‘ENV` is set to a non-empty string. Mirrors upstream’s ‘Boolean(env)`.

Returns:

  • (Boolean)

    whether ‘ENV` is set to a non-empty string. Mirrors upstream’s ‘Boolean(env)`.



134
135
136
137
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 134

def has_env_marker?(key)
  value = ENV[key]
  !value.nil? && !value.empty?
end

.microsoft_marker?Boolean

Returns whether either ‘Etc.uname` or `/proc/version` contains `“microsoft”` (case-insensitive).

Returns:

  • (Boolean)

    whether either ‘Etc.uname` or `/proc/version` contains `“microsoft”` (case-insensitive).



289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 289

def microsoft_marker?
  if defined?(::Etc) && ::Etc.respond_to?(:uname)
    release_str = ::Etc.uname[:release].to_s
    return true if release_str.downcase.include?("microsoft")
  end

  if File.exist?("/proc/version")
    return true if File.read("/proc/version").downcase.include?("microsoft")
  end

  false
rescue
  false
end

.non_docker_container?Boolean

Returns whether the host is detected as a non-Docker container — ‘/run/.containerenv` is present AND docker? is `false`.

Returns:

  • (Boolean)

    whether the host is detected as a non-Docker container — ‘/run/.containerenv` is present AND docker? is `false`.



307
308
309
310
311
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 307

def non_docker_container?
  File.exist?("/run/.containerenv") && !docker?
rescue
  false
end

.platformString?

Short platform identifier matching upstream ‘os.platform()` style.

Returns:

  • (String, nil)

    one of ‘“linux”`, `“darwin”`, `“windows”`, `“freebsd”`, `“openbsd”`, `“netbsd”`, `“sunos”`, `“aix”`. Falls back to `Gem::Platform.local.os` (or the raw `host_os`) when the `host_os` token does not match a known prefix.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 147

def platform
  host_os = RbConfig::CONFIG["host_os"].to_s.downcase
  case host_os
  when /linux/ then "linux"
  when /darwin/ then "darwin"
  when /mswin|mingw|cygwin/ then "windows"
  when /freebsd/ then "freebsd"
  when /openbsd/ then "openbsd"
  when /netbsd/ then "netbsd"
  when /sunos|solaris/ then "sunos"
  when /aix/ then "aix"
  else
    (Gem::Platform.local.os if defined?(::Gem::Platform)) || host_os
  end
end

.read_meminfo_bytesInteger?

Read ‘MemTotal` from `/proc/meminfo`. The field reports kilobytes; we multiply to bytes to match upstream’s ‘os.totalmem()` units.

Returns:

  • (Integer, nil)


226
227
228
229
230
231
232
233
234
235
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 226

def read_meminfo_bytes
  File.foreach("/proc/meminfo") do |line|
    if (m = line.match(/\AMemTotal:\s+(\d+)\s+kB/i))
      return m[1].to_i * 1024
    end
  end
  nil
rescue
  nil
end

.read_sysctl_memsizeInteger?

Run ‘sysctl -n hw.memsize` under a 1s timeout. The subprocess writes a single integer (bytes) to stdout.

Returns:

  • (Integer, nil)


241
242
243
244
245
246
247
248
249
250
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 241

def read_sysctl_memsize
  output = Timeout.timeout(SYSCTL_TIMEOUT_SECONDS) do
    IO.popen(["sysctl", "-n", "hw.memsize"], err: File::NULL, &:read)
  end
  return nil if output.nil? || output.strip.empty?
  value = output.strip.to_i
  (value > 0) ? value : nil
rescue
  nil
end

.releaseString?

Operating-system release string. Prefers ‘Etc.uname` (e.g. `“5.15.0-92-generic”` on Linux, `“24.6.0”` on macOS). Falls back to the trailing version digits of `RbConfig::CONFIG` (e.g. `“darwin25”` → `“25”`) when `Etc.uname` is unavailable.

Returns:

  • (String, nil)


170
171
172
173
174
175
176
177
178
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 170

def release
  if defined?(::Etc) && ::Etc.respond_to?(:uname)
    value = ::Etc.uname[:release]
    return value if value.is_a?(String) && !value.empty?
  end
  host_os = RbConfig::CONFIG["host_os"].to_s
  tail = host_os[/\d.*\z/]
  (tail.nil? || tail.empty?) ? nil : tail
end

.safely { ... } ⇒ Object?

Run ‘block` and rescue any `StandardError` to `nil`. The whole detector composes its return hash by calling each probe through this helper, so a raising probe degrades only that field rather than aborting the entire detector.

Yields:

  • the probe to run.

Returns:

  • (Object, nil)

    whatever the block returns, or ‘nil` if the block raised a `StandardError`.



114
115
116
117
118
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 114

def safely
  yield
rescue
  nil
end

.total_memory_bytesInteger?

Total system memory in bytes when reachable on the host platform, otherwise ‘nil`.

Returns:

  • (Integer, nil)


212
213
214
215
216
217
218
219
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 212

def total_memory_bytes
  case platform
  when "linux"
    read_meminfo_bytes
  when "darwin"
    read_sysctl_memsize
  end
end

.tty?Boolean

Returns ‘$stdout.tty?`.

Returns:

  • (Boolean)

    ‘$stdout.tty?`.



314
315
316
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 314

def tty?
  $stdout.tty?
end

.wsl?Boolean

Detect WSL.

‘true` iff `RUBY_PLATFORM` indicates Linux AND either `Etc.uname` or `/proc/version` contains the case-insensitive substring `“microsoft”`, AND the host is not detected as inside a non-Docker container (the `/run/.containerenv` sentinel).

Returns:

  • (Boolean)


276
277
278
279
280
281
282
283
284
285
# File 'lib/better_auth/telemetry/detectors/system_info.rb', line 276

def wsl?
  return false unless RUBY_PLATFORM.to_s.include?("linux")

  return false unless microsoft_marker?
  return false if non_docker_container?

  true
rescue
  false
end