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
Changes from all 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
@@ -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

81 changes: 81 additions & 0 deletions plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -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"
@@ -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
@@ -68,6 +73,18 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, US
}
return scope;
};
options.onCrashedLastRun = ^(SentryEvent* event) {
if (settings->AttachScreenshot)
{
// If a screenshot was captured during assertion/crash in the previous app run
// find the most recent one and upload it to Sentry.
const FString& screenshotPath = GetLatestScreenshot();
if (!screenshotPath.IsEmpty())
{
UploadScreenshotForEvent(MakeShareable(new SentryIdApple(event.eventId)), screenshotPath);
}
}
};
options.beforeSend = ^SentryEvent* (SentryEvent* event) {
if (FUObjectThreadContext::Get().IsRoutingPostLoad)
{
@@ -371,3 +388,67 @@ TSharedPtr<ISentryTransactionContext> FAppleSentrySubsystem::ContinueTrace(const

return MakeShareable(new SentryTransactionContextApple(transactionContext));
}

void FAppleSentrySubsystem::UploadScreenshotForEvent(TSharedPtr<ISentryId> eventId, const FString& screenshotPath) const
{
IFileManager& fileManager = IFileManager::Get();
if (!fileManager.FileExists(*screenshotPath))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to upload screenshot - path provided did not exist: %s"), *screenshotPath);
return;
}

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

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(*screenshotPath))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to delete screenshot: %s"), *screenshotPath);
}
}

FString FAppleSentrySubsystem::GetScreenshotPath() const
{
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryScreenshots"), FString::Printf(TEXT("screenshot-%s.png"), *FDateTime::Now().ToString()));
}

FString FAppleSentrySubsystem::GetLatestScreenshot() const
{
const FString& ScreenshotsDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryScreenshots"));

TArray<FString> Screenshots;
IFileManager::Get().FindFiles(Screenshots, *ScreenshotsDir, TEXT("*.png"));

if(Screenshots.Num() == 0)
{
UE_LOG(LogSentrySdk, Log, TEXT("There are no screenshots found."));
return FString("");
}

for (int i = 0; i < Screenshots.Num(); ++i)
{
Screenshots[i] = ScreenshotsDir / Screenshots[i];
}

Screenshots.Sort([](const FString& A, const FString& B)
{
const FDateTime TimestampA = IFileManager::Get().GetTimeStamp(*A);
const FDateTime TimestampB = IFileManager::Get().GetTimeStamp(*B);
return TimestampB < TimestampA;
});

return Screenshots[0];
}
Original file line number Diff line number Diff line change
@@ -34,4 +34,12 @@ 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 FString TryCaptureScreenshot() const { return FString(); };

protected:
void UploadScreenshotForEvent(TSharedPtr<ISentryId> eventId, const FString& screenshotPath) const;

virtual FString GetScreenshotPath() const;
virtual FString GetLatestScreenshot() const;
};
Original file line number Diff line number Diff line change
@@ -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
105 changes: 105 additions & 0 deletions plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#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);
}

FString FIOSSentrySubsystem::TryCaptureScreenshot() const
{
FString ScreenshotPath = GetScreenshotPath();

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);

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

return ScreenshotPath;
}
8 changes: 8 additions & 0 deletions plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h
Original file line number Diff line number Diff line change
@@ -4,7 +4,15 @@

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

virtual FString TryCaptureScreenshot() const override;
};

typedef FIOSSentrySubsystem FPlatformSentrySubsystem;
83 changes: 83 additions & 0 deletions plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#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 (IsEnabled() && 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)
{
const FString& screenshotPath = TryCaptureScreenshot();
if (!screenshotPath.IsEmpty())
{
UploadScreenshotForEvent(id, screenshotPath);
}
}

return id;
}

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

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 FString("");
}

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);

CGImageRelease(ScreenshotRef);

FString ScreenshotPath = GetScreenshotPath();

if (!FFileHelper::SaveArrayToFile(ImageBytes, *ScreenshotPath))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to save screenshot to: %s"), *ScreenshotPath);
return FString("");
}

return ScreenshotPath;
}
14 changes: 13 additions & 1 deletion plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h
Original file line number Diff line number Diff line change
@@ -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 FString TryCaptureScreenshot() 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
@@ -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)
9 changes: 7 additions & 2 deletions plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 4 additions & 0 deletions plugin-dev/Source/Sentry/Public/SentrySettings.h
Original file line number Diff line number Diff line change
@@ -225,6 +225,10 @@ class SENTRY_API USentrySettings : public UObject
Meta = (DisplayName = "Attach GPU dump", ToolTip = "Flag indicating whether to attach GPU crash dump when an error occurs. Currently this feature is supported for Nvidia graphics only."))
bool AttachGpuDump;

UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "General|Attachments",
Meta = (DisplayName = "Max attachment size in bytes", Tooltip = "Max attachment size for each attachment in bytes. Default is 20 MiB compressed but this size is planned to be increased. Please also check the maximum attachment size of Relay to make sure your attachments don't get discarded there: https://docs.sentry.io/product/relay/options/"))
int32 MaxAttachmentSize;

UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "General|Breadcrumbs",
Meta = (DisplayName = "Max breadcrumbs", Tooltip = "Total amount of breadcrumbs that should be captured."))
int32 MaxBreadcrumbs;
2 changes: 1 addition & 1 deletion plugin-dev/Source/Sentry/Sentry.Build.cs
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ public Sentry(ReadOnlyTargetRules Target) : base(Target)

PublicDefinitions.Add("USE_SENTRY_NATIVE=0");
PublicDefinitions.Add("COCOAPODS=0");
PublicDefinitions.Add("SENTRY_NO_UIKIT=1");
PublicDefinitions.Add("SENTRY_NO_UIKIT=0");
PublicDefinitions.Add("APPLICATION_EXTENSION_API_ONLY_NO=0");
}
else if (Target.Platform == UnrealTargetPlatform.Mac)
2 changes: 2 additions & 0 deletions scripts/packaging/package-github.snapshot
Original file line number Diff line number Diff line change
@@ -129,9 +129,11 @@ Source/Sentry/Private/Interface/SentryTransactionContextInterface.h
Source/Sentry/Private/Interface/SentryTransactionInterface.h
Source/Sentry/Private/Interface/SentryUserFeedbackInterface.h
Source/Sentry/Private/Interface/SentryUserInterface.h
Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp
Source/Sentry/Private/IOS/IOSSentrySubsystem.h
Source/Sentry/Private/Linux/LinuxSentrySubsystem.cpp
Source/Sentry/Private/Linux/LinuxSentrySubsystem.h
Source/Sentry/Private/Mac/MacSentrySubsystem.cpp
Source/Sentry/Private/Mac/MacSentrySubsystem.h
Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp
Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.h
2 changes: 2 additions & 0 deletions scripts/packaging/package-marketplace.snapshot
Original file line number Diff line number Diff line change
@@ -128,9 +128,11 @@ Source/Sentry/Private/Interface/SentryTransactionContextInterface.h
Source/Sentry/Private/Interface/SentryTransactionInterface.h
Source/Sentry/Private/Interface/SentryUserFeedbackInterface.h
Source/Sentry/Private/Interface/SentryUserInterface.h
Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp
Source/Sentry/Private/IOS/IOSSentrySubsystem.h
Source/Sentry/Private/Linux/LinuxSentrySubsystem.cpp
Source/Sentry/Private/Linux/LinuxSentrySubsystem.h
Source/Sentry/Private/Mac/MacSentrySubsystem.cpp
Source/Sentry/Private/Mac/MacSentrySubsystem.h
Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp
Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.h