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

Commit 6a62d10

Browse files
committed
Merge branch 'main' into sam/remove-test-rollout-pixel
# By Graeme Arthur (4) and others # Via GitHub (1) and Graeme Arthur (1) * main: VPN clean-up (#3502) add daily pixel for deleting suggestions (#3531) Fix VPN memory pressure monitor (#3535) Update Ruby to 3.3.4 (#3547) Onboarding Add To Dock Pixels (#3543) Switch to free runners for tests that run on Maestro (#3546) Fix email protection test (#3539) Update BSK for PixelKit suffix change (#3534) Adding app backgrounded result to rule compilation (#3533) Send pixel on sync secure storage failure (#3542) Onboarding Add to Dock Refactor for Intro scenario (#3538) Update C-S-S to 6.29.0 (#3541) Change save password Never for Site button to Not Now (#3471) Release 7.144.0-1 (#3540) UserDefaults misbehavior monitoring (#3510) Send pixel on sync secure storage read failure (#3530) Remove NewTabPage retain cycles (#3532) Update release notes (#3529) # Conflicts: # DuckDuckGo.xcodeproj/project.pbxproj # DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
2 parents 039376b + 3aad126 commit 6a62d10

File tree

78 files changed

+1498
-218
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1498
-218
lines changed

.github/workflows/end-to-end.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
end-to-end-tests:
6969
name: End to end Tests
7070
needs: build-end-to-end-tests
71-
runs-on: macos-14-xlarge
71+
runs-on: macos-14
7272
timeout-minutes: 90
7373
strategy:
7474
matrix:

.github/workflows/sync-end-to-end.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
sync-end-to-end-tests:
6969
name: Sync End To End Tests
7070
needs: build-for-sync-end-to-end-tests
71-
runs-on: macos-14-xlarge
71+
runs-on: macos-14
7272
timeout-minutes: 90
7373
strategy:
7474
matrix:

.maestro/release_tests/emailprotection.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ tags:
2727
- scroll
2828
- assertVisible: Email Protection
2929
- tapOn: Email Protection
30-
- assertVisible: Email privacy, simplified.
30+
- assertVisible: Email privacy, protected.
3131
- assertVisible:
3232
id: searchEntry
3333
- tapOn:

.ruby-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.0.4
1+
3.3.4

Core/BoolFileMarker.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// BoolFileMarker.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2024 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
public struct BoolFileMarker {
21+
let fileManager = FileManager.default
22+
private let url: URL
23+
24+
public var isPresent: Bool {
25+
fileManager.fileExists(atPath: url.path)
26+
}
27+
28+
public func mark() {
29+
if !isPresent {
30+
fileManager.createFile(atPath: url.path, contents: nil, attributes: [.protectionKey: FileProtectionType.none])
31+
}
32+
}
33+
34+
public func unmark() {
35+
if isPresent {
36+
try? fileManager.removeItem(at: url)
37+
}
38+
}
39+
40+
public init?(name: Name) {
41+
guard let applicationSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
42+
return nil
43+
}
44+
45+
self.url = applicationSupportDirectory.appendingPathComponent(name.rawValue)
46+
}
47+
48+
public struct Name: RawRepresentable {
49+
public let rawValue: String
50+
51+
public init(rawValue: String) {
52+
self.rawValue = "\(rawValue).marker"
53+
}
54+
}
55+
}

Core/BoolFileMarkerTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// BoolFileMarkerTests.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2024 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import XCTest
21+
@testable import Core
22+
23+
final class BoolFileMarkerTests: XCTestCase {
24+
25+
private let marker = BoolFileMarker(name: .init(rawValue: "test"))!
26+
27+
override func tearDown() {
28+
super.tearDown()
29+
30+
marker.unmark()
31+
}
32+
33+
private var testFileURL: URL? {
34+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.appendingPathComponent("test.marker")
35+
}
36+
37+
func testMarkCreatesCorrectFile() throws {
38+
39+
marker.mark()
40+
41+
let fileURL = try XCTUnwrap(testFileURL)
42+
43+
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
44+
XCTAssertNil(attributes[.protectionKey])
45+
XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path))
46+
XCTAssertEqual(marker.isPresent, true)
47+
}
48+
49+
func testUnmarkRemovesFile() throws {
50+
marker.mark()
51+
marker.unmark()
52+
53+
let fileURL = try XCTUnwrap(testFileURL)
54+
55+
XCTAssertFalse(marker.isPresent)
56+
XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
57+
}
58+
}

Core/PixelEvent.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,15 @@ extension Pixel {
137137
case autocompleteDisplayedLocalHistory
138138
case autocompleteDisplayedOpenedTab
139139
case autocompleteSwipeToDelete
140+
case autocompleteSwipeToDeleteDaily
140141

141142
case feedbackPositive
142143
case feedbackNegativePrefix(category: String)
143144

144145
case brokenSiteReport
145146

147+
// MARK: - Onboarding
148+
146149
case onboardingIntroShownUnique
147150
case onboardingIntroComparisonChartShownUnique
148151
case onboardingIntroChooseBrowserCTAPressed
@@ -173,6 +176,15 @@ extension Pixel {
173176
case daxDialogsEndOfJourneyNewTabUnique
174177
case daxDialogsEndOfJourneyDismissed
175178

179+
// MARK: - Onboarding Add To Dock
180+
181+
case onboardingAddToDockPromoImpressionsUnique
182+
case onboardingAddToDockPromoShowTutorialCTATapped
183+
case onboardingAddToDockPromoDismissCTATapped
184+
case onboardingAddToDockTutorialDismissCTATapped
185+
186+
// MARK: - Onboarding Add To Dock
187+
176188
case widgetsOnboardingCTAPressed
177189
case widgetsOnboardingDeclineOptionPressed
178190
case widgetsOnboardingMovedToBackground
@@ -400,10 +412,6 @@ extension Pixel {
400412
case networkProtectionClientFailedToParseRegisteredServersResponse
401413
case networkProtectionClientFailedToFetchLocations
402414
case networkProtectionClientFailedToParseLocationsResponse
403-
case networkProtectionClientFailedToEncodeRedeemRequest
404-
case networkProtectionClientInvalidInviteCode
405-
case networkProtectionClientFailedToRedeemInviteCode
406-
case networkProtectionClientFailedToParseRedeemResponse
407415
case networkProtectionClientInvalidAuthToken
408416

409417
case networkProtectionKeychainErrorFailedToCastKeychainValueToData
@@ -623,6 +631,7 @@ extension Pixel {
623631
case syncRemoveDeviceError
624632
case syncDeleteAccountError
625633
case syncLoginExistingAccountError
634+
case syncSecureStorageReadError
626635

627636
case syncGetOtherDevices
628637
case syncGetOtherDevicesCopy
@@ -834,6 +843,11 @@ extension Pixel {
834843

835844
// MARK: WebView Error Page Shown
836845
case webViewErrorPageShown
846+
847+
// MARK: UserDefaults incositency monitoring
848+
case protectedDataUnavailableWhenBecomeActive
849+
case statisticsLoaderATBStateMismatch
850+
case adAttributionReportStateMismatch
837851
}
838852

839853
}
@@ -961,6 +975,7 @@ extension Pixel.Event {
961975
case .autocompleteDisplayedLocalHistory: return "m_autocomplete_display_local_history"
962976
case .autocompleteDisplayedOpenedTab: return "m_autocomplete_display_switch_to_tab"
963977
case .autocompleteSwipeToDelete: return "m_autocomplete_result_deleted"
978+
case .autocompleteSwipeToDeleteDaily: return "m_autocomplete_result_deleted_daily"
964979

965980
case .feedbackPositive: return "mfbs_positive_submit"
966981
case .feedbackNegativePrefix(category: let category): return "mfbs_negative_\(category)"
@@ -997,6 +1012,11 @@ extension Pixel.Event {
9971012
case .daxDialogsEndOfJourneyNewTabUnique: return "m_dx_end_new_tab_unique"
9981013
case .daxDialogsEndOfJourneyDismissed: return "m_dx_end_dialog_dismissed"
9991014

1015+
case .onboardingAddToDockPromoImpressionsUnique: return "m_onboarding_add_to_dock_promo_impressions_unique"
1016+
case .onboardingAddToDockPromoShowTutorialCTATapped: return "m_onboarding_add_to_dock_promo_show_tutorial_button_tapped"
1017+
case .onboardingAddToDockPromoDismissCTATapped: return "m_onboarding_add_to_dock_promo_dismiss_button_tapped"
1018+
case .onboardingAddToDockTutorialDismissCTATapped: return "m_onboarding_add_to_dock_tutorial_dismiss_button_tapped"
1019+
10001020
case .widgetsOnboardingCTAPressed: return "m_o_w_a"
10011021
case .widgetsOnboardingDeclineOptionPressed: return "m_o_w_d"
10021022
case .widgetsOnboardingMovedToBackground: return "m_o_w_b"
@@ -1214,10 +1234,6 @@ extension Pixel.Event {
12141234
case .networkProtectionClientFailedToFetchLocations: return "m_netp_backend_api_error_failed_to_fetch_locations"
12151235
case .networkProtectionClientFailedToParseLocationsResponse:
12161236
return "m_netp_backend_api_error_parsing_locations_response_failed"
1217-
case .networkProtectionClientFailedToEncodeRedeemRequest: return "m_netp_backend_api_error_encoding_redeem_request_body_failed"
1218-
case .networkProtectionClientInvalidInviteCode: return "m_netp_backend_api_error_invalid_invite_code"
1219-
case .networkProtectionClientFailedToRedeemInviteCode: return "m_netp_backend_api_error_failed_to_redeem_invite_code"
1220-
case .networkProtectionClientFailedToParseRedeemResponse: return "m_netp_backend_api_error_parsing_redeem_response_failed"
12211237
case .networkProtectionClientInvalidAuthToken: return "m_netp_backend_api_error_invalid_auth_token"
12221238
case .networkProtectionKeychainErrorFailedToCastKeychainValueToData: return "m_netp_keychain_error_failed_to_cast_keychain_value_to_data"
12231239
case .networkProtectionKeychainReadError: return "m_netp_keychain_error_read_failed"
@@ -1430,6 +1446,7 @@ extension Pixel.Event {
14301446
case .syncRemoveDeviceError: return "m_d_sync_remove_device_error"
14311447
case .syncDeleteAccountError: return "m_d_sync_delete_account_error"
14321448
case .syncLoginExistingAccountError: return "m_d_sync_login_existing_account_error"
1449+
case .syncSecureStorageReadError: return "m_d_sync_secure_storage_error"
14331450

14341451
case .syncGetOtherDevices: return "sync_get_other_devices"
14351452
case .syncGetOtherDevicesCopy: return "sync_get_other_devices_copy"
@@ -1664,6 +1681,11 @@ extension Pixel.Event {
16641681

16651682
// MARK: - DuckPlayer FE Application Telemetry
16661683
case .duckPlayerLandscapeLayoutImpressions: return "duckplayer_landscape_layout_impressions"
1684+
1685+
// MARK: UserDefaults incositency monitoring
1686+
case .protectedDataUnavailableWhenBecomeActive: return "m_protected_data_unavailable_when_become_active"
1687+
case .statisticsLoaderATBStateMismatch: return "m_statistics_loader_atb_state_mismatch"
1688+
case .adAttributionReportStateMismatch: return "m_ad_attribution_report_state_mismatch"
16671689
}
16681690
}
16691691
}
@@ -1715,6 +1737,7 @@ extension Pixel.Event {
17151737

17161738
case tabClosed = "tab_closed"
17171739
case appQuit = "app_quit"
1740+
case appBackgrounded = "app_backgrounded"
17181741
case success
17191742

17201743
}

Core/StatisticsLoader.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,29 @@ public class StatisticsLoader {
3333
private let returnUserMeasurement: ReturnUserMeasurement
3434
private let usageSegmentation: UsageSegmenting
3535
private let parser = AtbParser()
36+
private let atbPresenceFileMarker = BoolFileMarker(name: .isATBPresent)
37+
private let inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring
3638

3739
init(statisticsStore: StatisticsStore = StatisticsUserDefaults(),
3840
returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(),
39-
usageSegmentation: UsageSegmenting = UsageSegmentation()) {
41+
usageSegmentation: UsageSegmenting = UsageSegmentation(),
42+
inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor()) {
4043
self.statisticsStore = statisticsStore
4144
self.returnUserMeasurement = returnUserMeasurement
4245
self.usageSegmentation = usageSegmentation
46+
self.inconsistencyMonitoring = inconsistencyMonitoring
4347
}
4448

4549
public func load(completion: @escaping Completion = {}) {
46-
if statisticsStore.hasInstallStatistics {
50+
let hasFileMarker = atbPresenceFileMarker?.isPresent ?? false
51+
let hasInstallStatistics = statisticsStore.hasInstallStatistics
52+
53+
inconsistencyMonitoring.statisticsDidLoad(hasFileMarker: hasFileMarker, hasInstallStatistics: hasInstallStatistics)
54+
55+
if hasInstallStatistics {
56+
// Synchronize file marker with current state
57+
createATBFileMarker()
58+
4759
completion()
4860
return
4961
}
@@ -85,10 +97,15 @@ public class StatisticsLoader {
8597
self.statisticsStore.installDate = Date()
8698
self.statisticsStore.atb = atb.version
8799
self.returnUserMeasurement.installCompletedWithATB(atb)
100+
self.createATBFileMarker()
88101
completion()
89102
}
90103
}
91104

105+
private func createATBFileMarker() {
106+
atbPresenceFileMarker?.mark()
107+
}
108+
92109
public func refreshSearchRetentionAtb(completion: @escaping Completion = {}) {
93110
guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeSearchAtbURL() else {
94111
requestInstallStatistics {
@@ -169,3 +186,7 @@ public class StatisticsLoader {
169186
processUsageSegmentation(atb: nil, activityType: activityType)
170187
}
171188
}
189+
190+
private extension BoolFileMarker.Name {
191+
static let isATBPresent = BoolFileMarker.Name(rawValue: "atb-present")
192+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// StorageInconsistencyMonitor.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2024 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import UIKit
21+
22+
public protocol AppActivationInconsistencyMonitoring {
23+
/// See `StorageInconsistencyMonitor` for details
24+
func didBecomeActive(isProtectedDataAvailable: Bool)
25+
}
26+
27+
public protocol StatisticsStoreInconsistencyMonitoring {
28+
/// See `StorageInconsistencyMonitor` for details
29+
func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool)
30+
}
31+
32+
public protocol AdAttributionReporterInconsistencyMonitoring {
33+
/// See `StorageInconsistencyMonitor` for details
34+
func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool)
35+
}
36+
37+
/// Takes care of reporting inconsistency in storage availability and/or state.
38+
/// See https://app.asana.com/0/481882893211075/1208618515043198/f for details.
39+
public struct StorageInconsistencyMonitor: AppActivationInconsistencyMonitoring & StatisticsStoreInconsistencyMonitoring & AdAttributionReporterInconsistencyMonitoring {
40+
41+
public init() { }
42+
43+
/// Reports a pixel if data is not available while app is active
44+
public func didBecomeActive(isProtectedDataAvailable: Bool) {
45+
if !isProtectedDataAvailable {
46+
Pixel.fire(pixel: .protectedDataUnavailableWhenBecomeActive)
47+
assertionFailure("This is unexpected state, debug if possible")
48+
}
49+
}
50+
51+
/// Reports a pixel if file marker exists but installStatistics are missing
52+
public func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) {
53+
if hasFileMarker == true && hasInstallStatistics == false {
54+
Pixel.fire(pixel: .statisticsLoaderATBStateMismatch)
55+
assertionFailure("This is unexpected state, debug if possible")
56+
}
57+
}
58+
59+
/// Reports a pixel if file marker exists but completion flag is false
60+
public func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) {
61+
if hasFileMarker == true && hasCompletedFlag == false {
62+
Pixel.fire(pixel: .adAttributionReportStateMismatch)
63+
assertionFailure("This is unexpected state, debug if possible")
64+
}
65+
}
66+
}

Core/SyncErrorHandler.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public class SyncErrorHandler: EventMapping<SyncError> {
100100
Pixel.fire(pixel: .syncFailedToLoadAccount, error: error)
101101
case .failedToSetupEngine:
102102
Pixel.fire(pixel: .syncFailedToSetupEngine, error: error)
103+
case .failedToReadSecureStore:
104+
Pixel.fire(pixel: .syncSecureStorageReadError, error: error)
103105
default:
104106
// Should this be so generic?
105107
let domainEvent = Pixel.Event.syncSentUnauthenticatedRequest

0 commit comments

Comments
 (0)