mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-12 08:13:26 +01:00
Adds VS App Center analytics + crash reporting
Currently tracks install, refresh, and update app events.
This commit is contained in:
97
AltStore/Analytics/AnalyticsManager.swift
Normal file
97
AltStore/Analytics/AnalyticsManager.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// AnalyticsManager.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 3/31/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AppCenter
|
||||
import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
extension AnalyticsManager
|
||||
{
|
||||
enum EventProperty: String
|
||||
{
|
||||
case name
|
||||
case bundleIdentifier
|
||||
case developerName
|
||||
case version
|
||||
case size
|
||||
case tintColor
|
||||
case sourceIdentifier
|
||||
case sourceURL
|
||||
}
|
||||
|
||||
enum Event
|
||||
{
|
||||
case installedApp(InstalledApp)
|
||||
case updatedApp(InstalledApp)
|
||||
case refreshedApp(InstalledApp)
|
||||
|
||||
var name: String {
|
||||
switch self
|
||||
{
|
||||
case .installedApp: return "installed_app"
|
||||
case .updatedApp: return "updated_app"
|
||||
case .refreshedApp: return "refreshed_app"
|
||||
}
|
||||
}
|
||||
|
||||
var properties: [EventProperty: String] {
|
||||
let properties: [EventProperty: String?]
|
||||
|
||||
switch self
|
||||
{
|
||||
case .installedApp(let app), .updatedApp(let app), .refreshedApp(let app):
|
||||
let appBundleURL = InstalledApp.fileURL(for: app)
|
||||
let appBundleSize = FileManager.default.directorySize(at: appBundleURL)
|
||||
|
||||
properties = [
|
||||
.name: app.name,
|
||||
.bundleIdentifier: app.bundleIdentifier,
|
||||
.developerName: app.storeApp?.developerName,
|
||||
.version: app.version,
|
||||
.size: appBundleSize?.description,
|
||||
.tintColor: app.storeApp?.tintColor?.hexString,
|
||||
.sourceIdentifier: app.storeApp?.sourceIdentifier,
|
||||
.sourceURL: app.storeApp?.source?.sourceURL.absoluteString
|
||||
]
|
||||
}
|
||||
|
||||
return properties.compactMapValues { $0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsManager
|
||||
{
|
||||
static let shared = AnalyticsManager()
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
extension AnalyticsManager
|
||||
{
|
||||
func start()
|
||||
{
|
||||
MSAppCenter.start("bb08e9bb-c126-408d-bf3f-324c8473fd40", withServices:[
|
||||
MSAnalytics.self,
|
||||
MSCrashes.self
|
||||
])
|
||||
}
|
||||
|
||||
func trackEvent(_ event: Event)
|
||||
{
|
||||
let properties = event.properties.reduce(into: [:]) { (properties, item) in
|
||||
properties[item.key.rawValue] = item.value
|
||||
}
|
||||
|
||||
MSAnalytics.trackEvent(event.name, withProperties: properties)
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
AnalyticsManager.shared.start()
|
||||
|
||||
self.setTintColor()
|
||||
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
36
AltStore/Extensions/FileManager+DirectorySize.swift
Normal file
36
AltStore/Extensions/FileManager+DirectorySize.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// FileManager+DirectorySize.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 3/31/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager
|
||||
{
|
||||
func directorySize(at directoryURL: URL) -> Int?
|
||||
{
|
||||
guard let enumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [.fileSizeKey]) else { return nil }
|
||||
|
||||
var total: Int = 0
|
||||
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
do
|
||||
{
|
||||
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||
guard let fileSize = resourceValues.fileSize else { continue }
|
||||
|
||||
total += fileSize
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to read file size for item: \(fileURL).", error)
|
||||
}
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,17 @@ import UIKit
|
||||
|
||||
extension UIColor
|
||||
{
|
||||
// Borrowed from https://stackoverflow.com/a/26341062
|
||||
var hexString: String {
|
||||
let components = self.cgColor.components
|
||||
let r: CGFloat = components?[0] ?? 0.0
|
||||
let g: CGFloat = components?[1] ?? 0.0
|
||||
let b: CGFloat = components?[2] ?? 0.0
|
||||
|
||||
let hexString = String.init(format: "%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255)))
|
||||
return hexString
|
||||
}
|
||||
|
||||
// Borrowed from https://stackoverflow.com/a/33397427
|
||||
convenience init?(hexString: String)
|
||||
{
|
||||
|
||||
@@ -269,6 +269,35 @@ extension AppManager
|
||||
return group.progress
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func update(_ app: InstalledApp, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
guard let storeApp = app.storeApp else {
|
||||
completionHandler(.failure(OperationError.appNotFound))
|
||||
return Progress.discreteProgress(totalUnitCount: 1)
|
||||
}
|
||||
|
||||
let group = RefreshGroup(context: context)
|
||||
group.completionHandler = { (results) in
|
||||
do
|
||||
{
|
||||
guard let result = results.values.first else { throw OperationError.unknown }
|
||||
completionHandler(result)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
let operation = AppOperation.update(storeApp)
|
||||
assert(operation.app as AnyObject === storeApp) // Make sure we never accidentally "update" to already installed app.
|
||||
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
|
||||
return group.progress
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: RefreshGroup? = nil) -> RefreshGroup
|
||||
{
|
||||
@@ -332,12 +361,13 @@ private extension AppManager
|
||||
enum AppOperation
|
||||
{
|
||||
case install(AppProtocol)
|
||||
case update(AppProtocol)
|
||||
case refresh(AppProtocol)
|
||||
|
||||
var app: AppProtocol {
|
||||
switch self
|
||||
{
|
||||
case .install(let app), .refresh(let app): return app
|
||||
case .install(let app), .update(let app), .refresh(let app): return app
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,16 +434,16 @@ private extension AppManager
|
||||
case .refresh(let installedApp as InstalledApp) where installedApp.certificateSerialNumber == group.context.certificate?.serialNumber:
|
||||
// Refreshing apps, but using same certificate as last time, so we can just refresh provisioning profiles.
|
||||
|
||||
let refreshProgress = self._refresh(installedApp, group: group) { (result) in
|
||||
let refreshProgress = self._refresh(installedApp, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .refresh(let app), .install(let app):
|
||||
case .refresh(let app), .install(let app), .update(let app):
|
||||
// Either installing for first time, or refreshing with a different signing certificate,
|
||||
// so we need to resign the app then install it.
|
||||
|
||||
let installProgress = self._install(app, group: group) { (result) in
|
||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(installProgress, withPendingUnitCount: 80)
|
||||
@@ -444,13 +474,25 @@ private extension AppManager
|
||||
return group
|
||||
}
|
||||
|
||||
|
||||
private func _install(_ app: AppProtocol, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
context.beginInstallationHandler = group.beginInstallationHandler
|
||||
context.beginInstallationHandler = { (installedApp) in
|
||||
switch operation
|
||||
{
|
||||
case .update where installedApp.bundleIdentifier == StoreApp.altstoreAppID:
|
||||
// AltStore will quit before installation finishes,
|
||||
// so assume if we get this far the update will finish successfully.
|
||||
let event = AnalyticsManager.Event.updatedApp(installedApp)
|
||||
AnalyticsManager.shared.trackEvent(event)
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
group.beginInstallationHandler?(installedApp)
|
||||
}
|
||||
|
||||
/* Download */
|
||||
let downloadOperation = DownloadAppOperation(app: app, context: context)
|
||||
@@ -546,7 +588,7 @@ private extension AppManager
|
||||
return progress
|
||||
}
|
||||
|
||||
private func _refresh(_ app: InstalledApp, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
private func _refresh(_ app: InstalledApp, operation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
@@ -575,7 +617,7 @@ private extension AppManager
|
||||
case .failure(ALTServerError.unknownRequest):
|
||||
// Fall back to installation if AltServer doesn't support newer provisioning profile requests.
|
||||
app.managedObjectContext?.performAndWait { // Must performAndWait to ensure we add operations before we return.
|
||||
let installProgress = self._install(app, group: group) { (result) in
|
||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
progress.addChild(installProgress, withPendingUnitCount: 40)
|
||||
@@ -635,6 +677,26 @@ private extension AppManager
|
||||
self.scheduleExpirationWarningLocalNotification(for: installedApp)
|
||||
}
|
||||
|
||||
let event: AnalyticsManager.Event?
|
||||
|
||||
switch operation
|
||||
{
|
||||
case .install: event = .installedApp(installedApp)
|
||||
case .refresh: event = .refreshedApp(installedApp)
|
||||
case .update where installedApp.bundleIdentifier == StoreApp.altstoreAppID:
|
||||
// AltStore quits before update finishes, so we've preemptively logged this update event.
|
||||
// In case AltStore doesn't quit, such as when update has a different bundle identifier,
|
||||
// make sure we don't log this update event a second time.
|
||||
event = nil
|
||||
|
||||
case .update: event = .updatedApp(installedApp)
|
||||
}
|
||||
|
||||
if let event = event
|
||||
{
|
||||
AnalyticsManager.shared.trackEvent(event)
|
||||
}
|
||||
|
||||
do { try installedApp.managedObjectContext?.save() }
|
||||
catch { print("Error saving installed app.", error) }
|
||||
}
|
||||
@@ -693,7 +755,7 @@ private extension AppManager
|
||||
{
|
||||
switch operation
|
||||
{
|
||||
case .install: return self.installationProgress[operation.bundleIdentifier]
|
||||
case .install, .update: return self.installationProgress[operation.bundleIdentifier]
|
||||
case .refresh: return self.refreshProgress[operation.bundleIdentifier]
|
||||
}
|
||||
}
|
||||
@@ -702,7 +764,7 @@ private extension AppManager
|
||||
{
|
||||
switch operation
|
||||
{
|
||||
case .install: self.installationProgress[operation.bundleIdentifier] = progress
|
||||
case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress
|
||||
case .refresh: self.refreshProgress[operation.bundleIdentifier] = progress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,15 +640,15 @@ private extension MyAppsViewController
|
||||
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||
|
||||
guard let storeApp = self.dataSource.item(at: indexPath).storeApp else { return }
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
|
||||
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
|
||||
guard previousProgress == nil else {
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
_ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in
|
||||
_ = AppManager.shared.update(installedApp, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
@@ -662,7 +662,7 @@ private extension MyAppsViewController
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
|
||||
case .success:
|
||||
print("Updated app:", storeApp.bundleIdentifier)
|
||||
print("Updated app:", installedApp.bundleIdentifier)
|
||||
// No need to reload, since the the update cell is gone now.
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user