diff --git a/Cartfile.resolved b/Cartfile.resolved index f04cb4142a..a72976c1cf 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ -github "apple/swift-protobuf" "1.14.0" +github "apple/swift-protobuf" "1.18.0" github "jrendel/SwiftKeychainWrapper" "4.0.1" diff --git a/components/logins/ios/Logins/LoginsStorage.swift b/components/logins/ios/Logins/LoginsStorage.swift index a0a740eea4..4d844270e3 100644 --- a/components/logins/ios/Logins/LoginsStorage.swift +++ b/components/logins/ios/Logins/LoginsStorage.swift @@ -10,6 +10,9 @@ import UIKit #if canImport(MozillaRustComponents) import MozillaRustComponents #endif +#if canImport(Glean) + @_exported import Glean +#endif typealias LoginsStoreError = LoginsStorageError @@ -73,26 +76,6 @@ open class LoginsStorage { } } - open func migrateLogins( - newDbPath: String, - newDbEncKey: String, - sqlCipherDbPath: String, - sqlCipherEncKey: String, - sqlCipherSalt: String - ) throws -> String { - return try queue.sync { - // last param is the "salt" which is only used on iOS. - - return try self.migrateLogins( - newDbPath: newDbPath, - newDbEncKey: newDbEncKey, - sqlCipherDbPath: sqlCipherDbPath, - sqlCipherEncKey: sqlCipherEncKey, - sqlCipherSalt: sqlCipherSalt - ) - } - } - /// Update `login` in the database. If `login.id` does not refer to a known /// login, then this throws `LoginStoreError.NoSuchRecord`. open func update(id: String, login: LoginEntry, encryptionKey: String) throws -> EncryptedLogin { @@ -142,3 +125,61 @@ open class LoginsStorage { } } } + +public func migrateLoginsWithMetrics( + path: String, + newEncryptionKey: String, + sqlcipherPath: String, + sqlcipherKey: String, + salt: String +) -> Bool { + var didMigrationSucceed = false + + do { + let metrics = try migrateLogins( + path: path, + newEncryptionKey: newEncryptionKey, + sqlcipherPath: sqlcipherPath, + sqlcipherKey: sqlcipherKey, + salt: salt + ) + didMigrationSucceed = true + + recordMigrationMetrics(jsonString: metrics) + } catch let err as NSError { + GleanMetrics.LoginsStoreMigration.errors.add(err.localizedDescription) + } + return didMigrationSucceed +} + +func recordMigrationMetrics(jsonString: String) { + guard + let data = jsonString.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data, options: []), + let metrics = json as? [String: Any] + else { + return + } + + if let processed = metrics["num_processed"] as? Int32 { + GleanMetrics.LoginsStoreMigration.numProcessed.add(processed) + } + + if let succeeded = metrics["num_succeeded"] as? Int32 { + GleanMetrics.LoginsStoreMigration.numSucceeded.add(succeeded) + } + + if let failed = metrics["num_failed"] as? Int32 { + GleanMetrics.LoginsStoreMigration.numFailed.add(failed) + } + + if let duration = metrics["total_duration"] as? UInt64 { + GleanMetrics.LoginsStoreMigration.totalDuration.setRawNanos(duration * 1_000_000) + } + + if let errors = metrics["errors"] as? [String] { + for error in errors { + GleanMetrics.LoginsStoreMigration.errors.add(error) + } + } +} diff --git a/components/logins/ios/metrics.yaml b/components/logins/ios/metrics.yaml new file mode 100644 index 0000000000..997db98c9c --- /dev/null +++ b/components/logins/ios/metrics.yaml @@ -0,0 +1,103 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This file defines the metrics that will be gathered for the "logins" +# storage component. +# These are emitted for all users of the component. Additional metrics +# specific to the *syncing* of logins are defined in a separate "sync_ping" +# package. +# +# Changes to these metrics require data review, which should take into +# consideration the following known consumers of the logins component +# Android bindings: +# +# * Fenix for Andriod + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +logins_store_migration: + + # Migrations from sqlcipher to sqlite. + num_processed: + type: counter + description: > + The total number of login records processed by the migration + bugs: + - https://github.com/mozilla/application-services/issues/4064 + - https://github.com/mozilla/application-services/issues/4102 + data_reviews: + - https://github.com/mozilla/application-services/issues/4467 + data_sensitivity: + - technical + - interaction + notification_emails: + - synced-client-integrations@mozilla.com + expires: 2022-06-30 + + num_succeeded: + type: counter + description: > + The total number of login records successfully migrated + bugs: + - https://github.com/mozilla/application-services/issues/4064 + - https://github.com/mozilla/application-services/issues/4102 + data_reviews: + - https://github.com/mozilla/application-services/issues/4467 + data_sensitivity: + - technical + - interaction + notification_emails: + - synced-client-integrations@mozilla.com + expires: 2022-06-30 + + num_failed: + type: counter + description: > + The total number of login records which failed to migrate + bugs: + - https://github.com/mozilla/application-services/issues/4064 + - https://github.com/mozilla/application-services/issues/4102 + data_reviews: + - https://github.com/mozilla/application-services/issues/4467 + data_sensitivity: + - technical + - interaction + notification_emails: + - synced-client-integrations@mozilla.com + expires: 2022-06-30 + + total_duration: + type: timespan + time_unit: millisecond + description: > + How long the migration tool + bugs: + - https://github.com/mozilla/application-services/issues/4064 + - https://github.com/mozilla/application-services/issues/4102 + data_reviews: + - https://github.com/mozilla/application-services/issues/4467 + data_sensitivity: + - technical + - interaction + notification_emails: + - synced-client-integrations@mozilla.com + expires: 2022-06-30 + + # Note glean limits this to 20 items each with a max length of 50 utf8 chars. + errors: + type: string_list + description: > + Errors discovered in the migration. + bugs: + - https://github.com/mozilla/application-services/issues/4064 + - https://github.com/mozilla/application-services/issues/4102 + data_reviews: + - https://github.com/mozilla/application-services/issues/4467 + data_sensitivity: + - technical + - interaction + notification_emails: + - synced-client-integrations@mozilla.com + expires: 2022-06-30 diff --git a/components/rc_log/ios/RustLog.swift b/components/rc_log/ios/RustLog.swift index 0aec5320a1..9307cb3509 100644 --- a/components/rc_log/ios/RustLog.swift +++ b/components/rc_log/ios/RustLog.swift @@ -225,7 +225,7 @@ private class RustLogState { } func disable() { - guard let adapter = self.adapter else { + guard let adapter = adapter else { return } self.adapter = nil diff --git a/megazords/ios/MozillaAppServices.xcodeproj/project.pbxproj b/megazords/ios/MozillaAppServices.xcodeproj/project.pbxproj index 8768d09f44..1bfcdfa594 100644 --- a/megazords/ios/MozillaAppServices.xcodeproj/project.pbxproj +++ b/megazords/ios/MozillaAppServices.xcodeproj/project.pbxproj @@ -758,6 +758,7 @@ "$(SRCROOT)/../../components/external/glean/glean-core/metrics.yaml", "$(SRCROOT)/../../components/external/glean/glean-core/pings.yaml", "$(SRCROOT)/../../components/nimbus/metrics.yaml", + "$(SRCROOT)/../../components/logins/ios/metrics.yaml", ); name = "Generate Glean metrics"; outputFileListPaths = ( diff --git a/megazords/ios/MozillaAppServicesTests/LoginsTests.swift b/megazords/ios/MozillaAppServicesTests/LoginsTests.swift index 5bccab1cde..aac830f1e0 100644 --- a/megazords/ios/MozillaAppServicesTests/LoginsTests.swift +++ b/megazords/ios/MozillaAppServicesTests/LoginsTests.swift @@ -6,5 +6,36 @@ import XCTest @testable import MozillaAppServices class LoginsTests: XCTestCase { - // TODO: Add migration tests here + var storage: LoginsStorage! + + override func setUp() { + Glean.shared.resetGlean(clearStores: true) + } + + override func tearDown() { + // This method is called after the invocation of each test method in the class. + } + + func testMigrationMetrics() throws { + let json = """ + {"fixup_phase":{ + "num_processed":0,"num_succeeded":0,"num_failed":0,"total_duration":0,"errors":[] + }, + "insert_phase":{"num_processed":0,"num_succeeded":0,"num_failed":0,"total_duration":0,"errors":[] + }, + "num_processed":3,"num_succeeded":1,"num_failed":2,"total_duration":53,"errors":[ + "Invalid login: Login has illegal field: Origin is Malformed", + "Invalid login: Origin is empty" + ]} + """ + + recordMigrationMetrics(jsonString: json) + XCTAssertEqual(3, try GleanMetrics.LoginsStoreMigration.numProcessed.testGetValue()) + XCTAssertEqual(2, try GleanMetrics.LoginsStoreMigration.numFailed.testGetValue()) + XCTAssertEqual(1, try GleanMetrics.LoginsStoreMigration.numSucceeded.testGetValue()) + XCTAssertEqual(53, try GleanMetrics.LoginsStoreMigration.totalDuration.testGetValue()) + + // Note the truncation of the first error string. + XCTAssertEqual(["Invalid login: Login has illegal field: Origin is ", "Invalid login: Origin is empty"], try GleanMetrics.LoginsStoreMigration.errors.testGetValue()) + } }