Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feature-autocapture' into featur…
Browse files Browse the repository at this point in the history
…e-autocapture
  • Loading branch information
PouriaAmini committed Jul 15, 2024
2 parents df08b88 + 83f1c92 commit c0522a7
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 2 deletions.
12 changes: 12 additions & 0 deletions Amplitude-Swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
4E05BB942BE41AEB009DE475 /* Amplitude+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */; };
4E2B646B2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */; };
4E3871622BB34DBC002890AB /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B6DF481F2B5B45BE00B3E6AA /* PrivacyInfo.xcprivacy */; };
6C23EF112C38AC19000DC8C8 /* UIKitUserInteractions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C23EF102C38AC19000DC8C8 /* UIKitUserInteractions.swift */; };
6C23EF132C38AC24000DC8C8 /* UserInteractionEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C23EF122C38AC24000DC8C8 /* UserInteractionEvent.swift */; };
6C23EF162C38AD31000DC8C8 /* UIKitUserInteractionPluginTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C23EF142C38AC32000DC8C8 /* UIKitUserInteractionPluginTest.swift */; };
8EDEC02B99EE2092B567A61D /* ObjCIngestionMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC500EBDA8B813056E2DB /* ObjCIngestionMetadata.swift */; };
8EDEC1073A308B12B5CCD975 /* AnalyticsConnectorPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDECD39BAA97DD4320C0AA5 /* AnalyticsConnectorPlugin.swift */; };
8EDEC10C56FA7F7DEEB48B6F /* ObjCBaseEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDECCFD935A0C5A6FE85E87 /* ObjCBaseEvent.swift */; };
Expand Down Expand Up @@ -152,6 +155,9 @@
3E281B902B9BCC14009D913B /* DispatchQueueHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueHolder.swift; sourceTree = "<group>"; };
4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Amplitude+Extensions.swift"; sourceTree = "<group>"; };
4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitScreenViewsPluginTests.swift; sourceTree = "<group>"; };
6C23EF102C38AC19000DC8C8 /* UIKitUserInteractions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitUserInteractions.swift; sourceTree = "<group>"; };
6C23EF122C38AC24000DC8C8 /* UserInteractionEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInteractionEvent.swift; sourceTree = "<group>"; };
6C23EF142C38AC32000DC8C8 /* UIKitUserInteractionPluginTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitUserInteractionPluginTest.swift; sourceTree = "<group>"; };
8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeSessionTests.swift; sourceTree = "<group>"; };
8EDEC1160D95DC3F0E48DDF7 /* ObjCPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCPlugin.swift; sourceTree = "<group>"; };
8EDEC1576C95A2EB2FEF00A8 /* ObjCAmplitude.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCAmplitude.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -329,6 +335,7 @@
B6F3389F2B6854A8006179E2 /* Plugins */ = {
isa = PBXGroup;
children = (
6C23EF142C38AC32000DC8C8 /* UIKitUserInteractionPluginTest.swift */,
B6F338A22B685793006179E2 /* NetworkConnectivityCheckerPluginTests.swift */,
4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */,
);
Expand All @@ -338,6 +345,7 @@
OBJ_13 /* Events */ = {
isa = PBXGroup;
children = (
6C23EF122C38AC24000DC8C8 /* UserInteractionEvent.swift */,
OBJ_14 /* BaseEvent.swift */,
OBJ_15 /* EventOptions.swift */,
OBJ_16 /* GroupIdentifyEvent.swift */,
Expand Down Expand Up @@ -390,6 +398,7 @@
OBJ_32 /* iOS */ = {
isa = PBXGroup;
children = (
6C23EF102C38AC19000DC8C8 /* UIKitUserInteractions.swift */,
OBJ_33 /* IOSLifecycleMonitor.swift */,
8EDEC650EF79B104DC3C9F4C /* UIKitScreenViews.swift */,
);
Expand Down Expand Up @@ -706,6 +715,7 @@
OBJ_150 /* RevenueTests.swift in Sources */,
OBJ_151 /* PersistentStorageTests.swift in Sources */,
OBJ_152 /* TestUtilities.swift in Sources */,
6C23EF162C38AD31000DC8C8 /* UIKitUserInteractionPluginTest.swift in Sources */,
OBJ_153 /* TimelineTests.swift in Sources */,
OBJ_154 /* TypesTests.swift in Sources */,
OBJ_155 /* EventPipelineTests.swift in Sources */,
Expand Down Expand Up @@ -770,6 +780,7 @@
OBJ_120 /* OutputFileStream.swift in Sources */,
OBJ_121 /* PersistentStorageResponseHandler.swift in Sources */,
OBJ_122 /* QueueTimer.swift in Sources */,
6C23EF132C38AC24000DC8C8 /* UserInteractionEvent.swift in Sources */,
OBJ_124 /* UrlExtension.swift in Sources */,
8EDECFCCF4219767F26210D6 /* Sessions.swift in Sources */,
3E281B8C2B967F19009D913B /* Diagonostics.swift in Sources */,
Expand All @@ -783,6 +794,7 @@
8EDEC30C0075E9D92B1B5210 /* UIKitScreenViews.swift in Sources */,
8EDEC43FB30802F70112E577 /* ScreenViewedEvent.swift in Sources */,
8EDEC51F746CC25D27E32F6A /* DeepLinkOpenedEvent.swift in Sources */,
6C23EF112C38AC19000DC8C8 /* UIKitUserInteractions.swift in Sources */,
8EDECF81C2B1B38D472FD7EF /* ObjCConfiguration.swift in Sources */,
8EDECB800546E37719391E65 /* ObjCPlan.swift in Sources */,
8EDEC02B99EE2092B567A61D /* ObjCIngestionMetadata.swift in Sources */,
Expand Down
8 changes: 8 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,13 @@ 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_APP_VIEW_CONTROLLER = "\(AMP_AMPLITUDE_PREFIX)View Controller"
static let AMP_APP_TITLE = "\(AMP_AMPLITUDE_PREFIX)Title"
static let AMP_APP_TARGET_ACCESSIBILITY_LABEL = "\(AMP_AMPLITUDE_PREFIX)Target Accessibility Label"
static let AMP_APP_ACTION = "\(AMP_AMPLITUDE_PREFIX)Action"
static let AMP_APP_TARGET_VIEW_CLASS = "\(AMP_AMPLITUDE_PREFIX)Target View Class"
static let AMP_APP_TARGET_TEXT = "\(AMP_AMPLITUDE_PREFIX)Target Text"
static let AMP_APP_HIERARCHY = "\(AMP_AMPLITUDE_PREFIX)Hierarchy"

public struct Configuration {
public static let FLUSH_QUEUE_SIZE = 30
Expand Down
5 changes: 4 additions & 1 deletion Sources/Amplitude/DefaultTrackingOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ 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,
appLifecycles: Bool = false,
screenViews: Bool = false
screenViews: Bool = false,
userInteractions: Bool = false
) {
self.sessions = sessions
self.appLifecycles = appLifecycles
self.screenViews = screenViews
self.userInteractions = userInteractions
}
}
23 changes: 23 additions & 0 deletions Sources/Amplitude/Events/UserInteractionEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

public class UserInteractionEvent: BaseEvent {
convenience init(
viewController: String? = nil,
title: String? = nil,
accessibilityLabel: String? = nil,
action: String,
targetViewClass: String,
targetText: String? = nil,
hierarchy: String
) {
self.init(eventType: Constants.AMP_USER_INTERACTION_EVENT, eventProperties: [
Constants.AMP_APP_VIEW_CONTROLLER: viewController,
Constants.AMP_APP_TITLE: title,
Constants.AMP_APP_TARGET_ACCESSIBILITY_LABEL: accessibilityLabel,
Constants.AMP_APP_ACTION: action,
Constants.AMP_APP_TARGET_VIEW_CLASS: targetViewClass,
Constants.AMP_APP_TARGET_TEXT: targetText,
Constants.AMP_APP_HIERARCHY: hierarchy
])
}
}
10 changes: 10 additions & 0 deletions Sources/Amplitude/ObjC/ObjCDefaultTrackingOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ public class ObjCDefaultTrackingOptions: NSObject {
options.screenViews = value
}
}

@objc
public var userInteractions: Bool {
get {
options.userInteractions
}
set(value) {
options.userInteractions = value
}
}
}
3 changes: 3 additions & 0 deletions Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class IOSLifecycleMonitor: UtilityPlugin {
if amplitude.configuration.defaultTracking.screenViews {
UIKitScreenViews.register(amplitude)
}
if amplitude.configuration.defaultTracking.userInteractions {
UIKitUserInteractions.register(amplitude)
}
}

@objc
Expand Down
172 changes: 172 additions & 0 deletions Sources/Amplitude/Plugins/iOS/UIKitUserInteractions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
import UIKit

class UIKitUserInteractions {
fileprivate static let amplitudeInstances = NSHashTable<Amplitude>.weakObjects()

private static let lock = NSLock()

private static let initializeSwizzle: () = {
swizzleSendAction()
}()

private static let initializeNotificationListeners: () = {
NotificationCenter.default.addObserver(UIKitUserInteractions.self, selector: #selector(UIKitUserInteractions.amp_textFieldDidBeginEditing), name: UITextField.textDidBeginEditingNotification, object: nil)
NotificationCenter.default.addObserver(UIKitUserInteractions.self, selector: #selector(UIKitUserInteractions.amp_textFieldDidEndEditing), name: UITextField.textDidEndEditingNotification, object: nil)
}()

static func register(_ amplitude: Amplitude) {
lock.withLock {
amplitudeInstances.add(amplitude)
}
initializeSwizzle
initializeNotificationListeners
}

private static func swizzleSendAction() {
let applicationClass = UIApplication.self

let originalSelector = #selector(UIApplication.sendAction)
let swizzledSelector = #selector(UIApplication.amp_sendAction)

guard
let originalMethod = class_getInstanceMethod(applicationClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(applicationClass, swizzledSelector)
else { return }

let originalImp = method_getImplementation(originalMethod)
let swizzledImp = method_getImplementation(swizzledMethod)

class_replaceMethod(applicationClass,
swizzledSelector,
originalImp,
method_getTypeEncoding(originalMethod))
class_replaceMethod(applicationClass,
originalSelector,
swizzledImp,
method_getTypeEncoding(swizzledMethod))
}

@objc static func amp_textFieldDidBeginEditing(_ notification: NSNotification) {
guard let textField = notification.object as? UITextField else { return }
let userInteractionEvent = textField.eventFromData(with: "didBeginEditing")
amplitudeInstances.allObjects.forEach {
$0.track(event: userInteractionEvent)
}
}

@objc static func amp_textFieldDidEndEditing(_ notification: NSNotification) {
guard let textField = notification.object as? UITextField else { return }
let userInteractionEvent = textField.eventFromData(with: "didEndEditing")
amplitudeInstances.allObjects.forEach {
$0.track(event: userInteractionEvent)
}
}
}

extension UIApplication {
@objc func amp_sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool {
let sendActionResult = amp_sendAction(action, to: target, from: sender, for: event)

guard
sendActionResult,
let view = sender as? UIView,
view.amp_shouldTrack(action, for: event)
else { return sendActionResult }

let userInteractionEvent = view.eventFromData(with: NSStringFromSelector(action).components(separatedBy: ":").first ?? "")

UIKitUserInteractions.amplitudeInstances.allObjects.forEach {
$0.track(event: userInteractionEvent)
}

return sendActionResult
}
}

extension UIView {
private static let viewHierarchyDelimiter = " -> "

struct ViewData {
let viewController: String?
let title: String?
let accessibilityLabel: String?
let action: String
let targetViewClass: String
let targetText: String?
let hierarchy: String
}

func eventFromData(with action: String) -> UserInteractionEvent {
let viewData = extractData(with: action)
return UserInteractionEvent(
viewController: viewData.viewController,
title: viewData.title,
accessibilityLabel: viewData.accessibilityLabel,
action: viewData.action,
targetViewClass: viewData.targetViewClass,
targetText: viewData.targetText,
hierarchy: viewData.hierarchy)
}

func extractData(with action: String) -> ViewData {
let viewController = owningViewController
return ViewData(
viewController: viewController?.descriptiveTypeName,
title: viewController?.title,
accessibilityLabel: accessibilityLabel,
action: action,
targetViewClass: descriptiveTypeName,
targetText: amp_title,
hierarchy: sequence(first: self, next: \.superview)
.map { $0.descriptiveTypeName }
.joined(separator: UIView.viewHierarchyDelimiter))
}
}

extension UIResponder {
var descriptiveTypeName: String {
String(describing: type(of: self))
}

var owningViewController: UIViewController? {
return self as? UIViewController ?? next?.owningViewController
}
}

protocol ActionTrackable {
var amp_title: String? { get }
func amp_shouldTrack(_ action: Selector, for event: UIEvent?) -> Bool
}

extension UIView: ActionTrackable {
@objc var amp_title: String? { nil }
@objc func amp_shouldTrack(_ action: Selector, for event: UIEvent?) -> Bool { true }
}

extension UIButton {
override var amp_title: String? { currentTitle }
}

extension UISegmentedControl {
override var amp_title: String? { titleForSegment(at: selectedSegmentIndex) }
}

extension UITextField {
override func amp_shouldTrack(_ action: Selector, for event: UIEvent?) -> Bool { false }
}

#if !os(tvOS)
extension UISlider {
override func amp_shouldTrack(_ action: Selector, for event: UIEvent?) -> Bool {
event?.allTouches?.contains { $0.phase == .ended && $0.view === self } ?? false
}
}

@available(iOS 14.0, *)
extension UIColorWell {
override var amp_title: String? { title }
}
#endif

#endif
1 change: 1 addition & 0 deletions Tests/AmplitudeTests/AmplitudeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ final class AmplitudeTests: XCTestCase {
let defaultTracking = amplitude.configuration.defaultTracking
XCTAssertFalse(defaultTracking.appLifecycles)
XCTAssertFalse(defaultTracking.screenViews)
XCTAssertFalse(defaultTracking.userInteractions)
XCTAssertTrue(defaultTracking.sessions)
}

Expand Down
6 changes: 5 additions & 1 deletion Tests/AmplitudeTests/DefaultTrackingOptionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,30 @@ final class DefaultTrackingOptionsTests: XCTestCase {
XCTAssertFalse(options.appLifecycles)
XCTAssertFalse(options.screenViews)
XCTAssertTrue(options.sessions)
XCTAssertFalse(options.userInteractions)
}

func testAll() {
let options = DefaultTrackingOptions.ALL
XCTAssertTrue(options.appLifecycles)
XCTAssertTrue(options.screenViews)
XCTAssertTrue(options.sessions)
XCTAssertFalse(options.userInteractions)
}

func testNone() {
let options = DefaultTrackingOptions.NONE
XCTAssertFalse(options.appLifecycles)
XCTAssertFalse(options.screenViews)
XCTAssertFalse(options.sessions)
XCTAssertFalse(options.userInteractions)
}

func testCustom() {
let options = DefaultTrackingOptions(sessions: false, appLifecycles: true, screenViews: true)
let options = DefaultTrackingOptions(sessions: false, appLifecycles: true, screenViews: true, userInteractions: true)
XCTAssertTrue(options.appLifecycles)
XCTAssertTrue(options.screenViews)
XCTAssertFalse(options.sessions)
XCTAssertTrue(options.userInteractions)
}
}
Loading

0 comments on commit c0522a7

Please sign in to comment.