Skip to content

Commit

Permalink
fix: improvements to app lifecycle monitoring (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
crleona authored Nov 14, 2024
1 parent f27b4f7 commit 8098782
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 100 deletions.
138 changes: 61 additions & 77 deletions Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,42 @@ import Foundation
import SwiftUI

class IOSLifecycleMonitor: UtilityPlugin {
private var application: UIApplication?
private var appNotifications: [NSNotification.Name] = [
UIApplication.didEnterBackgroundNotification,
UIApplication.willEnterForegroundNotification,
UIApplication.didFinishLaunchingNotification,
UIApplication.didBecomeActiveNotification,
]

private var utils: DefaultEventUtils?
private var sendApplicationOpenedOnDidBecomeActive = false

override init() {
// TODO: Check if lifecycle plugin works for app extension
// App extensions can't use UIApplication.shared, so
// funnel it through something to check; Could be nil.
application = IOSVendorSystem.sharedApplication
super.init()
setupListeners()

NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidFinishLaunchingNotification(notification:)),
name: UIApplication.didFinishLaunchingNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive(notification:)),
name: UIApplication.didBecomeActiveNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillEnterForeground(notification:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidEnterBackground(notification:)),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
}

public override func setup(amplitude: Amplitude) {
super.setup(amplitude: amplitude)
utils = DefaultEventUtils(amplitude: amplitude)

// If we are already in the foreground, dispatch installed / opened events now
if IOSVendorSystem.sharedApplication?.applicationState == .active {
utils?.trackAppUpdatedInstalledEvent()
amplitude.onEnterForeground(timestamp: currentTimestamp)
utils?.trackAppOpenedEvent()
}

if amplitude.configuration.autocapture.contains(.screenViews) {
UIKitScreenViews.register(amplitude)
}
Expand All @@ -42,40 +56,39 @@ class IOSLifecycleMonitor: UtilityPlugin {
}

@objc
func notificationResponse(notification: Notification) {
switch notification.name {
case UIApplication.didEnterBackgroundNotification:
didEnterBackground(notification: notification)
case UIApplication.willEnterForegroundNotification:
applicationWillEnterForeground(notification: notification)
case UIApplication.didFinishLaunchingNotification:
applicationDidFinishLaunchingNotification(notification: notification)
case UIApplication.didBecomeActiveNotification:
applicationDidBecomeActive(notification: notification)
default:
break
func applicationDidFinishLaunchingNotification(notification: Notification) {
utils?.trackAppUpdatedInstalledEvent()

// Pre SceneDelegate apps wil not fire a willEnterForeground notification on app launch.
// Instead, use the initial applicationDidBecomeActive
let sceneManifest = Bundle.main.infoDictionary?["UIApplicationSceneManifest"] as? [String: Any]
let sceneConfigurations = sceneManifest?["UISceneConfigurations"] as? [String: Any] ?? [:]
let hasSceneConfigurations = !sceneConfigurations.isEmpty

let appDelegate = IOSVendorSystem.sharedApplication?.delegate
let selector = #selector(UIApplicationDelegate.application(_:configurationForConnecting:options:))
let usesSceneDelegate = appDelegate?.responds(to: selector) ?? false

if !(hasSceneConfigurations || usesSceneDelegate) {
sendApplicationOpenedOnDidBecomeActive = true
}
}

func setupListeners() {
// Configure the current life cycle events
let notificationCenter = NotificationCenter.default
for notification in appNotifications {
notificationCenter.addObserver(
self,
selector: #selector(notificationResponse(notification:)),
name: notification,
object: application
)
@objc
func applicationDidBecomeActive(notification: Notification) {
guard sendApplicationOpenedOnDidBecomeActive else {
return
}
sendApplicationOpenedOnDidBecomeActive = false

amplitude?.onEnterForeground(timestamp: currentTimestamp)
utils?.trackAppOpenedEvent()
}

@objc
func applicationWillEnterForeground(notification: Notification) {
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)

let fromBackground: Bool
if let sharedApplication = application {
if let sharedApplication = IOSVendorSystem.sharedApplication {
switch sharedApplication.applicationState {
case .active, .inactive:
fromBackground = false
Expand All @@ -88,52 +101,23 @@ class IOSLifecycleMonitor: UtilityPlugin {
fromBackground = false
}

amplitude?.onEnterForeground(timestamp: timestamp)
sendApplicationOpened(fromBackground: fromBackground)
amplitude?.onEnterForeground(timestamp: currentTimestamp)
utils?.trackAppOpenedEvent(fromBackground: fromBackground)
}

func didEnterBackground(notification: Notification) {
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
self.amplitude?.onExitForeground(timestamp: timestamp)
if amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false {
self.amplitude?.track(eventType: Constants.AMP_APPLICATION_BACKGROUNDED_EVENT)
}
}

func applicationDidFinishLaunchingNotification(notification: Notification) {
utils?.trackAppUpdatedInstalledEvent()

// Pre SceneDelegate apps wil not fire a willEnterForeground notification on app launch.
// Instead, use the initial applicationDidBecomeActive
let usesSceneDelegate = application?.delegate?.responds(to: #selector(UIApplicationDelegate.application(_:configurationForConnecting:options:))) ?? false
if !usesSceneDelegate {
sendApplicationOpenedOnDidBecomeActive = true
}
}

func applicationDidBecomeActive(notification: Notification) {
guard sendApplicationOpenedOnDidBecomeActive else {
@objc
func applicationDidEnterBackground(notification: Notification) {
guard let amplitude = amplitude else {
return
}
sendApplicationOpenedOnDidBecomeActive = false

let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
amplitude?.onEnterForeground(timestamp: timestamp)
sendApplicationOpened(fromBackground: false)
amplitude.onExitForeground(timestamp: currentTimestamp)
if amplitude.configuration.autocapture.contains(.appLifecycles) {
amplitude.track(eventType: Constants.AMP_APPLICATION_BACKGROUNDED_EVENT)
}
}

private func sendApplicationOpened(fromBackground: Bool) {
guard amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false else {
return
}
let info = Bundle.main.infoDictionary
let currentBuild = info?["CFBundleVersion"] as? String
let currentVersion = info?["CFBundleShortVersionString"] as? String
self.amplitude?.track(eventType: Constants.AMP_APPLICATION_OPENED_EVENT, eventProperties: [
Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "",
Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "",
Constants.AMP_APP_FROM_BACKGROUND_PROPERTY: fromBackground,
])
private var currentTimestamp: Int64 {
return Int64(NSDate().timeIntervalSince1970 * 1000)
}
}

Expand Down
69 changes: 48 additions & 21 deletions Sources/Amplitude/Utilities/DefaultEventUtils.swift
Original file line number Diff line number Diff line change
@@ -1,42 +1,69 @@
import Foundation

public class DefaultEventUtils {

private static var instanceNamesThatSentAppUpdatedInstalled: Set<String> = []

private weak var amplitude: Amplitude?

public init(amplitude: Amplitude) {
self.amplitude = amplitude
}

public func trackAppUpdatedInstalledEvent() {
guard let amplitude = amplitude else {
return
}

let info = Bundle.main.infoDictionary
let currentBuild = info?["CFBundleVersion"] as? String
let currentVersion = info?["CFBundleShortVersionString"] as? String
let previousBuild: String? = amplitude?.storage.read(key: StorageKey.APP_BUILD)
let previousVersion: String? = amplitude?.storage.read(key: StorageKey.APP_VERSION)

if amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false {
let lastEventTime: Int64? = amplitude?.storage.read(key: StorageKey.LAST_EVENT_TIME)
if lastEventTime == nil {
self.amplitude?.track(eventType: Constants.AMP_APPLICATION_INSTALLED_EVENT, eventProperties: [
Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "",
Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "",
])
} else if currentBuild != previousBuild {
self.amplitude?.track(eventType: Constants.AMP_APPLICATION_UPDATED_EVENT, eventProperties: [
Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "",
Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "",
Constants.AMP_APP_PREVIOUS_BUILD_PROPERTY: previousBuild ?? "",
Constants.AMP_APP_PREVIOUS_VERSION_PROPERTY: previousVersion ?? "",
])
}
}
let previousBuild: String? = amplitude.storage.read(key: StorageKey.APP_BUILD)
let previousVersion: String? = amplitude.storage.read(key: StorageKey.APP_VERSION)

if currentBuild != previousBuild {
try? amplitude?.storage.write(key: StorageKey.APP_BUILD, value: currentBuild)
try? amplitude.storage.write(key: StorageKey.APP_BUILD, value: currentBuild)
}
if currentVersion != previousVersion {
try? amplitude?.storage.write(key: StorageKey.APP_VERSION, value: currentVersion)
try? amplitude.storage.write(key: StorageKey.APP_VERSION, value: currentVersion)
}

guard amplitude.configuration.autocapture.contains(.appLifecycles),
!Self.instanceNamesThatSentAppUpdatedInstalled.contains(amplitude.configuration.instanceName) else {
return
}
// Only send one app installed / updated event per instance name, no matter how many times we are
// reinitialized
Self.instanceNamesThatSentAppUpdatedInstalled.insert(amplitude.configuration.instanceName)

if previousBuild == nil || previousVersion == nil {
amplitude.track(eventType: Constants.AMP_APPLICATION_INSTALLED_EVENT, eventProperties: [
Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "",
Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "",
])
} else if currentBuild != previousBuild || currentVersion != previousVersion {
amplitude.track(eventType: Constants.AMP_APPLICATION_UPDATED_EVENT, eventProperties: [
Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "",
Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "",
Constants.AMP_APP_PREVIOUS_BUILD_PROPERTY: previousBuild ?? "",
Constants.AMP_APP_PREVIOUS_VERSION_PROPERTY: previousVersion ?? "",
])
}
}

func trackAppOpenedEvent(fromBackground: Bool = false) {
guard let amplitude = amplitude,
amplitude.configuration.autocapture.contains(.appLifecycles) else {
return
}

let info = Bundle.main.infoDictionary
let currentBuild = info?["CFBundleVersion"] as? String
let currentVersion = info?["CFBundleShortVersionString"] as? String
self.amplitude?.track(eventType: Constants.AMP_APPLICATION_OPENED_EVENT, eventProperties: [
Constants.AMP_APP_BUILD_PROPERTY: currentBuild ?? "",
Constants.AMP_APP_VERSION_PROPERTY: currentVersion ?? "",
Constants.AMP_APP_FROM_BACKGROUND_PROPERTY: fromBackground,
])
}
}
7 changes: 5 additions & 2 deletions Tests/AmplitudeTests/AmplitudeIOSTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ final class AmplitudeIOSTests: XCTestCase {
window.addSubview(rootViewController.view)
}

func testDidFinishLaunching_ApplicationInstalled() {
func testDidFinishLaunching_ApplicationInstalled() throws {
let configuration = Configuration(
apiKey: "api-key",
instanceName: #function,
storageProvider: storageMem,
identifyStorageProvider: interceptStorageMem,
autocapture: .appLifecycles
)
let amplitude = Amplitude(configuration: configuration)
try storageMem.write(key: StorageKey.APP_BUILD, value: nil)
try storageMem.write(key: StorageKey.APP_VERSION, value: nil)
NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil)

amplitude.waitForTrackingQueue()
Expand All @@ -47,11 +50,11 @@ final class AmplitudeIOSTests: XCTestCase {
func testDidFinishLaunching_ApplicationUpdated() throws {
let configuration = Configuration(
apiKey: "api-key",
instanceName: #function,
storageProvider: storageMem,
identifyStorageProvider: interceptStorageMem,
autocapture: .appLifecycles
)
try storageMem.write(key: StorageKey.LAST_EVENT_TIME, value: 123 as Int64)
try storageMem.write(key: StorageKey.APP_BUILD, value: "abc")
try storageMem.write(key: StorageKey.APP_VERSION, value: "xyz")
let amplitude = Amplitude(configuration: configuration)
Expand Down

0 comments on commit 8098782

Please sign in to comment.