Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

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

### Dependencies

- Bump Java SDK (Android) from v8.6.0 to v8.7.0 ([#863](https://github.com/getsentry/sentry-unreal/pull/863))
Expand Down
59 changes: 59 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 @@ -369,3 +380,51 @@ TSharedPtr<ISentryTransactionContext> FAppleSentrySubsystem::ContinueTrace(const

return MakeShareable(new SentryTransactionContextApple(transactionContext));
}

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

IFileManager& fileManager = IFileManager::Get();
if (!fileManager.FileExists(*screenshotFilePath))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to upload screenshot."));
return;
}

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

SentryAttachment* screenshotAttachment = [[SENTRY_APPLE_CLASS(SentryAttachment) alloc] initWithPath:screenshotFilePathExt.GetNSString()];

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 create its timestamped backup
CreateScreenshotBackup();
}

void FAppleSentrySubsystem::CreateScreenshotBackup() const
{
const FString& screenshotFilePath = GetScreenshotPath();

IFileManager& fileManager = IFileManager::Get();
if (fileManager.FileExists(*screenshotFilePath))
{
FString name, extension;
FString(screenshotFilePath).Split(TEXT("."), &name, &extension, ESearchCase::CaseSensitive, ESearchDir::FromEnd);
FDateTime originalTime = fileManager.GetTimeStamp(*screenshotFilePath);
FString backupFilePath = FString::Printf(TEXT("%s%s%s.%s"), *name, TEXT("-backup-"), *originalTime.ToString(), *extension);
if (!fileManager.Move(*backupFilePath, *screenshotFilePath, true))
{
UE_LOG(LogSentrySdk, Error, TEXT("Failed to backup screenshot."));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to make a backup of the file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more backups - now we delete screenshot right after it was sent to Sentry.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're certain those screenshots were taken by us, right? No file deletion of other types of attachment where we could be deleting someone's files?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshots are saved separately in Saved/SentryScreenshots dir so deleting them after sending to Sentry should be safe.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 void TryCaptureScreenshot() const {};

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

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

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

void FIOSSentrySubsystem::TryCaptureScreenshot() 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();

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

FString FIOSSentrySubsystem::GetScreenshotPath() const
{
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("screenshot.png"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we'll create conflicting file names.
Should we use something pseudo random here on the file name? We can return the file name around (I mean we are, here doing that already)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point - I've updated it so that we now append the event ID to the file name when one is already available (e.g. in the case of ensures).

For asserts/crashes we still fall back to using just screenshot.png so that the file can be easily found on the next app launch and attached to the corresponding event.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider any file that startWith("screenshot_") for example, and use a timestamp after _.
This way we are sure to never override anything accidently.

}
11 changes: 11 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,18 @@

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

virtual void TryCaptureScreenshot() const override;

protected:
virtual FString GetScreenshotPath() const override;
};

typedef FIOSSentrySubsystem FPlatformSentrySubsystem;
86 changes: 86 additions & 0 deletions plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Try.. return bool to indicate whether it was successful? We can skip the next method call altogether if we know it failed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the platform TryCaptureScreenshot may run asynchronously (i.e. on iOS screenshot capturing happens on the main thread) so returning a bool immediately won't always work as expected.
While we could wait for the operation to complete that's probably not ideal during crash handling. In that context, it's better to make a best-effort attempt to capture the screen without blocking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we call UploadScreenshotForEvent without synchronizing at all.
is it likely the screenshot will be ready by the time we call UploadScreenshotForEvent ?
We just check for the file path existing after but I imagine it's likely not as the code will run much faster than a screenshot can be taken + file I/O

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, now I see where the confusion is coming from - TryCaptureScreenshot() is a virtual void method defined in parent AppleSentrySubsystem which behaves synchronously on Mac and asynchronously on iOS.

Probably makes it worth considering separating the two implementations given their different nature.

UploadScreenshotForEvent(id);
}

return id;
}

void FMacSentrySubsystem::TryCaptureScreenshot() const
{
NSWindow* MainWindow = [NSApp mainWindow];
if (!MainWindow)
{
UE_LOG(LogSentrySdk, Log, 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, Log, TEXT("Failed to capture screenshot."));
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 FilePath = GetScreenshotPath();

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

CGImageRelease(ScreenshotRef);
}

FString FMacSentrySubsystem::GetScreenshotPath() const
{
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("screenshot.png"));
}
15 changes: 15 additions & 0 deletions plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,23 @@

class FMacSentrySubsystem : public FAppleSentrySubsystem
{
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() const override;

protected:
virtual FString GetScreenshotPath() 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();
}
}
4 changes: 4 additions & 0 deletions plugin-dev/Source/Sentry/Public/SentrySettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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;
Expand Down
2 changes: 1 addition & 1 deletion plugin-dev/Source/Sentry/Sentry.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions scripts/packaging/package-github.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions scripts/packaging/package-marketplace.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading