From 8bedaf371a49527bf018a2a93d2124ff65401a8f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 7 May 2026 21:24:50 +0100 Subject: [PATCH] feat(weather): Add Weather app logging Add OSLog categories for the Weather example app and emit useful runtime logs for app launch, location selection, settings changes, and weather service requests. Also add the local XcodeBuildMCP development MCP server configuration. Co-Authored-By: Codex --- .mcp.json | 14 ++++++ .../Weather/Weather/AppLogger.swift | 9 ++++ .../Weather/Weather/ContentView.swift | 3 ++ .../Weather/Services/WeatherService.swift | 46 ++++++++++++++++--- .../Views/Overlays/SettingsSheetView.swift | 22 +++++++++ .../Weather/Weather/WeatherApp.swift | 3 ++ 6 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 example_projects/Weather/Weather/AppLogger.swift diff --git a/.mcp.json b/.mcp.json index c25dcb38..01200b3d 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,6 +3,20 @@ "sentry": { "type": "http", "url": "https://mcp.sentry.dev/mcp" + }, + "xcodebuildmcp-dev": { + "type": "stdio", + "command": "node", + "args": [ + "../../build/cli.js", + "mcp" + ], + "env": { + "XCODEBUILDMCP_DEBUG": "true", + "XCODEBUILDMCP_SENTRY_DISABLED": "true", + "XCODEBUILDMCP_IOS_TEMPLATE_PATH": "../../../XcodeBuildMCP-iOS-Template", + "XCODEBUILDMCP_MACOS_TEMPLATE_PATH": "../../../XcodeBuildMCP-macOS-Template" + } } } } \ No newline at end of file diff --git a/example_projects/Weather/Weather/AppLogger.swift b/example_projects/Weather/Weather/AppLogger.swift new file mode 100644 index 00000000..fe5b30f5 --- /dev/null +++ b/example_projects/Weather/Weather/AppLogger.swift @@ -0,0 +1,9 @@ +import OSLog + +enum AppLog { + private static let subsystem = "com.sentry.weather.Weather" + static let app = Logger(subsystem: subsystem, category: "app") + static let service = Logger(subsystem: subsystem, category: "service") + static let settings = Logger(subsystem: subsystem, category: "settings") + static let location = Logger(subsystem: subsystem, category: "location") +} diff --git a/example_projects/Weather/Weather/ContentView.swift b/example_projects/Weather/Weather/ContentView.swift index 33fa02da..ca85cbf9 100644 --- a/example_projects/Weather/Weather/ContentView.swift +++ b/example_projects/Weather/Weather/ContentView.swift @@ -5,6 +5,7 @@ // Created by Cameron on 30/04/2026. // +import OSLog import SwiftUI enum WeatherSheet: Identifiable { @@ -121,10 +122,12 @@ struct ContentView: View { } private func selectLocation(_ location: WeatherLocation) { + AppLog.location.notice("select id=\(location.id, privacy: .public) name=\"\(location.name, privacy: .public)\"") selectedLocation = location } private func previewLocation(_ location: WeatherLocation) { + AppLog.location.notice("preview id=\(location.id, privacy: .public) name=\"\(location.name, privacy: .public)\"") selectedLocation = location } diff --git a/example_projects/Weather/Weather/Services/WeatherService.swift b/example_projects/Weather/Weather/Services/WeatherService.swift index 24644e60..59d46e1b 100644 --- a/example_projects/Weather/Weather/Services/WeatherService.swift +++ b/example_projects/Weather/Weather/Services/WeatherService.swift @@ -1,4 +1,5 @@ import Foundation +import OSLog struct WeatherService: Sendable { private let apiClient: any WeatherAPIClient @@ -8,23 +9,56 @@ struct WeatherService: Sendable { } func defaultLocations() async throws -> [WeatherLocation] { - try await apiClient.defaultLocations().map { dto in - try WeatherLocation(dto: dto) + let start = ContinuousClock.now + AppLog.service.notice("defaultLocations start") + do { + let result = try await apiClient.defaultLocations().map { dto in + try WeatherLocation(dto: dto) + } + AppLog.service.notice("defaultLocations ok count=\(result.count, privacy: .public) elapsed=\(elapsedMs(since: start), privacy: .public)ms") + return result + } catch { + AppLog.service.error("defaultLocations failed error=\(String(describing: error), privacy: .public) elapsed=\(elapsedMs(since: start), privacy: .public)ms") + throw error } } func weather(for locationID: WeatherLocation.ID) async throws -> WeatherReport { - let dto = try await apiClient.weather(for: locationID) - return try WeatherReport(dto: dto) + let start = ContinuousClock.now + AppLog.service.notice("weather start id=\(locationID, privacy: .public)") + do { + let dto = try await apiClient.weather(for: locationID) + let report = try WeatherReport(dto: dto) + AppLog.service.notice("weather ok id=\(locationID, privacy: .public) temp=\(report.current.temperatureC, privacy: .public)C elapsed=\(elapsedMs(since: start), privacy: .public)ms") + return report + } catch { + AppLog.service.error("weather failed id=\(locationID, privacy: .public) error=\(String(describing: error), privacy: .public) elapsed=\(elapsedMs(since: start), privacy: .public)ms") + throw error + } } func searchLocations(matching query: String) async throws -> [WeatherLocation] { - try await apiClient.searchLocations(matching: query).map { dto in - try WeatherLocation(dto: dto) + let start = ContinuousClock.now + AppLog.service.notice("search start query=\"\(query, privacy: .public)\"") + do { + let result = try await apiClient.searchLocations(matching: query).map { dto in + try WeatherLocation(dto: dto) + } + AppLog.service.notice("search ok query=\"\(query, privacy: .public)\" count=\(result.count, privacy: .public) elapsed=\(elapsedMs(since: start), privacy: .public)ms") + return result + } catch { + AppLog.service.error("search failed query=\"\(query, privacy: .public)\" error=\(String(describing: error), privacy: .public) elapsed=\(elapsedMs(since: start), privacy: .public)ms") + throw error } } } +private func elapsedMs(since start: ContinuousClock.Instant) -> Int { + let duration = ContinuousClock.now - start + let (seconds, attoseconds) = duration.components + return Int(seconds * 1000) + Int(attoseconds / 1_000_000_000_000_000) +} + extension WeatherService { static var production: WeatherService { WeatherService(apiClient: URLSessionWeatherAPIClient(configuration: .production)) diff --git a/example_projects/Weather/Weather/Views/Overlays/SettingsSheetView.swift b/example_projects/Weather/Weather/Views/Overlays/SettingsSheetView.swift index 1e8daa39..a571db22 100644 --- a/example_projects/Weather/Weather/Views/Overlays/SettingsSheetView.swift +++ b/example_projects/Weather/Weather/Views/Overlays/SettingsSheetView.swift @@ -1,3 +1,4 @@ +import OSLog import SwiftUI struct SettingsSheetView: View { @@ -53,6 +54,27 @@ struct SettingsSheetView: View { .scrollIndicators(.hidden) .background(sheetBackground) .accessibilityIdentifier("weather.settingsSheet") + .onChange(of: units.temperature) { _, new in + AppLog.settings.notice("temperature=\(new.label, privacy: .public)") + } + .onChange(of: units.wind) { _, new in + AppLog.settings.notice("wind=\(new.label, privacy: .public)") + } + .onChange(of: units.pressure) { _, new in + AppLog.settings.notice("pressure=\(new.label, privacy: .public)") + } + .onChange(of: units.distance) { _, new in + AppLog.settings.notice("distance=\(new.label, privacy: .public)") + } + .onChange(of: units.animationsEnabled) { _, new in + AppLog.settings.notice("animationsEnabled=\(new, privacy: .public)") + } + .onChange(of: units.alertsEnabled) { _, new in + AppLog.settings.notice("alertsEnabled=\(new, privacy: .public)") + } + .onChange(of: units.reduceTransparency) { _, new in + AppLog.settings.notice("reduceTransparency=\(new, privacy: .public)") + } } private var sheetBackground: some View { diff --git a/example_projects/Weather/Weather/WeatherApp.swift b/example_projects/Weather/Weather/WeatherApp.swift index f0570e30..9c619e4d 100644 --- a/example_projects/Weather/Weather/WeatherApp.swift +++ b/example_projects/Weather/Weather/WeatherApp.swift @@ -5,6 +5,7 @@ // Created by Cameron on 30/04/2026. // +import OSLog import SwiftUI @main @@ -12,7 +13,9 @@ struct WeatherApp: App { private let weatherService: WeatherService init() { + let useMock = ProcessInfo.processInfo.arguments.contains("--mock-weather-api") weatherService = AppWeatherServiceFactory.makeService() + AppLog.app.notice("launch service=\(useMock ? "mock" : "production", privacy: .public)") } var body: some Scene {