zworld-em/Plugins/sentry-unreal/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp
2025-05-11 22:07:21 +08:00

644 lines
21 KiB
C++

// Copyright (c) 2022 Sentry. All Rights Reserved.
#include "SentrySubsystemDesktop.h"
#include "SentryEventDesktop.h"
#include "SentryBreadcrumbDesktop.h"
#include "SentryUserDesktop.h"
#include "SentryUserFeedbackDesktop.h"
#include "SentryScopeDesktop.h"
#include "SentryTransactionDesktop.h"
#include "SentryTransactionContextDesktop.h"
#include "SentryIdDesktop.h"
#include "SentryDefines.h"
#include "SentrySettings.h"
#include "SentryEvent.h"
#include "SentryModule.h"
#include "SentryBeforeSendHandler.h"
#include "SentryTraceSampler.h"
#include "Utils/SentryFileUtils.h"
#include "Utils/SentryLogUtils.h"
#include "Utils/SentryScreenshotUtils.h"
#include "Infrastructure/SentryConvertorsDesktop.h"
#include "CrashReporter/SentryCrashReporter.h"
#include "CrashReporter/SentryCrashContext.h"
#include "Transport/SentryTransport.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
#include "Misc/CoreDelegates.h"
#include "HAL/FileManager.h"
#include "Misc/EngineVersionComparison.h"
#include "GenericPlatform/GenericPlatformOutputDevices.h"
#include "GenericPlatform/GenericPlatformCrashContext.h"
#include "HAL/ExceptionHandling.h"
#include "UObject/GarbageCollection.h"
#include "UObject/UObjectThreadContext.h"
#if PLATFORM_WINDOWS
#include "Windows/WindowsPlatformMisc.h"
#include "Windows/WindowsPlatformCrashContext.h"
#endif
extern CORE_API bool GIsGPUCrashed;
#if USE_SENTRY_NATIVE
void PrintVerboseLog(sentry_level_t level, const char *message, va_list args, void *userdata)
{
char buffer[512];
vsnprintf(buffer, 512, message, args);
FString MessageBuf = FString(buffer);
// The WER (Windows Error Reporting) module (crashpad_wer.dll) can't be distributed along with other Sentry binaries
// within the plugin package due to some UE Marketplace restrictions. Its absence doesn't affect crash capturing
// and the corresponding warning can be disregarded
if(MessageBuf.Equals(TEXT("crashpad WER handler module not found")))
{
return;
}
#if !NO_LOGGING
const FName SentryCategoryName(LogSentrySdk.GetCategoryName());
#else
const FName SentryCategoryName(TEXT("LogSentrySdk"));
#endif
GLog->CategorizedLogf(SentryCategoryName, SentryConvertorsDesktop::SentryLevelToLogVerbosity(level), TEXT("%s"), *MessageBuf);
}
void PrintCrashLog(const sentry_ucontext_t *uctx)
{
#if PLATFORM_WINDOWS && !UE_VERSION_OLDER_THAN(5, 0, 0)
SentryConvertorsDesktop::SentryCrashContextToString(uctx, GErrorExceptionDescription, UE_ARRAY_COUNT(GErrorExceptionDescription));
const SIZE_T StackTraceSize = 65535;
ANSICHAR* StackTrace = (ANSICHAR*)GMalloc->Malloc(StackTraceSize);
StackTrace[0] = 0;
// Currently raw crash data stored in `uctx` can be utilized for stalk walking on Windows only
void* ProgramCounter = uctx->exception_ptrs.ExceptionRecord->ExceptionAddress;
FPlatformStackWalk::StackWalkAndDump(StackTrace, StackTraceSize, ProgramCounter);
FCString::Strncat(GErrorHist, GErrorExceptionDescription, UE_ARRAY_COUNT(GErrorHist));
FCString::Strncat(GErrorHist, TEXT("\r\n\r\n"), UE_ARRAY_COUNT(GErrorHist));
FCString::Strncat(GErrorHist, ANSI_TO_TCHAR(StackTrace), UE_ARRAY_COUNT(GErrorHist));
#if !NO_LOGGING
FDebug::LogFormattedMessageWithCallstack(LogSentrySdk.GetCategoryName(), __FILE__, __LINE__, TEXT("=== Critical error: ==="), GErrorHist, ELogVerbosity::Error);
#endif
#if !UE_VERSION_OLDER_THAN(5, 1, 0)
GLog->Panic();
#endif
GMalloc->Free(StackTrace);
#endif
}
sentry_value_t HandleBeforeSend(sentry_value_t event, void *hint, void *closure)
{
SentrySubsystemDesktop* SentrySubsystem = static_cast<SentrySubsystemDesktop*>(closure);
TSharedPtr<SentryEventDesktop> eventDesktop = MakeShareable(new SentryEventDesktop(event));
SentrySubsystem->GetCurrentScope()->Apply(eventDesktop);
FGCScopeGuard GCScopeGuard;
USentryEvent* EventToProcess = NewObject<USentryEvent>();
EventToProcess->InitWithNativeImpl(eventDesktop);
USentryEvent* ProcessedEvent = EventToProcess;
if(!FUObjectThreadContext::Get().IsRoutingPostLoad)
{
// Executing UFUNCTION is allowed only when not post-loading
ProcessedEvent = SentrySubsystem->GetBeforeSendHandler()->HandleBeforeSend(EventToProcess, nullptr);
}
return ProcessedEvent ? event : sentry_value_new_null();
}
sentry_value_t HandleBeforeCrash(const sentry_ucontext_t *uctx, sentry_value_t event, void *closure)
{
#if PLATFORM_WINDOWS
// Ensures that error message and corresponding callstack flushed to a log file (if available)
// before it's attached to the captured crash event and uploaded to Sentry.
PrintCrashLog(uctx);
#endif
SentrySubsystemDesktop* SentrySubsystem = static_cast<SentrySubsystemDesktop*>(closure);
SentrySubsystem->TryCaptureScreenshot();
if (GIsGPUCrashed)
{
IFileManager::Get().Copy(*SentrySubsystem->GetGpuDumpBackupPath(), *SentryFileUtils::GetGpuDumpPath());
}
FSentryCrashContext::Get()->Apply(SentrySubsystem->GetCurrentScope());
TSharedPtr<SentryEventDesktop> eventDesktop = MakeShareable(new SentryEventDesktop(event, true));
SentrySubsystem->GetCurrentScope()->Apply(eventDesktop);
if(!IsGarbageCollecting())
{
USentryEvent* EventToProcess = NewObject<USentryEvent>();
EventToProcess->InitWithNativeImpl(eventDesktop);
USentryEvent* ProcessedEvent = EventToProcess;
if(!FUObjectThreadContext::Get().IsRoutingPostLoad)
{
// Executing UFUNCTION is allowed only when not post-loading
ProcessedEvent = SentrySubsystem->GetBeforeSendHandler()->HandleBeforeSend(EventToProcess, nullptr);
}
return ProcessedEvent ? event : sentry_value_new_null();
}
else
{
// If crash occured during garbage collection we can't just obtain a GC lock like with normal events
// since there is no guarantee it will be ever freed. In this case crash event will be reported
// without calling a `beforeSend` handler.
return event;
}
}
SentrySubsystemDesktop::SentrySubsystemDesktop()
: beforeSend(nullptr)
, crashReporter(MakeShareable(new SentryCrashReporter))
, isEnabled(false)
, isStackTraceEnabled(true)
, isScreenshotAttachmentEnabled(false)
{
}
void SentrySubsystemDesktop::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryTraceSampler* traceSampler)
{
beforeSend = beforeSendHandler;
scopeStack.Push(MakeShareable(new SentryScopeDesktop()));
sentry_options_t* options = sentry_options_new();
if(settings->EnableAutoLogAttachment)
{
const FString LogFilePath = FGenericPlatformOutputDevices::GetAbsoluteLogFilename();
#if PLATFORM_WINDOWS
sentry_options_add_attachmentw(options, *FPaths::ConvertRelativePathToFull(LogFilePath));
#elif PLATFORM_LINUX
sentry_options_add_attachment(options, TCHAR_TO_UTF8(*FPaths::ConvertRelativePathToFull(LogFilePath)));
#endif
}
switch (settings->DatabaseLocation)
{
case ESentryDatabaseLocation::ProjectDirectory:
databaseParentPath = FPaths::ProjectDir();
break;
case ESentryDatabaseLocation::ProjectUserDirectory:
databaseParentPath = FPaths::ProjectUserDir();
break;
}
if(databaseParentPath.IsEmpty())
{
UE_LOG(LogSentrySdk, Warning, TEXT("Unknown Sentry database location. Falling back to FPaths::ProjectUserDir()."));
databaseParentPath = FPaths::ProjectUserDir();
}
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->AttachGpuDump)
{
#if PLATFORM_WINDOWS
sentry_options_add_attachmentw(options, *GetGpuDumpBackupPath());
#elif PLATFORM_LINUX
sentry_options_add_attachment(options, TCHAR_TO_UTF8(*GetGpuDumpBackupPath()));
#endif
}
if(settings->UseProxy)
{
sentry_options_set_http_proxy(options, TCHAR_TO_ANSI(*settings->ProxyUrl));
}
if(settings->EnableTracing && settings->SamplingType == ESentryTracesSamplingType::UniformSampleRate)
{
sentry_options_set_traces_sample_rate(options, settings->TracesSampleRate);
}
if(settings->EnableTracing && settings->SamplingType == ESentryTracesSamplingType::TracesSampler)
{
UE_LOG(LogSentrySdk, Warning, TEXT("The Native SDK doesn't currently support sampling functions"));
}
#if PLATFORM_WINDOWS
if(!FSentryModule::Get().IsMarketplaceVersion())
{
const FString HandlerPath = GetHandlerPath();
if(!FPaths::FileExists(HandlerPath))
{
UE_LOG(LogSentrySdk, Log, TEXT("Crashpad executable couldn't be found so Breakpad will be used instead. "
"Please make sure that the plugin was rebuilt to avoid initialization failure."));
}
sentry_options_set_handler_pathw(options, *HandlerPath);
}
#elif PLATFORM_LINUX
sentry_options_set_handler_path(options, TCHAR_TO_UTF8(*GetHandlerPath()));
#endif
#if PLATFORM_WINDOWS
sentry_options_set_database_pathw(options, *GetDatabasePath());
#elif PLATFORM_LINUX
sentry_options_set_database_path(options, TCHAR_TO_UTF8(*GetDatabasePath()));
#endif
sentry_options_set_release(options, TCHAR_TO_ANSI(settings->OverrideReleaseName
? *settings->Release
: *settings->GetFormattedReleaseName()));
sentry_options_set_dsn(options, TCHAR_TO_ANSI(*settings->Dsn));
sentry_options_set_environment(options, TCHAR_TO_ANSI(*settings->Environment));
sentry_options_set_logger(options, PrintVerboseLog, nullptr);
sentry_options_set_debug(options, settings->Debug);
sentry_options_set_auto_session_tracking(options, settings->EnableAutoSessionTracking);
sentry_options_set_sample_rate(options, settings->SampleRate);
sentry_options_set_max_breadcrumbs(options, settings->MaxBreadcrumbs);
sentry_options_set_before_send(options, HandleBeforeSend, this);
sentry_options_set_on_crash(options, HandleBeforeCrash, this);
sentry_options_set_shutdown_timeout(options, 3000);
#if PLATFORM_LINUX
sentry_options_set_transport(options, FSentryTransport::Create());
#endif
int initResult = sentry_init(options);
UE_LOG(LogSentrySdk, Log, TEXT("Sentry initialization completed with result %d (0 on success)."), initResult);
isEnabled = initResult == 0 ? true : false;
sentry_clear_crashed_last_run();
#if PLATFORM_WINDOWS && !UE_VERSION_OLDER_THAN(5, 2, 0)
FPlatformMisc::SetCrashHandlingType(settings->EnableAutoCrashCapturing
? ECrashHandlingType::Disabled
: ECrashHandlingType::Default);
#endif
isStackTraceEnabled = settings->AttachStacktrace;
crashReporter->SetRelease(settings->Release);
crashReporter->SetEnvironment(settings->Environment);
}
void SentrySubsystemDesktop::Close()
{
isEnabled = false;
sentry_close();
scopeStack.Empty();
}
bool SentrySubsystemDesktop::IsEnabled()
{
return isEnabled;
}
ESentryCrashedLastRun SentrySubsystemDesktop::IsCrashedLastRun()
{
ESentryCrashedLastRun unrealIsCrashed = ESentryCrashedLastRun::NotEvaluated;
switch (sentry_get_crashed_last_run())
{
case -1:
unrealIsCrashed = ESentryCrashedLastRun::NotEvaluated;
break;
case 0:
unrealIsCrashed = ESentryCrashedLastRun::NotCrashed;
break;
case 1:
unrealIsCrashed = ESentryCrashedLastRun::Crashed;
break;
default:
UE_LOG(LogSentrySdk, Warning, TEXT("Unknown IsCrashedLastRun result. NotEvaluated will be returned."));
}
return unrealIsCrashed;
}
void SentrySubsystemDesktop::AddBreadcrumb(TSharedPtr<ISentryBreadcrumb> breadcrumb)
{
GetCurrentScope()->AddBreadcrumb(breadcrumb);
}
void SentrySubsystemDesktop::AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap<FString, FString>& Data, ESentryLevel Level)
{
TSharedPtr<SentryBreadcrumbDesktop> breadcrumbDesktop = MakeShareable(new SentryBreadcrumbDesktop());
breadcrumbDesktop->SetMessage(Message);
breadcrumbDesktop->SetCategory(Category);
breadcrumbDesktop->SetType(Type);
breadcrumbDesktop->SetData(Data);
breadcrumbDesktop->SetLevel(Level);
GetCurrentScope()->AddBreadcrumb(breadcrumbDesktop);
}
void SentrySubsystemDesktop::ClearBreadcrumbs()
{
GetCurrentScope()->ClearBreadcrumbs();
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureMessage(const FString& message, ESentryLevel level)
{
sentry_value_t sentryEvent = sentry_value_new_message_event(SentryConvertorsDesktop::SentryLevelToNative(level), nullptr, TCHAR_TO_UTF8(*message));
if(isStackTraceEnabled)
{
sentry_value_set_stacktrace(sentryEvent, nullptr, 0);
}
sentry_uuid_t id = sentry_capture_event(sentryEvent);
return MakeShareable(new SentryIdDesktop(id));
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureMessageWithScope(const FString& message, const FSentryScopeDelegate& onScopeConfigure, ESentryLevel level)
{
FScopeLock Lock(&CriticalSection);
TSharedPtr<SentryScopeDesktop> NewLocalScope = MakeShareable(new SentryScopeDesktop(*GetCurrentScope()));
onScopeConfigure.ExecuteIfBound(NewLocalScope);
scopeStack.Push(NewLocalScope);
TSharedPtr<ISentryId> Id = CaptureMessage(message, level);
scopeStack.Pop();
return Id;
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureEvent(TSharedPtr<ISentryEvent> event)
{
TSharedPtr<SentryEventDesktop> eventDesktop = StaticCastSharedPtr<SentryEventDesktop>(event);
sentry_value_t nativeEvent = eventDesktop->GetNativeObject();
if(isStackTraceEnabled)
{
sentry_value_set_stacktrace(nativeEvent, nullptr, 0);
}
sentry_uuid_t id = sentry_capture_event(nativeEvent);
return MakeShareable(new SentryIdDesktop(id));
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureEventWithScope(TSharedPtr<ISentryEvent> event, const FSentryScopeDelegate& onScopeConfigure)
{
FScopeLock Lock(&CriticalSection);
TSharedPtr<SentryScopeDesktop> NewLocalScope = MakeShareable(new SentryScopeDesktop(*GetCurrentScope()));
onScopeConfigure.ExecuteIfBound(NewLocalScope);
scopeStack.Push(NewLocalScope);
TSharedPtr<ISentryId> Id = CaptureEvent(event);
scopeStack.Pop();
return Id;
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureException(const FString& type, const FString& message, int32 framesToSkip)
{
sentry_value_t exceptionEvent = sentry_value_new_event();
auto StackFrames = FGenericPlatformStackWalk::GetStack(framesToSkip);
sentry_value_set_by_key(exceptionEvent, "stacktrace", SentryConvertorsDesktop::CallstackToNative(StackFrames));
sentry_value_t nativeException = sentry_value_new_exception(TCHAR_TO_ANSI(*type), TCHAR_TO_ANSI(*message));
sentry_event_add_exception(exceptionEvent, nativeException);
sentry_uuid_t id = sentry_capture_event(exceptionEvent);
return MakeShareable(new SentryIdDesktop(id));
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureAssertion(const FString& type, const FString& message)
{
#if PLATFORM_WINDOWS
int32 framesToSkip = 7;
#else
int32 framesToSkip = 5;
#endif
SentryLogUtils::LogStackTrace(*message, ELogVerbosity::Error, framesToSkip);
return CaptureException(type, message, framesToSkip);
}
TSharedPtr<ISentryId> SentrySubsystemDesktop::CaptureEnsure(const FString& type, const FString& message)
{
#if PLATFORM_WINDOWS && !UE_VERSION_OLDER_THAN(5, 3, 0)
int32 framesToSkip = 8;
#else
int32 framesToSkip = 7;
#endif
return CaptureException(type, message, framesToSkip);
}
void SentrySubsystemDesktop::CaptureUserFeedback(TSharedPtr<ISentryUserFeedback> userFeedback)
{
TSharedPtr<SentryUserFeedbackDesktop> userFeedbackDesktop = StaticCastSharedPtr<SentryUserFeedbackDesktop>(userFeedback);
sentry_capture_user_feedback(userFeedbackDesktop->GetNativeObject());
}
void SentrySubsystemDesktop::SetUser(TSharedPtr<ISentryUser> user)
{
TSharedPtr<SentryUserDesktop> userDesktop = StaticCastSharedPtr<SentryUserDesktop>(user);
sentry_set_user(userDesktop->GetNativeObject());
crashReporter->SetUser(userDesktop);
}
void SentrySubsystemDesktop::RemoveUser()
{
sentry_remove_user();
crashReporter->RemoveUser();
}
void SentrySubsystemDesktop::ConfigureScope(const FSentryScopeDelegate& onConfigureScope)
{
onConfigureScope.ExecuteIfBound(GetCurrentScope());
}
void SentrySubsystemDesktop::SetContext(const FString& key, const TMap<FString, FString>& values)
{
GetCurrentScope()->SetContext(key, values);
crashReporter->SetContext(key, values);
}
void SentrySubsystemDesktop::SetTag(const FString& key, const FString& value)
{
GetCurrentScope()->SetTagValue(key, value);
crashReporter->SetTag(key, value);
}
void SentrySubsystemDesktop::RemoveTag(const FString& key)
{
GetCurrentScope()->RemoveTag(key);
crashReporter->RemoveTag(key);
}
void SentrySubsystemDesktop::SetLevel(ESentryLevel level)
{
GetCurrentScope()->SetLevel(level);
}
void SentrySubsystemDesktop::StartSession()
{
sentry_start_session();
}
void SentrySubsystemDesktop::EndSession()
{
sentry_end_session();
}
TSharedPtr<ISentryTransaction> SentrySubsystemDesktop::StartTransaction(const FString& name, const FString& operation)
{
sentry_transaction_context_t* transactionContext = sentry_transaction_context_new(TCHAR_TO_ANSI(*name), TCHAR_TO_ANSI(*operation));
sentry_transaction_t* nativeTransaction = sentry_transaction_start(transactionContext, sentry_value_new_null());
return MakeShareable(new SentryTransactionDesktop(nativeTransaction));
}
TSharedPtr<ISentryTransaction> SentrySubsystemDesktop::StartTransactionWithContext(TSharedPtr<ISentryTransactionContext> context)
{
TSharedPtr<SentryTransactionContextDesktop> transactionContextDesktop = StaticCastSharedPtr<SentryTransactionContextDesktop>(context);
sentry_transaction_t* nativeTransaction = sentry_transaction_start(transactionContextDesktop->GetNativeObject(), sentry_value_new_null());
return MakeShareable(new SentryTransactionDesktop(nativeTransaction));
}
TSharedPtr<ISentryTransaction> SentrySubsystemDesktop::StartTransactionWithContextAndTimestamp(TSharedPtr<ISentryTransactionContext> context, int64 timestamp)
{
TSharedPtr<SentryTransactionContextDesktop> transactionContextDesktop = StaticCastSharedPtr<SentryTransactionContextDesktop>(context);
sentry_transaction_t* nativeTransaction = sentry_transaction_start_ts(transactionContextDesktop->GetNativeObject(), sentry_value_new_null(), timestamp);
return MakeShareable(new SentryTransactionDesktop(nativeTransaction));
}
TSharedPtr<ISentryTransaction> SentrySubsystemDesktop::StartTransactionWithContextAndOptions(TSharedPtr<ISentryTransactionContext> context, const TMap<FString, FString>& options)
{
UE_LOG(LogSentrySdk, Log, TEXT("Transaction options currently not supported on desktop."));
return StartTransactionWithContext(context);
}
TSharedPtr<ISentryTransactionContext> SentrySubsystemDesktop::ContinueTrace(const FString& sentryTrace, const TArray<FString>& baggageHeaders)
{
sentry_transaction_context_t* nativeTransactionContext = sentry_transaction_context_new("<unlabeled transaction>", "default");
sentry_transaction_context_update_from_header(nativeTransactionContext, "sentry-trace", TCHAR_TO_ANSI(*sentryTrace));
// currently `sentry-native` doesn't have API for `sentry_transaction_context_t` to set `baggageHeaders`
TSharedPtr<SentryTransactionContextDesktop> transactionContextDesktop = MakeShareable(new SentryTransactionContextDesktop(nativeTransactionContext));
return transactionContextDesktop;
}
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());
}
FString SentrySubsystemDesktop::GetGpuDumpBackupPath() const
{
static const FString DateTimeString = FDateTime::Now().ToString();
const FString GpuDumpPath = FPaths::Combine(GetDatabasePath(), TEXT("gpudumps"), *FString::Printf(TEXT("UEAftermath-%s.nv-gpudmp"), *DateTimeString));;
const FString GpuDumpFullPath = FPaths::ConvertRelativePathToFull(GpuDumpPath);
return GpuDumpFullPath;
}
TSharedPtr<SentryScopeDesktop> SentrySubsystemDesktop::GetCurrentScope()
{
if(scopeStack.Num() == 0)
{
UE_LOG(LogSentrySdk, Warning, TEXT("Scope stack is empty."));
return nullptr;
}
return scopeStack.Top();
}
FString SentrySubsystemDesktop::GetHandlerPath() const
{
#if PLATFORM_WINDOWS
const FString HandlerExecutableName = TEXT("crashpad_handler.exe");
#elif PLATFORM_LINUX
const FString HandlerExecutableName = TEXT("crashpad_handler");
#endif
const FString HandlerPath = FPaths::Combine(FSentryModule::Get().GetBinariesPath(), HandlerExecutableName);
const FString HandlerFullPath = FPaths::ConvertRelativePathToFull(HandlerPath);
return HandlerFullPath;
}
FString SentrySubsystemDesktop::GetDatabasePath() const
{
const FString DatabasePath = FPaths::Combine(databaseParentPath, TEXT(".sentry-native"));
const FString DatabaseFullPath = FPaths::ConvertRelativePathToFull(DatabasePath);
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