commit 7b6cb20ac3898f570961c9370a4665cb8a6556f3
parent 417d5882b824fd3e8e0ec9cd74a66a9b8da4d226
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 14:44:43 -0700
telemetry: add deterministic UI test probe
- expose recorded UI-test telemetry through a hidden safe probe
- route document interchange probes through app telemetry wrappers
- publish structured event coverage and unsafe-value absence flags
- keep the telemetry proof out of product-facing UI
Diffstat:
5 files changed, 156 insertions(+), 0 deletions(-)
diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj
@@ -42,6 +42,7 @@
B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */; };
C22DB0F3EB2E69A34DF941E0 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2818363B157125491FB84A1E /* App.swift */; };
C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */; };
+ CC5561169A29B5B2B6423959 /* FieldTelemetryUITestProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */; };
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */; };
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4289F43625DD65E6C4B25 /* CopyRow.swift */; };
D4CFDE54747B6D6957977025 /* FieldIdentityPublicMetadataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA942715DB13FFD494FD35A0 /* FieldIdentityPublicMetadataStore.swift */; };
@@ -85,6 +86,7 @@
7CC1D11E2E296B4B54E7E8A9 /* FieldCaptureIntake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldCaptureIntake.swift; sourceTree = "<group>"; };
7D69D200DB1F5FA7AA561CD7 /* TradeListing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListing.swift; sourceTree = "<group>"; };
8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldSecureIdentityStore.swift; sourceTree = "<group>"; };
+ 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetryUITestProbe.swift; sourceTree = "<group>"; };
8F0F21496E7A8490EB14AC5B /* Radroots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Radroots.swift; sourceTree = "<group>"; };
93AA285819DD1269C3EAD80A /* Radroots.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Radroots.app; sourceTree = BUILT_PRODUCTS_DIR; };
93D729E070C32490545FA837 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -201,6 +203,7 @@
E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */,
8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */,
3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */,
+ 8E6A1827AB4419F806CD848F /* FieldTelemetryUITestProbe.swift */,
D65835E1C633C7B946C64D11 /* FieldUserPresenceGate.swift */,
D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */,
63189EB90A86A9929BECD9ED /* Nostr.swift */,
@@ -436,6 +439,7 @@
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */,
D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */,
9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */,
+ CC5561169A29B5B2B6423959 /* FieldTelemetryUITestProbe.swift in Sources */,
25654E50F9519809A237759D /* FieldUserPresenceGate.swift in Sources */,
1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */,
B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */,
diff --git a/Radroots/App/AppEntry.swift b/Radroots/App/AppEntry.swift
@@ -43,6 +43,13 @@ public struct AppEntry<Main: View>: View {
.accessibilityIdentifier("field_ios.document_interchange.probe")
.accessibilityValue(probeValue)
}
+ if let probeValue = appState.telemetryProbeValue {
+ Color.clear
+ .frame(width: 1, height: 1)
+ .accessibilityElement()
+ .accessibilityIdentifier("field_ios.telemetry.probe")
+ .accessibilityValue(probeValue)
+ }
}
}
}
diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift
@@ -43,6 +43,7 @@ public final class AppState: ObservableObject {
@Published public private(set) var relayLastError: String?
@Published public private(set) var fileAccessProbeValue: String?
@Published public private(set) var documentInterchangeProbeValue: String?
+ @Published public private(set) var telemetryProbeValue: String?
@Published public private(set) var externalActionStatus: String?
@Published public private(set) var userPresenceStatus: String?
@Published public private(set) var canOpenNostrProfile: Bool = false
@@ -79,6 +80,7 @@ public final class AppState: ObservableObject {
private let lockKey = "field_ios.identity_locked"
private var statusTask: Task<Void, Never>?
+ private var telemetryProbeTask: Task<Void, Never>?
private var secureIdentityStore: FieldSecureIdentityStore?
private var identityMetadataStore: FieldIdentityPublicMetadataStore?
private var captureIntake: FieldCaptureIntake?
@@ -95,6 +97,7 @@ public final class AppState: ObservableObject {
deinit {
statusTask?.cancel()
+ telemetryProbeTask?.cancel()
}
public func start() async throws {
@@ -148,12 +151,16 @@ public final class AppState: ObservableObject {
runtimeIdentityReady: runtimeIdentityReady,
locked: isLocked
)
+ startTelemetryProbeRefreshForUITest()
} catch {
statusTask?.cancel()
statusTask = nil
+ telemetryProbeTask?.cancel()
+ telemetryProbeTask = nil
let message = error.localizedDescription
bootstrapPhase = .failed(message)
telemetry.appStartupFailed(error)
+ startTelemetryProbeRefreshForUITest()
throw error
}
}
@@ -772,6 +779,19 @@ public final class AppState: ObservableObject {
connectingCount: relayConnectingCount,
lastError: relayLastError
)
+ guard FieldDocumentInterchangeUITestProbe.isRequested else {
+ return
+ }
+ let diagnosticsExport = try prepareDiagnosticsDocumentExport()
+ releasePreparedDocumentExport(diagnosticsExport)
+ let relayConfigExport = try prepareRelayConfigDocumentExport()
+ releasePreparedDocumentExport(relayConfigExport)
+ if let relayImportDocument = try FieldDocumentInterchangeUITestProbe.relayImportDocument(
+ bundleIdentifier: bundleIdentifier
+ ) {
+ _ = try importedRelayConfig(from: relayImportDocument)
+ }
+ _ = try publicPostShareRequest(content: " public field update ")
}
private func requestExternalAction(
@@ -812,6 +832,23 @@ public final class AppState: ObservableObject {
}
}
+ private func startTelemetryProbeRefreshForUITest() {
+ guard FieldTelemetryUITestProbe.isRequested else {
+ return
+ }
+ telemetryProbeTask?.cancel()
+ telemetryProbeTask = Task { [weak self] in
+ while !Task.isCancelled {
+ await self?.refreshTelemetryProbeValue()
+ try? await Task.sleep(nanoseconds: 250_000_000)
+ }
+ }
+ }
+
+ private func refreshTelemetryProbeValue() async {
+ telemetryProbeValue = await FieldTelemetryUITestProbe.value(recordedBy: telemetry)
+ }
+
private func shortNpub(_ value: String) -> String {
guard value.count > 18 else { return value }
return "\(value.prefix(12))...\(value.suffix(6))"
diff --git a/Radroots/Runtime/FieldDocumentInterchangeUITestProbe.swift b/Radroots/Runtime/FieldDocumentInterchangeUITestProbe.swift
@@ -16,6 +16,24 @@ enum FieldDocumentInterchangeUITestProbe {
ProcessInfo.processInfo.environment[enabledKey] == "true"
}
+ static func relayImportDocument(bundleIdentifier: String) throws -> RadrootsImportedDocument? {
+ guard isRequested else {
+ return nil
+ }
+ let data = relayImportFixtureData()
+ try FieldLocalState.fileAccess(bundleIdentifier: bundleIdentifier).write(
+ .inline(data),
+ to: importFixture
+ )
+ return try RadrootsImportedDocument(
+ file: importFixture,
+ originalURL: nil,
+ suggestedFilename: "radroots-relays.json",
+ mediaType: "application/json",
+ sizeBytes: UInt64(data.count)
+ )
+ }
+
static func startupValue(
bundleIdentifier: String,
infoJSONString: String,
diff --git a/Radroots/Runtime/FieldTelemetryUITestProbe.swift b/Radroots/Runtime/FieldTelemetryUITestProbe.swift
@@ -0,0 +1,90 @@
+import Foundation
+import RadrootsKit
+
+enum FieldTelemetryUITestProbe {
+ private static let enabledKey = "RADROOTS_FIELD_IOS_UI_TEST_TELEMETRY_PROBE"
+ private static let redactionPolicy = RadrootsTelemetryRedactionPolicy.default
+
+ static var isRequested: Bool {
+ ProcessInfo.processInfo.environment[enabledKey] == "true"
+ }
+
+ static func value(recordedBy telemetry: FieldTelemetry) async -> String? {
+ guard isRequested else {
+ return nil
+ }
+ let events = await telemetry.recordedEventsForUITest()
+ return value(events: events)
+ }
+
+ private static func value(events: [RadrootsTelemetryEvent]) -> String {
+ let eventNames = Set(events.map(\.name))
+ let fieldKeys = Set(events.flatMap { event in
+ event.fields.map(\.key)
+ })
+ let values = events.flatMap(stringValues)
+ return [
+ "event_count=\(events.count)",
+ "event_names=\(eventNames.sorted().joined(separator: ","))",
+ "field_keys=\(fieldKeys.sorted().joined(separator: ","))",
+ "runtime_logging_seen=\(eventNames.contains("field_ios.runtime.logging_initialized"))",
+ "startup_success_seen=\(eventNames.contains("field_ios.startup.success"))",
+ "relay_status_seen=\(eventNames.contains("field_ios.relay.status_changed"))",
+ "identity_create_seen=\(eventNames.contains("field_ios.identity_custody.create"))",
+ "user_presence_save_identity_seen=\(eventNames.contains("field_ios.user_presence.save_identity"))",
+ "document_diagnostics_export_seen=\(eventNames.contains("field_ios.document_interchange.diagnostics_export"))",
+ "document_relay_config_export_seen=\(eventNames.contains("field_ios.document_interchange.relay_config_export"))",
+ "document_relay_config_import_seen=\(eventNames.contains("field_ios.document_interchange.relay_config_import"))",
+ "document_public_share_prepare_seen=\(eventNames.contains("field_ios.document_interchange.public_share_prepare"))",
+ "capture_support_refreshed_seen=\(eventNames.contains("field_ios.capture.support_refreshed"))",
+ "capture_import_photo_seen=\(eventNames.contains("field_ios.capture.import_photo"))",
+ "capture_scan_document_seen=\(eventNames.contains("field_ios.capture.scan_document"))",
+ "external_action_open_seen=\(eventNames.contains("field_ios.external_action.open"))",
+ "unsafe_values_present=\(values.contains(where: containsUnsafeValue))",
+ "relay_url_values_present=\(values.contains(where: containsRelayURL))",
+ "secret_like_values_present=\(values.contains(where: containsSecretLikeValue))",
+ "path_like_values_present=\(values.contains(where: containsPathLikeValue))",
+ "npub_values_present=\(values.contains(where: containsNpubValue))"
+ ].joined(separator: ";")
+ }
+
+ private static func stringValues(from event: RadrootsTelemetryEvent) -> [String] {
+ var values = event.message.map { [$0] } ?? []
+ values.append(contentsOf: event.fields.map { field in
+ field.value.renderedValue
+ })
+ return values
+ }
+
+ private static func containsUnsafeValue(_ value: String) -> Bool {
+ redactionPolicy.containsUnsafeValue(value)
+ || containsRelayURL(value)
+ || containsSecretLikeValue(value)
+ || containsPathLikeValue(value)
+ || containsNpubValue(value)
+ }
+
+ private static func containsRelayURL(_ value: String) -> Bool {
+ let normalized = value.lowercased()
+ return normalized.contains("ws://") || normalized.contains("wss://")
+ }
+
+ private static func containsSecretLikeValue(_ value: String) -> Bool {
+ let normalized = value.lowercased()
+ return normalized.contains("nsec")
+ || normalized.range(of: "[a-f0-9]{64}", options: .regularExpression) != nil
+ }
+
+ private static func containsPathLikeValue(_ value: String) -> Bool {
+ let normalized = value.lowercased()
+ return normalized.contains("/users/")
+ || normalized.contains("/private/var/")
+ || normalized.contains("/var/mobile/containers/")
+ || normalized.contains("/var/folders/")
+ || normalized.contains("file:///")
+ }
+
+ private static func containsNpubValue(_ value: String) -> Bool {
+ value.lowercased().contains("npub")
+ }
+}