Revisit submission of OpenVPN diagnostic report (#452)

Some improvements:

- Suggest replacing the template with the description of the issue
- Attach app log
- Append purchased features

Also reuse the same body for `mailto:` reports, as metadata were not
being attached in that case.

Closes #377
This commit is contained in:
Davide De Rosa 2024-01-07 12:11:16 +01:00 committed by GitHub
parent f13b4d0768
commit dde2d22eed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 125 additions and 80 deletions

View File

@ -127,7 +127,7 @@
<EnvironmentVariable <EnvironmentVariable
key = "APP_TYPE" key = "APP_TYPE"
value = "2" value = "2"
isEnabled = "NO"> isEnabled = "YES">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable <EnvironmentVariable
key = "LOG_LEVEL" key = "LOG_LEVEL"

View File

@ -27,6 +27,14 @@ import Foundation
import PassepartoutLibrary import PassepartoutLibrary
extension DebugLog { extension DebugLog {
static func decoratedMetadataString() -> String {
decoratedMetadataString(Constants.Global.appName, Constants.Global.appVersionString)
}
static func decoratedMetadataData() -> Data {
decoratedMetadataData(Constants.Global.appName, Constants.Global.appVersionString)
}
func decoratedString() -> String { func decoratedString() -> String {
decoratedString(Constants.Global.appName, Constants.Global.appVersionString) decoratedString(Constants.Global.appName, Constants.Global.appVersionString)
} }

View File

@ -74,33 +74,54 @@ enum Unlocalized {
static let subject = "\(appName) - Report issue" static let subject = "\(appName) - Report issue"
static func body(_ description: String, _ metadata: String) -> String { static let bodySentinel = "<replace this with a description of the issue>"
"Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
}
static let template = "description of the issue: "
static let maxLogBytes = UInt64(20000) static let maxLogBytes = UInt64(20000)
enum Filenames { enum Filenames {
static var debugLog: String { static let mime = "text/plain"
static var appLog: String {
let fmt = DateFormatter() let fmt = DateFormatter()
fmt.dateFormat = "yyyyMMdd-HHmmss" fmt.dateFormat = "yyyyMMdd-HHmmss"
let iso = fmt.string(from: Date()) let iso = fmt.string(from: Date())
return "debug-\(iso).txt" return "app-\(iso).txt"
} }
static let configuration = "profile.ovpn" static var tunnelLog: String {
// static let configuration = "profile.ovpn.txt" let fmt = DateFormatter()
fmt.dateFormat = "yyyyMMdd-HHmmss"
static let template = "description of the issue: " let iso = fmt.string(from: Date())
return "tunnel-\(iso).txt"
}
} }
enum MIME { private static func rawBody(_ description: String, _ metadata: String) -> String {
static let debugLog = "text/plain" "Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
}
// static let configuration = "application/x-openvpn-profile" static func body(
static let configuration = "text/plain" providerMetadata: ProviderMetadata?,
lastUpdate: Date?,
purchasedProductIdentifiers: Set<String>?
) -> String {
var content: [String] = ["Hi,\n"]
content.append(bodySentinel)
content.append("\n--\n")
content.append(DebugLog.decoratedMetadataString())
if let providerMetadata {
if let lastUpdate {
content.append("Provider: \(providerMetadata.fullName) (last updated: \(lastUpdate))")
} else {
content.append("Provider: \(providerMetadata.fullName)")
}
}
if let purchasedProductIdentifiers {
content.append("Purchased: \(purchasedProductIdentifiers)")
}
content.append("\n--\n")
content.append("Regards")
return content.joined(separator: "\n")
} }
} }

View File

@ -32,6 +32,8 @@ struct DebugLogView: View {
private let url: URL private let url: URL
private let shareFilename: String
private let timer: AnyPublisher<Date, Never> private let timer: AnyPublisher<Date, Never>
@State private var logLines: [String] = [] @State private var logLines: [String] = []
@ -44,12 +46,11 @@ struct DebugLogView: View {
private let appVersion = Constants.Global.appVersionString private let appVersion = Constants.Global.appVersionString
private let shareFilename = Unlocalized.Issues.Filenames.debugLog init(title: String, url: URL, filename: String, refreshInterval: TimeInterval?) {
init(title: String, url: URL, refreshInterval: TimeInterval?) {
self.title = title self.title = title
self.url = url self.url = url
if let refreshInterval = refreshInterval { shareFilename = filename
if let refreshInterval {
timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common) timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common)
.autoconnect() .autoconnect()
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

@ -135,20 +135,11 @@ private extension DiagnosticsView.OpenVPNView {
} }
func reportIssueView() -> some View { func reportIssueView() -> some View {
let logURL = vpnManager.debugLogURL(forProtocol: vpnProtocol) ReportIssueView(
var metadata: ProviderMetadata?
var lastUpdate: Date?
if let name = providerName {
metadata = providerManager.provider(withName: name)
lastUpdate = providerManager.lastUpdate(name, vpnProtocol: vpnProtocol)
}
return ReportIssueView(
isPresented: $isReportingIssue, isPresented: $isReportingIssue,
vpnProtocol: vpnProtocol, vpnProtocol: vpnProtocol,
logURL: logURL, messageBody: messageBody,
providerMetadata: metadata, logs: logs
lastUpdate: lastUpdate
) )
} }
@ -163,6 +154,40 @@ private extension DiagnosticsView.OpenVPNView {
return cfg.builder(withFallbacks: false) return cfg.builder(withFallbacks: false)
} }
var messageBody: String {
var providerMetadata: ProviderMetadata?
var lastUpdate: Date?
if let name = providerName {
providerMetadata = providerManager.provider(withName: name)
lastUpdate = providerManager.lastUpdate(name, vpnProtocol: vpnProtocol)
}
return Unlocalized.Issues.body(
providerMetadata: providerMetadata,
lastUpdate: lastUpdate,
purchasedProductIdentifiers: productManager.purchasedProductIdentifiers
)
}
var logs: [MailComposerView.Attachment] {
var pairs: [(url: URL, filename: String)] = []
if let appLogURL {
pairs.append((appLogURL, Unlocalized.Issues.Filenames.appLog))
}
if let tunnelLogURL {
pairs.append((tunnelLogURL, Unlocalized.Issues.Filenames.tunnelLog))
}
return pairs.map {
let logContent = $0.url.trailingContent(bytes: Unlocalized.Issues.maxLogBytes)
let attachment = DebugLog(content: logContent).decoratedData()
return MailComposerView.Attachment(
data: attachment,
mimeType: Unlocalized.Issues.Filenames.mime,
fileName: $0.filename
)
}
}
var appLogURL: URL? { var appLogURL: URL? {
Passepartout.shared.logger.logFile Passepartout.shared.logger.logFile
} }
@ -189,9 +214,7 @@ private extension DiagnosticsView.OpenVPNView {
func openReportIssueMailTo() { func openReportIssueMailTo() {
let V = Unlocalized.Issues.self let V = Unlocalized.Issues.self
let body = V.body(V.template, DebugLog(content: "--").decoratedString()) guard let url = URL.mailto(to: V.recipient, subject: V.subject, body: messageBody) else {
guard let url = URL.mailto(to: V.recipient, subject: V.subject, body: body) else {
return return
} }
guard URL.open(url) else { guard URL.open(url) else {

View File

@ -98,6 +98,7 @@ private extension DiagnosticsView.DebugLogGroup {
navigationLink( navigationLink(
withTitle: L10n.Diagnostics.Items.AppLog.title, withTitle: L10n.Diagnostics.Items.AppLog.title,
url: appLogURL, url: appLogURL,
filename: Unlocalized.Issues.Filenames.appLog,
refreshInterval: nil refreshInterval: nil
) )
} }
@ -106,19 +107,22 @@ private extension DiagnosticsView.DebugLogGroup {
navigationLink( navigationLink(
withTitle: Unlocalized.VPN.vpn, withTitle: Unlocalized.VPN.vpn,
url: tunnelLogURL, url: tunnelLogURL,
filename: Unlocalized.Issues.Filenames.tunnelLog,
refreshInterval: refreshInterval refreshInterval: refreshInterval
) )
} }
func navigationLink(withTitle title: String, url: URL?, refreshInterval: TimeInterval?) -> some View { func navigationLink(withTitle title: String, url: URL?, filename: String, refreshInterval: TimeInterval?) -> some View {
NavigationLink(title) { NavigationLink(title) {
url.map { url.map {
DebugLogView( DebugLogView(
title: title, title: title,
url: $0, url: $0,
filename: filename,
refreshInterval: refreshInterval refreshInterval: refreshInterval
) )
} }
}.disabled(url == nil) }
.disabled(url == nil)
} }
} }

View File

@ -37,46 +37,20 @@ struct ReportIssueView: View {
private let messageBody: String private let messageBody: String
private let attachments: [MailComposerView.Attachment] private let logs: [MailComposerView.Attachment]
init( init(
isPresented: Binding<Bool>, isPresented: Binding<Bool>,
vpnProtocol: VPNProtocolType, vpnProtocol: VPNProtocolType,
logURL: URL?, messageBody: String,
providerMetadata: ProviderMetadata? = nil, logs: [MailComposerView.Attachment]
lastUpdate: Date? = nil
) { ) {
_isPresented = isPresented _isPresented = isPresented
toRecipients = [Unlocalized.Issues.recipient] toRecipients = [Unlocalized.Issues.recipient]
subject = Unlocalized.Issues.subject subject = Unlocalized.Issues.subject
self.messageBody = messageBody
let bodyContent = Unlocalized.Issues.template self.logs = logs
var bodyMetadata = "--\n\n"
bodyMetadata += DebugLog(content: "").decoratedString()
if let metadata = providerMetadata {
bodyMetadata += "Provider: \(metadata.fullName)\n"
if let lastUpdate = lastUpdate {
bodyMetadata += "Last updated: \(lastUpdate)\n"
}
bodyMetadata += "\n"
}
bodyMetadata += "--"
messageBody = Unlocalized.Issues.body(bodyContent, bodyMetadata)
var attachments: [MailComposerView.Attachment] = []
if let logURL = logURL {
let logContent = logURL.trailingContent(bytes: Unlocalized.Issues.maxLogBytes)
let attachment = DebugLog(content: logContent).decoratedData()
attachments.append(.init(
data: attachment,
mimeType: Unlocalized.Issues.MIME.debugLog,
fileName: Unlocalized.Issues.Filenames.debugLog
))
}
self.attachments = attachments
} }
var body: some View { var body: some View {
@ -85,7 +59,7 @@ struct ReportIssueView: View {
toRecipients: toRecipients, toRecipients: toRecipients,
subject: subject, subject: subject,
messageBody: messageBody, messageBody: messageBody,
attachments: attachments attachments: logs
) )
} }
} }

View File

@ -48,6 +48,10 @@ public final class ProductManager: NSObject, ObservableObject {
// //
public var purchasedProductIdentifiers: Set<String> {
Set(purchasedFeatures.map(\.rawValue))
}
private var purchasedAppBuild: Int? private var purchasedAppBuild: Int?
private var purchasedFeatures: Set<LocalProduct> private var purchasedFeatures: Set<LocalProduct>

View File

@ -31,7 +31,7 @@ import AppKit
#endif #endif
extension DebugLog { extension DebugLog {
public func decoratedString(_ appName: String, _ appVersion: String) -> String { public static func decoratedMetadataString(_ appName: String, _ appVersion: String) -> String {
let osVersion: String let osVersion: String
let deviceType: String? let deviceType: String?
@ -57,10 +57,20 @@ extension DebugLog {
metadata.append("Device: \(deviceType)") metadata.append("Device: \(deviceType)")
} }
var fullText = metadata.joined(separator: "\n") return metadata.joined(separator: "\n")
fullText += "\n\n" }
fullText += content
return fullText public func decoratedString(_ appName: String, _ appVersion: String) -> String {
[Self.decoratedMetadataString(appName, appVersion), content]
.joined(separator: "\n\n")
}
public static func decoratedMetadataData(_ appName: String, _ appVersion: String) -> Data {
guard let data = decoratedMetadataString(appName, appVersion).data(using: .utf8) else {
assertionFailure("Could not encode log metadata to UTF8?")
return Data()
}
return data
} }
public func decoratedData(_ appName: String, _ appVersion: String) -> Data { public func decoratedData(_ appName: String, _ appVersion: String) -> Data {