Преглед изворни кода

Default debug logging on and prompt for description when sharing logs (#627)

Flip the debugLogLevel default to true and add a migration step so
existing users with it stored as false also get it enabled, ensuring
shared logs contain useful detail when reporting problems.

When the user taps Share Logs, present a sheet asking for a short
description of the issue. The description is written to a
ShareNotice_<timestamp>.txt file (date, app version, branch+sha,
user description) and included alongside the log files in the iOS
share sheet.
Jonas Björkert пре 1 месец
родитељ
комит
985e8b294b

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -61,6 +61,7 @@
 		6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC582E9E7D1600BB18FE /* CalendarSettingsView.swift */; };
 		6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */; };
 		6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */; };
+		6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */; };
 		6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; };
 		6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; };
 		65A100012F5AA00000AA1001 /* UnitsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */; };
@@ -527,6 +528,7 @@
 		6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = "<group>"; };
 		6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = "<group>"; };
 		6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = "<group>"; };
+		6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogNoticeView.swift; sourceTree = "<group>"; };
 		6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = "<group>"; };
 		6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = "<group>"; };
 		65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsSettingsView.swift; sourceTree = "<group>"; };
@@ -1002,6 +1004,7 @@
 				6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */,
 				657F98172F043D8100F732BD /* HomeContentView.swift */,
 				6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */,
+				6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */,
 				65A100002F5AA00000AA1001 /* UnitsSettingsView.swift */,
 				65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */,
 				6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */,
@@ -2329,6 +2332,7 @@
 				65A100032F5AA00000AA1002 /* UnitsConfigurationView.swift in Sources */,
 				657F98182F043D8100F732BD /* HomeContentView.swift in Sources */,
 				6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */,
+				6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */,
 				DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */,
 				DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */,
 				DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */,

+ 37 - 0
LoopFollow/Settings/ShareLogNoticeView.swift

@@ -0,0 +1,37 @@
+// LoopFollow
+// ShareLogNoticeView.swift
+
+import SwiftUI
+
+struct ShareLogNoticeView: View {
+    @State private var noticeText: String = ""
+    let onCancel: () -> Void
+    let onShare: (String) -> Void
+
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    Text("Thanks for sharing these logs to help us find the problem. Please describe it in as much detail as possible — what time did it happen, what did you do, and what did you expect to happen that didn't?")
+                        .font(.callout)
+                        .foregroundColor(.secondary)
+                }
+
+                Section(header: Text("Description")) {
+                    TextEditor(text: $noticeText)
+                        .frame(minHeight: 180)
+                }
+            }
+            .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
+            .navigationBarTitle("Share Logs", displayMode: .inline)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("Cancel", action: onCancel)
+                }
+                ToolbarItem(placement: .confirmationAction) {
+                    Button("Share") { onShare(noticeText) }
+                }
+            }
+        }
+    }
+}

+ 7 - 0
LoopFollow/Storage/Storage+Migrate.swift

@@ -60,6 +60,13 @@ extension Storage {
         UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: legacyNotificationIDs)
     }
 
+    func migrateStep9() {
+        // Default for debugLogLevel changed from false to true so users ship useful
+        // logs when they report a problem. Force-enable for existing users.
+        LogManager.shared.log(category: .general, message: "Running migrateStep9 — enabling debug log level")
+        debugLogLevel.value = true
+    }
+
     func migrateStep6() {
         // APNs credential separation
         LogManager.shared.log(category: .general, message: "Running migrateStep6 — APNs credential separation")

+ 2 - 2
LoopFollow/Storage/Storage.swift

@@ -39,7 +39,7 @@ class Storage {
 
     var selectedBLEDevice = StorageValue<BLEDevice?>(key: "selectedBLEDevice", defaultValue: nil)
 
-    var debugLogLevel = StorageValue<Bool>(key: "debugLogLevel", defaultValue: false)
+    var debugLogLevel = StorageValue<Bool>(key: "debugLogLevel", defaultValue: true)
 
     var contactTrend = StorageValue<ContactIncludeOption>(key: "contactTrend", defaultValue: .off)
     var contactDelta = StorageValue<ContactIncludeOption>(key: "contactDelta", defaultValue: .off)
@@ -214,7 +214,7 @@ class Storage {
     // When adding a new migration step in `runMigrationsIfNeeded()`, bump this default
     // to the new latest step number so fresh installs skip all migrations. Other defaults
     // in this file must reflect the post-migration final state for a fresh install.
-    var migrationStep = StorageValue<Int>(key: "migrationStep", defaultValue: 7)
+    var migrationStep = StorageValue<Int>(key: "migrationStep", defaultValue: 9)
 
     var persistentNotification = StorageValue<Bool>(key: "persistentNotification", defaultValue: false)
     var persistentNotificationLastBGTime = StorageValue<Date>(key: "persistentNotificationLastBGTime", defaultValue: .distantPast)

+ 5 - 0
LoopFollow/ViewControllers/MainViewController.swift

@@ -692,6 +692,11 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio
             Storage.shared.migrateStep8()
             Storage.shared.migrationStep.value = 8
         }
+
+        if Storage.shared.migrationStep.value < 9 {
+            Storage.shared.migrateStep9()
+            Storage.shared.migrationStep.value = 9
+        }
     }
 
     @objc func appDidBecomeActive() {

+ 56 - 1
LoopFollow/ViewControllers/MoreMenuView.swift

@@ -140,10 +140,65 @@ struct MoreMenuView: View {
             showAlert = true
             return
         }
-        let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
+
+        let noticeView = ShareLogNoticeView(
+            onCancel: {
+                UIApplication.shared.topMost?.dismiss(animated: true)
+            },
+            onShare: { noticeText in
+                let presenter = UIApplication.shared.topMost
+                presenter?.dismiss(animated: true) {
+                    presentLogShareSheet(noticeText: noticeText, logFiles: files)
+                }
+            }
+        )
+        let host = UIHostingController(rootView: noticeView)
+        host.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        host.modalPresentationStyle = .formSheet
+        UIApplication.shared.topMost?.present(host, animated: true)
+    }
+
+    private func presentLogShareSheet(noticeText: String, logFiles: [URL]) {
+        var items: [Any] = logFiles
+        if let noticeURL = writeShareNoticeFile(text: noticeText) {
+            items.insert(noticeURL, at: 0)
+        }
+        let avc = UIActivityViewController(activityItems: items, applicationActivities: nil)
         UIApplication.shared.topMost?.present(avc, animated: true)
     }
 
+    private func writeShareNoticeFile(text: String) -> URL? {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyy-MM-dd_HHmm"
+        let timestamp = formatter.string(from: Date())
+
+        let version = AppVersionManager().version()
+        let branchAndSha = BuildDetails.default.branchAndSha
+
+        let body = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+            ? "(no description provided)"
+            : text
+
+        let contents = """
+        LoopFollow Log Share Notice
+        Date: \(ISO8601DateFormatter().string(from: Date()))
+        App version: \(version) (\(branchAndSha))
+
+        User description:
+        \(body)
+        """
+
+        let url = FileManager.default.temporaryDirectory
+            .appendingPathComponent("ShareNotice_\(timestamp).txt")
+        do {
+            try contents.write(to: url, atomically: true, encoding: .utf8)
+            return url
+        } catch {
+            LogManager.shared.log(category: .general, message: "Failed to write share notice file: \(error)")
+            return nil
+        }
+    }
+
     private func fetchVersionInfo() async {
         let mgr = AppVersionManager()
         let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()