Skip to content

Commit

Permalink
Merge branch 'main' into sam/rmf-survey-updates
Browse files Browse the repository at this point in the history
* main:
  Autofill engagement KPIs for pixel reporting (#830)
  Bump C-S-S to 5.17.0 (#828)
  ensure bookmarks can be shown in top hits (#818)
  Subscription refactoring (#815)
  • Loading branch information
samsymons committed May 24, 2024
2 parents 2a4f30c + e1e4364 commit f0550f3
Show file tree
Hide file tree
Showing 46 changed files with 1,661 additions and 521 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/content-scope-scripts",
"state" : {
"revision" : "bb8e7e62104ed6506c7bfd3ef7aa4aca3686ed4f",
"version" : "5.15.0"
"revision" : "fa861c4eccb21d235e34070b208b78bdc32ece08",
"version" : "5.17.0"
}
},
{
Expand Down
16 changes: 15 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ let package = Package(
.library(name: "NetworkProtectionTestUtils", targets: ["NetworkProtectionTestUtils"]),
.library(name: "SecureStorage", targets: ["SecureStorage"]),
.library(name: "Subscription", targets: ["Subscription"]),
.library(name: "SubscriptionTestingUtilities", targets: ["SubscriptionTestingUtilities"]),
.library(name: "History", targets: ["History"]),
.library(name: "Suggestions", targets: ["Suggestions"]),
.library(name: "PixelKit", targets: ["PixelKit"]),
Expand All @@ -43,7 +44,7 @@ let package = Package(
.package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "2.1.1"),
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"),
.package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "5.15.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "5.17.0"),
.package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.6.0"),
.package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"),
.package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"),
Expand Down Expand Up @@ -331,6 +332,12 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "SubscriptionTestingUtilities",
dependencies: [
"Subscription"
]
),
.target(
name: "PixelKit",
swiftSettings: [
Expand Down Expand Up @@ -505,6 +512,13 @@ let package = Package(
"TestUtils",
]
),
.testTarget(
name: "SubscriptionTests",
dependencies: [
"Subscription",
"SubscriptionTestingUtilities",
]
),
.testTarget(
name: "PixelKitTests",
dependencies: [
Expand Down
243 changes: 243 additions & 0 deletions Sources/BrowserServicesKit/Autofill/AutofillPixelReporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
//
// AutofillPixelReporter.swift
//
// Copyright © 2024 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 Foundation
import Persistence
import SecureStorage
import Common

public enum AutofillPixelEvent {
case autofillActiveUser
case autofillEnabledUser
case autofillOnboardedUser
case autofillLoginsStacked
case autofillCreditCardsStacked

enum Parameter {
static let countBucket = "count_bucket"
}
}

public final class AutofillPixelReporter {

enum Keys {
static let autofillSearchDauDateKey = "com.duckduckgo.app.autofill.SearchDauDate"
static let autofillFillDateKey = "com.duckduckgo.app.autofill.FillDate"
static let autofillOnboardedUserKey = "com.duckduckgo.app.autofill.OnboardedUser"
}

enum BucketName: String {
case none
case few
case some
case many
case lots
}

private enum EventType {
case fill
case searchDAU
}

private let userDefaults: UserDefaults
private let eventMapping: EventMapping<AutofillPixelEvent>
private var secureVault: (any AutofillSecureVault)?
private var reporter: SecureVaultReporting?
// Third party password manager
private let passwordManager: PasswordManager?
private var installDate: Date?

private var autofillSearchDauDate: Date? { userDefaults.object(forKey: Keys.autofillSearchDauDateKey) as? Date ?? .distantPast }
private var autofillFillDate: Date? { userDefaults.object(forKey: Keys.autofillFillDateKey) as? Date ?? .distantPast }
private var autofillOnboardedUser: Bool { userDefaults.object(forKey: Keys.autofillOnboardedUserKey) as? Bool ?? false }

public init(userDefaults: UserDefaults,
eventMapping: EventMapping<AutofillPixelEvent>,
secureVault: (any AutofillSecureVault)? = nil,
reporter: SecureVaultReporting? = nil,
passwordManager: PasswordManager? = nil,
installDate: Date? = nil
) {
self.userDefaults = userDefaults
self.eventMapping = eventMapping
self.secureVault = secureVault
self.reporter = reporter
self.passwordManager = passwordManager
self.installDate = installDate

createNotificationObservers()
}

public func resetStoreDefaults() {
userDefaults.set(Date.distantPast, forKey: Keys.autofillSearchDauDateKey)
userDefaults.set(Date.distantPast, forKey: Keys.autofillFillDateKey)
userDefaults.set(false, forKey: Keys.autofillOnboardedUserKey)
}

private func createNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSearchDAU), name: .searchDAU, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveFillEvent), name: .autofillFillEvent, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil)
}

@objc
private func didReceiveSearchDAU() {
guard let autofillSearchDauDate = autofillSearchDauDate, !Date.isSameDay(Date(), autofillSearchDauDate) else {
return
}

userDefaults.set(Date(), forKey: Keys.autofillSearchDauDateKey)

firePixelsFor(.searchDAU)
}

@objc
private func didReceiveFillEvent() {
guard let autofillFillDate = autofillFillDate, !Date.isSameDay(Date(), autofillFillDate) else {
return
}

userDefaults.set(Date(), forKey: Keys.autofillFillDateKey)

firePixelsFor(.fill)
}

@objc
private func didReceiveSaveEvent() {
guard !autofillOnboardedUser else {
return
}

if shouldFireOnboardedUserPixel() {
eventMapping.fire(.autofillOnboardedUser)
}
}

private func firePixelsFor(_ type: EventType) {
if shouldFireActiveUserPixel() {
eventMapping.fire(.autofillActiveUser)

if let accountsCountBucket = getAccountsCountBucket() {
eventMapping.fire(.autofillLoginsStacked, parameters: [AutofillPixelEvent.Parameter.countBucket: accountsCountBucket])
}

if let cardsCount = try? vault()?.creditCardsCount() {
eventMapping.fire(.autofillCreditCardsStacked, parameters: [AutofillPixelEvent.Parameter.countBucket: creditCardsBucketNameFrom(count: cardsCount)])
}
}

switch type {
case .searchDAU:
if shouldFireEnabledUserPixel() {
eventMapping.fire(.autofillEnabledUser)
}
default:
break
}
}

private func getAccountsCountBucket() -> String? {
if let passwordManager = passwordManager, passwordManager.isEnabled {
// if a user is using a password manager we can't get a count of their passwords so we are assuming they are likely to have a lot of passwords saved
return BucketName.lots.rawValue
} else if let accountsCount = try? vault()?.accountsCount() {
return accountsBucketNameFrom(count: accountsCount)
}
return nil
}

private func shouldFireActiveUserPixel() -> Bool {
let today = Date()
if Date.isSameDay(today, autofillSearchDauDate) && Date.isSameDay(today, autofillFillDate) {
return true
}
return false
}

private func shouldFireEnabledUserPixel() -> Bool {
if Date.isSameDay(Date(), autofillSearchDauDate) {
if let passwordManager = passwordManager, passwordManager.isEnabled {
return true
} else if let count = try? vault()?.accountsCount(), count >= 10 {
return true
}
}
return false
}

private func shouldFireOnboardedUserPixel() -> Bool {
guard !autofillOnboardedUser, let installDate = installDate else {
return false
}

let pastWeek = Date().addingTimeInterval(.days(-7))

if installDate >= pastWeek {
if let passwordManager = passwordManager, passwordManager.isEnabled {
return true
} else if let count = try? vault()?.accountsCount(), count > 0 {
userDefaults.set(true, forKey: Keys.autofillOnboardedUserKey)
return true
}
} else {
userDefaults.set(true, forKey: Keys.autofillOnboardedUserKey)
}

return false
}

private func vault() -> (any AutofillSecureVault)? {
if secureVault == nil {
secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: reporter)
}
return secureVault
}

private func accountsBucketNameFrom(count: Int) -> String {
if count == 0 {
return BucketName.none.rawValue
} else if count < 4 {
return BucketName.few.rawValue
} else if count < 11 {
return BucketName.some.rawValue
} else if count < 50 {
return BucketName.many.rawValue
} else {
return BucketName.lots.rawValue
}
}

private func creditCardsBucketNameFrom(count: Int) -> String {
if count == 0 {
return BucketName.none.rawValue
} else if count < 4 {
return BucketName.some.rawValue
} else {
return BucketName.many.rawValue
}
}

}

public extension NSNotification.Name {

static let autofillFillEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.AutofillFillEvent")
static let autofillSaveEvent: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.AutofillSaveEvent")
static let searchDAU: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.SearchDAU")

}
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,10 @@ extension AutofillUserScript {
case autofillPrivateAddress = "autofill_private_address"
}

private enum IdentityPixelName: String {
case autofillIdentity = "autofill_identity"
}

/// The pixel name sent by the JS layer. This name does not include the platform on which it was sent.
private let originalPixelName: String

Expand All @@ -739,6 +743,13 @@ extension AutofillUserScript {
}
}

public var isIdentityPixel: Bool {
if case IdentityPixelName.autofillIdentity.rawValue = originalPixelName {
return true
}
return false
}

public var pixelName: String {
switch originalPixelName {
case EmailPixelName.autofillPersonalAddress.rawValue:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public protocol AutofillDatabaseProvider: SecureStorageDatabaseProvider {
func deleteIdentityForIdentityId(_ identityId: Int64) throws

func creditCards() throws -> [SecureVaultModels.CreditCard]
func creditCardsCount() throws -> Int
func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard?
@discardableResult
func storeCreditCard(_ creditCard: SecureVaultModels.CreditCard) throws -> Int64
Expand Down Expand Up @@ -557,6 +558,13 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro
}
}

public func creditCardsCount() throws -> Int {
let count = try db.read {
try SecureVaultModels.CreditCard.fetchCount($0)
}
return count
}

public func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? {
try db.read {
return try SecureVaultModels.CreditCard.fetchOne($0, sql: """
Expand Down
16 changes: 8 additions & 8 deletions Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public protocol AutofillSecureVault: SecureVault {
func deleteIdentityFor(identityId: Int64) throws

func creditCards() throws -> [SecureVaultModels.CreditCard]
func creditCardsCount() throws -> Int
func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard?
func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard?
@discardableResult
Expand Down Expand Up @@ -231,15 +232,8 @@ public class DefaultAutofillSecureVault<T: AutofillDatabaseProvider>: AutofillSe
}

public func accountsCount() throws -> Int {
lock.lock()
defer {
lock.unlock()
}

do {
return try executeThrowingDatabaseOperation {
return try self.providers.database.accountsCount()
} catch {
throw SecureStorageError.databaseError(cause: error)
}
}

Expand Down Expand Up @@ -526,6 +520,12 @@ public class DefaultAutofillSecureVault<T: AutofillDatabaseProvider>: AutofillSe
}
}

public func creditCardsCount() throws -> Int {
return try executeThrowingDatabaseOperation {
return try self.providers.database.creditCardsCount()
}
}

public func creditCardFor(id: Int64) throws -> SecureVaultModels.CreditCard? {
return try executeThrowingDatabaseOperation {
guard var card = try self.providers.database.creditCardForCardId(id) else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate {

if autofilldata.trigger == .passwordGeneration {
autogeneratedPassword = data.credentials?.autogenerated ?? false
NotificationCenter.default.post(name: .autofillFillEvent, object: nil)
}

// Account for cases when the user has manually changed an autogenerated password or private email
Expand Down Expand Up @@ -616,6 +617,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate {
var account = SecureVaultModels.WebsiteAccount(username: username, domain: domain, lastUsed: Date())
let credentials = try? vault?.storeWebsiteCredentials(SecureVaultModels.WebsiteCredentials(account: account, password: password))
account.id = String(credentials ?? -1)
NotificationCenter.default.post(name: .autofillSaveEvent, object: nil, userInfo: nil)
return account
}

Expand Down
Loading

0 comments on commit f0550f3

Please sign in to comment.