diff --git a/Analytics.podspec b/Analytics.podspec index 1ad140ef2..ddb107a8e 100644 --- a/Analytics.podspec +++ b/Analytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Analytics" - s.version = "3.7.0-beta.4" + s.version = "3.8.0-beta.1" s.summary = "The hassle-free way to add analytics to your iOS app." s.description = <<-DESC @@ -17,7 +17,8 @@ Pod::Spec.new do |s| s.ios.deployment_target = '7.0' s.tvos.deployment_target = '9.0' - s.frameworks = 'CoreTelephony', 'Security', 'StoreKit', 'SystemConfiguration', 'UIKit' + s.ios.frameworks = 'CoreTelephony' + s.frameworks = 'Security', 'StoreKit', 'SystemConfiguration', 'UIKit' s.source_files = [ 'Analytics/Classes/**/*', diff --git a/Analytics.xcodeproj/project.pbxproj b/Analytics.xcodeproj/project.pbxproj index c40b6a9a3..ea97bbbe9 100644 --- a/Analytics.xcodeproj/project.pbxproj +++ b/Analytics.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 3481C14AEC76E5A8DA64DA59 /* Pods_AnalyticsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89033CBF22319674E6CE7E61 /* Pods_AnalyticsTests.framework */; }; 6E265C791FB1178C0030E08E /* IntegrationsManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E265C781FB1178C0030E08E /* IntegrationsManagerTest.swift */; }; 6EEC1C712017EA370089C478 /* EndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EEC1C702017EA370089C478 /* EndToEndTests.swift */; }; + A31958EF2385AC3A00A47EFA /* SerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A31958EE2385AC3A00A47EFA /* SerializationTests.m */; }; + A352176023AD5825005B07F6 /* SEGMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = A352175F23AD5825005B07F6 /* SEGMacros.h */; settings = {ATTRIBUTES = (Private, ); }; }; EA88A5981DED7608009FB66A /* SEGSerializableValue.h in Headers */ = {isa = PBXBuildFile; fileRef = EA88A5971DED7608009FB66A /* SEGSerializableValue.h */; settings = {ATTRIBUTES = (Public, ); }; }; EA8F09741E24C5C600B8B93F /* MiddlewareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA8F09731E24C5C600B8B93F /* MiddlewareTests.swift */; }; EAA542771EB4035400945DA7 /* TrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA542761EB4035400945DA7 /* TrackingTests.swift */; }; @@ -92,6 +94,8 @@ 6E265C781FB1178C0030E08E /* IntegrationsManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsManagerTest.swift; sourceTree = ""; }; 6EEC1C702017EA370089C478 /* EndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndToEndTests.swift; sourceTree = ""; }; 89033CBF22319674E6CE7E61 /* Pods_AnalyticsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AnalyticsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A31958EE2385AC3A00A47EFA /* SerializationTests.m */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = SerializationTests.m; sourceTree = ""; tabWidth = 4; }; + A352175F23AD5825005B07F6 /* SEGMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SEGMacros.h; sourceTree = ""; }; D3BF8AE673FE0FD91DF5B503 /* Pods-AnalyticsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AnalyticsTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests.release.xcconfig"; sourceTree = ""; }; EA88A5971DED7608009FB66A /* SEGSerializableValue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SEGSerializableValue.h; sourceTree = ""; }; EA8F09731E24C5C600B8B93F /* MiddlewareTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiddlewareTests.swift; sourceTree = ""; }; @@ -266,6 +270,7 @@ EADEB8EC1DECD335005322DA /* AnalyticsTests-Bridging-Header.h */, 6E265C781FB1178C0030E08E /* IntegrationsManagerTest.swift */, 6EEC1C702017EA370089C478 /* EndToEndTests.swift */, + A31958EE2385AC3A00A47EFA /* SerializationTests.m */, ); indentWidth = 2; path = AnalyticsTests; @@ -344,6 +349,7 @@ EADEB89F1DECD12B005322DA /* SEGUserDefaultsStorage.m */, EADEB8A01DECD12B005322DA /* SEGUtils.h */, EADEB8A11DECD12B005322DA /* SEGUtils.m */, + A352175F23AD5825005B07F6 /* SEGMacros.h */, EADEB8A21DECD12B005322DA /* UIViewController+SEGScreen.h */, EADEB8A31DECD12B005322DA /* UIViewController+SEGScreen.m */, ); @@ -407,6 +413,7 @@ EADEB8DE1DECD12B005322DA /* SEGAnalyticsConfiguration.h in Headers */, EADEB8D61DECD12B005322DA /* UIViewController+SEGScreen.h in Headers */, EADEB8CF1DECD12B005322DA /* SEGStorage.h in Headers */, + A352176023AD5825005B07F6 /* SEGMacros.h in Headers */, EADEB8D21DECD12B005322DA /* SEGUserDefaultsStorage.h in Headers */, EADEB8D01DECD12B005322DA /* SEGStoreKitTracker.h in Headers */, EADEB8D41DECD12B005322DA /* SEGUtils.h in Headers */, @@ -486,6 +493,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = EADEB8511DECD080005322DA; @@ -541,7 +549,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", "${BUILT_PRODUCTS_DIR}/Alamofire-Synchronous/Alamofire_Synchronous.framework", "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", @@ -560,7 +568,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AnalyticsTests/Pods-AnalyticsTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -604,6 +612,7 @@ EADEB8F21DECD335005322DA /* UserDefaultsStorageTest.swift in Sources */, EAA542771EB4035400945DA7 /* TrackingTests.swift in Sources */, EADEB8F41DECD335005322DA /* ContextTest.swift in Sources */, + A31958EF2385AC3A00A47EFA /* SerializationTests.m in Sources */, EADEB8EE1DECD335005322DA /* HTTPClientTest.swift in Sources */, EADEB8F31DECD335005322DA /* AnalyticsTests.swift in Sources */, EADEB8F11DECD335005322DA /* FileStorageTest.swift in Sources */, diff --git a/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme b/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme index 377a38856..cce21eeec 100644 --- a/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme +++ b/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme @@ -26,9 +26,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES" codeCoverageEnabled = "YES"> + + + + @@ -41,23 +49,11 @@ - - - - - - - - - - - - - - - - + + + + + + - - +-(id _Nullable) serializableDeepCopy; +@end + +@interface NSDictionary(SerializableDeepCopy) +@end + +@interface NSArray(SerializableDeepCopy) +@end + + NS_ASSUME_NONNULL_END diff --git a/Analytics/Classes/Internal/SEGAnalyticsUtils.m b/Analytics/Classes/Internal/SEGAnalyticsUtils.m index 4ccefe55b..5f6edaa8c 100644 --- a/Analytics/Classes/Internal/SEGAnalyticsUtils.m +++ b/Analytics/Classes/Internal/SEGAnalyticsUtils.m @@ -107,25 +107,23 @@ void SEGLog(NSString *format, ...) static id SEGCoerceJSONObject(id obj) { - // Hotfix: Storage format should support NSNull instead - if ([obj isKindOfClass:[NSNull class]]) { - return @""; - } // if the object is a NSString, NSNumber // then we're good if ([obj isKindOfClass:[NSString class]] || - [obj isKindOfClass:[NSNumber class]]) { + [obj isKindOfClass:[NSNumber class]] || + [obj isKindOfClass:[NSNull class]]) { return obj; } if ([obj isKindOfClass:[NSArray class]]) { NSMutableArray *array = [NSMutableArray array]; for (id i in obj) { + NSObject *value = i; // Hotfix: Storage format should support NSNull instead - if ([i isKindOfClass:[NSNull class]]) { - continue; + if ([value isKindOfClass:[NSNull class]]) { + value = [NSData data]; } - [array addObject:SEGCoerceJSONObject(i)]; + [array addObject:SEGCoerceJSONObject(value)]; } return array; } @@ -133,16 +131,12 @@ static id SEGCoerceJSONObject(id obj) if ([obj isKindOfClass:[NSDictionary class]]) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; for (NSString *key in obj) { - // Hotfix for issue where SEGFileStorage uses plist which does NOT support NSNull - // So when `[NSNull null]` gets passed in as track property values the queue serialization fails - if ([obj[key] isKindOfClass:[NSNull class]]) { - continue; - } + NSObject *value = obj[key]; if (![key isKindOfClass:[NSString class]]) SEGLog(@"warning: dictionary keys should be strings. got: %@. coercing " @"to: %@", [key class], [key description]); - dict[key.description] = SEGCoerceJSONObject(obj[key]); + dict[key.description] = SEGCoerceJSONObject(value); } return dict; } @@ -160,31 +154,12 @@ static id SEGCoerceJSONObject(id obj) return [obj description]; } -static void AssertDictionaryTypes(id dict) -{ -#ifdef DEBUG - assert([dict isKindOfClass:[NSDictionary class]]); - for (id key in dict) { - assert([key isKindOfClass:[NSString class]]); - id value = dict[key]; - - assert([value isKindOfClass:[NSString class]] || - [value isKindOfClass:[NSNumber class]] || - [value isKindOfClass:[NSNull class]] || - [value isKindOfClass:[NSArray class]] || - [value isKindOfClass:[NSDictionary class]] || - [value isKindOfClass:[NSDate class]] || - [value isKindOfClass:[NSURL class]]); - } -#endif -} - NSDictionary *SEGCoerceDictionary(NSDictionary *dict) { // make sure that a new dictionary exists even if the input is null dict = dict ?: @{}; // assert that the proper types are in the dictionary - AssertDictionaryTypes(dict); + dict = [dict serializableDeepCopy]; // coerce urls, and dates to the proper format return SEGCoerceJSONObject(dict); } @@ -214,3 +189,75 @@ static void AssertDictionaryTypes(id dict) { return [[NSString alloc] initWithFormat:@"Viewed %@ Screen", title]; } + + +@implementation NSDictionary(SerializableDeepCopy) + +- (NSDictionary *)serializableDeepCopy +{ + NSMutableDictionary *returnDict = [[NSMutableDictionary alloc] initWithCapacity:self.count]; + NSArray *keys = [self allKeys]; + for (id key in keys) { + id aValue = [self objectForKey:key]; + id theCopy = nil; + + if (![aValue conformsToProtocol:@protocol(NSCoding)]) { +#ifdef DEBUG + NSAssert(FALSE, @"key `%@` doesn't conform to NSCoding and can't be serialized for delivery.", key); +#else + SEGLog(@"key `%@` doesn't conform to NSCoding and can't be serialized for delivery.", key); + // simply leave it out since we can't encode it anyway. + continue; +#endif + } + + if ([aValue conformsToProtocol:@protocol(SEGSerializableDeepCopy)]) { + theCopy = [aValue serializableDeepCopy]; + } else if ([aValue conformsToProtocol:@protocol(NSCopying)]) { + theCopy = [aValue copy]; + } else { + theCopy = aValue; + } + + [returnDict setValue:theCopy forKey:key]; + } + + return [returnDict copy]; +} + +@end + + +@implementation NSArray(SerializableDeepCopy) + +-(NSArray *)serializableDeepCopy +{ + NSMutableArray *returnArray = [[NSMutableArray alloc] initWithCapacity:self.count]; + + for (id aValue in self) { + id theCopy = nil; + + if (![aValue conformsToProtocol:@protocol(NSCoding)]) { +#ifdef DEBUG + NSAssert(FALSE, @"type `%@` doesn't conform to NSCoding and can't be serialized for delivery.", NSStringFromClass([aValue class])); +#else + SEGLog(@"type `%@` doesn't conform to NSCoding and can't be serialized for delivery.", NSStringFromClass([aValue class])); + // simply leave it out since we can't encode it anyway. + continue; +#endif + } + + if ([aValue conformsToProtocol:@protocol(SEGSerializableDeepCopy)]) { + theCopy = [aValue serializableDeepCopy]; + } else if ([aValue conformsToProtocol:@protocol(NSCopying)]) { + theCopy = [aValue copy]; + } else { + theCopy = aValue; + } + [returnArray addObject:theCopy]; + } + + return [returnArray copy]; +} + +@end diff --git a/Analytics/Classes/Internal/SEGFileStorage.m b/Analytics/Classes/Internal/SEGFileStorage.m index dd0fd21dd..16870133f 100644 --- a/Analytics/Classes/Internal/SEGFileStorage.m +++ b/Analytics/Classes/Internal/SEGFileStorage.m @@ -87,32 +87,38 @@ - (NSData *)dataForKey:(NSString *)key - (NSDictionary *)dictionaryForKey:(NSString *)key { - return [self plistForKey:key]; + return [self jsonForKey:key]; } - (void)setDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { - [self setPlist:dictionary forKey:key]; + [self setJSON:dictionary forKey:key]; } - (NSArray *)arrayForKey:(NSString *)key { - return [self plistForKey:key]; + return [self jsonForKey:key]; } - (void)setArray:(NSArray *)array forKey:(NSString *)key { - [self setPlist:array forKey:key]; + [self setJSON:array forKey:key]; } - (NSString *)stringForKey:(NSString *)key { - return [self plistForKey:key]; + // unlike plists, we can't have stray values, they need to be + // in a dictionary or array container. + NSDictionary *data = [self jsonForKey:key]; + return data[key]; } - (void)setString:(NSString *)string forKey:(NSString *)key { - [self setPlist:string forKey:key]; + // unlike plists, we can't have stray values, they need to be + // in a dictionary or array container. + NSDictionary *data = @{key: string}; + [self setJSON:data forKey:key]; } + (NSURL *)applicationSupportDirectoryURL @@ -129,44 +135,63 @@ - (NSURL *)urlForKey:(NSString *)key #pragma mark - Helpers -- (id _Nullable)plistForKey:(NSString *)key +- (id _Nullable)jsonForKey:(NSString *)key { + id result = nil; + NSData *data = [self dataForKey:key]; - return data ? [self plistFromData:data] : nil; + if (data) { + BOOL needsConversion = NO; + result = [self jsonFromData:data needsConversion:&needsConversion]; + if (needsConversion) { + [self setJSON:result forKey:key]; + } + } + return result; } -- (void)setPlist:(id _Nonnull)plist forKey:(NSString *)key +- (void)setJSON:(id _Nonnull)plist forKey:(NSString *)key { - NSData *data = [self dataFromPlist:plist]; + NSDictionary *dict = nil; + + // json doesn't allow stand alone values like plist (previous storage format) does so + // we need to massage it a little. + if ([plist isKindOfClass:[NSDictionary class]] || [plist isKindOfClass:[NSArray class]]) { + dict = plist; + } else { + dict = @{key: plist}; + } + + NSData *data = [self dataFromJSON:dict]; if (data) { [self setData:data forKey:key]; } } -- (NSData *_Nullable)dataFromPlist:(nonnull id)plist +- (NSData *_Nullable)dataFromJSON:(id)json { NSError *error = nil; - NSData *data = [NSPropertyListSerialization dataWithPropertyList:plist - format:NSPropertyListXMLFormat_v1_0 - options:0 - error:&error]; + NSData *data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; if (error) { - SEGLog(@"Unable to serialize data from plist object", error, plist); + SEGLog(@"Unable to serialize data from json object; %@, %@", error, json); } return data; } -- (id _Nullable)plistFromData:(NSData *_Nonnull)data +- (id _Nullable)jsonFromData:(NSData *_Nonnull)data needsConversion:(BOOL *)needsConversion { NSError *error = nil; - id plist = [NSPropertyListSerialization propertyListWithData:data - options:0 - format:nil - error:&error]; + id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error) { - SEGLog(@"Unable to parse plist from data %@", error); + // maybe it's a plist and needs to be converted. + result = [self plistFromData:data]; + if (result != nil) { + *needsConversion = YES; + } else { + SEGLog(@"Unable to parse json from data %@", error); + } } - return plist; + return result; } - (void)createDirectoryAtURLIfNeeded:(NSURL *)url @@ -183,4 +208,39 @@ - (void)createDirectoryAtURLIfNeeded:(NSURL *)url } } +/// Deprecated +- (NSData *_Nullable)dataFromPlist:(nonnull id)plist +{ + NSError *error = nil; + NSData *data = nil; + // Temporary just-in-case fix for issue #846; Follow-on PR to move away from plist storage. + @try { + data = [NSPropertyListSerialization dataWithPropertyList:plist + format:NSPropertyListXMLFormat_v1_0 + options:0 + error:&error]; + } @catch (NSException *e) { + SEGLog(@"Unable to serialize data from plist object; Exception: %@, plist: %@", e, plist); + } @finally { + if (error) { + SEGLog(@"Unable to serialize data from plist object; Error: %@, plist: %@", error, plist); + } + } + return data; +} + +/// Deprecated +- (id _Nullable)plistFromData:(NSData *_Nonnull)data +{ + NSError *error = nil; + id plist = [NSPropertyListSerialization propertyListWithData:data + options:0 + format:nil + error:&error]; + if (error) { + SEGLog(@"Unable to parse plist from data %@", error); + } + return plist; +} + @end diff --git a/Analytics/Classes/Internal/SEGMacros.h b/Analytics/Classes/Internal/SEGMacros.h new file mode 100644 index 000000000..bfc470046 --- /dev/null +++ b/Analytics/Classes/Internal/SEGMacros.h @@ -0,0 +1,22 @@ +// +// SEGMacros.h +// Analytics +// +// Created by Brandon Sneed on 12/20/19. +// Copyright © 2019 Segment. All rights reserved. +// + +#ifndef SEGMacros_h +#define SEGMacros_h + +#define __deprecated__(s) __attribute__((deprecated(s))) + +#define weakify(var) __weak typeof(var) __weak_##var = var; + +#define strongify(var) \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +__strong typeof(var) var = __weak_##var; \ +_Pragma("clang diagnostic pop") + +#endif /* SEGMacros_h */ diff --git a/Analytics/Classes/Internal/SEGSegmentIntegration.m b/Analytics/Classes/Internal/SEGSegmentIntegration.m index 1c7b68730..3d16084d3 100644 --- a/Analytics/Classes/Internal/SEGSegmentIntegration.m +++ b/Analytics/Classes/Internal/SEGSegmentIntegration.m @@ -7,6 +7,7 @@ #import "SEGReachability.h" #import "SEGHTTPClient.h" #import "SEGStorage.h" +#import "SEGMacros.h" #if TARGET_OS_IOS #import @@ -53,7 +54,7 @@ static BOOL GetAdTrackingEnabled() @interface SEGSegmentIntegration () @property (nonatomic, strong) NSMutableArray *queue; -@property (nonatomic, strong) NSDictionary *cachedStaticContext; +@property (nonatomic, strong) NSDictionary *_cachedStaticContext; @property (nonatomic, strong) NSURLSessionUploadTask *batchRequest; @property (nonatomic, assign) UIBackgroundTaskIdentifier flushTaskID; @property (nonatomic, strong) SEGReachability *reachability; @@ -107,26 +108,24 @@ - (id)initWithAnalytics:(SEGAnalytics *)analytics httpClient:(SEGHTTPClient *)ht [self trackAttributionData:self.configuration.trackAttributionData]; }]; - if ([NSThread isMainThread]) { - [self setupFlushTimer]; - } else { - dispatch_sync(dispatch_get_main_queue(), ^{ - [self setupFlushTimer]; - }); - } + self.flushTimer = [NSTimer timerWithTimeInterval:self.configuration.flushInterval + target:self + selector:@selector(flush) + userInfo:nil + repeats:YES]; + + [NSRunLoop.mainRunLoop addTimer:self.flushTimer + forMode:NSDefaultRunLoopMode]; + + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateStaticContext) + name:UIApplicationWillEnterForegroundNotification + object:nil]; } return self; } -- (void)setupFlushTimer -{ - self.flushTimer = [NSTimer scheduledTimerWithTimeInterval:self.configuration.flushInterval - target:self - selector:@selector(flush) - userInfo:nil - repeats:YES]; -} - /* * There is an iOS bug that causes instances of the CTTelephonyNetworkInfo class to * sometimes get notifications after they have been deallocated. @@ -168,6 +167,7 @@ - (NSDictionary *)staticContext dict[@"type"] = @"ios"; dict[@"model"] = GetDeviceModel(); dict[@"id"] = [[device identifierForVendor] UUIDString]; + dict[@"name"] = [device model]; if (NSClassFromString(SEGAdvertisingClassIdentifier)) { dict[@"adTrackingEnabled"] = @(GetAdTrackingEnabled()); } @@ -212,6 +212,29 @@ - (NSDictionary *)staticContext return dict; } +- (void)updateStaticContext +{ + self.cachedStaticContext = [self staticContext]; +} + +- (NSDictionary *)cachedStaticContext { + __block NSDictionary *result = nil; + weakify(self); + dispatch_sync(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + strongify(self); + result = self._cachedStaticContext; + }); + return result; +} + +- (void)setCachedStaticContext:(NSDictionary *)cachedStaticContext { + weakify(self); + dispatch_sync(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + strongify(self); + self._cachedStaticContext = cachedStaticContext; + }); +} + - (NSDictionary *)liveContext { NSMutableDictionary *context = [[NSMutableDictionary alloc] init]; diff --git a/Analytics/Classes/Internal/SEGUtils.m b/Analytics/Classes/Internal/SEGUtils.m index afbc01418..b6019a109 100644 --- a/Analytics/Classes/Internal/SEGUtils.m +++ b/Analytics/Classes/Internal/SEGUtils.m @@ -37,10 +37,6 @@ + (id _Nullable)plistFromData:(NSData *_Nonnull)data +(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary*)patterns { - if (object == nil || object == NSNull.null || [object isKindOfClass:NSNull.class]) { - return object; - } - if ([object isKindOfClass:NSDictionary.class]) { NSDictionary* dict = object; NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:dict.count]; diff --git a/Analytics/Classes/SEGAnalytics.m b/Analytics/Classes/SEGAnalytics.m index b62116330..67b638b5d 100644 --- a/Analytics/Classes/SEGAnalytics.m +++ b/Analytics/Classes/SEGAnalytics.m @@ -111,6 +111,8 @@ - (void)handleAppStateNotification:(NSNotification *)note [self _applicationDidFinishLaunchingWithOptions:note.userInfo]; } else if ([note.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { [self _applicationWillEnterForeground]; + } else if ([note.name isEqualToString: UIApplicationDidEnterBackgroundNotification]) { + [self _applicationDidEnterBackground]; } } @@ -167,15 +169,19 @@ - (void)_applicationWillEnterForeground if (!self.configuration.trackApplicationLifecycleEvents) { return; } - NSString *currentVersion = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; - NSString *currentBuild = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"]; [self track:@"Application Opened" properties:@{ @"from_background" : @YES, - @"version" : currentVersion ?: @"", - @"build" : currentBuild ?: @"", }]; } +- (void)_applicationDidEnterBackground +{ + if (!self.configuration.trackApplicationLifecycleEvents) { + return; + } + [self track: @"Application Backgrounded"]; +} + #pragma mark - Public API @@ -416,7 +422,9 @@ + (void)debug:(BOOL)showDebugLogs + (NSString *)version { - return @"3.7.0-beta.4"; + // this has to match the actual version, NOT what's in info.plist + // because Apple only accepts X.X.X as versions in the review process. + return @"3.8.0-beta.1"; } #pragma mark - Helpers diff --git a/Analytics/Info.plist b/Analytics/Info.plist index 111a5b05b..2e2340f5c 100644 --- a/Analytics/Info.plist +++ b/Analytics/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.7.0 + 3.8.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/AnalyticsTests/AnalyticsTests.swift b/AnalyticsTests/AnalyticsTests.swift index 17ec11c18..0166e642c 100644 --- a/AnalyticsTests/AnalyticsTests.swift +++ b/AnalyticsTests/AnalyticsTests.swift @@ -103,6 +103,13 @@ class AnalyticsTests: QuickSpec { expect(event?.event) == "Application Opened" expect(event?.properties?["from_background"] as? Bool) == true } + + it("fires Application Backgrounded during UIApplicationDidEnterBackground") { + testMiddleware.swallowEvent = true + NotificationCenter.default.post(name: .UIApplicationDidEnterBackground, object: testApplication) + let event = testMiddleware.lastContext?.payload as? SEGTrackPayload + expect(event?.event) == "Application Backgrounded" + } it("flushes when UIApplicationDidEnterBackgroundNotification is fired") { analytics.track("test") diff --git a/AnalyticsTests/IntegrationsManagerTest.swift b/AnalyticsTests/IntegrationsManagerTest.swift index 9f78b699a..8e1d41824 100644 --- a/AnalyticsTests/IntegrationsManagerTest.swift +++ b/AnalyticsTests/IntegrationsManagerTest.swift @@ -1,6 +1,7 @@ import Analytics import Quick import Nimble +import SwiftTryCatch class IntegrationsManagerTest: QuickSpec { @@ -8,6 +9,40 @@ class IntegrationsManagerTest: QuickSpec { describe("IntegrationsManager") { context("is track event enabled for integration in plan") { + it("asserts when invalid value types are used integration enablement flags") { + var exception: NSException? = nil + SwiftTryCatch.tryRun({ + SEGIntegrationsManager.isIntegration("comScore", enabledInOptions: ["comScore": "blah"]) + }, catchRun: { e in + exception = e + }, finallyRun: nil) + + expect(exception).toNot(beNil()) + } + + it("asserts when invalid value types are used integration enablement flags") { + var exception: NSException? = nil + SwiftTryCatch.tryRun({ + SEGIntegrationsManager.isIntegration("comScore", enabledInOptions: ["comScore": ["key": 1]]) + }, catchRun: { e in + exception = e + }, finallyRun: nil) + + expect(exception).toNot(beNil()) + } + + it("pulls valid integration data when supplied") { + let enabled = SEGIntegrationsManager.isIntegration("comScore", enabledInOptions: ["comScore": true]) + expect(enabled).to(beTrue()) + } + + it("falls back correctly when values aren't explicitly specified") { + let enabled = SEGIntegrationsManager.isIntegration("comScore", enabledInOptions: ["all": true]) + expect(enabled).to(beTrue()) + let allEnabled = SEGIntegrationsManager.isIntegration("comScore", enabledInOptions: ["All": true]) + expect(allEnabled).to(beTrue()) + } + it("returns true when there is no plan") { let enabled = SEGIntegrationsManager.isTrackEvent("hello world", enabledForIntegration: "Amplitude", inPlan:[:]) expect(enabled).to(beTrue()) diff --git a/AnalyticsTests/MiddlewareTests.swift b/AnalyticsTests/MiddlewareTests.swift index 3dd86eb4e..2d8a3c9bd 100644 --- a/AnalyticsTests/MiddlewareTests.swift +++ b/AnalyticsTests/MiddlewareTests.swift @@ -21,6 +21,7 @@ let customizeAllTrackCalls = SEGBlockMiddleware { (context, next) in let newEvent = "[New] \(track.event)" var newProps = track.properties ?? [:] newProps["customAttribute"] = "Hello" + newProps["nullTest"] = NSNull() ctx.payload = SEGTrackPayload( event: newEvent, properties: newProps, @@ -65,6 +66,8 @@ class MiddlewareTests: QuickSpec { let track = passthrough.lastContext?.payload as? SEGTrackPayload expect(track?.event) == "[New] Purchase Success" expect(track?.properties?["customAttribute"] as? String) == "Hello" + let isNull = (track?.properties?["nullTest"] is NSNull) + expect(isNull) == true } it("expects event to be swallowed if next is not called") { diff --git a/AnalyticsTests/SerializationTests.m b/AnalyticsTests/SerializationTests.m new file mode 100644 index 000000000..76c57eb66 --- /dev/null +++ b/AnalyticsTests/SerializationTests.m @@ -0,0 +1,55 @@ +// +// PropSerializationTests.m +// AnalyticsTests +// +// Created by Brandon Sneed on 11/20/19. +// Copyright © 2019 Segment. All rights reserved. +// + +#import +@import Analytics; + +@protocol SEGSerializableDeepCopy +-(id _Nullable) serializableDeepCopy; +@end + +@interface NSDictionary(SerializableDeepCopy) +@end + +@interface NSArray(SerializableDeepCopy) +@end + +@interface SerializationTests : XCTestCase + +@end + +@implementation SerializationTests + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testDeepCopyAndConformance { + NSDictionary *nonserializable = @{@"test": @1, @"nonserializable": self, @"nested": @{@"nonserializable": self}, @"array": @[@1, @2, @3, self]}; + NSDictionary *serializable = @{@"test": @1, @"nonserializable": @0, @"nested": @{@"nonserializable": @0}, @"array": @[@1, @2, @3, @0]}; + + NSDictionary *aCopy = [serializable serializableDeepCopy]; + XCTAssert(aCopy != serializable); + + NSDictionary *sub = [serializable objectForKey:@"nested"]; + NSDictionary *subCopy = [aCopy objectForKey:@"nested"]; + XCTAssert(sub != subCopy); + + NSDictionary *array = [serializable objectForKey:@"array"]; + NSDictionary *arrayCopy = [aCopy objectForKey:@"array"]; + XCTAssert(array != arrayCopy); + + XCTAssertNoThrow([serializable serializableDeepCopy]); + XCTAssertThrows([nonserializable serializableDeepCopy]); +} + +@end diff --git a/AnalyticsTests/TrackingTests.swift b/AnalyticsTests/TrackingTests.swift index 77c2bf9e8..53f8ced2b 100644 --- a/AnalyticsTests/TrackingTests.swift +++ b/AnalyticsTests/TrackingTests.swift @@ -89,6 +89,15 @@ class TrackingTests: QuickSpec { expect(payload?.groupId) == "acme-company" expect(payload?.traits?["employees"] as? Int) == 2333 } + + it("handles null values") { + analytics.track("null test", properties: [ + "nullTest": NSNull() + ]) + let payload = passthrough.lastContext?.payload as? SEGTrackPayload + let isNull = (payload?.properties?["nullTest"] is NSNull) + expect(isNull) == true + } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 723fbb370..28056324e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ Change Log ========== +Version 3.8.0-beta.1 *(7th January, 2020)* +------------------------------------------ + + * [Fix](https://github.com/segmentio/analytics-ios/pull/856) Reload static context data when the app returns from background. + * [Fix](https://github.com/segmentio/analytics-ios/pull/855) Fixes issue where customers can overwrite information regarding integration enablement. + * [Fix](https://github.com/segmentio/analytics-ios/pull/854) Swapped JSON in for the storage format instead of plists. + * [Fix](https://github.com/segmentio/analytics-ios/pull/853) Hardened handling of user-supplied data in event properties. + * [New](https://github.com/segmentio/analytics-ios/pull/839) Added support for SSL pinning. + * [Fix](https://github.com/segmentio/analytics-ios/pull/842) CoreTelephony library is now only included on iOS targets. + +Version 3.8.0-beta.0 *(25th July, 2019)* +---------------------------------------- + + * [New](https://github.com/segmentio/analytics-ios/pull/831): Add iOS Backgrounded Event. + * [Fix](https://github.com/segmentio/analytics-ios/pull/785): Fix GCD mutual dependency + * [Fix](https://github.com/segmentio/analytics-ios/pull/): adding “name” field to payload; updated nimble to version 7.3.4 + +Version 3.7.0 *(22nd July, 2019)* +--------------------------------- + +This release promotes 3.7.0-beta.4 to stable. + Version 3.7.0-beta.4 *(19th June, 2019)* ----------------------------------------- * [Fix](https://github.com/segmentio/analytics-ios/pull/812): Remove invalid `.clang-format` symlink which can cause issues with manual builds. diff --git a/Examples/CarthageExample/Cartfile b/Examples/CarthageExample/Cartfile index 65d12a1b4..0fa78b699 100644 --- a/Examples/CarthageExample/Cartfile +++ b/Examples/CarthageExample/Cartfile @@ -1,3 +1,3 @@ -github "segmentio/analytics-ios" "3.7.0-beta.4" +github "segmentio/analytics-ios" "3.8.0-beta.0" # Use a local project when debugging # git "~/Code/segmentio/analytics-ios/" "master" diff --git a/Podfile b/Podfile index 4c1ed0721..2446fe8af 100644 --- a/Podfile +++ b/Podfile @@ -1,10 +1,10 @@ target 'AnalyticsTests' do platform :ios, '11' - + use_frameworks! - + pod 'Quick', '~> 1.2.0' - pod 'Nimble', '~> 7.3.1' + pod 'Nimble', '~> 7.3.4' pod 'Nocilla', '~> 0.11.0' pod 'Alamofire', '~> 4.5' pod 'Alamofire-Synchronous', '~> 4.0' diff --git a/Podfile.lock b/Podfile.lock index 162d71864..53dc69dda 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,7 +2,7 @@ PODS: - Alamofire (4.6.0) - Alamofire-Synchronous (4.0.0): - Alamofire (~> 4.0) - - Nimble (7.3.1) + - Nimble (7.3.4) - Nocilla (0.11.0) - Quick (1.2.0) - SwiftTryCatch (1.0.0) @@ -10,13 +10,13 @@ PODS: DEPENDENCIES: - Alamofire (~> 4.5) - Alamofire-Synchronous (~> 4.0) - - Nimble (~> 7.3.1) + - Nimble (~> 7.3.4) - Nocilla (~> 0.11.0) - Quick (~> 1.2.0) - SwiftTryCatch (from `https://github.com/segmentio/SwiftTryCatch.git`) SPEC REPOS: - https://github.com/cocoapods/specs.git: + https://github.com/CocoaPods/Specs.git: - Alamofire - Alamofire-Synchronous - Nimble @@ -35,11 +35,11 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: f41a599bd63041760b26d393ec1069d9d7b917f4 Alamofire-Synchronous: eedf1e6e961c3795a63c74990b3f7d9fbfac7e50 - Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae + Nimble: 051e3d8912d40138fa5591c78594f95fb172af37 Nocilla: 7af7a386071150cc8aa5da4da97d060f049dd61c Quick: 58d203b1c5e27fff7229c4c1ae445ad7069a7a08 SwiftTryCatch: 2f4ef36cf5396bdb450006b70633dbce5260d3b3 -PODFILE CHECKSUM: cf4abb4263c7b514d71c70514284ac657d90865d +PODFILE CHECKSUM: 6c11ce6879d40225a5ec2b9b5ac275626edbeeb6 -COCOAPODS: 1.5.3 +COCOAPODS: 1.8.4 diff --git a/README.md b/README.md index 5df485b85..a9c905752 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Analytics is available through [CocoaPods](http://cocoapods.org) and [Carthage]( ### CocoaPods ```ruby -pod "Analytics", "3.6.10" +pod "Analytics", "3.7.0" ``` ### Carthage