Skip to content
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

fix: improvements to app lifecycle monitoring #243

Merged
merged 1 commit into from
Nov 14, 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
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
Loading