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
key = "APP_TYPE"
value = "2"
isEnabled = "NO">
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "LOG_LEVEL"

View File

@ -27,6 +27,14 @@ import Foundation
import PassepartoutLibrary
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 {
decoratedString(Constants.Global.appName, Constants.Global.appVersionString)
}

View File

@ -74,33 +74,54 @@ enum Unlocalized {
static let subject = "\(appName) - Report issue"
static func body(_ description: String, _ metadata: String) -> String {
"Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
}
static let template = "description of the issue: "
static let bodySentinel = "<replace this with a description of the issue>"
static let maxLogBytes = UInt64(20000)
enum Filenames {
static var debugLog: String {
static let mime = "text/plain"
static var appLog: String {
let fmt = DateFormatter()
fmt.dateFormat = "yyyyMMdd-HHmmss"
let iso = fmt.string(from: Date())
return "debug-\(iso).txt"
return "app-\(iso).txt"
}
static let configuration = "profile.ovpn"
// static let configuration = "profile.ovpn.txt"
static let template = "description of the issue: "
static var tunnelLog: String {
let fmt = DateFormatter()
fmt.dateFormat = "yyyyMMdd-HHmmss"
let iso = fmt.string(from: Date())
return "tunnel-\(iso).txt"
}
}
enum MIME {
static let debugLog = "text/plain"
private static func rawBody(_ description: String, _ metadata: String) -> String {
"Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
}
// static let configuration = "application/x-openvpn-profile"
static let configuration = "text/plain"
static func body(
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 shareFilename: String
private let timer: AnyPublisher<Date, Never>
@State private var logLines: [String] = []
@ -44,12 +46,11 @@ struct DebugLogView: View {
private let appVersion = Constants.Global.appVersionString
private let shareFilename = Unlocalized.Issues.Filenames.debugLog
init(title: String, url: URL, refreshInterval: TimeInterval?) {
init(title: String, url: URL, filename: String, refreshInterval: TimeInterval?) {
self.title = title
self.url = url
if let refreshInterval = refreshInterval {
shareFilename = filename
if let refreshInterval {
timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common)
.autoconnect()
.eraseToAnyPublisher()

View File

@ -135,20 +135,11 @@ private extension DiagnosticsView.OpenVPNView {
}
func reportIssueView() -> some View {
let logURL = vpnManager.debugLogURL(forProtocol: vpnProtocol)
var metadata: ProviderMetadata?
var lastUpdate: Date?
if let name = providerName {
metadata = providerManager.provider(withName: name)
lastUpdate = providerManager.lastUpdate(name, vpnProtocol: vpnProtocol)
}
return ReportIssueView(
ReportIssueView(
isPresented: $isReportingIssue,
vpnProtocol: vpnProtocol,
logURL: logURL,
providerMetadata: metadata,
lastUpdate: lastUpdate
messageBody: messageBody,
logs: logs
)
}
@ -163,6 +154,40 @@ private extension DiagnosticsView.OpenVPNView {
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? {
Passepartout.shared.logger.logFile
}
@ -189,9 +214,7 @@ private extension DiagnosticsView.OpenVPNView {
func openReportIssueMailTo() {
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: body) else {
guard let url = URL.mailto(to: V.recipient, subject: V.subject, body: messageBody) else {
return
}
guard URL.open(url) else {

View File

@ -98,6 +98,7 @@ private extension DiagnosticsView.DebugLogGroup {
navigationLink(
withTitle: L10n.Diagnostics.Items.AppLog.title,
url: appLogURL,
filename: Unlocalized.Issues.Filenames.appLog,
refreshInterval: nil
)
}
@ -106,19 +107,22 @@ private extension DiagnosticsView.DebugLogGroup {
navigationLink(
withTitle: Unlocalized.VPN.vpn,
url: tunnelLogURL,
filename: Unlocalized.Issues.Filenames.tunnelLog,
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) {
url.map {
DebugLogView(
title: title,
url: $0,
filename: filename,
refreshInterval: refreshInterval
)
}
}.disabled(url == nil)
}
.disabled(url == nil)
}
}

View File

@ -37,46 +37,20 @@ struct ReportIssueView: View {
private let messageBody: String
private let attachments: [MailComposerView.Attachment]
private let logs: [MailComposerView.Attachment]
init(
isPresented: Binding<Bool>,
vpnProtocol: VPNProtocolType,
logURL: URL?,
providerMetadata: ProviderMetadata? = nil,
lastUpdate: Date? = nil
messageBody: String,
logs: [MailComposerView.Attachment]
) {
_isPresented = isPresented
toRecipients = [Unlocalized.Issues.recipient]
subject = Unlocalized.Issues.subject
let bodyContent = Unlocalized.Issues.template
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
self.messageBody = messageBody
self.logs = logs
}
var body: some View {
@ -85,7 +59,7 @@ struct ReportIssueView: View {
toRecipients: toRecipients,
subject: subject,
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 purchasedFeatures: Set<LocalProduct>

View File

@ -31,23 +31,23 @@ import AppKit
#endif
extension DebugLog {
public func decoratedString(_ appName: String, _ appVersion: String) -> String {
public static func decoratedMetadataString(_ appName: String, _ appVersion: String) -> String {
let osVersion: String
let deviceType: String?
#if os(iOS) || os(tvOS)
#if os(iOS) || os(tvOS)
let device: UIDevice = .current
osVersion = "\(device.systemName) \(device.systemVersion)"
#if targetEnvironment(macCatalyst)
#if targetEnvironment(macCatalyst)
deviceType = "\(device.model) (Catalyst)"
#else
#else
deviceType = "\(device.model) (\(device.userInterfaceIdiom.debugDescription))"
#endif
#else
#endif
#else
let os = ProcessInfo().operatingSystemVersion
osVersion = "macOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
deviceType = nil
#endif
#endif
var metadata = [
"App: \(appName) \(appVersion)",
@ -57,10 +57,20 @@ extension DebugLog {
metadata.append("Device: \(deviceType)")
}
var fullText = metadata.joined(separator: "\n")
fullText += "\n\n"
fullText += content
return fullText
return metadata.joined(separator: "\n")
}
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 {