Skip to content

Commit

Permalink
Merge branch 'main' into fcappelli/subscription_oauth_api_v2
Browse files Browse the repository at this point in the history
  • Loading branch information
federicocappelli committed Jan 30, 2025
2 parents 76ba897 + 20b2408 commit 1349661
Show file tree
Hide file tree
Showing 16 changed files with 4,227 additions and 59 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/content-scope-scripts",
"state" : {
"revision" : "181696656f3627c1b3ca8cda06d54971e03436ef",
"version" : "7.10.0"
"revision" : "5a463e35ce86fc11394fe62392d32960de22f7d7",
"version" : "7.11.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ let package = Package(
.package(url: "https://github.com/duckduckgo/TrackerRadarKit.git", exact: "3.0.0"),
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.4.0"),
.package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "3.0.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "7.10.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "7.11.0"),
.package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "8.1.0"),
.package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"),
.package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"),
Expand Down
119 changes: 119 additions & 0 deletions Sources/BrowserServicesKit/Statistics/Atb.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// Atb.swift
//
// Copyright © 2025 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

public struct Atb: Decodable, Equatable {

/// Format is v<week>-<day>
/// * day is `1...7` with 1 being Wednesday
/// * note that week is NOT padded but ATBs older than week 100 should never be seen by the apps, ie no one has this installed before Feb 2018 and week 99 is Jan 2018
/// * ATBs > 999 would be about 10 years in the future (Apr 2035), we can fix it nearer the time
static let template = "v100-1"

/// Same as `template` two characters on the end, e.g. `ma`
static let templateWithVariant = template + "xx"

public let version: String
public let updateVersion: String?
let numeric: AtbNumeric?

public init(version: String, updateVersion: String?) {
self.version = version
self.updateVersion = updateVersion
self.numeric = AtbNumeric.makeFromVersion(version)
}

enum CodingKeys: CodingKey {
case version
case updateVersion
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.updateVersion = try container.decodeIfPresent(String.self, forKey: .updateVersion)
self.numeric = AtbNumeric.makeFromVersion(version)
}

/// Equality is about the version without any variants. e.g. v100-1 == v100-1ma. `updateVersion` is ignored because that's a signal from the server to update the locally stored Atb so not relevant to any calculation
public static func == (lhs: Atb, rhs: Atb) -> Bool {
return lhs.droppingVariant == rhs.droppingVariant
}

/// Subtracts one ATB from the other.
/// @return difference in days
public static func - (lhs: Atb, rhs: Atb) -> Int {
return lhs.ageInDays - rhs.ageInDays
}

/// Gives age in days since first ATB. If badly formatted returns -1. Only the server should be giving us ATB values, so if it is giving us something wrong there are bigger problems in the world.
var ageInDays: Int {
numeric?.ageInDays ?? -1
}

/// Gives the current week or -1 if badly formatted
var week: Int {
numeric?.week ?? -1
}

var isReturningUser: Bool {
version.count == Self.templateWithVariant.count && version.hasSuffix("ru")
}

struct AtbNumeric {

let week: Int
let day: Int
let ageInDays: Int

static func makeFromVersion(_ version: String) -> AtbNumeric? {
let version = String(version.prefix(Atb.template.count))
guard version.count == Atb.template.count,
let week = Int(version.substring(1...3)),
let day = Int(version.substring(5...5)),
(1...7).contains(day) else {

return nil
}

return AtbNumeric(week: week, day: day, ageInDays: (week * 7) + (day - 1))
}

}

}

extension Atb {

var droppingVariant: String {
return String(version.prefix(Atb.template.count))
}

}

private extension String {

func substring(_ range: ClosedRange<Int>) -> String {
let startIndex = self.index(self.startIndex, offsetBy: range.lowerBound)
let endIndex = self.index(self.startIndex, offsetBy: min(self.count, range.upperBound + 1))
let substring = self[startIndex..<endIndex]
return String(substring)
}

}
108 changes: 108 additions & 0 deletions Sources/BrowserServicesKit/Statistics/UsageSegmentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// UsageSegmentation.swift
//
// Copyright © 2025 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
import Common

public enum UsageActivityType: String {

case search
case appUse = "app_use"

}

public protocol UsageSegmenting {

func processATB(_ atb: Atb, withInstallAtb installAtb: Atb, andActivityType activityType: UsageActivityType)

}

public enum UsageSegmentationPixel {
case usageSegments
}

public final class UsageSegmentation: UsageSegmenting {

public var pixelEvents: EventMapping<UsageSegmentationPixel>?
private let storage: UsageSegmentationStoring
private let calculatorFactory: UsageSegmentationCalculatorMaking

public init(pixelEvents: EventMapping<UsageSegmentationPixel>?,
storage: UsageSegmentationStoring = UsageSegmentationStorage(),
calculatorFactory: UsageSegmentationCalculatorMaking = DefaultCalculatorFactory()) {
self.pixelEvents = pixelEvents
self.storage = storage
self.calculatorFactory = calculatorFactory
}

public 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 {
pixelEvents?.fire(.usageSegments, parameters: 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
}
}

}
Loading

0 comments on commit 1349661

Please sign in to comment.