Skip to content

Commit

Permalink
Add user interactions autocapture
Browse files Browse the repository at this point in the history
  • Loading branch information
PouriaAmini committed Jul 1, 2024
1 parent acbe89d commit e612696
Show file tree
Hide file tree
Showing 9 changed files with 1,015 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"pins" : [
{
"identity" : "analytics-connector-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/amplitude/analytics-connector-ios.git",
"state" : {
"revision" : "e2ca17ac735bcbc48b13062484541702ef45153d",
"version" : "1.0.3"
}
},
{
"identity" : "experiment-ios-client",
"kind" : "remoteSourceControl",
"location" : "https://github.com/amplitude/experiment-ios-client",
"state" : {
"revision" : "6a94d70915b3756daae2a989fa57f03c19547c67",
"version" : "1.13.5"
}
}
],
"version" : 2
}
6 changes: 6 additions & 0 deletions Sources/Amplitude/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public struct Constants {
static let AMP_APPLICATION_BACKGROUNDED_EVENT = "\(AMP_AMPLITUDE_PREFIX)Application Backgrounded"
static let AMP_DEEP_LINK_OPENED_EVENT = "\(AMP_AMPLITUDE_PREFIX)Deep Link Opened"
static let AMP_SCREEN_VIEWED_EVENT = "\(AMP_AMPLITUDE_PREFIX)Screen Viewed"
static let AMP_USER_INTERACTION_EVENT = "\(AMP_AMPLITUDE_PREFIX)User Interaction"
static let AMP_REVENUE_EVENT = "revenue_amount"

static let AMP_APP_VERSION_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Version"
Expand All @@ -93,6 +94,11 @@ public struct Constants {
static let AMP_APP_LINK_URL_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Link URL"
static let AMP_APP_LINK_REFERRER_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Link Referrer"
static let AMP_APP_SCREEN_NAME_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Screen Name"

static let AMP_INTERACTION_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Interaction"
static let AMP_ELEMENT_LABEL_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Element Label"
static let AMP_ELEMENT_VALUE_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Element Value"
static let AMP_ELEMENT_TYPE_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Element Type"

public struct Configuration {
public static let FLUSH_QUEUE_SIZE = 30
Expand Down
12 changes: 12 additions & 0 deletions Sources/Amplitude/DefaultTrackingOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class DefaultTrackingOptions {
public var sessions: Bool = true
public var appLifecycles: Bool
public var screenViews: Bool
public var userInteractions: Bool

public init(
sessions: Bool = true,
Expand All @@ -20,5 +21,16 @@ public class DefaultTrackingOptions {
self.sessions = sessions
self.appLifecycles = appLifecycles
self.screenViews = screenViews
self.userInteractions = false
}

public convenience init (
sessions: Bool = true,
appLifecycles: Bool = false,
screenViews: Bool = false,
userInteractions: Bool = false
) {
self.init(sessions: sessions, appLifecycles: appLifecycles, screenViews: screenViews)
self.userInteractions = userInteractions
}
}
60 changes: 60 additions & 0 deletions Sources/Amplitude/Events/UserInteractionEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import UIKit

public class UserInteractionEvent: BaseEvent {

public enum InteractionValue {
case tap(dead: Bool = false)
case longPress(dead: Bool = false)
case rageTap
case focusGained
case focusLost(didTextFieldChange: Bool = false)
case sliderChanged(to: Int)

var description: String {
switch self {
case .tap(let dead): return dead ? "Dead Tapped" : "Tapped"
case .longPress(let dead): return dead ? "Dead Long Pressed" : "Long Pressed"
case .rageTap: return "Rage Tapped"
case .focusGained: return "Focus Gained"
case .focusLost(let didTextFieldChange):
return didTextFieldChange ? "Focus Lost After Text Modification" : "Focus Lost"
case .sliderChanged(let percentage):
return "Value Changed To \(percentage)%"
}
}
}

convenience init(_ interactionValue: InteractionValue, label: String? = nil, value: String? = nil, type: UIAccessibilityTraits = .none) {
self.init(eventType: Constants.AMP_USER_INTERACTION_EVENT, eventProperties: [
Constants.AMP_INTERACTION_PROPERTY: interactionValue.description,
Constants.AMP_ELEMENT_LABEL_PROPERTY: label,
Constants.AMP_ELEMENT_VALUE_PROPERTY: value,
Constants.AMP_ELEMENT_TYPE_PROPERTY: type.stringify()
])
}
}

extension UIAccessibilityTraits {
func stringify() -> String? {
var strings = [String]()
if contains(.adjustable) { strings.append("Adjustable") }
if contains(.allowsDirectInteraction) { strings.append("Allows Direct Interaction") }
if contains(.button) { strings.append("Button") }
if contains(.causesPageTurn) { strings.append("Causes Page Turn") }
if contains(.header) { strings.append("Header") }
if contains(.image) { strings.append("Image") }
if contains(.keyboardKey) { strings.append("Keyboard Key") }
if contains(.link) { strings.append("Link") }
if contains(.notEnabled) { strings.append("Not Enabled") }
if contains(.playsSound) { strings.append("Plays Sound") }
if contains(.searchField) { strings.append("Search Field") }
if contains(.selected) { strings.append("Selected") }
if contains(.startsMediaSession) { strings.append("Starts Media Session") }
if contains(.staticText) { strings.append("Static Text") }
if contains(.summaryElement) { strings.append("Summary Element") }
if contains(.tabBar) { strings.append("Tab Bar") }
if contains(.updatesFrequently) { strings.append("Updates Frequently") }

return strings.isEmpty ? nil : strings.joined(separator: ", ")
}
}
216 changes: 111 additions & 105 deletions Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,131 +7,137 @@

#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)

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 = UIApplication.value(forKeyPath: "sharedApplication") as? UIApplication
super.init()
setupListeners()
import Foundation
import SwiftUI

class IOSLifecycleMonitor: UtilityPlugin {
private var application: UIApplication? {
// 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.
UIApplication.value(forKeyPath: "sharedApplication") as? UIApplication
}

private var appNotifications: [NSNotification.Name] = [
UIApplication.didEnterBackgroundNotification,
UIApplication.willEnterForegroundNotification,
UIApplication.didFinishLaunchingNotification,
UIApplication.didBecomeActiveNotification,
]
private var utils: DefaultEventUtils?
private var sendApplicationOpenedOnDidBecomeActive = false
private var userInteractionCaptureDelegate: UserInteractionCaptureDelegate?

override init() {
super.init()
setupListeners()
}

public override func setup(amplitude: Amplitude) {
super.setup(amplitude: amplitude)
utils = DefaultEventUtils(amplitude: amplitude)
if amplitude.configuration.defaultTracking.screenViews {
UIKitScreenViews.register(amplitude)
}
if amplitude.configuration.defaultTracking.userInteractions {
userInteractionCaptureDelegate = UserInteractionCaptureDelegate(amplitude, with: { [weak self] in self?.application })
}
}

public override func setup(amplitude: Amplitude) {
super.setup(amplitude: amplitude)
utils = DefaultEventUtils(amplitude: amplitude)
if amplitude.configuration.defaultTracking.screenViews {
UIKitScreenViews.register(amplitude)
}
@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
}
}

@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 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
)
}

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
)
}
}

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

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

let fromBackground: Bool
if let sharedApplication = application {
switch sharedApplication.applicationState {
case .active, .inactive:
fromBackground = false
case .background:
fromBackground = true
@unknown default:
fromBackground = false
}
} else {
let fromBackground: Bool
if let sharedApplication = application {
switch sharedApplication.applicationState {
case .active, .inactive:
fromBackground = false
case .background:
fromBackground = true
@unknown default:
fromBackground = false
}

amplitude?.onEnterForeground(timestamp: timestamp)
sendApplicationOpened(fromBackground: fromBackground)
} else {
fromBackground = false
}

func didEnterBackground(notification: Notification) {
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
self.amplitude?.onExitForeground(timestamp: timestamp)
if self.amplitude?.configuration.defaultTracking.appLifecycles == true {
self.amplitude?.track(eventType: Constants.AMP_APPLICATION_BACKGROUNDED_EVENT)
}
amplitude?.onEnterForeground(timestamp: timestamp)
sendApplicationOpened(fromBackground: fromBackground)
}

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

func applicationDidFinishLaunchingNotification(notification: Notification) {
utils?.trackAppUpdatedInstalledEvent()
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
}
// 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 {
return
}
sendApplicationOpenedOnDidBecomeActive = false

let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
amplitude?.onEnterForeground(timestamp: timestamp)
sendApplicationOpened(fromBackground: false)
func applicationDidBecomeActive(notification: Notification) {
guard sendApplicationOpenedOnDidBecomeActive else {
return
}
sendApplicationOpenedOnDidBecomeActive = false

private func sendApplicationOpened(fromBackground: Bool) {
guard amplitude?.configuration.defaultTracking.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,
])
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
amplitude?.onEnterForeground(timestamp: timestamp)
sendApplicationOpened(fromBackground: false)
}

private func sendApplicationOpened(fromBackground: Bool) {
guard amplitude?.configuration.defaultTracking.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,
])
}
}

#endif
Loading

0 comments on commit e612696

Please sign in to comment.