Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #8274: Add support for the daily browser session time P3A metric (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
kylehickinson committed Feb 5, 2024
1 parent 9504684 commit 036165e
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 0 deletions.
1 change: 1 addition & 0 deletions App/iOS/Delegates/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class AppState {
public let profile: Profile
public let rewards: Brave.BraveRewards
public let newsFeedDataSource: FeedDataSource
public let uptimeMonitor = UptimeMonitor()
private var didBecomeActive = false

public var state: State = .launching(options: [:], active: false) {
Expand Down
2 changes: 2 additions & 0 deletions App/iOS/Delegates/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

Preferences.AppState.backgroundedCleanly.value = false
AppState.shared.profile.reopen()
AppState.shared.uptimeMonitor.beginMonitoring()

appDelegate.receivedURLs = nil
UIApplication.shared.applicationIconBadgeNumber = 0
Expand Down Expand Up @@ -248,6 +249,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneWillResignActive(_ scene: UIScene) {
Preferences.AppState.backgroundedCleanly.value = true
scene.userActivity?.resignCurrent()
AppState.shared.uptimeMonitor.pauseMonitoring()
}

func sceneWillEnterForeground(_ scene: UIScene) {
Expand Down
5 changes: 5 additions & 0 deletions Sources/Growth/GrowthPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ extension Preferences {
/// The date when the rating card in news feed is shown
public static let newsCardShownDate = Option<Date?>(key: "review.news-card", default: nil)
}

final class UptimeMonitor {
static let startTime: Option<Date?> = .init(key: "uptime-monitor-start-time", default: nil)
static let uptimeSum: Option<TimeInterval> = .init(key: "uptime-monitor-uptime-sum", default: 0)
}
}
92 changes: 92 additions & 0 deletions Sources/Growth/UptimeMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import Foundation
import Preferences

/// Monitors how long the browser is foregrounded to answer the `Brave.Uptime.BrowserOpenTime` P3A question
public class UptimeMonitor {
private var timer: Timer?

private(set) static var usageInterval: TimeInterval = 15
private(set) static var now: () -> Date = { .now }
private(set) static var calendar: Calendar = .current

public init() {
if Preferences.UptimeMonitor.startTime.value == nil {
// If today is the first time monitoring uptime, set the frame start time to now.
resetPrefs()
}
recordP3A()
}

deinit {
timer?.invalidate()
}

// For testing
var didRecordP3A: ((_ durationInMinutes: Int) -> Void)?

public var isMonitoring: Bool {
timer != nil
}

/// Begins a timer to monitor uptime
public func beginMonitoring() {
if isMonitoring {
return
}
timer = Timer.scheduledTimer(withTimeInterval: Self.usageInterval, repeats: true, block: { [weak self] _ in
guard let self else { return }
Preferences.UptimeMonitor.uptimeSum.value += Self.usageInterval
self.recordP3A()
})
}
/// Pauses the timer to monitor uptime
public func pauseMonitoring() {
timer?.invalidate()
timer = nil
}

private func recordP3A() {
guard let startTime = Preferences.UptimeMonitor.startTime.value,
!Self.calendar.isDate(startTime, inSameDayAs: Self.now()) else {
// Do not report, since 1 day has not passed.
return
}
let buckets: [Bucket] = [
.r(0...30),
.r(31...60),
.r(61...120), // 1-2 hours
.r(121...180), // 2-3 hours
.r(181...300), // 3-5 hours
.r(301...420), // 5-7 hours
.r(421...600), // 7-10 hours
.r(601...) // 10+ hours
]
let durationInMinutes = Int(Preferences.UptimeMonitor.uptimeSum.value / 60.0)
UmaHistogramRecordValueToBucket("Brave.Uptime.BrowserOpenMinutes", buckets: buckets, value: durationInMinutes)
resetPrefs()
didRecordP3A?(durationInMinutes)
}

private func resetPrefs() {
Preferences.UptimeMonitor.startTime.value = Self.now()
Preferences.UptimeMonitor.uptimeSum.value = 0
}

static func setUsageIntervalForTesting(_ usageInterval: TimeInterval) {
Self.usageInterval = usageInterval
}

static func setNowForTesting(_ now: @escaping () -> Date) {
Self.now = now
}

static func setCalendarForTesting(_ calendar: Calendar) {
Self.calendar = calendar
}
}

76 changes: 76 additions & 0 deletions Tests/GrowthTests/UptimeMonitorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import Foundation
import XCTest
import Preferences
@testable import Growth

class UptimeMonitorTests: XCTestCase {

// How much a single second is actually accounted for during the test
static let testSecond: TimeInterval = 0.01

override func setUp() {
super.setUp()

Preferences.UptimeMonitor.startTime.reset()
Preferences.UptimeMonitor.uptimeSum.reset()

var testCalendar = Calendar(identifier: .gregorian)
testCalendar.timeZone = .init(abbreviation: "GMT")!
testCalendar.locale = .init(identifier: "en_US_POSIX")
UptimeMonitor.setCalendarForTesting(testCalendar)
UptimeMonitor.setNowForTesting({ .now })
UptimeMonitor.setUsageIntervalForTesting(Self.testSecond)
}

func testNoStartTimeInit() {
XCTAssertNil(Preferences.UptimeMonitor.startTime.value)
let um = UptimeMonitor()
XCTAssertNotNil(Preferences.UptimeMonitor.startTime.value)
XCTAssertFalse(um.isMonitoring)
}

func testRecordAfterDay() {
let um = UptimeMonitor()
let e = expectation(description: "recorded")
let now = Date()
UptimeMonitor.setNowForTesting({ now })
Preferences.UptimeMonitor.startTime.value = .now.addingTimeInterval(-60*60*24)
Preferences.UptimeMonitor.uptimeSum.value = 60
um.didRecordP3A = { minutes in
XCTAssertEqual(minutes, 1)
e.fulfill()
}
um.beginMonitoring()
wait(for: [e], timeout: Self.testSecond * 2)
um.pauseMonitoring()

// Ensure prefs are reset
XCTAssertEqual(Preferences.UptimeMonitor.startTime.value, now)
XCTAssertEqual(Preferences.UptimeMonitor.uptimeSum.value, 0)
}

func testNoRecordBeforeOneDay() {
let um = UptimeMonitor()
let now = Date()
UptimeMonitor.setNowForTesting({ now })
let e = expectation(description: "not-recorded")
e.isInverted = true
Preferences.UptimeMonitor.startTime.value = now
Preferences.UptimeMonitor.uptimeSum.value = 60
um.didRecordP3A = { _ in
XCTFail("Should not record any data before a day has passed")
}
um.beginMonitoring()
wait(for: [e], timeout: Self.testSecond * 2)
um.pauseMonitoring()

// Ensure prefs are not reset
XCTAssertEqual(Preferences.UptimeMonitor.startTime.value, now)
XCTAssertNotEqual(Preferences.UptimeMonitor.uptimeSum.value, 0)
}
}

0 comments on commit 036165e

Please sign in to comment.