Widget diagnostics: per-intent beacons (keychain slots + relay query)

The single-slot beacon couldn't distinguish small vs medium (last writer
wins). Beacon keychain service now embeds the intent type name so small
and medium never overwrite each other; the relay GET now carries the
intent type + entry state as query params so the access log shows exactly
which widget kind ran makeEntry, when, and with what. Diagnostics screen
shows SMALL and MEDIUM beacons side by side — a missing SMALL beacon
after adding the small widget proves its timeline never reaches the
provider (system-level config resolution failure).
This commit is contained in:
FuelBoard Contributor
2026-08-13 20:13:24 +01:00
parent 335a9751dc
commit 3cf1d5c9af
3 changed files with 50 additions and 23 deletions
+20 -7
View File
@@ -133,22 +133,35 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
}
/// Fire-and-forget diagnostics beacon: writes the entry state to keychain
/// (app-readable) and GETs the relay so its access log records that a
/// widget timeline actually ran in the extension and what it produced.
/// Deliberately outside the timeline result can never affect rendering.
/// (app-readable, one slot per intent type) and GETs the relay so its
/// access log records that a widget timeline actually ran in the
/// extension and what it produced. Deliberately outside the timeline
/// result can never affect rendering.
private static func writeDiagBeacon(entry: FuelPriceEntry) {
let intentType = String(describing: Configuration.self)
let first = entry.stations.first
let json = """
{"intent":"\(Configuration.self)","source":"\(entry.locationSource)",\
{"intent":"\(intentType)","source":"\(entry.locationSource)",\
"n":\(entry.stations.count),"fuel":"\(entry.fuel.rawValue)",\
"sort":"\(entry.sort.rawValue)","fav":\(entry.isFavourites),\
"station":"\(first?.name ?? "")","price":\(first?.prices[entry.fuel] ?? -1)}
"""
FuelStore.saveWidgetDiag(json)
guard let url = URLComponents(
FuelStore.saveWidgetDiag(json, intentType: intentType)
guard var components = URLComponents(
url: RelayFuelProvider().baseURL.appendingPathComponent("api/v1/widget-diag"),
resolvingAgainstBaseURL: false
)?.url else { return }
) else { return }
components.queryItems = [
URLQueryItem(name: "intent", value: intentType),
URLQueryItem(name: "source", value: entry.locationSource),
URLQueryItem(name: "n", value: String(entry.stations.count)),
URLQueryItem(name: "fuel", value: entry.fuel.rawValue),
URLQueryItem(name: "sort", value: entry.sort.rawValue),
URLQueryItem(name: "fav", value: entry.isFavourites ? "1" : "0"),
URLQueryItem(name: "station", value: first?.name ?? ""),
URLQueryItem(name: "price", value: String(first?.prices[entry.fuel] ?? -1)),
]
guard let url = components.url else { return }
var request = URLRequest(url: url)
request.timeoutInterval = 2
Task {