Skip to content

Add screenshot capturing for Mac/iOS #849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@

## Unreleased

### Features

- Add screenshot capturing for Mac/iOS ([#849](https://github.com/getsentry/sentry-unreal/pull/849))

### Fixes

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

### Dependencies

- Bump Java SDK (Android) from v8.6.0 to v8.7.0 ([#863](https://github.com/getsentry/sentry-unreal/pull/863))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#870)
- [diff](https://github.com/getsentry/sentry-java/compare/8.6.0...8.7.0)
- 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))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#880)
- [diff](https://github.com/getsentry/sentry-java/compare/8.6.0...8.8.0)
- Bump Cocoa SDK (iOS and Mac) from v8.48.0 to v8.49.0 ([#866](https://github.com/getsentry/sentry-unreal/pull/866))
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8490)
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.48.0...8.49.0)
- Bump Java SDK (Android) from v8.7.0 to v8.8.0 ([#869](https://github.com/getsentry/sentry-unreal/pull/869))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#880)
- [diff](https://github.com/getsentry/sentry-java/compare/8.7.0...8.8.0)

## 1.0.0-alpha.5

Expand Down
60 changes: 60 additions & 0 deletions plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@

#include "GenericPlatform/GenericPlatformOutputDevices.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformSentryAttachment.h"
#include "Misc/CoreDelegates.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "UObject/GarbageCollection.h"
#include "UObject/UObjectThreadContext.h"
#include "Utils/SentryLogUtils.h"
Expand Down Expand Up @@ -57,6 +61,7 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, US
options.sampleRate = [NSNumber numberWithFloat:settings->SampleRate];
options.maxBreadcrumbs = settings->MaxBreadcrumbs;
options.sendDefaultPii = settings->SendDefaultPii;
options.maxAttachmentSize = settings->MaxAttachmentSize;
#if SENTRY_UIKIT_AVAILABLE
options.attachScreenshot = settings->AttachScreenshot;
#endif
Expand All @@ -68,6 +73,12 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, US
}
return scope;
};
options.onCrashedLastRun = ^(SentryEvent* event) {
if (settings->AttachScreenshot)
{
UploadScreenshotForEvent(MakeShareable(new SentryIdApple(event.eventId)));
}
};
options.beforeSend = ^SentryEvent* (SentryEvent* event) {
if (FUObjectThreadContext::Get().IsRoutingPostLoad)
{
Expand Down Expand Up @@ -371,3 +382,52 @@ TSharedPtr<ISentryTransactionContext> FAppleSentrySubsystem::ContinueTrace(const

return MakeShareable(new SentryTransactionContextApple(transactionContext));
}

void FAppleSentrySubsystem::UploadScreenshotForEvent(TSharedPtr<ISentryId> eventId) const
{
FString screenshotFilePath = GetScreenshotPath(eventId);

IFileManager& fileManager = IFileManager::Get();
if (!fileManager.FileExists(*screenshotFilePath))
{
// Couldn't find screenshot with a name that matches given Event ID so we will check for the default 'screenshot.png'
// that gets created during assertion/crash and is not associated with any event.
screenshotFilePath = GetScreenshotPath();

if (!fileManager.FileExists(*screenshotFilePath))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to upload screenshot - path provided did not exist: %s"), *screenshotFilePath);
return;
}
}

const FString& screenshotFilePathExt = fileManager.ConvertToAbsolutePathForExternalAppForRead(*screenshotFilePath);

SentryAttachment* screenshotAttachment = [[SENTRY_APPLE_CLASS(SentryAttachment) alloc] initWithPath:screenshotFilePathExt.GetNSString() filename:@"screenshot.png"];

SentryOptions* options = [SENTRY_APPLE_CLASS(PrivateSentrySDKOnly) options];
int32 size = options.maxAttachmentSize;

SentryEnvelopeItem* envelopeItem = [[SENTRY_APPLE_CLASS(SentryEnvelopeItem) alloc] initWithAttachment:screenshotAttachment maxAttachmentSize:size];

SentryId* id = StaticCastSharedPtr<SentryIdApple>(eventId)->GetNativeObject();

SentryEnvelope* envelope = [[SENTRY_APPLE_CLASS(SentryEnvelope) alloc] initWithId:id singleItem:envelopeItem];

[SENTRY_APPLE_CLASS(PrivateSentrySDKOnly) captureEnvelope:envelope];

// After uploading screenshot it's no longer needed so delete
if (!fileManager.Delete(*screenshotFilePath))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to delete screenshot: %s"), *screenshotFilePath);
}
}

FString FAppleSentrySubsystem::GetScreenshotPath(TSharedPtr<ISentryId> eventId) const
{
FString screenshotFileName = eventId != nullptr
? FString::Printf(TEXT("screenshot-%s.png"), *eventId->ToString())
: TEXT("screenshot.png");

return FPaths::Combine(FPaths::ProjectSavedDir(), screenshotFileName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ class FAppleSentrySubsystem : public ISentrySubsystem
virtual TSharedPtr<ISentryTransaction> StartTransactionWithContextAndTimestamp(TSharedPtr<ISentryTransactionContext> context, int64 timestamp) override;
virtual TSharedPtr<ISentryTransaction> StartTransactionWithContextAndOptions(TSharedPtr<ISentryTransactionContext> context, const TMap<FString, FString>& options) override;
virtual TSharedPtr<ISentryTransactionContext> ContinueTrace(const FString& sentryTrace, const TArray<FString>& baggageHeaders) override;

virtual void TryCaptureScreenshot(TSharedPtr<ISentryId> eventId = nullptr) const {};

protected:
void UploadScreenshotForEvent(TSharedPtr<ISentryId> eventId) const;

virtual FString GetScreenshotPath(TSharedPtr<ISentryId> eventId = nullptr) const;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

#if PLATFORM_MAC
#include <Sentry/Sentry.h>
#include <Sentry/SentryEnvelope.h>
#include <Sentry/PrivateSentrySDKOnly.h>
#include <Sentry/SentrySwift.h>
#elif PLATFORM_IOS
#import <Sentry/Sentry.h>
#import <Sentry/SentryEnvelope.h>
#import <Sentry/PrivateSentrySDKOnly.h>
#import <Sentry/SentrySwift.h>
#endif
107 changes: 107 additions & 0 deletions plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "IOS/IOSSentrySubsystem.h"

#include "IOS/IOSAppDelegate.h"

#include "SentryDefines.h"
#include "SentrySettings.h"

#include "Misc/CoreDelegates.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Utils/SentryScreenshotUtils.h"

static FIOSSentrySubsystem* GIOSSentrySubsystem = nullptr;

struct sigaction DefaultSigIllHandler;
struct sigaction DefaultSigEmtHandler;
struct sigaction DefaultSigFpeHandler;
struct sigaction DefaultSigBusHandler;
struct sigaction DefaultSigSegvHandler;
struct sigaction DefaultSigSysHandler;

void SaveDefaultSignalHandlers()
{
sigaction(SIGILL, NULL, &DefaultSigIllHandler);
sigaction(SIGEMT, NULL, &DefaultSigEmtHandler);
sigaction(SIGFPE, NULL, &DefaultSigFpeHandler);
sigaction(SIGBUS, NULL, &DefaultSigBusHandler);
sigaction(SIGSEGV, NULL, &DefaultSigSegvHandler);
sigaction(SIGSYS, NULL, &DefaultSigSysHandler);
}

void RestoreDefaultSignalHandlers()
{
sigaction(SIGILL, &DefaultSigIllHandler, NULL);
sigaction(SIGEMT, &DefaultSigEmtHandler, NULL);
sigaction(SIGFPE, &DefaultSigFpeHandler, NULL);
sigaction(SIGBUS, &DefaultSigBusHandler, NULL);
sigaction(SIGSEGV, &DefaultSigSegvHandler, NULL);
sigaction(SIGSYS, &DefaultSigSysHandler, NULL);
}

static void IOSSentrySignalHandler(int Signal, siginfo_t *Info, void *Context)
{
if (GIOSSentrySubsystem && GIOSSentrySubsystem->IsEnabled())
{
GIOSSentrySubsystem->TryCaptureScreenshot();
}

RestoreDefaultSignalHandlers();

// Re-raise signal to default handler
raise(Signal);
}

void InstallSentrySignalHandler()
{
struct sigaction Action;
memset(&Action, 0, sizeof(Action));
Action.sa_sigaction = IOSSentrySignalHandler;
Action.sa_flags = SA_SIGINFO | SA_ONSTACK;

sigaction(SIGILL, &Action, NULL);
sigaction(SIGEMT, &Action, NULL);
sigaction(SIGFPE, &Action, NULL);
sigaction(SIGBUS, &Action, NULL);
sigaction(SIGSEGV, &Action, NULL);
sigaction(SIGSYS, &Action, NULL);
}

void FIOSSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
USentryTraceSampler* TraceSampler)
{
GIOSSentrySubsystem = this;

SaveDefaultSignalHandlers();
InstallSentrySignalHandler();

FAppleSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, TraceSampler);
}

void FIOSSentrySubsystem::TryCaptureScreenshot(TSharedPtr<ISentryId> eventId) const
{
dispatch_sync(dispatch_get_main_queue(), ^{
UIGraphicsBeginImageContextWithOptions([IOSAppDelegate GetDelegate].RootView.bounds.size, NO, 2.0f);
[[IOSAppDelegate GetDelegate].RootView drawViewHierarchyInRect:[IOSAppDelegate GetDelegate].RootView.bounds afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

NSData *ImageData = UIImagePNGRepresentation(image);

TArray<uint8> ImageBytes;
uint32 SavedSize = ImageData.length;
ImageBytes.AddUninitialized(SavedSize);
FPlatformMemory::Memcpy(ImageBytes.GetData(), [ImageData bytes], SavedSize);

FString FilePath = GetScreenshotPath(eventId);

if (FFileHelper::SaveArrayToFile(ImageBytes, *FilePath))
{
UE_LOG(LogSentrySdk, Log, TEXT("Screenshot saved to: %s"), *FilePath);
}
else
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to save screenshot to: %s"), *FilePath);
}
});
}
8 changes: 8 additions & 0 deletions plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

class FIOSSentrySubsystem : public FAppleSentrySubsystem
{
public:
virtual void InitWithSettings(
const USentrySettings* Settings,
USentryBeforeSendHandler* BeforeSendHandler,
USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
USentryTraceSampler* TraceSampler
) override;

virtual void TryCaptureScreenshot(TSharedPtr<ISentryId> eventId = nullptr) const override;
};

typedef FIOSSentrySubsystem FPlatformSentrySubsystem;
81 changes: 81 additions & 0 deletions plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include "Mac/MacSentrySubsystem.h"

#include "SentryIdApple.h"

#include "SentryDefines.h"
#include "SentryModule.h"
#include "SentrySettings.h"

#include "Misc/CoreDelegates.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"

void FMacSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
USentryTraceSampler* TraceSampler)
{
FAppleSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, TraceSampler);

isScreenshotAttachmentEnabled = Settings->AttachScreenshot;

if (Settings->AttachScreenshot)
{
FCoreDelegates::OnHandleSystemError.AddLambda([this]()
{
TryCaptureScreenshot();
});
}
}

TSharedPtr<ISentryId> FMacSentrySubsystem::CaptureEnsure(const FString& type, const FString& message)
{
TSharedPtr<ISentryId> id = FAppleSentrySubsystem::CaptureEnsure(type, message);

if (isScreenshotAttachmentEnabled)
{
TryCaptureScreenshot(id);
UploadScreenshotForEvent(id);
}

return id;
}

void FMacSentrySubsystem::TryCaptureScreenshot(TSharedPtr<ISentryId> eventId) const
{
NSWindow* MainWindow = [NSApp mainWindow];
if (!MainWindow)
{
UE_LOG(LogSentrySdk, Error, TEXT("No main window found!"));
return;
}

NSRect WindowRect = [MainWindow frame];
CGWindowID WindowID = (CGWindowID)[MainWindow windowNumber];
CGImageRef ScreenshotRef = CGWindowListCreateImage(WindowRect, kCGWindowListOptionIncludingWindow, WindowID, kCGWindowImageDefault);

if (!ScreenshotRef)
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to capture screenshot - invalid ScreenshotRef."));
return;
}

NSBitmapImageRep* BitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:ScreenshotRef];
NSData* ImageData = [BitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];

TArray<uint8> ImageBytes;
uint32 SavedSize = (uint32)[ImageData length];
ImageBytes.AddUninitialized(SavedSize);
FPlatformMemory::Memcpy(ImageBytes.GetData(), [ImageData bytes], SavedSize);

FString ScreenshotPath = GetScreenshotPath(eventId);

if (FFileHelper::SaveArrayToFile(ImageBytes, *ScreenshotPath))
{
UE_LOG(LogSentrySdk, Log, TEXT("Screenshot saved to: %s"), *ScreenshotPath);
}
else
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to save screenshot to: %s"), *ScreenshotPath);
}

CGImageRelease(ScreenshotRef);
}
14 changes: 13 additions & 1 deletion plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@

class FMacSentrySubsystem : public FAppleSentrySubsystem
{
protected:
public:
virtual void InitWithSettings(
const USentrySettings* Settings,
USentryBeforeSendHandler* BeforeSendHandler,
USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler,
USentryTraceSampler* TraceSampler
) override;

virtual TSharedPtr<ISentryId> CaptureEnsure(const FString& type, const FString& message) override;

virtual void TryCaptureScreenshot(TSharedPtr<ISentryId> eventId = nullptr) const override;

private:
bool isScreenshotAttachmentEnabled = false;
};

typedef FMacSentrySubsystem FPlatformSentrySubsystem;
1 change: 1 addition & 0 deletions plugin-dev/Source/Sentry/Private/SentrySettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ USentrySettings::USentrySettings(const FObjectInitializer& ObjectInitializer)
, SendDefaultPii(false)
, AttachScreenshot(false)
, AttachGpuDump(true)
, MaxAttachmentSize(20 * 1024 * 1024)
, MaxBreadcrumbs(100)
, EnableAutoSessionTracking(true)
, SessionTimeout(30000)
Expand Down
9 changes: 7 additions & 2 deletions plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,13 @@ void USentrySubsystem::ConfigureErrorOutputDevice()
GError->HandleError();
PLATFORM_BREAK();
});
#endif // PLATFORM_ANDROID

#elif PLATFORM_IOS
OnAssertDelegate = OutputDeviceError->OnAssert.AddWeakLambda(this, [this](const FString& Message)
{
check(SubsystemNativeImpl);
StaticCastSharedPtr<FIOSSentrySubsystem>(SubsystemNativeImpl)->TryCaptureScreenshot();
});
#endif
GError = OutputDeviceError.Get();
}
}
Loading