Skip to content

Commit

Permalink
Merge branch 'main' into dominik/xcode-16
Browse files Browse the repository at this point in the history
# By Daniel Bernal (4) and others
# Via Daniel Bernal (1) and others
* main:
  Pixel retrying (#3358)
  Remove `voiceSearchHelper` from `AppDependencyProvider` (#3452)
  Update AutoClearSettingsViewController to use DI for app settings (#3448)
  Bump BSK (#3441)
  Remove `SubscriptionFeatureAvailability` from `AppDependencyProvider` (#3447)
  Release 7.141.0-2 (#3451)
  Do not notify the FE on experiment activation (#3450)
  point to bsk branch (#3444)
  bump bsk for content blocker rules fix (#3445)
  speculative fix for set bars visibility crashes (#3442)
  Release 7.141.0-1 (#3443)
  Fix browsing menu bottom offset when bar location set to bottom (#3440)
  Properly handle responses that should trigger download action (#3407)
  Add Events Firing for Phishing Detection Settings: Point to BSK (#3423)
  DuckPlayer: Temporary Fix for Watch In Youtube (#3437)
  Add 'Open in New Tab' support for DuckPlayer (#3431)
  update BSK dependency (#3434)
  Release 7.141.0-0 (#3435)
  Add error handling to contrainer removal (#3424)
  Prevent autofill prompt crash for edge case where a context menu is also visible on screen (#3417)

# Conflicts:
#	DuckDuckGo.xcodeproj/project.pbxproj
#	DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Loading branch information
samsymons committed Oct 21, 2024
2 parents f67e7f2 + 3932989 commit 7d6ad39
Show file tree
Hide file tree
Showing 84 changed files with 2,588 additions and 3,158 deletions.
2 changes: 1 addition & 1 deletion Configuration/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MARKETING_VERSION = 7.140.0
MARKETING_VERSION = 7.141.0
4 changes: 2 additions & 2 deletions Core/AppPrivacyConfigurationDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import BrowserServicesKit
final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider {

public struct Constants {
public static let embeddedDataETag = "\"6330c36f7ff354d26f32ee951e0a972e\""
public static let embeddedDataSHA = "40f77d6db1db544f06740b3290c5a72e5f03f706d17c9c0e05b13cd9255f2778"
public static let embeddedDataETag = "\"4ffe7d2b6c8e252d0289b1398cc2685d\""
public static let embeddedDataSHA = "9795ade4fdbc474688250f2ecfa097e917feea21a54fd97c524b851245d170e8"
}

public var embeddedDataEtag: String {
Expand Down
9 changes: 6 additions & 3 deletions Core/DailyPixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ public final class DailyPixel {

}

private enum Constant {
public enum Constant {

static let dailyPixelStorageIdentifier = "com.duckduckgo.daily.pixel.storage"
public static let dailyPixelSuffixes = (dailySuffix: "_daily", countSuffix: "_count")
public static let legacyDailyPixelSuffixes = (dailySuffix: "_d", countSuffix: "_c")

}

Expand Down Expand Up @@ -80,6 +82,7 @@ public final class DailyPixel {
/// This means a pixel will get sent twice the first time it is called per-day, and subsequent calls that day will only send the `_c` variant.
/// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected.
public static func fireDailyAndCount(pixel: Pixel.Event,
pixelNameSuffixes: (dailySuffix: String, countSuffix: String) = Constant.dailyPixelSuffixes,
error: Swift.Error? = nil,
withAdditionalParameters params: [String: String] = [:],
includedParameters: [Pixel.QueryParameters] = [.appVersion],
Expand All @@ -91,7 +94,7 @@ public final class DailyPixel {

if !hasBeenFiredToday(forKey: key, dailyPixelStore: dailyPixelStore) {
pixelFiring.fire(
pixelNamed: pixel.name + "_d",
pixelNamed: pixel.name + pixelNameSuffixes.dailySuffix,
withAdditionalParameters: params,
includedParameters: includedParameters,
onComplete: onDailyComplete
Expand All @@ -105,7 +108,7 @@ public final class DailyPixel {
newParams.appendErrorPixelParams(error: error)
}
pixelFiring.fire(
pixelNamed: pixel.name + "_c",
pixelNamed: pixel.name + pixelNameSuffixes.countSuffix,
withAdditionalParameters: newParams,
includedParameters: includedParameters,
onComplete: onCountComplete
Expand Down
13 changes: 12 additions & 1 deletion Core/DailyPixelFiring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,22 @@
//

import Foundation
import Persistence

public protocol DailyPixelFiring {
static func fireDaily(_ pixel: Pixel.Event,
withAdditionalParameters params: [String: String])


static func fireDailyAndCount(pixel: Pixel.Event,
pixelNameSuffixes: (dailySuffix: String, countSuffix: String),
error: Swift.Error?,
withAdditionalParameters params: [String: String],
includedParameters: [Pixel.QueryParameters],
pixelFiring: PixelFiring.Type,
dailyPixelStore: KeyValueStoring,
onDailyComplete: @escaping (Swift.Error?) -> Void,
onCountComplete: @escaping (Swift.Error?) -> Void)

static func fireDaily(_ pixel: Pixel.Event)
}

Expand Down
3 changes: 3 additions & 0 deletions Core/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public enum FeatureFlag: String {
case history
case newTabPageSections
case duckPlayer
case duckPlayerOpenInNewTab
case sslCertificatesBypass
case syncPromotionBookmarks
case syncPromotionPasswords
Expand Down Expand Up @@ -83,6 +84,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .remoteDevelopment(.feature(.newTabPageImprovements))
case .duckPlayer:
return .remoteReleasable(.subfeature(DuckPlayerSubfeature.enableDuckPlayer))
case .duckPlayerOpenInNewTab:
return .remoteReleasable(.subfeature(DuckPlayerSubfeature.openInNewTab))
case .sslCertificatesBypass:
return .remoteReleasable(.subfeature(SslCertificatesSubfeature.allowBypass))
case .syncPromotionBookmarks:
Expand Down
291 changes: 291 additions & 0 deletions Core/PersistentPixel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
//
// PersistentPixel.swift
// DuckDuckGo
//
// 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 os.log
import Networking
import Persistence

public protocol PersistentPixelFiring {
func fire(pixel: Pixel.Event,
error: Swift.Error?,
includedParameters: [Pixel.QueryParameters],
withAdditionalParameters params: [String: String],
onComplete: @escaping (Error?) -> Void)

func fireDailyAndCount(pixel: Pixel.Event,
pixelNameSuffixes: (dailySuffix: String, countSuffix: String),
error: Swift.Error?,
withAdditionalParameters params: [String: String],
includedParameters: [Pixel.QueryParameters],
completion: @escaping ((dailyPixelStorageError: Error?, countPixelStorageError: Error?)) -> Void)

func sendQueuedPixels(completion: @escaping (PersistentPixelStorageError?) -> Void)
}

public final class PersistentPixel: PersistentPixelFiring {

enum Constants {
static let lastProcessingDateKey = "com.duckduckgo.ios.persistent-pixel.last-processing-timestamp"

#if DEBUG
static let minimumProcessingInterval: TimeInterval = .minutes(1)
#else
static let minimumProcessingInterval: TimeInterval = .hours(1)
#endif
}

private let pixelFiring: PixelFiring.Type
private let dailyPixelFiring: DailyPixelFiring.Type
private let persistentPixelStorage: PersistentPixelStoring
private let lastProcessingDateStorage: KeyValueStoring
private let calendar: Calendar
private let dateGenerator: () -> Date
private let workQueue = DispatchQueue(label: "Persistent Pixel Retry Queue")

private let dateFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return formatter
}()

public convenience init() {
self.init(pixelFiring: Pixel.self,
dailyPixelFiring: DailyPixel.self,
persistentPixelStorage: DefaultPersistentPixelStorage(),
lastProcessingDateStorage: UserDefaults.standard)
}

init(pixelFiring: PixelFiring.Type,
dailyPixelFiring: DailyPixelFiring.Type,
persistentPixelStorage: PersistentPixelStoring,
lastProcessingDateStorage: KeyValueStoring,
calendar: Calendar = .current,
dateGenerator: @escaping () -> Date = { Date() }) {
self.pixelFiring = pixelFiring
self.dailyPixelFiring = dailyPixelFiring
self.persistentPixelStorage = persistentPixelStorage
self.lastProcessingDateStorage = lastProcessingDateStorage
self.calendar = calendar
self.dateGenerator = dateGenerator
}

// MARK: - Pixel Firing

public func fire(pixel: Pixel.Event,
error: Swift.Error? = nil,
includedParameters: [Pixel.QueryParameters] = [.appVersion],
withAdditionalParameters additionalParameters: [String: String] = [:],
onComplete: @escaping (Error?) -> Void = { _ in }) {
let fireDate = dateGenerator()
let dateString = dateFormatter.string(from: fireDate)
var additionalParameters = additionalParameters
additionalParameters[PixelParameters.originalPixelTimestamp] = dateString

Logger.general.debug("Firing persistent pixel named \(pixel.name)")

pixelFiring.fire(pixel: pixel,
error: error,
includedParameters: includedParameters,
withAdditionalParameters: additionalParameters) { pixelFireError in
if pixelFireError != nil {
do {
if let error {
additionalParameters.appendErrorPixelParams(error: error)
}

try self.persistentPixelStorage.append(pixels: [
PersistentPixelMetadata(eventName: pixel.name,
additionalParameters: additionalParameters,
includedParameters: includedParameters)
])

onComplete(nil)
} catch {
onComplete(error)
}
}
}
}

public func fireDailyAndCount(pixel: Pixel.Event,
pixelNameSuffixes: (dailySuffix: String, countSuffix: String) = DailyPixel.Constant.dailyPixelSuffixes,
error: Swift.Error? = nil,
withAdditionalParameters additionalParameters: [String: String],
includedParameters: [Pixel.QueryParameters] = [.appVersion],
completion: @escaping ((dailyPixelStorageError: Error?, countPixelStorageError: Error?)) -> Void = { _ in }) {
let dispatchGroup = DispatchGroup()

dispatchGroup.enter() // onDailyComplete
dispatchGroup.enter() // onCountComplete

var dailyPixelStorageError: Error?
var countPixelStorageError: Error?

let fireDate = dateGenerator()
let dateString = dateFormatter.string(from: fireDate)
var additionalParameters = additionalParameters
additionalParameters[PixelParameters.originalPixelTimestamp] = dateString

Logger.general.debug("Firing persistent daily/count pixel named \(pixel.name)")

dailyPixelFiring.fireDailyAndCount(
pixel: pixel,
pixelNameSuffixes: pixelNameSuffixes,
error: error,
withAdditionalParameters: additionalParameters,
includedParameters: includedParameters,
pixelFiring: Pixel.self,
dailyPixelStore: DailyPixel.storage,
onDailyComplete: { dailyError in
if let dailyError, (dailyError as? DailyPixel.Error) != .alreadyFired {
do {
if let error { additionalParameters.appendErrorPixelParams(error: error) }
Logger.general.debug("Saving persistent daily pixel named \(pixel.name)")
try self.persistentPixelStorage.append(pixels: [
PersistentPixelMetadata(eventName: pixel.name + pixelNameSuffixes.dailySuffix,
additionalParameters: additionalParameters,
includedParameters: includedParameters)
])
} catch {
dailyPixelStorageError = error
}
}

dispatchGroup.leave()
}, onCountComplete: { countError in
if countError != nil {
do {
if let error { additionalParameters.appendErrorPixelParams(error: error) }
Logger.general.debug("Saving persistent count pixel named \(pixel.name)")
try self.persistentPixelStorage.append(pixels: [
PersistentPixelMetadata(eventName: pixel.name + pixelNameSuffixes.countSuffix,
additionalParameters: additionalParameters,
includedParameters: includedParameters)
])
} catch {
countPixelStorageError = error
}
}

dispatchGroup.leave()
}
)

dispatchGroup.notify(queue: .global()) {
completion((dailyPixelStorageError: dailyPixelStorageError, countPixelStorageError: countPixelStorageError))
}
}

// MARK: - Queue Processing

public func sendQueuedPixels(completion: @escaping (PersistentPixelStorageError?) -> Void) {
workQueue.async {
if let lastProcessingDate = self.lastProcessingDateStorage.object(forKey: Constants.lastProcessingDateKey) as? Date {
let threshold = self.dateGenerator().addingTimeInterval(-Constants.minimumProcessingInterval)
if threshold <= lastProcessingDate {
completion(nil)
return
}
}

self.lastProcessingDateStorage.set(self.dateGenerator(), forKey: Constants.lastProcessingDateKey)

do {
let queuedPixels = try self.persistentPixelStorage.storedPixels()

if queuedPixels.isEmpty {
completion(nil)
return
}

Logger.general.debug("Persistent pixel retrying \(queuedPixels.count, privacy: .public) pixels")

self.fire(queuedPixels: queuedPixels) { pixelIDsToRemove in
Logger.general.debug("Persistent pixel retrying done, \(pixelIDsToRemove.count, privacy: .public) pixels successfully sent")

do {
try self.persistentPixelStorage.remove(pixelsWithIDs: pixelIDsToRemove)
completion(nil)
} catch {
completion(PersistentPixelStorageError.writeError(error))
}
}
} catch {
completion(PersistentPixelStorageError.readError(error))
}
}
}

// MARK: - Private

/// Sends queued pixels and calls the completion handler with those that should be removed.
private func fire(queuedPixels: [PersistentPixelMetadata], completion: @escaping (Set<UUID>) -> Void) {
let dispatchGroup = DispatchGroup()

let pixelIDsAccessQueue = DispatchQueue(label: "Failed Pixel Retry Attempt Metadata Queue")
var pixelIDsToRemove: Set<UUID> = []
let currentDate = dateGenerator()
let date28DaysAgo = calendar.date(byAdding: .day, value: -28, to: currentDate)

for pixelMetadata in queuedPixels {
if let sendDateString = pixelMetadata.timestamp, let sendDate = dateFormatter.date(from: sendDateString), let date28DaysAgo {
if sendDate < date28DaysAgo {
pixelIDsAccessQueue.sync {
_ = pixelIDsToRemove.insert(pixelMetadata.id)
}
continue
}
} else {
// If we don't have a timestamp for some reason, ignore the retry - retries are only useful if they have a timestamp attached.
// It's not expected that this will ever happen, so an assertion failure is used to report it when debugging.
assertionFailure("Did not find a timestamp for pixel \(pixelMetadata.eventName)")
pixelIDsAccessQueue.sync {
_ = pixelIDsToRemove.insert(pixelMetadata.id)
}
continue
}

var pixelParameters = pixelMetadata.additionalParameters
pixelParameters[PixelParameters.retriedPixel] = "1"

dispatchGroup.enter()

pixelFiring.fire(
pixelNamed: pixelMetadata.eventName,
withAdditionalParameters: pixelParameters,
includedParameters: pixelMetadata.includedParameters,
onComplete: { error in
if error == nil {
pixelIDsAccessQueue.sync {
_ = pixelIDsToRemove.insert(pixelMetadata.id)
}
}

dispatchGroup.leave()
}
)
}

dispatchGroup.notify(queue: .global()) {
completion(pixelIDsToRemove)
}
}

}
Loading

0 comments on commit 7d6ad39

Please sign in to comment.