Skip to content

Commit

Permalink
feat: Add screenshot at crash (#1920)
Browse files Browse the repository at this point in the history
Co-authored-by: Philipp Hofmann <philipp.hofmann@sentry.io>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
3 people committed Jul 6, 2022
1 parent 4f00e3e commit b711c79
Show file tree
Hide file tree
Showing 24 changed files with 452 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Add screenshot at crash (#1920)
- Track timezone changes as breadcrumbs (#1930)

## 7.19.0
Expand Down
8 changes: 8 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@
A811D867248E2770008A41EA /* SentrySystemEventBreadcrumbsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A811D866248E2770008A41EA /* SentrySystemEventBreadcrumbsTest.swift */; };
A839D89824864B80003B7AFD /* SentrySystemEventBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = A839D89724864B80003B7AFD /* SentrySystemEventBreadcrumbs.h */; };
A839D89A24864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = A839D89924864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m */; };
D8019910286B089000C277F0 /* SentryCrashReportSinkTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTest.swift */; };
D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */; };
D808FB8B281BCE96009A2A33 /* TestSentrySwizzleWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */; };
D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */; };
Expand All @@ -627,6 +628,7 @@
D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */; };
D8479328278873A100BE8E99 /* SentryByteCountFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = D8479327278873A100BE8E99 /* SentryByteCountFormatter.h */; };
D85596F3280580F10041FF8B /* SentryScreenshotIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D85596F1280580F10041FF8B /* SentryScreenshotIntegration.m */; };
D855AD62286ED6A4002573E1 /* SentryCrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D855AD61286ED6A4002573E1 /* SentryCrashTests.m */; };
D855B3E827D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D855B3E727D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift */; };
D855B3EA27D652C700BCED76 /* TestCoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D855B3E927D652C700BCED76 /* TestCoreDataStack.swift */; };
D85852B627ECEEDA00C6D8AE /* SentryScreenshot.m in Sources */ = {isa = PBXBuildFile; fileRef = D85852B427ECEEDA00C6D8AE /* SentryScreenshot.m */; };
Expand Down Expand Up @@ -1351,6 +1353,7 @@
A811D866248E2770008A41EA /* SentrySystemEventBreadcrumbsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySystemEventBreadcrumbsTest.swift; sourceTree = "<group>"; };
A839D89724864B80003B7AFD /* SentrySystemEventBreadcrumbs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySystemEventBreadcrumbs.h; path = include/SentrySystemEventBreadcrumbs.h; sourceTree = "<group>"; };
A839D89924864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySystemEventBreadcrumbs.m; sourceTree = "<group>"; };
D801990F286B089000C277F0 /* SentryCrashReportSinkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTest.swift; sourceTree = "<group>"; };
D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackerTests.swift; sourceTree = "<group>"; };
D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentrySwizzleWrapper.swift; sourceTree = "<group>"; };
D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackingIntegrationTests.swift; sourceTree = "<group>"; };
Expand All @@ -1362,6 +1365,7 @@
D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryByteCountFormatter.m; sourceTree = "<group>"; };
D8479327278873A100BE8E99 /* SentryByteCountFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryByteCountFormatter.h; path = include/SentryByteCountFormatter.h; sourceTree = "<group>"; };
D85596F1280580F10041FF8B /* SentryScreenshotIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshotIntegration.m; sourceTree = "<group>"; };
D855AD61286ED6A4002573E1 /* SentryCrashTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCrashTests.m; sourceTree = "<group>"; };
D855B3E727D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackingIntegrationTest.swift; sourceTree = "<group>"; };
D855B3E927D652C700BCED76 /* TestCoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCoreDataStack.swift; sourceTree = "<group>"; };
D85852B427ECEEDA00C6D8AE /* SentryScreenshot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshot.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2030,6 +2034,7 @@
63FE71F120DA66EA00CDBAE8 /* Container+DeepSearch_Tests.m */,
63FE71F620DA66EB00CDBAE8 /* FileBasedTestCase.h */,
63FE71D920DA66E700CDBAE8 /* FileBasedTestCase.m */,
D855AD61286ED6A4002573E1 /* SentryCrashTests.m */,
63FE71E220DA66E800CDBAE8 /* NSError+SimpleConstructor_Tests.m */,
63FE71D520DA66E600CDBAE8 /* RFC3339UTFString_Tests.m */,
63FE71E520DA66E800CDBAE8 /* SentryCrashCachedData_Tests.m */,
Expand Down Expand Up @@ -2069,6 +2074,7 @@
7B6D98EA24C6E84F005502FA /* SentryCrashInstallationReporterTests.swift */,
7B0A542D2521C62400A71716 /* SentryFrameRemoverTests.swift */,
7BBC827825DFD7D7005F1ED8 /* SentryInAppLogicTests.swift */,
D801990F286B089000C277F0 /* SentryCrashReportSinkTest.swift */,
7BED3574266F7BC600EAA70D /* TestSentryCrashWrapper.h */,
7BED3575266F7BFF00EAA70D /* TestSentryCrashWrapper.m */,
);
Expand Down Expand Up @@ -3382,6 +3388,7 @@
7B6C5ED6264E62CA0010D138 /* SentryTransactionTests.swift in Sources */,
D81FDF12280EA1060045E0E4 /* SentryScreenShotTests.swift in Sources */,
7BE3C7772445E50A00A38442 /* TestCurrentDateProvider.swift in Sources */,
D8019910286B089000C277F0 /* SentryCrashReportSinkTest.swift in Sources */,
D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */,
7BBD18992449DE9D00427C76 /* TestRateLimits.swift in Sources */,
8E4A038625F76A7600000D77 /* TypeMapping.swift in Sources */,
Expand Down Expand Up @@ -3419,6 +3426,7 @@
7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */,
7B4260342630315C00B36EDD /* SampleError.swift in Sources */,
D855B3E827D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift in Sources */,
D855AD62286ED6A4002573E1 /* SentryCrashTests.m in Sources */,
D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */,
7B16FD022654F86B008177D3 /* SentrySysctlTests.swift in Sources */,
7BAF3DB5243C743E008A5414 /* SentryClientTests.swift in Sources */,
Expand Down
1 change: 0 additions & 1 deletion Sources/Sentry/SentryCrashReportConverter.m
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ - (SentryEvent *_Nullable)convertReportToEvent
[NSString stringWithFormat:@"Could not convert report:%@", exception.description];
[SentryLog logWithMessage:errorMessage andLevel:kSentryLevelError];
}

return nil;
}

Expand Down
12 changes: 11 additions & 1 deletion Sources/Sentry/SentryCrashReportSink.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "SentryCrashReportSink.h"
#import "SentryAttachment.h"
#import "SentryClient.h"
#import "SentryCrash.h"
#import "SentryCrashReportConverter.h"
Expand All @@ -9,6 +10,7 @@
#import "SentryLog.h"
#import "SentrySDK+Private.h"
#import "SentrySDK.h"
#import "SentryScope.h"
#import "SentryThread.h"

@interface
Expand All @@ -33,7 +35,15 @@ - (void)handleConvertedEvent:(SentryEvent *)event
sentReports:(NSMutableArray *)sentReports
{
[sentReports addObject:report];
[SentrySDK captureCrashEvent:event];
SentryScope *scope = [[SentryScope alloc] initWithScope:SentrySDK.currentHub.scope];

if (report[SENTRYCRASH_REPORT_SCREENSHOT_ITEM]) {
for (NSString *ssPath in report[SENTRYCRASH_REPORT_SCREENSHOT_ITEM]) {
[scope addAttachment:[[SentryAttachment alloc] initWithPath:ssPath]];
}
}

[SentrySDK captureCrashEvent:event withScope:scope];
}

- (void)filterReports:(NSArray *)reports
Expand Down
11 changes: 8 additions & 3 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,18 @@ - (nullable SentrySession *)incrementSessionErrors
return sessionCopy;
}

- (void)captureCrashEvent:(SentryEvent *)event
{
[self captureCrashEvent:event withScope:self.scope];
}

/**
* If autoSessionTracking is enabled we want to send the crash and the event together to get proper
* numbers for release health statistics. If there are multiple crash events to be sent on the start
* of the SDK there is currently no way to know which one belongs to the crashed session so we just
* send the session with the first crashed event we receive.
*/
- (void)captureCrashEvent:(SentryEvent *)event
- (void)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope
{
event.isCrashEvent = YES;

Expand All @@ -226,13 +231,13 @@ - (void)captureCrashEvent:(SentryEvent *)event
// It can be that there is no session yet, because autoSessionTracking was just enabled and
// there is a previous crash on disk. In this case we just send the crash event.
if (nil != crashedSession) {
[client captureCrashEvent:event withSession:crashedSession withScope:self.scope];
[client captureCrashEvent:event withSession:crashedSession withScope:scope];
[fileManager deleteCrashedSession];
return;
}
}

[client captureCrashEvent:event withScope:self.scope];
[client captureCrashEvent:event withScope:scope];
}

- (SentryId *)captureTransaction:(SentryTransaction *)transaction withScope:(SentryScope *)scope
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentrySDK.m
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ + (void)captureCrashEvent:(SentryEvent *)event
[SentrySDK.currentHub captureCrashEvent:event];
}

+ (void)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope
{
[SentrySDK.currentHub captureCrashEvent:event withScope:scope];
}

+ (SentryId *)captureEvent:(SentryEvent *)event
{
return [SentrySDK captureEvent:event withScope:SentrySDK.currentHub.scope];
Expand Down
55 changes: 37 additions & 18 deletions Sources/Sentry/SentryScreenshot.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,9 @@ @implementation SentryScreenshot

- (NSArray<NSData *> *)appScreenshots
{
__block NSMutableArray *result;
__block NSArray *result;

void (^takeScreenShot)(void) = ^{
NSArray<UIWindow *> *windows =
[SentryDependencyContainer.sharedInstance.application windows];

result = [NSMutableArray arrayWithCapacity:windows.count];

for (UIWindow *window in windows) {
UIGraphicsBeginImageContext(window.frame.size);

if ([window drawViewHierarchyInRect:window.bounds afterScreenUpdates:false]) {
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
[result addObject:UIImagePNGRepresentation(img)];
}

UIGraphicsEndImageContext();
}
};
void (^takeScreenShot)(void) = ^{ result = [self takeScreenshots]; };

if ([NSThread isMainThread]) {
takeScreenShot();
Expand All @@ -38,6 +22,41 @@ @implementation SentryScreenshot
return result;
}

- (void)saveScreenShots:(NSString *)path
{
// This function does not dispatch the screenshot to the main thread.
// The caller should be aware of that.
// We did it this way because we use this function to save screenshots
// during signal handling, and if we dispatch it to the main thread,
// that is probably blocked by the crash event, we freeze the application.
[[self takeScreenshots] enumerateObjectsUsingBlock:^(NSData *obj, NSUInteger idx, BOOL *stop) {
NSString *name = idx == 0
? @"screenshot.png"
: [NSString stringWithFormat:@"screenshot-%li.png", (unsigned long)idx + 1];
NSString *fileName = [path stringByAppendingPathComponent:name];
[obj writeToFile:fileName atomically:YES];
}];
}

- (NSArray<NSData *> *)takeScreenshots
{
NSArray<UIWindow *> *windows = [SentryDependencyContainer.sharedInstance.application windows];

NSMutableArray *result = [NSMutableArray arrayWithCapacity:windows.count];

for (UIWindow *window in windows) {
UIGraphicsBeginImageContext(window.frame.size);

if ([window drawViewHierarchyInRect:window.bounds afterScreenUpdates:false]) {
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
[result addObject:UIImagePNGRepresentation(img)];
}

UIGraphicsEndImageContext();
}
return result;
}

@end

#endif
44 changes: 39 additions & 5 deletions Sources/Sentry/SentryScreenshotIntegration.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "SentryScreenshotIntegration.h"
#import "SentryAttachment.h"
#import "SentryClient+Private.h"
#import "SentryCrashC.h"
#import "SentryDependencyContainer.h"
#import "SentryEvent+Private.h"
#import "SentryEvent.h"
Expand All @@ -10,6 +11,35 @@
#import "SentrySDK+Private.h"

#if SENTRY_HAS_UIKIT

void
saveScreenShot(const char *path)
{
NSString *reportPath = [NSString stringWithUTF8String:path];
NSError *error = nil;

if (![NSFileManager.defaultManager fileExistsAtPath:reportPath]) {
[NSFileManager.defaultManager createDirectoryAtPath:reportPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (error != nil)
return;
} else {
// We first delete any screenshot that could be from an old crash report
NSArray *oldFiles = [NSFileManager.defaultManager contentsOfDirectoryAtPath:reportPath
error:&error];

if (!error) {
[oldFiles enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) {
[NSFileManager.defaultManager removeItemAtPath:obj error:nil];
}];
}
}

[SentryDependencyContainer.sharedInstance.screenshot saveScreenShots:reportPath];
}

@implementation SentryScreenshotIntegration

- (void)installWithOptions:(nonnull SentryOptions *)options
Expand All @@ -21,6 +51,13 @@ - (void)installWithOptions:(nonnull SentryOptions *)options

SentryClient *client = [SentrySDK.currentHub getClient];
client.attachmentProcessor = self;

sentrycrash_setSaveScreenshots(&saveScreenShot);
}

- (void)uninstall
{
sentrycrash_setSaveScreenshots(NULL);
}

- (NSArray<SentryAttachment *> *)processAttachments:(NSArray<SentryAttachment *> *)attachments
Expand All @@ -39,11 +76,8 @@ - (void)installWithOptions:(nonnull SentryOptions *)options
[result addObjectsFromArray:attachments];

for (int i = 0; i < screenshot.count; i++) {
NSString *name;
if (i == 0)
name = @"screenshot.png";
else
name = [NSString stringWithFormat:@"screenshot-%i.png", i + 1];
NSString *name
= i == 0 ? @"screenshot.png" : [NSString stringWithFormat:@"screenshot-%i.png", i + 1];

SentryAttachment *att = [[SentryAttachment alloc] initWithData:screenshot[i]
filename:name
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryHub+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ SentryHub (Private)

- (void)captureCrashEvent:(SentryEvent *)event;

- (void)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope;

- (void)setSampleRandomValue:(NSNumber *)value;

- (void)closeCachedSessionWithTimestamp:(NSDate *_Nullable)timestamp;
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentrySDK+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ SentrySDK (Private)

+ (void)captureCrashEvent:(SentryEvent *)event;

+ (void)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope;

/**
* SDK private field to store the state if onCrashedLastRun was called.
*/
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryScreenshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (nullable NSArray<NSData *> *)appScreenshots;

- (void)saveScreenShots:(NSString *)path;
@end

NS_ASSUME_NONNULL_END
Expand Down
2 changes: 2 additions & 0 deletions Sources/SentryCrash/Recording/SentryCrash.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ typedef enum {
SentryCrashCDeleteAlways
} SentryCrashCDeleteBehavior;

static NSString *const SENTRYCRASH_REPORT_SCREENSHOT_ITEM = @"screenshots";

/**
* Reports any crashes that occur in the application.
*
Expand Down
30 changes: 30 additions & 0 deletions Sources/SentryCrash/Recording/SentryCrash.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#import "SentryCrashMonitor_AppState.h"
#import "SentryCrashMonitor_System.h"
#import "SentryCrashReportFields.h"
#import "SentryCrashReportStore.h"
#import "SentryCrashSystemCapabilities.h"
#import <NSData+Sentry.h>

Expand Down Expand Up @@ -407,6 +408,28 @@ - (NSData *)loadCrashReportJSONWithID:(int64_t)reportID
return nil;
}

- (NSArray<NSString *> *)getScreenshotPaths:(int64_t)reportID
{
char report_screenshot_path[SentryCrashCRS_MAX_PATH_LENGTH];
sentrycrashcrs_getScreenshotPath_forReportId(reportID, report_screenshot_path);
NSString *path = [NSString stringWithUTF8String:report_screenshot_path];

BOOL isDir = false;
if (![NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir] || !isDir)
return @[];

NSArray *files = [NSFileManager.defaultManager contentsOfDirectoryAtPath:path error:nil];
if (files == nil)
return @[];

NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:files.count];
[files enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
[result addObject:[NSString stringWithFormat:@"%@/%@", path, obj]];
}];

return result;
}

- (void)doctorReport:(NSMutableDictionary *)report
{
NSMutableDictionary *crashReport = report[@SentryCrashField_Crash];
Expand Down Expand Up @@ -452,6 +475,7 @@ - (NSDictionary *)reportWithIntID:(int64_t)reportID
| SentryCrashJSONDecodeOptionIgnoreNullInObject
| SentryCrashJSONDecodeOptionKeepPartialObject
error:&error];

if (error != nil) {
SentryCrashLOG_ERROR(
@"Encountered error loading crash report %" PRIx64 ": %@", reportID, error);
Expand All @@ -460,6 +484,12 @@ - (NSDictionary *)reportWithIntID:(int64_t)reportID
SentryCrashLOG_ERROR(@"Could not load crash report");
return nil;
}

NSArray *screenShots = [self getScreenshotPaths:reportID];
if (screenShots.count > 0) {
crashReport[SENTRYCRASH_REPORT_SCREENSHOT_ITEM] = screenShots;
}

[self doctorReport:crashReport];

return crashReport;
Expand Down

0 comments on commit b711c79

Please sign in to comment.