From 60025b03c16643aa11fad8d08de7ef09d06da19a Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Mon, 26 Jun 2023 16:36:07 -0700 Subject: [PATCH] feat: add app lifecycle events and screen view events --- Amplitude.xcodeproj/project.pbxproj | 28 ++++ Framework/AmplitudeFramework.h | 1 + Sources/Amplitude/AMPDefaultTrackingOptions.m | 67 ++++++++ Sources/Amplitude/Amplitude.m | 112 ++++++++++++-- .../Public/AMPDefaultTrackingOptions.h | 61 ++++++++ Sources/Amplitude/Public/Amplitude.h | 8 +- .../Amplitude/UIViewController+AMPScreen.h | 35 +++++ .../Amplitude/UIViewController+AMPScreen.m | 144 ++++++++++++++++++ Tests/DefaultTrackingOptionsTests.m | 58 +++++++ 9 files changed, 498 insertions(+), 16 deletions(-) create mode 100644 Sources/Amplitude/AMPDefaultTrackingOptions.m create mode 100644 Sources/Amplitude/Public/AMPDefaultTrackingOptions.h create mode 100644 Sources/Amplitude/UIViewController+AMPScreen.h create mode 100644 Sources/Amplitude/UIViewController+AMPScreen.m create mode 100644 Tests/DefaultTrackingOptionsTests.m diff --git a/Amplitude.xcodeproj/project.pbxproj b/Amplitude.xcodeproj/project.pbxproj index d61a821a..1568b2e4 100644 --- a/Amplitude.xcodeproj/project.pbxproj +++ b/Amplitude.xcodeproj/project.pbxproj @@ -186,6 +186,17 @@ 582516F028C075D100ECAD0D /* IngestionMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 582516EB28C075C300ECAD0D /* IngestionMetadataTests.m */; }; 582516F128C075D200ECAD0D /* IngestionMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 582516EB28C075C300ECAD0D /* IngestionMetadataTests.m */; }; 582516F228C075D300ECAD0D /* IngestionMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 582516EB28C075C300ECAD0D /* IngestionMetadataTests.m */; }; + 58B7FACB2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 58B7FACA2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 58B7FACC2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 58B7FACA2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 58B7FACD2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 58B7FACA2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 58B7FACE2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 58B7FACA2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 58B7FAD02A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FACF2A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m */; }; + 58B7FAD12A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FACF2A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m */; }; + 58B7FAD22A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FACF2A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m */; }; + 58B7FAD32A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FACF2A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m */; }; + 58B7FAD92A3D0D6000CC5BB4 /* DefaultTrackingOptionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FAD42A3D0D5500CC5BB4 /* DefaultTrackingOptionsTests.m */; }; + 58B7FADA2A3D0D6100CC5BB4 /* DefaultTrackingOptionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FAD42A3D0D5500CC5BB4 /* DefaultTrackingOptionsTests.m */; }; + 58B7FADB2A3D0D6200CC5BB4 /* DefaultTrackingOptionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B7FAD42A3D0D5500CC5BB4 /* DefaultTrackingOptionsTests.m */; }; 58E4B1BC287F62C3007AC408 /* AmazonRootCA1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 58E4B1BB287F62C3007AC408 /* AmazonRootCA1.cer */; }; 58E4B1BD287F62C3007AC408 /* AmazonRootCA1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 58E4B1BB287F62C3007AC408 /* AmazonRootCA1.cer */; }; 58E4B1BE287F62C3007AC408 /* AmazonRootCA1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 58E4B1BB287F62C3007AC408 /* AmazonRootCA1.cer */; }; @@ -365,6 +376,9 @@ 582516E128C048D600ECAD0D /* AMPIngestionMetadata.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AMPIngestionMetadata.h; sourceTree = ""; }; 582516E628C048E700ECAD0D /* AMPIngestionMetadata.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AMPIngestionMetadata.m; sourceTree = ""; }; 582516EB28C075C300ECAD0D /* IngestionMetadataTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IngestionMetadataTests.m; sourceTree = ""; }; + 58B7FACA2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AMPDefaultTrackingOptions.h; sourceTree = ""; }; + 58B7FACF2A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AMPDefaultTrackingOptions.m; sourceTree = ""; }; + 58B7FAD42A3D0D5500CC5BB4 /* DefaultTrackingOptionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DefaultTrackingOptionsTests.m; sourceTree = ""; }; 58E4B1BB287F62C3007AC408 /* AmazonRootCA1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AmazonRootCA1.cer; sourceTree = ""; }; 6C55E7B2C7CB09D0EDC07910 /* Pods-shared-Amplitude_iOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-Amplitude_iOSTests.release.xcconfig"; path = "Target Support Files/Pods-shared-Amplitude_iOSTests/Pods-shared-Amplitude_iOSTests.release.xcconfig"; sourceTree = ""; }; 70CE71E7AEFA4FE3E4F565AD /* Pods-shared-Amplitude_iOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-shared-Amplitude_iOSTests.debug.xcconfig"; path = "Target Support Files/Pods-shared-Amplitude_iOSTests/Pods-shared-Amplitude_iOSTests.debug.xcconfig"; sourceTree = ""; }; @@ -530,6 +544,7 @@ 3EF608EE2726256800133703 /* AMPMiddlewareRunner.m */, 3EF6090127267C9800133703 /* AMPMiddleware.m */, 582516E628C048E700ECAD0D /* AMPIngestionMetadata.m */, + 58B7FACF2A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m */, ); path = Amplitude; sourceTree = ""; @@ -572,6 +587,7 @@ 3EF608E027211F8A00133703 /* ConfigManagerTests.m */, 3EF608FD272666E300133703 /* MiddlewareRunnerTests.m */, 582516EB28C075C300ECAD0D /* IngestionMetadataTests.m */, + 58B7FAD42A3D0D5500CC5BB4 /* DefaultTrackingOptionsTests.m */, ); path = Tests; sourceTree = ""; @@ -594,6 +610,7 @@ 3EF608C82720E74D00133703 /* AMPServerZone.h */, 3EF608E42724BFB700133703 /* AMPMiddleware.h */, 582516E128C048D600ECAD0D /* AMPIngestionMetadata.h */, + 58B7FACA2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h */, ); path = Public; sourceTree = ""; @@ -675,6 +692,7 @@ 1279F91725244E8E003DCE07 /* AMPConfigManager.h in Headers */, 1279F91625244E8E003DCE07 /* AMPUtils.h in Headers */, 582516E328C048D600ECAD0D /* AMPIngestionMetadata.h in Headers */, + 58B7FACC2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -706,6 +724,7 @@ 1279F8F925244E8D003DCE07 /* AMPConfigManager.h in Headers */, 1279F8F825244E8D003DCE07 /* AMPUtils.h in Headers */, 582516E228C048D600ECAD0D /* AMPIngestionMetadata.h in Headers */, + 58B7FACB2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -737,6 +756,7 @@ 1279F93525244E8F003DCE07 /* AMPConfigManager.h in Headers */, 1279F93425244E8F003DCE07 /* AMPUtils.h in Headers */, 582516E428C048D600ECAD0D /* AMPIngestionMetadata.h in Headers */, + 58B7FACD2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -768,6 +788,7 @@ 759E93A525FBF44500BF7C3D /* AMPConfigManager.h in Headers */, 759E93B125FBF44500BF7C3D /* AMPUtils.h in Headers */, 582516E528C048D600ECAD0D /* AMPIngestionMetadata.h in Headers */, + 58B7FACE2A3BD71F00CC5BB4 /* AMPDefaultTrackingOptions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1127,6 +1148,7 @@ 1279F91D25244E8E003DCE07 /* AMPTrackingOptions.m in Sources */, 1279FA782525949D003DCE07 /* ISPPinnedNSURLSessionDelegate.m in Sources */, 3E2411ED26F9A46500793829 /* AMPPlan.m in Sources */, + 58B7FAD12A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */, 3EF608D92720F64500133703 /* AMPServerZoneUtil.m in Sources */, 1279F91C25244E8E003DCE07 /* AMPRevenue.m in Sources */, 1279F91F25244E8E003DCE07 /* AMPURLSession.m in Sources */, @@ -1140,6 +1162,7 @@ 12C973B6241244C600E9CDDB /* SetupTests.m in Sources */, 12C973C3241244F000E9CDDB /* IdentifyTests.m in Sources */, 12C973A7241244A600E9CDDB /* DeviceInfoTests.m in Sources */, + 58B7FADA2A3D0D6100CC5BB4 /* DefaultTrackingOptionsTests.m in Sources */, 12C973B1241244BF00E9CDDB /* AMPDatabaseHelperTests.m in Sources */, 58E4B1C1287F6FD7007AC408 /* SSLPinningTests.m in Sources */, 12C973C8241244F800E9CDDB /* AmplitudeTVOSTests.m in Sources */, @@ -1179,6 +1202,7 @@ 1279FA772525949D003DCE07 /* ISPPinnedNSURLSessionDelegate.m in Sources */, 1279FA6E2525949D003DCE07 /* ISPCertificatePinning.m in Sources */, 3EF6090227267C9800133703 /* AMPMiddleware.m in Sources */, + 58B7FAD02A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */, 1279F90725244E8D003DCE07 /* AMPDeviceInfo.m in Sources */, 1279F90425244E8D003DCE07 /* Amplitude.m in Sources */, 3EF608EF2726256800133703 /* AMPMiddlewareRunner.m in Sources */, @@ -1192,6 +1216,7 @@ 12C973B5241244C500E9CDDB /* SetupTests.m in Sources */, 12DF9471251DAC27008B2C25 /* AmplitudeiOSTests.m in Sources */, 12C973C0241244EF00E9CDDB /* IdentifyTests.m in Sources */, + 58B7FAD92A3D0D6000CC5BB4 /* DefaultTrackingOptionsTests.m in Sources */, 19619D9628A247DF00A2CC53 /* AmplitudeTests.m in Sources */, 12C973A6241244A400E9CDDB /* DeviceInfoTests.m in Sources */, 58E4B1C0287F6FD6007AC408 /* SSLPinningTests.m in Sources */, @@ -1231,6 +1256,7 @@ 1279F93B25244E8F003DCE07 /* AMPTrackingOptions.m in Sources */, 1279FA792525949D003DCE07 /* ISPPinnedNSURLSessionDelegate.m in Sources */, 3E2411EE26F9A46500793829 /* AMPPlan.m in Sources */, + 58B7FAD22A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */, 3EF608DA2720F64500133703 /* AMPServerZoneUtil.m in Sources */, 1279F93A25244E8F003DCE07 /* AMPRevenue.m in Sources */, 1279F93D25244E8F003DCE07 /* AMPURLSession.m in Sources */, @@ -1244,6 +1270,7 @@ 12C973B7241244C700E9CDDB /* SetupTests.m in Sources */, 3E2411F726F9A4E400793829 /* PlanTests.m in Sources */, 12C973C6241244F100E9CDDB /* IdentifyTests.m in Sources */, + 58B7FADB2A3D0D6200CC5BB4 /* DefaultTrackingOptionsTests.m in Sources */, 12C973A8241244A700E9CDDB /* DeviceInfoTests.m in Sources */, 12C973B3241244BF00E9CDDB /* AMPDatabaseHelperTests.m in Sources */, 58E4B1C2287F6FD7007AC408 /* SSLPinningTests.m in Sources */, @@ -1282,6 +1309,7 @@ 759E939925FBF3DC00BF7C3D /* AMPURLSession.m in Sources */, 759E939A25FBF3DC00BF7C3D /* AMPUtils.m in Sources */, 759E939B25FBF3DC00BF7C3D /* ISPCertificatePinning.m in Sources */, + 58B7FAD32A3CDC7E00CC5BB4 /* AMPDefaultTrackingOptions.m in Sources */, 3E2411EF26F9A46500793829 /* AMPPlan.m in Sources */, 3EF608DB2720F64500133703 /* AMPServerZoneUtil.m in Sources */, 759E939D25FBF3DC00BF7C3D /* ISPPinnedNSURLSessionDelegate.m in Sources */, diff --git a/Framework/AmplitudeFramework.h b/Framework/AmplitudeFramework.h index fad62717..381b46e0 100644 --- a/Framework/AmplitudeFramework.h +++ b/Framework/AmplitudeFramework.h @@ -8,6 +8,7 @@ #import #import #import +#import #if TARGET_OS_WATCH #import diff --git a/Sources/Amplitude/AMPDefaultTrackingOptions.m b/Sources/Amplitude/AMPDefaultTrackingOptions.m new file mode 100644 index 00000000..3d4041a7 --- /dev/null +++ b/Sources/Amplitude/AMPDefaultTrackingOptions.m @@ -0,0 +1,67 @@ +// +// AMPDefaultTrackingOptions.m +// Copyright (c) 2023 Amplitude Inc. (https://amplitude.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#import "AMPDefaultTrackingOptions.h" + +@implementation AMPDefaultTrackingOptions + +/* + * Create an AMPDefaultTrackingOptions object + */ +- (instancetype)init { + if (self = [super init]) { + self.sessions = NO; + self.appLifecycles = NO; + self.deepLinks = NO; + self.screenViews = NO; + } + return self; +} + ++ (instancetype)initWithSessions:(BOOL)sessions + appLifecycles:(BOOL)appLifecycles + deepLinks:(BOOL)deepLinks + screenViews:(BOOL)screenViews { + AMPDefaultTrackingOptions *instance = [[self alloc] init]; + instance.sessions = sessions; + instance.appLifecycles = appLifecycles; + instance.deepLinks = deepLinks; + instance.screenViews = screenViews; + return instance; +} + ++ (instancetype)initWithAllEnabled { + return [self initWithSessions:YES + appLifecycles:YES + deepLinks:YES + screenViews:YES]; +} + ++ (instancetype)initWithNoneEnabled { + return [self initWithSessions:NO + appLifecycles:NO + deepLinks:NO + screenViews:NO]; +} + +@end diff --git a/Sources/Amplitude/Amplitude.m b/Sources/Amplitude/Amplitude.m index e788c92b..2e53f6b6 100644 --- a/Sources/Amplitude/Amplitude.m +++ b/Sources/Amplitude/Amplitude.m @@ -66,6 +66,7 @@ #import "AMPMiddlewareRunner.h" #import "AMPIdentifyInterceptor.h" #import "AMPEventUtils.h" +#import "UIViewController+AMPScreen.h" #import #import @@ -103,8 +104,23 @@ @interface Amplitude () NSString *const kAMPSessionStartEvent = @"session_start"; NSString *const kAMPSessionEndEvent = @"session_end"; +NSString *const kAMPApplicationInstalled = @"[Amplitude] Application Installed"; +NSString *const kAMPApplicationUpdated = @"[Amplitude] Application Updated"; +NSString *const kAMPApplicationOpened = @"[Amplitude] Application Opened"; +NSString *const kAMPApplicationBackgrounded = @"[Amplitude] Application Backgrounded"; +NSString *const kAMPDeepLinkOpened = @"[Amplitude] Deep Link Opened"; NSString *const kAMPRevenueEvent = @"revenue_amount"; +NSString *const kAMPEventPropVersion = @"[Amplitude] Version"; +NSString *const kAMPEventPropBuild = @"[Amplitude] Build"; +NSString *const kAMPEventPropPreviousVersion = @"[Amplitude] Previous Version"; +NSString *const kAMPEventPropPreviousBuild = @"[Amplitude] Previous Build"; +NSString *const kAMPEventPropFromBackground = @"[Amplitude] From Background"; +NSString *const kAMPEventPropReferringApplication = @"[Amplitude] Referring Application"; +NSString *const kAMPEventPropReferringUrl = @"[Amplitude] Referring URL"; +NSString *const kAMPEventPropLinkUrl = @"[Amplitude] Link URL"; +NSString *const kAMPEventPropLinkReferrer = @"[Amplitude] Link Referrer"; + static NSString *const BACKGROUND_QUEUE_NAME = @"BACKGROUND"; static NSString *const DATABASE_VERSION = @"database_version"; static NSString *const DEVICE_ID = @"device_id"; @@ -117,6 +133,9 @@ @interface Amplitude () static NSString *const OPT_OUT = @"opt_out"; static NSString *const USER_ID = @"user_id"; static NSString *const SEQUENCE_NUMBER = @"sequence_number"; +// for app lifecycle events +static NSString *const APP_VERSION = @"app_version"; +static NSString *const APP_BUILD = @"app_build"; @implementation Amplitude { @@ -240,6 +259,8 @@ - (instancetype)initWithInstanceName:(NSString *)instanceName { [[[AnalyticsConnector getInstance:self.instanceName] eventBridge] setEventReceiver:^(AnalyticsEvent * _Nonnull event) { [self logEvent:[event eventType] withEventProperties:[event eventProperties] withApiProperties:nil withUserProperties:[event userProperties] withGroups:nil withGroupProperties:nil withTimestamp:nil outOfSession:false]; }]; + + self.defaultTracking = [[AMPDefaultTrackingOptions alloc] init]; _initializerQueue = [[NSOperationQueue alloc] init]; _backgroundQueue = [[NSOperationQueue alloc] init]; @@ -316,6 +337,8 @@ - (instancetype)initWithInstanceName:(NSString *)instanceName { backgroundQueue:_backgroundQueue]; [self addObservers]; + + } return self; } @@ -403,20 +426,70 @@ - (void)addObservers { name:NSApplicationDidResignActiveNotification object:nil]; #endif + +#if !TARGET_OS_OSX && !TARGET_OS_WATCH + // mount the default events handler + UIApplication *app = [AMPUtils getSharedApplication]; + if (app && self.defaultTracking.appLifecycles) { + for (NSString *name in @[UIApplicationDidEnterBackgroundNotification, + UIApplicationDidFinishLaunchingNotification, + UIApplicationWillEnterForegroundNotification]) { + [center addObserver:self selector:@selector(handleAppStateUpdates:) name:name object:app]; + } + } +#endif } +#if !TARGET_OS_OSX && !TARGET_OS_WATCH +- (void)handleAppStateUpdates:(NSNotification *)notification { + if ([notification.name isEqualToString:UIApplicationDidFinishLaunchingNotification]) { + NSDictionary *launchOptions = notification.userInfo; + NSString *previousBuild = [_dbHelper getValue:APP_BUILD]; + NSString *previousVersion = [_dbHelper getValue:APP_VERSION]; + NSString *currentBuild = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"]; + NSString *currentVersion = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; + if (!previousBuild) { + [self logEvent:kAMPApplicationInstalled withEventProperties:@{ + kAMPEventPropBuild: currentBuild ?: @"", + kAMPEventPropVersion: currentVersion ?: @"", + }]; + } else if (![currentBuild isEqualToString:previousBuild]) { + [self logEvent:kAMPApplicationUpdated withEventProperties:@{ + kAMPEventPropBuild: currentBuild ?: @"", + kAMPEventPropVersion: currentVersion ?: @"", + kAMPEventPropPreviousBuild: previousBuild ?: @"", + kAMPEventPropPreviousVersion: previousVersion ?: @"", + }]; + } + [self logEvent:kAMPApplicationOpened withEventProperties:@{ + kAMPEventPropBuild: currentBuild ?: @"", + kAMPEventPropVersion: currentVersion ?: @"", + kAMPEventPropFromBackground: @NO, + kAMPEventPropReferringApplication: launchOptions[UIApplicationLaunchOptionsSourceApplicationKey] ?: @"", + kAMPEventPropReferringUrl: launchOptions[UIApplicationLaunchOptionsURLKey] ?: @"", + }]; + + // persist the build/version + [_dbHelper insertOrReplaceKeyValue:APP_BUILD value:currentBuild]; + [_dbHelper insertOrReplaceKeyValue:APP_VERSION value:currentVersion]; + } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { + NSString *currentBuild = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"]; + NSString *currentVersion = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; + [self logEvent:kAMPApplicationOpened withEventProperties:@{ + kAMPEventPropBuild: currentBuild ?: @"", + kAMPEventPropVersion: currentVersion ?: @"", + kAMPEventPropFromBackground: @YES, + }]; + } else if ([notification.name isEqualToString:UIApplicationDidEnterBackgroundNotification]) { + [self logEvent:kAMPApplicationBackgrounded]; + } +} +#endif + - (void)removeObservers { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; -#if TARGET_OS_WATCH - [center removeObserver:self name:AMPAppWillEnterForegroundNotification object:nil]; - [center removeObserver:self name:AMPAppDidEnterBackgroundNotification object:nil]; -#elif !TARGET_OS_OSX - [center removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; - [center removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; -#else - [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil]; - [center removeObserver:self name:NSApplicationDidResignActiveNotification object:nil]; -#endif + // unregister all observers added by addObservers method + [center removeObserver:self]; } - (void)dealloc { @@ -477,6 +550,13 @@ - (void)initializeApiKey:(NSString *)apiKey if (self.initCompletionBlock != nil) { self.initCompletionBlock(); } + +#if !TARGET_OS_OSX && !TARGET_OS_WATCH + // Unlike other default events options that can be evaluated later, screenViews has to be evaluated during the actual initialization + if (self.defaultTracking.screenViews) { + [UIViewController amp_swizzleViewDidAppear]; + } +#endif }]; if (!self.deferCheckInForeground) { @@ -622,7 +702,7 @@ - (void)logEvent:(NSString *)eventType withEventProperties:(NSDictionary *)event } // skip session check if logging start_session or end_session events - BOOL loggingSessionEvent = self->_trackingSessionEvents && ([eventType isEqualToString:kAMPSessionStartEvent] || [eventType isEqualToString:kAMPSessionEndEvent]); + BOOL loggingSessionEvent = (self->_trackingSessionEvents || self.defaultTracking.sessions) && ([eventType isEqualToString:kAMPSessionStartEvent] || [eventType isEqualToString:kAMPSessionEndEvent]); if (!loggingSessionEvent && !outOfSession) { [self startOrContinueSessionNSNumber:timestamp inForeground:inForeground]; } @@ -1231,12 +1311,13 @@ - (BOOL)startOrContinueSession:(long long)timestamp { } - (void)startNewSession:(NSNumber *)timestamp { - if (_trackingSessionEvents) { + BOOL loggingSessionEvent = _trackingSessionEvents || self.defaultTracking.sessions; + if (loggingSessionEvent) { [self sendSessionEvent:kAMPSessionEndEvent]; } [self setSessionId:[timestamp longLongValue]]; [self refreshSessionTime:timestamp]; - if (_trackingSessionEvents) { + if (loggingSessionEvent) { [self sendSessionEvent:kAMPSessionStartEvent]; } } @@ -1426,7 +1507,8 @@ - (void)setUserId:(NSString *)userId startNewSession:(BOOL)startNewSession { } [self runOnBackgroundQueue:^{ - if (startNewSession && self->_trackingSessionEvents) { + BOOL loggingSessionEvent = self->_trackingSessionEvents || self.defaultTracking.sessions; + if (startNewSession && loggingSessionEvent) { [self sendSessionEvent:kAMPSessionEndEvent]; } @@ -1442,7 +1524,7 @@ - (void)setUserId:(NSString *)userId startNewSession:(BOOL)startNewSession { NSNumber *timestamp = [NSNumber numberWithLongLong:[[self currentTime] timeIntervalSince1970] * 1000]; [self setSessionId:[timestamp longLongValue]]; [self refreshSessionTime:timestamp]; - if (self->_trackingSessionEvents) { + if (loggingSessionEvent) { [self sendSessionEvent:kAMPSessionStartEvent]; } } diff --git a/Sources/Amplitude/Public/AMPDefaultTrackingOptions.h b/Sources/Amplitude/Public/AMPDefaultTrackingOptions.h new file mode 100644 index 00000000..0b1a63d6 --- /dev/null +++ b/Sources/Amplitude/Public/AMPDefaultTrackingOptions.h @@ -0,0 +1,61 @@ +// +// AMPDefaultTrackingOptions.h +// Copyright (c) 2023 Amplitude Inc. (https://amplitude.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#import + +#ifndef AMPDefaultTrackingOptions_h +#define AMPDefaultTrackingOptions_h + +@interface AMPDefaultTrackingOptions : NSObject + +/** + Enables/disables session tracking. Default to enabled. + */ +@property (nonatomic, assign) BOOL sessions; + +/** + Enables/disables app lifecycle events tracking. Default to disabled. + */ +@property (nonatomic, assign) BOOL appLifecycles; + +/** + Enables/disables deep link events tracking. Default to disabled. + */ +@property (nonatomic, assign) BOOL deepLinks; + +/** + Enables/disables screen view events tracking. Default to disabled. + */ +@property (nonatomic, assign) BOOL screenViews; + +- (instancetype)init; ++ (instancetype)initWithSessions:(BOOL)sessions + appLifecycles:(BOOL)appLifecycles + deepLinks:(BOOL)deepLinks + screenViews:(BOOL)screenViews; ++ (instancetype)initWithAllEnabled; ++ (instancetype)initWithNoneEnabled; + +@end + +#endif /* AMPDefaultTrackingOptions_h */ diff --git a/Sources/Amplitude/Public/Amplitude.h b/Sources/Amplitude/Public/Amplitude.h index 28d42b2a..e6e5b243 100644 --- a/Sources/Amplitude/Public/Amplitude.h +++ b/Sources/Amplitude/Public/Amplitude.h @@ -29,6 +29,7 @@ #import "AMPIngestionMetadata.h" #import "AMPServerZone.h" #import "AMPMiddleware.h" +#import "AMPDefaultTrackingOptions.h" NS_ASSUME_NONNULL_BEGIN @@ -129,7 +130,12 @@ typedef void (^AMPInitCompletionBlock)(void); /** Whether to automatically log start and end session events corresponding to the start and end of a user's session. */ -@property (nonatomic, assign) BOOL trackingSessionEvents; +@property (nonatomic, assign) BOOL trackingSessionEvents DEPRECATED_MSG_ATTRIBUTE("Use `defaultTracking.sessions` instead"); + +/** + Whether to automatically log start and end session events corresponding to the start and end of a user's session. + */ +@property (nonatomic, strong) AMPDefaultTrackingOptions *defaultTracking; /** Library name is default as `amplitude-ios`. diff --git a/Sources/Amplitude/UIViewController+AMPScreen.h b/Sources/Amplitude/UIViewController+AMPScreen.h new file mode 100644 index 00000000..29ab5cff --- /dev/null +++ b/Sources/Amplitude/UIViewController+AMPScreen.h @@ -0,0 +1,35 @@ +// +// UIViewController+AMPScreen.h +// Copyright (c) 2023 Amplitude Inc. (https://amplitude.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include + +#if !TARGET_OS_OSX +#import +#endif + +@interface UIViewController (AMPScreen) + ++ (void)amp_swizzleViewDidAppear; ++ (UIViewController *)amp_rootViewControllerFromView:(UIView *)view; + +@end diff --git a/Sources/Amplitude/UIViewController+AMPScreen.m b/Sources/Amplitude/UIViewController+AMPScreen.m new file mode 100644 index 00000000..84237a76 --- /dev/null +++ b/Sources/Amplitude/UIViewController+AMPScreen.m @@ -0,0 +1,144 @@ +// +// UIViewController+AMPScreen.m +// Copyright (c) 2023 Amplitude Inc. (https://amplitude.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#ifndef AMPLITUDE_DEBUG +#define AMPLITUDE_DEBUG 0 +#endif + +#ifndef AMPLITUDE_LOG +#if AMPLITUDE_DEBUG +# define AMPLITUDE_LOG(fmt, ...) NSLog(fmt, ##__VA_ARGS__) +#else +# define AMPLITUDE_LOG(...) +#endif +#endif + +#import +#import "UIViewController+AMPScreen.h" +#import "Amplitude.h" + +NSString *const kAMPScreenViewed = @"[Amplitude] Screen Viewed"; +NSString *const kAMPEventPropScreenName = @"[Amplitude] Screen Name"; + +@implementation UIViewController (AMPScreen) + ++ (void)amp_swizzleViewDidAppear { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class class = [self class]; + + SEL originalSelector = @selector(viewDidAppear:); + SEL swizzledSelector = @selector(amp_viewDidAppear:); + + Method originalMethod = class_getInstanceMethod(class, originalSelector); + Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); + + BOOL didAddMethod = + class_addMethod(class, + originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)); + + if (didAddMethod) { + class_replaceMethod(class, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, swizzledMethod); + } + }); +} + + ++ (UIViewController *)amp_rootViewControllerFromView:(UIView *)view { + UIViewController *root = view.window.rootViewController; + return [self amp_topViewController:root]; +} + ++ (UIViewController *)amp_topViewController:(UIViewController *)rootViewController { + AMPLITUDE_LOG(@"rootViewController is %@", rootViewController); + UIViewController *nextRootViewController = [self amp_nextRootViewController:rootViewController]; + if (nextRootViewController) { + AMPLITUDE_LOG(@"nextRootViewController is %@", nextRootViewController); + return [self amp_topViewController:nextRootViewController]; + } + + return rootViewController; +} + ++ (UIViewController *)amp_nextRootViewController:(UIViewController *)rootViewController { + UIViewController *presentedViewController = rootViewController.presentedViewController; + if (presentedViewController != nil) { + return presentedViewController; + } + + if ([rootViewController isKindOfClass:[UINavigationController class]]) { + UIViewController *lastViewController = ((UINavigationController *)rootViewController).viewControllers.lastObject; + return lastViewController; + } + + if ([rootViewController isKindOfClass:[UITabBarController class]]) { + __auto_type *currentTabViewController = ((UITabBarController*)rootViewController).selectedViewController; + if (currentTabViewController != nil) { + return currentTabViewController; + } + } + + if (rootViewController.childViewControllers.count > 0) { + __auto_type *firstChildViewController = rootViewController.childViewControllers.firstObject; + if (firstChildViewController != nil) { + return firstChildViewController; + } + } + + return nil; +} + +- (void)amp_viewDidAppear:(BOOL)animated { + AMPLITUDE_LOG(@"self is %@", self); + UIViewController *top = [[self class] amp_rootViewControllerFromView:self.view]; + if (!top) { + AMPLITUDE_LOG(@"Failed to infer screen"); + return; + } + + NSString *name = [top title]; + if (!name || name.length == 0) { + // if no class title found, try view controller's description + name = [[[top class] description] stringByReplacingOccurrencesOfString:@"ViewController" withString:@""]; + if (name.length == 0) { + AMPLITUDE_LOG(@"Failed to infer screen name"); + name = @"Unknown"; + } + } + + [[Amplitude instance] logEvent:kAMPScreenViewed withEventProperties:@{ + kAMPEventPropScreenName: name ?: @"", + }]; + + // call original method, this is not recurrsive method call + [self amp_viewDidAppear:animated]; +} + +@end diff --git a/Tests/DefaultTrackingOptionsTests.m b/Tests/DefaultTrackingOptionsTests.m new file mode 100644 index 00000000..a3d5f466 --- /dev/null +++ b/Tests/DefaultTrackingOptionsTests.m @@ -0,0 +1,58 @@ +// +// DefaultTrackingOptionsTests.m +// Amplitude +// +// Created by Marvin Liu on 6/16/23. +// Copyright © 2023 Amplitude. All rights reserved. +// + +#import +#import "AMPDefaultTrackingOptions.h" +#import "AMPConstants.h" + +@interface DefaultTrackingOptionsTests : XCTestCase + +@end + +@implementation DefaultTrackingOptionsTests + +- (void)testInit { + AMPDefaultTrackingOptions *instance = [[AMPDefaultTrackingOptions alloc] init]; + + XCTAssertFalse(instance.sessions); + XCTAssertFalse(instance.appLifecycles); + XCTAssertFalse(instance.deepLinks); + XCTAssertFalse(instance.screenViews); +} + +- (void)testInitWithCustomeOptions { + AMPDefaultTrackingOptions *instance = [AMPDefaultTrackingOptions initWithSessions:NO + appLifecycles:YES + deepLinks:YES + screenViews:YES]; + + XCTAssertFalse(instance.sessions); + XCTAssertTrue(instance.appLifecycles); + XCTAssertTrue(instance.deepLinks); + XCTAssertTrue(instance.screenViews); +} + +- (void)testInitWithAllEnabled { + AMPDefaultTrackingOptions *instance = [AMPDefaultTrackingOptions initWithAllEnabled]; + + XCTAssertTrue(instance.sessions); + XCTAssertTrue(instance.appLifecycles); + XCTAssertTrue(instance.deepLinks); + XCTAssertTrue(instance.screenViews); +} + +- (void)testInitWithNoneEnabled { + AMPDefaultTrackingOptions *instance = [AMPDefaultTrackingOptions initWithNoneEnabled]; + + XCTAssertFalse(instance.sessions); + XCTAssertFalse(instance.appLifecycles); + XCTAssertFalse(instance.deepLinks); + XCTAssertFalse(instance.screenViews); +} + +@end