-
Notifications
You must be signed in to change notification settings - Fork 21
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
feat: add user interactions autocapture #186
Changes from 2 commits
e612696
80d1271
5989c9f
ffab6f3
1d5f0c6
078b44a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we change the name to be "User Interacted" to follow the pattern of other default tracking event names? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can discuss with Alan to align on the naming. |
||
static let AMP_REVENUE_EVENT = "revenue_amount" | ||
|
||
static let AMP_APP_VERSION_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Version" | ||
|
@@ -94,6 +95,11 @@ public struct Constants { | |
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 | ||
public static let FLUSH_INTERVAL_MILLIS = 30 * 1000 // 30s | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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 | ||
Comment on lines
+27
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason you are adding a new convenience initializer vs just adding userInteractions to the default initializer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was to bypass an error related to the new parameter of the original initializer. I'll look into a different way to fix that. |
||
} | ||
} |
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: ", ") | ||
} | ||
} | ||
Comment on lines
+37
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's not a lot here that's informative in any sort of analytical sense, we should not collect this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that some of them are unnecessary, but traits like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, in many cases, this data will be used to help uniquely identify an auto captured event - something like "signup button clicked". |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,131 +7,137 @@ | |
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of the changes to this file are to correct the indentation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please check in any style related changes separately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do. |
||
#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 | ||
} | ||
Comment on lines
+14
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrapped the logic to get the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? That should not be the case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've tried accessing the |
||
|
||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably a relic from including a local Amplitude-Swift in another project - please do not check this in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.