diff --git a/CHANGELOG.md b/CHANGELOG.md index d45ecb62..f67a5141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes +- The SDK now correctly captures and groups Assertions ([#537](https://github.com/getsentry/sentry-unreal/pull/537)) - Add path strings escaping for debug symbol upload script ([#561](https://github.com/getsentry/sentry-unreal/pull/561)) - Fix crashes not being reported during garbage collection ([#566](https://github.com/getsentry/sentry-unreal/pull/566)) - The SDK now uploads debug symbols properly with the `Android File Server` plugin enabled in UE 5.0 and newer ([#568](https://github.com/getsentry/sentry-unreal/pull/568)) diff --git a/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java b/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java index 482466cd..30049182 100644 --- a/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java +++ b/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java @@ -3,7 +3,6 @@ package io.sentry.unreal; import android.app.Activity; -import android.util.Log; import androidx.annotation.NonNull; @@ -11,7 +10,9 @@ import org.json.JSONException; import org.json.JSONObject; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import io.sentry.Breadcrumb; @@ -26,7 +27,10 @@ import io.sentry.SentryOptions; import io.sentry.android.core.SentryAndroid; import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryId; +import io.sentry.protocol.SentryStackFrame; +import io.sentry.protocol.SentryStackTrace; public class SentryBridgeJava { public static native void onConfigureScope(long callbackAddr, IScope scope); @@ -53,6 +57,7 @@ public void configure(SentryAndroidOptions options) { options.setBeforeSend(new SentryOptions.BeforeSendCallback() { @Override public SentryEvent execute(SentryEvent event, Hint hint) { + preProcessEvent(event); return onBeforeSend(beforeSendHandler, event, hint); } }); @@ -73,12 +78,12 @@ public SentryEvent execute(SentryEvent event, Hint hint) { options.setTracesSampler(new SentryOptions.TracesSamplerCallback() { @Override public Double sample(SamplingContext samplingContext) { - float sampleRate = onTracesSampler(samplerAddr, samplingContext); - if(sampleRate >= 0.0f) { - return (double) sampleRate; - } else { - return null; - } + float sampleRate = onTracesSampler(samplerAddr, samplingContext); + if(sampleRate >= 0.0f) { + return (double) sampleRate; + } else { + return null; + } } }); } @@ -89,6 +94,24 @@ public Double sample(SamplingContext samplingContext) { }); } + private static void preProcessEvent(SentryEvent event) { + if (event.getTags().containsKey("sentry_unreal_exception")) { + SentryException exception = event.getUnhandledException(); + if (exception != null) { + exception.setType(event.getTag("sentry_unreal_exception_type")); + exception.setValue(event.getTag("sentry_unreal_exception_message")); + SentryStackTrace trace = exception.getStacktrace(); + int numFramesToSkip = Integer.parseInt(event.getTag("sentry_unreal_exception_skip_frames")); + List frames = trace.getFrames(); + trace.setFrames(frames.subList(0, frames.size() - numFramesToSkip)); + } + event.removeTag("sentry_unreal_exception_type"); + event.removeTag("sentry_unreal_exception_message"); + event.removeTag("sentry_unreal_exception_skip_frames"); + event.removeTag("sentry_unreal_exception"); + } + } + public static void addBreadcrumb(final String message, final String category, final String type, final HashMap data, final SentryLevel level) { Breadcrumb breadcrumb = new Breadcrumb(); breadcrumb.setMessage(message); @@ -106,8 +129,8 @@ public static SentryId captureMessageWithScope(final String message, final Sentr SentryId messageId = Sentry.captureMessage(message, new ScopeCallback() { @Override public void run(@NonNull IScope scope) { - scope.setLevel(level); - onConfigureScope(callback, scope); + scope.setLevel(level); + onConfigureScope(callback, scope); } }); return messageId; @@ -117,12 +140,22 @@ public static SentryId captureEventWithScope(final SentryEvent event, final long SentryId eventId = Sentry.captureEvent(event, new ScopeCallback() { @Override public void run(@NonNull IScope scope) { - onConfigureScope(callback, scope); + onConfigureScope(callback, scope); } }); return eventId; } + public static SentryId captureException(final String type, final String value) { + SentryException exception = new SentryException(); + exception.setType(type); + exception.setValue(value); + SentryEvent event = new SentryEvent(); + event.setExceptions(Collections.singletonList(exception)); + SentryId eventId = Sentry.captureEvent(event); + return eventId; + } + public static void configureScope(final long callback) { Sentry.configureScope(new ScopeCallback() { @Override diff --git a/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.cpp b/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.cpp index e9970c60..63ae7c3d 100644 --- a/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.cpp +++ b/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.cpp @@ -169,6 +169,34 @@ USentryId* SentrySubsystemAndroid::CaptureEventWithScope(USentryEvent* event, co return SentryConvertorsAndroid::SentryIdToUnreal(*id); } +USentryId* SentrySubsystemAndroid::CaptureException(const FString& type, const FString& message, int32 framesToSkip) +{ + return nullptr; +} + +USentryId* SentrySubsystemAndroid::CaptureAssertion(const FString& type, const FString& message) +{ + const int32 framesToSkip = 8; + + // add marker tags specific for Unreal assertions + SetTag(TEXT("sentry_unreal_exception"), TEXT("assert")); + SetTag(TEXT("sentry_unreal_exception_skip_frames"), FString::Printf(TEXT("%d"), framesToSkip)); + SetTag(TEXT("sentry_unreal_exception_type"), type); + SetTag(TEXT("sentry_unreal_exception_message"), message); + + PLATFORM_BREAK(); + + return nullptr; +} + +USentryId* SentrySubsystemAndroid::CaptureEnsure(const FString& type, const FString& message) +{ + auto id = FSentryJavaObjectWrapper::CallStaticObjectMethod(SentryJavaClasses::SentryBridgeJava, "captureException", "(Ljava/lang/String;Ljava/lang/String;)Lio/sentry/protocol/SentryId;", + *FSentryJavaObjectWrapper::GetJString(type), *FSentryJavaObjectWrapper::GetJString(message)); + + return SentryConvertorsAndroid::SentryIdToUnreal(*id); +} + void SentrySubsystemAndroid::CaptureUserFeedback(USentryUserFeedback* userFeedback) { TSharedPtr userFeedbackAndroid = StaticCastSharedPtr(userFeedback->GetNativeImpl()); diff --git a/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.h b/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.h index 1729fc4b..eb3bbe56 100644 --- a/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.h +++ b/plugin-dev/Source/Sentry/Private/Android/SentrySubsystemAndroid.h @@ -18,6 +18,9 @@ class SentrySubsystemAndroid : public ISentrySubsystem virtual USentryId* CaptureMessageWithScope(const FString& message, const FConfigureScopeNativeDelegate& onConfigureScope, ESentryLevel level) override; virtual USentryId* CaptureEvent(USentryEvent* event) override; virtual USentryId* CaptureEventWithScope(USentryEvent* event, const FConfigureScopeNativeDelegate& onConfigureScope) override; + virtual USentryId* CaptureException(const FString& type, const FString& message, int32 framesToSkip) override; + virtual USentryId* CaptureAssertion(const FString& type, const FString& message) override; + virtual USentryId* CaptureEnsure(const FString& type, const FString& message) override; virtual void CaptureUserFeedback(USentryUserFeedback* userFeedback) override; virtual void SetUser(USentryUser* user) override; virtual void RemoveUser() override; diff --git a/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.cpp b/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.cpp index 6c47e05d..3a2f8034 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.cpp +++ b/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.cpp @@ -15,6 +15,8 @@ #include "Apple/SentryTransactionApple.h" #include "Apple/SentrySpanApple.h" +#include "Convenience/SentryMacro.h" + SentryLevel SentryConvertorsApple::SentryLevelToNative(ESentryLevel level) { SentryLevel nativeLevel = kSentryLevelDebug; @@ -72,6 +74,24 @@ NSData* SentryConvertorsApple::ByteDataToNative(const TArray& array) return [NSData dataWithBytes:array.GetData() length:array.Num()]; } +SentryStacktrace* SentryConvertorsApple::CallstackToNative(const TArray& callstack) +{ + int32 framesCount = callstack.Num(); + + NSMutableArray *arr = [NSMutableArray arrayWithCapacity:framesCount]; + + for (int i = 0; i < framesCount; ++i) + { + SentryFrame *frame = [[SENTRY_APPLE_CLASS(SentryFrame) alloc] init]; + frame.instructionAddress = FString::Printf(TEXT("0x%llx"), callstack[framesCount - i - 1].ProgramCounter).GetNSString(); + [arr addObject:frame]; + } + + SentryStacktrace *trace = [[SENTRY_APPLE_CLASS(SentryStacktrace) alloc] initWithFrames:arr registers:@{}]; + + return trace; +} + ESentryLevel SentryConvertorsApple::SentryLevelToUnreal(SentryLevel level) { ESentryLevel unrealLevel = ESentryLevel::Debug; diff --git a/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.h b/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.h index 68b63bfc..4b20b697 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.h +++ b/plugin-dev/Source/Sentry/Private/Apple/Infrastructure/SentryConvertorsApple.h @@ -6,6 +6,8 @@ #include "Convenience/SentryInclude.h" +#include "GenericPlatform/GenericPlatformStackWalk.h" + class USentryTransactionContext; class USentryScope; class USentryId; @@ -20,6 +22,7 @@ class SentryConvertorsApple static NSDictionary* StringMapToNative(const TMap& map); static NSArray* StringArrayToNative(const TArray& array); static NSData* ByteDataToNative(const TArray& array); + static SentryStacktrace* CallstackToNative(const TArray& callstack); /** Conversions from native iOS types */ static ESentryLevel SentryLevelToUnreal(SentryLevel level); diff --git a/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.cpp b/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.cpp index 6121c71f..2bfe2994 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.cpp +++ b/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.cpp @@ -172,6 +172,37 @@ USentryId* SentrySubsystemApple::CaptureEventWithScope(USentryEvent* event, cons return SentryConvertorsApple::SentryIdToUnreal(id); } +USentryId* SentrySubsystemApple::CaptureException(const FString& type, const FString& message, int32 framesToSkip) +{ + auto StackFrames = FGenericPlatformStackWalk::GetStack(framesToSkip); + + SentryException *nativeException = [[SENTRY_APPLE_CLASS(SentryException) alloc] initWithValue:message.GetNSString() type:type.GetNSString()]; + NSMutableArray *nativeExceptionArray = [NSMutableArray arrayWithCapacity:1]; + [nativeExceptionArray addObject:nativeException]; + + SentryEvent *exceptionEvent = [[SENTRY_APPLE_CLASS(SentryEvent) alloc] init]; + exceptionEvent.exceptions = nativeExceptionArray; + exceptionEvent.stacktrace = SentryConvertorsApple::CallstackToNative(StackFrames); + + SentryId* id = [SENTRY_APPLE_CLASS(SentrySDK) captureEvent:exceptionEvent]; + return SentryConvertorsApple::SentryIdToUnreal(id); +} + +USentryId* SentrySubsystemApple::CaptureAssertion(const FString& type, const FString& message) +{ +#if PLATFORM_MAC + int32 framesToSkip = 6; +#elif PLATFORM_IOS + int32 framesToSkip = 5; +#endif + return CaptureException(type, message, framesToSkip); +} + +USentryId* SentrySubsystemApple::CaptureEnsure(const FString& type, const FString& message) +{ + return CaptureException(type, message, 6); +} + void SentrySubsystemApple::CaptureUserFeedback(USentryUserFeedback* userFeedback) { TSharedPtr userFeedbackIOS = StaticCastSharedPtr(userFeedback->GetNativeImpl()); diff --git a/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.h b/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.h index a4df5e7e..14c880f4 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.h +++ b/plugin-dev/Source/Sentry/Private/Apple/SentrySubsystemApple.h @@ -18,6 +18,9 @@ class SentrySubsystemApple : public ISentrySubsystem virtual USentryId* CaptureMessageWithScope(const FString& message, const FConfigureScopeNativeDelegate& onConfigureScope, ESentryLevel level) override; virtual USentryId* CaptureEvent(USentryEvent* event) override; virtual USentryId* CaptureEventWithScope(USentryEvent* event, const FConfigureScopeNativeDelegate& onConfigureScope) override; + virtual USentryId* CaptureException(const FString& type, const FString& message, int32 framesToSkip) override; + virtual USentryId* CaptureAssertion(const FString& type, const FString& message) override; + virtual USentryId* CaptureEnsure(const FString& type, const FString& message) override; virtual void CaptureUserFeedback(USentryUserFeedback* userFeedback) override; virtual void SetUser(USentryUser* user) override; virtual void RemoveUser() override; diff --git a/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.cpp b/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.cpp index f63bfd79..ee98aa72 100644 --- a/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.cpp +++ b/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.cpp @@ -71,6 +71,36 @@ sentry_value_t SentryConvertorsDesktop::StringArrayToNative(const TArray= sizeof(buffer)) + { + return sentry_value_new_null(); + } + buffer[written] = '\0'; + return sentry_value_new_string(buffer); +} + +sentry_value_t SentryConvertorsDesktop::CallstackToNative(const TArray& callstack) +{ + int32 framesCount = callstack.Num(); + + sentry_value_t frames = sentry_value_new_list(); + for (int i = 0; i < framesCount; ++i) + { + sentry_value_t frame = sentry_value_new_object(); + sentry_value_set_by_key(frame, "instruction_addr", AddressToNative(callstack[framesCount - i - 1].ProgramCounter)); + sentry_value_append(frames, frame); + } + + sentry_value_t stacktrace = sentry_value_new_object(); + sentry_value_set_by_key(stacktrace, "frames", frames); + + return stacktrace; +} + ESentryLevel SentryConvertorsDesktop::SentryLevelToUnreal(sentry_value_t level) { FString levelStr = FString(sentry_value_as_string(level)); diff --git a/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.h b/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.h index 138ac751..66a2e84d 100644 --- a/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.h +++ b/plugin-dev/Source/Sentry/Private/Desktop/Infrastructure/SentryConvertorsDesktop.h @@ -6,6 +6,8 @@ #include "Convenience/SentryInclude.h" +#include "GenericPlatform/GenericPlatformStackWalk.h" + #if USE_SENTRY_NATIVE class USentryId; @@ -18,7 +20,9 @@ class SentryConvertorsDesktop /** Conversions to native desktop (Windows/Mac) types */ static sentry_level_e SentryLevelToNative(ESentryLevel level); static sentry_value_t StringMapToNative(const TMap& map); - static sentry_value_t StringArrayToNative(const TArray& array ); + static sentry_value_t StringArrayToNative(const TArray& array); + static sentry_value_t AddressToNative(uint64 address); + static sentry_value_t CallstackToNative(const TArray& callstack); /** Conversions from native desktop (Windows/Mac) types */ static ESentryLevel SentryLevelToUnreal(sentry_value_t level); diff --git a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp index 6fdae76c..759c13f4 100644 --- a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp +++ b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.cpp @@ -179,6 +179,7 @@ void SentrySubsystemDesktop::InitWithSettings(const USentrySettings* settings, U 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()); @@ -326,6 +327,35 @@ USentryId* SentrySubsystemDesktop::CaptureEventWithScope(USentryEvent* event, co return Id; } +USentryId* 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 SentryConvertorsDesktop::SentryIdToUnreal(id); +} + +USentryId* SentrySubsystemDesktop::CaptureAssertion(const FString& type, const FString& message) +{ + return CaptureException(type, message, 7); +} + +USentryId* SentrySubsystemDesktop::CaptureEnsure(const FString& type, const FString& message) +{ +#if PLATFORM_WINDOWS && ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 3 + int32 framesToSkip = 8; +#else + int32 framesToSkip = 7; +#endif + return CaptureException(type, message, framesToSkip); +} + void SentrySubsystemDesktop::CaptureUserFeedback(USentryUserFeedback* userFeedback) { TSharedPtr userFeedbackDesktop = StaticCastSharedPtr(userFeedback->GetNativeImpl()); diff --git a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h index 9ea3159f..0d594d7f 100644 --- a/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h +++ b/plugin-dev/Source/Sentry/Private/Desktop/SentrySubsystemDesktop.h @@ -27,6 +27,9 @@ class SentrySubsystemDesktop : public ISentrySubsystem virtual USentryId* CaptureMessageWithScope(const FString& message, const FConfigureScopeNativeDelegate& onScopeConfigure, ESentryLevel level) override; virtual USentryId* CaptureEvent(USentryEvent* event) override; virtual USentryId* CaptureEventWithScope(USentryEvent* event, const FConfigureScopeNativeDelegate& onScopeConfigure) override; + virtual USentryId* CaptureException(const FString& type, const FString& message, int32 framesToSkip) override; + virtual USentryId* CaptureAssertion(const FString& type, const FString& message) override; + virtual USentryId* CaptureEnsure(const FString& type, const FString& message) override; virtual void CaptureUserFeedback(USentryUserFeedback* userFeedback) override; virtual void SetUser(USentryUser* user) override; virtual void RemoveUser() override; diff --git a/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h b/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h index b1012209..89b1574a 100644 --- a/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h +++ b/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h @@ -34,6 +34,9 @@ class ISentrySubsystem virtual USentryId* CaptureMessageWithScope(const FString& message, const FConfigureScopeNativeDelegate& onConfigureScope, ESentryLevel level) = 0; virtual USentryId* CaptureEvent(USentryEvent* event) = 0; virtual USentryId* CaptureEventWithScope(USentryEvent* event, const FConfigureScopeNativeDelegate& onConfigureScope) = 0; + virtual USentryId* CaptureException(const FString& type, const FString& message, int32 framesToSkip = 0) = 0; + virtual USentryId* CaptureAssertion(const FString& type, const FString& message) = 0; + virtual USentryId* CaptureEnsure(const FString& type, const FString& message) = 0; virtual void CaptureUserFeedback(USentryUserFeedback* userFeedback) = 0; virtual void SetUser(USentryUser* user) = 0; virtual void RemoveUser() = 0; diff --git a/plugin-dev/Source/Sentry/Private/SentryOutputDevice.cpp b/plugin-dev/Source/Sentry/Private/SentryOutputDevice.cpp index fe93b045..31656098 100644 --- a/plugin-dev/Source/Sentry/Private/SentryOutputDevice.cpp +++ b/plugin-dev/Source/Sentry/Private/SentryOutputDevice.cpp @@ -11,6 +11,12 @@ void FSentryOutputDevice::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) { + const FString Message = FString(V).TrimStartAndEnd(); + if (Message.IsEmpty()) + { + return; + } + const USentrySettings* Settings = FSentryModule::Get().GetSettings(); bool bAddBreadcrumb; @@ -56,7 +62,7 @@ void FSentryOutputDevice::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosit return; } - SentrySubsystem->AddBreadcrumbWithParams(V, Category.ToString(), FString(), TMap(), BreadcrumbLevel); + SentrySubsystem->AddBreadcrumbWithParams(Message, Category.ToString(), FString(), TMap(), BreadcrumbLevel); } bool FSentryOutputDevice::CanBeUsedOnAnyThread() const diff --git a/plugin-dev/Source/Sentry/Private/SentryOutputDeviceError.cpp b/plugin-dev/Source/Sentry/Private/SentryOutputDeviceError.cpp new file mode 100644 index 00000000..8f2493c0 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/SentryOutputDeviceError.cpp @@ -0,0 +1,36 @@ +// Copyright (c) 2024 Sentry. All Rights Reserved. + +#include "SentryOutputDeviceError.h" + +#include "Misc/AssertionMacros.h" + +FSentryOutputDeviceError::FSentryOutputDeviceError(FOutputDeviceError* Parent) + : ParentDevice(Parent) +{ +} + +void FSentryOutputDeviceError::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) +{ + if(FDebug::HasAsserted()) + { + OnError.Broadcast(V); + } + + if (!ParentDevice) + return; + + ParentDevice->Serialize(V, Verbosity, Category); +} + +void FSentryOutputDeviceError::HandleError() +{ + if (!ParentDevice) + return; + + ParentDevice->HandleError(); +} + +FOutputDeviceError* FSentryOutputDeviceError::GetParentDevice() +{ + return ParentDevice; +} diff --git a/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp index a785559d..993d3dd8 100644 --- a/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp @@ -15,6 +15,7 @@ #include "SentryOutputDevice.h" #include "CoreGlobals.h" +#include "SentryOutputDeviceError.h" #include "Engine/World.h" #include "Misc/EngineVersion.h" #include "Misc/CoreDelegates.h" @@ -118,12 +119,14 @@ void USentrySubsystem::Initialize() PromoteTags(); ConfigureBreadcrumbs(); - OutputDevice = MakeShareable(new FSentryOutputDevice()); - if(OutputDevice) + ConfigureOutputDevice(); + ConfigureOutputDeviceError(); + + FCoreDelegates::OnHandleSystemEnsure.AddLambda([this]() { - GLog->AddOutputDevice(OutputDevice.Get()); - GLog->SerializeBacklog(OutputDevice.Get()); - } + FString EnsureMessage = GErrorHist; + SubsystemNativeImpl->CaptureEnsure(TEXT("Ensure failed"), EnsureMessage.TrimStartAndEnd()); + }); } void USentrySubsystem::InitializeWithSettings(const FConfigureSettingsDelegate& OnConfigureSettings) @@ -142,6 +145,11 @@ void USentrySubsystem::Close() GLog->RemoveOutputDevice(OutputDevice.Get()); } + if(GError && OutputDeviceError) + { + GError = OutputDeviceError->GetParentDevice(); + } + if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) return; @@ -198,9 +206,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) @@ -221,8 +227,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) @@ -610,3 +615,32 @@ bool USentrySubsystem::IsPromotedBuildsOnlyEnabled() return Settings->EnableForPromotedBuildsOnly; } + +void USentrySubsystem::ConfigureOutputDevice() +{ + OutputDevice = MakeShareable(new FSentryOutputDevice()); + if (OutputDevice) + { + GLog->AddOutputDevice(OutputDevice.Get()); + GLog->SerializeBacklog(OutputDevice.Get()); + } +} + +void USentrySubsystem::ConfigureOutputDeviceError() +{ + OutputDeviceError = MakeShareable(new FSentryOutputDeviceError(GError)); + if (OutputDeviceError) + { + OutputDeviceError->OnError.AddLambda([this](const FString& Message) + { + SubsystemNativeImpl->CaptureAssertion(TEXT("Assertion failed"), Message); + + // Shut things down before exiting to ensure all the outgoing events are sent to Sentry + Close(); + + FPlatformMisc::RequestExit( true); + }); + + GError = OutputDeviceError.Get(); + } +} diff --git a/plugin-dev/Source/Sentry/Public/SentryOutputDeviceError.h b/plugin-dev/Source/Sentry/Public/SentryOutputDeviceError.h new file mode 100644 index 00000000..1ea4c63f --- /dev/null +++ b/plugin-dev/Source/Sentry/Public/SentryOutputDeviceError.h @@ -0,0 +1,21 @@ +// Copyright (c) 2024 Sentry. All Rights Reserved. + +#pragma once + +#include "Misc/OutputDeviceError.h" + +class FSentryOutputDeviceError : public FOutputDeviceError +{ +public: + FSentryOutputDeviceError(FOutputDeviceError* Parent); + + virtual void Serialize( const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override; + virtual void HandleError() override; + + FOutputDeviceError* GetParentDevice(); + + TMulticastDelegate OnError; + +private: + FOutputDeviceError* ParentDevice; +}; diff --git a/plugin-dev/Source/Sentry/Public/SentrySubsystem.h b/plugin-dev/Source/Sentry/Public/SentrySubsystem.h index dff28abf..0b901037 100644 --- a/plugin-dev/Source/Sentry/Public/SentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Public/SentrySubsystem.h @@ -23,6 +23,7 @@ class USentryTransactionContext; class ISentrySubsystem; class FSentryOutputDevice; +class FSentryOutputDeviceError; DECLARE_DYNAMIC_DELEGATE_OneParam(FConfigureSettingsDelegate, USentrySettings*, Settings); @@ -303,10 +304,17 @@ class SENTRY_API USentrySubsystem : public UEngineSubsystem /** Check whether the event capturing should be enabled for promoted builds only */ bool IsPromotedBuildsOnlyEnabled(); + /** Add custom Sentry output device to intercept logs */ + void ConfigureOutputDevice(); + + /** Add custom Sentry output device to intercept errors */ + void ConfigureOutputDeviceError(); + private: TSharedPtr SubsystemNativeImpl; TSharedPtr OutputDevice; + TSharedPtr OutputDeviceError; UPROPERTY() USentryBeforeSendHandler* BeforeSendHandler; diff --git a/sample/Content/UI/W_SentryDemo.uasset b/sample/Content/UI/W_SentryDemo.uasset index 5a672861..1e93cc59 100644 Binary files a/sample/Content/UI/W_SentryDemo.uasset and b/sample/Content/UI/W_SentryDemo.uasset differ diff --git a/sample/Source/SentryPlayground/SentryPlaygroundUtils.cpp b/sample/Source/SentryPlayground/SentryPlaygroundUtils.cpp index 85182cbc..faf3c5b4 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundUtils.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundUtils.cpp @@ -34,6 +34,12 @@ void USentryPlaygroundUtils::Terminate(ESentryAppTerminationType Type) check(assertPtr != nullptr); } break; + case ESentryAppTerminationType::Ensure: + { + char *ensurePtr = nullptr; + ensure(ensurePtr != nullptr); + } + break; default: { UE_LOG(LogTemp, Warning, TEXT("Uknown app termination type!")); diff --git a/sample/Source/SentryPlayground/SentryPlaygroundUtils.h b/sample/Source/SentryPlayground/SentryPlaygroundUtils.h index de75f747..41614366 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundUtils.h +++ b/sample/Source/SentryPlayground/SentryPlaygroundUtils.h @@ -11,7 +11,8 @@ enum class ESentryAppTerminationType : uint8 ArrayOutOfBounds, BadFunctionPtr, InvalidMemoryAccess, - Assert + Assert, + Ensure }; UCLASS() diff --git a/scripts/packaging/package-github.snapshot b/scripts/packaging/package-github.snapshot index a179d301..0bdd9d87 100644 --- a/scripts/packaging/package-github.snapshot +++ b/scripts/packaging/package-github.snapshot @@ -149,6 +149,7 @@ Source/Sentry/Private/SentryId.cpp Source/Sentry/Private/SentryLibrary.cpp Source/Sentry/Private/SentryModule.cpp Source/Sentry/Private/SentryOutputDevice.cpp +Source/Sentry/Private/SentryOutputDeviceError.cpp Source/Sentry/Private/SentrySamplingContext.cpp Source/Sentry/Private/SentryScope.cpp Source/Sentry/Private/SentrySettings.cpp @@ -179,6 +180,7 @@ Source/Sentry/Public/SentryId.h Source/Sentry/Public/SentryLibrary.h Source/Sentry/Public/SentryModule.h Source/Sentry/Public/SentryOutputDevice.h +Source/Sentry/Public/SentryOutputDeviceError.h Source/Sentry/Public/SentrySamplingContext.h Source/Sentry/Public/SentryScope.h Source/Sentry/Public/SentrySettings.h diff --git a/scripts/packaging/package-marketplace.snapshot b/scripts/packaging/package-marketplace.snapshot index 7c60fb0d..64bdb571 100644 --- a/scripts/packaging/package-marketplace.snapshot +++ b/scripts/packaging/package-marketplace.snapshot @@ -147,6 +147,7 @@ Source/Sentry/Private/SentryId.cpp Source/Sentry/Private/SentryLibrary.cpp Source/Sentry/Private/SentryModule.cpp Source/Sentry/Private/SentryOutputDevice.cpp +Source/Sentry/Private/SentryOutputDeviceError.cpp Source/Sentry/Private/SentrySamplingContext.cpp Source/Sentry/Private/SentryScope.cpp Source/Sentry/Private/SentrySettings.cpp @@ -177,6 +178,7 @@ Source/Sentry/Public/SentryId.h Source/Sentry/Public/SentryLibrary.h Source/Sentry/Public/SentryModule.h Source/Sentry/Public/SentryOutputDevice.h +Source/Sentry/Public/SentryOutputDeviceError.h Source/Sentry/Public/SentrySamplingContext.h Source/Sentry/Public/SentryScope.h Source/Sentry/Public/SentrySettings.h