Skip to content

Commit

Permalink
Report browser version in Pixel on data import failure (#1873)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1199230911884351/1205627400731993/f
- Adds source browser version parameter to the Data import failure Pixel
  • Loading branch information
mallexxx authored Dec 22, 2023
1 parent 761a444 commit 04e69eb
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 41 deletions.
9 changes: 9 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2851,6 +2851,10 @@
B6B4D1C52B0B3B5400C26286 /* DataImportReportModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */; };
B6B4D1C62B0B3B5400C26286 /* DataImportReportModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */; };
B6B4D1C82B0B3B5400C26286 /* DataImportReportModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */; };
B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */; };
B6B4D1CB2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */; };
B6B4D1CC2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */; };
B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */; };
B6B4D1CF2B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1CE2B0E0DD000C26286 /* DataImportNoDataView.swift */; };
B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1CE2B0E0DD000C26286 /* DataImportNoDataView.swift */; };
B6B4D1D22B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B4D1CE2B0E0DD000C26286 /* DataImportNoDataView.swift */; };
Expand Down Expand Up @@ -4195,6 +4199,7 @@
B6B2400D28083B49001B8F3A /* WebViewContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewContainerView.swift; sourceTree = "<group>"; };
B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSScreenExtension.swift; sourceTree = "<group>"; };
B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportReportModel.swift; sourceTree = "<group>"; };
B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxCompatibilityPreferences.swift; sourceTree = "<group>"; };
B6B4D1CE2B0E0DD000C26286 /* DataImportNoDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportNoDataView.swift; sourceTree = "<group>"; };
B6B5F57E2B024105008DB58A /* DataImportSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportSummaryView.swift; sourceTree = "<group>"; };
B6B5F5832B03580A008DB58A /* RequestFilePermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestFilePermissionView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5400,6 +5405,7 @@
4B5A4F4B27F3A5AA008FBD88 /* NSNotificationName+DataImport.swift */,
4B59024726B3673600489384 /* ThirdPartyBrowser.swift */,
4B7A57CE279A4EF300B1C70E /* ChromiumPreferences.swift */,
B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */,
4BB99CF326FE191E001E4761 /* Bookmarks */,
4B723DF126B0002B00E14D75 /* Logins */,
4B723DEC26B0002B00E14D75 /* View */,
Expand Down Expand Up @@ -9633,6 +9639,7 @@
3706FBBD293F65D500E42796 /* Preferences.swift in Sources */,
3706FBBE293F65D500E42796 /* DownloadListViewModel.swift in Sources */,
3706FBBF293F65D500E42796 /* BookmarkManagementDetailViewController.swift in Sources */,
B6B4D1CB2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */,
3706FBC0293F65D500E42796 /* CSVImporter.swift in Sources */,
3706FBC1293F65D500E42796 /* StartupPreferences.swift in Sources */,
3706FBC2293F65D500E42796 /* MainMenu.swift in Sources */,
Expand Down Expand Up @@ -10369,6 +10376,7 @@
4B9579542AC7AE700062CA31 /* DownloadListStore.swift in Sources */,
4B9579552AC7AE700062CA31 /* Logging.swift in Sources */,
4B9579562AC7AE700062CA31 /* CrashReportPromptPresenter.swift in Sources */,
B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */,
4B9579572AC7AE700062CA31 /* BWCredential.swift in Sources */,
4B9579582AC7AE700062CA31 /* PreferencesRootView.swift in Sources */,
4B9579592AC7AE700062CA31 /* AppStateChangedPublisher.swift in Sources */,
Expand Down Expand Up @@ -11564,6 +11572,7 @@
EA0BA3A9272217E6002A0B6C /* ClickToLoadUserScript.swift in Sources */,
AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */,
85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */,
B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */,
AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */,
B696AFFB2AC5924800C93203 /* FileLineError.swift in Sources */,
85CC1D7B26A05ECF0062F04E /* PasswordManagementItemListModel.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,12 @@ extension NSWorkspace {
return missionControlWindows.count > allScreenSizes.count
}

@available(macOS, obsoleted: 14.0, message: "This needs to be removed as it‘s no longer necessary.")
@nonobjc func urls(forApplicationsWithBundleId bundleId: String) -> [URL] {
if #available(macOS 12.0, *) {
return self.urlsForApplications(withBundleIdentifier: bundleId)
}
return LSCopyApplicationURLsForBundleIdentifier(bundleId as CFString, nil)?.takeRetainedValue() as? [URL] ?? []
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ final class SafariDataImporter: DataImporter {
await faviconManager.handleFaviconsByDocumentUrl(faviconsByDocument)

case .failure(let error):
Pixel.fire(.dataImportFailed(source: source, error: error))
Pixel.fire(.dataImportFailed(source: source, sourceVersion: profile.installedAppsMajorVersionDescription(), error: error))
}
}

Expand Down
23 changes: 23 additions & 0 deletions DuckDuckGo/DataImport/ChromiumPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,34 @@ struct ChromiumPreferences: Decodable {
let name: String?
let createdByVersion: String?
}
struct Extensions: Decodable {
let lastChromeVersion: String?
let lastOperaVersion: String?
}

enum Constants {
static let chromiumPreferencesFileName = "Preferences"
}

let accountInfo: [AccountInfo]?
let profile: Profile

let extensions: Extensions?

init(from data: Data) throws {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

self = try decoder.decode(Self.self, from: data)
}

init(profileURL: URL, fileStore: FileStore = FileManager.default) throws {
guard let preferencesData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.chromiumPreferencesFileName)) else {
throw CocoaError(.fileReadUnknown)
}
try self.init(from: preferencesData)
}

var profileName: String? {
for account in accountInfo ?? [] {
switch (account.fullName, account.email) {
Expand All @@ -54,4 +71,10 @@ struct ChromiumPreferences: Decodable {
return profile.name
}

var appVersion: String? {
// profile.createdByVersion updated on Chrome launch;
// if it‘s missing - check extensions.last_chrome_version or last_opera_version - for Opera[GX]
profile.createdByVersion ?? extensions?.lastChromeVersion ?? extensions?.lastOperaVersion
}

}
87 changes: 65 additions & 22 deletions DuckDuckGo/DataImport/DataImport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ enum DataImport {
}
}

func installedAppsMajorVersionDescription(selectedProfile: BrowserProfile?) -> String? {
let installedVersions: Set<String>
if let appVersion = selectedProfile?.appVersion, !appVersion.isEmpty {
installedVersions = [appVersion]
} else if let versions = ThirdPartyBrowser.browser(for: self)?.installedAppsVersions {
installedVersions = versions
} else {
return nil
}
return Set(installedVersions.map {
// get major version
$0.components(separatedBy: ".")[0] // [0] component is always there even if no "."
}.sorted())
// list installed browsers major versions separated
.joined(separator: "; ")
}

}

enum DataType: String, Hashable, CaseIterable, CustomStringConvertible {
Expand Down Expand Up @@ -226,27 +243,67 @@ enum DataImport {
struct BrowserProfile: Comparable {

enum Constants {
static let chromiumPreferencesFileName = "Preferences"
static let chromiumSystemProfileName = "System Profile"
}

let profileURL: URL
var profileName: String {
return chromiumPreferences?.profileName ?? fallbackProfileName
if profileURL.lastPathComponent == Constants.chromiumSystemProfileName {
return Constants.chromiumSystemProfileName
}

return profilePreferences?.profileName ?? fallbackProfileName
}

let browser: ThirdPartyBrowser
private let fileStore: FileStore
private let fallbackProfileName: String
let chromiumPreferences: ChromiumPreferences?

enum ProfilePreferences {
case chromium(ChromiumPreferences)
case firefox(FirefoxCompatibilityPreferences)

var appVersion: String? {
switch self {
case .chromium(let preferences): preferences.appVersion
case .firefox(let preferences): preferences.lastVersion
}
}

var profileName: String? {
switch self {
case .chromium(let preferences): preferences.profileName
case .firefox: nil
}
}

var isChromium: Bool {
if case .chromium = self { true } else { false }
}
}
let profilePreferences: ProfilePreferences?

var appVersion: String? {
profilePreferences?.appVersion
}

init(browser: ThirdPartyBrowser, profileURL: URL, fileStore: FileStore = FileManager.default) {
self.browser = browser
self.fileStore = fileStore
self.profileURL = profileURL

self.fallbackProfileName = Self.getDefaultProfileName(at: profileURL)
self.chromiumPreferences = Self.getChromiumProfilePreferences(at: profileURL, fileStore: fileStore)

switch browser {
case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi, .yandex:
self.profilePreferences = (try? ChromiumPreferences(profileURL: profileURL, fileStore: fileStore))
.map(ProfilePreferences.chromium)
case .firefox, .tor:
self.profilePreferences = (try? FirefoxCompatibilityPreferences(profileURL: profileURL, fileStore: fileStore))
.map(ProfilePreferences.firefox)
case .bitwarden, .safari, .safariTechnologyPreview, .lastPass, .onePassword7, .onePassword8:
self.profilePreferences = nil
}
}

enum ProfileDataItemValidationResult {
Expand Down Expand Up @@ -329,24 +386,6 @@ enum DataImport {
return profileURL.lastPathComponent.components(separatedBy: ".").last ?? profileURL.lastPathComponent
}

private static func getChromiumProfilePreferences(at profileURL: URL, fileStore: FileStore) -> ChromiumPreferences? {
guard let profileDirectoryContents = try? fileStore.directoryContents(at: profileURL.path) else {
return nil
}

guard profileURL.lastPathComponent != Constants.chromiumSystemProfileName else {
return nil
}

if profileDirectoryContents.contains(Constants.chromiumPreferencesFileName),
let preferencesData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.chromiumPreferencesFileName)),
let preferences = try? ChromiumPreferences(from: preferencesData) {
return preferences
}

return nil
}

static func < (lhs: DataImport.BrowserProfile, rhs: DataImport.BrowserProfile) -> Bool {
// first sort by profiles folder name if multiple profiles folders are present (Chrome, Chrome Canary…)
let profilesDirName1 = lhs.profileURL.deletingLastPathComponent().lastPathComponent
Expand All @@ -361,6 +400,10 @@ enum DataImport {
static func == (lhs: DataImport.BrowserProfile, rhs: DataImport.BrowserProfile) -> Bool {
return lhs.profileURL == rhs.profileURL
}

func installedAppsMajorVersionDescription() -> String? {
self.browser.importSource.installedAppsMajorVersionDescription(selectedProfile: self)
}
}

enum ErrorType: String, CustomStringConvertible, CaseIterable {
Expand Down
51 changes: 51 additions & 0 deletions DuckDuckGo/DataImport/FirefoxCompatibilityPreferences.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// FirefoxCompatibilityPreferences.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Common
import Foundation

struct FirefoxCompatibilityPreferences {

enum Constants {
static let firefoxCompatibilityPreferencesFileName = "compatibility.ini"
}

let lastVersion: String?

private static let lastVersionRegex = regex("^\\s*LastVersion\\s*=\\s*(\\S+)\\s*$")

init(from data: Data) {
var lastVersion: String?
data.utf8String()?.enumerateLines(invoking: { line, stop in
guard let match = Self.lastVersionRegex.firstMatch(in: line, options: [], range: line.fullRange),
let range = Range(match.range(at: 1), in: line) else { return }
lastVersion = String(line[range])
stop = true
})

self.lastVersion = lastVersion
}

init(profileURL: URL, fileStore: FileStore = FileManager.default) throws {
guard let preferencesData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.firefoxCompatibilityPreferencesFileName)) else {
throw CocoaError(.fileReadUnknown)
}
self.init(from: preferencesData)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ internal class ChromiumDataImporter: DataImporter {
await faviconManager.handleFaviconsByDocumentUrl(faviconsByDocument)

case .failure(let error):
Pixel.fire(.dataImportFailed(source: source, error: error))
Pixel.fire(.dataImportFailed(source: source, sourceVersion: profile.installedAppsMajorVersionDescription(), error: error))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ internal class FirefoxDataImporter: DataImporter {
await faviconManager.handleFaviconsByDocumentUrl(faviconsByDocument)

case .failure(let error):
Pixel.fire(.dataImportFailed(source: source, error: error))
Pixel.fire(.dataImportFailed(source: source, sourceVersion: profile.installedAppsMajorVersionDescription(), error: error))
}
}

Expand Down
15 changes: 10 additions & 5 deletions DuckDuckGo/DataImport/Model/DataImportViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ struct DataImportViewModel {
// switch to file import of the failed data type displaying successful import results
nextScreen = .fileImport(dataType: dataType, summary: Set(summary.filter({ $0.value.isSuccess }).keys))
}
Pixel.fire(.dataImportFailed(source: importSource, error: error))
Pixel.fire(.dataImportFailed(source: importSource, sourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile), error: error))
}
}

Expand Down Expand Up @@ -283,7 +283,7 @@ struct DataImportViewModel {
switch error {
// chromium user denied keychain prompt error
case let error as ChromiumLoginReader.ImportError where error.type == .userDeniedKeychainPrompt:
Pixel.fire(.dataImportFailed(source: importSource, error: error))
Pixel.fire(.dataImportFailed(source: importSource, sourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile), error: error))
// stay on the same screen
return true

Expand All @@ -305,7 +305,7 @@ struct DataImportViewModel {
break
}
log("file read no permission for \(url.path)")
Pixel.fire(.dataImportFailed(source: importSource, error: importError))
Pixel.fire(.dataImportFailed(source: importSource, sourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile), error: importError))
screen = .getReadPermission(url)
return true

Expand Down Expand Up @@ -710,8 +710,13 @@ extension DataImportViewModel {

var reportModel: DataImportReportModel {
get {
DataImportReportModel(importSource: importSource, error: summarizedError, text: userReportText, retryNumber: retryNumber)
} set {
DataImportReportModel(importSource: importSource,
importSourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile),
error: summarizedError,
text: userReportText,
retryNumber: retryNumber)
}
set {
userReportText = newValue.text
}
}
Expand Down
15 changes: 14 additions & 1 deletion DuckDuckGo/DataImport/ThirdPartyBrowser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ enum ThirdPartyBrowser: CaseIterable {
return nil
}

var installedAppsVersions: Set<String>? {
let versions = bundleIdentifiers.all
.reduce(into: Set<String>()) { result, bundleId in
for url in NSWorkspace.shared.urls(forApplicationsWithBundleId: bundleId) {
guard let version = ApplicationVersionReader.getVersion(of: url.path),
!version.isEmpty else { continue }
result.insert(version)
}
}
guard !versions.isEmpty else { return nil }
return versions
}

private var bundleIdentifiers: BundleIdentifiers {
switch self {
case .brave: return BundleIdentifiers(productionBundleID: "com.brave.Browser", relatedBundleIDs: ["com.brave.Browser.nightly"])
Expand Down Expand Up @@ -208,7 +221,7 @@ enum ThirdPartyBrowser: CaseIterable {
}

let filteredProfiles = potentialProfiles.filter {
$0.chromiumPreferences != nil
$0.profilePreferences?.isChromium == true
|| $0.profileName == DataImport.BrowserProfileList.Constants.chromiumDefaultProfileName
|| $0.profileName.hasPrefix(DataImport.BrowserProfileList.Constants.chromiumProfilePrefix)
}
Expand Down
Loading

0 comments on commit 04e69eb

Please sign in to comment.