diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de6de73ac8..e7c9bae6276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Store breadcrumbs to disk for OOM events (#2347) - Report pre-warmed app starts (#1969) ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 300f34bebbd..75695df6b91 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 0A1B497328E597DD00D7BFA3 /* TestLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1B497228E597DD00D7BFA3 /* TestLogOutput.swift */; }; 0A1C3592287D7107007D01E3 /* SentryMetaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1C3591287D7107007D01E3 /* SentryMetaTests.swift */; }; 0A2690B72885C2E000E4432D /* TestSentryPermissionsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AABE2EF2885C2120057ED69 /* TestSentryPermissionsObserver.swift */; }; + 0A2D7BBA29152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */; }; 0A283E79291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A283E78291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift */; }; 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D8D5A289815C0008720F6 /* SentryBaseIntegration.m */; }; 0A2D8D5D289815EB008720F6 /* SentryBaseIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */; }; @@ -52,6 +53,8 @@ 0A5370A128A3EC2400B2DCDE /* SentryViewHierarchyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5370A028A3EC2400B2DCDE /* SentryViewHierarchyTests.swift */; }; 0A56DA5F28ABA01B00C400D5 /* SentryTransactionContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A56DA5E28ABA01B00C400D5 /* SentryTransactionContext+Private.h */; }; 0A6EEADD28A657970076B469 /* UIViewRecursiveDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6EEADC28A657970076B469 /* UIViewRecursiveDescriptionTests.swift */; }; + 0A80E433291017C300095219 /* SentryOutOfMemoryScopeObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A80E432291017C300095219 /* SentryOutOfMemoryScopeObserver.m */; }; + 0A80E435291017D500095219 /* SentryOutOfMemoryScopeObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A80E434291017D500095219 /* SentryOutOfMemoryScopeObserver.h */; }; 0A8F0A392886CC70000B15F6 /* SentryPermissionsObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 0AABE2EE288592750057ED69 /* SentryPermissionsObserver.h */; }; 0A94158228F6C4C2006A5DD1 /* SentryAppStateManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A94158128F6C4C2006A5DD1 /* SentryAppStateManagerTests.swift */; }; 0A9415BA28F96CAC006A5DD1 /* TestSentryReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9415B928F96CAC006A5DD1 /* TestSentryReachability.swift */; }; @@ -770,6 +773,7 @@ 03F9D37B2819A65C00602916 /* SentryProfilerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryProfilerTests.mm; sourceTree = ""; }; 0A1B497228E597DD00D7BFA3 /* TestLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLogOutput.swift; sourceTree = ""; }; 0A1C3591287D7107007D01E3 /* SentryMetaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetaTests.swift; sourceTree = ""; }; + 0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOutOfMemoryScopeObserverTests.swift; sourceTree = ""; }; 0A283E78291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIDeviceWrapperTests.swift; sourceTree = ""; }; 0A2D8D5A289815C0008720F6 /* SentryBaseIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBaseIntegration.m; sourceTree = ""; }; 0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBaseIntegration.h; path = include/SentryBaseIntegration.h; sourceTree = ""; }; @@ -782,6 +786,8 @@ 0A5370A028A3EC2400B2DCDE /* SentryViewHierarchyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewHierarchyTests.swift; sourceTree = ""; }; 0A56DA5E28ABA01B00C400D5 /* SentryTransactionContext+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryTransactionContext+Private.h"; path = "include/SentryTransactionContext+Private.h"; sourceTree = ""; }; 0A6EEADC28A657970076B469 /* UIViewRecursiveDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewRecursiveDescriptionTests.swift; sourceTree = ""; }; + 0A80E432291017C300095219 /* SentryOutOfMemoryScopeObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryOutOfMemoryScopeObserver.m; sourceTree = ""; }; + 0A80E434291017D500095219 /* SentryOutOfMemoryScopeObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryOutOfMemoryScopeObserver.h; path = include/SentryOutOfMemoryScopeObserver.h; sourceTree = ""; }; 0A94158128F6C4C2006A5DD1 /* SentryAppStateManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAppStateManagerTests.swift; sourceTree = ""; }; 0A9415B928F96CAC006A5DD1 /* TestSentryReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryReachability.swift; sourceTree = ""; }; 0A9BF4E128A114940068D266 /* SentryViewHierarchyIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryViewHierarchyIntegration.m; sourceTree = ""; }; @@ -2503,6 +2509,8 @@ 7B6C5F8626034395007F7DFF /* SentryOutOfMemoryLogic.m */, 7B98D7CA25FB64EC00C5A389 /* SentryOutOfMemoryTrackingIntegration.h */, 7B98D7CE25FB650F00C5A389 /* SentryOutOfMemoryTrackingIntegration.m */, + 0A80E434291017D500095219 /* SentryOutOfMemoryScopeObserver.h */, + 0A80E432291017C300095219 /* SentryOutOfMemoryScopeObserver.m */, ); name = OutOfMemory; sourceTree = ""; @@ -2625,6 +2633,7 @@ children = ( 7B98D7DF25FB73B900C5A389 /* SentryOutOfMemoryTrackerTests.swift */, 7BFE7A0927A1B6B000D2B66E /* SentryOutOfMemoryIntegrationTests.swift */, + 0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */, ); path = OutOfMemory; sourceTree = ""; @@ -3195,6 +3204,7 @@ 7B42C48027E08F33009B58C2 /* SentryDependencyContainer.h in Headers */, 6334314120AD9AE40077E581 /* SentryMechanism.h in Headers */, 03F84D2827DD414C008FE43F /* SentryCPU.h in Headers */, + 0A80E435291017D500095219 /* SentryOutOfMemoryScopeObserver.h in Headers */, 7B610D642512399600B0B5D9 /* SentryHub+Private.h in Headers */, D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */, 639FCF9C1EBC7F9500778193 /* SentryThread.h in Headers */, @@ -3416,6 +3426,7 @@ 84A8891D28DBD28900C51DFD /* SentryDevice.mm in Sources */, 7B56D73324616D9500B842DA /* SentryConcurrentRateLimitsDictionary.m in Sources */, 8ECC674825C23A20000E2BF6 /* SentryTransaction.m in Sources */, + 0A80E433291017C300095219 /* SentryOutOfMemoryScopeObserver.m in Sources */, 7BECF42826145CD900D9826E /* SentryMechanismMeta.m in Sources */, 8E7C982F2693D56000E6336C /* SentryTraceHeader.m in Sources */, 63FE715F20DA4C1100CDBAE8 /* SentryCrashID.c in Sources */, @@ -3693,6 +3704,7 @@ 7B30B68226527C55006B2752 /* TestDisplayLinkWrapper.swift in Sources */, 7BB6550D253EEB3900887E87 /* SentryUserFeedbackTests.swift in Sources */, 7BBD18B7245180FF00427C76 /* SentryDsnTests.m in Sources */, + 0A2D7BBA29152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift in Sources */, 7BD4BD4B27EB2DC20071F4FF /* SentryDiscardedEventTests.swift in Sources */, 63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m in Sources */, 7B88F30424BC8E6500ADF90A /* SentrySerializationTests.swift in Sources */, diff --git a/Sources/Sentry/SentryCrashScopeObserver.m b/Sources/Sentry/SentryCrashScopeObserver.m index 2c609667794..5395c93d438 100644 --- a/Sources/Sentry/SentryCrashScopeObserver.m +++ b/Sources/Sentry/SentryCrashScopeObserver.m @@ -9,11 +9,6 @@ #import #import -@interface -SentryCrashScopeObserver () - -@end - @implementation SentryCrashScopeObserver - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs @@ -90,10 +85,9 @@ - (void)setLevel:(enum SentryLevel)level sentrycrash_scopesync_setLevel([json bytes]); } -- (void)addBreadcrumb:(SentryBreadcrumb *)crumb +- (void)addSerializedBreadcrumb:(NSDictionary *)crumb { - NSDictionary *serialized = [crumb serialize]; - NSData *json = [self toJSONEncodedCString:serialized]; + NSData *json = [self toJSONEncodedCString:crumb]; if (json == nil) { return; } diff --git a/Sources/Sentry/SentryEvent.m b/Sources/Sentry/SentryEvent.m index 9c9416f0ff1..23154046e25 100644 --- a/Sources/Sentry/SentryEvent.m +++ b/Sources/Sentry/SentryEvent.m @@ -22,6 +22,14 @@ @property (nonatomic) BOOL isCrashEvent; +// We're storing serialized breadcrumbs to disk in JSON, and when we're reading them back (in +// the case of OOM), we end up with the serialized breadcrumbs again. Instead of turning those +// dictionaries into proper SentryBreadcrumb instances which then need to be serialized again in +// SentryEvent, we use this serializedBreadcrumbs property to set the pre-serialized +// breadcrumbs. It saves a LOT of work - especially turning an NSDictionary into a SentryBreadcrumb +// is silly when we're just going to do the opposite right after. +@property (nonatomic, strong) NSArray *serializedBreadcrumbs; + @end @implementation SentryEvent @@ -138,7 +146,13 @@ - (void)addSimpleProperties:(NSMutableDictionary *)serializedData [serializedData setValue:[self.stacktrace serialize] forKey:@"stacktrace"]; - [serializedData setValue:[self serializeBreadcrumbs] forKey:@"breadcrumbs"]; + NSMutableArray *breadcrumbs = [self serializeBreadcrumbs]; + if (self.serializedBreadcrumbs.count > 0) { + [breadcrumbs addObjectsFromArray:self.serializedBreadcrumbs]; + } + if (breadcrumbs.count > 0) { + [serializedData setValue:breadcrumbs forKey:@"breadcrumbs"]; + } [serializedData setValue:[self.context sentry_sanitize] forKey:@"contexts"]; @@ -164,15 +178,12 @@ - (void)addSimpleProperties:(NSMutableDictionary *)serializedData } } -- (NSArray *_Nullable)serializeBreadcrumbs +- (NSMutableArray *)serializeBreadcrumbs { NSMutableArray *crumbs = [NSMutableArray new]; for (SentryBreadcrumb *crumb in self.breadcrumbs) { [crumbs addObject:[crumb serialize]]; } - if (crumbs.count <= 0) { - return nil; - } return crumbs; } diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index a82eaa779de..daefd998bc9 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -28,6 +28,10 @@ @property (nonatomic, copy) NSString *lastInForegroundFilePath; @property (nonatomic, copy) NSString *previousAppStateFilePath; @property (nonatomic, copy) NSString *appStateFilePath; +@property (nonatomic, copy) NSString *previousBreadcrumbsFilePathOne; +@property (nonatomic, copy) NSString *previousBreadcrumbsFilePathTwo; +@property (nonatomic, copy) NSString *breadcrumbsFilePathOne; +@property (nonatomic, copy) NSString *breadcrumbsFilePathTwo; @property (nonatomic, copy) NSString *timezoneOffsetFilePath; @property (nonatomic, assign) NSUInteger currentFileCounter; @property (nonatomic, assign) NSUInteger maxEnvelopes; @@ -83,6 +87,14 @@ - (nullable instancetype)initWithOptions:(SentryOptions *)options self.previousAppStateFilePath = [self.sentryPath stringByAppendingPathComponent:@"previous.app.state"]; self.appStateFilePath = [self.sentryPath stringByAppendingPathComponent:@"app.state"]; + self.previousBreadcrumbsFilePathOne = + [self.sentryPath stringByAppendingPathComponent:@"previous.breadcrumbs.1.state"]; + self.previousBreadcrumbsFilePathTwo = + [self.sentryPath stringByAppendingPathComponent:@"previous.breadcrumbs.2.state"]; + self.breadcrumbsFilePathOne = + [self.sentryPath stringByAppendingPathComponent:@"breadcrumbs.1.state"]; + self.breadcrumbsFilePathTwo = + [self.sentryPath stringByAppendingPathComponent:@"breadcrumbs.2.state"]; self.timezoneOffsetFilePath = [self.sentryPath stringByAppendingPathComponent:@"timezone.offset"]; @@ -240,8 +252,12 @@ - (BOOL)removeFileAtPath:(NSString *)path NSError *error = nil; @synchronized(self) { [fileManager removeItemAtPath:path error:&error]; + if (nil != error) { - SENTRY_LOG_ERROR(@"Couldn't delete file %@: %@", path, error); + // We don't want to log an error if the file doesn't exist. + if (error.code != NSFileNoSuchFileError) { + SENTRY_LOG_ERROR(@"Couldn't delete file %@: %@", path, error); + } return NO; } } @@ -455,26 +471,90 @@ - (void)storeAppState:(SentryAppState *)appState - (void)moveAppStateToPreviousAppState { @synchronized(self.appStateFilePath) { - NSFileManager *fileManager = [NSFileManager defaultManager]; + [self moveState:self.appStateFilePath toPreviousState:self.previousAppStateFilePath]; + } +} - // We first need to remove the old previous app state file, - // or we can't move the current app state file to it. - [self removeFileAtPath:self.previousAppStateFilePath]; +- (void)moveBreadcrumbsToPreviousBreadcrumbs +{ + @synchronized(self.breadcrumbsFilePathOne) { + [self moveState:self.breadcrumbsFilePathOne + toPreviousState:self.previousBreadcrumbsFilePathOne]; + [self moveState:self.breadcrumbsFilePathTwo + toPreviousState:self.previousBreadcrumbsFilePathTwo]; + } +} - NSError *error = nil; - [fileManager moveItemAtPath:self.appStateFilePath - toPath:self.previousAppStateFilePath - error:&error]; +- (void)moveState:(NSString *)stateFilePath toPreviousState:(NSString *)previousStateFilePath +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; - // We don't want to log an error if the file doesn't exist. - if (nil != error && error.code != NSFileNoSuchFileError) { - [SentryLog - logWithMessage:[NSString - stringWithFormat: - @"Failed to move app state to previous app state: %@", error] - andLevel:kSentryLevelError]; + // We first need to remove the old previous state file, + // or we can't move the current state file to it. + [self removeFileAtPath:previousStateFilePath]; + + NSError *error = nil; + [fileManager moveItemAtPath:stateFilePath toPath:previousStateFilePath error:&error]; + + // We don't want to log an error if the file doesn't exist. + if (nil != error && error.code != NSFileNoSuchFileError) { + SENTRY_LOG_ERROR(@"Failed to move %@ to previous state file: %@", stateFilePath, error); + } +} + +- (NSArray *)readPreviousBreadcrumbs +{ + NSArray *fileOneLines = @[]; + NSArray *fileTwoLines = @[]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:self.previousBreadcrumbsFilePathOne]) { + NSString *fileContents = + [NSString stringWithContentsOfFile:self.previousBreadcrumbsFilePathOne + encoding:NSUTF8StringEncoding + error:nil]; + fileOneLines = [fileContents + componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + } + + if ([[NSFileManager defaultManager] fileExistsAtPath:self.previousBreadcrumbsFilePathTwo]) { + NSString *fileContents = + [NSString stringWithContentsOfFile:self.previousBreadcrumbsFilePathTwo + encoding:NSUTF8StringEncoding + error:nil]; + fileTwoLines = [fileContents + componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + } + + NSMutableArray *breadcrumbs = [NSMutableArray array]; + + if (fileOneLines.count > 0 || fileTwoLines.count > 0) { + NSArray *combinedLines; + + if (fileOneLines.count > fileTwoLines.count) { + // If file one has more lines than file two, then file one contains the older crumbs, + // and thus needs to come first. + combinedLines = [fileOneLines arrayByAddingObjectsFromArray:fileTwoLines]; + } else { + combinedLines = [fileTwoLines arrayByAddingObjectsFromArray:fileOneLines]; + } + + for (NSString *line in combinedLines) { + NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding]; + + NSError *error; + NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&error]; + + if (error) { + SENTRY_LOG_ERROR(@"Error deserializing breadcrumb: %@", error); + } else { + [breadcrumbs addObject:dict]; + } } } + + return breadcrumbs; } - (SentryAppState *_Nullable)readAppState @@ -494,8 +574,7 @@ - (SentryAppState *_Nullable)readPreviousAppState - (SentryAppState *_Nullable)readAppStateFrom:(NSString *)path { NSFileManager *fileManager = [NSFileManager defaultManager]; - NSData *currentData = nil; - currentData = [fileManager contentsAtPath:path]; + NSData *currentData = [fileManager contentsAtPath:path]; if (nil == currentData) { return nil; } diff --git a/Sources/Sentry/SentryOutOfMemoryScopeObserver.m b/Sources/Sentry/SentryOutOfMemoryScopeObserver.m new file mode 100644 index 00000000000..713202fc9f5 --- /dev/null +++ b/Sources/Sentry/SentryOutOfMemoryScopeObserver.m @@ -0,0 +1,155 @@ +#import "SentryOutOfMemoryScopeObserver.h" +#import +#import +#import + +@interface +SentryOutOfMemoryScopeObserver () + +@property (strong, nonatomic) SentryFileManager *fileManager; +@property (strong, nonatomic) NSFileHandle *fileHandle; +@property (nonatomic) NSInteger maxBreadcrumbs; +@property (nonatomic) NSInteger breadcrumbCounter; +@property (strong, nonatomic) NSString *activeFilePath; + +@end + +@implementation SentryOutOfMemoryScopeObserver + +- (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs + fileManager:(SentryFileManager *)fileManager +{ + if (self = [super init]) { + self.maxBreadcrumbs = maxBreadcrumbs; + self.fileManager = fileManager; + self.breadcrumbCounter = 0; + + [self switchFileHandle]; + } + + return self; +} + +- (void)dealloc +{ + [self.fileHandle closeFile]; +} + +// PRAGMA MARK: - Helper methods + +- (void)deleteFiles +{ + [self.fileHandle closeFile]; + self.fileHandle = nil; + self.activeFilePath = nil; + self.breadcrumbCounter = 0; + + [self.fileManager removeFileAtPath:self.fileManager.breadcrumbsFilePathOne]; + [self.fileManager removeFileAtPath:self.fileManager.breadcrumbsFilePathTwo]; +} + +- (void)switchFileHandle +{ + if ([self.activeFilePath isEqualToString:self.fileManager.breadcrumbsFilePathOne]) { + self.activeFilePath = self.fileManager.breadcrumbsFilePathTwo; + } else { + self.activeFilePath = self.fileManager.breadcrumbsFilePathOne; + } + + // Close the current filehandle (if any) + [self.fileHandle closeFile]; + + // Create a fresh file for the new active path + [self.fileManager removeFileAtPath:self.activeFilePath]; + [[NSFileManager defaultManager] createFileAtPath:self.activeFilePath + contents:nil + attributes:nil]; + + // Open the file for writing + self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.activeFilePath]; + + if (!self.fileHandle) { + SENTRY_LOG_ERROR(@"Couldn't open file handle for %@", self.activeFilePath); + } +} + +- (void)store:(NSData *)data +{ + [self.fileHandle seekToEndOfFile]; + [self.fileHandle writeData:data]; + [self.fileHandle writeData:[@"\n" dataUsingEncoding:NSASCIIStringEncoding]]; + + self.breadcrumbCounter += 1; + + if (self.breadcrumbCounter >= self.maxBreadcrumbs) { + [self switchFileHandle]; + self.breadcrumbCounter = 0; + } +} + +// PRAGMA MARK: - SentryScopeObserver + +- (void)addSerializedBreadcrumb:(NSDictionary *)crumb +{ + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:crumb options:0 error:&error]; + + if (error) { + SENTRY_LOG_ERROR(@"Error serializing breadcrumb: %@", error); + } else { + [self store:jsonData]; + } +} + +- (void)clear +{ + [self clearBreadcrumbs]; +} + +- (void)clearBreadcrumbs +{ + [self deleteFiles]; + [self switchFileHandle]; +} + +- (void)setContext:(nullable NSDictionary *)context +{ + // Left blank on purpose +} + +- (void)setDist:(nullable NSString *)dist +{ + // Left blank on purpose +} + +- (void)setEnvironment:(nullable NSString *)environment +{ + // Left blank on purpose +} + +- (void)setExtras:(nullable NSDictionary *)extras +{ + // Left blank on purpose +} + +- (void)setFingerprint:(nullable NSArray *)fingerprint +{ + // Left blank on purpose +} + +- (void)setLevel:(enum SentryLevel)level +{ + // Left blank on purpose +} + +- (void)setTags:(nullable NSDictionary *)tags +{ + // Left blank on purpose +} + +- (void)setUser:(nullable SentryUser *)user +{ + // Left blank on purpose +} + +@end diff --git a/Sources/Sentry/SentryOutOfMemoryTracker.m b/Sources/Sentry/SentryOutOfMemoryTracker.m index 65b9b372774..5aa953956d9 100644 --- a/Sources/Sentry/SentryOutOfMemoryTracker.m +++ b/Sources/Sentry/SentryOutOfMemoryTracker.m @@ -1,10 +1,10 @@ +#import "SentryEvent+Private.h" #import "SentryFileManager.h" #import #import #import #import #import -#import #import #import #import @@ -59,6 +59,15 @@ - (void)start // Set to empty list so no breadcrumbs of the current scope are added event.breadcrumbs = @[]; + // Load the previous breascrumbs from disk, which are already serialized + event.serializedBreadcrumbs = [self.fileManager readPreviousBreadcrumbs]; + if (event.serializedBreadcrumbs.count > self.options.maxBreadcrumbs) { + event.serializedBreadcrumbs = [event.serializedBreadcrumbs + subarrayWithRange:NSMakeRange(event.serializedBreadcrumbs.count + - self.options.maxBreadcrumbs, + self.options.maxBreadcrumbs)]; + } + SentryException *exception = [[SentryException alloc] initWithValue:SentryOutOfMemoryExceptionValue type:SentryOutOfMemoryExceptionType]; diff --git a/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m b/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m index d83de13656d..e8854f26232 100644 --- a/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m +++ b/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m @@ -1,4 +1,5 @@ #import "SentryDefines.h" +#import "SentryScope+Private.h" #import #import #import @@ -8,6 +9,7 @@ #import #import #import +#import #import #import #import @@ -65,6 +67,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options appStateManager:appStateManager dispatchQueueWrapper:dispatchQueueWrapper fileManager:fileManager]; + [self.tracker start]; self.anrTracker = @@ -73,6 +76,13 @@ - (BOOL)installWithOptions:(SentryOptions *)options self.appStateManager = appStateManager; + SentryOutOfMemoryScopeObserver *scopeObserver = [[SentryOutOfMemoryScopeObserver alloc] + initWithMaxBreadcrumbs:options.maxBreadcrumbs + fileManager:[[[SentrySDK currentHub] getClient] fileManager]]; + + [SentrySDK.currentHub configureScope:^( + SentryScope *_Nonnull outerScope) { [outerScope addObserver:scopeObserver]; }]; + return YES; } diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index ad9b925b709..e08b87dac1a 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -149,6 +149,7 @@ + (void)startWithOptionsObject:(SentryOptions *)options SentryClient *newClient = [[SentryClient alloc] initWithOptions:options]; [newClient.fileManager moveAppStateToPreviousAppState]; + [newClient.fileManager moveBreadcrumbsToPreviousBreadcrumbs]; // The Hub needs to be initialized with a client so that closing a session // can happen. diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index df43faaf00b..30612828c46 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -134,7 +134,7 @@ - (void)addBreadcrumb:(SentryBreadcrumb *)crumb } for (id observer in self.observers) { - [observer addBreadcrumb:crumb]; + [observer addSerializedBreadcrumb:[crumb serialize]]; } } } diff --git a/Sources/Sentry/include/SentryEvent+Private.h b/Sources/Sentry/include/SentryEvent+Private.h index 734f1e5aeb8..f750acc84ef 100644 --- a/Sources/Sentry/include/SentryEvent+Private.h +++ b/Sources/Sentry/include/SentryEvent+Private.h @@ -8,5 +8,6 @@ SentryEvent (Private) * This indicates whether this event is a result of a crash. */ @property (nonatomic) BOOL isCrashEvent; +@property (nonatomic, strong) NSArray *serializedBreadcrumbs; @end diff --git a/Sources/Sentry/include/SentryFileManager.h b/Sources/Sentry/include/SentryFileManager.h index 44548397295..e53edf6c2c1 100644 --- a/Sources/Sentry/include/SentryFileManager.h +++ b/Sources/Sentry/include/SentryFileManager.h @@ -15,6 +15,10 @@ NS_SWIFT_NAME(SentryFileManager) SENTRY_NO_INIT @property (nonatomic, readonly) NSString *sentryPath; +@property (nonatomic, readonly) NSString *breadcrumbsFilePathOne; +@property (nonatomic, readonly) NSString *breadcrumbsFilePathTwo; +@property (nonatomic, readonly) NSString *previousBreadcrumbsFilePathOne; +@property (nonatomic, readonly) NSString *previousBreadcrumbsFilePathTwo; - (nullable instancetype)initWithOptions:(SentryOptions *)options andCurrentDateProvider:(id)currentDateProvider @@ -70,6 +74,9 @@ SENTRY_NO_INIT - (SentryAppState *_Nullable)readPreviousAppState; - (void)deleteAppState; +- (void)moveBreadcrumbsToPreviousBreadcrumbs; +- (NSArray *)readPreviousBreadcrumbs; + - (NSNumber *_Nullable)readTimezoneOffset; - (void)storeTimezoneOffset:(NSInteger)offset; - (void)deleteTimezoneOffset; diff --git a/Sources/Sentry/include/SentryOutOfMemoryScopeObserver.h b/Sources/Sentry/include/SentryOutOfMemoryScopeObserver.h new file mode 100644 index 00000000000..da27299c823 --- /dev/null +++ b/Sources/Sentry/include/SentryOutOfMemoryScopeObserver.h @@ -0,0 +1,18 @@ +#import "SentryDefines.h" +#import "SentryScopeObserver.h" + +@class SentryFileManager; + +NS_ASSUME_NONNULL_BEGIN + +/// This scope observer is used by the Out of Memory integration to write breadcrumbs to disk. +/// The overhead is ~0.015 seconds for 1000 breadcrumbs. +@interface SentryOutOfMemoryScopeObserver : NSObject +SENTRY_NO_INIT + +- (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs + fileManager:(SentryFileManager *)fileManager; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryScopeObserver.h b/Sources/Sentry/include/SentryScopeObserver.h index 32aba7aff1a..a0fada9c772 100644 --- a/Sources/Sentry/include/SentryScopeObserver.h +++ b/Sources/Sentry/include/SentryScopeObserver.h @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setLevel:(enum SentryLevel)level; -- (void)addBreadcrumb:(SentryBreadcrumb *)crumb; +- (void)addSerializedBreadcrumb:(NSDictionary *)crumb; - (void)clearBreadcrumbs; diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index 6b3d833415a..083333a284b 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -577,6 +577,44 @@ class SentryFileManagerTests: XCTestCase { XCTAssertNotNil(sut.readTimezoneOffset()) } + func testReadPreviousBreadcrumbs() { + let observer = SentryOutOfMemoryScopeObserver(maxBreadcrumbs: 2, fileManager: sut) + + for count in 0..<3 { + let crumb = TestData.crumb + crumb.message = "\(count)" + let serializedBreadcrumb = crumb.serialize() + + observer.addSerializedBreadcrumb(serializedBreadcrumb) + } + + sut.moveBreadcrumbsToPreviousBreadcrumbs() + let result = sut.readPreviousBreadcrumbs() + XCTAssertEqual(result.count, 3) + XCTAssertEqual((result[0] as! NSDictionary)["message"] as! String, "0") + XCTAssertEqual((result[1] as! NSDictionary)["message"] as! String, "1") + XCTAssertEqual((result[2] as! NSDictionary)["message"] as! String, "2") + } + + func testReadPreviousBreadcrumbsCorrectOrderWhenFileTwoHasMoreCrumbs() { + let observer = SentryOutOfMemoryScopeObserver(maxBreadcrumbs: 2, fileManager: sut) + + for count in 0..<5 { + let crumb = TestData.crumb + crumb.message = "\(count)" + let serializedBreadcrumb = crumb.serialize() + + observer.addSerializedBreadcrumb(serializedBreadcrumb) + } + + sut.moveBreadcrumbsToPreviousBreadcrumbs() + let result = sut.readPreviousBreadcrumbs() + XCTAssertEqual(result.count, 3) + XCTAssertEqual((result[0] as! NSDictionary)["message"] as! String, "2") + XCTAssertEqual((result[1] as! NSDictionary)["message"] as! String, "3") + XCTAssertEqual((result[2] as! NSDictionary)["message"] as! String, "4") + } + func testReadGarbageTimezoneOffset() throws { try "garbage".write(to: URL(fileURLWithPath: sut.timezoneOffsetFilePath), atomically: true, encoding: .utf8) XCTAssertNil(sut.readTimezoneOffset()) diff --git a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryScopeObserverTests.swift b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryScopeObserverTests.swift new file mode 100644 index 00000000000..c26e10ad1a4 --- /dev/null +++ b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryScopeObserverTests.swift @@ -0,0 +1,121 @@ +import XCTest + +class SentryOutOfMemoryScopeObserverTests: XCTestCase { + private class Fixture { + let breadcrumb: Breadcrumb + let options: Options + let fileManager: SentryFileManager + let currentDate = TestCurrentDateProvider() + + init() { + breadcrumb = TestData.crumb + breadcrumb.data = nil + + options = Options() + fileManager = try! SentryFileManager(options: options, andCurrentDateProvider: currentDate) + } + + func getSut() -> SentryOutOfMemoryScopeObserver { + return getSut(fileManager: self.fileManager) + } + + func getSut(fileManager: SentryFileManager) -> SentryOutOfMemoryScopeObserver { + return SentryOutOfMemoryScopeObserver(maxBreadcrumbs: 10, fileManager: fileManager) + } + } + + private var fixture: Fixture! + private var sut: SentryOutOfMemoryScopeObserver! + + override func setUp() { + super.setUp() + + fixture = Fixture() + sut = fixture.getSut() + } + + override func tearDown() { + super.tearDown() + fixture.fileManager.deleteAllFolders() + } + + // Test that we're storing the serialized breadcrumb in a proper JSON string + func testStoreBreadcrumb() throws { + let breadcrumb = fixture.breadcrumb.serialize() as! [String: String] + + sut.addSerializedBreadcrumb(breadcrumb) + + let fileOneContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathOne) + let firstLine = String(fileOneContents.split(separator: "\n").first!) + let dict = try JSONSerialization.jsonObject(with: firstLine.data(using: .utf8)!) as! [String: String] + + XCTAssertEqual(dict, breadcrumb) + } + + func testStoreInMultipleFiles() throws { + let breadcrumb = fixture.breadcrumb.serialize() + + for _ in 0..<9 { + sut.addSerializedBreadcrumb(breadcrumb) + } + + var fileOneContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathOne) + var fileOneLines = fileOneContents.split(separator: "\n") + XCTAssertEqual(fileOneLines.count, 9) + + XCTAssertFalse(FileManager.default.fileExists(atPath: fixture.fileManager.breadcrumbsFilePathTwo)) + + // Now store one more, which means it'll change over to the second file (which should be empty) + sut.addSerializedBreadcrumb(breadcrumb) + + fileOneContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathOne) + fileOneLines = fileOneContents.split(separator: "\n") + XCTAssertEqual(fileOneLines.count, 10) + + var fileTwoContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathTwo) + XCTAssertEqual(fileTwoContents, "") + + // Next one will be stored in the second file + sut.addSerializedBreadcrumb(breadcrumb) + + fileTwoContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathTwo) + var fileTwoLines = fileTwoContents.split(separator: "\n") + + XCTAssertEqual(fileOneLines.count, 10) + XCTAssertEqual(fileTwoLines.count, 1) + + // Store 10 more + for _ in 0..<10 { + sut.addSerializedBreadcrumb(breadcrumb) + } + + fileOneContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathOne) + fileOneLines = fileOneContents.split(separator: "\n") + XCTAssertEqual(fileOneLines.count, 1) + + fileTwoContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathTwo) + fileTwoLines = fileTwoContents.split(separator: "\n") + XCTAssertEqual(fileTwoLines.count, 10) + } + + func testClearBreadcrumbs() throws { + let breadcrumb = fixture.breadcrumb.serialize() + + for _ in 0..<15 { + sut.addSerializedBreadcrumb(breadcrumb) + } + + var fileOneContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathOne) + XCTAssertEqual(fileOneContents.count, 1_210) + + let fileTwoContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathTwo) + XCTAssertEqual(fileTwoContents.count, 605) + + sut.clearBreadcrumbs() + + fileOneContents = try String(contentsOfFile: fixture.fileManager.breadcrumbsFilePathOne) + XCTAssertEqual(fileOneContents.count, 0) + + XCTAssertFalse(FileManager.default.fileExists(atPath: fixture.fileManager.breadcrumbsFilePathTwo)) + } +} diff --git a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryTrackerTests.swift b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryTrackerTests.swift index fa72aeacfaf..686ae66da3a 100644 --- a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryTrackerTests.swift +++ b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryTrackerTests.swift @@ -18,6 +18,7 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase { init() { options = Options() + options.maxBreadcrumbs = 2 options.dsn = SentryOutOfMemoryTrackerTests.dsnAsString options.releaseName = TestData.appState.releaseName @@ -195,7 +196,7 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase { sut.start() assertOOMEventSent() } - + func testANR_NoOOM() { sut.start() goToForeground() @@ -207,7 +208,25 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase { sut.start() assertNoOOMSent() } - + + func testAppOOM_WithBreadcrumbs() { + let breadcrumb = TestData.crumb + + let sentryOutOfMemoryScopeObserver = SentryOutOfMemoryScopeObserver(maxBreadcrumbs: Int(fixture.options.maxBreadcrumbs), fileManager: fixture.fileManager) + + for _ in 0..<3 { + sentryOutOfMemoryScopeObserver.addSerializedBreadcrumb(breadcrumb.serialize()) + } + + sut.start() + goToForeground() + + fixture.fileManager.moveAppStateToPreviousAppState() + fixture.fileManager.moveBreadcrumbsToPreviousBreadcrumbs() + sut.start() + assertOOMEventSent(expectedBreadcrumbs: 2) + } + func testAppOOM_WithOnlyHybridSdkDidBecomeActive() { sut.start() hybridSdkDidBecomeActive() @@ -281,12 +300,13 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase { } } - private func assertOOMEventSent() { + private func assertOOMEventSent(expectedBreadcrumbs: Int = 0) { XCTAssertEqual(1, fixture.client.captureCrashEventInvocations.count) let crashEvent = fixture.client.captureCrashEventInvocations.first?.event XCTAssertEqual(SentryLevel.fatal, crashEvent?.level) - XCTAssertEqual([], crashEvent?.breadcrumbs) + XCTAssertEqual(crashEvent?.breadcrumbs?.count, 0) + XCTAssertEqual(crashEvent?.serializedBreadcrumbs?.count, expectedBreadcrumbs) XCTAssertEqual(1, crashEvent?.exceptions?.count) @@ -298,7 +318,7 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase { XCTAssertEqual(false, exception?.mechanism?.handled) XCTAssertEqual("out_of_memory", exception?.mechanism?.type) } - + private func assertNoOOMSent() { XCTAssertEqual(0, fixture.client.captureCrashEventInvocations.count) } diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift index a2ba1621253..79442d79a44 100644 --- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift +++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashScopeObserverTests.swift @@ -204,7 +204,7 @@ class SentryCrashScopeObserverTests: XCTestCase { func testAddCrumb() { let sut = fixture.sut let crumb = TestData.crumb - sut.add(crumb) + sut.addSerializedBreadcrumb(crumb.serialize()) assertOneCrumbSetToScope(crumb: crumb) } @@ -216,14 +216,14 @@ class SentryCrashScopeObserverTests: XCTestCase { func testCallConfigureCrumbTwice() { let sut = fixture.sut let crumb = TestData.crumb - sut.add(crumb) + sut.addSerializedBreadcrumb(crumb.serialize()) sentrycrash_scopesync_configureBreadcrumbs(fixture.maxBreadcrumbs) let scope = sentrycrash_scopesync_getScope() XCTAssertEqual(0, scope?.pointee.currentCrumb) - sut.add(crumb) + sut.addSerializedBreadcrumb(crumb.serialize()) assertOneCrumbSetToScope(crumb: crumb) } @@ -234,7 +234,7 @@ class SentryCrashScopeObserverTests: XCTestCase { for i in 0...fixture.maxBreadcrumbs { let crumb = TestData.crumb crumb.message = "\(i)" - sut.add(crumb) + sut.addSerializedBreadcrumb(crumb.serialize()) crumbs.append(crumb) } crumbs.removeFirst() @@ -273,11 +273,11 @@ class SentryCrashScopeObserverTests: XCTestCase { sut.setExtras(fixture.extras) sut.setFingerprint(fixture.fingerprint) sut.setLevel(SentryLevel.fatal) - sut.add(TestData.crumb) - + sut.addSerializedBreadcrumb(TestData.crumb.serialize()) + sut.clear() - - assertEmptyScope() + + assertEmptyScope() } func testEmptyScope() { diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index 47fdc84948a..13d9ffe079a 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -536,6 +536,24 @@ class SentrySDKTests: XCTestCase { let timestamp = self.fixture.currentDate.date().addingTimeInterval(TimeInterval(amount)) XCTAssertEqual(timestamp, SentrySDK.getAppStartMeasurement()?.appStartTimestamp) } + + func testMovesBreadcrumbsToPreviousBreadcrumbs() throws { + let options = Options() + options.dsn = SentrySDKTests.dsnAsString + + let filemanager = try SentryFileManager(options: options, andCurrentDateProvider: TestCurrentDateProvider()) + let observer = SentryOutOfMemoryScopeObserver(maxBreadcrumbs: 10, fileManager: filemanager) + let serializedBreadcrumb = TestData.crumb.serialize() + + for _ in 0..<3 { + observer.addSerializedBreadcrumb(serializedBreadcrumb) + } + + SentrySDK.start(options: options) + + let result = filemanager.readPreviousBreadcrumbs() + XCTAssertEqual(result.count, 3) + } private func givenSdkWithHub() { SentrySDK.setCurrentHub(fixture.hub) diff --git a/Tests/SentryTests/SentryScopeSwiftTests.swift b/Tests/SentryTests/SentryScopeSwiftTests.swift index 9313805d8bf..263e1615693 100644 --- a/Tests/SentryTests/SentryScopeSwiftTests.swift +++ b/Tests/SentryTests/SentryScopeSwiftTests.swift @@ -484,7 +484,10 @@ class SentryScopeSwiftTests: XCTestCase { sut.add(crumb) sut.add(crumb) - XCTAssertEqual([crumb, crumb], observer.crumbs) + XCTAssertEqual( + [crumb.serialize() as! [String: AnyHashable], crumb.serialize() as! [String: AnyHashable]], + observer.crumbs + ) } func testScopeObserver_clearBreadcrumb() { @@ -545,11 +548,11 @@ class SentryScopeSwiftTests: XCTestCase { self.level = level } - var crumbs: [Breadcrumb] = [] - func add(_ crumb: Breadcrumb) { - crumbs.append(crumb) + var crumbs: [[String: AnyHashable]] = [] + func addSerializedBreadcrumb(_ crumb: [String: Any]) { + crumbs.append(crumb as! [String: AnyHashable]) } - + var clearBreadcrumbInvocations = 0 func clearBreadcrumbs() { clearBreadcrumbInvocations += 1 diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 919d5b32586..b9c0e8b2309 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -112,6 +112,7 @@ #import "SentryObjCRuntimeWrapper.h" #import "SentryOptions+Private.h" #import "SentryOutOfMemoryLogic.h" +#import "SentryOutOfMemoryScopeObserver.h" #import "SentryOutOfMemoryTracker.h" #import "SentryOutOfMemoryTrackingIntegration.h" #import "SentryPerformanceTracker.h" diff --git a/develop-docs/README.md b/develop-docs/README.md index d3ddb94d7f8..9a5110c50f9 100644 --- a/develop-docs/README.md +++ b/develop-docs/README.md @@ -127,7 +127,7 @@ Date: October 21st 2022 Contributors: @philipphofmann GH actions will remove the macOS-10.15 image, which contains an iOS 12 simulator on 12/1/22; see https://github.com/actions/runner-images/issues/5583. -Neither the[ macOS-11](https://github.com/actions/runner-images/blob/main/images/macos/macos-11-Readme.md#installed-sdks) nor the +Neither the [macOS-11](https://github.com/actions/runner-images/blob/main/images/macos/macos-11-Readme.md#installed-sdks) nor the [macOS-12](https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#installed-sdks) image contains an iOS 12 simulator. GH [concluded](https://github.com/actions/runner-images/issues/551#issuecomment-788822538) to not add more pre-installed simulators. SauceLabs doesn't support running unit tests and adding another cloud solution as Firebase TestLab would increase the complexity of CI. Not running the unit tests on @@ -135,3 +135,10 @@ iOS 12 opens a risk of introducing bugs, which has already happened in the past, the iOS 12 simulator a try. Related to [GH-2218](https://github.com/getsentry/sentry-cocoa/issues/2218) + +### Writing breadcrumbs to disk in the main thread + +Date November 15, 2022 +Contributors: @kevinrenskers, @brustolin and @philipphofmann + +For the benefit of OOM crashes, we write breadcrumbs to disk; see https://github.com/getsentry/sentry-cocoa/pull/2347. We have decided to do this in the main thread to ensure we're not missing out on any breadcrumbs. It's mainly the last breadcrumb(s) that are important to figure out what is causing an OOM. And since we're only appending to an open file stream, the overhead is acceptable compared to the benefit of having accurate breadcrumbs.