Skip to content

Commit

Permalink
Merge branch 'main' into dominik/xcode-16
Browse files Browse the repository at this point in the history
* main:
  Make sure to apply only relevant (last requested) rules on attribution (#1059)
  Update TLD list  (#1018)
  • Loading branch information
samsymons committed Nov 8, 2024
2 parents 1f2e0e4 + 6319be3 commit 2d60823
Show file tree
Hide file tree
Showing 3 changed files with 1,748 additions and 1,333 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,27 @@ public protocol AdClickAttributionLogicDelegate: AnyObject {

public class AdClickAttributionLogic {

public enum State {
public enum State: CustomDebugStringConvertible {

case noAttribution
case preparingAttribution(vendor: String, session: SessionInfo, completionBlocks: [(() -> Void)])
case preparingAttribution(vendor: String, session: SessionInfo, requestID: UUID, completionBlocks: [(() -> Void)])
case activeAttribution(vendor: String, session: SessionInfo, rules: ContentBlockerRulesManager.Rules)

var isActiveAttribution: Bool {
if case .activeAttribution = self { return true }
return false
}

public var debugDescription: String {
switch self {
case .noAttribution:
return "noAttribution"
case .preparingAttribution(let vendor, _, let requestID, let completionBlocks):
return "preparingAttribution(\(vendor), \(requestID), blocks: \(completionBlocks.count))"
case .activeAttribution(let vendor, _, _):
return "activeAttribution(\(vendor))"
}
}
}

public struct SessionInfo {
Expand All @@ -63,7 +74,15 @@ public class AdClickAttributionLogic {
self.eventReporting?.fire(.adAttributionPageLoads, parameters: [AdClickAttributionEvents.Parameters.count: String(count)])
})

public private(set) var state = State.noAttribution
public var debugID: String {
ObjectIdentifier(self).debugDescription
}

public private(set) var state = State.noAttribution {
willSet {
Logger.contentBlocking.debug("<\(self.debugID)> will set state from \(self.state.debugDescription) to \(newValue.debugDescription)")
}
}

private var registerFirstActivity = false

Expand Down Expand Up @@ -94,7 +113,7 @@ public class AdClickAttributionLogic {
switch state {
case .noAttribution:
self.state = state
case .preparingAttribution(let vendor, let info, _):
case .preparingAttribution(let vendor, let info, _, _):
requestAttribution(forVendor: vendor,
attributionStartedAt: info.attributionStartedAt)
case .activeAttribution(_, let sessionInfo, _):
Expand All @@ -106,10 +125,11 @@ public class AdClickAttributionLogic {
}

public func onRulesChanged(latestRules: [ContentBlockerRulesManager.Rules]) {
Logger.contentBlocking.debug("<\(self.debugID)> Responding to RulesChanged event")
switch state {
case .noAttribution:
applyRules()
case .preparingAttribution(let vendor, _, let completionBlocks):
case .preparingAttribution(let vendor, _, _, let completionBlocks):
requestAttribution(forVendor: vendor, completionBlocks: completionBlocks)
case .activeAttribution(let vendor, _, _):
requestAttribution(forVendor: vendor)
Expand Down Expand Up @@ -145,19 +165,20 @@ public class AdClickAttributionLogic {
switch state {
case .noAttribution:
completion()
case .preparingAttribution(let vendor, let session, var completionBlocks):
Logger.contentBlocking.debug("Suspending provisional navigation...")
case .preparingAttribution(let vendor, let session, let id, var completionBlocks):
Logger.contentBlocking.debug("<\(self.debugID)> Suspending provisional navigation...")
completionBlocks.append(completion)
state = .preparingAttribution(vendor: vendor,
session: session,
requestID: id,
completionBlocks: completionBlocks)
case .activeAttribution(_, let session, _):
if currentTime.timeIntervalSince(session.attributionStartedAt) >= featureConfig.totalExpiration {
Logger.contentBlocking.debug("Attribution has expired - total expiration")
Logger.contentBlocking.debug("<\(self.debugID)> Attribution has expired - total expiration")
disableAttribution()
} else if let leftAttributionContextAt = session.leftAttributionContextAt,
currentTime.timeIntervalSince(leftAttributionContextAt) >= featureConfig.navigationExpiration {
Logger.contentBlocking.debug("Attribution has expired - navigational expiration")
Logger.contentBlocking.debug("<\(self.debugID)> Attribution has expired - navigational expiration")
disableAttribution()
}
completion()
Expand All @@ -183,23 +204,23 @@ public class AdClickAttributionLogic {
}

if currentTime.timeIntervalSince(session.attributionStartedAt) >= featureConfig.totalExpiration {
Logger.contentBlocking.debug("Attribution has expired - total expiration")
Logger.contentBlocking.debug("<\(self.debugID)> Attribution has expired - total expiration")
disableAttribution()
return
}

if let leftAttributionContextAt = session.leftAttributionContextAt {
if currentTime.timeIntervalSince(leftAttributionContextAt) >= featureConfig.navigationExpiration {
Logger.contentBlocking.debug("Attribution has expired - navigational expiration")
Logger.contentBlocking.debug("<\(self.debugID)> Attribution has expired - navigational expiration")
disableAttribution()
} else if tld.eTLDplus1(host) == vendor {
Logger.contentBlocking.debug("Refreshing navigational duration for attribution")
Logger.contentBlocking.debug("<\(self.debugID)> Refreshing navigational duration for attribution")
state = .activeAttribution(vendor: vendor,
session: SessionInfo(start: session.attributionStartedAt),
rules: rules)
}
} else if tld.eTLDplus1(host) != vendor {
Logger.contentBlocking.debug("Leaving attribution context")
Logger.contentBlocking.debug("<\(self.debugID)> Leaving attribution context")
state = .activeAttribution(vendor: vendor,
session: SessionInfo(start: session.attributionStartedAt,
leftContextAt: Date()),
Expand All @@ -221,39 +242,47 @@ public class AdClickAttributionLogic {
applyRules()
}

private func onAttributedRulesCompiled(forVendor vendor: String, _ rules: ContentBlockerRulesManager.Rules) {
guard case .preparingAttribution(let expectedVendor, let session, let completionBlocks) = state else {
Logger.contentBlocking.error("Attributed Rules received unexpectedly")
private func onAttributedRulesCompiled(forVendor vendor: String, requestID: UUID, _ rules: ContentBlockerRulesManager.Rules) {
guard case .preparingAttribution(let expectedVendor, let session, let id, let completionBlocks) = state else {
Logger.contentBlocking.error("<\(self.debugID)> Attributed Rules received unexpectedly")
errorReporting?.fire(.adAttributionLogicUnexpectedStateOnRulesCompiled)
return
}
guard id == requestID else {
Logger.contentBlocking.debug("<\(self.debugID)> Ignoring outdated rules")
return
}
guard expectedVendor == vendor else {
Logger.contentBlocking.debug("Attributed Rules received for wrong vendor")
Logger.contentBlocking.debug("<\(self.debugID)> Attributed Rules received for wrong vendor")
errorReporting?.fire(.adAttributionLogicWrongVendorOnSuccessfulCompilation)
return
}
state = .activeAttribution(vendor: vendor, session: session, rules: rules)
applyRules()
Logger.contentBlocking.debug("Resuming provisional navigation for \(completionBlocks.count, privacy: .public) requests")
Logger.contentBlocking.debug("<\(self.debugID)> Resuming provisional navigation for \(completionBlocks.count, privacy: .public) requests")
for completion in completionBlocks {
completion()
}
}

private func onAttributedRulesCompilationFailed(forVendor vendor: String) {
guard case .preparingAttribution(let expectedVendor, _, let completionBlocks) = state else {
Logger.contentBlocking.error("Attributed Rules compilation failed")
private func onAttributedRulesCompilationFailed(forVendor vendor: String, requestID: UUID) {
guard case .preparingAttribution(let expectedVendor, _, let id, let completionBlocks) = state else {
Logger.contentBlocking.error("<\(self.debugID)> Attributed Rules compilation failed")
errorReporting?.fire(.adAttributionLogicUnexpectedStateOnRulesCompilationFailed)
return
}
guard id == requestID else {
Logger.contentBlocking.debug("<\(self.debugID)> Ignoring outdated rules")
return
}
guard expectedVendor == vendor else {
errorReporting?.fire(.adAttributionLogicWrongVendorOnFailedCompilation)
return
}
state = .noAttribution

applyRules()
Logger.contentBlocking.debug("Resuming provisional navigation for \(completionBlocks.count, privacy: .public) requests")
Logger.contentBlocking.debug("<\(self.debugID)> Resuming provisional navigation for \(completionBlocks.count, privacy: .public) requests")
for completion in completionBlocks {
completion()
}
Expand All @@ -269,24 +298,27 @@ public class AdClickAttributionLogic {

/// Request attribution when we detect it is needed
private func requestAttribution(forVendor vendorHost: String, attributionStartedAt: Date = Date(), completionBlocks: [() -> Void] = []) {
Logger.contentBlocking.debug("<\(self.debugID)> Requesting attribution and new rules for \(vendorHost)")
let requestID = UUID()
state = .preparingAttribution(vendor: vendorHost,
session: SessionInfo(start: attributionStartedAt),
requestID: requestID,
completionBlocks: completionBlocks)

scheduleTimeout(forVendor: vendorHost)
scheduleTimeout(forVendor: vendorHost, requestID: requestID)
rulesProvider.requestAttribution(forVendor: vendorHost) { [weak self] rules in
self?.cancelTimeout()
if let rules = rules {
self?.onAttributedRulesCompiled(forVendor: vendorHost, rules)
self?.onAttributedRulesCompiled(forVendor: vendorHost, requestID: requestID, rules)
} else {
self?.onAttributedRulesCompilationFailed(forVendor: vendorHost)
self?.onAttributedRulesCompilationFailed(forVendor: vendorHost, requestID: requestID)
}
}
}

private func scheduleTimeout(forVendor vendor: String) {
private func scheduleTimeout(forVendor vendor: String, requestID: UUID) {
let timeoutWorkItem = DispatchWorkItem { [weak self] in
self?.onAttributedRulesCompilationFailed(forVendor: vendor)
self?.onAttributedRulesCompilationFailed(forVendor: vendor, requestID: requestID)
self?.attributionTimeout = nil

self?.errorReporting?.fire(.adAttributionLogicRequestingAttributionTimedOut)
Expand All @@ -307,21 +339,21 @@ public class AdClickAttributionLogic {

switch state {
case .noAttribution:
Logger.contentBlocking.debug("Preparing attribution for \(vendorHost)")
Logger.contentBlocking.debug("<\(self.debugID)> Preparing attribution for \(vendorHost)")
requestAttribution(forVendor: vendorHost)
case .preparingAttribution(let expectedVendor, _, let completionBlocks):
case .preparingAttribution(let expectedVendor, _, _, let completionBlocks):
if expectedVendor != vendorHost {
Logger.contentBlocking.debug("Preparing attributon for \(vendorHost) replacing pending one for \(expectedVendor)")
Logger.contentBlocking.debug("<\(self.debugID)> Preparing attributon for \(vendorHost) replacing pending one for \(expectedVendor)")
requestAttribution(forVendor: vendorHost, completionBlocks: completionBlocks)
} else {
Logger.contentBlocking.debug("Preparing attribution for \(vendorHost) already in progress")
Logger.contentBlocking.debug("<\(self.debugID)> Preparing attribution for \(vendorHost) already in progress")
}
case .activeAttribution(let expectedVendor, _, _):
if expectedVendor != vendorHost {
Logger.contentBlocking.debug("Preparing attributon for \(vendorHost) replacing \(expectedVendor)")
Logger.contentBlocking.debug("<\(self.debugID)> Preparing attributon for \(vendorHost) replacing \(expectedVendor)")
requestAttribution(forVendor: vendorHost)
} else {
Logger.contentBlocking.debug("Attribution for \(vendorHost) already active")
Logger.contentBlocking.debug("<\(self.debugID)> Attribution for \(vendorHost) already active")
}
}
}
Expand All @@ -332,7 +364,7 @@ extension AdClickAttributionLogic: AdClickAttributionDetectionDelegate {

public func attributionDetection(_ detection: AdClickAttributionDetection,
didDetectVendor vendorHost: String) {
Logger.contentBlocking.debug("Detected attribution requests for \(vendorHost)")
Logger.contentBlocking.debug("<\(self.debugID)> Detected attribution requests for \(vendorHost)")
onAttributionRequested(forVendor: vendorHost)
registerFirstActivity = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ public class AdClickAttributionRulesProvider: AdClickAttributionRulesProviding {
// This is optimization: in case multiple tabs request same attribution at the same time, we will respond quickly.
var matchingTasks = tasks.filter { $0 == attributionTask }
tasks.removeAll(where: { $0 == attributionTask })
matchingTasks.append(attributionTask)

// Preserve order in which rules were requested
matchingTasks.insert(attributionTask, at: 0)

Logger.contentBlocking.debug("Returning attribution rules for vendor \(attributionTask.vendor) to \(matchingTasks.count, privacy: .public) caller(s)")

Expand Down
Loading

0 comments on commit 2d60823

Please sign in to comment.