field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

commit 2156def908cb25c2e7968dfab91b5edb34292730
parent b1267fb6c240f6211a07f08b2ddb37d4dc0fc5fe
Author: triesap <tyson@radroots.org>
Date:   Thu, 18 Jun 2026 12:55:45 -0700

runtime: make config and logging deterministic

- read release app config from plist-backed build values only
- keep launch environment overrides behind the Debug UI-test harness
- fail startup explicitly when runtime logging setup fails
- record logging initialization without fallback diagnostics

Diffstat:
MRadroots/Runtime/BuildConfig.swift | 28++++++++++++++++++----------
MRadroots/Runtime/FieldTelemetry.swift | 10++++------
MRadroots/Runtime/LoggingSettings.swift | 11+++++++++++
MRadroots/Runtime/Radroots.swift | 6++----
4 files changed, 35 insertions(+), 20 deletions(-)

diff --git a/Radroots/Runtime/BuildConfig.swift b/Radroots/Runtime/BuildConfig.swift @@ -16,12 +16,12 @@ enum BuildConfigKey: String { enum BuildConfig { static func string(_ key: BuildConfigKey) -> String? { - envString(key) ?? infoString(key).map { stripOuterQuotes($0) } + debugLaunchOverrideString(key) ?? infoString(key).map { stripOuterQuotes($0) } } static func bool(_ key: BuildConfigKey) -> Bool? { - if let env = ProcessInfo.processInfo.environment[key.rawValue], - let parsed = parseBool(env) { + if let raw = debugLaunchOverrideString(key), + let parsed = parseBool(raw) { return parsed } if let v = infoValue(for: key.rawValue) { @@ -33,7 +33,7 @@ enum BuildConfig { } static func array(_ key: BuildConfigKey, splitBy set: CharacterSet = .whitespacesAndNewlines) -> [String]? { - if let raw = envString(key) { + if let raw = debugLaunchOverrideString(key) { return parseArray(raw, splitBy: set) } if let direct = infoArray(key) { @@ -66,12 +66,6 @@ enum BuildConfig { return out } - private static func envString(_ key: BuildConfigKey) -> String? { - ProcessInfo.processInfo.environment[key.rawValue] - .flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .flatMap { $0.isEmpty ? nil : stripOuterQuotes($0) } - } - private static func parseArray(_ value: String, splitBy set: CharacterSet) -> [String]? { var raw = value.trimmingCharacters(in: .whitespacesAndNewlines) guard !raw.isEmpty else { return nil } @@ -133,4 +127,18 @@ enum BuildConfig { } return s } + + #if DEBUG + private static func debugLaunchOverrideString(_ key: BuildConfigKey) -> String? { + guard FieldUITestHarness.isRequested else { + return nil + } + return FieldUITestHarness.string(key.rawValue) + .flatMap { $0.isEmpty ? nil : stripOuterQuotes($0) } + } + #else + private static func debugLaunchOverrideString(_ key: BuildConfigKey) -> String? { + nil + } + #endif } diff --git a/Radroots/Runtime/FieldTelemetry.swift b/Radroots/Runtime/FieldTelemetry.swift @@ -77,18 +77,14 @@ final class FieldTelemetry: @unchecked Sendable { await sink.record(event) } - func runtimeLoggingInitialized( - settings: LoggingSettings, - fallbackUsed: Bool - ) { + func runtimeLoggingInitialized(settings: LoggingSettings) { record( name: "field_ios.runtime.logging_initialized", - level: fallbackUsed ? .warning : .info, + level: .info, fields: [ try? .bool("stdout_enabled", settings.stdout), try? .bool("file_enabled", settings.fileEnabled), try? .string("logging_filter", settings.level ?? "unset"), - try? .bool("fallback_used", fallbackUsed) ].compactMap { $0 } ) } @@ -327,6 +323,8 @@ final class FieldTelemetry: @unchecked Sendable { switch error { case FieldUserPresenceGateError.notVerified: return "unverified" + case is FieldRuntimeLoggingError: + return "logging_initialization_failed" case let error as RadrootsUserPresenceError: return userPresenceOutcome(for: error) case let error as RadrootsCaptureIntakeError: diff --git a/Radroots/Runtime/LoggingSettings.swift b/Radroots/Runtime/LoggingSettings.swift @@ -1,5 +1,16 @@ import Foundation +enum FieldRuntimeLoggingError: LocalizedError { + case initializationFailed(String) + + var errorDescription: String? { + switch self { + case .initializationFailed(let message): + "Runtime logging initialization failed: \(message)" + } + } +} + struct LoggingSettings: Equatable { var stdout: Bool var fileEnabled: Bool diff --git a/Radroots/Runtime/Radroots.swift b/Radroots/Runtime/Radroots.swift @@ -15,14 +15,12 @@ public final class Radroots: ObservableObject { telemetry: FieldTelemetry = .shared ) throws -> FieldRuntimeService { let settings = LoggingSettings.load() - var loggingFallbackUsed = false do { try settings.apply(bundleIdentifier: bundleId) } catch { - try? initLoggingStdout() - loggingFallbackUsed = true + throw FieldRuntimeLoggingError.initializationFailed(error.localizedDescription) } - telemetry.runtimeLoggingInitialized(settings: settings, fallbackUsed: loggingFallbackUsed) + telemetry.runtimeLoggingInitialized(settings: settings) let rt = try RadrootsRuntime() let resolvedSha = buildSha ?? (Bundle.main.object(forInfoDictionaryKey: "GIT_SHA") as? String)