From 04e69ebceaa86e9e2d2d68141e8d358725b95971 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 22 Dec 2023 12:59:32 +0600 Subject: [PATCH] Report browser version in Pixel on data import failure (#1873) Task/Issue URL: https://app.asana.com/0/1199230911884351/1205627400731993/f - Adds source browser version parameter to the Data import failure Pixel --- DuckDuckGo.xcodeproj/project.pbxproj | 9 ++ .../Extensions/NSWorkspaceExtension.swift | 8 ++ .../Bookmarks/Safari/SafariDataImporter.swift | 2 +- .../DataImport/ChromiumPreferences.swift | 23 ++++ DuckDuckGo/DataImport/DataImport.swift | 87 +++++++++---- .../FirefoxCompatibilityPreferences.swift | 51 ++++++++ .../Chromium/ChromiumDataImporter.swift | 2 +- .../Logins/Firefox/FirefoxDataImporter.swift | 2 +- .../Model/DataImportViewModel.swift | 15 ++- DuckDuckGo/DataImport/ThirdPartyBrowser.swift | 15 ++- DuckDuckGo/Statistics/PixelEvent.swift | 6 +- DuckDuckGo/Statistics/PixelParameters.swift | 11 +- .../PixelKit/PixelKit+Parameters.swift | 1 + .../DataImport/BrowserProfileTests.swift | 117 +++++++++++++++++- .../DataImport/DataImportViewModelTests.swift | 2 +- 15 files changed, 310 insertions(+), 41 deletions(-) create mode 100644 DuckDuckGo/DataImport/FirefoxCompatibilityPreferences.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9bebf5a189..8524e61cb3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -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 */; }; @@ -4195,6 +4199,7 @@ B6B2400D28083B49001B8F3A /* WebViewContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewContainerView.swift; sourceTree = ""; }; B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSScreenExtension.swift; sourceTree = ""; }; B6B4D1C42B0B3B5400C26286 /* DataImportReportModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportReportModel.swift; sourceTree = ""; }; + B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxCompatibilityPreferences.swift; sourceTree = ""; }; B6B4D1CE2B0E0DD000C26286 /* DataImportNoDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportNoDataView.swift; sourceTree = ""; }; B6B5F57E2B024105008DB58A /* DataImportSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportSummaryView.swift; sourceTree = ""; }; B6B5F5832B03580A008DB58A /* RequestFilePermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestFilePermissionView.swift; sourceTree = ""; }; @@ -5400,6 +5405,7 @@ 4B5A4F4B27F3A5AA008FBD88 /* NSNotificationName+DataImport.swift */, 4B59024726B3673600489384 /* ThirdPartyBrowser.swift */, 4B7A57CE279A4EF300B1C70E /* ChromiumPreferences.swift */, + B6B4D1C92B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift */, 4BB99CF326FE191E001E4761 /* Bookmarks */, 4B723DF126B0002B00E14D75 /* Logins */, 4B723DEC26B0002B00E14D75 /* View */, @@ -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 */, @@ -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 */, @@ -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 */, diff --git a/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift b/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift index 294c9d7401..8e7c81a0ac 100644 --- a/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSWorkspaceExtension.swift @@ -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] ?? [] + } + } diff --git a/DuckDuckGo/DataImport/Bookmarks/Safari/SafariDataImporter.swift b/DuckDuckGo/DataImport/Bookmarks/Safari/SafariDataImporter.swift index 04473b5742..5da0fbb2d0 100644 --- a/DuckDuckGo/DataImport/Bookmarks/Safari/SafariDataImporter.swift +++ b/DuckDuckGo/DataImport/Bookmarks/Safari/SafariDataImporter.swift @@ -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)) } } diff --git a/DuckDuckGo/DataImport/ChromiumPreferences.swift b/DuckDuckGo/DataImport/ChromiumPreferences.swift index acfcea22eb..b0318927ee 100644 --- a/DuckDuckGo/DataImport/ChromiumPreferences.swift +++ b/DuckDuckGo/DataImport/ChromiumPreferences.swift @@ -28,10 +28,20 @@ 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 @@ -39,6 +49,13 @@ struct ChromiumPreferences: Decodable { 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) { @@ -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 + } + } diff --git a/DuckDuckGo/DataImport/DataImport.swift b/DuckDuckGo/DataImport/DataImport.swift index 9495409727..fc8e6094d9 100644 --- a/DuckDuckGo/DataImport/DataImport.swift +++ b/DuckDuckGo/DataImport/DataImport.swift @@ -129,6 +129,23 @@ enum DataImport { } } + func installedAppsMajorVersionDescription(selectedProfile: BrowserProfile?) -> String? { + let installedVersions: Set + 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 { @@ -226,19 +243,49 @@ 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 @@ -246,7 +293,17 @@ enum DataImport { 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 { @@ -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 @@ -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 { diff --git a/DuckDuckGo/DataImport/FirefoxCompatibilityPreferences.swift b/DuckDuckGo/DataImport/FirefoxCompatibilityPreferences.swift new file mode 100644 index 0000000000..993e2eb25b --- /dev/null +++ b/DuckDuckGo/DataImport/FirefoxCompatibilityPreferences.swift @@ -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) + } + +} diff --git a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift index 1531b9f77c..a387ac7d34 100644 --- a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift +++ b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift @@ -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)) } } diff --git a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift index 3160979618..a6b87ee62b 100644 --- a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift +++ b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift @@ -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)) } } diff --git a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift index bf3e4df183..7d982588ad 100644 --- a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift @@ -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)) } } @@ -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 @@ -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 @@ -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 } } diff --git a/DuckDuckGo/DataImport/ThirdPartyBrowser.swift b/DuckDuckGo/DataImport/ThirdPartyBrowser.swift index 68539c3137..8692ac74e3 100644 --- a/DuckDuckGo/DataImport/ThirdPartyBrowser.swift +++ b/DuckDuckGo/DataImport/ThirdPartyBrowser.swift @@ -135,6 +135,19 @@ enum ThirdPartyBrowser: CaseIterable { return nil } + var installedAppsVersions: Set? { + let versions = bundleIdentifiers.all + .reduce(into: Set()) { 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"]) @@ -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) } diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 22b205d371..26bfd253ce 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -101,7 +101,7 @@ extension Pixel { case dailyOsVersionCounter - case dataImportFailed(source: DataImport.Source, error: any DataImportError) + case dataImportFailed(source: DataImport.Source, sourceVersion: String?, error: any DataImportError) case formAutofilled(kind: FormAutofillKind) case autofillItemSaved(kind: FormAutofillKind) @@ -373,9 +373,9 @@ extension Pixel.Event { case .dailyOsVersionCounter: return "m_mac_daily-os-version-counter" - case .dataImportFailed(source: let source, error: let error) where error.action == .favicons: + case .dataImportFailed(source: let source, sourceVersion: _, error: let error) where error.action == .favicons: return "m_mac_favicon-import-failed_\(source)" - case .dataImportFailed(source: let source, error: let error): + case .dataImportFailed(source: let source, sourceVersion: _, error: let error): return "m_mac_data-import-failed_\(error.action)_\(source)" case .formAutofilled(kind: let kind): diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 311fee433b..468a1b7f93 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Foundation import PixelKit extension Pixel.Event { @@ -26,7 +27,6 @@ extension Pixel.Event { return event.parameters case .debug(event: let debugEvent, error: let error): - var params = error?.pixelParameters ?? [:] if case let .assertionFailure(message, file, line) = debugEvent { @@ -37,8 +37,13 @@ extension Pixel.Event { return params - case .dataImportFailed(source: _, error: let error): - return error.pixelParameters + case .dataImportFailed(source: _, sourceVersion: let version, error: let error): + var params = error.pixelParameters + + if let version { + params[PixelKit.Parameters.sourceBrowserVersion] = version + } + return params case .launchInitial(let cohort): return [PixelKit.Parameters.experimentCohort: cohort] diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index 9673f0dae0..b3a482cd68 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -31,6 +31,7 @@ public extension PixelKit { public static let errorDesc = "d" public static let errorCount = "c" public static let errorSource = "error_source" + public static let sourceBrowserVersion = "source_browser_version" public static let underlyingErrorCode = "ue" public static let underlyingErrorDomain = "underlyingErrorDomain" public static let underlyingErrorDesc = "ud" diff --git a/UnitTests/DataImport/BrowserProfileTests.swift b/UnitTests/DataImport/BrowserProfileTests.swift index 6e274a9e39..0c9192e823 100644 --- a/UnitTests/DataImport/BrowserProfileTests.swift +++ b/UnitTests/DataImport/BrowserProfileTests.swift @@ -89,7 +89,7 @@ class BrowserProfileTests: XCTestCase { let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) XCTAssertEqual(profile.profileName, "User Name (profile@duck.com)") - XCTAssertNotNil(profile.chromiumPreferences?.profileName) + XCTAssertNotNil(profile.profilePreferences?.profileName) } func testWhenGettingProfileName_AndProfileHasNoDetectedChromiumName_ThenDetectedNameIsUsed() { @@ -111,7 +111,7 @@ class BrowserProfileTests: XCTestCase { let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) XCTAssertEqual(profile.profileName, "ChromeProfile") - XCTAssertNotNil(profile.chromiumPreferences?.profileName) + XCTAssertNotNil(profile.profilePreferences?.profileName) } func testWhenGettingProfileName_AndChromiumPreferencesAreDetected_AndProfileNameIsSystemProfile_ThenProfileHasDefaultProfileName() { @@ -132,11 +132,122 @@ class BrowserProfileTests: XCTestCase { let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) XCTAssertEqual(profile.profileName, "System Profile") - XCTAssertNil(profile.chromiumPreferences) + XCTAssertEqual(profile.profilePreferences?.profileName, "ChromeProfile") } private func profile(named name: String) -> URL { return mockURL.appendingPathComponent(name) } + func testWhenLastChromiumVersionIsPresentInProfile_InstalledAppsReturnsMajorVersion() { + let profileURL = profile(named: "System Profile") + let fileStore = FileStoreMock() + + let json = """ + { + "profile": { + "created_by_version": "118.0.5993.54" + }, + "extensions": { + "last_chrome_version": "120.0.1111.42" + } + } + """ + + fileStore.storage["Preferences"] = json.utf8data + fileStore.directoryStorage[profileURL.absoluteString] = ["Preferences"] + + let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) + + XCTAssertEqual(profile.appVersion, "118.0.5993.54") + XCTAssertEqual(profile.installedAppsMajorVersionDescription(), "118") + XCTAssertEqual(DataImport.Source.chrome.installedAppsMajorVersionDescription(selectedProfile: profile), "118") + } + + func testWhenLastOperaVersionIsPresent_InstalledAppsReturnsMajorVersion() { + let profileURL = profile(named: "System Profile") + let fileStore = FileStoreMock() + + let json = """ + { + "profile": { + }, + "extensions": { + "last_opera_version": "117.0.5938.13" + } + } + """ + + fileStore.storage["Preferences"] = json.utf8data + fileStore.directoryStorage[profileURL.absoluteString] = ["Preferences"] + + let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) + + XCTAssertEqual(profile.appVersion, "117.0.5938.13") + XCTAssertEqual(profile.installedAppsMajorVersionDescription(), "117") + XCTAssertEqual(DataImport.Source.chrome.installedAppsMajorVersionDescription(selectedProfile: profile), "117") + } + + func testWhenLastChromiumVersionIsNotPresentInProfile_CreatedByVersionIsReturned() { + let profileURL = profile(named: "System Profile") + let fileStore = FileStoreMock() + + let json = """ + { + "profile": { + "created_by_version": "118.0.5993.54" + } + } + """ + + fileStore.storage["Preferences"] = json.utf8data + fileStore.directoryStorage[profileURL.absoluteString] = ["Preferences"] + + let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) + + XCTAssertEqual(profile.appVersion, "118.0.5993.54") + XCTAssertEqual(profile.installedAppsMajorVersionDescription(), "118") + XCTAssertEqual(DataImport.Source.chrome.installedAppsMajorVersionDescription(selectedProfile: profile), "118") + } + + func testWhenFirefoxLastVersionIsPresentInProfile_LastVersionIsReturned() { + let profileURL = profile(named: "Firefox.default") + let fileStore = FileStoreMock() + + let conf = """ + [Compatibility] + LastVersion = 118.0.1_20230927232528/20230927232528 + LastOSABI=Darwin_aarch64-gcc3 + LastPlatformDir=/Applications/Firefox.app/Contents/Resources + LastAppDir=/Applications/Firefox.app/Contents/Resources/browser + """ + + fileStore.storage["compatibility.ini"] = conf.utf8data + fileStore.directoryStorage[profileURL.absoluteString] = ["compatibility.ini"] + + let profile = DataImport.BrowserProfile(browser: .firefox, profileURL: profileURL, fileStore: fileStore) + + XCTAssertEqual(profile.appVersion, "118.0.1_20230927232528/20230927232528") + XCTAssertEqual(profile.installedAppsMajorVersionDescription(), "118") + XCTAssertEqual(DataImport.Source.chrome.installedAppsMajorVersionDescription(selectedProfile: profile), "118") + } + + func testWhenNoVersionInProfile_InstalledAppsVersionsReturned() { + let profileURL = profile(named: "System Profile") + let fileStore = FileStoreMock() + + let json = """ + { "profile": {} } + """ + + fileStore.storage["Preferences"] = json.utf8data + fileStore.directoryStorage[profileURL.absoluteString] = ["Preferences"] + + let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) + + XCTAssertNil(profile.appVersion) + // TODO: dependency + XCTAssertEqual(profile.installedAppsMajorVersionDescription()?.sorted(), DataImport.Source.chrome.installedAppsMajorVersionDescription(selectedProfile: profile)?.sorted()) + } + } diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift index 7f09d54e00..ee6c5ad4b4 100644 --- a/UnitTests/DataImport/DataImportViewModelTests.swift +++ b/UnitTests/DataImport/DataImportViewModelTests.swift @@ -1526,7 +1526,7 @@ import XCTest XCTAssertTrue(report.error.localizedDescription.contains(error.localizedDescription)) } } - XCTAssertEqual(report.importSourceDescription, Source.safari.importSourceName) + XCTAssertEqual(report.importSourceDescription, Source.safari.importSourceName + " " + "\(SafariVersionReader.getMajorVersion() ?? 0)") XCTAssertEqual(report.appVersion, "\(AppVersion.shared.versionNumber)") XCTAssertEqual(report.osVersion, "\(ProcessInfo.processInfo.operatingSystemVersion)") XCTAssertEqual(report.retryNumber, 2)