Skip to content

[Woo POS][Local Catalog] Beta-fix Add Better Analytics to Background Task Refresh System #16014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 18, 2025
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
import Foundation
import WooFoundation

extension WooAnalyticsEvent {
enum BackgroundUpdates {

private enum Keys {
static let timeTaken = "time_taken"
static let backgroundTimeGranted = "background_time_granted"
static let networkType = "network_type"
static let isExpensiveConnection = "is_expensive_connection"
static let isLowDataMode = "is_low_data_mode"
static let isPowered = "is_powered"
static let batteryLevel = "battery_level"
static let isLowPowerMode = "is_low_power_mode"
static let timeSinceLastRun = "time_since_last_run"
}

static func dataSynced(timeTaken: TimeInterval) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .backgroundDataSynced, properties: [Keys.timeTaken: timeTaken])
static func dataSynced(
timeTaken: TimeInterval,
backgroundTimeGranted: TimeInterval?,
networkType: String,
isExpensiveConnection: Bool,
isLowDataMode: Bool,
isPowered: Bool,
batteryLevel: Float,
isLowPowerMode: Bool,
timeSinceLastRun: TimeInterval?
) -> WooAnalyticsEvent {
var properties: [String: WooAnalyticsEventPropertyType] = [
Keys.timeTaken: Int64(timeTaken),
Keys.networkType: networkType,
Keys.isExpensiveConnection: isExpensiveConnection,
Keys.isLowDataMode: isLowDataMode,
Keys.isPowered: isPowered,
Keys.batteryLevel: Float64(batteryLevel),
Keys.isLowPowerMode: isLowPowerMode
]

if let backgroundTimeGranted = backgroundTimeGranted {
properties[Keys.backgroundTimeGranted] = Int64(backgroundTimeGranted)
}

if let timeSinceLastRun = timeSinceLastRun {
properties[Keys.timeSinceLastRun] = Int64(timeSinceLastRun)
}

return WooAnalyticsEvent(statName: .backgroundDataSynced, properties: properties)
}

static func dataSyncError(_ error: Error) -> WooAnalyticsEvent {
Expand Down
1 change: 1 addition & 0 deletions WooCommerce/Classes/Extensions/UserDefaults+Woo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ extension UserDefaults {

// Background Task Refresh
case latestBackgroundOrderSyncDate
case lastBackgroundRefreshCompletionTime

// Blaze Local notification
case blazeNoCampaignReminderOpened
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import UIKit
import Foundation
import BackgroundTasks
import Network

final class BackgroundTaskRefreshDispatcher {

Expand Down Expand Up @@ -60,6 +61,7 @@ final class BackgroundTaskRefreshDispatcher {
// Launch all refresh tasks in parallel.
let refreshTasks = Task {
do {
async let systemInfo = BackgroundTaskSystemInfo()

let startTime = Date.now

Expand All @@ -78,7 +80,27 @@ final class BackgroundTaskRefreshDispatcher {
}

let timeTaken = round(Date.now.timeIntervalSince(startTime))
ServiceLocator.analytics.track(event: .BackgroundUpdates.dataSynced(timeTaken: timeTaken))

var timeSinceLastRun: TimeInterval? = nil
if let lastRunTime = UserDefaults.standard[.lastBackgroundRefreshCompletionTime] as? Date {
timeSinceLastRun = round(lastRunTime.timeIntervalSinceNow.magnitude)
}

await ServiceLocator.analytics.track(event: .BackgroundUpdates.dataSynced(
timeTaken: timeTaken,
backgroundTimeGranted: systemInfo.backgroundTimeGranted,
networkType: systemInfo.networkType,
isExpensiveConnection: systemInfo.isExpensiveConnection,
isLowDataMode: systemInfo.isLowDataMode,
isPowered: systemInfo.isPowered,
batteryLevel: systemInfo.batteryLevel,
isLowPowerMode: systemInfo.isLowPowerMode,
timeSinceLastRun: timeSinceLastRun
))

// Save date, for use in analytics next time we refresh
UserDefaults.standard[.lastBackgroundRefreshCompletionTime] = Date.now

backgroundTask.setTaskCompleted(success: true)

} catch {
Expand All @@ -93,7 +115,7 @@ final class BackgroundTaskRefreshDispatcher {
ServiceLocator.analytics.track(event: .BackgroundUpdates.dataSyncError(BackgroundError.expired))
refreshTasks.cancel()
}
}
}
}

private extension BackgroundTaskRefreshDispatcher {
Expand All @@ -109,3 +131,87 @@ extension BackgroundTaskRefreshDispatcher {
case expired
}
}

// MARK: - System Information Helper

private struct NetworkInfo {
let type: String
let isExpensive: Bool
let isLowDataMode: Bool
}

private struct BackgroundTaskSystemInfo {
let backgroundTimeGranted: TimeInterval?
private let networkInfo: NetworkInfo
let isPowered: Bool
let batteryLevel: Float
let isLowPowerMode: Bool

// Computed properties for clean external access
var networkType: String { networkInfo.type }
var isExpensiveConnection: Bool { networkInfo.isExpensive }
var isLowDataMode: Bool { networkInfo.isLowDataMode }

@MainActor
init() async {
// Background time granted (nil if foreground/unlimited)
let backgroundTime = UIApplication.shared.backgroundTimeRemaining
self.backgroundTimeGranted = backgroundTime < Double.greatestFiniteMagnitude ? backgroundTime : nil

// Network info
self.networkInfo = await Self.getNetworkInfo()

// Power and battery info
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true

self.isPowered = device.batteryState == .charging || device.batteryState == .full
self.batteryLevel = device.batteryLevel
self.isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled

device.isBatteryMonitoringEnabled = false
}

private static func getNetworkInfo() async -> NetworkInfo {
return await withCheckedContinuation { continuation in
let monitor = NWPathMonitor()

monitor.pathUpdateHandler = { path in
continuation.resume(returning: NetworkInfo(path: path))
monitor.cancel()
}

let queue = DispatchQueue(label: "network.monitor.queue")
monitor.start(queue: queue)
}
}
}

private extension NetworkInfo {
init(path: NWPath) {
guard path.status == .satisfied else {
self.type = "no_connection"
self.isExpensive = false
self.isLowDataMode = false
return
}

self.type = Self.networkType(from: path)
self.isExpensive = path.isExpensive
self.isLowDataMode = path.isConstrained
}

private static func networkType(from path: NWPath) -> String {
if path.usesInterfaceType(.wifi) {
return "wifi"
} else if path.usesInterfaceType(.cellular) {
return "cellular"
} else if path.usesInterfaceType(.wiredEthernet) {
return "ethernet"
} else if path.usesInterfaceType(.loopback) {
return "loopback"
} else {
return "other"
}
}
}