diff --git a/RadarSDK.podspec b/RadarSDK.podspec index ffd0f2607..73fb30c11 100644 --- a/RadarSDK.podspec +++ b/RadarSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RadarSDK' - s.version = '3.8.12' + s.version = '3.8.13' s.summary = 'iOS SDK for Radar, the leading geofencing and location tracking platform' s.homepage = 'https://radar.com' s.author = { 'Radar Labs, Inc.' => 'support@radar.com' } diff --git a/RadarSDK.xcodeproj/project.pbxproj b/RadarSDK.xcodeproj/project.pbxproj index 8f46bce20..fbecb6ede 100644 --- a/RadarSDK.xcodeproj/project.pbxproj +++ b/RadarSDK.xcodeproj/project.pbxproj @@ -159,6 +159,8 @@ DD8E2F7A24018C37002D51AB /* CLLocationManagerMock.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8E2F7924018C37002D51AB /* CLLocationManagerMock.m */; }; DD8E2F7D24018C54002D51AB /* CLVisitMock.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8E2F7C24018C54002D51AB /* CLVisitMock.m */; }; DE1E7644239724FD006F34A1 /* search_geofences.json in Resources */ = {isa = PBXBuildFile; fileRef = DE1E7643239724FD006F34A1 /* search_geofences.json */; }; + E6EEC56E2B20F41A00DD096B /* RadarFileStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = E6EEC56D2B20F41A00DD096B /* RadarFileStorage.h */; }; + E6EEC5702B20F45D00DD096B /* RadarFileStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = E6EEC56F2B20F45D00DD096B /* RadarFileStorage.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -315,6 +317,8 @@ DDD7BD0325EC3015002473B3 /* RadarRouteMatrix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RadarRouteMatrix.m; sourceTree = ""; }; DDF1157C2524E18100D575C4 /* RadarTrip.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RadarTrip.m; sourceTree = ""; }; DE1E7643239724FD006F34A1 /* search_geofences.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = search_geofences.json; sourceTree = ""; }; + E6EEC56D2B20F41A00DD096B /* RadarFileStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RadarFileStorage.h; sourceTree = ""; }; + E6EEC56F2B20F45D00DD096B /* RadarFileStorage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RadarFileStorage.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -447,6 +451,8 @@ 58F950CF2407038300364B15 /* RadarCollectionAdditions.m */, 01F99CFA2965C182004E8CF3 /* RadarConfig.h */, 01F99CFC2965C1C4004E8CF3 /* RadarConfig.m */, + E6EEC56D2B20F41A00DD096B /* RadarFileStorage.h */, + E6EEC56F2B20F45D00DD096B /* RadarFileStorage.m */, 96A5A11727ADA02E007B960B /* RadarDelegateHolder.h */, DD4C104925D87E3E009C2E36 /* RadarDelegateHolder.m */, DD236CF723088F8400EB88F9 /* RadarLocationManager.h */, @@ -604,6 +610,7 @@ 96A5A10527AD9F7F007B960B /* RadarTrackingOptions.h in Headers */, 96A5A10927AD9F7F007B960B /* RadarContext.h in Headers */, 0107AA1226220049008AB52F /* RadarCollectionAdditions.h in Headers */, + E6EEC56E2B20F41A00DD096B /* RadarFileStorage.h in Headers */, 015C53AD29B8E8BA004F53A6 /* (null) in Headers */, 0107AA1C26220055008AB52F /* RadarPermissionsHelper.h in Headers */, 96A5A11227AD9F7F007B960B /* Radar.h in Headers */, @@ -790,6 +797,7 @@ 9679F4A327CD8DE200800797 /* CLLocation+Radar.m in Sources */, 0107AB08262201CE008AB52F /* RadarAPIClient.m in Sources */, 0107AB05262201CB008AB52F /* Radar.m in Sources */, + E6EEC5702B20F45D00DD096B /* RadarFileStorage.m in Sources */, 0107AB29262201F4008AB52F /* RadarTrackingOptions.m in Sources */, 0107AB2F262201FB008AB52F /* RadarUtils.m in Sources */, 0107AA8926220140008AB52F /* RadarChain.m in Sources */, @@ -860,7 +868,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 3.8.12; + MARKETING_VERSION = 3.8.13; PRODUCT_BUNDLE_IDENTIFIER = io.radar.sdk; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -890,7 +898,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 3.8.12; + MARKETING_VERSION = 3.8.13; PRODUCT_BUNDLE_IDENTIFIER = io.radar.sdk; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -973,7 +981,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - MARKETING_VERSION = 3.8.12; + MARKETING_VERSION = 3.8.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1031,7 +1039,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - MARKETING_VERSION = 3.8.12; + MARKETING_VERSION = 3.8.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CFLAGS = "-fembed-bitcode"; diff --git a/RadarSDK/Include/Radar.h b/RadarSDK/Include/Radar.h index cc43eb240..89b268851 100644 --- a/RadarSDK/Include/Radar.h +++ b/RadarSDK/Include/Radar.h @@ -1003,6 +1003,24 @@ logConversionWithNotification */ + (void)setLogLevel:(RadarLogLevel)level; +/** + Log application terminating. Include this in your application delegate's applicationWillTerminate: method. + + */ ++ (void)logTermination; + +/** + Log application entering background. Include this in your application delegate's applicationDidEnterBackground: method. + */ ++ (void)logBackgrounding; + +/** + Log application resigning active. Include this in your application delegate's applicationWillResignActive: method. + + */ ++ (void)logResigningActive; + + #pragma mark - Helpers /** diff --git a/RadarSDK/Radar.m b/RadarSDK/Radar.m index d545cdf80..63ef9912a 100644 --- a/RadarSDK/Radar.m +++ b/RadarSDK/Radar.m @@ -66,7 +66,9 @@ + (void)initializeWithPublishableKey:(NSString *)publishableKey { completionHandler:^(RadarStatus status, RadarConfig *config) { [[RadarLocationManager sharedInstance] updateTrackingFromMeta:config.meta]; [RadarSettings setFeatureSettings:config.meta.featureSettings]; + [self flushLogs]; }]; + } #pragma mark - Properties @@ -1036,6 +1038,20 @@ + (void)setLogLevel:(RadarLogLevel)level { [RadarSettings setLogLevel:level]; } ++ (void)logTermination { + [[RadarLogger sharedInstance] logWithLevel:RadarLogLevelInfo type:RadarLogTypeNone message:@"App terminating" includeDate:YES includeBattery:YES append:YES]; +} + ++ (void)logBackgrounding { + [[RadarLogger sharedInstance] logWithLevel:RadarLogLevelInfo type:RadarLogTypeNone message:@"App entering background" includeDate:YES includeBattery:YES append:YES]; + [[RadarLogBuffer sharedInstance] persistLogs]; +} + ++ (void)logResigningActive { + [[RadarLogger sharedInstance] logWithLevel:RadarLogLevelInfo type:RadarLogTypeNone message:@"App resigning active" includeDate:YES includeBattery:YES]; +} + + #pragma mark - Helpers + (NSString *)stringForStatus:(RadarStatus)status { @@ -1256,21 +1272,14 @@ + (void)flushLogs { return; } - NSArray *flushableLogs = [[RadarLogBuffer sharedInstance] flushableLogs]; - + NSArray *flushableLogs = [[RadarLogBuffer sharedInstance] flushableLogs]; NSUInteger pendingLogCount = [flushableLogs count]; if (pendingLogCount == 0) { return; } - // remove logs from buffer to handle multiple flushLogs calls - [[RadarLogBuffer sharedInstance] removeLogsFromBuffer:pendingLogCount]; - RadarSyncLogsAPICompletionHandler onComplete = ^(RadarStatus status) { - // if an error occurs in syncing, add the logs back to the buffer - if (status != RadarStatusSuccess) { - [[RadarLogBuffer sharedInstance] addLogsToBuffer:flushableLogs]; - } + [[RadarLogBuffer sharedInstance] onFlush:status == RadarStatusSuccess logs:flushableLogs]; }; [[RadarAPIClient sharedInstance] syncLogs:flushableLogs diff --git a/RadarSDK/RadarFeatureSettings.h b/RadarSDK/RadarFeatureSettings.h index ddc3659b4..141dd7256 100644 --- a/RadarSDK/RadarFeatureSettings.h +++ b/RadarSDK/RadarFeatureSettings.h @@ -21,6 +21,7 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL usePersistence; @property (nonatomic, assign) BOOL extendFlushReplays; +@property (nonatomic, assign) BOOL useLogPersistence; /** Initializes a new RadarFeatureSettings object with given value. @@ -28,7 +29,8 @@ NS_ASSUME_NONNULL_BEGIN @param usePersistence A flag indicating whether to use persistence. */ - (instancetype)initWithUsePersistence:(BOOL)usePersistence - extendFlushReplays:(BOOL)extendFlushReplays; + extendFlushReplays:(BOOL)extendFlushReplays + useLogPersistence:(BOOL)useLogPersistence; /** Creates a RadarFeatureSettings object from the provided dictionary. diff --git a/RadarSDK/RadarFeatureSettings.m b/RadarSDK/RadarFeatureSettings.m index cdb759462..9455a312f 100644 --- a/RadarSDK/RadarFeatureSettings.m +++ b/RadarSDK/RadarFeatureSettings.m @@ -10,17 +10,19 @@ @implementation RadarFeatureSettings - (instancetype)initWithUsePersistence:(BOOL)usePersistence - extendFlushReplays:(BOOL)extendFlushReplays { + extendFlushReplays:(BOOL)extendFlushReplays + useLogPersistence:(BOOL)useLogPersistence { if (self = [super init]) { _usePersistence = usePersistence; _extendFlushReplays = extendFlushReplays; + _useLogPersistence = useLogPersistence; } return self; } + (RadarFeatureSettings *_Nullable)featureSettingsFromDictionary:(NSDictionary *)dict { if (!dict) { - return [[RadarFeatureSettings alloc] initWithUsePersistence:NO extendFlushReplays:NO]; + return [[RadarFeatureSettings alloc] initWithUsePersistence:NO extendFlushReplays:NO useLogPersistence:NO]; } NSObject *usePersistenceObj = dict[@"usePersistence"]; @@ -34,13 +36,21 @@ + (RadarFeatureSettings *_Nullable)featureSettingsFromDictionary:(NSDictionary * if (extendFlushReplaysObj && [extendFlushReplaysObj isKindOfClass:[NSNumber class]]) { extendFlushReplays = [(NSNumber *)extendFlushReplaysObj boolValue]; } - return [[RadarFeatureSettings alloc] initWithUsePersistence:usePersistence extendFlushReplays:extendFlushReplays]; + + NSObject *useLogPersistenceObj = dict[@"useLogPersistence"]; + BOOL useLogPersistence = NO; + if (useLogPersistenceObj && [useLogPersistenceObj isKindOfClass:[NSNumber class]]) { + useLogPersistence = [(NSNumber *)useLogPersistenceObj boolValue]; + } + + return [[RadarFeatureSettings alloc] initWithUsePersistence:usePersistence extendFlushReplays:extendFlushReplays useLogPersistence:useLogPersistence]; } - (NSDictionary *)dictionaryValue { NSMutableDictionary *dict = [NSMutableDictionary new]; [dict setValue:@(self.usePersistence) forKey:@"usePersistence"]; [dict setValue:@(self.extendFlushReplays) forKey:@"extendFlushReplays"]; + [dict setValue:@(self.useLogPersistence) forKey:@"useLogPersistence"]; return dict; } diff --git a/RadarSDK/RadarFileStorage.h b/RadarSDK/RadarFileStorage.h new file mode 100644 index 000000000..4d8e3602b --- /dev/null +++ b/RadarSDK/RadarFileStorage.h @@ -0,0 +1,24 @@ +// +// RadarFileStorage.h +// RadarSDK +// +// Created by Kenny Hu on 12/6/23. +// Copyright © 2023 Radar Labs, Inc. All rights reserved. +// + +#import + +@interface RadarFileStorage : NSObject + + +- (NSData *)readFileAtPath:(NSString *)filePath; + +- (void)writeData:(NSData *)data toFileAtPath:(NSString *)filePath; + +- (void)deleteFileAtPath:(NSString *)filePath; + +- (NSArray *)sortedFilesInDirectory:(NSString *)directoryPath; + +- (NSArray *)sortedFilesInDirectory:(NSString *)directoryPath usingComparator:(NSComparator)comparator; + +@end diff --git a/RadarSDK/RadarFileStorage.m b/RadarSDK/RadarFileStorage.m new file mode 100644 index 000000000..47900065e --- /dev/null +++ b/RadarSDK/RadarFileStorage.m @@ -0,0 +1,61 @@ +// +// RadarFileStorage.m +// RadarSDK +// +// Created by Kenny Hu on 12/6/23. +// Copyright © 2023 Radar Labs, Inc. All rights reserved. +// + +#import +#import "RadarFileStorage.h" + +@implementation RadarFileStorage + + + +- (NSData *)readFileAtPath:(NSString *)filePath { + __block NSData *fileData = nil; + + NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init]; + [fileCoordinator coordinateReadingItemAtURL:[NSURL fileURLWithPath:filePath] options:0 error:nil byAccessor:^(NSURL *newURL) { + fileData = [NSData dataWithContentsOfURL:newURL]; + }]; + + return fileData; +} + +- (void)writeData:(NSData *)data toFileAtPath:(NSString *)filePath { + + NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init]; + [fileCoordinator coordinateWritingItemAtURL:[NSURL fileURLWithPath:filePath] options:NSFileCoordinatorWritingForReplacing error:nil byAccessor:^(NSURL *newURL) { + [data writeToURL:newURL options:NSDataWritingAtomic error:nil]; + }]; +} + +- (void)deleteFileAtPath:(NSString *)filePath { + NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init]; + [fileCoordinator coordinateWritingItemAtURL:[NSURL fileURLWithPath:filePath] options:NSFileCoordinatorWritingForDeleting error:nil byAccessor:^(NSURL *newURL) { + [[NSFileManager defaultManager] removeItemAtURL:newURL error:nil]; + }]; +} + +- (NSArray *)sortedFilesInDirectory:(NSString *)directoryPath { + return [self sortedFilesInDirectory:directoryPath usingComparator:^NSComparisonResult(NSString *fileName1, NSString *fileName2) { + return [fileName1 compare:fileName2]; + }]; +} + +- (NSArray *)sortedFilesInDirectory:(NSString *)directoryPath usingComparator:(NSComparator)comparator { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + NSArray *files = [fileManager contentsOfDirectoryAtPath:directoryPath error:&error]; + + if (error) { + NSLog(@"Failed to get files in directory: %@", [error localizedDescription]); + return nil; + } + + return [files sortedArrayUsingComparator: comparator]; +} + +@end diff --git a/RadarSDK/RadarLog.h b/RadarSDK/RadarLog.h index fb552a506..c066e2e77 100644 --- a/RadarSDK/RadarLog.h +++ b/RadarSDK/RadarLog.h @@ -10,7 +10,7 @@ /** Represents a debug log. */ -@interface RadarLog : NSObject +@interface RadarLog : NSObject /** The levels for debug logs. diff --git a/RadarSDK/RadarLog.m b/RadarSDK/RadarLog.m index cdd218432..c747dcd50 100644 --- a/RadarSDK/RadarLog.m +++ b/RadarSDK/RadarLog.m @@ -94,4 +94,24 @@ - (NSDictionary *)dictionaryValue { return arr; } +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _level = [coder decodeIntegerForKey:@"level"]; + _type = [coder decodeIntegerForKey:@"type"]; + _message = [coder decodeObjectForKey:@"message"]; + _createdAt = [coder decodeObjectForKey:@"createdAt"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeInteger:_level forKey:@"level"]; + [coder encodeInteger:_type forKey:@"type"]; + [coder encodeObject:_message forKey:@"message"]; + [coder encodeObject:_createdAt forKey:@"createdAt"]; +} + @end diff --git a/RadarSDK/RadarLogBuffer.h b/RadarSDK/RadarLogBuffer.h index a8fe5978d..d5fb6366c 100644 --- a/RadarSDK/RadarLogBuffer.h +++ b/RadarSDK/RadarLogBuffer.h @@ -8,22 +8,31 @@ #import #import "RadarLog.h" +#import "RadarFileStorage.h" NS_ASSUME_NONNULL_BEGIN @interface RadarLogBuffer : NSObject @property (assign, nonatomic, readonly) NSArray *flushableLogs; +@property (strong, nonatomic) NSString *logFileDir; +@property (strong, nonatomic) RadarFileStorage *fileHandler; +@property (nonatomic, strong) NSTimer *timer; +@property (nonatomic, assign) BOOL persistentLogFeatureFlag; + (instancetype)sharedInstance; - (void)write:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message; -- (void)purgeOldestLogs; +- (void)write:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message forcePersist:(BOOL)forcePersist; -- (void)removeLogsFromBuffer:(NSUInteger)numLogs; +- (void)persistLogs; -- (void)addLogsToBuffer:(NSArray *)logs; +- (void)clearBuffer; + +- (void)setPersistentLogFeatureFlag:(BOOL)persistentLogFeatureFlag; + +- (void)onFlush:(BOOL)success logs:(NSArray *)logs; @end diff --git a/RadarSDK/RadarLogBuffer.m b/RadarSDK/RadarLogBuffer.m index 294b582d2..604612a98 100644 --- a/RadarSDK/RadarLogBuffer.m +++ b/RadarSDK/RadarLogBuffer.m @@ -7,24 +7,44 @@ #import "RadarLogBuffer.h" #import "RadarLog.h" +#import "RadarFileStorage.h" +#import "RadarSettings.h" +static const int MAX_PERSISTED_BUFFER_SIZE = 500; +static const int MAX_MEMORY_BUFFER_SIZE = 200; +static const int PURGE_AMOUNT = 250; static const int MAX_BUFFER_SIZE = 500; -static const int PURGE_AMOUNT = 200; static NSString *const kPurgedLogLine = @"----- purged oldest logs -----"; +static int fileCounter = 0; + @implementation RadarLogBuffer { - NSMutableArray *mutableLogBuffer; + NSMutableArray *logBuffer; } - (instancetype)init { self = [super init]; if (self) { - mutableLogBuffer = [NSMutableArray new]; + _persistentLogFeatureFlag = [RadarSettings featureSettings].useLogPersistence; + logBuffer = [NSMutableArray new]; + + NSString *documentsDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + self.logFileDir = [documentsDirectory stringByAppendingPathComponent:@"radar_logs"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:self.logFileDir isDirectory:nil]) { + [[NSFileManager defaultManager] createDirectoryAtPath:self.logFileDir withIntermediateDirectories:YES attributes:nil error:nil]; + } + self.fileHandler = [[RadarFileStorage alloc] init]; + _timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(persistLogs) userInfo:nil repeats:YES]; + } return self; } +- (void)setPersistentLogFeatureFlag:(BOOL)persistentLogFeatureFlag { + _persistentLogFeatureFlag = persistentLogFeatureFlag; +} + + (instancetype)sharedInstance { static dispatch_once_t once; static id sharedInstance; @@ -35,34 +55,171 @@ + (instancetype)sharedInstance { } - (void)write:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message { - // purge oldest log if reached the max buffer size - NSUInteger logLength = [mutableLogBuffer count]; - if (logLength >= MAX_BUFFER_SIZE) { - [self purgeOldestLogs]; - } - // add new log to buffer + [self write:level type:type message:message forcePersist:NO]; +} + +- (void)write:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message forcePersist:(BOOL)forcePersist { RadarLog *radarLog = [[RadarLog alloc] initWithLevel:level type:type message:message]; - [mutableLogBuffer addObject:radarLog]; + // bypass sync lock here to ensure that other writes or persisting logs don't block the current thread as the app is terminating + if (forcePersist && (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"])) { + [self writeToFileStorage:@[radarLog]]; + return; + } + @synchronized (self) { + [logBuffer addObject:radarLog]; + if (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"]) { + if ([logBuffer count] >= MAX_MEMORY_BUFFER_SIZE) { + [self persistLogs]; + } + } else { + if ([logBuffer count] >= MAX_BUFFER_SIZE) { + [self purgeOldestLogs]; + } + } + } } +- (void)persistLogs { + @synchronized (self) { + if (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"]) { + if ([logBuffer count] > 0) { + [self writeToFileStorage:logBuffer]; + [logBuffer removeAllObjects]; + } + + } + } +} + +- (NSArray *)getLogFilesInTimeOrder { + NSString *characterToStrip = @"_"; + NSComparator compareTimeStamps = ^NSComparisonResult(NSString *str1, NSString *str2) { + return [@([[str1 stringByReplacingOccurrencesOfString:characterToStrip withString:@""] integerValue]) + compare:@([[str2 stringByReplacingOccurrencesOfString:characterToStrip withString:@""] integerValue])]; + }; + + return [self.fileHandler sortedFilesInDirectory:self.logFileDir usingComparator:compareTimeStamps]; +} + +- (NSMutableArray *)readFromFileStorage { + + NSArray *files = [self getLogFilesInTimeOrder]; + NSMutableArray *logs = [NSMutableArray array]; + if (!files) { + return logs; + } + for (NSString *file in files) { + NSString *filePath = [self.logFileDir stringByAppendingPathComponent:file]; + NSData *fileData = [self.fileHandler readFileAtPath:filePath]; + RadarLog *log = [NSKeyedUnarchiver unarchiveObjectWithData:fileData]; + if (log && log.message) { + [logs addObject:log]; + } + } + + return logs; +} + + - (void)writeToFileStorage:(NSArray *)logs { + for (RadarLog *log in logs) { + NSData *logData = [NSKeyedArchiver archivedDataWithRootObject:log]; + NSTimeInterval unixTimestamp = [log.createdAt timeIntervalSince1970]; + // Logs may be created in the same millisecond, so we append a counter to the end of the timestamp to "tiebreak" + NSString *unixTimestampString = [NSString stringWithFormat:@"%lld_%04d", (long long)unixTimestamp, fileCounter++]; + NSString *filePath = [self.logFileDir stringByAppendingPathComponent:unixTimestampString]; + [self.fileHandler writeData:logData toFileAtPath:filePath]; + } + } + - (NSArray *)flushableLogs { - NSArray *flushableLogs = [mutableLogBuffer copy]; - return flushableLogs; + @synchronized (self) { + if (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"]) { + [self persistLogs]; + [self purgeOldestLogs]; + NSArray *existingLogsArray = [self.readFromFileStorage copy]; + [self removeLogs:[existingLogsArray count]]; + return existingLogsArray; + } else { + NSArray *flushableLogs = [logBuffer copy]; + [logBuffer removeAllObjects]; + return flushableLogs; + } + } } - (void)purgeOldestLogs { - // drop the oldest N logs from the buffer - [mutableLogBuffer removeObjectsInRange:NSMakeRange(0, PURGE_AMOUNT)]; - RadarLog *purgeLog = [[RadarLog alloc] initWithLevel:RadarLogLevelDebug type:RadarLogTypeNone message:kPurgedLogLine]; - [mutableLogBuffer insertObject:purgeLog atIndex:0]; + if (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"]) { + NSArray *files = [self getLogFilesInTimeOrder]; + NSUInteger dirSize = [files count]; + BOOL printedPurgedLogs = NO; + while (dirSize >= MAX_PERSISTED_BUFFER_SIZE) { + [self removeLogs:PURGE_AMOUNT]; + dirSize = [[self getLogFilesInTimeOrder] count]; + if (!printedPurgedLogs) { + printedPurgedLogs = YES; + RadarLog *purgeLog = [[RadarLog alloc] initWithLevel:RadarLogLevelDebug type:RadarLogTypeNone message:kPurgedLogLine]; + [self writeToFileStorage:@[purgeLog]]; + } + } + } else { + // drop the oldest N logs from the buffer + [logBuffer removeObjectsInRange:NSMakeRange(0, PURGE_AMOUNT)]; + RadarLog *purgeLog = [[RadarLog alloc] initWithLevel:RadarLogLevelDebug type:RadarLogTypeNone message:kPurgedLogLine]; + [logBuffer insertObject:purgeLog atIndex:0]; + } } + -- (void)removeLogsFromBuffer:(NSUInteger)numLogs { - [mutableLogBuffer removeObjectsInRange:NSMakeRange(0, numLogs)]; + +- (void)removeLogs:(NSUInteger)numLogs { + @synchronized (self) { + if (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"]) { + NSArray *files = [self getLogFilesInTimeOrder]; + for (NSUInteger i = 0; i < MIN(numLogs, [files count]); i++) { + NSString *file = [files objectAtIndex:i]; + NSString *filePath = [self.logFileDir stringByAppendingPathComponent:file]; + [self.fileHandler deleteFileAtPath:filePath]; + } + } else { + [logBuffer removeObjectsInRange:NSMakeRange(0, MIN(numLogs, [logBuffer count]))]; + } + } } -- (void)addLogsToBuffer:(NSArray *)logs { - [mutableLogBuffer addObjectsFromArray:logs]; + +- (void)onFlush:(BOOL)success logs:(NSArray *)logs{ + @synchronized (self) { + if (_persistentLogFeatureFlag || [[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"]) { + if (!success) { + [self writeToFileStorage:logs]; + // Attempt purge to only remove the oldest logs to reduce payload size of next attempt. + [self purgeOldestLogs]; + } + } else { + if (!success) { + [logBuffer addObjectsFromArray:logs]; + if ([logBuffer count] >= MAX_BUFFER_SIZE) { + [self purgeOldestLogs]; + } + } + } + } +} + +/** +* Clears the in-memory buffer and deletes all persisted logs. (For use in testing only.) +*/ +-(void)clearBuffer { + @synchronized (self) { + [logBuffer removeAllObjects]; + NSArray *files = [self getLogFilesInTimeOrder]; + if (files) { + for (NSString *file in files) { + NSString *filePath = [self.logFileDir stringByAppendingPathComponent:file]; + [self.fileHandler deleteFileAtPath:filePath]; + } + } + } } @end diff --git a/RadarSDK/RadarLogger.h b/RadarSDK/RadarLogger.h index 5017b6484..d334e6540 100644 --- a/RadarSDK/RadarLogger.h +++ b/RadarSDK/RadarLogger.h @@ -13,9 +13,15 @@ NS_ASSUME_NONNULL_BEGIN @interface RadarLogger : NSObject +@property (strong, nonatomic) NSDateFormatter *dateFormatter; +@property (strong, nonatomic) UIDevice *device; + + (instancetype)sharedInstance; - (void)logWithLevel:(RadarLogLevel)level message:(NSString *)message; - (void)logWithLevel:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message; +- (void)logWithLevel:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message includeDate:(BOOL)includeDate includeBattery:(BOOL)includeBattery; +- (void)logWithLevel:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message includeDate:(BOOL)includeDate includeBattery:(BOOL)includeBattery append:(BOOL)append; + @end NS_ASSUME_NONNULL_END diff --git a/RadarSDK/RadarLogger.m b/RadarSDK/RadarLogger.m index cfb7a1477..a04751c41 100644 --- a/RadarSDK/RadarLogger.m +++ b/RadarSDK/RadarLogger.m @@ -11,6 +11,8 @@ #import "RadarSettings.h" #import "RadarUtils.h" #import +#import "RadarLog.h" +#import "RadarLogBuffer.h" @implementation RadarLogger @@ -23,23 +25,55 @@ + (instancetype)sharedInstance { return sharedInstance; } +- (instancetype)init { + self = [super init]; + if (self) { + self.dateFormatter = [[NSDateFormatter alloc] init]; + [self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + self.device = [UIDevice currentDevice]; + self.device.batteryMonitoringEnabled = YES; + } + return self; +} + - (void)logWithLevel:(RadarLogLevel)level message:(NSString *)message { [self logWithLevel:level type:RadarLogTypeNone message:message]; } - (void)logWithLevel:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message { - dispatch_async(dispatch_get_main_queue(), ^{ - [Radar sendLog:level type:type message:message]; + [self logWithLevel:level type:type message:message includeDate:NO includeBattery:NO]; +} - RadarLogLevel logLevel = [RadarSettings logLevel]; - if (logLevel >= level) { - NSString *log = [NSString stringWithFormat:@"%@ | backgroundTimeRemaining = %g", message, [RadarUtils backgroundTimeRemaining]]; +- (void)logWithLevel:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message includeDate:(BOOL)includeDate includeBattery:(BOOL)includeBattery{ + [self logWithLevel:level type:type message:message includeDate:includeDate includeBattery:includeBattery append:NO]; +} - os_log(OS_LOG_DEFAULT, "%@", log); +- (void)logWithLevel:(RadarLogLevel)level type:(RadarLogType)type message:(NSString *)message includeDate:(BOOL)includeDate includeBattery:(BOOL)includeBattery append:(BOOL)append{ + NSString *dateString = [self.dateFormatter stringFromDate:[NSDate date]]; + float batteryLevel = [self.device batteryLevel]; + if (includeDate && includeBattery) { + message = [NSString stringWithFormat:@"%@ | at %@ | with %2.f%% battery", message, dateString, batteryLevel*100]; + } else if (includeDate) { + message = [NSString stringWithFormat:@"%@ | at %@", message, dateString]; + } else if (includeBattery) { + message = [NSString stringWithFormat:@"%@ | with %2.f%% battery", message, batteryLevel*100]; + } + if (append) { + [[RadarLogBuffer sharedInstance] write:level type:type message:message forcePersist:YES]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [Radar sendLog:level type:type message:message]; - [[RadarDelegateHolder sharedInstance] didLogMessage:log]; - } - }); + RadarLogLevel logLevel = [RadarSettings logLevel]; + if (logLevel >= level) { + NSString *log = [NSString stringWithFormat:@"%@ | backgroundTimeRemaining = %g", message, [RadarUtils backgroundTimeRemaining]]; + + os_log(OS_LOG_DEFAULT, "%@", log); + + [[RadarDelegateHolder sharedInstance] didLogMessage:log]; + } + }); + } } @end diff --git a/RadarSDK/RadarSettings.m b/RadarSDK/RadarSettings.m index 865df780e..3b8c57931 100644 --- a/RadarSDK/RadarSettings.m +++ b/RadarSDK/RadarSettings.m @@ -12,6 +12,7 @@ #import "RadarTripOptions.h" #import "RadarFeatureSettings.h" #import "RadarReplayBuffer.h" +#import "RadarLogBuffer.h" @implementation RadarSettings @@ -214,9 +215,12 @@ + (RadarFeatureSettings *)featureSettings { + (void)setFeatureSettings:(RadarFeatureSettings *)featureSettings { if (featureSettings) { + //This is added as reading from NSUserdefaults is too slow for this feature flag. To be removed when throttling is done. + [[RadarLogBuffer sharedInstance] setPersistentLogFeatureFlag:featureSettings.useLogPersistence]; NSDictionary *featureSettingsDict = [featureSettings dictionaryValue]; [[NSUserDefaults standardUserDefaults] setObject:featureSettingsDict forKey:kFeatureSettings]; } else { + [[RadarLogBuffer sharedInstance] setPersistentLogFeatureFlag:NO]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:kFeatureSettings]; } } diff --git a/RadarSDK/RadarUtils.m b/RadarSDK/RadarUtils.m index 1ac4561b6..17e2cf75d 100644 --- a/RadarSDK/RadarUtils.m +++ b/RadarSDK/RadarUtils.m @@ -45,7 +45,7 @@ + (NSNumber *)timeZoneOffset { } + (NSString *)sdkVersion { - return @"3.8.12"; + return @"3.8.13"; } + (NSString *)deviceId { diff --git a/RadarSDKTests/RadarSDKTests.m b/RadarSDKTests/RadarSDKTests.m index 2835500f8..88336d19e 100644 --- a/RadarSDKTests/RadarSDKTests.m +++ b/RadarSDKTests/RadarSDKTests.m @@ -12,19 +12,24 @@ #import "../RadarSDK/RadarAPIHelper.h" #import "../RadarSDK/RadarLocationManager.h" #import "../RadarSDK/RadarSettings.h" +#import "../RadarSDK/RadarLogBuffer.h" #import "CLLocationManagerMock.h" #import "CLVisitMock.h" #import "RadarAPIHelperMock.h" #import "RadarPermissionsHelperMock.h" #import "RadarTestUtils.h" #import "RadarTripOptions.h" +#import "RadarFileStorage.h" + @interface RadarSDKTests : XCTestCase @property (nonnull, strong, nonatomic) RadarAPIHelperMock *apiHelperMock; @property (nonnull, strong, nonatomic) CLLocationManagerMock *locationManagerMock; @property (nonnull, strong, nonatomic) RadarPermissionsHelperMock *permissionsHelperMock; - +@property (nonatomic, strong) RadarFileStorage *fileSystem; +@property (nonatomic, strong) NSString *testFilePath; +@property (nonatomic, strong) RadarLogBuffer *logBuffer; @end @implementation RadarSDKTests @@ -278,7 +283,6 @@ - (void)assertRoutesOk:(RadarRoutes *)routes { - (void)setUp { [super setUp]; - [Radar initializeWithPublishableKey:kPublishableKey]; [Radar setLogLevel:RadarLogLevelDebug]; @@ -291,9 +295,17 @@ - (void)setUp { self.locationManagerMock.delegate = [RadarLocationManager sharedInstance]; [RadarLocationManager sharedInstance].lowPowerLocationManager = self.locationManagerMock; [RadarLocationManager sharedInstance].permissionsHelper = self.permissionsHelperMock; + self.fileSystem = [[RadarFileStorage alloc] init]; + self.testFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"testfile"]; + [[RadarLogBuffer sharedInstance]clearBuffer]; + [[RadarLogBuffer sharedInstance]setPersistentLogFeatureFlag:YES]; + } - (void)tearDown { + [[NSFileManager defaultManager] removeItemAtPath:self.testFilePath error:nil]; + [[RadarLogBuffer sharedInstance]clearBuffer]; + [super tearDown]; } - (void)test_Radar_initialize { @@ -1395,4 +1407,87 @@ - (void)test_RadarTrackingOptions_isEqual { XCTAssertNotEqualObjects(options, @"foo"); } +- (void)test_RadarFileStorage_writeAndRead { + NSData *originalData = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding]; + [self.fileSystem writeData:originalData toFileAtPath:self.testFilePath]; + NSData *originalData2 = [@"Newer Test data" dataUsingEncoding:NSUTF8StringEncoding]; + [self.fileSystem writeData:originalData2 toFileAtPath:self.testFilePath]; + NSData *readData = [self.fileSystem readFileAtPath:self.testFilePath]; + XCTAssertEqualObjects(originalData2, readData, @"Data read from file should be equal to original data"); +} + +- (void)test_RadarFileStorage_allFilesInDirectory { + NSString *testDir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"newDir"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:testDir isDirectory:nil]) { + [[NSFileManager defaultManager] removeItemAtPath:testDir error:nil]; + } + [[NSFileManager defaultManager] createDirectoryAtPath:testDir withIntermediateDirectories:YES attributes:nil error:nil]; + + NSArray *files = [self.fileSystem sortedFilesInDirectory: testDir]; + XCTAssertEqual(files.count, 0); + NSData *originalData = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding]; + [self.fileSystem writeData:originalData toFileAtPath: [testDir stringByAppendingPathComponent: @"file1"]]; + [self.fileSystem writeData:originalData toFileAtPath: [testDir stringByAppendingPathComponent: @"file2"]]; + NSArray *newFiles = [self.fileSystem sortedFilesInDirectory: testDir]; + XCTAssertEqual(newFiles.count, 2); + +} + +- (void)test_RadarFileStorage_deleteFile { + NSData *originalData = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding]; + [self.fileSystem writeData:originalData toFileAtPath:self.testFilePath]; + [self.fileSystem deleteFileAtPath:self.testFilePath]; + NSData *readData = [self.fileSystem readFileAtPath:self.testFilePath]; + XCTAssertNil(readData, @"Data read from file should be nil after file is deleted"); +} + +- (void)test_RadarLogBuffer_writeAndFlushableLogs { + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 1"]; + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 2"]; + [[RadarLogBuffer sharedInstance]persistLogs]; + NSArray *logs = [[RadarLogBuffer sharedInstance]flushableLogs]; + XCTAssertEqual(logs.count, 2); + XCTAssertEqualObjects(logs.firstObject.message, @"Test message 1"); + XCTAssertEqualObjects(logs.lastObject.message, @"Test message 2"); + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 3"]; + NSArray *newLogs = [[RadarLogBuffer sharedInstance]flushableLogs]; + XCTAssertEqual(newLogs.count, 1); + XCTAssertEqualObjects(newLogs.firstObject.message, @"Test message 3"); +} + +- (void)test_RadarLogBuffer_flush { + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 1"]; + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 2"]; + [[RadarLogBuffer sharedInstance]persistLogs]; + NSArray *logs = [[RadarLogBuffer sharedInstance]flushableLogs]; + [[RadarLogBuffer sharedInstance] onFlush:NO logs:logs]; + logs = [[RadarLogBuffer sharedInstance]flushableLogs]; + XCTAssertEqual(logs.count, 2); + [[RadarLogBuffer sharedInstance] onFlush:YES logs:logs]; + logs = [[RadarLogBuffer sharedInstance]flushableLogs]; + XCTAssertEqual(logs.count, 0); +} + +- (void)test_RadarLogBuffer_append { + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 1" forcePersist:YES]; + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:@"Test message 2" forcePersist:YES]; + NSArray *logs = [[RadarLogBuffer sharedInstance]flushableLogs]; + XCTAssertEqual(logs.count, 2); + XCTAssertEqualObjects(logs.firstObject.message, @"Test message 1"); + XCTAssertEqualObjects(logs.lastObject.message, @"Test message 2"); +} + +- (void)test_RadarLogBuffer_purge { + [[RadarLogBuffer sharedInstance]clearBuffer]; + for (NSUInteger i = 0; i < 600; i++) { + [[RadarLogBuffer sharedInstance]write:RadarLogLevelDebug type:RadarLogTypeNone message:[NSString stringWithFormat:@"message_%d", i]]; + } + NSArray *logs = [[RadarLogBuffer sharedInstance]flushableLogs]; + XCTAssertEqual(logs.count, 351); + XCTAssertEqualObjects(logs.firstObject.message, @"message_250"); + XCTAssertEqualObjects(logs.lastObject.message, @"----- purged oldest logs -----"); + [[RadarLogBuffer sharedInstance]clearBuffer]; +} + + @end