Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 61fab94

Browse files
authored
[navigation #2] AdClick attribution navigation Tab Extension (#886)
* AdClickAttributionTabExtension+NavigationResponder * fix TabExtension test overrides, adClick extension tests * fix AdClickAttributionTabExtensionTests teardown * cleanup * don‘t use shared state in tests * fix adClick inherited attribution initialization * cleanup * Adjust adClick tests to better match UserScripts/UserContentController behaviour; cleanup * fix RELEASE * remove Swifter dependency * fix file header * [navigation #3+4] canGoBack/GoForward; SERP headers (#885) * canGoBack, canGoForward RePublished, Tab.publishers refactored * redo redirect(_:NavigationAction, invalidatingBackItemIfNeeded) * tests * don‘t use shared state in tests * fix RELEASE * rollback renaming * drop RePublished * fix header name * [navigation #4] SERP headers navigation responder (#887) * SERP headers handling in SerpHeadersNavigationResponder * convert TabTests to custom SchemeHandler
1 parent beda9c6 commit 61fab94

19 files changed

+1661
-210
lines changed

.swiftlint.tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ disabled_rules:
1414
- cyclomatic_complexity
1515
- identifier_name
1616
- implicit_getter
17+
- trailing_comma
1718

1819
opt_in_rules:
1920
- file_header

DuckDuckGo.xcodeproj/project.pbxproj

Lines changed: 63 additions & 5 deletions
Large diffs are not rendered by default.

DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
// limitations under the License.
1717
//
1818

19-
import os.log
19+
import BrowserServicesKit
2020
import Combine
2121
import Common
2222
import ContentBlocking
2323
import Foundation
24-
import BrowserServicesKit
24+
import Navigation
25+
import os.log
2526
import PrivacyDashboard
27+
import TrackerRadarKit
2628
import WebKit
2729

2830
protocol AdClickAttributionDependencies {
@@ -46,18 +48,48 @@ protocol UserContentControllerProtocol: AnyObject {
4648
func installLocalContentRuleList(_ ruleList: WKContentRuleList, identifier: String)
4749
}
4850

51+
protocol AdClickAttributionDetecting {
52+
func onStartNavigation(url: URL?)
53+
func on2XXResponse(url: URL?)
54+
func onDidFinishNavigation(url: URL?)
55+
func onDidFailNavigation()
56+
}
57+
extension AdClickAttributionDetection: AdClickAttributionDetecting {}
58+
59+
protocol AdClickLogicProtocol: AnyObject {
60+
var state: AdClickAttributionLogic.State { get }
61+
var delegate: AdClickAttributionLogicDelegate? { get set }
62+
63+
func applyInheritedAttribution(state: AdClickAttributionLogic.State?)
64+
func onRulesChanged(latestRules: [ContentBlockerRulesManager.Rules])
65+
func onRequestDetected(request: DetectedRequest)
66+
67+
func onBackForwardNavigation(mainFrameURL: URL?)
68+
func onProvisionalNavigation() async
69+
func onDidFinishNavigation(host: String?, currentTime: Date)
70+
}
71+
extension AdClickAttributionLogic: AdClickLogicProtocol {}
72+
73+
protocol ContentBlockerScriptProtocol: AnyObject {
74+
var currentAdClickAttributionVendor: String? { get set }
75+
var supplementaryTrackerData: [TrackerData] { get set }
76+
}
77+
extension ContentBlockerRulesUserScript: ContentBlockerScriptProtocol {}
78+
4979
final class AdClickAttributionTabExtension: TabExtension {
5080

51-
private static func makeAdClickAttributionDetection(with dependencies: some AdClickAttributionDependencies) -> AdClickAttributionDetection {
52-
return AdClickAttributionDetection(feature: dependencies.adClickAttribution,
53-
tld: dependencies.tld,
54-
eventReporting: dependencies.attributionEvents,
55-
errorReporting: dependencies.attributionDebugEvents,
56-
log: OSLog.attribution)
81+
private static func makeAdClickAttributionDetection(with dependencies: any AdClickAttributionDependencies, delegate: AdClickAttributionLogic) -> AdClickAttributionDetection {
82+
let detection = AdClickAttributionDetection(feature: dependencies.adClickAttribution,
83+
tld: dependencies.tld,
84+
eventReporting: dependencies.attributionEvents,
85+
errorReporting: dependencies.attributionDebugEvents,
86+
log: OSLog.attribution)
87+
detection.delegate = delegate
88+
return detection
5789

5890
}
5991

60-
private static func makeAdClickAttributionLogic(with dependencies: some AdClickAttributionDependencies) -> AdClickAttributionLogic {
92+
private static func makeAdClickAttributionLogic(with dependencies: any AdClickAttributionDependencies) -> AdClickAttributionLogic {
6193
return AdClickAttributionLogic(featureConfig: dependencies.adClickAttribution,
6294
rulesProvider: dependencies.adClickAttributionRulesProvider,
6395
tld: dependencies.tld,
@@ -66,33 +98,40 @@ final class AdClickAttributionTabExtension: TabExtension {
6698
log: OSLog.attribution)
6799
}
68100

101+
private static func makeAdClickAttribution(with dependencies: any AdClickAttributionDependencies) -> (AdClickLogicProtocol, AdClickAttributionDetecting) {
102+
let logic = makeAdClickAttributionLogic(with: dependencies)
103+
let detection = makeAdClickAttributionDetection(with: dependencies, delegate: logic)
104+
return (logic, detection)
105+
}
106+
69107
private let dependencies: any AdClickAttributionDependencies
70108

71109
private weak var userContentController: UserContentControllerProtocol?
72-
private weak var contentBlockerRulesScript: ContentBlockerRulesUserScript?
110+
private weak var contentBlockerRulesScript: ContentBlockerScriptProtocol?
111+
private let dateTimeProvider: () -> Date
73112

74-
// to be made private
75-
let detection: AdClickAttributionDetection
76-
let logic: AdClickAttributionLogic
113+
private let detection: AdClickAttributionDetecting
114+
private let logic: AdClickLogicProtocol
77115

78-
public var currentAttributionState: AdClickAttributionLogic.State? {
116+
public var currentAttributionState: AdClickAttributionLogic.State {
79117
logic.state
80118
}
81119

82120
private var cancellables = Set<AnyCancellable>()
83121

84122
init(inheritedAttribution: AdClickAttributionLogic.State?,
85123
userContentControllerFuture: Future<UserContentControllerProtocol, Never>,
86-
contentBlockerRulesScriptPublisher: some Publisher<ContentBlockerRulesUserScript?, Never>,
124+
contentBlockerRulesScriptPublisher: some Publisher<(any ContentBlockerScriptProtocol)?, Never>,
87125
trackerInfoPublisher: some Publisher<DetectedRequest, Never>,
88-
dependencies: some AdClickAttributionDependencies) {
126+
dependencies: some AdClickAttributionDependencies,
127+
dateTimeProvider: @escaping () -> Date = Date.init,
128+
logicsProvider: (AdClickAttributionDependencies) -> (AdClickLogicProtocol, AdClickAttributionDetecting) = AdClickAttributionTabExtension.makeAdClickAttribution) {
89129

90130
self.dependencies = dependencies
91-
self.detection = Self.makeAdClickAttributionDetection(with: dependencies)
92-
self.logic = Self.makeAdClickAttributionLogic(with: dependencies)
131+
self.dateTimeProvider = dateTimeProvider
93132

94-
logic.delegate = self
95-
detection.delegate = logic
133+
(self.logic, self.detection) = logicsProvider(dependencies)
134+
self.logic.delegate = self
96135

97136
// delay firing up until UserContentController is published
98137
userContentControllerFuture.sink { [weak self] userContentController in
@@ -103,7 +142,7 @@ final class AdClickAttributionTabExtension: TabExtension {
103142
}.store(in: &cancellables)
104143
}
105144

106-
private func delayedInitialization(with userContentController: UserContentControllerProtocol, inheritedAttribution: AdClickAttributionLogic.State?, contentBlockerRulesScriptPublisher: some Publisher<ContentBlockerRulesUserScript?, Never>, trackerInfoPublisher: some Publisher<DetectedRequest, Never>) {
145+
private func delayedInitialization(with userContentController: UserContentControllerProtocol, inheritedAttribution: AdClickAttributionLogic.State?, contentBlockerRulesScriptPublisher: some Publisher<(any ContentBlockerScriptProtocol)?, Never>, trackerInfoPublisher: some Publisher<DetectedRequest, Never>) {
107146

108147
cancellables.removeAll()
109148
self.userContentController = userContentController
@@ -173,14 +212,48 @@ extension AdClickAttributionTabExtension: AdClickAttributionLogicDelegate {
173212

174213
}
175214

176-
extension AppContentBlocking: AdClickAttributionDependencies {}
215+
extension AdClickAttributionTabExtension: NavigationResponder {
216+
217+
func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? {
218+
if navigationAction.isForMainFrame, navigationAction.navigationType.isBackForward {
219+
logic.onBackForwardNavigation(mainFrameURL: navigationAction.url)
220+
}
221+
return .next
222+
}
223+
224+
func didStart(_ navigation: Navigation) {
225+
detection.onStartNavigation(url: navigation.url)
226+
}
227+
228+
func decidePolicy(for navigationResponse: NavigationResponse) async -> NavigationResponsePolicy? {
229+
if navigationResponse.isForMainFrame,
230+
let currentNavigation = navigationResponse.mainFrameNavigation,
231+
navigationResponse.isSuccessful == true {
232+
detection.on2XXResponse(url: currentNavigation.url)
233+
}
234+
235+
await logic.onProvisionalNavigation()
236+
237+
return .next
238+
}
239+
240+
func navigationDidFinish(_ navigation: Navigation) {
241+
guard navigation.isCurrent else { return }
242+
detection.onDidFinishNavigation(url: navigation.url)
243+
logic.onDidFinishNavigation(host: navigation.url.host, currentTime: dateTimeProvider())
244+
}
245+
246+
func navigation(_ navigation: Navigation, didFailWith error: WKError) {
247+
guard navigation.isCurrent else { return }
248+
detection.onDidFailNavigation()
249+
}
250+
251+
}
177252

178-
protocol AdClickAttributionProtocol {
179-
var currentAttributionState: AdClickAttributionLogic.State? { get }
253+
extension AppContentBlocking: AdClickAttributionDependencies {}
180254

181-
// to be removed
182-
var detection: AdClickAttributionDetection { get }
183-
var logic: AdClickAttributionLogic { get }
255+
protocol AdClickAttributionProtocol: AnyObject, NavigationResponder {
256+
var currentAttributionState: AdClickAttributionLogic.State { get }
184257
}
185258

186259
extension AdClickAttributionTabExtension: AdClickAttributionProtocol {

DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ extension TabExtensionsBuilder {
102102
add {
103103
AdClickAttributionTabExtension(inheritedAttribution: args.inheritedAttribution,
104104
userContentControllerFuture: args.userContentControllerFuture,
105-
contentBlockerRulesScriptPublisher: userScripts.map(\.?.contentBlockerRulesScript),
105+
contentBlockerRulesScriptPublisher: userScripts.map { $0?.contentBlockerRulesScript },
106106
trackerInfoPublisher: trackerInfoPublisher,
107107
dependencies: dependencies.privacyFeatures.contentBlocking)
108108
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// NavigationActionPolicyExtension.swift
3+
//
4+
// Copyright © 2023 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
import Navigation
20+
21+
extension NavigationActionPolicy {
22+
23+
/// cancel+redirect Navigation Action popping last WebView Back Item
24+
/// if a client-redirected navigation has been committed its BackForwardItem will stay in history
25+
/// when the Navigation Action is cancelled in decidePolicyForNavigationAction:
26+
/// https://app.asana.com/0/inbox/1199237043628108/1201280322539473/1201353436736961
27+
@MainActor
28+
static func redirect(_ navigationAction: NavigationAction, invalidatingBackItemIfNeededFor webView: WebView, do redirect: @escaping (Navigator) -> Void) -> NavigationActionPolicy {
29+
guard let mainFrame = navigationAction.mainFrameTarget else {
30+
assertionFailure("Trying to redirect non-main-frame NavigationAction")
31+
return .cancel
32+
}
33+
return .redirect(mainFrame) { navigator in
34+
// Cancelled & Upgraded Client Redirect URL leaves wrong backForwardList record
35+
36+
if case .redirect(.client(delay: 0)) = navigationAction.navigationType,
37+
// initial NavigationAction BackForwardListItem is not the Current Item (new item was pushed during navigation)
38+
let fromHistoryItemIdentity = navigationAction.redirectHistory?.last?.fromHistoryItemIdentity,
39+
fromHistoryItemIdentity != webView.backForwardList.currentItem?.identity {
40+
41+
navigator.goBack()?.overrideResponders { _, _ in
42+
// don‘t perform actual navigation, just pop the back item
43+
.cancel
44+
}
45+
}
46+
47+
redirect(navigator)
48+
}
49+
}
50+
51+
}

DuckDuckGo/Browser Tab/Model/Tab+Navigation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ extension Tab: NavigationResponder {
3232
navigationDelegate.setResponders(
3333
.weak(self),
3434

35-
// ...
35+
.weak(nullable: self.adClickAttribution),
36+
.struct(SerpHeadersNavigationResponder()),
3637

3738
// should be the last, for Unit Tests navigation events tracking
3839
.struct(nullable: testsClosureNavigationResponder)

0 commit comments

Comments
 (0)