Skip to content

Commit c8c2ac0

Browse files
Add screenshot capturing for Mac/iOS (#849)
* Add screen capturing utility for iOS * Rework screenshot capturing for Apple * Update snapshot * Update snapshot * FIx init params * Add screenshot attachment for crashes via envelope * Update changelog * Add screenshot for ensures on Mac * Add screenshot backup * Fix module getter that could cause ensure * Add check if screenshot exists * Add custom signal handler to capture screenshots on crash (iOS) * Update logging * Remove screenshot backup * Update plugin-dev/Source/Sentry/Public/SentrySettings.h Co-authored-by: Bruno Garcia <[email protected]> * Minor logging tweaks * Fix ensure handler for Mac * Rework screenshot uploading --------- Co-authored-by: Bruno Garcia <[email protected]>
1 parent 367cd4e commit c8c2ac0

14 files changed

+324
-10
lines changed

CHANGELOG.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add screenshot capturing for Mac/iOS ([#849](https://github.com/getsentry/sentry-unreal/pull/849))
8+
59
### Fixes
610

711
- Fix warnings caused by deprecated Cocoa SDK API usages ([#868](https://github.com/getsentry/sentry-unreal/pull/868))
812

913
### Dependencies
1014

11-
- Bump Java SDK (Android) from v8.6.0 to v8.7.0 ([#863](https://github.com/getsentry/sentry-unreal/pull/863))
12-
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#870)
13-
- [diff](https://github.com/getsentry/sentry-java/compare/8.6.0...8.7.0)
15+
- Bump Java SDK (Android) from v8.6.0 to v8.8.0 ([#863](https://github.com/getsentry/sentry-unreal/pull/863), [#869](https://github.com/getsentry/sentry-unreal/pull/869))
16+
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#880)
17+
- [diff](https://github.com/getsentry/sentry-java/compare/8.6.0...8.8.0)
1418
- Bump Cocoa SDK (iOS and Mac) from v8.48.0 to v8.49.0 ([#866](https://github.com/getsentry/sentry-unreal/pull/866))
1519
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8490)
1620
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.48.0...8.49.0)
17-
- Bump Java SDK (Android) from v8.7.0 to v8.8.0 ([#869](https://github.com/getsentry/sentry-unreal/pull/869))
18-
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#880)
19-
- [diff](https://github.com/getsentry/sentry-java/compare/8.7.0...8.8.0)
2021

2122
## 1.0.0-alpha.5
2223

plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp

+81
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828

2929
#include "GenericPlatform/GenericPlatformOutputDevices.h"
3030
#include "HAL/FileManager.h"
31+
#include "HAL/PlatformSentryAttachment.h"
32+
#include "Misc/CoreDelegates.h"
33+
#include "Misc/FileHelper.h"
34+
#include "Misc/Paths.h"
3135
#include "UObject/GarbageCollection.h"
3236
#include "UObject/UObjectThreadContext.h"
3337
#include "Utils/SentryLogUtils.h"
@@ -57,6 +61,7 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, US
5761
options.sampleRate = [NSNumber numberWithFloat:settings->SampleRate];
5862
options.maxBreadcrumbs = settings->MaxBreadcrumbs;
5963
options.sendDefaultPii = settings->SendDefaultPii;
64+
options.maxAttachmentSize = settings->MaxAttachmentSize;
6065
#if SENTRY_UIKIT_AVAILABLE
6166
options.attachScreenshot = settings->AttachScreenshot;
6267
#endif
@@ -68,6 +73,18 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, US
6873
}
6974
return scope;
7075
};
76+
options.onCrashedLastRun = ^(SentryEvent* event) {
77+
if (settings->AttachScreenshot)
78+
{
79+
// If a screenshot was captured during assertion/crash in the previous app run
80+
// find the most recent one and upload it to Sentry.
81+
const FString& screenshotPath = GetLatestScreenshot();
82+
if (!screenshotPath.IsEmpty())
83+
{
84+
UploadScreenshotForEvent(MakeShareable(new SentryIdApple(event.eventId)), screenshotPath);
85+
}
86+
}
87+
};
7188
options.beforeSend = ^SentryEvent* (SentryEvent* event) {
7289
if (FUObjectThreadContext::Get().IsRoutingPostLoad)
7390
{
@@ -371,3 +388,67 @@ TSharedPtr<ISentryTransactionContext> FAppleSentrySubsystem::ContinueTrace(const
371388

372389
return MakeShareable(new SentryTransactionContextApple(transactionContext));
373390
}
391+
392+
void FAppleSentrySubsystem::UploadScreenshotForEvent(TSharedPtr<ISentryId> eventId, const FString& screenshotPath) const
393+
{
394+
IFileManager& fileManager = IFileManager::Get();
395+
if (!fileManager.FileExists(*screenshotPath))
396+
{
397+
UE_LOG(LogSentrySdk, Error, TEXT("Failed to upload screenshot - path provided did not exist: %s"), *screenshotPath);
398+
return;
399+
}
400+
401+
const FString& screenshotFilePathExt = fileManager.ConvertToAbsolutePathForExternalAppForRead(*screenshotPath);
402+
403+
SentryAttachment* screenshotAttachment = [[SENTRY_APPLE_CLASS(SentryAttachment) alloc] initWithPath:screenshotFilePathExt.GetNSString() filename:@"screenshot.png"];
404+
405+
SentryOptions* options = [SENTRY_APPLE_CLASS(PrivateSentrySDKOnly) options];
406+
int32 size = options.maxAttachmentSize;
407+
408+
SentryEnvelopeItem* envelopeItem = [[SENTRY_APPLE_CLASS(SentryEnvelopeItem) alloc] initWithAttachment:screenshotAttachment maxAttachmentSize:size];
409+
410+
SentryId* id = StaticCastSharedPtr<SentryIdApple>(eventId)->GetNativeObject();
411+
412+
SentryEnvelope* envelope = [[SENTRY_APPLE_CLASS(SentryEnvelope) alloc] initWithId:id singleItem:envelopeItem];
413+
414+
[SENTRY_APPLE_CLASS(PrivateSentrySDKOnly) captureEnvelope:envelope];
415+
416+
// After uploading screenshot it's no longer needed so delete
417+
if (!fileManager.Delete(*screenshotPath))
418+
{
419+
UE_LOG(LogSentrySdk, Error, TEXT("Failed to delete screenshot: %s"), *screenshotPath);
420+
}
421+
}
422+
423+
FString FAppleSentrySubsystem::GetScreenshotPath() const
424+
{
425+
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryScreenshots"), FString::Printf(TEXT("screenshot-%s.png"), *FDateTime::Now().ToString()));
426+
}
427+
428+
FString FAppleSentrySubsystem::GetLatestScreenshot() const
429+
{
430+
const FString& ScreenshotsDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryScreenshots"));
431+
432+
TArray<FString> Screenshots;
433+
IFileManager::Get().FindFiles(Screenshots, *ScreenshotsDir, TEXT("*.png"));
434+
435+
if(Screenshots.Num() == 0)
436+
{
437+
UE_LOG(LogSentrySdk, Log, TEXT("There are no screenshots found."));
438+
return FString("");
439+
}
440+
441+
for (int i = 0; i < Screenshots.Num(); ++i)
442+
{
443+
Screenshots[i] = ScreenshotsDir / Screenshots[i];
444+
}
445+
446+
Screenshots.Sort([](const FString& A, const FString& B)
447+
{
448+
const FDateTime TimestampA = IFileManager::Get().GetTimeStamp(*A);
449+
const FDateTime TimestampB = IFileManager::Get().GetTimeStamp(*B);
450+
return TimestampB < TimestampA;
451+
});
452+
453+
return Screenshots[0];
454+
}

plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h

+8
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ class FAppleSentrySubsystem : public ISentrySubsystem
3434
virtual TSharedPtr<ISentryTransaction> StartTransactionWithContextAndTimestamp(TSharedPtr<ISentryTransactionContext> context, int64 timestamp) override;
3535
virtual TSharedPtr<ISentryTransaction> StartTransactionWithContextAndOptions(TSharedPtr<ISentryTransactionContext> context, const TMap<FString, FString>& options) override;
3636
virtual TSharedPtr<ISentryTransactionContext> ContinueTrace(const FString& sentryTrace, const TArray<FString>& baggageHeaders) override;
37+
38+
virtual FString TryCaptureScreenshot() const { return FString(); };
39+
40+
protected:
41+
void UploadScreenshotForEvent(TSharedPtr<ISentryId> eventId, const FString& screenshotPath) const;
42+
43+
virtual FString GetScreenshotPath() const;
44+
virtual FString GetLatestScreenshot() const;
3745
};

plugin-dev/Source/Sentry/Private/Apple/Convenience/SentryInclude.h

+2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
#if PLATFORM_MAC
1212
#include <Sentry/Sentry.h>
13+
#include <Sentry/SentryEnvelope.h>
1314
#include <Sentry/PrivateSentrySDKOnly.h>
1415
#include <Sentry/SentrySwift.h>
1516
#elif PLATFORM_IOS
1617
#import <Sentry/Sentry.h>
18+
#import <Sentry/SentryEnvelope.h>
1719
#import <Sentry/PrivateSentrySDKOnly.h>
1820
#import <Sentry/SentrySwift.h>
1921
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#include "IOS/IOSSentrySubsystem.h"
2+
3+
#include "IOS/IOSAppDelegate.h"
4+
5+
#include "SentryDefines.h"
6+
#include "SentrySettings.h"
7+
8+
#include "Misc/CoreDelegates.h"
9+
#include "Misc/FileHelper.h"
10+
#include "Misc/Paths.h"
11+
#include "Utils/SentryScreenshotUtils.h"
12+
13+
static FIOSSentrySubsystem* GIOSSentrySubsystem = nullptr;
14+
15+
struct sigaction DefaultSigIllHandler;
16+
struct sigaction DefaultSigEmtHandler;
17+
struct sigaction DefaultSigFpeHandler;
18+
struct sigaction DefaultSigBusHandler;
19+
struct sigaction DefaultSigSegvHandler;
20+
struct sigaction DefaultSigSysHandler;
21+
22+
void SaveDefaultSignalHandlers()
23+
{
24+
sigaction(SIGILL, NULL, &DefaultSigIllHandler);
25+
sigaction(SIGEMT, NULL, &DefaultSigEmtHandler);
26+
sigaction(SIGFPE, NULL, &DefaultSigFpeHandler);
27+
sigaction(SIGBUS, NULL, &DefaultSigBusHandler);
28+
sigaction(SIGSEGV, NULL, &DefaultSigSegvHandler);
29+
sigaction(SIGSYS, NULL, &DefaultSigSysHandler);
30+
}
31+
32+
void RestoreDefaultSignalHandlers()
33+
{
34+
sigaction(SIGILL, &DefaultSigIllHandler, NULL);
35+
sigaction(SIGEMT, &DefaultSigEmtHandler, NULL);
36+
sigaction(SIGFPE, &DefaultSigFpeHandler, NULL);
37+
sigaction(SIGBUS, &DefaultSigBusHandler, NULL);
38+
sigaction(SIGSEGV, &DefaultSigSegvHandler, NULL);
39+
sigaction(SIGSYS, &DefaultSigSysHandler, NULL);
40+
}
41+
42+
static void IOSSentrySignalHandler(int Signal, siginfo_t *Info, void *Context)
43+
{
44+
if (GIOSSentrySubsystem && GIOSSentrySubsystem->IsEnabled())
45+
{
46+
GIOSSentrySubsystem->TryCaptureScreenshot();
47+
}
48+
49+
RestoreDefaultSignalHandlers();
50+
51+
// Re-raise signal to default handler
52+
raise(Signal);
53+
}
54+
55+
void InstallSentrySignalHandler()
56+
{
57+
struct sigaction Action;
58+
memset(&Action, 0, sizeof(Action));
59+
Action.sa_sigaction = IOSSentrySignalHandler;
60+
Action.sa_flags = SA_SIGINFO | SA_ONSTACK;
61+
62+
sigaction(SIGILL, &Action, NULL);
63+
sigaction(SIGEMT, &Action, NULL);
64+
sigaction(SIGFPE, &Action, NULL);
65+
sigaction(SIGBUS, &Action, NULL);
66+
sigaction(SIGSEGV, &Action, NULL);
67+
sigaction(SIGSYS, &Action, NULL);
68+
}
69+
70+
void FIOSSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
71+
USentryTraceSampler* TraceSampler)
72+
{
73+
GIOSSentrySubsystem = this;
74+
75+
SaveDefaultSignalHandlers();
76+
InstallSentrySignalHandler();
77+
78+
FAppleSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, TraceSampler);
79+
}
80+
81+
FString FIOSSentrySubsystem::TryCaptureScreenshot() const
82+
{
83+
FString ScreenshotPath = GetScreenshotPath();
84+
85+
dispatch_sync(dispatch_get_main_queue(), ^{
86+
UIGraphicsBeginImageContextWithOptions([IOSAppDelegate GetDelegate].RootView.bounds.size, NO, 2.0f);
87+
[[IOSAppDelegate GetDelegate].RootView drawViewHierarchyInRect:[IOSAppDelegate GetDelegate].RootView.bounds afterScreenUpdates:YES];
88+
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
89+
UIGraphicsEndImageContext();
90+
91+
NSData *ImageData = UIImagePNGRepresentation(image);
92+
93+
TArray<uint8> ImageBytes;
94+
uint32 SavedSize = ImageData.length;
95+
ImageBytes.AddUninitialized(SavedSize);
96+
FPlatformMemory::Memcpy(ImageBytes.GetData(), [ImageData bytes], SavedSize);
97+
98+
if (!FFileHelper::SaveArrayToFile(ImageBytes, *ScreenshotPath))
99+
{
100+
UE_LOG(LogSentrySdk, Error, TEXT("Failed to save screenshot to: %s"), *ScreenshotPath);
101+
}
102+
});
103+
104+
return ScreenshotPath;
105+
}

plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h

+8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55
class FIOSSentrySubsystem : public FAppleSentrySubsystem
66
{
7+
public:
8+
virtual void InitWithSettings(
9+
const USentrySettings* Settings,
10+
USentryBeforeSendHandler* BeforeSendHandler,
11+
USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
12+
USentryTraceSampler* TraceSampler
13+
) override;
714

15+
virtual FString TryCaptureScreenshot() const override;
816
};
917

1018
typedef FIOSSentrySubsystem FPlatformSentrySubsystem;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#include "Mac/MacSentrySubsystem.h"
2+
3+
#include "SentryIdApple.h"
4+
5+
#include "SentryDefines.h"
6+
#include "SentryModule.h"
7+
#include "SentrySettings.h"
8+
9+
#include "Misc/CoreDelegates.h"
10+
#include "Misc/FileHelper.h"
11+
#include "Misc/Paths.h"
12+
13+
void FMacSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
14+
USentryTraceSampler* TraceSampler)
15+
{
16+
FAppleSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, TraceSampler);
17+
18+
isScreenshotAttachmentEnabled = Settings->AttachScreenshot;
19+
20+
if (IsEnabled() && Settings->AttachScreenshot)
21+
{
22+
FCoreDelegates::OnHandleSystemError.AddLambda([this]()
23+
{
24+
TryCaptureScreenshot();
25+
});
26+
}
27+
}
28+
29+
TSharedPtr<ISentryId> FMacSentrySubsystem::CaptureEnsure(const FString& type, const FString& message)
30+
{
31+
TSharedPtr<ISentryId> id = FAppleSentrySubsystem::CaptureEnsure(type, message);
32+
33+
if (isScreenshotAttachmentEnabled)
34+
{
35+
const FString& screenshotPath = TryCaptureScreenshot();
36+
if (!screenshotPath.IsEmpty())
37+
{
38+
UploadScreenshotForEvent(id, screenshotPath);
39+
}
40+
}
41+
42+
return id;
43+
}
44+
45+
FString FMacSentrySubsystem::TryCaptureScreenshot() const
46+
{
47+
NSWindow* MainWindow = [NSApp mainWindow];
48+
if (!MainWindow)
49+
{
50+
UE_LOG(LogSentrySdk, Error, TEXT("No main window found!"));
51+
return FString("");
52+
}
53+
54+
NSRect WindowRect = [MainWindow frame];
55+
CGWindowID WindowID = (CGWindowID)[MainWindow windowNumber];
56+
CGImageRef ScreenshotRef = CGWindowListCreateImage(WindowRect, kCGWindowListOptionIncludingWindow, WindowID, kCGWindowImageDefault);
57+
58+
if (!ScreenshotRef)
59+
{
60+
UE_LOG(LogSentrySdk, Error, TEXT("Failed to capture screenshot - invalid ScreenshotRef."));
61+
return FString("");
62+
}
63+
64+
NSBitmapImageRep* BitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:ScreenshotRef];
65+
NSData* ImageData = [BitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
66+
67+
TArray<uint8> ImageBytes;
68+
uint32 SavedSize = (uint32)[ImageData length];
69+
ImageBytes.AddUninitialized(SavedSize);
70+
FPlatformMemory::Memcpy(ImageBytes.GetData(), [ImageData bytes], SavedSize);
71+
72+
CGImageRelease(ScreenshotRef);
73+
74+
FString ScreenshotPath = GetScreenshotPath();
75+
76+
if (!FFileHelper::SaveArrayToFile(ImageBytes, *ScreenshotPath))
77+
{
78+
UE_LOG(LogSentrySdk, Error, TEXT("Failed to save screenshot to: %s"), *ScreenshotPath);
79+
return FString("");
80+
}
81+
82+
return ScreenshotPath;
83+
}

plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@
44

55
class FMacSentrySubsystem : public FAppleSentrySubsystem
66
{
7-
protected:
7+
public:
8+
virtual void InitWithSettings(
9+
const USentrySettings* Settings,
10+
USentryBeforeSendHandler* BeforeSendHandler,
11+
USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
12+
USentryTraceSampler* TraceSampler
13+
) override;
814

15+
virtual TSharedPtr<ISentryId> CaptureEnsure(const FString& type, const FString& message) override;
16+
17+
virtual FString TryCaptureScreenshot() const override;
18+
19+
private:
20+
bool isScreenshotAttachmentEnabled = false;
921
};
1022

1123
typedef FMacSentrySubsystem FPlatformSentrySubsystem;

plugin-dev/Source/Sentry/Private/SentrySettings.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ USentrySettings::USentrySettings(const FObjectInitializer& ObjectInitializer)
2222
, SendDefaultPii(false)
2323
, AttachScreenshot(false)
2424
, AttachGpuDump(true)
25+
, MaxAttachmentSize(20 * 1024 * 1024)
2526
, MaxBreadcrumbs(100)
2627
, EnableAutoSessionTracking(true)
2728
, SessionTimeout(30000)

0 commit comments

Comments
 (0)