Skip to content

Commit

Permalink
UserDefaults misbehavior monitoring (#3510)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1206226850447395/1208659072736427/f
Tech Design URL:
https://app.asana.com/0/481882893211075/1208618515043198/f
CC:

**Description**:

Attempt to validate a hypothesis about unreliable/inaccessible
UserDefaults data during app launch.

**Steps to test this PR**:

#### Statistics loader
1. Launch the app
2. Stop and put a breakpoint in `StatisticsLoader.swift:50`
3. Run the app again. On breakpoint run a debugger command: `expr
statisticsStore.atb = nil`
4. Continue execution.
5. Verify proper pixel is fired.
6. On assertion go to `StatisticsLoader.load()` frame in the stack and
run: `expr atbPresenceFileMarker?.unmark()` or remove the app. This will
prevent assertion for next scenario.

#### Ad attribution reporter
1. Enable `adAttributionReporting` feature flag.
1. Put a breakpoint in `AdAttributionPixelReporter.swift:60`
3. Run the app. On breakpoint run a debugger command: `expr
attributionReportSuccessfulFileMarker?.mark()`
4. Continue execution
5. Verify proper pixel is fired.
6. On assertion go to
`AdAttributionPixelReporter.reportAttributionIfNeeded()` frame in the
stack and run: `expr attributionReportSuccessfulFileMarker?.unmark()` or
remove the app.

<!--
Before submitting a PR, please ensure you have tested the combinations
you expect the reviewer to test, then delete configurations you *know*
do not need explicit testing.

Using a simulator where a physical device is unavailable is acceptable.
-->

**Definition of Done (Internal Only)**:

* [ ] Does this PR satisfy our [Definition of
Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?

---
###### Internal references:
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
  • Loading branch information
dus7 authored Nov 5, 2024
1 parent 1c2abb9 commit c5a97dd
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 10 deletions.
55 changes: 55 additions & 0 deletions Core/BoolFileMarker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// BoolFileMarker.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.
//

public struct BoolFileMarker {
let fileManager = FileManager.default
private let url: URL

public var isPresent: Bool {
fileManager.fileExists(atPath: url.path)
}

public func mark() {
if !isPresent {
fileManager.createFile(atPath: url.path, contents: nil, attributes: [.protectionKey: FileProtectionType.none])
}
}

public func unmark() {
if isPresent {
try? fileManager.removeItem(at: url)
}
}

public init?(name: Name) {
guard let applicationSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
return nil
}

self.url = applicationSupportDirectory.appendingPathComponent(name.rawValue)
}

public struct Name: RawRepresentable {
public let rawValue: String

public init(rawValue: String) {
self.rawValue = "\(rawValue).marker"
}
}
}
58 changes: 58 additions & 0 deletions Core/BoolFileMarkerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// BoolFileMarkerTests.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 XCTest
@testable import Core

final class BoolFileMarkerTests: XCTestCase {

private let marker = BoolFileMarker(name: .init(rawValue: "test"))!

override func tearDown() {
super.tearDown()

marker.unmark()
}

private var testFileURL: URL? {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.appendingPathComponent("test.marker")
}

func testMarkCreatesCorrectFile() throws {

marker.mark()

let fileURL = try XCTUnwrap(testFileURL)

let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
XCTAssertNil(attributes[.protectionKey])
XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path))
XCTAssertEqual(marker.isPresent, true)
}

func testUnmarkRemovesFile() throws {
marker.mark()
marker.unmark()

let fileURL = try XCTUnwrap(testFileURL)

XCTAssertFalse(marker.isPresent)
XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
}
}
10 changes: 10 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,11 @@ extension Pixel {

// MARK: WebView Error Page Shown
case webViewErrorPageShown

// MARK: UserDefaults incositency monitoring
case protectedDataUnavailableWhenBecomeActive
case statisticsLoaderATBStateMismatch
case adAttributionReportStateMismatch
}

}
Expand Down Expand Up @@ -1666,6 +1671,11 @@ extension Pixel.Event {

// MARK: - DuckPlayer FE Application Telemetry
case .duckPlayerLandscapeLayoutImpressions: return "duckplayer_landscape_layout_impressions"

// MARK: UserDefaults incositency monitoring
case .protectedDataUnavailableWhenBecomeActive: return "m_protected_data_unavailable_when_become_active"
case .statisticsLoaderATBStateMismatch: return "m_statistics_loader_atb_state_mismatch"
case .adAttributionReportStateMismatch: return "m_ad_attribution_report_state_mismatch"
}
}
}
Expand Down
25 changes: 23 additions & 2 deletions Core/StatisticsLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,29 @@ public class StatisticsLoader {
private let returnUserMeasurement: ReturnUserMeasurement
private let usageSegmentation: UsageSegmenting
private let parser = AtbParser()
private let atbPresenceFileMarker = BoolFileMarker(name: .isATBPresent)
private let inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring

init(statisticsStore: StatisticsStore = StatisticsUserDefaults(),
returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(),
usageSegmentation: UsageSegmenting = UsageSegmentation()) {
usageSegmentation: UsageSegmenting = UsageSegmentation(),
inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor()) {
self.statisticsStore = statisticsStore
self.returnUserMeasurement = returnUserMeasurement
self.usageSegmentation = usageSegmentation
self.inconsistencyMonitoring = inconsistencyMonitoring
}

public func load(completion: @escaping Completion = {}) {
if statisticsStore.hasInstallStatistics {
let hasFileMarker = atbPresenceFileMarker?.isPresent ?? false
let hasInstallStatistics = statisticsStore.hasInstallStatistics

inconsistencyMonitoring.statisticsDidLoad(hasFileMarker: hasFileMarker, hasInstallStatistics: hasInstallStatistics)

if hasInstallStatistics {
// Synchronize file marker with current state
createATBFileMarker()

completion()
return
}
Expand Down Expand Up @@ -85,10 +97,15 @@ public class StatisticsLoader {
self.statisticsStore.installDate = Date()
self.statisticsStore.atb = atb.version
self.returnUserMeasurement.installCompletedWithATB(atb)
self.createATBFileMarker()
completion()
}
}

private func createATBFileMarker() {
atbPresenceFileMarker?.mark()
}

public func refreshSearchRetentionAtb(completion: @escaping Completion = {}) {
guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeSearchAtbURL() else {
requestInstallStatistics {
Expand Down Expand Up @@ -169,3 +186,7 @@ public class StatisticsLoader {
processUsageSegmentation(atb: nil, activityType: activityType)
}
}

private extension BoolFileMarker.Name {
static let isATBPresent = BoolFileMarker.Name(rawValue: "atb-present")
}
66 changes: 66 additions & 0 deletions Core/StorageInconsistencyMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// StorageInconsistencyMonitor.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 UIKit

public protocol AppActivationInconsistencyMonitoring {
/// See `StorageInconsistencyMonitor` for details
func didBecomeActive(isProtectedDataAvailable: Bool)
}

public protocol StatisticsStoreInconsistencyMonitoring {
/// See `StorageInconsistencyMonitor` for details
func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool)
}

public protocol AdAttributionReporterInconsistencyMonitoring {
/// See `StorageInconsistencyMonitor` for details
func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool)
}

/// Takes care of reporting inconsistency in storage availability and/or state.
/// See https://app.asana.com/0/481882893211075/1208618515043198/f for details.
public struct StorageInconsistencyMonitor: AppActivationInconsistencyMonitoring & StatisticsStoreInconsistencyMonitoring & AdAttributionReporterInconsistencyMonitoring {

public init() { }

/// Reports a pixel if data is not available while app is active
public func didBecomeActive(isProtectedDataAvailable: Bool) {
if !isProtectedDataAvailable {
Pixel.fire(pixel: .protectedDataUnavailableWhenBecomeActive)
assertionFailure("This is unexpected state, debug if possible")
}
}

/// Reports a pixel if file marker exists but installStatistics are missing
public func statisticsDidLoad(hasFileMarker: Bool, hasInstallStatistics: Bool) {
if hasFileMarker == true && hasInstallStatistics == false {
Pixel.fire(pixel: .statisticsLoaderATBStateMismatch)
assertionFailure("This is unexpected state, debug if possible")
}
}

/// Reports a pixel if file marker exists but completion flag is false
public func addAttributionReporter(hasFileMarker: Bool, hasCompletedFlag: Bool) {
if hasFileMarker == true && hasCompletedFlag == false {
Pixel.fire(pixel: .adAttributionReportStateMismatch)
assertionFailure("This is unexpected state, debug if possible")
}
}
}
12 changes: 12 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,14 @@
6F03CB052C32EFCC004179A8 /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */; };
6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB062C32F173004179A8 /* PixelFiring.swift */; };
6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */; };
6F04224D2CD2A3AD00729FA6 /* StorageInconsistencyMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */; };
6F0FEF6B2C516D540090CDE4 /* NewTabPageSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6A2C516D540090CDE4 /* NewTabPageSettingsStorage.swift */; };
6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */; };
6F35379E2C4AAF2E009F8717 /* NewTabPageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379D2C4AAF2E009F8717 /* NewTabPageSettingsView.swift */; };
6F3537A02C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */; };
6F3537A22C4AB97A009F8717 /* NewTabPageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */; };
6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */; };
6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */; };
6F40D15B2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */; };
6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; };
6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */; };
Expand All @@ -323,6 +325,7 @@
6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */; };
6F934F862C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */; };
6F96FF102C2B128500162692 /* NewTabPageCustomizeButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */; };
6F9857342CD27FA2001BE9A0 /* BoolFileMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */; };
6F9FFE262C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */; };
6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */; };
6F9FFE2A2C57ADB100A238BE /* EditableShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */; };
Expand Down Expand Up @@ -1596,6 +1599,7 @@
6F35379F2C4AAFD2009F8717 /* NewTabPageSettingsSectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsSectionItemView.swift; sourceTree = "<group>"; };
6F3537A12C4AB97A009F8717 /* NewTabPageSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsModel.swift; sourceTree = "<group>"; };
6F3537A32C4AC140009F8717 /* NewTabPageDaxLogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageDaxLogoView.swift; sourceTree = "<group>"; };
6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolFileMarkerTests.swift; sourceTree = "<group>"; };
6F40D15A2C34423800BF22F0 /* HomePageDisplayDailyPixelBucket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucket.swift; sourceTree = "<group>"; };
6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = "<group>"; };
6F5041C82CC11A5100989E48 /* SimpleNewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNewTabPageView.swift; sourceTree = "<group>"; };
Expand All @@ -1616,6 +1620,8 @@
6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.swift; sourceTree = "<group>"; };
6F934F852C58DB00008364E4 /* NewTabPageSettingsPersistentStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSettingsPersistentStorageTests.swift; sourceTree = "<group>"; };
6F96FF0F2C2B128500162692 /* NewTabPageCustomizeButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizeButtonView.swift; sourceTree = "<group>"; };
6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolFileMarker.swift; sourceTree = "<group>"; };
6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageInconsistencyMonitor.swift; sourceTree = "<group>"; };
6F9FFE252C579BCD00A238BE /* NewTabPageShortcutsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShortcutsSettingsStorage.swift; sourceTree = "<group>"; };
6F9FFE272C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSectionsSettingsStorage.swift; sourceTree = "<group>"; };
6F9FFE292C57ADB100A238BE /* EditableShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableShortcutsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5923,6 +5929,9 @@
F143C3191E4A99DD00CFDE3A /* Utilities */ = {
isa = PBXGroup;
children = (
6F98573F2CD2933B001BE9A0 /* StorageInconsistencyMonitor.swift */,
6F9857332CD27F98001BE9A0 /* BoolFileMarker.swift */,
6F395BB92CD2C84300B92FC3 /* BoolFileMarkerTests.swift */,
9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */,
B603974829C19F6F00902A34 /* Assertions.swift */,
CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */,
Expand Down Expand Up @@ -8018,6 +8027,7 @@
310E79BD2949CAA5007C49E8 /* FireButtonReferenceTests.swift in Sources */,
4B62C4BA25B930DD008912C6 /* AppConfigurationFetchTests.swift in Sources */,
31C7D71C27515A6300A95D0A /* MockVoiceSearchHelper.swift in Sources */,
6F395BBB2CD2C87D00B92FC3 /* BoolFileMarkerTests.swift in Sources */,
8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */,
98AAF8E4292EB46000DBDF06 /* BookmarksMigrationTests.swift in Sources */,
85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */,
Expand Down Expand Up @@ -8253,6 +8263,7 @@
9876B75E2232B36900D81D9F /* TabInstrumentation.swift in Sources */,
026DABA428242BC80089E0B5 /* MockUserAgent.swift in Sources */,
1E05D1D829C46EDA00BF9A1F /* TimedPixel.swift in Sources */,
6F9857342CD27FA2001BE9A0 /* BoolFileMarker.swift in Sources */,
C14882DC27F2011C00D59F0C /* BookmarksImporter.swift in Sources */,
CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */,
37CBCA9E2A8A659C0050218F /* SyncSettingsAdapter.swift in Sources */,
Expand Down Expand Up @@ -8328,6 +8339,7 @@
CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */,
85D2187624BF6164004373D2 /* FaviconSourcesProvider.swift in Sources */,
98B000532915C46E0034BCA0 /* LegacyBookmarksStoreMigration.swift in Sources */,
6F04224D2CD2A3AD00729FA6 /* StorageInconsistencyMonitor.swift in Sources */,
85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */,
9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */,
1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */,
Expand Down
Loading

0 comments on commit c5a97dd

Please sign in to comment.