diff --git a/Amplitude.xcodeproj/project.pbxproj b/Amplitude.xcodeproj/project.pbxproj index dcf1e845..4b976f45 100644 --- a/Amplitude.xcodeproj/project.pbxproj +++ b/Amplitude.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 605E43CA1BA0D92A00FD78CE /* AMPDatabaseHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 605E43C91BA0D92A00FD78CE /* AMPDatabaseHelper.m */; }; 605E43CD1BA0FEE500FD78CE /* AMPDatabaseHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 605E43CC1BA0FEE500FD78CE /* AMPDatabaseHelperTests.m */; }; 605E43CF1BA1065D00FD78CE /* AMPDatabaseHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 605E43C91BA0D92A00FD78CE /* AMPDatabaseHelper.m */; }; + 608865D51BC30EBB00233023 /* AMPIdentify.m in Sources */ = {isa = PBXBuildFile; fileRef = 608865D41BC30EBB00233023 /* AMPIdentify.m */; settings = {ASSET_TAGS = (); }; }; + 608865D71BC311D700233023 /* AMPIdentify.m in Sources */ = {isa = PBXBuildFile; fileRef = 608865D41BC30EBB00233023 /* AMPIdentify.m */; settings = {ASSET_TAGS = (); }; }; + 608865D91BC3176600233023 /* IdentifyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 608865D81BC3176600233023 /* IdentifyTests.m */; settings = {ASSET_TAGS = (); }; }; 60D418B41BC1BFE9006CC505 /* AMPUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D418B31BC1BFE9006CC505 /* AMPUtils.m */; settings = {ASSET_TAGS = (); }; }; 60D418B51BC1C019006CC505 /* AMPUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 60D418B31BC1BFE9006CC505 /* AMPUtils.m */; settings = {ASSET_TAGS = (); }; }; 9D265EF41AB3FA2500399D93 /* SSLPinningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D265EF31AB3FA2500399D93 /* SSLPinningTests.m */; }; @@ -61,7 +64,9 @@ 605E43C91BA0D92A00FD78CE /* AMPDatabaseHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMPDatabaseHelper.m; sourceTree = ""; }; 605E43CB1BA0DB4F00FD78CE /* AMPDatabaseHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMPDatabaseHelper.h; sourceTree = ""; }; 605E43CC1BA0FEE500FD78CE /* AMPDatabaseHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMPDatabaseHelperTests.m; sourceTree = ""; }; - 605E43CE1BA0FF4300FD78CE /* AMPDatabaseHelperTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMPDatabaseHelperTests.h; sourceTree = ""; }; + 608865D41BC30EBB00233023 /* AMPIdentify.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMPIdentify.m; sourceTree = ""; }; + 608865D61BC30ECA00233023 /* AMPIdentify.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AMPIdentify.h; sourceTree = ""; }; + 608865D81BC3176600233023 /* IdentifyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IdentifyTests.m; sourceTree = ""; }; 60D418B21BC1BFE9006CC505 /* AMPUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMPUtils.h; sourceTree = ""; }; 60D418B31BC1BFE9006CC505 /* AMPUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMPUtils.m; sourceTree = ""; }; 73478F50C66F6A68F4367561 /* Pods-test.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-test.debug.xcconfig"; path = "Pods/Target Support Files/Pods-test/Pods-test.debug.xcconfig"; sourceTree = ""; }; @@ -182,15 +187,15 @@ E98C05251A48E7FE00800C63 /* Amplitude */ = { isa = PBXGroup; children = ( - 60D418B21BC1BFE9006CC505 /* AMPUtils.h */, - 60D418B31BC1BFE9006CC505 /* AMPUtils.m */, E96785E11A48E93F00887CCD /* AMPARCMacros.h */, E96785E21A48E93F00887CCD /* AMPConstants.h */, 9DC708591AD4B28300949778 /* AMPConstants.m */, - 605E43C91BA0D92A00FD78CE /* AMPDatabaseHelper.m */, 605E43CB1BA0DB4F00FD78CE /* AMPDatabaseHelper.h */, + 605E43C91BA0D92A00FD78CE /* AMPDatabaseHelper.m */, E96785E31A48E93F00887CCD /* AMPDeviceInfo.h */, E96785E41A48E93F00887CCD /* AMPDeviceInfo.m */, + 608865D61BC30ECA00233023 /* AMPIdentify.h */, + 608865D41BC30EBB00233023 /* AMPIdentify.m */, E96785E61A48E93F00887CCD /* Amplitude.h */, E96785E71A48E93F00887CCD /* Amplitude.m */, 9DC708561AD4A49E00949778 /* Amplitude+SSLPinning.h */, @@ -198,6 +203,8 @@ E96785E91A48E93F00887CCD /* AMPLocationManagerDelegate.m */, 9D40E1791AB3BF7F0095C7C6 /* AMPURLConnection.h */, 9D40E17A1AB3BF7F0095C7C6 /* AMPURLConnection.m */, + 60D418B21BC1BFE9006CC505 /* AMPUtils.h */, + 60D418B31BC1BFE9006CC505 /* AMPUtils.m */, 9D76A3A41ABA5F890062E132 /* ComodoRsaCA.der */, 9D76A3A31ABA5F890062E132 /* ComodoRsaDomainValidationCA.der */, 9D6E194B1ACCDE1C00352C6C /* SSLCertificatePinning */, @@ -208,7 +215,6 @@ E98C052F1A48E7FE00800C63 /* AmplitudeTests */ = { isa = PBXGroup; children = ( - 605E43CE1BA0FF4300FD78CE /* AMPDatabaseHelperTests.h */, 605E43CC1BA0FEE500FD78CE /* AMPDatabaseHelperTests.m */, 9DFBB9C51AB0D1DD0017F703 /* Amplitude+Test.h */, 9DFBB9C61AB0D1DD0017F703 /* Amplitude+Test.m */, @@ -217,6 +223,7 @@ 9DFBB9C81AB0D26E0017F703 /* BaseTestCase.h */, 9DFBB9C91AB0D26E0017F703 /* BaseTestCase.m */, 9DDE2C021AE7069200B740EC /* DeviceInfoTests.m */, + 608865D81BC3176600233023 /* IdentifyTests.m */, 9D40E1941AB3F6550095C7C6 /* InvalidCertificationAuthority.der */, 9DFBB9CB1AB0D47A0017F703 /* SessionTests.m */, 9D82D1D71AC1006600C3F321 /* SetupTests.m */, @@ -393,6 +400,7 @@ files = ( 9D6E19541ACCDE1C00352C6C /* ISPPinnedNSURLConnectionDelegate.m in Sources */, 605E43CA1BA0D92A00FD78CE /* AMPDatabaseHelper.m in Sources */, + 608865D51BC30EBB00233023 /* AMPIdentify.m in Sources */, 60D418B41BC1BFE9006CC505 /* AMPUtils.m in Sources */, 9D40E17E1AB3BF7F0095C7C6 /* AMPURLConnection.m in Sources */, E96785EC1A48E93F00887CCD /* AMPDeviceInfo.m in Sources */, @@ -409,7 +417,9 @@ buildActionMask = 2147483647; files = ( 9D6E19561ACCDE9000352C6C /* ISPCertificatePinning.m in Sources */, + 608865D91BC3176600233023 /* IdentifyTests.m in Sources */, 605E43CF1BA1065D00FD78CE /* AMPDatabaseHelper.m in Sources */, + 608865D71BC311D700233023 /* AMPIdentify.m in Sources */, 9D6E19571ACCDE9000352C6C /* ISPPinnedNSURLConnectionDelegate.m in Sources */, 9D6E19581ACCDE9000352C6C /* ISPPinnedNSURLSessionDelegate.m in Sources */, 602A9A6B1B754E7B0067230C /* AmplitudeTests.m in Sources */, @@ -512,6 +522,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 764FF1F5D4DC82CCE3BF4FE7 /* Pods.debug.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_OBJC_ARC = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -526,6 +537,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 78F948F3BAF6B3E720A38B16 /* Pods.release.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_OBJC_ARC = YES; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/Amplitude/AMPConstants.h b/Amplitude/AMPConstants.h index 052fe8c8..ead68dde 100644 --- a/Amplitude/AMPConstants.h +++ b/Amplitude/AMPConstants.h @@ -16,3 +16,10 @@ extern const int kAMPEventMaxCount; extern const int kAMPEventRemoveBatchSize; extern const int kAMPEventUploadPeriodSeconds; extern const long kAMPMinTimeBetweenSessionsMillis; +extern const int kAMPMaxStringLength; + +extern NSString *const IDENTIFY_EVENT; +extern NSString *const AMP_OP_ADD; +extern NSString *const AMP_OP_SET; +extern NSString *const AMP_OP_SET_ONCE; +extern NSString *const AMP_OP_UNSET; diff --git a/Amplitude/AMPConstants.m b/Amplitude/AMPConstants.m index e7f59fbe..283ba306 100644 --- a/Amplitude/AMPConstants.m +++ b/Amplitude/AMPConstants.m @@ -8,8 +8,8 @@ NSString *const kAMPVersion = @"3.1.1"; NSString *const kAMPEventLogDomain = @"api.amplitude.com"; NSString *const kAMPEventLogUrl = @"https://api.amplitude.com/"; -const int kAMPApiVersion = 2; -const int kAMPDBVersion = 2; +const int kAMPApiVersion = 3; +const int kAMPDBVersion = 3; const int kAMPDBFirstVersion = 2; // to detect if DB exists yet const int kAMPEventUploadThreshold = 30; const int kAMPEventUploadMaxBatchSize = 100; @@ -17,3 +17,10 @@ const int kAMPEventRemoveBatchSize = 20; const int kAMPEventUploadPeriodSeconds = 30; // 30s const long kAMPMinTimeBetweenSessionsMillis = 5 * 60 * 1000; // 5m +const int kAMPMaxStringLength = 1024; + +NSString *const IDENTIFY_EVENT = @"$identify"; +NSString *const AMP_OP_ADD = @"$add"; +NSString *const AMP_OP_SET = @"$set"; +NSString *const AMP_OP_SET_ONCE = @"$setOnce"; +NSString *const AMP_OP_UNSET = @"$unset"; diff --git a/Amplitude/AMPDatabaseHelper.h b/Amplitude/AMPDatabaseHelper.h index e4ba4c78..1e7fa14b 100644 --- a/Amplitude/AMPDatabaseHelper.h +++ b/Amplitude/AMPDatabaseHelper.h @@ -16,11 +16,18 @@ - (BOOL)deleteDB; - (BOOL)addEvent:(NSString*) event; -- (NSDictionary*)getEvents:(long) upToId limit:(long) limit; +- (BOOL)addIdentify:(NSString*) identify; +- (NSMutableArray*)getEvents:(long) upToId limit:(long) limit; +- (NSMutableArray*)getIdentifys:(long) upToId limit:(long) limit; - (int)getEventCount; +- (int)getIdentifyCount; +- (int)getTotalEventCount; - (BOOL)removeEvents:(long) maxId; +- (BOOL)removeIdentifys:(long) maxIdentifyId; - (BOOL)removeEvent:(long) eventId; +- (BOOL)removeIdentify:(long) identifyId; - (long long)getNthEventId:(long) n; +- (long long)getNthIdentifyId:(long) n; - (BOOL)insertOrReplaceKeyValue:(NSString*) key value:(NSString*) value; - (BOOL)insertOrReplaceKeyLongValue:(NSString*) key value:(NSNumber*) value; diff --git a/Amplitude/AMPDatabaseHelper.m b/Amplitude/AMPDatabaseHelper.m index 8b39f86b..12ba4245 100644 --- a/Amplitude/AMPDatabaseHelper.m +++ b/Amplitude/AMPDatabaseHelper.m @@ -23,6 +23,7 @@ @implementation AMPDatabaseHelper } static NSString *const EVENT_TABLE_NAME = @"events"; +static NSString *const IDENTIFY_TABLE_NAME = @"identifys"; static NSString *const ID_FIELD = @"id"; static NSString *const EVENT_FIELD = @"event"; @@ -33,6 +34,7 @@ @implementation AMPDatabaseHelper static NSString *const DROP_TABLE = @"DROP TABLE IF EXISTS %@;"; static NSString *const CREATE_EVENT_TABLE = @"CREATE TABLE IF NOT EXISTS %@ (%@ INTEGER PRIMARY KEY AUTOINCREMENT, %@ TEXT);"; +static NSString *const CREATE_IDENTIFY_TABLE = @"CREATE TABLE IF NOT EXISTS %@ (%@ INTEGER PRIMARY KEY AUTOINCREMENT, %@ TEXT);"; static NSString *const CREATE_STORE_TABLE = @"CREATE TABLE IF NOT EXISTS %@ (%@ TEXT PRIMARY KEY NOT NULL, %@ TEXT);"; static NSString *const CREATE_LONG_STORE_TABLE = @"CREATE TABLE IF NOT EXISTS %@ (%@ TEXT PRIMARY KEY NOT NULL, %@ INTEGER);"; @@ -97,6 +99,9 @@ - (BOOL)createTables NSString *createEventsTable = [NSString stringWithFormat:CREATE_EVENT_TABLE, EVENT_TABLE_NAME, ID_FIELD, EVENT_FIELD]; success &= [db executeUpdate:createEventsTable]; + NSString *createIdentifysTable = [NSString stringWithFormat:CREATE_IDENTIFY_TABLE, IDENTIFY_TABLE_NAME, ID_FIELD, EVENT_FIELD]; + success &= [db executeUpdate:createIdentifysTable]; + NSString *createStoreTable = [NSString stringWithFormat:CREATE_STORE_TABLE, STORE_TABLE_NAME, KEY_FIELD, VALUE_FIELD]; success &= [db executeUpdate:createStoreTable]; @@ -136,6 +141,9 @@ - (BOOL)upgrade:(int) oldVersion newVersion:(int) newVersion if (newVersion <= 2) break; } case 2: { + NSString *createIdentifysTable = [NSString stringWithFormat:CREATE_IDENTIFY_TABLE, IDENTIFY_TABLE_NAME, ID_FIELD, EVENT_FIELD]; + success &= [db executeUpdate:createIdentifysTable]; + if (newVersion <= 3) break; } default: @@ -168,6 +176,9 @@ - (BOOL)dropTables NSString *dropEventTableSQL = [NSString stringWithFormat:DROP_TABLE, EVENT_TABLE_NAME]; success &= [db executeUpdate: dropEventTableSQL]; + NSString *dropIdentifyTableSQL = [NSString stringWithFormat:DROP_TABLE, IDENTIFY_TABLE_NAME]; + success &= [db executeUpdate: dropIdentifyTableSQL]; + NSString *dropStoreTableSQL = [NSString stringWithFormat:DROP_TABLE, STORE_TABLE_NAME]; success &= [db executeUpdate: dropStoreTableSQL]; @@ -208,6 +219,11 @@ - (BOOL)addEvent:(NSString*) event return [self addEventToTable:EVENT_TABLE_NAME event:event]; } +- (BOOL)addIdentify:(NSString*) identifyEvent +{ + return [self addEventToTable:IDENTIFY_TABLE_NAME event:identifyEvent]; +} + - (BOOL)addEventToTable:(NSString*) table event:(NSString*) event { __block BOOL success = NO; @@ -234,15 +250,19 @@ - (BOOL)addEventToTable:(NSString*) table event:(NSString*) event return success; } -- (NSDictionary*)getEvents:(long) upToId limit:(long) limit +- (NSMutableArray*)getEvents:(long) upToId limit:(long) limit { return [self getEventsFromTable:EVENT_TABLE_NAME upToId:upToId limit:limit]; } -- (NSDictionary*)getEventsFromTable:(NSString*) table upToId:(long) upToId limit:(long) limit +- (NSMutableArray*)getIdentifys:(long) upToId limit:(long) limit { - __block long maxId = -1; - __block NSMutableArray *events = [NSMutableArray array]; + return [self getEventsFromTable:IDENTIFY_TABLE_NAME upToId:upToId limit:limit]; +} + +- (NSMutableArray*)getEventsFromTable:(NSString*) table upToId:(long) upToId limit:(long) limit +{ + __block NSMutableArray *events = [[NSMutableArray alloc] init]; [_dbQueue inDatabase:^(FMDatabase *db) { if (![db open]) { @@ -283,14 +303,12 @@ - (NSDictionary*)getEventsFromTable:(NSString*) table upToId:(long) upToId limit [event setValue:[NSNumber numberWithInt:eventId] forKey:@"event_id"]; [events addObject:event]; SAFE_ARC_RELEASE(event); - maxId = eventId; } [db close]; }]; - NSDictionary *fetchedEvents = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithLong:maxId], @"max_id", events, @"events", nil]; - return SAFE_ARC_AUTORELEASE(fetchedEvents); + return SAFE_ARC_AUTORELEASE(events); } - (BOOL)insertOrReplaceKeyValue:(NSString*) key value:(NSString*) value @@ -386,6 +404,16 @@ - (int)getEventCount return [self getEventCountFromTable:EVENT_TABLE_NAME]; } +- (int)getIdentifyCount +{ + return [self getEventCountFromTable:IDENTIFY_TABLE_NAME]; +} + +- (int)getTotalEventCount +{ + return [self getEventCount] + [self getIdentifyCount]; +} + - (int)getEventCountFromTable:(NSString*) table { __block int count = 0; @@ -421,6 +449,11 @@ - (BOOL)removeEvents:(long) maxId return [self removeEventsFromTable:EVENT_TABLE_NAME maxId:maxId]; } +- (BOOL)removeIdentifys:(long) maxIdentifyId +{ + return [self removeEventsFromTable:IDENTIFY_TABLE_NAME maxId:maxIdentifyId]; +} + - (BOOL)removeEventsFromTable:(NSString*) table maxId:(long) maxId { __block BOOL success = NO; @@ -448,6 +481,11 @@ - (BOOL)removeEvent:(long) eventId return [self removeEventFromTable:EVENT_TABLE_NAME eventId:eventId]; } +- (BOOL)removeIdentify:(long) identifyId +{ + return [self removeEventFromTable:IDENTIFY_TABLE_NAME eventId:identifyId]; +} + - (BOOL)removeEventFromTable:(NSString*) table eventId:(long) eventId { __block BOOL success = NO; @@ -475,6 +513,11 @@ - (long long)getNthEventId:(long) n return [self getNthEventIdFromTable:EVENT_TABLE_NAME n:n]; } +- (long long)getNthIdentifyId:(long) n +{ + return [self getNthEventIdFromTable:IDENTIFY_TABLE_NAME n:n]; +} + - (long long)getNthEventIdFromTable:(NSString*) table n:(long) n { __block long long eventId = -1; diff --git a/Amplitude/AMPIdentify.h b/Amplitude/AMPIdentify.h new file mode 100644 index 00000000..17d78229 --- /dev/null +++ b/Amplitude/AMPIdentify.h @@ -0,0 +1,19 @@ +// +// AMPIdentify.h +// Amplitude +// +// Created by Daniel Jih on 10/5/15. +// Copyright © 2015 Amplitude. All rights reserved. +// + +@interface AMPIdentify : NSObject + +@property (nonatomic, strong, readonly) NSMutableDictionary *userPropertyOperations; + ++ (instancetype)identify; +- (AMPIdentify*)add:(NSString*) property value:(NSObject*) value; +- (AMPIdentify*)set:(NSString*) property value:(NSObject*) value; +- (AMPIdentify*)setOnce:(NSString*) property value:(NSObject*) value; +- (AMPIdentify*)unset:(NSString*) property; + +@end diff --git a/Amplitude/AMPIdentify.m b/Amplitude/AMPIdentify.m new file mode 100644 index 00000000..850685eb --- /dev/null +++ b/Amplitude/AMPIdentify.m @@ -0,0 +1,88 @@ +// +// AMPIdentify.m +// Amplitude +// +// Created by Daniel Jih on 10/5/15. +// Copyright © 2015 Amplitude. All rights reserved. +// + +#import +#import "AMPIdentify.h" +#import "AMPARCMacros.h" +#import "AMPConstants.h" + +@interface AMPIdentify() +@end + +@implementation AMPIdentify +{ + NSMutableSet *_userProperties; +} + +- (id)init +{ + if (self = [super init]) { + _userPropertyOperations = [[NSMutableDictionary alloc] init]; + _userProperties = [[NSMutableSet alloc] init]; + } + return self; +} + ++ (instancetype)identify +{ + return SAFE_ARC_AUTORELEASE([[self alloc] init]); +} + +- (void)dealloc +{ + SAFE_ARC_RELEASE(_userPropertyOperations); + SAFE_ARC_RELEASE(_userProperties); + SAFE_ARC_SUPER_DEALLOC(); +} + +- (AMPIdentify*)add:(NSString*) property value:(NSObject*) value +{ + if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { + [self addToUserProperties:AMP_OP_ADD property:property value:value]; + } else { + NSLog(@"Unsupported value type for ADD operation, expecting NSNumber or NSString"); + } + return self; +} + +- (AMPIdentify*)set:(NSString*) property value:(NSObject*) value +{ + [self addToUserProperties:AMP_OP_SET property:property value:value]; + return self; +} + +- (AMPIdentify*)setOnce:(NSString*) property value:(NSObject*) value +{ + [self addToUserProperties:AMP_OP_SET_ONCE property:property value:value]; + return self; +} + +- (AMPIdentify*)unset:(NSString*) property +{ + [self addToUserProperties:AMP_OP_UNSET property:property value:@"-"]; + return self; +} + +- (void)addToUserProperties:(NSString*)operation property:(NSString*) property value:(NSObject*) value +{ + // check if property already used in a previous operation + if ([_userProperties containsObject:property]) { + NSLog(@"Already used property '%@' in previous operation, ignoring for operation '%@'", property, operation); + return; + } + + NSMutableDictionary *operations = [_userPropertyOperations objectForKey:operation]; + if (operations == nil) { + operations = [NSMutableDictionary dictionary]; + [_userPropertyOperations setObject:operations forKey:operation]; + } + [operations setObject:value forKey:property]; + [_userProperties addObject:property]; +} + +@end diff --git a/Amplitude/Amplitude.h b/Amplitude/Amplitude.h index b428caf4..8da25eec 100644 --- a/Amplitude/Amplitude.h +++ b/Amplitude/Amplitude.h @@ -2,6 +2,7 @@ // Amplitude.h #import +#import "AMPIdentify.h" /*! @@ -19,15 +20,15 @@ #import "Amplitude.h" // First, be sure to initialize the API in your didFinishLaunchingWithOptions delegate - [Amplitude initializeApiKey:@"YOUR_API_KEY_HERE"]; + [[Amplitude instance] initializeApiKey:@"YOUR_API_KEY_HERE"]; // Track an event anywhere in the app - [Amplitude logEvent:@"EVENT_IDENTIFIER_HERE"]; + [[Amplitude instance] logEvent:@"EVENT_IDENTIFIER_HERE"]; // You can attach additional data to any event by passing a NSDictionary object NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary]; [eventProperties setValue:@"VALUE_GOES_HERE" forKey:@"KEY_GOES_HERE"]; - [Amplitude logEvent:@"Compute Hash" withEventProperties:eventProperties]; + [[Amplitude instance] logEvent:@"Compute Hash" withEventProperties:eventProperties]; For more details on the setup and usage, be sure to check out the docs here: @@ -96,7 +97,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize your shared Analytics instance. - [Amplitude initializeApiKey:@"YOUR_API_KEY_HERE"]; + [[Amplitude instance] initializeApiKey:@"YOUR_API_KEY_HERE"]; // YOUR OTHER APP LAUNCH CODE HERE.... @@ -140,17 +141,35 @@ @param amount The amount of revenue to track, e.g. "3.99". @discussion - To track revenue from a user, call [Amplitude logRevenue:[NSNumber numberWithDouble:3.99]] each time the user generates revenue. + To track revenue from a user, call [[Amplitude instance] logRevenue:[NSNumber numberWithDouble:3.99]] each time the user generates revenue. logRevenue: takes in an NSNumber with the dollar amount of the sale as the only argument. This allows us to automatically display data relevant to revenue on the Amplitude website, including average revenue per daily active user (ARPDAU), 7, 30, and 90 day revenue, lifetime value (LTV) estimates, and revenue by advertising campaign cohort and daily/weekly/monthly cohorts. - For validating revenue, use [Amplitude logRevenue:@"com.company.app.productId" quantity:1 price:[NSNumber numberWithDouble:3.99] receipt:transactionReceipt] + For validating revenue, use [[Amplitude instance] logRevenue:@"com.company.app.productId" quantity:1 price:[NSNumber numberWithDouble:3.99] receipt:transactionReceipt] */ - (void)logRevenue:(NSNumber*) amount; - (void)logRevenue:(NSString*) productIdentifier quantity:(NSInteger) quantity price:(NSNumber*) price; - (void)logRevenue:(NSString*) productIdentifier quantity:(NSInteger) quantity price:(NSNumber*) price receipt:(NSData*) receipt; +/*! + @method + + @abstract + Update user properties using operations provided via Identify API. + + @param identify An AMPIdentify object with the intended user property operations + + @discussion + To update user properties, first create an AMPIdentify object. For example if you wanted to set a user's gender, and then increment their + karma count by 1, you would do: + AMPIdentify *identify = [[[AMPIdentify identify] set:@"gender" value:@"male"] add:@"karma" value:[NSNumber numberWithInt:1]]; + Then you would pass this AMPIdentify object to the identify function to send to the server: [[Amplitude instance] identify:identify]; + The Identify API supports add, set, setOnce, unset operations. See the AMPIdentify.h header file for the method signatures. + */ + +- (void)identify:(AMPIdentify *)identify; + /*! @method diff --git a/Amplitude/Amplitude.m b/Amplitude/Amplitude.m index 389ec388..8626c0ff 100644 --- a/Amplitude/Amplitude.m +++ b/Amplitude/Amplitude.m @@ -20,6 +20,7 @@ #import "AMPURLConnection.h" #import "AMPDatabaseHelper.h" #import "AMPUtils.h" +#import "AMPIdentify.h" #import #import #import @@ -44,15 +45,18 @@ @interface Amplitude () NSString *const kAMPSessionEndEvent = @"session_end"; NSString *const kAMPRevenueEvent = @"revenue_amount"; -NSString *const BACKGROUND_QUEUE_NAME = @"BACKGROUND"; -NSString *const DATABASE_VERSION = @"database_version"; -NSString *const DEVICE_ID = @"device_id"; -NSString *const EVENTS = @"events"; -NSString *const PREVIOUS_SESSION_ID = @"previous_session_id"; -NSString *const PREVIOUS_SESSION_TIME = @"previous_session_time"; -NSString *const MAX_ID = @"max_id"; -NSString *const OPT_OUT = @"opt_out"; -NSString *const USER_ID = @"user_id"; +static NSString *const BACKGROUND_QUEUE_NAME = @"BACKGROUND"; +static NSString *const DATABASE_VERSION = @"database_version"; +static NSString *const DEVICE_ID = @"device_id"; +static NSString *const EVENTS = @"events"; +static NSString *const EVENT_ID = @"event_id"; +static NSString *const PREVIOUS_SESSION_ID = @"previous_session_id"; +static NSString *const PREVIOUS_SESSION_TIME = @"previous_session_time"; +static NSString *const MAX_EVENT_ID = @"max_event_id"; +static NSString *const MAX_IDENTIFY_ID = @"max_identify_id"; +static NSString *const OPT_OUT = @"opt_out"; +static NSString *const USER_ID = @"user_id"; +static NSString *const SEQUENCE_NUMBER = @"sequence_number"; @implementation Amplitude { @@ -66,7 +70,6 @@ @implementation Amplitude { AMPDeviceInfo *_deviceInfo; BOOL _useAdvertisingIdForDeviceId; - NSDictionary *_userProperties; CLLocation *_lastKnownLocation; BOOL _locationListeningEnabled; @@ -350,7 +353,6 @@ - (void) dealloc { SAFE_ARC_RELEASE(_locationManagerDelegate); SAFE_ARC_RELEASE(_propertyList); SAFE_ARC_RELEASE(_propertyListPath); - SAFE_ARC_RELEASE(_userProperties); SAFE_ARC_SUPER_DEALLOC(); } @@ -402,12 +404,16 @@ - (void)initializeApiKey:(NSString*) apiKey userId:(NSString*) userId setUserId: } }]; - UIApplicationState state = [UIApplication sharedApplication].applicationState; - if (state != UIApplicationStateBackground) { + UIApplication * app = [self getSharedApplication]; + if (app) { + UIApplicationState state = app.applicationState; + if (state != UIApplicationStateBackground) { // If this is called while the app is running in the background, for example // via a push notification, don't call enterForeground [self enterForeground]; + } } + _initialized = YES; } @@ -432,6 +438,15 @@ - (BOOL)runOnBackgroundQueue:(void (^)(void))block } } +- (UIApplication *)getSharedApplication +{ + Class UIApplicationClass = NSClassFromString(@"UIApplication"); + if (UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { + return [UIApplication performSelector:@selector(sharedApplication)]; + } + return nil; +} + #pragma mark - logEvent - (void)logEvent:(NSString*) eventType @@ -446,10 +461,10 @@ - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) event - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) eventProperties outOfSession:(BOOL) outOfSession { - [self logEvent:eventType withEventProperties:eventProperties withApiProperties:nil withTimestamp:nil outOfSession:outOfSession]; + [self logEvent:eventType withEventProperties:eventProperties withApiProperties:nil withUserProperties:nil withTimestamp:nil outOfSession:outOfSession]; } -- (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) eventProperties withApiProperties:(NSDictionary*) apiProperties withTimestamp:(NSNumber*) timestamp outOfSession:(BOOL) outOfSession +- (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) eventProperties withApiProperties:(NSDictionary*) apiProperties withUserProperties:(NSDictionary*) userProperties withTimestamp:(NSNumber*) timestamp outOfSession:(BOOL) outOfSession { if (_apiKey == nil) { NSLog(@"ERROR: apiKey cannot be nil or empty, set apiKey with initializeApiKey: before calling logEvent"); @@ -470,7 +485,7 @@ - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) event // Create snapshot of all event json objects, to prevent deallocation crash eventProperties = [eventProperties copy]; apiProperties = [apiProperties mutableCopy]; - NSDictionary *userProperties = [_userProperties copy]; + userProperties = [userProperties copy]; [self runOnBackgroundQueue:^{ AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; @@ -491,9 +506,9 @@ - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) event } [event setValue:eventType forKey:@"event_type"]; - [event setValue:[self replaceWithEmptyJSON:eventProperties] forKey:@"event_properties"]; + [event setValue:[self replaceWithEmptyJSON:[self truncate:eventProperties]] forKey:@"event_properties"]; [event setValue:[self replaceWithEmptyJSON:apiProperties] forKey:@"api_properties"]; - [event setValue:[self replaceWithEmptyJSON:userProperties] forKey:@"user_properties"]; + [event setValue:[self replaceWithEmptyJSON:[self truncate:userProperties]] forKey:@"user_properties"]; [event setValue:[NSNumber numberWithLongLong:outOfSession ? -1 : _sessionId] forKey:@"session_id"]; [event setValue:timestamp forKey:@"timestamp"]; @@ -506,7 +521,11 @@ - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) event // convert event dictionary to JSON String NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:0 error:NULL]; NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - [dbHelper addEvent:jsonString]; + if ([eventType isEqualToString:IDENTIFY_EVENT]) { + [dbHelper addIdentify:jsonString]; + } else { + [dbHelper addEvent:jsonString]; + } SAFE_ARC_RELEASE(jsonString); AMPLITUDE_LOG(@"Logged %@ Event", event[@"event_type"]); @@ -514,8 +533,11 @@ - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) event if ([dbHelper getEventCount] >= self.eventMaxCount) { [dbHelper removeEvents:([dbHelper getNthEventId:kAMPEventRemoveBatchSize])]; } + if ([dbHelper getIdentifyCount] >= self.eventMaxCount) { + [dbHelper removeIdentifys:([dbHelper getNthIdentifyId:kAMPEventRemoveBatchSize])]; + } - int eventCount = [dbHelper getEventCount]; // refetch since events may have been deleted + int eventCount = [dbHelper getTotalEventCount]; // refetch since events may have been deleted if ((eventCount % self.eventUploadThreshold) == 0 && eventCount >= self.eventUploadThreshold) { [self uploadEvents]; } else { @@ -543,6 +565,7 @@ - (void)annotateEvent:(NSMutableDictionary*) event }; [event setValue:library forKey:@"library"]; [event setValue:[AMPUtils generateUUID] forKey:@"uuid"]; + [event setValue:[NSNumber numberWithLongLong:[self getNextSequenceNumber]] forKey:@"sequence_number"]; NSMutableDictionary *apiProperties = [event valueForKey:@"api_properties"]; @@ -615,7 +638,7 @@ - (void)logRevenue:(NSString*) productIdentifier quantity:(NSInteger) quantity p #pragma clang diagnostic pop } - [self logEvent:kAMPRevenueEvent withEventProperties:nil withApiProperties:apiProperties withTimestamp:nil outOfSession:NO]; + [self logEvent:kAMPRevenueEvent withEventProperties:nil withApiProperties:apiProperties withUserProperties:nil withTimestamp:nil outOfSession:NO]; } #pragma mark - Upload events @@ -668,15 +691,20 @@ - (void)uploadEventsWithLimit:(int) limit } AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; - long eventCount = [dbHelper getEventCount]; + long eventCount = [dbHelper getTotalEventCount]; long numEvents = limit > 0 ? fminl(eventCount, limit) : eventCount; if (numEvents == 0) { _updatingCurrently = NO; return; } - NSDictionary *events = [dbHelper getEvents:-1 limit:numEvents]; - NSArray *uploadEvents = [events objectForKey:EVENTS]; - long lastEventIdUploaded = [[events objectForKey:MAX_ID] longValue]; + NSMutableArray *events = [dbHelper getEvents:-1 limit:numEvents]; + NSMutableArray *identifys = [dbHelper getIdentifys:-1 limit:numEvents]; + NSDictionary *merged = [self mergeEventsAndIdentifys:events identifys:identifys numEvents:numEvents]; + + NSMutableArray *uploadEvents = [merged objectForKey:EVENTS]; + long long maxEventId = [[merged objectForKey:MAX_EVENT_ID] longLongValue]; + long long maxIdentifyId = [[merged objectForKey:MAX_IDENTIFY_ID] longLongValue]; + NSError *error = nil; NSData *eventsDataLocal = nil; @try { @@ -694,15 +722,81 @@ - (void)uploadEventsWithLimit:(int) limit } if (eventsDataLocal) { NSString *eventsString = [[NSString alloc] initWithData:eventsDataLocal encoding:NSUTF8StringEncoding]; - [self makeEventUploadPostRequest:kAMPEventLogUrl events:eventsString lastEventIDUploaded:lastEventIdUploaded]; + [self makeEventUploadPostRequest:kAMPEventLogUrl events:eventsString maxEventId:maxEventId maxIdentifyId:maxIdentifyId]; SAFE_ARC_RELEASE(eventsString); } + }]; +} +- (long long)getNextSequenceNumber +{ + AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; + NSNumber *sequenceNumberFromDB = [dbHelper getLongValue:SEQUENCE_NUMBER]; + long long sequenceNumber = 0; + if (sequenceNumberFromDB != nil) { + sequenceNumber = [sequenceNumberFromDB longLongValue]; + } - }]; + sequenceNumber++; + [dbHelper insertOrReplaceKeyLongValue:SEQUENCE_NUMBER value:[NSNumber numberWithLongLong:sequenceNumber]]; + + return sequenceNumber; +} + +- (NSDictionary*)mergeEventsAndIdentifys:(NSMutableArray*)events identifys:(NSMutableArray*)identifys numEvents:(long) numEvents +{ + NSMutableArray *mergedEvents = [[NSMutableArray alloc] init]; + long long maxEventId = -1; + long long maxIdentifyId = -1; + + // NSArrays actually have O(1) performance for push/pop + while ([mergedEvents count] < numEvents) { + NSDictionary *event = nil; + NSDictionary *identify = nil; + + // case 1: no identifys grab from events + if ([identifys count] == 0) { + event = SAFE_ARC_RETAIN(events[0]); + [events removeObjectAtIndex:0]; + maxEventId = [[event objectForKey:@"event_id"] longValue]; + + // case 2: no events grab from identifys + } else if ([events count] == 0) { + identify = SAFE_ARC_RETAIN(identifys[0]); + [identifys removeObjectAtIndex:0]; + maxIdentifyId = [[identify objectForKey:@"event_id"] longValue]; + + // case 3: need to compare sequence numbers + } else { + // events logged before v3.2.0 won't have sequeunce number, put those first + event = SAFE_ARC_RETAIN(events[0]); + identify = SAFE_ARC_RETAIN(identifys[0]); + if ([event objectForKey:SEQUENCE_NUMBER] == nil || + ([[event objectForKey:SEQUENCE_NUMBER] longLongValue] < + [[identify objectForKey:SEQUENCE_NUMBER] longLongValue])) { + [events removeObjectAtIndex:0]; + maxEventId = [[event objectForKey:EVENT_ID] longValue]; + SAFE_ARC_RELEASE(identify); + identify = nil; + } else { + [identifys removeObjectAtIndex:0]; + maxIdentifyId = [[identify objectForKey:EVENT_ID] longValue]; + SAFE_ARC_RELEASE(event); + event = nil; + } + } + + [mergedEvents addObject: event != nil ? event : identify]; + SAFE_ARC_RELEASE(event); + SAFE_ARC_RELEASE(identify); + } + + NSDictionary *results = [[NSDictionary alloc] initWithObjectsAndKeys: mergedEvents, EVENTS, [NSNumber numberWithLongLong:maxEventId], MAX_EVENT_ID, [NSNumber numberWithLongLong:maxIdentifyId], MAX_IDENTIFY_ID, nil]; + SAFE_ARC_RELEASE(mergedEvents); + return SAFE_ARC_AUTORELEASE(results); } -- (void)makeEventUploadPostRequest:(NSString*) url events:(NSString*) events lastEventIDUploaded:(long long) lastEventIdUploaded +- (void)makeEventUploadPostRequest:(NSString*) url events:(NSString*) events maxEventId:(long long) maxEventId maxIdentifyId:(long long) maxIdentifyId { NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; [request setTimeoutInterval:60.0]; @@ -753,7 +847,12 @@ - (void)makeEventUploadPostRequest:(NSString*) url events:(NSString*) events las if ([result isEqualToString:@"success"]) { // success, remove existing events from dictionary uploadSuccessful = YES; - [dbHelper removeEvents:lastEventIdUploaded]; + if (maxEventId >= 0) { + [dbHelper removeEvents:maxEventId]; + } + if (maxIdentifyId >= 0) { + [dbHelper removeIdentifys:maxIdentifyId]; + } } else if ([result isEqualToString:@"invalid_api_key"]) { NSLog(@"ERROR: Invalid API Key, make sure your API key is correct in initializeApiKey:"); } else if ([result isEqualToString:@"bad_checksum"]) { @@ -767,7 +866,12 @@ - (void)makeEventUploadPostRequest:(NSString*) url events:(NSString*) events las } else if ([httpResponse statusCode] == 413) { // If blocked by one massive event, drop it if (_backoffUpload && _backoffUploadBatchSize == 1) { - [dbHelper removeEvent: lastEventIdUploaded]; + if (maxEventId >= 0) { + [dbHelper removeEvent: maxEventId]; + } + if (maxIdentifyId >= 0) { + [dbHelper removeIdentifys: maxIdentifyId]; + } } // server complained about length of request, backoff and try again @@ -809,7 +913,7 @@ - (void)makeEventUploadPostRequest:(NSString*) url events:(NSString*) events las } // Upload finished, allow background task to be ended - [[UIApplication sharedApplication] endBackgroundTask:_uploadTaskID]; + [[self getSharedApplication] endBackgroundTask:_uploadTaskID]; _uploadTaskID = UIBackgroundTaskInvalid; } }]; @@ -819,13 +923,18 @@ - (void)makeEventUploadPostRequest:(NSString*) url events:(NSString*) events las - (void)enterForeground { + UIApplication *app = [self getSharedApplication]; + if (!app) { + return; + } + [self updateLocation]; NSNumber* now = [NSNumber numberWithLongLong:[[self currentTime] timeIntervalSince1970] * 1000]; // Stop uploading if (_uploadTaskID != UIBackgroundTaskInvalid) { - [[UIApplication sharedApplication] endBackgroundTask:_uploadTaskID]; + [app endBackgroundTask:_uploadTaskID]; _uploadTaskID = UIBackgroundTaskInvalid; } [self runOnBackgroundQueue:^{ @@ -837,16 +946,22 @@ - (void)enterForeground - (void)enterBackground { + UIApplication *app = [self getSharedApplication]; + if (!app) { + return; + } + NSNumber* now = [NSNumber numberWithLongLong:[[self currentTime] timeIntervalSince1970] * 1000]; // Stop uploading if (_uploadTaskID != UIBackgroundTaskInvalid) { - [[UIApplication sharedApplication] endBackgroundTask:_uploadTaskID]; + [app endBackgroundTask:_uploadTaskID]; } - _uploadTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + + _uploadTaskID = [app beginBackgroundTaskWithExpirationHandler:^{ //Took too long, manually stop if (_uploadTaskID != UIBackgroundTaskInvalid) { - [[UIApplication sharedApplication] endBackgroundTask:_uploadTaskID]; + [app endBackgroundTask:_uploadTaskID]; _uploadTaskID = UIBackgroundTaskInvalid; } }]; @@ -925,7 +1040,7 @@ - (void)sendSessionEvent:(NSString*) sessionEvent NSMutableDictionary *apiProperties = [NSMutableDictionary dictionary]; [apiProperties setValue:sessionEvent forKey:@"special"]; NSNumber* timestamp = [self lastEventTime]; - [self logEvent:sessionEvent withEventProperties:nil withApiProperties:apiProperties withTimestamp:timestamp outOfSession:NO]; + [self logEvent:sessionEvent withEventProperties:nil withApiProperties:apiProperties withUserProperties:nil withTimestamp:timestamp outOfSession:NO]; } - (BOOL)inSession @@ -991,32 +1106,37 @@ - (void)startSession return; } -#pragma mark - configurations - -- (void)setUserProperties:(NSDictionary*) userProperties +- (void)identify:(AMPIdentify *)identify { - [self setUserProperties:userProperties replace:NO]; + if (identify == nil || [identify.userPropertyOperations count] == 0) { + return; + } + [self logEvent:IDENTIFY_EVENT withEventProperties:nil withApiProperties:nil withUserProperties:identify.userPropertyOperations withTimestamp:nil outOfSession:NO]; } -- (void)setUserProperties:(NSDictionary*) userProperties replace:(BOOL) replace +#pragma mark - configurations + +- (void)setUserProperties:(NSDictionary*) userProperties { - if (![self isArgument:userProperties validType:[NSDictionary class] methodName:@"setUserProperties:"]) { + if (userProperties == nil || ![self isArgument:userProperties validType:[NSDictionary class] methodName:@"setUserProperties:"] || [userProperties count] == 0) { return; } - (void) SAFE_ARC_RETAIN(userProperties); - - // Merge the given properties into the existing set if not asked to replace. - if (!replace && _userProperties) { - NSMutableDictionary *mergedProperties = [_userProperties mutableCopy]; - [mergedProperties addEntriesFromDictionary:userProperties]; - - (void) SAFE_ARC_AUTORELEASE(userProperties); - userProperties = mergedProperties; - } + NSDictionary *copy = [userProperties copy]; + [self runOnBackgroundQueue:^{ + AMPIdentify *identify = [AMPIdentify identify]; + for (NSString *key in copy) { + NSObject *value = [copy objectForKey:key]; + [identify set:key value:value]; + } + [self identify:identify]; + }]; +} - (void) SAFE_ARC_AUTORELEASE(_userProperties); - _userProperties = userProperties; +// maintain for legacy +- (void)setUserProperties:(NSDictionary*) userProperties replace:(BOOL) replace +{ + [self setUserProperties:userProperties]; } - (void)setUserId:(NSString*) userId @@ -1154,6 +1274,40 @@ - (NSDictionary*)replaceWithEmptyJSON:(NSDictionary*) dictionary return dictionary == nil ? [NSMutableDictionary dictionary] : dictionary; } +- (id) truncate:(id) obj +{ + if ([obj isKindOfClass:[NSString class]]) { + obj = (NSString*)obj; + if ([obj length] > kAMPMaxStringLength) { + obj = [obj substringToIndex:kAMPMaxStringLength]; + } + } else if ([obj isKindOfClass:[NSArray class]]) { + NSMutableArray *arr = [NSMutableArray array]; + id objCopy = [obj copy]; + for (id i in objCopy) { + [arr addObject:[self truncate:i]]; + } + SAFE_ARC_RELEASE(objCopy); + obj = [NSArray arrayWithArray:arr]; + } else if ([obj isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + id objCopy = [obj copy]; + for (id key in objCopy) { + NSString *coercedKey; + if (![key isKindOfClass:[NSString class]]) { + coercedKey = [key description]; + NSLog(@"WARNING: Non-string property key, received %@, coercing to %@", [key class], coercedKey); + } else { + coercedKey = key; + } + dict[coercedKey] = [self truncate:objCopy[key]]; + } + SAFE_ARC_RELEASE(objCopy); + obj = [NSDictionary dictionaryWithDictionary:dict]; + } + return obj; +} + - (id) makeJSONSerializable:(id) obj { if (obj == nil) { diff --git a/AmplitudeTests/AMPDatabaseHelperTests.h b/AmplitudeTests/AMPDatabaseHelperTests.h deleted file mode 100644 index a7bffa5e..00000000 --- a/AmplitudeTests/AMPDatabaseHelperTests.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// AMPDatabaseHelperTests.h -// Amplitude -// -// Created by Daniel Jih on 9/9/15. -// Copyright (c) 2015 Amplitude. All rights reserved. -// - -#import -#import "AMPDatabaseHelper.h" - -@interface AMPDatabaseHelperTests : XCTestCase - -@property (nonatomic, strong) AMPDatabaseHelper *databaseHelper; - -@end diff --git a/AmplitudeTests/AMPDatabaseHelperTests.m b/AmplitudeTests/AMPDatabaseHelperTests.m index 676cc790..0d47b4f3 100644 --- a/AmplitudeTests/AMPDatabaseHelperTests.m +++ b/AmplitudeTests/AMPDatabaseHelperTests.m @@ -8,8 +8,12 @@ #import #import "AMPDatabaseHelper.h" -#import "AMPDatabaseHelperTests.h" #import "AMPARCMacros.h" +#import "AMPConstants.h" + +@interface AMPDatabaseHelperTests : XCTestCase +@property (nonatomic, strong) AMPDatabaseHelper *databaseHelper; +@end @implementation AMPDatabaseHelperTests {} @@ -29,19 +33,18 @@ - (void)testCreate { XCTAssertTrue([self.databaseHelper addEvent:@"test"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + XCTAssertTrue([self.databaseHelper addIdentify:@"identify"]); } - (void)testGetEvents { - NSDictionary *emptyResults = [self.databaseHelper getEvents:-1 limit:-1]; - XCTAssertEqual(-1, [[emptyResults objectForKey:@"max_id"] longValue]); + NSArray *emptyResults = [self.databaseHelper getEvents:-1 limit:-1]; + XCTAssertEqual(0, [emptyResults count]); [self.databaseHelper addEvent:@"{\"event_type\":\"test1\"}"]; [self.databaseHelper addEvent:@"{\"event_type\":\"test2\"}"]; // test get all events - NSDictionary *results = [self.databaseHelper getEvents:-1 limit:-1]; - XCTAssertEqual(2, [[results objectForKey:@"max_id"] longValue]); - NSArray *events = [results objectForKey:@"events"]; + NSArray *events = [self.databaseHelper getEvents:-1 limit:-1]; XCTAssertEqual(2, events.count); XCTAssert([[[events objectAtIndex:0] objectForKey:@"event_type"] isEqualToString:@"test1"]); XCTAssertEqual(1, [[[events objectAtIndex:0] objectForKey:@"event_id"] longValue]); @@ -49,16 +52,46 @@ - (void)testGetEvents { XCTAssertEqual(2, [[[events objectAtIndex:1] objectForKey:@"event_id"] longValue]); // test get all events up to certain id - results = [self.databaseHelper getEvents:1 limit:-1]; - XCTAssertEqual(1, [[results objectForKey:@"max_id"] longValue]); - events = [results objectForKey:@"events"]; + events = [self.databaseHelper getEvents:1 limit:-1]; XCTAssertEqual(1, events.count); + XCTAssertEqual(1, [[events[0] objectForKey:@"event_id"] intValue]); // test get all events with limit - results = [self.databaseHelper getEvents:1 limit:1]; - XCTAssertEqual(1, [[results objectForKey:@"max_id"] longValue]); - events = [results objectForKey:@"events"]; + events = [self.databaseHelper getEvents:1 limit:1]; + XCTAssertEqual(1, events.count); + XCTAssertEqual(1, [[events[0] objectForKey:@"event_id"] intValue]); +} + +- (void)testGetIdentifys { + NSArray *emptyResults = [self.databaseHelper getIdentifys:-1 limit:-1]; + XCTAssertEqual(0, [emptyResults count]); + XCTAssertEqual(0, [self.databaseHelper getTotalEventCount]); + + [self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]; + [self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]; + + XCTAssertEqual(0, [self.databaseHelper getEventCount]); + XCTAssertEqual(2, [self.databaseHelper getIdentifyCount]); + XCTAssertEqual(2, [self.databaseHelper getTotalEventCount]); + + // test get all identify events + NSArray *events = [self.databaseHelper getIdentifys:-1 limit:-1]; + XCTAssertEqual(2, events.count); + XCTAssertEqual(2, [[events[1] objectForKey:@"event_id"] intValue]); + XCTAssert([[[events objectAtIndex:0] objectForKey:@"event_type"] isEqualToString:IDENTIFY_EVENT]); + XCTAssertEqual(1, [[[events objectAtIndex:0] objectForKey:@"event_id"] longValue]); + XCTAssert([[[events objectAtIndex:1] objectForKey:@"event_type"] isEqualToString:IDENTIFY_EVENT]); + XCTAssertEqual(2, [[[events objectAtIndex:1] objectForKey:@"event_id"] longValue]); + + // test get all identify events up to certain id + events = [self.databaseHelper getIdentifys:1 limit:-1]; XCTAssertEqual(1, events.count); + XCTAssertEqual(1, [[events[0] objectForKey:@"event_id"] intValue]); + + // test get all identify events with limit + events = [self.databaseHelper getIdentifys:1 limit:1]; + XCTAssertEqual(1, events.count); + XCTAssertEqual(1, [[events[0] objectForKey:@"event_id"] intValue]); } - (void)testInsertAndReplaceKeyValue { @@ -111,6 +144,27 @@ - (void)testEventCount { XCTAssertEqual(0, [self.databaseHelper getEventCount]); } +- (void)testIdentifyCount { + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + + XCTAssertEqual(0, [self.databaseHelper getEventCount]); + XCTAssertEqual(5, [self.databaseHelper getIdentifyCount]); + XCTAssertEqual(5, [self.databaseHelper getTotalEventCount]); + + [self.databaseHelper removeIdentify:1]; + XCTAssertEqual(4, [self.databaseHelper getIdentifyCount]); + + [self.databaseHelper removeIdentifys:3]; + XCTAssertEqual(2, [self.databaseHelper getIdentifyCount]); + + [self.databaseHelper removeIdentifys:10]; + XCTAssertEqual(0, [self.databaseHelper getIdentifyCount]); +} + - (void)testGetNthEventId { XCTAssertTrue([self.databaseHelper addEvent:@"{\"event_type\":\"test1\"}"]); XCTAssertTrue([self.databaseHelper addEvent:@"{\"event_type\":\"test2\"}"]); @@ -135,6 +189,82 @@ - (void)testGetNthEventId { XCTAssertEqual(-1, [self.databaseHelper getNthEventId:1]); } +- (void)testGetNthIdentifyId { + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + + XCTAssertEqual(1, [self.databaseHelper getNthIdentifyId:0]); + XCTAssertEqual(1, [self.databaseHelper getNthIdentifyId:1]); + XCTAssertEqual(2, [self.databaseHelper getNthIdentifyId:2]); + XCTAssertEqual(3, [self.databaseHelper getNthIdentifyId:3]); + XCTAssertEqual(4, [self.databaseHelper getNthIdentifyId:4]); + XCTAssertEqual(5, [self.databaseHelper getNthIdentifyId:5]); + + [self.databaseHelper removeIdentify:1]; + XCTAssertEqual(2, [self.databaseHelper getNthIdentifyId:1]); + + [self.databaseHelper removeIdentifys:3]; + XCTAssertEqual(4, [self.databaseHelper getNthIdentifyId:1]); + + [self.databaseHelper removeIdentifys:10]; + XCTAssertEqual(-1, [self.databaseHelper getNthIdentifyId:1]); +} + +- (void)testNoConflictBetweenEventsAndIdentifys{ + XCTAssertTrue([self.databaseHelper addEvent:@"{\"event_type\":\"test1\"}"]); + XCTAssertTrue([self.databaseHelper addEvent:@"{\"event_type\":\"test2\"}"]); + XCTAssertTrue([self.databaseHelper addEvent:@"{\"event_type\":\"test3\"}"]); + XCTAssertTrue([self.databaseHelper addEvent:@"{\"event_type\":\"test4\"}"]); + XCTAssertEqual(4, [self.databaseHelper getEventCount]); + XCTAssertEqual(0, [self.databaseHelper getIdentifyCount]); + + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertTrue([self.databaseHelper addIdentify:@"{\"event_type\":\"$identify\"}"]); + XCTAssertEqual(4, [self.databaseHelper getEventCount]); + XCTAssertEqual(2, [self.databaseHelper getIdentifyCount]); + + [self.databaseHelper removeEvent:1]; + XCTAssertEqual(3, [self.databaseHelper getEventCount]); + XCTAssertEqual(2, [self.databaseHelper getIdentifyCount]); + + [self.databaseHelper removeIdentify:1]; + XCTAssertEqual(3, [self.databaseHelper getEventCount]); + XCTAssertEqual(1, [self.databaseHelper getIdentifyCount]); + + [self.databaseHelper removeEvents:4]; + XCTAssertEqual(0, [self.databaseHelper getEventCount]); + XCTAssertEqual(1, [self.databaseHelper getIdentifyCount]); +} + +- (void)testUpgradeFromVersion0ToVersion2{ + // inserts will fail since no tables exist + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper addEvent:@"test_event"]); + + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper insertOrReplaceKeyValue:@"test_key" value:@"test_value"]); + + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper insertOrReplaceKeyLongValue:@"test_key" value:[NSNumber numberWithInt:0]]); + + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); + + // after upgrade, can insert into event, store, long_store + [self.databaseHelper dropTables]; + XCTAssertTrue([self.databaseHelper upgrade:0 newVersion:2]); + XCTAssertTrue([self.databaseHelper addEvent:@"test"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + + // still can't insert into identify + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); +} + +// should be exact same as upgrading from 0 to 2 - (void)testUpgradeFromVersion1ToVersion2{ // inserts will fail since no tables exist [self.databaseHelper dropTables]; @@ -146,15 +276,43 @@ - (void)testUpgradeFromVersion1ToVersion2{ [self.databaseHelper dropTables]; XCTAssertFalse([self.databaseHelper insertOrReplaceKeyLongValue:@"test_key" value:[NSNumber numberWithInt:0]]); + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); + // after upgrade, can insert into event, store, long_store [self.databaseHelper dropTables]; XCTAssertTrue([self.databaseHelper upgrade:1 newVersion:2]); XCTAssertTrue([self.databaseHelper addEvent:@"test"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + + // still can't insert into identify + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); } -- (void)testUpgradeFromVersion0ToVersion2{ +- (void)testUpgradeFromVersion2ToVersion3 { + [self.databaseHelper dropTables]; + [self.databaseHelper upgrade:1 newVersion:2]; + + // can insert into events, store, long_store + XCTAssertTrue([self.databaseHelper addEvent:@"test"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + + // insert into identifys fail since table doesn't exist yet + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); + + // after upgrade, can insert into identify + [self.databaseHelper dropTables]; + [self.databaseHelper upgrade:1 newVersion:2]; + [self.databaseHelper upgrade:2 newVersion:3]; + XCTAssertTrue([self.databaseHelper addEvent:@"test"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + XCTAssertTrue([self.databaseHelper addIdentify:@"test_identify"]); +} + +- (void)testUpgradeFromVersion0ToVersion3 { // inserts will fail since no tables exist [self.databaseHelper dropTables]; XCTAssertFalse([self.databaseHelper addEvent:@"test_event"]); @@ -165,20 +323,50 @@ - (void)testUpgradeFromVersion0ToVersion2{ [self.databaseHelper dropTables]; XCTAssertFalse([self.databaseHelper insertOrReplaceKeyLongValue:@"test_key" value:[NSNumber numberWithInt:0]]); - // after upgrade, can insert into event, store, long_store [self.databaseHelper dropTables]; - XCTAssertTrue([self.databaseHelper upgrade:0 newVersion:2]); + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); + + // after upgrade, can insert into event, store, long_store, identify + [self.databaseHelper dropTables]; + XCTAssertTrue([self.databaseHelper upgrade:0 newVersion:3]); + XCTAssertTrue([self.databaseHelper addEvent:@"test"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); + XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + XCTAssertTrue([self.databaseHelper addIdentify:@"test_identify"]); +} + +// should be exact same as upgrading from 0 to 3 +- (void)testUpgradeFromVersion1ToVersion3 { + // inserts will fail since no tables exist + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper addEvent:@"test_event"]); + + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper insertOrReplaceKeyValue:@"test_key" value:@"test_value"]); + + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper insertOrReplaceKeyLongValue:@"test_key" value:[NSNumber numberWithInt:0]]); + + [self.databaseHelper dropTables]; + XCTAssertFalse([self.databaseHelper addIdentify:@"test_identify"]); + + // after upgrade, can insert into event, store, long_store, identify + [self.databaseHelper dropTables]; + XCTAssertTrue([self.databaseHelper upgrade:1 newVersion:3]); XCTAssertTrue([self.databaseHelper addEvent:@"test"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + XCTAssertTrue([self.databaseHelper addIdentify:@"test_identify"]); } -- (void)testUpgradeFromVersion2ToVersion2{ - // upgrade does nothing, can insert into event, store, long_store - XCTAssertTrue([self.databaseHelper upgrade:2 newVersion:2]); +- (void)testUpgradeFromVersion3ToVersion3{ + // upgrade does nothing, can insert into event, store, long_store, identify + [self.databaseHelper dropTables]; + XCTAssertTrue([self.databaseHelper upgrade:3 newVersion:3]); XCTAssertTrue([self.databaseHelper addEvent:@"test"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyValue:@"key" value:@"value"]); XCTAssertTrue([self.databaseHelper insertOrReplaceKeyLongValue:@"key" value:[NSNumber numberWithLongLong:0LL]]); + XCTAssertTrue([self.databaseHelper addIdentify:@"test"]); } @end diff --git a/AmplitudeTests/Amplitude+Test.h b/AmplitudeTests/Amplitude+Test.h index c12c0203..94831185 100644 --- a/AmplitudeTests/Amplitude+Test.h +++ b/AmplitudeTests/Amplitude+Test.h @@ -21,6 +21,7 @@ - (void)flushQueueWithQueue:(NSOperationQueue*) queue; - (void)flushUploads:(void (^)())handler; - (NSDictionary *)getLastEvent; +- (NSDictionary *)getLastIdentify; - (NSDictionary *)getEvent:(NSInteger) fromEnd; - (NSUInteger)queuedEventCount; - (void)enterForeground; diff --git a/AmplitudeTests/Amplitude+Test.m b/AmplitudeTests/Amplitude+Test.m index 50b35d4a..86a9ec71 100644 --- a/AmplitudeTests/Amplitude+Test.m +++ b/AmplitudeTests/Amplitude+Test.m @@ -28,17 +28,20 @@ - (void)flushQueueWithQueue:(NSOperationQueue*) queue { } - (NSDictionary *)getEvent:(NSInteger) fromEnd { - NSDictionary *result = [[AMPDatabaseHelper getDatabaseHelper] getEvents:-1 limit:-1]; - NSArray *events = [result objectForKey:@"events"]; + NSArray *events = [[AMPDatabaseHelper getDatabaseHelper] getEvents:-1 limit:-1]; return [events objectAtIndex:[events count] - fromEnd - 1]; } - (NSDictionary *)getLastEvent { - NSDictionary *result = [[AMPDatabaseHelper getDatabaseHelper] getEvents:-1 limit:-1]; - NSArray *events = [result objectForKey:@"events"]; + NSArray *events = [[AMPDatabaseHelper getDatabaseHelper] getEvents:-1 limit:-1]; return [events lastObject]; } +- (NSDictionary *)getLastIdentify { + NSArray *identifys = [[AMPDatabaseHelper getDatabaseHelper] getIdentifys:-1 limit:-1]; + return [identifys lastObject]; +} + - (NSUInteger)queuedEventCount { return [[AMPDatabaseHelper getDatabaseHelper] getEventCount]; } diff --git a/AmplitudeTests/AmplitudeTests.m b/AmplitudeTests/AmplitudeTests.m index ae8629e3..9f471e88 100644 --- a/AmplitudeTests/AmplitudeTests.m +++ b/AmplitudeTests/AmplitudeTests.m @@ -16,6 +16,13 @@ #import "AMPDeviceInfo.h" #import "AMPARCMacros.h" +// expose private methods for unit testing +@interface Amplitude (Tests) +- (NSDictionary*)mergeEventsAndIdentifys:(NSMutableArray*)events identifys:(NSMutableArray*)identifys numEvents:(long) numEvents; +- (id) truncate:(id) obj; +- (long long)getNextSequenceNumber; +@end + @interface AmplitudeTests : BaseTestCase @end @@ -165,13 +172,200 @@ - (void)testUUIDInEvent { [self.amplitude flushQueue]; XCTAssertEqual([self.amplitude queuedEventCount], 2); - NSDictionary *eventsDict = [[AMPDatabaseHelper getDatabaseHelper] getEvents:-1 limit:-1]; - XCTAssertEqual([[eventsDict objectForKey:@"max_id"] intValue], 2); - NSArray *events = [eventsDict objectForKey:@"events"]; + NSArray *events = [[AMPDatabaseHelper getDatabaseHelper] getEvents:-1 limit:-1]; + XCTAssertEqual(2, [[events[1] objectForKey:@"event_id"] intValue]); XCTAssertNotNil([events[0] objectForKey:@"uuid"]); XCTAssertNotNil([events[1] objectForKey:@"uuid"]); XCTAssertNotEqual([events[0] objectForKey:@"uuid"], [events[1] objectForKey:@"uuid"]); } +- (void)testIdentify { + AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; + [self.amplitude setEventUploadThreshold:2]; + + AMPIdentify *identify = [[AMPIdentify identify] set:@"key1" value:@"value1"]; + [self.amplitude identify:identify]; + [self.amplitude flushQueue]; + + XCTAssertEqual([dbHelper getEventCount], 0); + XCTAssertEqual([dbHelper getIdentifyCount], 1); + XCTAssertEqual([dbHelper getTotalEventCount], 1); + + NSDictionary *operations = [NSDictionary dictionaryWithObject:@"value1" forKey:@"key1"]; + NSDictionary *expected = [NSDictionary dictionaryWithObject:operations forKey:@"$set"]; + NSDictionary *event = [self.amplitude getLastIdentify]; + XCTAssertEqualObjects([event objectForKey:@"event_type"], IDENTIFY_EVENT); + XCTAssertEqualObjects([event objectForKey:@"user_properties"], expected); + XCTAssertEqualObjects([event objectForKey:@"event_properties"], [NSDictionary dictionary]); // event properties should be empty + XCTAssertEqual(1, [[event objectForKey:@"sequence_number"] intValue]); + + NSMutableDictionary *serverResponse = [NSMutableDictionary dictionaryWithDictionary: + @{ @"response" : [[NSHTTPURLResponse alloc] initWithURL:nil statusCode:200 HTTPVersion:nil headerFields:@{}], + @"data" : [@"success" dataUsingEncoding:NSUTF8StringEncoding] + }]; + [self setupAsyncResponse:_connectionMock response:serverResponse]; + AMPIdentify *identify2 = [[[AMPIdentify alloc] init] set:@"key2" value:@"value2"]; + [self.amplitude identify:identify2]; + SAFE_ARC_RELEASE(identify2); + [self.amplitude flushQueue]; + + XCTAssertEqual([dbHelper getEventCount], 0); + XCTAssertEqual([dbHelper getIdentifyCount], 0); + XCTAssertEqual([dbHelper getTotalEventCount], 0); +} + +- (void)testMergeEventsAndIdentifys { + AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; + [self.amplitude setEventUploadThreshold:7]; + NSMutableDictionary *serverResponse = [NSMutableDictionary dictionaryWithDictionary: + @{ @"response" : [[NSHTTPURLResponse alloc] initWithURL:nil statusCode:200 HTTPVersion:nil headerFields:@{}], + @"data" : [@"success" dataUsingEncoding:NSUTF8StringEncoding] + }]; + [self setupAsyncResponse:_connectionMock response:serverResponse]; + + [self.amplitude logEvent:@"test_event1"]; + [self.amplitude identify:[[AMPIdentify identify] add:@"photoCount" value:[NSNumber numberWithInt:1]]]; + [self.amplitude logEvent:@"test_event2"]; + [self.amplitude logEvent:@"test_event3"]; + [self.amplitude logEvent:@"test_event4"]; + [self.amplitude identify:[[AMPIdentify identify] set:@"gender" value:@"male"]]; + [self.amplitude flushQueue]; + + XCTAssertEqual([dbHelper getEventCount], 4); + XCTAssertEqual([dbHelper getIdentifyCount], 2); + XCTAssertEqual([dbHelper getTotalEventCount], 6); + + // verify merging + NSMutableArray *events = [dbHelper getEvents:-1 limit:-1]; + NSMutableArray *identifys = [dbHelper getIdentifys:-1 limit:-1]; + NSDictionary *merged = [self.amplitude mergeEventsAndIdentifys:events identifys:identifys numEvents:[dbHelper getTotalEventCount]]; + NSArray *mergedEvents = [merged objectForKey:@"events"]; + + XCTAssertEqual(4, [[merged objectForKey:@"max_event_id"] intValue]); + XCTAssertEqual(2, [[merged objectForKey:@"max_identify_id"] intValue]); + XCTAssertEqual(6, [mergedEvents count]); + + XCTAssertEqualObjects([mergedEvents[0] objectForKey:@"event_type"], @"test_event1"); + XCTAssertEqual([[mergedEvents[0] objectForKey:@"event_id"] intValue], 1); + XCTAssertEqual([[mergedEvents[0] objectForKey:@"sequence_number"] intValue], 1); + + XCTAssertEqualObjects([mergedEvents[1] objectForKey:@"event_type"], @"$identify"); + XCTAssertEqual([[mergedEvents[1] objectForKey:@"event_id"] intValue], 1); + XCTAssertEqual([[mergedEvents[1] objectForKey:@"sequence_number"] intValue], 2); + XCTAssertEqualObjects([mergedEvents[1] objectForKey:@"user_properties"], [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"photoCount"] forKey:@"$add"]); + + XCTAssertEqualObjects([mergedEvents[2] objectForKey:@"event_type"], @"test_event2"); + XCTAssertEqual([[mergedEvents[2] objectForKey:@"event_id"] intValue], 2); + XCTAssertEqual([[mergedEvents[2] objectForKey:@"sequence_number"] intValue], 3); + + XCTAssertEqualObjects([mergedEvents[3] objectForKey:@"event_type"], @"test_event3"); + XCTAssertEqual([[mergedEvents[3] objectForKey:@"event_id"] intValue], 3); + XCTAssertEqual([[mergedEvents[3] objectForKey:@"sequence_number"] intValue], 4); + + XCTAssertEqualObjects([mergedEvents[4] objectForKey:@"event_type"], @"test_event4"); + XCTAssertEqual([[mergedEvents[4] objectForKey:@"event_id"] intValue], 4); + XCTAssertEqual([[mergedEvents[4] objectForKey:@"sequence_number"] intValue], 5); + + XCTAssertEqualObjects([mergedEvents[5] objectForKey:@"event_type"], @"$identify"); + XCTAssertEqual([[mergedEvents[5] objectForKey:@"event_id"] intValue], 2); + XCTAssertEqual([[mergedEvents[5] objectForKey:@"sequence_number"] intValue], 6); + XCTAssertEqualObjects([mergedEvents[5] objectForKey:@"user_properties"], [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:@"male" forKey:@"gender"] forKey:@"$set"]); + + [self.amplitude identify:[[AMPIdentify identify] unset:@"karma"]]; + [self.amplitude flushQueue]; + + XCTAssertEqual([dbHelper getEventCount], 0); + XCTAssertEqual([dbHelper getIdentifyCount], 0); + XCTAssertEqual([dbHelper getTotalEventCount], 0); +} + +-(void)testMergeEventsBackwardsCompatible { + AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; + [self.amplitude identify:[[AMPIdentify identify] unset:@"key"]]; + [self.amplitude logEvent:@"test_event"]; + [self.amplitude flushQueue]; + + // reinsert test event without sequence_number + NSMutableDictionary *event = [NSMutableDictionary dictionaryWithDictionary:[self.amplitude getLastEvent]]; + [event removeObjectForKey:@"sequence_number"]; + long eventId = [[event objectForKey:@"event_id"] longValue]; + [dbHelper removeEvent:eventId]; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:0 error:NULL]; + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [dbHelper addEvent:jsonString]; + SAFE_ARC_RELEASE(jsonString); + + // the event without sequence number should be ordered before the identify + NSMutableArray *events = [dbHelper getEvents:-1 limit:-1]; + NSMutableArray *identifys = [dbHelper getIdentifys:-1 limit:-1]; + NSDictionary *merged = [self.amplitude mergeEventsAndIdentifys:events identifys:identifys numEvents:[dbHelper getTotalEventCount]]; + NSArray *mergedEvents = [merged objectForKey:@"events"]; + XCTAssertEqualObjects([mergedEvents[0] objectForKey:@"event_type"], @"test_event"); + XCTAssertNil([mergedEvents[0] objectForKey:@"sequence_number"]); + XCTAssertEqualObjects([mergedEvents[1] objectForKey:@"event_type"], @"$identify"); + XCTAssertEqual(1, [[mergedEvents[1] objectForKey:@"sequence_number"] intValue]); +} + +-(void)testTruncateLongStrings { + NSString *longString = [@"" stringByPaddingToLength:kAMPMaxStringLength*2 withString: @"c" startingAtIndex:0]; + XCTAssertEqual([longString length], kAMPMaxStringLength*2); + NSString *truncatedString = [self.amplitude truncate:longString]; + XCTAssertEqual([truncatedString length], kAMPMaxStringLength); + XCTAssertEqualObjects(truncatedString, [@"" stringByPaddingToLength:kAMPMaxStringLength withString: @"c" startingAtIndex:0]); + + NSString *shortString = [@"" stringByPaddingToLength:kAMPMaxStringLength-1 withString: @"c" startingAtIndex:0]; + XCTAssertEqual([shortString length], kAMPMaxStringLength-1); + truncatedString = [self.amplitude truncate:shortString]; + XCTAssertEqual([truncatedString length], kAMPMaxStringLength-1); + XCTAssertEqualObjects(truncatedString, shortString); +} + +-(void)testTruncateNullObjects { + XCTAssertNil([self.amplitude truncate:nil]); +} + +-(void)testTruncateDictionary { + NSString *longString = [@"" stringByPaddingToLength:kAMPMaxStringLength*2 withString: @"c" startingAtIndex:0]; + NSString *truncString = [@"" stringByPaddingToLength:kAMPMaxStringLength withString: @"c" startingAtIndex:0]; + NSMutableDictionary *object = [NSMutableDictionary dictionary]; + [object setValue:[NSNumber numberWithInt:10] forKey:@"int value"]; + [object setValue:[NSNumber numberWithBool:NO] forKey:@"bool value"]; + [object setValue:longString forKey:@"long string"]; + [object setValue:[NSArray arrayWithObject:longString] forKey:@"array"]; + + object = [self.amplitude truncate:object]; + XCTAssertEqual([[object objectForKey:@"int value"] intValue], 10); + XCTAssertFalse([[object objectForKey:@"bool value"] boolValue]); + XCTAssertEqual([[object objectForKey:@"long string"] length], kAMPMaxStringLength); + XCTAssertEqual([[object objectForKey:@"array"] count], 1); + XCTAssertEqualObjects([object objectForKey:@"array"][0], truncString); + XCTAssertEqual([[object objectForKey:@"array"][0] length], kAMPMaxStringLength); +} + +-(void)testTruncateEventAndIdentify { + NSString *longString = [@"" stringByPaddingToLength:kAMPMaxStringLength*2 withString: @"c" startingAtIndex:0]; + NSString *truncString = [@"" stringByPaddingToLength:kAMPMaxStringLength withString: @"c" startingAtIndex:0]; + + [self.amplitude logEvent:@"test" withEventProperties:[NSDictionary dictionaryWithObject:longString forKey:@"long_string"]]; + [self.amplitude identify:[[AMPIdentify identify] set:@"long_string" value:longString]]; + [self.amplitude flushQueue]; + + NSDictionary *event = [self.amplitude getLastEvent]; + XCTAssertEqualObjects([event objectForKey:@"event_type"], @"test"); + XCTAssertEqualObjects([event objectForKey:@"event_properties"], [NSDictionary dictionaryWithObject:truncString forKey:@"long_string"]); + + NSDictionary *identify = [self.amplitude getLastIdentify]; + XCTAssertEqualObjects([identify objectForKey:@"event_type"], @"$identify"); + XCTAssertEqualObjects([identify objectForKey:@"user_properties"], [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:truncString forKey:@"long_string"] forKey:@"$set"]); +} + +-(void)testAutoIncrementSequenceNumber { + AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; + int limit = 10; + for (int i = 0; i < limit; i++) { + XCTAssertEqual([self.amplitude getNextSequenceNumber], i+1); + XCTAssertEqual([[dbHelper getLongValue:@"sequence_number"] intValue], i+1); + } +} @end \ No newline at end of file diff --git a/AmplitudeTests/IdentifyTests.m b/AmplitudeTests/IdentifyTests.m new file mode 100644 index 00000000..6c812993 --- /dev/null +++ b/AmplitudeTests/IdentifyTests.m @@ -0,0 +1,201 @@ +// +// IdentifyTests.m +// Amplitude +// +// Created by Daniel Jih on 10/5/15. +// Copyright © 2015 Amplitude. All rights reserved. +// + +#import +#import "AMPIdentify.h" +#import "AMPARCMacros.h" +#import "AMPConstants.h" + +@interface IdentifyTests : XCTestCase + +@end + +@implementation IdentifyTests { } + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testAddProperty { + NSString *property1 = @"int value"; + NSNumber *value1 = [NSNumber numberWithInt:5]; + + NSString *property2 = @"double value"; + NSNumber *value2 = [NSNumber numberWithDouble:0.123]; + + NSString *property3 = @"float value"; + NSNumber *value3 = [NSNumber numberWithFloat:0.625]; + + NSString *property4 = @"long value"; + NSNumber *value4 = [NSNumber numberWithLong:18]; + + NSString *property5 = @"NSDecimal number value"; + NSDecimalNumber *value5 = [NSDecimalNumber decimalNumberWithString:@"1.234"]; + + NSString *property6 = @"string value"; + NSString *value6 = @"10"; + + // add should ignore nonnumbers and nonstrings + NSString *property7 = @"array value"; + NSArray *value7 = [NSArray array]; + + AMPIdentify *identify = [[AMPIdentify identify] add:property1 value:value1]; + [[[identify add:property2 value:value2] add:property3 value:value3] add:property4 value:value4]; + [[[identify add:property5 value:value5] add:property6 value:value6] add:property7 value:value7]; + + // identify should ignore this since duplicate key + [identify add:property1 value:value3]; + + // generate expected operations + NSMutableDictionary *operations = [NSMutableDictionary dictionary]; + [operations setObject:value1 forKey:property1]; + [operations setObject:value2 forKey:property2]; + [operations setObject:value3 forKey:property3]; + [operations setObject:value4 forKey:property4]; + [operations setObject:value5 forKey:property5]; + [operations setObject:value6 forKey:property6]; + + NSMutableDictionary *expected = [NSMutableDictionary dictionary]; + [expected setObject:operations forKey:AMP_OP_ADD]; + + XCTAssertEqualObjects(identify.userPropertyOperations, expected); +} + +- (void)testSetProperty { + NSString *property1 = @"string value"; + NSString *value1 = @"test value"; + + NSString *property2 = @"double value"; + NSNumber *value2 = [NSNumber numberWithDouble:0.123]; + + NSString *property3 = @"boolean value"; + NSNumber *value3 = [NSNumber numberWithBool:YES]; + + NSString *property4 = @"array value"; + NSArray *value4 = [NSArray array]; + + AMPIdentify *identify = [[AMPIdentify identify] set:property1 value:value1]; + [[[identify set:property2 value:value2] set:property3 value:value3] set:property4 value:value4]; + + // identify should ignore this since duplicate key + [identify set:property1 value:value3]; + + // generate expected operations + NSMutableDictionary *operations = [NSMutableDictionary dictionary]; + [operations setObject:value1 forKey:property1]; + [operations setObject:value2 forKey:property2]; + [operations setObject:value3 forKey:property3]; + [operations setObject:value4 forKey:property4]; + + NSMutableDictionary *expected = [NSMutableDictionary dictionary]; + [expected setObject:operations forKey:AMP_OP_SET]; + + XCTAssertEqualObjects(identify.userPropertyOperations, expected); +} + +- (void)testSetOnceProperty { + NSString *property1 = @"string value"; + NSString *value1 = @"test value"; + + NSString *property2 = @"double value"; + NSNumber *value2 = [NSNumber numberWithDouble:0.123]; + + NSString *property3 = @"boolean value"; + NSNumber *value3 = [NSNumber numberWithBool:YES]; + + NSString *property4 = @"array value"; + NSArray *value4 = [NSArray array]; + + AMPIdentify *identify = [[AMPIdentify identify] setOnce:property1 value:value1]; + [[[identify setOnce:property2 value:value2] setOnce:property3 value:value3] setOnce:property4 value:value4]; + + // identify should ignore this since duplicate key + [identify setOnce:property1 value:value3]; + + // generate expected operations + NSMutableDictionary *operations = [NSMutableDictionary dictionary]; + [operations setObject:value1 forKey:property1]; + [operations setObject:value2 forKey:property2]; + [operations setObject:value3 forKey:property3]; + [operations setObject:value4 forKey:property4]; + + NSMutableDictionary *expected = [NSMutableDictionary dictionary]; + [expected setObject:operations forKey:AMP_OP_SET_ONCE]; + + XCTAssertEqualObjects(identify.userPropertyOperations, expected); +} + +- (void)testUnsetProperty { + NSString *property1 = @"testProperty1"; + NSString *property2 = @"testProperty2"; + + AMPIdentify *identify = [AMPIdentify identify]; + [[identify unset:property1] unset:property2]; + + NSMutableDictionary *operations = [NSMutableDictionary dictionary]; + [operations setObject:@"-" forKey:property1]; + [operations setObject:@"-" forKey:property2]; + + NSMutableDictionary *expected = [NSMutableDictionary dictionary]; + [expected setObject:operations forKey:AMP_OP_UNSET]; + + XCTAssertEqualObjects(identify.userPropertyOperations, expected); +} + +- (void)testMultipleOperations { + NSString *property1 = @"string value"; + NSString *value1 = @"test value"; + + NSString *property2 = @"double value"; + NSNumber *value2 = [NSNumber numberWithDouble:0.123]; + + NSString *property3 = @"boolean value"; + NSNumber *value3 = [NSNumber numberWithBool:YES]; + + NSString *property4 = @"array value"; + + AMPIdentify *identify = [[AMPIdentify identify] setOnce:property1 value:value1]; + [[[identify add:property2 value:value2] set:property3 value:value3] unset:property4]; + + // identify should ignore this since duplicate key + [identify set:property4 value:value3]; + + // generate expected operations + NSDictionary *setOnce = [NSDictionary dictionaryWithObject:value1 forKey:property1]; + NSDictionary *add = [NSDictionary dictionaryWithObject:value2 forKey:property2]; + NSDictionary *set = [NSDictionary dictionaryWithObject:value3 forKey:property3]; + NSDictionary *unset = [NSDictionary dictionaryWithObject:@"-" forKey:property4]; + + NSDictionary *expected = [NSDictionary dictionaryWithObjectsAndKeys:setOnce, AMP_OP_SET_ONCE, add, AMP_OP_ADD, set, AMP_OP_SET, unset, AMP_OP_UNSET, nil]; + + XCTAssertEqualObjects(identify.userPropertyOperations, expected); +} + +- (void)testDisallowDuplicateProperties { + NSString *property = @"testProperty"; + NSString *value1 = @"testValue"; + NSNumber *value2 = [NSNumber numberWithDouble:0.123]; + NSNumber *value3 = [NSNumber numberWithBool:YES]; + + AMPIdentify *identify = [AMPIdentify identify]; + [[[[identify setOnce:property value:value1] add:property value:value2] set:property value:value3] unset:property]; + + NSMutableDictionary *operations = [NSMutableDictionary dictionary]; + [operations setObject:value1 forKey:property]; + + NSMutableDictionary *expected = [NSMutableDictionary dictionary]; + [expected setObject:operations forKey:AMP_OP_SET_ONCE]; + + XCTAssertEqualObjects(identify.userPropertyOperations, expected); +} + +@end diff --git a/AmplitudeTests/SetupTests.m b/AmplitudeTests/SetupTests.m index 80020f50..b843975a 100644 --- a/AmplitudeTests/SetupTests.m +++ b/AmplitudeTests/SetupTests.m @@ -12,6 +12,7 @@ #import "Amplitude.h" #import "Amplitude+Test.h" #import "BaseTestCase.h" +#import "AMPConstants.h" #import "AMPUtils.h" @interface SetupTests : BaseTestCase @@ -78,6 +79,8 @@ - (void)testOptOut { - (void)testUserPropertiesSet { [self.amplitude initializeApiKey:apiKey]; + AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper]; + XCTAssertEqual([dbHelper getEventCount], 0); NSDictionary *properties = @{ @"shoeSize": @10, @@ -85,42 +88,19 @@ - (void)testUserPropertiesSet { @"name": @"John" }; - [self.amplitude setUserProperties:@{@"property": @"true"} replace:YES]; - [self.amplitude setUserProperties:properties replace:YES]; - - [self.amplitude logEvent:@"Test Event"]; - [self.amplitude flushQueue]; - - NSDictionary *event = [self.amplitude getLastEvent]; - XCTAssert([event[@"user_properties"] isEqualToDictionary:properties]); -} - -- (void)testUserPropertiesMerge { - [self.amplitude initializeApiKey:apiKey]; - - NSMutableDictionary *properties = [@{ - @"shoeSize": @10, - @"hatSize": @5.125, - @"name": @"John" - } mutableCopy]; - [self.amplitude setUserProperties:properties]; - - [self.amplitude logEvent:@"Test Event"]; [self.amplitude flushQueue]; + XCTAssertEqual([dbHelper getEventCount], 0); + XCTAssertEqual([dbHelper getIdentifyCount], 1); + XCTAssertEqual([dbHelper getTotalEventCount], 1); - NSDictionary *event = [self.amplitude getLastEvent]; - XCTAssert([event[@"user_properties"] isEqualToDictionary:properties]); - - NSDictionary *extraProperties = @{@"mergedProperty": @"merged"}; - [self.amplitude setUserProperties:extraProperties replace:NO]; - - [self.amplitude logEvent:@"Test Event"]; - [self.amplitude flushQueue]; + NSDictionary *expected = [NSDictionary dictionaryWithObject:properties forKey:AMP_OP_SET]; - event = [self.amplitude getLastEvent]; - [properties addEntriesFromDictionary:extraProperties]; - XCTAssert([event[@"user_properties"] isEqualToDictionary:properties]); + NSDictionary *event = [self.amplitude getLastIdentify]; + XCTAssertEqualObjects([event objectForKey:@"event_type"], IDENTIFY_EVENT); + XCTAssertEqualObjects([event objectForKey:@"user_properties"], expected); + XCTAssertEqualObjects([event objectForKey:@"event_properties"], [NSDictionary dictionary]); // event properties should be empty + XCTAssertEqual(1, [[event objectForKey:@"sequence_number"] intValue]); } - (void)testSetDeviceId { diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d220d75..2db78379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased * Add ability to set custom deviceId. +* Add support for user property operations (set, setOnce, add, unset). ## 3.1.1 (October 8, 2015) diff --git a/README.md b/README.md index 3aa16b75..6ea7c06c 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,49 @@ NSMutableDictionary *userProperties = [NSMutableDictionary dictionary]; [[Amplitude instance] setUserProperties:userProperties replace:YES]; ``` +# User Property Operations # + +The SDK supports the operations set, setOnce, unset, and add on individual user properties. The operations are declared via a provided `AMPIdentify` interface. Multiple operations can be chained together in a single `AMPIdentify` object. The `AMPIdentify` object is then passed to the Amplitude client to send to the server. The results of the operations will be visible immediately in the dashboard, and take effect for events logged after. Note, each +operation on the `AMPIdentify` object returns the same instance, allowing you to chain multiple operations together. + +1. `set`: this sets the value of a user property. + + ``` objective-c + AMPIdentify *identify = [[[AMPIdentify identify] set:@"gender" value:@"female"] set:@"age" value:[NSNumber numberForInt:20]]; + [[Amplitude instance] identify:identify]; + ``` + +2. `setOnce`: this sets the value of a user property only once. Subsequent `setOnce` operations on that user property will be ignored. In the following example, `sign_up_date` will be set once to `08/24/2015`, and the following setOnce to `09/14/2015` will be ignored: + + ``` objective-c + AMPIdentify *identify1 = [[AMPIdentify identify] setOnce:@"sign_up_date" value:@"09/06/2015"]; + [[Amplitude instance] identify:identify1]; + + AMPIdentify *identify2 = [[AMPIdentify identify] setOnce:@"sign_up_date" value:@"10/06/2015"]; + [[Amplitude instance] identify:identify2]; + ``` + +3. `unset`: this will unset and remove a user property. + + ``` objective-c + AMPIdentify *identify = [[[AMPIdentify identify] unset:@"gender"] unset:@"age"]; + [[Amplitude instance] identify:identify]; + ``` + +4. `add`: this will increment a user property by some numerical value. If the user property does not have a value set yet, it will be initialized to 0 before being incremented. + + ``` objective-c + AMPIdentify *identify = [[[AMPIdentify identify] add:@"karma" value:[NSNumber numberWithFloat:0.123]] add:@"friends" value:[NSNumber numberWithInt:1]]; + [[Amplitude instance] identify:identify]; + ``` + +Note: if a user property is used in multiple operations on the same `Identify` object, only the first operation will be saved, and the rest will be ignored. In this example, only the set operation will be saved, and the add and unset will be ignored: + +```objective-c +AMPIdentify *identify = [[[[AMPIdentify identify] set:@"karma" value:[NSNumber numberWithInt:10]] add:@"friends" value:[NSNumber numberWithInt:1]] unset:@"karma"]; + [[Amplitude instance] identify:identify]; +``` + # Allowing Users to Opt Out To stop all event and session logging for a user, call setOptOut: