mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-02-16 12:52:11 +00:00
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
|
<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"
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -31,23 +31,23 @@ 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?
|
||||||
|
|
||||||
#if os(iOS) || os(tvOS)
|
#if os(iOS) || os(tvOS)
|
||||||
let device: UIDevice = .current
|
let device: UIDevice = .current
|
||||||
osVersion = "\(device.systemName) \(device.systemVersion)"
|
osVersion = "\(device.systemName) \(device.systemVersion)"
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
deviceType = "\(device.model) (Catalyst)"
|
deviceType = "\(device.model) (Catalyst)"
|
||||||
#else
|
#else
|
||||||
deviceType = "\(device.model) (\(device.userInterfaceIdiom.debugDescription))"
|
deviceType = "\(device.model) (\(device.userInterfaceIdiom.debugDescription))"
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
let os = ProcessInfo().operatingSystemVersion
|
let os = ProcessInfo().operatingSystemVersion
|
||||||
osVersion = "macOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
|
osVersion = "macOS \(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
|
||||||
deviceType = nil
|
deviceType = nil
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var metadata = [
|
var metadata = [
|
||||||
"App: \(appName) \(appVersion)",
|
"App: \(appName) \(appVersion)",
|
||||||
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user