From 4dc30dfbf5b0d6d1757f632bb524f6169d183d5c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 1 Jul 2024 10:08:43 +0300 Subject: [PATCH] Add opt-in screenshot attachment for crash events captured on Windows/Linux (#582) * Add screenshot attachment for desktop crash events * Fix includes * Fix more includes * Fix indents * Add ability to capture screenshot including game UI * Update snapshot * Add missing include * Update changelog * Update CHANGELOG.md Co-authored-by: Stefan Jandl * Remove redundant variable --------- Co-authored-by: Stefan Jandl --- CHANGELOG.md | 8 +-- .../Desktop/SentrySubsystemDesktop.cpp | 34 +++++++++++ .../Private/Desktop/SentrySubsystemDesktop.h | 4 ++ .../Source/Sentry/Private/SentrySubsystem.cpp | 12 ++-- .../Private/Utils/SentryScreenshotUtils.cpp | 59 +++++++++++++++++++ .../Private/Utils/SentryScreenshotUtils.h | 11 ++++ scripts/packaging/package-github.snapshot | 2 + .../packaging/package-marketplace.snapshot | 2 + 8 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.cpp create mode 100644 plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.h diff --git a/CHANGELOG.md b/CHANGELOG.md index ebdea8af..b9b21b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- On Windows/Linux the SDK can now automatically attach a screenshot to crash events([#582](https://github.com/getsentry/sentry-unreal/pull/582)) - Add API allowing to check if captured event is ANR error ([#581](https://github.com/getsentry/sentry-unreal/pull/581)) ### Fixes @@ -12,12 +13,9 @@ ### Dependencies -- Bump Cocoa SDK (iOS) from v8.29.0 to v8.29.1 ([#580](https://github.com/getsentry/sentry-unreal/pull/580)) - - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8291) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.29.0...8.29.1) -- Bump Cocoa SDK (iOS) from v8.29.1 to v8.30.0 ([#585](https://github.com/getsentry/sentry-unreal/pull/585)) +- Bump Cocoa SDK (iOS) from v8.29.0 to v8.30.0 ([#580](https://github.com/getsentry/sentry-unreal/pull/580), [#585](https://github.com/getsentry/sentry-unreal/pull/585)) - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8300) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.29.1...8.30.0) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.29.0...8.30.0) ## 0.18.0 diff --git a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp index db5aad61..dfbb6dc9 100644 --- a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp +++ b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp @@ -21,6 +21,8 @@ #include "SentryTransactionContext.h" #include "SentryUserFeedbackDesktop.h" +#include "Utils/SentryScreenshotUtils.h" + #include "Infrastructure/SentryConvertorsDesktop.h" #include "CrashReporter/SentryCrashReporter.h" @@ -87,6 +89,7 @@ sentry_value_t HandleBeforeSend(sentry_value_t event, void *hint, void *closure) sentry_value_t HandleBeforeCrash(const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) { SentrySubsystemDesktop* SentrySubsystem = static_cast(closure); + SentrySubsystem->TryCaptureScreenshot(); FSentryCrashContext::Get()->Apply(SentrySubsystem->GetCurrentScope()); @@ -117,6 +120,7 @@ SentrySubsystemDesktop::SentrySubsystemDesktop() , crashReporter(MakeShareable(new SentryCrashReporter)) , isEnabled(false) , isStackTraceEnabled(true) + , isScreenshotAttachmentEnabled(false) { } @@ -139,6 +143,17 @@ void SentrySubsystemDesktop::InitWithSettings(const USentrySettings* settings, U #endif } + if(settings->AttachScreenshot) + { + isScreenshotAttachmentEnabled = true; + +#if PLATFORM_WINDOWS + sentry_options_add_attachmentw(options, *GetScreenshotPath()); +#elif PLATFORM_LINUX + sentry_options_add_attachment(options, TCHAR_TO_UTF8(*GetScreenshotPath())); +#endif + } + if(settings->UseProxy) { sentry_options_set_http_proxy(options, TCHAR_TO_ANSI(*settings->ProxyUrl)); @@ -460,6 +475,17 @@ USentryBeforeSendHandler* SentrySubsystemDesktop::GetBeforeSendHandler() return beforeSend; } +void SentrySubsystemDesktop::TryCaptureScreenshot() const +{ + if(!isScreenshotAttachmentEnabled) + { + UE_LOG(LogSentrySdk, Log, TEXT("Screenshot attachment is disabled in plugin settings.")); + return; + } + + SentryScreenshotUtils::CaptureScreenshot(GetScreenshotPath()); +} + TSharedPtr SentrySubsystemDesktop::GetCurrentScope() { if(scopeStack.Num() == 0) @@ -493,4 +519,12 @@ FString SentrySubsystemDesktop::GetDatabasePath() const return DatabaseFullPath; } +FString SentrySubsystemDesktop::GetScreenshotPath() const +{ + const FString ScreenshotPath = FPaths::Combine(GetDatabasePath(), TEXT("screenshots"), TEXT("crash_screenshot.png")); + const FString ScreenshotFullPath = FPaths::ConvertRelativePathToFull(ScreenshotPath); + + return ScreenshotFullPath; +} + #endif diff --git a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h index 0d594d7f..ca2ae610 100644 --- a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h +++ b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h @@ -46,11 +46,14 @@ class SentrySubsystemDesktop : public ISentrySubsystem USentryBeforeSendHandler* GetBeforeSendHandler(); + void TryCaptureScreenshot() const; + TSharedPtr GetCurrentScope(); private: FString GetHandlerPath() const; FString GetDatabasePath() const; + FString GetScreenshotPath() const; USentryBeforeSendHandler* beforeSend; @@ -61,6 +64,7 @@ class SentrySubsystemDesktop : public ISentrySubsystem bool isEnabled; bool isStackTraceEnabled; + bool isScreenshotAttachmentEnabled; FCriticalSection CriticalSection; }; diff --git a/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp index c22ccbf0..6e68ca0f 100644 --- a/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp @@ -217,7 +217,7 @@ USentryId* USentrySubsystem::CaptureMessage(const FString& Message, ESentryLevel USentryId* USentrySubsystem::CaptureMessageWithScope(const FString& Message, const FConfigureScopeDelegate& OnConfigureScope, ESentryLevel Level) { - return CaptureMessageWithScope(Message, FConfigureScopeNativeDelegate::CreateUFunction(const_cast(OnConfigureScope.GetUObject()), OnConfigureScope.GetFunctionName()), Level); + return CaptureMessageWithScope(Message, FConfigureScopeNativeDelegate::CreateUFunction(const_cast(OnConfigureScope.GetUObject()), OnConfigureScope.GetFunctionName()), Level); } USentryId* USentrySubsystem::CaptureMessageWithScope(const FString& Message, const FConfigureScopeNativeDelegate& OnConfigureScope, ESentryLevel Level) @@ -225,7 +225,7 @@ USentryId* USentrySubsystem::CaptureMessageWithScope(const FString& Message, con if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) return nullptr; - return SubsystemNativeImpl->CaptureMessageWithScope(Message, OnConfigureScope, Level); + return SubsystemNativeImpl->CaptureMessageWithScope(Message, OnConfigureScope, Level); } USentryId* USentrySubsystem::CaptureEvent(USentryEvent* Event) @@ -238,7 +238,7 @@ USentryId* USentrySubsystem::CaptureEvent(USentryEvent* Event) USentryId* USentrySubsystem::CaptureEventWithScope(USentryEvent* Event, const FConfigureScopeDelegate& OnConfigureScope) { - return CaptureEventWithScope(Event, FConfigureScopeNativeDelegate::CreateUFunction(const_cast(OnConfigureScope.GetUObject()), OnConfigureScope.GetFunctionName())); + return CaptureEventWithScope(Event, FConfigureScopeNativeDelegate::CreateUFunction(const_cast(OnConfigureScope.GetUObject()), OnConfigureScope.GetFunctionName())); } USentryId* USentrySubsystem::CaptureEventWithScope(USentryEvent* Event, const FConfigureScopeNativeDelegate& OnConfigureScope) @@ -246,7 +246,7 @@ USentryId* USentrySubsystem::CaptureEventWithScope(USentryEvent* Event, const FC if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) return nullptr; - return SubsystemNativeImpl->CaptureEventWithScope(Event, OnConfigureScope); + return SubsystemNativeImpl->CaptureEventWithScope(Event, OnConfigureScope); } void USentrySubsystem::CaptureUserFeedback(USentryUserFeedback* UserFeedback) @@ -286,7 +286,7 @@ void USentrySubsystem::RemoveUser() void USentrySubsystem::ConfigureScope(const FConfigureScopeDelegate& OnConfigureScope) { - ConfigureScope(FConfigureScopeNativeDelegate::CreateUFunction(const_cast(OnConfigureScope.GetUObject()), OnConfigureScope.GetFunctionName())); + ConfigureScope(FConfigureScopeNativeDelegate::CreateUFunction(const_cast(OnConfigureScope.GetUObject()), OnConfigureScope.GetFunctionName())); } void USentrySubsystem::ConfigureScope(const FConfigureScopeNativeDelegate& OnConfigureScope) @@ -294,7 +294,7 @@ void USentrySubsystem::ConfigureScope(const FConfigureScopeNativeDelegate& OnCon if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) return; - SubsystemNativeImpl->ConfigureScope(OnConfigureScope); + SubsystemNativeImpl->ConfigureScope(OnConfigureScope); } void USentrySubsystem::SetContext(const FString& Key, const TMap& Values) diff --git a/plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.cpp b/plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.cpp new file mode 100644 index 00000000..00636403 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2024 Sentry. All Rights Reserved. + +#include "SentryScreenshotUtils.h" + +#include "HighResScreenshot.h" +#include "SentryDefines.h" + +#include "Engine/Engine.h" +#include "ImageUtils.h" +#include "UnrealClient.h" +#include "Misc/FileHelper.h" +#include "Engine/GameViewportClient.h" +#include "Framework/Application/SlateApplication.h" + +bool SentryScreenshotUtils::CaptureScreenshot(const FString& ScreenshotSavePath) +{ + if (!GEngine || !GEngine->GameViewport) + { + UE_LOG(LogSentrySdk, Error, TEXT("GameViewport required for screenshot capturing is not valid")); + return false; + } + + UGameViewportClient* GameViewportClient = GEngine->GameViewport; + if (!GameViewportClient) + { + UE_LOG(LogSentrySdk, Error, TEXT("Game Viewport Client required for screenshot capturing is not valid")); + return false; + } + + FIntVector ViewportSize(GameViewportClient->Viewport->GetSizeXY().X, GameViewportClient->Viewport->GetSizeXY().Y, 0); + + TArray Bitmap; + + if (!FSlateApplication::IsInitialized()) + { + UE_LOG(LogSentrySdk, Error, TEXT("Slate application required for screenshot capturing is not initialized")); + return false; + } + + TSharedPtr WindowPtr = GameViewportClient->GetWindow(); + TSharedRef WindowRef = WindowPtr.ToSharedRef(); + + bool bScreenshotSuccessful = FSlateApplication::Get().TakeScreenshot(WindowRef, Bitmap, ViewportSize); + if (!bScreenshotSuccessful) + { + UE_LOG(LogSentrySdk, Error, TEXT("Failed to capture screenshot")); + return false; + } + + GetHighResScreenshotConfig().MergeMaskIntoAlpha(Bitmap, FIntRect()); + + TArray64 CompressedBitmap; + FImageUtils::PNGCompressImageArray(ViewportSize.X, ViewportSize.Y, Bitmap, CompressedBitmap); + FFileHelper::SaveArrayToFile(CompressedBitmap, *ScreenshotSavePath); + + UE_LOG(LogSentrySdk, Log, TEXT("Screenshot saved to: %s"), *ScreenshotSavePath); + + return true; +} diff --git a/plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.h b/plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.h new file mode 100644 index 00000000..f1a6ac37 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Utils/SentryScreenshotUtils.h @@ -0,0 +1,11 @@ +// Copyright (c) 2024 Sentry. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +class SentryScreenshotUtils +{ +public: + static bool CaptureScreenshot(const FString& ScreenshotSavePath); +}; diff --git a/scripts/packaging/package-github.snapshot b/scripts/packaging/package-github.snapshot index 0bdd9d87..1609cd86 100644 --- a/scripts/packaging/package-github.snapshot +++ b/scripts/packaging/package-github.snapshot @@ -169,6 +169,8 @@ Source/Sentry/Private/Tests/SentryUser.spec.cpp Source/Sentry/Private/Utils/ Source/Sentry/Private/Utils/SentryFileUtils.cpp Source/Sentry/Private/Utils/SentryFileUtils.h +Source/Sentry/Private/Utils/SentryScreenshotUtils.cpp +Source/Sentry/Private/Utils/SentryScreenshotUtils.h Source/Sentry/Public/ Source/Sentry/Public/SentryAttachment.h Source/Sentry/Public/SentryBeforeSendHandler.h diff --git a/scripts/packaging/package-marketplace.snapshot b/scripts/packaging/package-marketplace.snapshot index 64bdb571..03beb84a 100644 --- a/scripts/packaging/package-marketplace.snapshot +++ b/scripts/packaging/package-marketplace.snapshot @@ -167,6 +167,8 @@ Source/Sentry/Private/Tests/SentryUser.spec.cpp Source/Sentry/Private/Utils/ Source/Sentry/Private/Utils/SentryFileUtils.cpp Source/Sentry/Private/Utils/SentryFileUtils.h +Source/Sentry/Private/Utils/SentryScreenshotUtils.cpp +Source/Sentry/Private/Utils/SentryScreenshotUtils.h Source/Sentry/Public/ Source/Sentry/Public/SentryAttachment.h Source/Sentry/Public/SentryBeforeSendHandler.h