Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure to apply only relevant (last requested) rules on attribution #1059

Merged
merged 4 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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