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 {