-
Notifications
You must be signed in to change notification settings - Fork 425
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
<!-- Note: This checklist is a reminder of our shared engineering expectations. Feel free to change it, although assigning a GitHub reviewer and the items in bold are required.⚠️ If you're an external contributor, please file an issue first before working on a PR, as we can't guarantee that we will accept your changes if they haven't been discussed ahead of time. Thanks! --> Task/Issue URL: https://app.asana.com/0/392891325557410/1206955287217877/f Tech Design URL: https://app.asana.com/0/392891325557410/1208090005627335/f CC: **Description**: This PR ads usage segmentation calculations to the app so that we can monitor usage across segments in a private way, without having to send granular usage information to the server. Deviation from tech design: * Added "Calculation" class / protocol in order to encapsulate the calculation logic exclusively and code it so that it's similar to the python implementation. **Steps to test this PR**: ***First run, new user***: 1. Reset the simulator (so that the keychain is cleared) 1. Run the app 2. No `m_retention_segments` pixel should be fired ***Subsequent run a few days later*** 1. Reset the simulator (so that the keychain is cleared) 1. Modify StatisticsLoader add the following line at the start of the `load` function: self.statisticsStore.atb = "445-1" 4. Run the app 5. An assertion failure should crash the app saying this is not a valid ATB. 6. Update the above line as follows and reset the simulator again: self.statisticsStore.atb = "v445-1" 8. Ensure that `m_retention_segments` pixel is fired. There will be various parameters but one should indicate "app_use" activity 9. Re-launch the app 10. No additional pixel should be fired 11. Perform a search 12. The pixel will be fired again and there should be a parameter indicating search activity. 13. Perform another search 14. No additional pixel should be fired ***Return user test*** 1. Repeat the above test bit with the following atb. The pixels sent should include `reinstaller` in the segments parameter. self.statisticsStore.atb = "v445-1ru" **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?
- Loading branch information
Showing
15 changed files
with
4,140 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// | ||
// UsageSegmentation.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 Foundation | ||
|
||
enum UsageActivityType: String { | ||
|
||
case search | ||
case appUse = "app_use" | ||
|
||
} | ||
|
||
protocol UsageSegmenting { | ||
|
||
func processATB(_ atb: Atb, withInstallAtb installAtb: Atb, andActivityType activityType: UsageActivityType) | ||
|
||
} | ||
|
||
final class UsageSegmentation: UsageSegmenting { | ||
|
||
private let pixelFiring: PixelFiring.Type | ||
private let storage: UsageSegmentationStoring | ||
private let calculatorFactory: UsageSegmentationCalculatorMaking | ||
|
||
init(pixelFiring: PixelFiring.Type = Pixel.self, | ||
storage: UsageSegmentationStoring = UsageSegmentationStorage(), | ||
calculatorFactory: UsageSegmentationCalculatorMaking = DefaultCalculatorFactory()) { | ||
self.pixelFiring = pixelFiring | ||
self.storage = storage | ||
self.calculatorFactory = calculatorFactory | ||
} | ||
|
||
func processATB(_ atb: Atb, withInstallAtb installAtb: Atb, andActivityType activityType: UsageActivityType) { | ||
var atbs = activityType.atbsFromStorage(storage) | ||
|
||
guard !atbs.contains(where: { $0 == atb }) else { return } | ||
|
||
defer { | ||
activityType.updateStorage(storage, withAtbs: atbs) | ||
} | ||
|
||
if atbs.isEmpty { | ||
atbs.append(installAtb) | ||
} | ||
|
||
if installAtb != atb { | ||
atbs.append(atb) | ||
} | ||
|
||
var pixelInfo: [String: String]? | ||
let calculator = calculatorFactory.make(installAtb: installAtb) | ||
|
||
// The calculator updates its internal state starting from the first atb, so iterate over them all and take | ||
// the last result. | ||
// | ||
// This is pretty fast (see performance test) and consider that we'll have max 1 atb per day so over a few years it's up | ||
// to the mid thousands so hardly taxing. | ||
for atb in atbs { | ||
pixelInfo = calculator.processAtb(atb, forActivityType: activityType) | ||
} | ||
|
||
if let pixelInfo { | ||
pixelFiring.fire(.usageSegments, withAdditionalParameters: pixelInfo) | ||
} | ||
} | ||
|
||
} | ||
|
||
private extension UsageActivityType { | ||
|
||
func atbsFromStorage(_ storage: UsageSegmentationStoring) -> [Atb] { | ||
switch self { | ||
case .appUse: return storage.appUseAtbs | ||
case .search: return storage.searchAtbs | ||
} | ||
} | ||
|
||
func updateStorage(_ storage: UsageSegmentationStoring, withAtbs atbs: [Atb]) { | ||
var storage = storage | ||
switch self { | ||
case .appUse: | ||
storage.appUseAtbs = atbs | ||
case .search: | ||
storage.searchAtbs = atbs | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.