diff --git a/Amplitude-Swift.xcodeproj/project.pbxproj b/Amplitude-Swift.xcodeproj/project.pbxproj index 5edfcedb..3485f74d 100644 --- a/Amplitude-Swift.xcodeproj/project.pbxproj +++ b/Amplitude-Swift.xcodeproj/project.pbxproj @@ -27,6 +27,12 @@ 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 */; }; + 6C04FC3F2C58973C00EA8667 /* ElementInteractionEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC3E2C58973C00EA8667 /* ElementInteractionEvent.swift */; }; + 6C04FC412C58974A00EA8667 /* UIKitElementInteractions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC402C58974A00EA8667 /* UIKitElementInteractions.swift */; }; + 6C04FC432C58976800EA8667 /* ObjCAutocaptureOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC422C58976800EA8667 /* ObjCAutocaptureOptions.swift */; }; + 6C04FC452C58978900EA8667 /* AutocaptureOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC442C58978900EA8667 /* AutocaptureOptions.swift */; }; + 6C04FC482C589C8100EA8667 /* AutocaptureOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC462C5897AC00EA8667 /* AutocaptureOptionsTests.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 */; }; @@ -63,7 +69,6 @@ B6F338A32B685793006179E2 /* NetworkConnectivityCheckerPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F338A22B685793006179E2 /* NetworkConnectivityCheckerPluginTests.swift */; }; BA0359CA2A51585D007C383B /* legacy_v3.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BA0359C92A51585D007C383B /* legacy_v3.sqlite */; }; BA0639F62A4DD491000F1CEE /* LegacyDatabaseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0639F52A4DD491000F1CEE /* LegacyDatabaseStorage.swift */; }; - BA1EC0F42A9F2FC700C2D547 /* DefaultTrackingOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1EC0F32A9F2FC700C2D547 /* DefaultTrackingOptionsTests.swift */; }; BA1EC0F62A9F63FD00C2D547 /* AmplitudeIOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1EC0F52A9F63FD00C2D547 /* AmplitudeIOSTests.swift */; }; BA34B23C2AA0723A00F88097 /* AnalyticsConnector in Frameworks */ = {isa = PBXBuildFile; productRef = BA34B23B2AA0723A00F88097 /* AnalyticsConnector */; }; BA994B9A2A4F48DE00D0913F /* LegacyDatabaseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA994B992A4F48DE00D0913F /* LegacyDatabaseStorageTests.swift */; }; @@ -152,6 +157,12 @@ 3E281B902B9BCC14009D913B /* DispatchQueueHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueHolder.swift; sourceTree = ""; }; 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Amplitude+Extensions.swift"; sourceTree = ""; }; 4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitScreenViewsPluginTests.swift; sourceTree = ""; }; + 6C04FC3E2C58973C00EA8667 /* ElementInteractionEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElementInteractionEvent.swift; sourceTree = ""; }; + 6C04FC402C58974A00EA8667 /* UIKitElementInteractions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitElementInteractions.swift; sourceTree = ""; }; + 6C04FC422C58976800EA8667 /* ObjCAutocaptureOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCAutocaptureOptions.swift; sourceTree = ""; }; + 6C04FC442C58978900EA8667 /* AutocaptureOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocaptureOptions.swift; sourceTree = ""; }; + 6C04FC462C5897AC00EA8667 /* AutocaptureOptionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocaptureOptionsTests.swift; sourceTree = ""; }; + 6C23EF142C38AC32000DC8C8 /* UIKitUserInteractionPluginTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitUserInteractionPluginTest.swift; sourceTree = ""; }; 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeSessionTests.swift; sourceTree = ""; }; 8EDEC1160D95DC3F0E48DDF7 /* ObjCPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCPlugin.swift; sourceTree = ""; }; 8EDEC1576C95A2EB2FEF00A8 /* ObjCAmplitude.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCAmplitude.swift; sourceTree = ""; }; @@ -189,7 +200,6 @@ B6F338A22B685793006179E2 /* NetworkConnectivityCheckerPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectivityCheckerPluginTests.swift; sourceTree = ""; }; BA0359C92A51585D007C383B /* legacy_v3.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = legacy_v3.sqlite; sourceTree = ""; }; BA0639F52A4DD491000F1CEE /* LegacyDatabaseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDatabaseStorage.swift; sourceTree = ""; }; - BA1EC0F32A9F2FC700C2D547 /* DefaultTrackingOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTrackingOptionsTests.swift; sourceTree = ""; }; BA1EC0F52A9F63FD00C2D547 /* AmplitudeIOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmplitudeIOSTests.swift; sourceTree = ""; }; BA994B992A4F48DE00D0913F /* LegacyDatabaseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDatabaseStorageTests.swift; sourceTree = ""; }; BA994B9B2A4F4B7500D0913F /* legacy_v4.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = legacy_v4.sqlite; sourceTree = ""; }; @@ -293,6 +303,7 @@ 8EDECAFD8271434E8DC7BA78 /* ObjC */ = { isa = PBXGroup; children = ( + 6C04FC422C58976800EA8667 /* ObjCAutocaptureOptions.swift */, B6CCC6CC2B6B14510004B203 /* ObjCNetworkConnectivityCheckerPlugin.swift */, 8EDECEC5AAE15FD05E76359A /* ObjCConfiguration.swift */, 8EDECB1FA2AFF022A19104EE /* ObjCPlan.swift */, @@ -329,6 +340,7 @@ B6F3389F2B6854A8006179E2 /* Plugins */ = { isa = PBXGroup; children = ( + 6C23EF142C38AC32000DC8C8 /* UIKitUserInteractionPluginTest.swift */, B6F338A22B685793006179E2 /* NetworkConnectivityCheckerPluginTests.swift */, 4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */, ); @@ -338,6 +350,7 @@ OBJ_13 /* Events */ = { isa = PBXGroup; children = ( + 6C04FC3E2C58973C00EA8667 /* ElementInteractionEvent.swift */, OBJ_14 /* BaseEvent.swift */, OBJ_15 /* EventOptions.swift */, OBJ_16 /* GroupIdentifyEvent.swift */, @@ -390,6 +403,7 @@ OBJ_32 /* iOS */ = { isa = PBXGroup; children = ( + 6C04FC402C58974A00EA8667 /* UIKitElementInteractions.swift */, OBJ_33 /* IOSLifecycleMonitor.swift */, 8EDEC650EF79B104DC3C9F4C /* UIKitScreenViews.swift */, ); @@ -451,6 +465,7 @@ OBJ_53 /* Tests */ = { isa = PBXGroup; children = ( + 6C04FC462C5897AC00EA8667 /* AutocaptureOptionsTests.swift */, B6F3389F2B6854A8006179E2 /* Plugins */, OBJ_54 /* AmplitudeTests.swift */, OBJ_55 /* ConfigurationTests.swift */, @@ -463,7 +478,6 @@ OBJ_70 /* Utilities */, 8EDEC0630C3B587334275D9B /* AmplitudeSessionTests.swift */, 8EDECBC5925DC68913C7CB89 /* Migration */, - BA1EC0F32A9F2FC700C2D547 /* DefaultTrackingOptionsTests.swift */, BA1EC0F52A9F63FD00C2D547 /* AmplitudeIOSTests.swift */, 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */, ); @@ -503,6 +517,7 @@ OBJ_7 /* Sources */ = { isa = PBXGroup; children = ( + 6C04FC442C58978900EA8667 /* AutocaptureOptions.swift */, B6DF481F2B5B45BE00B3E6AA /* PrivacyInfo.xcprivacy */, OBJ_8 /* Amplitude.swift */, OBJ_9 /* Configuration.swift */, @@ -694,7 +709,6 @@ buildActionMask = 0; files = ( OBJ_142 /* AmplitudeTests.swift in Sources */, - BA1EC0F42A9F2FC700C2D547 /* DefaultTrackingOptionsTests.swift in Sources */, OBJ_143 /* ConfigurationTests.swift in Sources */, OBJ_144 /* ConsoleLoggerTests.swift in Sources */, OBJ_145 /* BaseEventTests.swift in Sources */, @@ -706,11 +720,13 @@ 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 */, 3E281B8E2B96833D009D913B /* DiagnosticsTests.swift in Sources */, B6F338A32B685793006179E2 /* NetworkConnectivityCheckerPluginTests.swift in Sources */, + 6C04FC482C589C8100EA8667 /* AutocaptureOptionsTests.swift in Sources */, OBJ_156 /* HttpClientTests.swift in Sources */, D01043612B6C5A8500F8173C /* SandboxHelperTests.swift in Sources */, OBJ_157 /* PersistentStorageResponseHandlerTests.swift in Sources */, @@ -766,6 +782,7 @@ OBJ_116 /* Atomic.swift in Sources */, OBJ_117 /* CodableExtension.swift in Sources */, OBJ_118 /* EventPipeline.swift in Sources */, + 6C04FC412C58974A00EA8667 /* UIKitElementInteractions.swift in Sources */, OBJ_119 /* HttpClient.swift in Sources */, OBJ_120 /* OutputFileStream.swift in Sources */, OBJ_121 /* PersistentStorageResponseHandler.swift in Sources */, @@ -781,6 +798,8 @@ 8EDEC3283B812D5D34DADF7B /* AnalyticsConnectorIdentityPlugin.swift in Sources */, 8EDEC4D0C0CE07BF211804CC /* DefaultTrackingOptions.swift in Sources */, 8EDEC30C0075E9D92B1B5210 /* UIKitScreenViews.swift in Sources */, + 6C04FC3F2C58973C00EA8667 /* ElementInteractionEvent.swift in Sources */, + 6C04FC432C58976800EA8667 /* ObjCAutocaptureOptions.swift in Sources */, 8EDEC43FB30802F70112E577 /* ScreenViewedEvent.swift in Sources */, 8EDEC51F746CC25D27E32F6A /* DeepLinkOpenedEvent.swift in Sources */, 8EDECF81C2B1B38D472FD7EF /* ObjCConfiguration.swift in Sources */, @@ -797,6 +816,7 @@ 8EDEC74C71FEC9056DC7358F /* ObjCLoggerProvider.swift in Sources */, 8EDECA4DAFA67CD4785D0161 /* ObjCDefaultTrackingOptions.swift in Sources */, 8EDEC43520B2DCF584F1035D /* ObjCScreenViewedEvent.swift in Sources */, + 6C04FC452C58978900EA8667 /* AutocaptureOptions.swift in Sources */, 8EDECC1FC97DDF0BEFAA96E7 /* ObjCDeepLinkOpenedEvent.swift in Sources */, B6EDB4D02B643C8400454B90 /* NetworkConnectivityCheckerPlugin.swift in Sources */, 8EDEC5F7208B1C327C8703D7 /* ObjCStorage.swift in Sources */, diff --git a/Examples/AmplitudeObjCExample/AmplitudeObjCExample/AppDelegate.m b/Examples/AmplitudeObjCExample/AmplitudeObjCExample/AppDelegate.m index 746c46f1..7c775ebe 100644 --- a/Examples/AmplitudeObjCExample/AmplitudeObjCExample/AppDelegate.m +++ b/Examples/AmplitudeObjCExample/AmplitudeObjCExample/AppDelegate.m @@ -13,7 +13,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( AMPConfiguration* configuration = [AMPConfiguration initWithApiKey:apiKey]; configuration.logLevel = AMPLogLevelLOG; configuration.serverZone = AMPServerZoneUS; - configuration.defaultTracking = AMPDefaultTrackingOptions.ALL; + NSArray *autocaptureOptions = @[ + AMPAutocaptureOptions.sessions, + AMPAutocaptureOptions.appLifecycles, + AMPAutocaptureOptions.screenViews + ]; + configuration.autocapture = [[AMPAutocaptureOptions alloc] initWithOptionsToUnion:autocaptureOptions]; configuration.loggerProvider = ^(NSInteger logLevel, NSString* _Nonnull message) { NSLog(@"%@", message); }; diff --git a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m index c6fe2a24..7f1e84b3 100644 --- a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m +++ b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/AmplitudeObjCExampleTests.m @@ -325,7 +325,7 @@ - (void)testEventProperties { - (Amplitude *)getAmplitude:(NSString *)instancePrefix { NSString* instanceName = [NSString stringWithFormat:@"%@-%f", instancePrefix, [[NSDate date] timeIntervalSince1970]]; AMPConfiguration* configuration = [AMPConfiguration initWithApiKey:@"API-KEY" instanceName:instanceName]; - configuration.defaultTracking = AMPDefaultTrackingOptions.NONE; + configuration.autocapture = [[AMPAutocaptureOptions alloc] init]; Amplitude *amplitude = [[TestAmplitude alloc] initWithConfiguration:configuration]; return amplitude; } diff --git a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift index 4e540ea2..4d5bebe3 100644 --- a/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift +++ b/Examples/AmplitudeObjCExample/AmplitudeObjCExampleTests/TestAmplitude.swift @@ -36,7 +36,7 @@ public class TestAmplitude: ObjCAmplitude { enableCoppaControl: config.enableCoppaControl, flushEventsOnClose: config.flushEventsOnClose, minTimeBetweenSessionsMillis: config.minTimeBetweenSessionsMillis, - defaultTracking: config.defaultTracking, + autocapture: config.autocapture, identifyBatchIntervalMillis: config.identifyBatchIntervalMillis, migrateLegacyData: config.migrateLegacyData, offline: NetworkConnectivityCheckerPlugin.Disabled) diff --git a/Sources/Amplitude/AutocaptureOptions.swift b/Sources/Amplitude/AutocaptureOptions.swift new file mode 100644 index 00000000..cd1133ce --- /dev/null +++ b/Sources/Amplitude/AutocaptureOptions.swift @@ -0,0 +1,14 @@ +import Foundation + +public struct AutocaptureOptions: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let sessions = AutocaptureOptions(rawValue: 1 << 0) + public static let appLifecycles = AutocaptureOptions(rawValue: 1 << 1) + public static let screenViews = AutocaptureOptions(rawValue: 1 << 2) + public static let elementInteractions = AutocaptureOptions(rawValue: 1 << 3) +} diff --git a/Sources/Amplitude/Configuration.swift b/Sources/Amplitude/Configuration.swift index 2e2f8410..65c568cb 100644 --- a/Sources/Amplitude/Configuration.swift +++ b/Sources/Amplitude/Configuration.swift @@ -32,10 +32,78 @@ public class Configuration { public var minTimeBetweenSessionsMillis: Int public var identifyBatchIntervalMillis: Int public internal(set) var migrateLegacyData: Bool - public var defaultTracking: DefaultTrackingOptions + @available(*, deprecated, renamed: "autocapture", message: "Please use `autocapture` instead.") + public lazy var defaultTracking: DefaultTrackingOptions = { + DefaultTrackingOptions(delegate: self) + }() { + didSet { + defaultTracking.delegate = self + autocapture = defaultTracking.autocaptureOptions + } + } + public internal(set) var autocapture: AutocaptureOptions public var offline: Bool? internal let diagonostics: Diagnostics + @available(*, deprecated, message: "Please use the `autocapture` parameter instead.") + public convenience init( + apiKey: String, + flushQueueSize: Int = Constants.Configuration.FLUSH_QUEUE_SIZE, + flushIntervalMillis: Int = Constants.Configuration.FLUSH_INTERVAL_MILLIS, + instanceName: String = "", + optOut: Bool = false, + storageProvider: (any Storage)? = nil, + identifyStorageProvider: (any Storage)? = nil, + logLevel: LogLevelEnum = LogLevelEnum.WARN, + loggerProvider: any Logger = ConsoleLogger(), + minIdLength: Int? = nil, + partnerId: String? = nil, + callback: EventCallback? = nil, + flushMaxRetries: Int = Constants.Configuration.FLUSH_MAX_RETRIES, + useBatch: Bool = false, + serverZone: ServerZone = ServerZone.US, + serverUrl: String? = nil, + plan: Plan? = nil, + ingestionMetadata: IngestionMetadata? = nil, + trackingOptions: TrackingOptions = TrackingOptions(), + enableCoppaControl: Bool = false, + flushEventsOnClose: Bool = true, + minTimeBetweenSessionsMillis: Int = Constants.Configuration.MIN_TIME_BETWEEN_SESSIONS_MILLIS, + // `trackingSessionEvents` has been replaced by `defaultTracking.sessions` + defaultTracking: DefaultTrackingOptions, + identifyBatchIntervalMillis: Int = Constants.Configuration.IDENTIFY_BATCH_INTERVAL_MILLIS, + migrateLegacyData: Bool = true, + offline: Bool? = false + ) { + self.init(apiKey: apiKey, + flushQueueSize: flushQueueSize, + flushIntervalMillis: flushIntervalMillis, + instanceName: instanceName, + optOut: optOut, + storageProvider: storageProvider, + identifyStorageProvider: identifyStorageProvider, + logLevel: logLevel, + loggerProvider: loggerProvider, + minIdLength: minIdLength, + partnerId: partnerId, + callback: callback, + flushMaxRetries: flushMaxRetries, + useBatch: useBatch, + serverZone: serverZone, + serverUrl: serverUrl, + plan: plan, + ingestionMetadata: ingestionMetadata, + trackingOptions: trackingOptions, + enableCoppaControl: enableCoppaControl, + flushEventsOnClose: flushEventsOnClose, + minTimeBetweenSessionsMillis: minTimeBetweenSessionsMillis, + autocapture: defaultTracking.autocaptureOptions, + identifyBatchIntervalMillis: identifyBatchIntervalMillis, + migrateLegacyData: migrateLegacyData, + offline: offline) + self.defaultTracking = defaultTracking + } + public init( apiKey: String, flushQueueSize: Int = Constants.Configuration.FLUSH_QUEUE_SIZE, @@ -60,7 +128,7 @@ public class Configuration { flushEventsOnClose: Bool = true, minTimeBetweenSessionsMillis: Int = Constants.Configuration.MIN_TIME_BETWEEN_SESSIONS_MILLIS, // `trackingSessionEvents` has been replaced by `defaultTracking.sessions` - defaultTracking: DefaultTrackingOptions = DefaultTrackingOptions(), + autocapture: AutocaptureOptions = .sessions, identifyBatchIntervalMillis: Int = Constants.Configuration.IDENTIFY_BATCH_INTERVAL_MILLIS, migrateLegacyData: Bool = true, offline: Bool? = false @@ -92,7 +160,7 @@ public class Configuration { self.enableCoppaControl = enableCoppaControl self.flushEventsOnClose = flushEventsOnClose self.minTimeBetweenSessionsMillis = minTimeBetweenSessionsMillis - self.defaultTracking = defaultTracking + self.autocapture = autocapture self.identifyBatchIntervalMillis = identifyBatchIntervalMillis self.migrateLegacyData = migrateLegacyData // Logging is OFF by default @@ -114,3 +182,10 @@ public class Configuration { return Configuration.getNormalizeInstanceName(self.instanceName) } } + +extension Configuration: DefaultTrackingOptionsDelegate { + @available(*, deprecated) + func didChangeOptions(options: DefaultTrackingOptions) { + autocapture = options.autocaptureOptions + } +} diff --git a/Sources/Amplitude/Constants.swift b/Sources/Amplitude/Constants.swift index 9976f87f..2494e933 100644 --- a/Sources/Amplitude/Constants.swift +++ b/Sources/Amplitude/Constants.swift @@ -83,6 +83,8 @@ 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_ELEMENT_INTERACTED_EVENT = "\(AMP_AMPLITUDE_PREFIX)Element Interacted" + static let AMP_REVENUE_EVENT = "revenue_amount" static let AMP_APP_VERSION_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Version" @@ -93,6 +95,14 @@ 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_TARGET_AXLABEL_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Target Accessibility Label" + static let AMP_APP_TARGET_AXIDENTIFIER_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Target Accessibility Identifier" + static let AMP_APP_ACTION_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Action" + static let AMP_APP_TARGET_VIEW_CLASS_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Target View Class" + static let AMP_APP_TARGET_TEXT_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Target Text" + static let AMP_APP_HIERARCHY_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Hierarchy" + static let AMP_APP_ACTION_METHOD_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Action Method" + static let AMP_APP_GESTURE_RECOGNIZER_PROPERTY = "\(AMP_AMPLITUDE_PREFIX)Gesture Recognizer" public struct Configuration { public static let FLUSH_QUEUE_SIZE = 30 diff --git a/Sources/Amplitude/DefaultTrackingOptions.swift b/Sources/Amplitude/DefaultTrackingOptions.swift index e4553cfb..bf5a0142 100644 --- a/Sources/Amplitude/DefaultTrackingOptions.swift +++ b/Sources/Amplitude/DefaultTrackingOptions.swift @@ -1,5 +1,11 @@ import Foundation +protocol DefaultTrackingOptionsDelegate: AnyObject { + @available(*, deprecated) + func didChangeOptions(options: DefaultTrackingOptions) +} + +@available(*, deprecated, renamed: "AutocaptureOptions", message: "Please use `AutocaptureOptions` instead") public class DefaultTrackingOptions { public static var ALL: DefaultTrackingOptions { DefaultTrackingOptions(sessions: true, appLifecycles: true, screenViews: true) @@ -8,9 +14,33 @@ public class DefaultTrackingOptions { DefaultTrackingOptions(sessions: false, appLifecycles: false, screenViews: false) } - public var sessions: Bool = true - public var appLifecycles: Bool - public var screenViews: Bool + public var sessions: Bool { + didSet { + delegate?.didChangeOptions(options: self) + } + } + + public var appLifecycles: Bool { + didSet { + delegate?.didChangeOptions(options: self) + } + } + + public var screenViews: Bool { + didSet { + delegate?.didChangeOptions(options: self) + } + } + + weak var delegate: DefaultTrackingOptionsDelegate? + + var autocaptureOptions: AutocaptureOptions { + return [ + sessions ? .sessions : [], + appLifecycles ? .appLifecycles : [], + screenViews ? .screenViews : [] + ].reduce(into: []) { $0.formUnion($1) } + } public init( sessions: Bool = true, @@ -21,4 +51,9 @@ public class DefaultTrackingOptions { self.appLifecycles = appLifecycles self.screenViews = screenViews } + + convenience init(delegate: DefaultTrackingOptionsDelegate) { + self.init() + self.delegate = delegate + } } diff --git a/Sources/Amplitude/Events/ElementInteractionEvent.swift b/Sources/Amplitude/Events/ElementInteractionEvent.swift new file mode 100644 index 00000000..4dbd0e86 --- /dev/null +++ b/Sources/Amplitude/Events/ElementInteractionEvent.swift @@ -0,0 +1,27 @@ +import Foundation + +public class ElementInteractionEvent: BaseEvent { + convenience init( + screenName: String? = nil, + accessibilityLabel: String? = nil, + accessibilityIdentifier: String? = nil, + action: String, + targetViewClass: String, + targetText: String? = nil, + hierarchy: String, + actionMethod: String? = nil, + gestureRecognizer: String? = nil + ) { + self.init(eventType: Constants.AMP_ELEMENT_INTERACTED_EVENT, eventProperties: [ + Constants.AMP_APP_SCREEN_NAME_PROPERTY: screenName, + Constants.AMP_APP_TARGET_AXLABEL_PROPERTY: accessibilityLabel, + Constants.AMP_APP_TARGET_AXIDENTIFIER_PROPERTY: accessibilityIdentifier, + Constants.AMP_APP_ACTION_PROPERTY: action, + Constants.AMP_APP_TARGET_VIEW_CLASS_PROPERTY: targetViewClass, + Constants.AMP_APP_TARGET_TEXT_PROPERTY: targetText, + Constants.AMP_APP_HIERARCHY_PROPERTY: hierarchy, + Constants.AMP_APP_ACTION_METHOD_PROPERTY: actionMethod, + Constants.AMP_APP_GESTURE_RECOGNIZER_PROPERTY: gestureRecognizer + ]) + } +} diff --git a/Sources/Amplitude/ObjC/ObjCAutocaptureOptions.swift b/Sources/Amplitude/ObjC/ObjCAutocaptureOptions.swift new file mode 100644 index 00000000..a601fe34 --- /dev/null +++ b/Sources/Amplitude/ObjC/ObjCAutocaptureOptions.swift @@ -0,0 +1,92 @@ +import Foundation + +@objc(AMPAutocaptureOptions) +public final class ObjCAutocaptureOptions: NSObject { + internal var _options: AutocaptureOptions + + public override init() { + _options = AutocaptureOptions() + super.init() + } + + @objc + public convenience init(optionsToUnion: [ObjCAutocaptureOptions]) { + self.init() + for option in optionsToUnion { + formUnion(option) + } + } + + internal convenience init(options: AutocaptureOptions) { + self.init() + _options = options + } + + internal var options: AutocaptureOptions { + get { + return _options + } + set { + _options = newValue + } + } + + @objc + public static let sessions = ObjCAutocaptureOptions(options: .sessions) + + @objc + public static let appLifecycles = ObjCAutocaptureOptions(options: .appLifecycles) + + @objc + public static let screenViews = ObjCAutocaptureOptions(options: .screenViews) + + @objc + public static let elementInteractions = ObjCAutocaptureOptions(options: .elementInteractions) + + // MARK: NSObject + + public override var hash: Int { + return _options.rawValue + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let that = object as? ObjCAutocaptureOptions else { + return false + } + return _options == that._options + } + + // MARK: OptionSet-like behavior + + @objc + public func formUnion(_ other: ObjCAutocaptureOptions) { + _options.formUnion(other._options) + } + + @objc + public func formIntersection(_ other: ObjCAutocaptureOptions) { + _options.formIntersection(other._options) + } + + @objc + public func formSymmetricDifference(_ other: ObjCAutocaptureOptions) { + _options.formSymmetricDifference(other._options) + } + + // MARK: Convenience methods for Objective-C + + @objc + public func contains(_ option: ObjCAutocaptureOptions) -> Bool { + return _options.contains(option._options) + } + + @objc + public func union(_ option: ObjCAutocaptureOptions) -> ObjCAutocaptureOptions { + return ObjCAutocaptureOptions(options: _options.union(option._options)) + } + + @objc + public func intersect(_ option: ObjCAutocaptureOptions) -> ObjCAutocaptureOptions { + return ObjCAutocaptureOptions(options: _options.intersection(option._options)) + } +} diff --git a/Sources/Amplitude/ObjC/ObjCConfiguration.swift b/Sources/Amplitude/ObjC/ObjCConfiguration.swift index 39a44b7f..3a7fd0fd 100644 --- a/Sources/Amplitude/ObjC/ObjCConfiguration.swift +++ b/Sources/Amplitude/ObjC/ObjCConfiguration.swift @@ -244,6 +244,8 @@ public class ObjCConfiguration: NSObject { } @objc + @available(*, deprecated, renamed: "autocapture", message: "Please use `autocapture` instead.") + /// The SDK no longer tracks changes to the defaultTracking options after initialization. public var defaultTracking: ObjCDefaultTrackingOptions { get { ObjCDefaultTrackingOptions(configuration.defaultTracking) @@ -253,6 +255,16 @@ public class ObjCConfiguration: NSObject { } } + @objc + public var autocapture: ObjCAutocaptureOptions { + get { + ObjCAutocaptureOptions(options: configuration.autocapture) + } + set(value) { + configuration.autocapture = value.options + } + } + @objc public var identifyBatchIntervalMillis: Int { get { diff --git a/Sources/Amplitude/ObjC/ObjCDefaultTrackingOptions.swift b/Sources/Amplitude/ObjC/ObjCDefaultTrackingOptions.swift index 4e80f46c..e3f07346 100644 --- a/Sources/Amplitude/ObjC/ObjCDefaultTrackingOptions.swift +++ b/Sources/Amplitude/ObjC/ObjCDefaultTrackingOptions.swift @@ -1,6 +1,7 @@ import Foundation @objc(AMPDefaultTrackingOptions) +@available(*, deprecated, renamed: "AMPAutocaptureOptions", message: "Please use `AMPAutocaptureOptions` instead") public class ObjCDefaultTrackingOptions: NSObject { internal let options: DefaultTrackingOptions diff --git a/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift b/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift index 5a9e8cbd..511c0c06 100644 --- a/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift +++ b/Sources/Amplitude/Plugins/iOS/IOSLifecycleMonitor.swift @@ -33,9 +33,12 @@ class IOSLifecycleMonitor: UtilityPlugin { public override func setup(amplitude: Amplitude) { super.setup(amplitude: amplitude) utils = DefaultEventUtils(amplitude: amplitude) - if amplitude.configuration.defaultTracking.screenViews { + if amplitude.configuration.autocapture.contains(.screenViews) { UIKitScreenViews.register(amplitude) } + if amplitude.configuration.autocapture.contains(.elementInteractions) { + UIKitElementInteractions.register(amplitude) + } } @objc @@ -92,7 +95,7 @@ class IOSLifecycleMonitor: UtilityPlugin { func didEnterBackground(notification: Notification) { let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000) self.amplitude?.onExitForeground(timestamp: timestamp) - if self.amplitude?.configuration.defaultTracking.appLifecycles == true { + if amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false { self.amplitude?.track(eventType: Constants.AMP_APPLICATION_BACKGROUNDED_EVENT) } } @@ -120,7 +123,7 @@ class IOSLifecycleMonitor: UtilityPlugin { } private func sendApplicationOpened(fromBackground: Bool) { - guard amplitude?.configuration.defaultTracking.appLifecycles ?? false else { + guard amplitude?.configuration.autocapture.contains(.appLifecycles) ?? false else { return } let info = Bundle.main.infoDictionary diff --git a/Sources/Amplitude/Plugins/iOS/UIKitElementInteractions.swift b/Sources/Amplitude/Plugins/iOS/UIKitElementInteractions.swift new file mode 100644 index 00000000..38e6280d --- /dev/null +++ b/Sources/Amplitude/Plugins/iOS/UIKitElementInteractions.swift @@ -0,0 +1,263 @@ +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +import UIKit + +class UIKitElementInteractions { + struct EventData { + enum Source { + case actionMethod + + case gestureRecognizer + } + + let screenName: String? + + let accessibilityLabel: String? + + let accessibilityIdentifier: String? + + let targetViewClass: String + + let targetText: String? + + let hierarchy: String + + fileprivate func elementInteractionEvent(for action: String, from source: Source? = nil, withName sourceName: String? = nil) -> ElementInteractionEvent { + return ElementInteractionEvent( + screenName: screenName, + accessibilityLabel: accessibilityLabel, + accessibilityIdentifier: accessibilityIdentifier, + action: action, + targetViewClass: targetViewClass, + targetText: targetText, + hierarchy: hierarchy, + actionMethod: source == .actionMethod ? sourceName : nil, + gestureRecognizer: source == .gestureRecognizer ? sourceName : nil + ) + } + } + + fileprivate static let amplitudeInstances = NSHashTable.weakObjects() + + private static let lock = NSLock() + + private static let addNotificationObservers: Void = { + NotificationCenter.default.addObserver(UIKitElementInteractions.self, selector: #selector(didEndEditing), name: UITextField.textDidEndEditingNotification, object: nil) + NotificationCenter.default.addObserver(UIKitElementInteractions.self, selector: #selector(didEndEditing), name: UITextView.textDidEndEditingNotification, object: nil) + }() + + private static let setupMethodSwizzling: Void = { + swizzleMethod(UIApplication.self, from: #selector(UIApplication.sendAction), to: #selector(UIApplication.amp_sendAction)) + swizzleMethod(UIGestureRecognizer.self, from: #selector(setter: UIGestureRecognizer.state), to: #selector(UIGestureRecognizer.amp_setState)) + }() + + static func register(_ amplitude: Amplitude) { + lock.withLock { + amplitudeInstances.add(amplitude) + } + setupMethodSwizzling + addNotificationObservers + } + + @objc static func didEndEditing(_ notification: NSNotification) { + guard let view = notification.object as? UIView else { return } + // Text fields in SwiftUI are identifiable only after the text field is edited. + let elementInteractionEvent = view.eventData.elementInteractionEvent(for: "didEndEditing") + amplitudeInstances.allObjects.forEach { + $0.track(event: elementInteractionEvent) + } + } + + private static func swizzleMethod(_ cls: AnyClass?, from original: Selector, to swizzled: Selector) { + guard + let originalMethod = class_getInstanceMethod(cls, original), + let swizzledMethod = class_getInstanceMethod(cls, swizzled) + else { return } + + let originalImp = method_getImplementation(originalMethod) + let swizzledImp = method_getImplementation(swizzledMethod) + + class_replaceMethod(cls, + swizzled, + originalImp, + method_getTypeEncoding(originalMethod)) + class_replaceMethod(cls, + original, + swizzledImp, + method_getTypeEncoding(swizzledMethod)) + } +} + +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) + + // TODO: Reduce SwiftUI noise by finding the unique view that the action method is attached to. + // Currently, the action methods pointing to a SwiftUI target are blocked. + let targetClass = String(cString: object_getClassName(target)) + if targetClass.contains("SwiftUI") { + return sendActionResult + } + + guard sendActionResult, + let control = sender as? UIControl, + control.amp_shouldTrack(action, for: target), + let actionEvent = control.event(for: action, to: target)?.description + else { return sendActionResult } + + let elementInteractionEvent = control.eventData.elementInteractionEvent(for: actionEvent, from: .actionMethod, withName: NSStringFromSelector(action)) + + UIKitElementInteractions.amplitudeInstances.allObjects.forEach { + $0.track(event: elementInteractionEvent) + } + + return sendActionResult + } +} + +extension UIGestureRecognizer { + @objc func amp_setState(_ state: UIGestureRecognizer.State) { + amp_setState(state) + + guard state == .ended, let view else { return } + + // Block scroll and zoom events for `UIScrollView`. + if let scrollView = view as? UIScrollView, self === scrollView.panGestureRecognizer || self === scrollView.pinchGestureRecognizer { + return + } + + let gestureAction: String? = switch self { + case is UITapGestureRecognizer: "tap" + case is UISwipeGestureRecognizer: "swipe" + case is UIPanGestureRecognizer: "pan" + case is UILongPressGestureRecognizer: "longPress" +#if !os(tvOS) + case is UIPinchGestureRecognizer: "pinch" + case is UIRotationGestureRecognizer: "rotation" + case is UIScreenEdgePanGestureRecognizer: "screenEdgePan" +#endif + default: nil + } + + guard let gestureAction else { return } + + let elementInteractionEvent = view.eventData.elementInteractionEvent(for: gestureAction, from: .gestureRecognizer, withName: descriptiveTypeName) + + UIKitElementInteractions.amplitudeInstances.allObjects.forEach { + $0.track(event: elementInteractionEvent) + } + } +} + +extension UIView { + private static let viewHierarchyDelimiter = " → " + + var eventData: UIKitElementInteractions.EventData { + return UIKitElementInteractions.EventData( + screenName: owningViewController + .flatMap(UIViewController.amp_topViewController) + .flatMap(UIKitScreenViews.screenName), + accessibilityLabel: accessibilityLabel, + accessibilityIdentifier: accessibilityIdentifier, + targetViewClass: descriptiveTypeName, + targetText: amp_title, + hierarchy: sequence(first: self, next: \.superview) + .map { $0.descriptiveTypeName } + .joined(separator: UIView.viewHierarchyDelimiter)) + } +} + +extension UIControl { + func event(for action: Selector, to target: Any?) -> UIControl.Event? { + var events: [UIControl.Event] = [ + .touchDown, .touchDownRepeat, .touchDragInside, .touchDragOutside, + .touchDragEnter, .touchDragExit, .touchUpInside, .touchUpOutside, + .touchCancel, .valueChanged, .editingDidBegin, .editingChanged, + .editingDidEnd, .editingDidEndOnExit, .primaryActionTriggered + ] + if #available(iOS 14.0, tvOS 14.0, macCatalyst 14.0, *) { + events.append(.menuActionTriggered) + } + + return events.first { event in + self.actions(forTarget: target, forControlEvent: event)?.contains(action.description) ?? false + } + } +} + +extension UIControl.Event { + var description: String? { + if UIControl.Event.allTouchEvents.contains(self) { + return "touch" + } else if UIControl.Event.allEditingEvents.contains(self) { + return "edit" + } else if self == .valueChanged { + return "valueChange" + } else if self == .primaryActionTriggered { + return "primaryAction" + } else if #available(iOS 14.0, tvOS 14.0, macCatalyst 14.0, *), self == .menuActionTriggered { + return "menuAction" + } + return nil + } +} + +extension UIResponder { + var owningViewController: UIViewController? { + self as? UIViewController ?? next?.owningViewController + } +} + +extension NSObject { + var descriptiveTypeName: String { + String(describing: type(of: self)) + } +} + +protocol ActionTrackable { + var amp_title: String? { get } + func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool +} + +extension UIView: ActionTrackable { + @objc var amp_title: String? { nil } + @objc func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool { false } +} + +extension UIControl { + override func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + actions(forTarget: target, forControlEvent: .touchUpInside)?.contains(action.description) ?? false + } +} + +extension UIButton { + override var amp_title: String? { currentTitle ?? currentImage?.accessibilityIdentifier } +} + +extension UISegmentedControl { + override var amp_title: String? { titleForSegment(at: selectedSegmentIndex) } + override func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + actions(forTarget: target, forControlEvent: .valueChanged)?.contains(action.description) ?? false + } +} + +extension UIPageControl { + override func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + actions(forTarget: target, forControlEvent: .valueChanged)?.contains(action.description) ?? false + } +} + +#if !os(tvOS) +extension UIDatePicker { + override func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + actions(forTarget: target, forControlEvent: .valueChanged)?.contains(action.description) ?? false + } +} + +extension UISwitch { + override func amp_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + actions(forTarget: target, forControlEvent: .valueChanged)?.contains(action.description) ?? false + } +} +#endif + +#endif diff --git a/Sources/Amplitude/Sessions.swift b/Sources/Amplitude/Sessions.swift index 18875bd2..a73ea5a6 100644 --- a/Sources/Amplitude/Sessions.swift +++ b/Sources/Amplitude/Sessions.swift @@ -119,7 +119,7 @@ public class Sessions { public func startNewSession(timestamp: Int64) -> [BaseEvent] { var sessionEvents: [BaseEvent] = Array() - let trackingSessionEvents = configuration.defaultTracking.sessions + let trackingSessionEvents = configuration.autocapture.contains(.sessions) // end previous session if trackingSessionEvents && self.sessionId >= 0 { @@ -148,7 +148,7 @@ public class Sessions { public func endCurrentSession() -> [BaseEvent] { var sessionEvents: [BaseEvent] = Array() - let trackingSessionEvents = configuration.defaultTracking.sessions + let trackingSessionEvents = configuration.autocapture.contains(.sessions) if trackingSessionEvents && self.sessionId >= 0 { let sessionEndEvent = BaseEvent( diff --git a/Sources/Amplitude/Utilities/DefaultEventUtils.swift b/Sources/Amplitude/Utilities/DefaultEventUtils.swift index 740fefde..5e78a185 100644 --- a/Sources/Amplitude/Utilities/DefaultEventUtils.swift +++ b/Sources/Amplitude/Utilities/DefaultEventUtils.swift @@ -14,7 +14,7 @@ public class DefaultEventUtils { let previousBuild: String? = amplitude?.storage.read(key: StorageKey.APP_BUILD) let previousVersion: String? = amplitude?.storage.read(key: StorageKey.APP_VERSION) - if self.amplitude?.configuration.defaultTracking.appLifecycles == true { + 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: [ diff --git a/Tests/AmplitudeTests/AmplitudeIOSTests.swift b/Tests/AmplitudeTests/AmplitudeIOSTests.swift index ab96dc0c..8a456f4e 100644 --- a/Tests/AmplitudeTests/AmplitudeIOSTests.swift +++ b/Tests/AmplitudeTests/AmplitudeIOSTests.swift @@ -24,7 +24,7 @@ final class AmplitudeIOSTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) + autocapture: .appLifecycles ) let amplitude = Amplitude(configuration: configuration) NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil) @@ -49,7 +49,7 @@ final class AmplitudeIOSTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) + autocapture: .appLifecycles ) try storageMem.write(key: StorageKey.LAST_EVENT_TIME, value: 123 as Int64) try storageMem.write(key: StorageKey.APP_BUILD, value: "abc") @@ -78,7 +78,7 @@ final class AmplitudeIOSTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) + autocapture: .appLifecycles ) let info = Bundle.main.infoDictionary @@ -121,7 +121,7 @@ final class AmplitudeIOSTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) + autocapture: .appLifecycles ) let info = Bundle.main.infoDictionary @@ -187,7 +187,7 @@ final class AmplitudeIOSTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) + autocapture: .appLifecycles ) let info = Bundle.main.infoDictionary @@ -220,7 +220,7 @@ final class AmplitudeIOSTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false, appLifecycles: true) + autocapture: .appLifecycles ) let amplitude = Amplitude(configuration: configuration) diff --git a/Tests/AmplitudeTests/AmplitudeSessionTests.swift b/Tests/AmplitudeTests/AmplitudeSessionTests.swift index 0084cbc8..fe10fab8 100644 --- a/Tests/AmplitudeTests/AmplitudeSessionTests.swift +++ b/Tests/AmplitudeTests/AmplitudeSessionTests.swift @@ -115,7 +115,7 @@ final class AmplitudeSessionTests: XCTestCase { storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, minTimeBetweenSessionsMillis: 100, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: customCongiguration) amplitude.setSessionId(timestamp: 800) diff --git a/Tests/AmplitudeTests/AmplitudeTests.swift b/Tests/AmplitudeTests/AmplitudeTests.swift index 1038891c..9c6c64ac 100644 --- a/Tests/AmplitudeTests/AmplitudeTests.swift +++ b/Tests/AmplitudeTests/AmplitudeTests.swift @@ -39,7 +39,7 @@ final class AmplitudeTests: XCTestCase { apiKey: apiKey, storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions.NONE + autocapture: [] ) } @@ -205,7 +205,7 @@ final class AmplitudeTests: XCTestCase { apiKey: apiKey, storageProvider: storageTest, identifyStorageProvider: interceptStorageTest, - defaultTracking: DefaultTrackingOptions.NONE + autocapture: [] )) amplitude.setUserId(userId: "test-user") @@ -288,13 +288,14 @@ final class AmplitudeTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - func testInit_defaultTracking() { + func testInit_autocapture() { let configuration = Configuration(apiKey: "api-key") let amplitude = Amplitude(configuration: configuration) - let defaultTracking = amplitude.configuration.defaultTracking - XCTAssertFalse(defaultTracking.appLifecycles) - XCTAssertFalse(defaultTracking.screenViews) - XCTAssertTrue(defaultTracking.sessions) + let autocapture = amplitude.configuration.autocapture + XCTAssertFalse(autocapture.contains(.appLifecycles)) + XCTAssertFalse(autocapture.contains(.screenViews)) + XCTAssertFalse(autocapture.contains(.elementInteractions)) + XCTAssertTrue(autocapture.contains(.sessions)) } func testTrackNSUserActivity() throws { @@ -302,7 +303,7 @@ final class AmplitudeTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: configuration) @@ -330,7 +331,7 @@ final class AmplitudeTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: configuration) @@ -353,7 +354,7 @@ final class AmplitudeTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: configuration) @@ -376,7 +377,7 @@ final class AmplitudeTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: configuration) @@ -399,7 +400,7 @@ final class AmplitudeTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: configuration) let eventOptions = EventOptions(sessionId: -1) @@ -417,7 +418,7 @@ final class AmplitudeTests: XCTestCase { apiKey: "api-key", storageProvider: storageMem, identifyStorageProvider: interceptStorageMem, - defaultTracking: DefaultTrackingOptions(sessions: false) + autocapture: [] ) let amplitude = Amplitude(configuration: configuration) amplitude.sessions = SessionsWithDelayedEventStartProcessing(amplitude: amplitude) @@ -457,7 +458,7 @@ final class AmplitudeTests: XCTestCase { flushQueueSize: 1000, flushIntervalMillis: 99999, logLevel: LogLevelEnum.DEBUG, - defaultTracking: DefaultTrackingOptions.NONE + autocapture: [] ) // Create storages using instance name only @@ -473,7 +474,7 @@ final class AmplitudeTests: XCTestCase { storageProvider: legacyEventStorage, identifyStorageProvider: legacyIdentityStorage, logLevel: config.logLevel, - defaultTracking: config.defaultTracking + autocapture: config.autocapture )) let legacyDeviceId = legacyStorageAmplitude.getDeviceId() @@ -541,7 +542,7 @@ final class AmplitudeTests: XCTestCase { flushQueueSize: 1000, flushIntervalMillis: 99999, logLevel: LogLevelEnum.DEBUG, - defaultTracking: DefaultTrackingOptions.NONE + autocapture: [] ) // Create storages using instance name only @@ -557,7 +558,7 @@ final class AmplitudeTests: XCTestCase { storageProvider: legacyEventStorage, identifyStorageProvider: legacyIdentityStorage, logLevel: config.logLevel, - defaultTracking: config.defaultTracking + autocapture: config.autocapture )) let legacyDeviceId = legacyStorageAmplitude.getDeviceId() @@ -704,8 +705,7 @@ final class AmplitudeTests: XCTestCase { func testConcurrentAccess() { let amplitude = Amplitude(configuration: Configuration(apiKey: "test-api-key", storageProvider: InMemoryStorage(), - defaultTracking: .init(sessions: true, - appLifecycles: true))) + autocapture: [.sessions, .appLifecycles])) let eventCollector = EventCollectorPlugin() amplitude.add(plugin: eventCollector) let sessionID = Int64(Date().timeIntervalSince1970 * 1000) diff --git a/Tests/AmplitudeTests/AutocaptureOptionsTests.swift b/Tests/AmplitudeTests/AutocaptureOptionsTests.swift new file mode 100644 index 00000000..5a308c67 --- /dev/null +++ b/Tests/AmplitudeTests/AutocaptureOptionsTests.swift @@ -0,0 +1,66 @@ +import XCTest + +@testable import AmplitudeSwift + +final class AutocaptureOptionsTests: XCTestCase { + func testDefault() { + let config = Configuration(apiKey: "TEST_KEY") + XCTAssertFalse(config.autocapture.contains(.appLifecycles)) + XCTAssertFalse(config.autocapture.contains(.screenViews)) + XCTAssertTrue(config.autocapture.contains(.sessions)) + XCTAssertFalse(config.autocapture.contains(.elementInteractions)) + } + + func testCustom() { + let options: AutocaptureOptions = [.appLifecycles, .screenViews, .elementInteractions] + XCTAssertTrue(options.contains(.appLifecycles)) + XCTAssertTrue(options.contains(.screenViews)) + XCTAssertFalse(options.contains(.sessions)) + XCTAssertTrue(options.contains(.elementInteractions)) + } + + func testDefaultTrackingOptionChangesReflectInAutocapture() { + let configuration = Configuration( + apiKey: "test-api-key" + ) + + XCTAssertTrue(configuration.autocapture.contains(.sessions)) + + (configuration as DeprecationWarningDiscardable).setDefaultTrackingOptions(sessions: false, appLifecycles: true, screenViews: true) + + XCTAssertFalse(configuration.autocapture.contains(.sessions)) + XCTAssertTrue(configuration.autocapture.contains(.appLifecycles)) + XCTAssertTrue(configuration.autocapture.contains(.screenViews)) + } + + func testDefaultTrackingInstanceChangeReflectInAutocapture() { + let configuration = Configuration( + apiKey: "test-api-key" + ) + + (configuration as DeprecationWarningDiscardable).setDefaultTracking(sessions: false, appLifecycles: true, screenViews: true) + + XCTAssertFalse(configuration.autocapture.contains(.sessions)) + XCTAssertTrue(configuration.autocapture.contains(.appLifecycles)) + XCTAssertTrue(configuration.autocapture.contains(.screenViews)) + } +} + +private protocol DeprecationWarningDiscardable { + func setDefaultTracking(sessions: Bool, appLifecycles: Bool, screenViews: Bool) + func setDefaultTrackingOptions(sessions: Bool, appLifecycles: Bool, screenViews: Bool) +} + +extension Configuration: DeprecationWarningDiscardable { + @available(*, deprecated) + func setDefaultTracking(sessions: Bool, appLifecycles: Bool, screenViews: Bool) { + defaultTracking = DefaultTrackingOptions(sessions: sessions, appLifecycles: appLifecycles, screenViews: screenViews) + } + + @available(*, deprecated) + func setDefaultTrackingOptions(sessions: Bool, appLifecycles: Bool, screenViews: Bool) { + defaultTracking.sessions = sessions + defaultTracking.appLifecycles = appLifecycles + defaultTracking.screenViews = screenViews + } +} diff --git a/Tests/AmplitudeTests/DefaultTrackingOptionsTests.swift b/Tests/AmplitudeTests/DefaultTrackingOptionsTests.swift deleted file mode 100644 index 80e7c88f..00000000 --- a/Tests/AmplitudeTests/DefaultTrackingOptionsTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -import XCTest - -@testable import AmplitudeSwift - -final class DefaultTrackingOptionsTests: XCTestCase { - func testDefault() { - let options = DefaultTrackingOptions() - XCTAssertFalse(options.appLifecycles) - XCTAssertFalse(options.screenViews) - XCTAssertTrue(options.sessions) - } - - func testAll() { - let options = DefaultTrackingOptions.ALL - XCTAssertTrue(options.appLifecycles) - XCTAssertTrue(options.screenViews) - XCTAssertTrue(options.sessions) - } - - func testNone() { - let options = DefaultTrackingOptions.NONE - XCTAssertFalse(options.appLifecycles) - XCTAssertFalse(options.screenViews) - XCTAssertFalse(options.sessions) - } - - func testCustom() { - let options = DefaultTrackingOptions(sessions: false, appLifecycles: true, screenViews: true) - XCTAssertTrue(options.appLifecycles) - XCTAssertTrue(options.screenViews) - XCTAssertFalse(options.sessions) - } -} diff --git a/Tests/AmplitudeTests/Plugins/UIKitUserInteractionPluginTest.swift b/Tests/AmplitudeTests/Plugins/UIKitUserInteractionPluginTest.swift new file mode 100644 index 00000000..9fa3e438 --- /dev/null +++ b/Tests/AmplitudeTests/Plugins/UIKitUserInteractionPluginTest.swift @@ -0,0 +1,65 @@ +import XCTest + +@testable import AmplitudeSwift + +#if os(iOS) + +class UIKitElementInteractionsTests: XCTestCase { + func testExtractDataForUIButton() { + let mockVC = UIViewController() + mockVC.title = "Mock VC Title" + + let button = UIButton(type: .system) + button.setTitle("Test Button", for: .normal) + button.accessibilityLabel = "Accessibility Button" + mockVC.view.addSubview(button) + + let buttonData = button.eventData + + XCTAssertEqual(buttonData.screenName, "Mock VC Title") + XCTAssertEqual(buttonData.accessibilityLabel, "Accessibility Button") + XCTAssertEqual(buttonData.targetViewClass, "UIButton") + XCTAssertEqual(buttonData.targetText, "Test Button") + XCTAssertTrue(buttonData.hierarchy.hasSuffix("UIButton → UIView")) + } + + func testExtractDataForCustomView() { + let mockVC = UIViewController() + mockVC.title = "Mock VC Title" + + class CustomView: UIView {} + let customView = CustomView() + mockVC.view.addSubview(customView) + + let customViewData = customView.eventData + + XCTAssertEqual(customViewData.screenName, "Mock VC Title") + XCTAssertNil(customViewData.accessibilityLabel) + XCTAssertEqual(customViewData.targetViewClass, "CustomView") + XCTAssertTrue(customViewData.hierarchy.hasSuffix("CustomView → UIView")) + } + + func testExtractDataForOrphanView() { + let orphanView = UIView() + let orphanData = orphanView.eventData + + XCTAssertNil(orphanData.screenName) + XCTAssertNil(orphanData.accessibilityLabel) + XCTAssertEqual(orphanData.targetViewClass, "UIView") + XCTAssertNil(orphanData.targetText) + XCTAssertEqual(orphanData.hierarchy, "UIView") + } + + func testDescriptiveTypeName() { + let button = UIButton() + XCTAssertEqual(button.descriptiveTypeName, "UIButton") + + let vc = UIViewController() + XCTAssertEqual(vc.descriptiveTypeName, "UIViewController") + + class ConstrainedGenericView: UIView {} + XCTAssertEqual(ConstrainedGenericView().descriptiveTypeName, "ConstrainedGenericView") + } +} + +#endif