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:
parent
f13b4d0768
commit
dde2d22eed
|
@ -127,7 +127,7 @@
|
|||
<EnvironmentVariable
|
||||
key = "APP_TYPE"
|
||||
value = "2"
|
||||
isEnabled = "NO">
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "LOG_LEVEL"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue