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

Fix #8274: Add support for the daily browser session time P3A metric #8730

Merged
merged 1 commit into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading