diff --git a/CHANGELOG.md b/CHANGELOG.md index b24e39809..53a43f242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add Metrics API support ([#1214](https://github.com/getsentry/sentry-unreal/pull/1214)) + ### Dependencies - Bump Cocoa SDK from v9.3.0 to v9.4.0 ([#1218](https://github.com/getsentry/sentry-unreal/pull/1218)) diff --git a/plugin-dev/Source/Sentry/Private/Android/AndroidSentryMetric.cpp b/plugin-dev/Source/Sentry/Private/Android/AndroidSentryMetric.cpp new file mode 100644 index 000000000..5819c618a --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Android/AndroidSentryMetric.cpp @@ -0,0 +1,134 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "AndroidSentryMetric.h" + +#include "Infrastructure/AndroidSentryConverters.h" +#include "Infrastructure/AndroidSentryJavaClasses.h" + +FAndroidSentryMetric::FAndroidSentryMetric() + : FSentryJavaObjectWrapper(SentryJavaClasses::SentryMetricsEvent, "()V") +{ + SetupClassMethods(); +} + +FAndroidSentryMetric::FAndroidSentryMetric(jobject metricEvent) + : FSentryJavaObjectWrapper(SentryJavaClasses::SentryMetricsEvent, metricEvent) +{ + SetupClassMethods(); +} + +void FAndroidSentryMetric::SetupClassMethods() +{ + SetNameMethod = GetMethod("setName", "(Ljava/lang/String;)V"); + GetNameMethod = GetMethod("getName", "()Ljava/lang/String;"); + SetTypeMethod = GetMethod("setType", "(Ljava/lang/String;)V"); + GetTypeMethod = GetMethod("getType", "()Ljava/lang/String;"); + SetValueMethod = GetMethod("setValue", "(Ljava/lang/Double;)V"); + GetValueMethod = GetMethod("getValue", "()Ljava/lang/Double;"); + SetUnitMethod = GetMethod("setUnit", "(Ljava/lang/String;)V"); + GetUnitMethod = GetMethod("getUnit", "()Ljava/lang/String;"); +} + +void FAndroidSentryMetric::SetName(const FString& name) +{ + CallMethod(SetNameMethod, *GetJString(name)); +} + +FString FAndroidSentryMetric::GetName() const +{ + return CallMethod(GetNameMethod); +} + +void FAndroidSentryMetric::SetType(const FString& type) +{ + CallMethod(SetTypeMethod, *GetJString(type)); +} + +FString FAndroidSentryMetric::GetType() const +{ + return CallMethod(GetTypeMethod); +} + +void FAndroidSentryMetric::SetValue(float value) +{ + TSharedPtr javaDouble = MakeShareable(new FSentryJavaObjectWrapper(SentryJavaClasses::Double, "(D)V", static_cast(value))); + CallMethod(SetValueMethod, javaDouble->GetJObject()); +} + +float FAndroidSentryMetric::GetValue() const +{ + auto valueObject = CallObjectMethod(GetValueMethod); + if (!valueObject) + { + return 0.0f; + } + + FSentryJavaObjectWrapper valueWrapper(SentryJavaClasses::Double, *valueObject); + FSentryJavaMethod floatValueMethod = valueWrapper.GetMethod("floatValue", "()F"); + return valueWrapper.CallMethod(floatValueMethod); +} + +void FAndroidSentryMetric::SetUnit(const FString& unit) +{ + CallMethod(SetUnitMethod, *GetJString(unit)); +} + +FString FAndroidSentryMetric::GetUnit() const +{ + return CallMethod(GetUnitMethod); +} + +void FAndroidSentryMetric::SetAttribute(const FString& key, const FSentryVariant& value) +{ + TSharedPtr attribute = FAndroidSentryConverters::VariantToNative(value); + + if (!attribute) + { + return; + } + + CallStaticMethod(SentryJavaClasses::SentryBridgeJava, "setMetricAttribute", "(Lio/sentry/SentryMetricsEvent;Ljava/lang/String;Ljava/lang/Object;)V", + GetJObject(), *GetJString(key), attribute->GetJObject()); +} + +FSentryVariant FAndroidSentryMetric::GetAttribute(const FString& key) const +{ + auto attribute = CallStaticObjectMethod(SentryJavaClasses::SentryBridgeJava, "getMetricAttribute", "(Lio/sentry/SentryMetricsEvent;Ljava/lang/String;)Ljava/lang/Object;", + GetJObject(), *GetJString(key)); + + if (!attribute) + { + return FSentryVariant(); + } + + return FAndroidSentryConverters::VariantToUnreal(*attribute); +} + +bool FAndroidSentryMetric::TryGetAttribute(const FString& key, FSentryVariant& value) const +{ + auto attribute = CallStaticObjectMethod(SentryJavaClasses::SentryBridgeJava, "getMetricAttribute", "(Lio/sentry/SentryMetricsEvent;Ljava/lang/String;)Ljava/lang/Object;", + GetJObject(), *GetJString(key)); + + if (!attribute) + { + return false; + } + + value = FAndroidSentryConverters::VariantToUnreal(*attribute); + + return true; +} + +void FAndroidSentryMetric::RemoveAttribute(const FString& key) +{ + CallStaticMethod(SentryJavaClasses::SentryBridgeJava, "removeMetricAttribute", "(Lio/sentry/SentryMetricsEvent;Ljava/lang/String;)V", + GetJObject(), *GetJString(key)); +} + +void FAndroidSentryMetric::AddAttributes(const TMap& attributes) +{ + for (const auto& pair : attributes) + { + SetAttribute(pair.Key, pair.Value); + } +} diff --git a/plugin-dev/Source/Sentry/Private/Android/AndroidSentryMetric.h b/plugin-dev/Source/Sentry/Private/Android/AndroidSentryMetric.h new file mode 100644 index 000000000..5d37302d8 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Android/AndroidSentryMetric.h @@ -0,0 +1,43 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "Interface/SentryMetricInterface.h" + +#include "Infrastructure/AndroidSentryJavaObjectWrapper.h" + +class FAndroidSentryMetric : public ISentryMetric, public FSentryJavaObjectWrapper +{ +public: + FAndroidSentryMetric(); + FAndroidSentryMetric(jobject metricEvent); + + void SetupClassMethods(); + + virtual void SetName(const FString& name) override; + virtual FString GetName() const override; + virtual void SetType(const FString& type) override; + virtual FString GetType() const override; + virtual void SetValue(float value) override; + virtual float GetValue() const override; + virtual void SetUnit(const FString& unit) override; + virtual FString GetUnit() const override; + + virtual void SetAttribute(const FString& key, const FSentryVariant& value) override; + virtual FSentryVariant GetAttribute(const FString& key) const override; + virtual bool TryGetAttribute(const FString& key, FSentryVariant& value) const override; + virtual void RemoveAttribute(const FString& key) override; + virtual void AddAttributes(const TMap& attributes) override; + +private: + FSentryJavaMethod SetNameMethod; + FSentryJavaMethod GetNameMethod; + FSentryJavaMethod SetTypeMethod; + FSentryJavaMethod GetTypeMethod; + FSentryJavaMethod SetValueMethod; + FSentryJavaMethod GetValueMethod; + FSentryJavaMethod SetUnitMethod; + FSentryJavaMethod GetUnitMethod; +}; + +typedef FAndroidSentryMetric FPlatformSentryMetric; diff --git a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp index 55295ef34..ecc06fb24 100644 --- a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp @@ -45,7 +45,7 @@ FAndroidSentrySubsystem::~FAndroidSentrySubsystem() SentryJavaClasses::ClearJavaClassRefsCache(); } -void FAndroidSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) +void FAndroidSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) { isScreenshotAttachmentEnabled = settings->AttachScreenshot; @@ -67,6 +67,7 @@ void FAndroidSentrySubsystem::InitWithSettings(const USentrySettings* settings, SettingsJson->SetBoolField(TEXT("enableAnrTracking"), settings->EnableAppNotRespondingTracking); SettingsJson->SetBoolField(TEXT("enableAutoLogAttachment"), settings->EnableAutoLogAttachment); SettingsJson->SetBoolField(TEXT("enableStructuredLogging"), settings->EnableStructuredLogging); + SettingsJson->SetBoolField(TEXT("enableMetrics"), settings->EnableMetrics); if (settings->EnableTracing && settings->SamplingType == ESentryTracesSamplingType::UniformSampleRate) { SettingsJson->SetNumberField(TEXT("tracesSampleRate"), settings->TracesSampleRate); @@ -87,6 +88,10 @@ void FAndroidSentrySubsystem::InitWithSettings(const USentrySettings* settings, { SettingsJson->SetNumberField(TEXT("beforeLogHandler"), (jlong)beforeLogHandler); } + if (beforeMetricHandler != nullptr) + { + SettingsJson->SetNumberField(TEXT("beforeMetricHandler"), (jlong)beforeMetricHandler); + } FString SettingsJsonStr; TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&SettingsJsonStr); @@ -194,6 +199,27 @@ void FAndroidSentrySubsystem::AddLog(const FString& Message, ESentryLevel Level, } } +void FAndroidSentrySubsystem::AddCount(const FString& Key, int32 Value, const TMap& Attributes) +{ + TSharedPtr attributesMap = FAndroidSentryConverters::VariantMapToNative(Attributes); + FSentryJavaObjectWrapper::CallStaticMethod(SentryJavaClasses::SentryBridgeJava, "metricCount", "(Ljava/lang/String;DLjava/util/HashMap;)V", + *FSentryJavaObjectWrapper::GetJString(Key), static_cast(Value), attributesMap->GetJObject()); +} + +void FAndroidSentrySubsystem::AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) +{ + TSharedPtr attributesMap = FAndroidSentryConverters::VariantMapToNative(Attributes); + FSentryJavaObjectWrapper::CallStaticMethod(SentryJavaClasses::SentryBridgeJava, "metricDistribution", "(Ljava/lang/String;DLjava/lang/String;Ljava/util/HashMap;)V", + *FSentryJavaObjectWrapper::GetJString(Key), static_cast(Value), *FSentryJavaObjectWrapper::GetJString(Unit), attributesMap->GetJObject()); +} + +void FAndroidSentrySubsystem::AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) +{ + TSharedPtr attributesMap = FAndroidSentryConverters::VariantMapToNative(Attributes); + FSentryJavaObjectWrapper::CallStaticMethod(SentryJavaClasses::SentryBridgeJava, "metricGauge", "(Ljava/lang/String;DLjava/lang/String;Ljava/util/HashMap;)V", + *FSentryJavaObjectWrapper::GetJString(Key), static_cast(Value), *FSentryJavaObjectWrapper::GetJString(Unit), attributesMap->GetJObject()); +} + void FAndroidSentrySubsystem::ClearBreadcrumbs() { FSentryJavaObjectWrapper::CallStaticMethod(SentryJavaClasses::Sentry, "clearBreadcrumbs", "()V"); diff --git a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h index df95782df..f6af60494 100644 --- a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h @@ -10,13 +10,16 @@ class FAndroidSentrySubsystem : public ISentrySubsystem FAndroidSentrySubsystem(); ~FAndroidSentrySubsystem(); - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; virtual void Close() override; virtual bool IsEnabled() override; virtual ESentryCrashedLastRun IsCrashedLastRun() override; virtual void AddBreadcrumb(TSharedPtr breadcrumb) override; virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap& Data, ESentryLevel Level) override; virtual void AddLog(const FString& Message, ESentryLevel Level, const TMap& Attributes) override; + virtual void AddCount(const FString& Key, int32 Value, const TMap& Attributes) override; + virtual void AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override; + virtual void AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override; virtual void ClearBreadcrumbs() override; virtual void AddAttachment(TSharedPtr attachment) override; virtual void RemoveAttachment(TSharedPtr attachment) override; diff --git a/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.cpp b/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.cpp index 54e6c23b0..352392c8f 100644 --- a/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.cpp +++ b/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.cpp @@ -29,6 +29,7 @@ const FSentryJavaClass SentryJavaClasses::TransactionOptions = FSentryJavaClass const FSentryJavaClass SentryJavaClasses::SentryTraceHeader = FSentryJavaClass { "io/sentry/SentryTraceHeader", ESentryJavaClassType::External }; const FSentryJavaClass SentryJavaClasses::SentryLogEvent = FSentryJavaClass { "io/sentry/SentryLogEvent", ESentryJavaClassType::External }; const FSentryJavaClass SentryJavaClasses::SentryLogLevel = FSentryJavaClass { "io/sentry/SentryLogLevel", ESentryJavaClassType::External }; +const FSentryJavaClass SentryJavaClasses::SentryMetricsEvent = FSentryJavaClass { "io/sentry/SentryMetricsEvent", ESentryJavaClassType::External }; // System Java classes definitions const FSentryJavaClass SentryJavaClasses::ArrayList = FSentryJavaClass { "java/util/ArrayList", ESentryJavaClassType::System }; @@ -73,6 +74,7 @@ void SentryJavaClasses::InitJavaClassRefsCache() JavaClassRefsCache.Add(SentryTraceHeader.Name, FindJavaClassRef(SentryTraceHeader)); JavaClassRefsCache.Add(SentryLogEvent.Name, FindJavaClassRef(SentryLogEvent)); JavaClassRefsCache.Add(SentryLogLevel.Name, FindJavaClassRef(SentryLogLevel)); + JavaClassRefsCache.Add(SentryMetricsEvent.Name, FindJavaClassRef(SentryMetricsEvent)); // System Java classes definitions JavaClassRefsCache.Add(ArrayList.Name, FindJavaClassRef(ArrayList)); diff --git a/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.h b/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.h index a9d0478cf..c522cb715 100644 --- a/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.h +++ b/plugin-dev/Source/Sentry/Private/Android/Infrastructure/AndroidSentryJavaClasses.h @@ -29,6 +29,7 @@ struct SentryJavaClasses const static FSentryJavaClass SentryTraceHeader; const static FSentryJavaClass SentryLogEvent; const static FSentryJavaClass SentryLogLevel; + const static FSentryJavaClass SentryMetricsEvent; // System Java classes const static FSentryJavaClass ArrayList; diff --git a/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java b/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java index 0194c29a4..64e1c46cc 100644 --- a/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java +++ b/plugin-dev/Source/Sentry/Private/Android/Java/SentryBridgeJava.java @@ -39,12 +39,15 @@ import io.sentry.SentryLogEventAttributeValue; import io.sentry.logger.SentryLogParameters; import io.sentry.SentryLogLevel; +import io.sentry.SentryMetricsEvent; +import io.sentry.metrics.SentryMetricsParameters; public class SentryBridgeJava { public static native void onConfigureScope(long callbackAddr, IScope scope); public static native SentryEvent onBeforeSend(long handlerAddr, SentryEvent event, Hint hint); public static native Breadcrumb onBeforeBreadcrumb(long handlerAddr, Breadcrumb breadcrumb, Hint hint); public static native SentryLogEvent onBeforeLog(long handlerAddr, SentryLogEvent logEvent); + public static native SentryMetricsEvent onBeforeMetric(long handlerAddr, SentryMetricsEvent metricEvent); public static native float onTracesSampler(long samplerAddr, SamplingContext samplingContext); public static native String getLogFilePath(boolean isCrash); public static native String getScreenshotFilePath(); @@ -76,6 +79,7 @@ public void configure(SentryAndroidOptions options) { } options.setAnrEnabled(settingJson.getBoolean("enableAnrTracking")); options.getLogs().setEnabled(settingJson.getBoolean("enableStructuredLogging")); + options.getMetrics().setEnabled(settingJson.getBoolean("enableMetrics")); if(settingJson.has("tracesSampleRate")) { options.setTracesSampleRate(settingJson.getDouble("tracesSampleRate")); } @@ -114,6 +118,9 @@ public Breadcrumb execute(Breadcrumb breadcrumb, Hint hint) { if (settingJson.has("beforeLogHandler")) { options.getLogs().setBeforeSend(new SentryUnrealBeforeLogCallback(settingJson.getLong("beforeLogHandler"))); } + if (settingJson.has("beforeMetricHandler")) { + options.getMetrics().setBeforeSend(new SentryUnrealBeforeMetricCallback(settingJson.getLong("beforeMetricHandler"))); + } } catch (JSONException e) { throw new RuntimeException(e); } @@ -293,7 +300,78 @@ private static void addLog(final SentryLogLevel level, final String message, fin } public static void setLogAttribute(final SentryLogEvent logEvent, final String key, final Object value) { - if (logEvent == null || key == null || value == null) { + if (logEvent == null) { + return; + } + setAttributeInternal(key, value, logEvent::setAttribute); + } + + public static Object getLogAttribute(final SentryLogEvent logEvent, final String key) { + if (logEvent == null) { + return null; + } + return getAttributeInternal(key, logEvent.getAttributes()); + } + + public static void removeLogAttribute(final SentryLogEvent logEvent, final String key) { + if (logEvent == null) { + return; + } + removeAttributeInternal(key, logEvent.getAttributes()); + } + + public static void metricCount(final String key, final double value, final HashMap attributesMap) { + SentryMetricsParameters params = createMetricsParams(attributesMap); + // Currently counters do not support unit param so pass null + Sentry.metrics().count(key, value, null, params); + } + + public static void metricDistribution(final String key, final double value, final String unit, final HashMap attributesMap) { + String effectiveUnit = (unit != null && !unit.isEmpty()) ? unit : null; + SentryMetricsParameters params = createMetricsParams(attributesMap); + Sentry.metrics().distribution(key, value, effectiveUnit, params); + } + + public static void metricGauge(final String key, final double value, final String unit, final HashMap attributesMap) { + String effectiveUnit = (unit != null && !unit.isEmpty()) ? unit : null; + SentryMetricsParameters params = createMetricsParams(attributesMap); + Sentry.metrics().gauge(key, value, effectiveUnit, params); + } + + private static SentryMetricsParameters createMetricsParams(final HashMap attributesMap) { + if (attributesMap == null || attributesMap.isEmpty()) { + return SentryMetricsParameters.create((Map) null); + } + return SentryMetricsParameters.create(attributesMap); + } + + public static void setMetricAttribute(final SentryMetricsEvent metricEvent, final String key, final Object value) { + if (metricEvent == null) { + return; + } + setAttributeInternal(key, value, metricEvent::setAttribute); + } + + public static Object getMetricAttribute(final SentryMetricsEvent metricEvent, final String key) { + if (metricEvent == null) { + return null; + } + return getAttributeInternal(key, metricEvent.getAttributes()); + } + + public static void removeMetricAttribute(final SentryMetricsEvent metricEvent, final String key) { + if (metricEvent == null) { + return; + } + removeAttributeInternal(key, metricEvent.getAttributes()); + } + + private interface AttributeSetter { + void setAttribute(String key, SentryLogEventAttributeValue value); + } + + private static void setAttributeInternal(final String key, final Object value, AttributeSetter setter) { + if (key == null || value == null) { return; } @@ -306,7 +384,7 @@ public static void setLogAttribute(final SentryLogEvent logEvent, final String k } else if (value instanceof Boolean) { attributeValue = new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, value); } else if (value instanceof Float) { - // Unreal's variant doesn't support Double so manual conversion is required + // Unreal's variant doesn't support Double so manual conversion is required attributeValue = new SentryLogEventAttributeValue(SentryAttributeType.DOUBLE, ((Float) value).doubleValue()); } else { // Unsupported type (e.g. ArrayList, HashMap) - convert to JSON string for consistency with other platforms @@ -321,16 +399,11 @@ public static void setLogAttribute(final SentryLogEvent logEvent, final String k attributeValue = new SentryLogEventAttributeValue(SentryAttributeType.STRING, jsonString); } - logEvent.setAttribute(key, attributeValue); + setter.setAttribute(key, attributeValue); } - public static Object getLogAttribute(final SentryLogEvent logEvent, final String key) { - if (logEvent == null || key == null) { - return null; - } - - Map attributes = logEvent.getAttributes(); - if (attributes == null) { + private static Object getAttributeInternal(final String key, final Map attributes) { + if (key == null || attributes == null) { return null; } @@ -339,25 +412,21 @@ public static Object getLogAttribute(final SentryLogEvent logEvent, final String return null; } - Object value = attributeValue.getValue(); + Object value = attributeValue.getValue(); - if (value instanceof Double) { - // Unreal's variant doesn't support Double so manual conversion is required - return ((Double) attributeValue.getValue()).floatValue(); - } + if (value instanceof Double) { + // Unreal's variant doesn't support Double so manual conversion is required + return ((Double) value).floatValue(); + } return value; } - public static void removeLogAttribute(final SentryLogEvent logEvent, final String key) { - if (logEvent == null || key == null) { + private static void removeAttributeInternal(final String key, final Map attributes) { + if (key == null || attributes == null) { return; } - - Map attributes = logEvent.getAttributes(); - if (attributes != null) { - attributes.remove(key); - } + attributes.remove(key); } private static class SentryUnrealBeforeSendCallback implements SentryOptions.BeforeSendCallback { @@ -429,6 +498,22 @@ public SentryLogEvent execute(SentryLogEvent logEvent) { } } + private static class SentryUnrealBeforeMetricCallback implements SentryOptions.Metrics.BeforeSendMetricCallback { + private final long beforeMetricAddr; + + public SentryUnrealBeforeMetricCallback(long beforeMetricAddr) { + this.beforeMetricAddr = beforeMetricAddr; + } + + @Override + public SentryMetricsEvent execute(SentryMetricsEvent metricEvent, Hint hint) { + if (beforeMetricAddr != 0) { + return onBeforeMetric(beforeMetricAddr, metricEvent); + } + return metricEvent; + } + } + private static byte[] readFileToBytes(File file) throws Exception { FileInputStream fis = new FileInputStream(file); try { diff --git a/plugin-dev/Source/Sentry/Private/Android/Jni/AndroidSentryJni.cpp b/plugin-dev/Source/Sentry/Private/Android/Jni/AndroidSentryJni.cpp index 1aca295dd..8768e3a8d 100644 --- a/plugin-dev/Source/Sentry/Private/Android/Jni/AndroidSentryJni.cpp +++ b/plugin-dev/Source/Sentry/Private/Android/Jni/AndroidSentryJni.cpp @@ -4,6 +4,7 @@ #include "Android/AndroidSentryEvent.h" #include "Android/AndroidSentryHint.h" #include "Android/AndroidSentryLog.h" +#include "Android/AndroidSentryMetric.h" #include "Android/AndroidSentrySamplingContext.h" #include "Android/AndroidSentryScope.h" #include "Android/AndroidSentrySubsystem.h" @@ -15,12 +16,14 @@ #include "SentryBeforeBreadcrumbHandler.h" #include "SentryBeforeLogHandler.h" +#include "SentryBeforeMetricHandler.h" #include "SentryBeforeSendHandler.h" #include "SentryBreadcrumb.h" #include "SentryDefines.h" #include "SentryEvent.h" #include "SentryHint.h" #include "SentryLog.h" +#include "SentryMetric.h" #include "SentrySamplingContext.h" #include "SentryTraceSampler.h" @@ -100,6 +103,23 @@ JNI_METHOD jobject Java_io_sentry_unreal_SentryBridgeJava_onBeforeLog(JNIEnv* en return ProcessedLogData ? logEvent : nullptr; } +JNI_METHOD jobject Java_io_sentry_unreal_SentryBridgeJava_onBeforeMetric(JNIEnv* env, jclass clazz, jlong objAddr, jobject metricEvent) +{ + if (!SentryCallbackUtils::IsCallbackSafeToRun()) + { + // Metric will be sent without calling a `onBeforeMetric` handler + return metricEvent; + } + + USentryBeforeMetricHandler* handler = reinterpret_cast(objAddr); + + USentryMetric* MetricDataToProcess = USentryMetric::Create(MakeShareable(new FAndroidSentryMetric(metricEvent))); + + USentryMetric* ProcessedMetricData = handler->HandleBeforeMetric(MetricDataToProcess); + + return ProcessedMetricData ? metricEvent : nullptr; +} + JNI_METHOD jfloat Java_io_sentry_unreal_SentryBridgeJava_onTracesSampler(JNIEnv* env, jclass clazz, jlong objAddr, jobject samplingContext) { if (!SentryCallbackUtils::IsCallbackSafeToRun()) diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp index ce38ebf25..5901d9b36 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp @@ -41,7 +41,7 @@ #include "UObject/GarbageCollection.h" #include "UObject/UObjectThreadContext.h" -void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) +void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) { isScreenshotAttachmentEnabled = settings->AttachScreenshot; isGameLogAttachmentEnabled = settings->EnableAutoLogAttachment; @@ -65,6 +65,7 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, US options.sendDefaultPii = settings->SendDefaultPii; options.maxAttachmentSize = settings->MaxAttachmentSize; options.enableLogs = settings->EnableStructuredLogging; + options.experimental.enableMetrics = settings->EnableMetrics; #if SENTRY_UIKIT_AVAILABLE options.attachScreenshot = settings->AttachScreenshot; #endif @@ -234,6 +235,65 @@ void FAppleSentrySubsystem::AddLog(const FString& Message, ESentryLevel Level, c } } +void FAppleSentrySubsystem::AddCount(const FString& Key, int32 Value, const TMap& Attributes) +{ + // Expected API once sentry-cocoa adds ObjC metrics bridge: + + // NSMutableDictionary* attributesDict = [NSMutableDictionary dictionaryWithCapacity:Attributes.Num()]; + // for (const auto& pair : Attributes) + // { + // SentryAttribute* attribute = FAppleSentryConverters::VariantToAttributeNative(pair.Value); + // if (attribute != nil) + // { + // [attributesDict setObject:attribute.value forKey:pair.Key.GetNSString()]; + // } + // } + // + // [[SENTRY_APPLE_CLASS(SentrySDK) metrics] countWithKey:Key.GetNSString() value:(NSUInteger)Value attributes:attributesDict]; + + UE_LOG(LogSentrySdk, Verbose, TEXT("Metrics are not yet supported on Apple platforms.")); +} + +void FAppleSentrySubsystem::AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) +{ + // Expected API once sentry-cocoa adds ObjC metrics bridge: + + // NSMutableDictionary* attributesDict = [NSMutableDictionary dictionaryWithCapacity:Attributes.Num()]; + // for (const auto& pair : Attributes) + // { + // SentryAttribute* attribute = FAppleSentryConverters::VariantToAttributeNative(pair.Value); + // if (attribute != nil) + // { + // [attributesDict setObject:attribute.value forKey:pair.Key.GetNSString()]; + // } + // } + // + // NSString* effectiveUnit = Unit.IsEmpty() ? nil : Unit.GetNSString(); + // [[SENTRY_APPLE_CLASS(SentrySDK) metrics] distributionWithKey:Key.GetNSString() value:(double)Value unit:effectiveUnit attributes:attributesDict]; + + UE_LOG(LogSentrySdk, Verbose, TEXT("Metrics are not yet supported on Apple platforms.")); +} + +void FAppleSentrySubsystem::AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) +{ + // Expected API once sentry-cocoa adds ObjC metrics bridge: + + // NSMutableDictionary* attributesDict = [NSMutableDictionary dictionaryWithCapacity:Attributes.Num()]; + // for (const auto& pair : Attributes) + // { + // SentryAttribute* attribute = FAppleSentryConverters::VariantToAttributeNative(pair.Value); + // if (attribute != nil) + // { + // [attributesDict setObject:attribute.value forKey:pair.Key.GetNSString()]; + // } + // } + // + // NSString* effectiveUnit = Unit.IsEmpty() ? nil : Unit.GetNSString(); + // [[SENTRY_APPLE_CLASS(SentrySDK) metrics] gaugeWithKey:Key.GetNSString() value:(double)Value unit:effectiveUnit attributes:attributesDict]; + + UE_LOG(LogSentrySdk, Verbose, TEXT("Metrics are not yet supported on Apple platforms.")); +} + void FAppleSentrySubsystem::ClearBreadcrumbs() { [SENTRY_APPLE_CLASS(SentrySDK) configureScope:^(SentryScope* scope) { diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h index e647ad7a7..57b226945 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h @@ -7,13 +7,16 @@ class FAppleSentrySubsystem : public ISentrySubsystem { public: - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; virtual void Close() override; virtual bool IsEnabled() override; virtual ESentryCrashedLastRun IsCrashedLastRun() override; virtual void AddBreadcrumb(TSharedPtr breadcrumb) override; virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap& Data, ESentryLevel Level) override; virtual void AddLog(const FString& Message, ESentryLevel Level, const TMap& Attributes) override; + virtual void AddCount(const FString& Key, int32 Value, const TMap& Attributes) override; + virtual void AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override; + virtual void AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override; virtual void ClearBreadcrumbs() override; virtual void AddAttachment(TSharedPtr attachment) override; virtual void RemoveAttachment(TSharedPtr attachment) override; diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.cpp b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.cpp new file mode 100644 index 000000000..cbf2a8935 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.cpp @@ -0,0 +1,158 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "GenericPlatformSentryMetric.h" + +#if USE_SENTRY_NATIVE + +#include "Infrastructure/GenericPlatformSentryConverters.h" + +FGenericPlatformSentryMetric::FGenericPlatformSentryMetric() +{ + Metric = sentry_value_new_object(); +} + +FGenericPlatformSentryMetric::FGenericPlatformSentryMetric(sentry_value_t metric) +{ + Metric = metric; +} + +sentry_value_t FGenericPlatformSentryMetric::GetNativeObject() +{ + return Metric; +} + +void FGenericPlatformSentryMetric::SetName(const FString& name) +{ + sentry_value_set_by_key(Metric, "name", sentry_value_new_string(TCHAR_TO_UTF8(*name))); +} + +FString FGenericPlatformSentryMetric::GetName() const +{ + sentry_value_t name = sentry_value_get_by_key(Metric, "name"); + return FString(UTF8_TO_TCHAR(sentry_value_as_string(name))); +} + +void FGenericPlatformSentryMetric::SetType(const FString& type) +{ + sentry_value_set_by_key(Metric, "type", sentry_value_new_string(TCHAR_TO_UTF8(*type))); +} + +FString FGenericPlatformSentryMetric::GetType() const +{ + sentry_value_t type = sentry_value_get_by_key(Metric, "type"); + return FString(UTF8_TO_TCHAR(sentry_value_as_string(type))); +} + +void FGenericPlatformSentryMetric::SetValue(float value) +{ + if (GetType() == TEXT("counter")) + { + sentry_value_set_by_key(Metric, "value", sentry_value_new_int64(value)); + } + else + { + sentry_value_set_by_key(Metric, "value", sentry_value_new_double(value)); + } +} + +float FGenericPlatformSentryMetric::GetValue() const +{ + sentry_value_t value = sentry_value_get_by_key(Metric, "value"); + + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_INT64) + { + return static_cast(sentry_value_as_int64(value)); + } + else + { + return static_cast(sentry_value_as_double(value)); + } +} + +void FGenericPlatformSentryMetric::SetUnit(const FString& unit) +{ + sentry_value_set_by_key(Metric, "unit", sentry_value_new_string(TCHAR_TO_UTF8(*unit))); +} + +FString FGenericPlatformSentryMetric::GetUnit() const +{ + sentry_value_t unit = sentry_value_get_by_key(Metric, "unit"); + return FString(UTF8_TO_TCHAR(sentry_value_as_string(unit))); +} + +void FGenericPlatformSentryMetric::SetAttribute(const FString& key, const FSentryVariant& value) +{ + sentry_value_t attributes = sentry_value_get_by_key(Metric, "attributes"); + if (sentry_value_is_null(attributes)) + { + attributes = sentry_value_new_object(); + sentry_value_set_by_key(Metric, "attributes", attributes); + } + + sentry_value_t attribute = FGenericPlatformSentryConverters::VariantToAttributeNative(value); + sentry_value_set_by_key(attributes, TCHAR_TO_UTF8(*key), attribute); +} + +FSentryVariant FGenericPlatformSentryMetric::GetAttribute(const FString& key) const +{ + sentry_value_t attributes = sentry_value_get_by_key(Metric, "attributes"); + if (sentry_value_is_null(attributes)) + { + return FSentryVariant(); + } + + sentry_value_t attribute = sentry_value_get_by_key(attributes, TCHAR_TO_UTF8(*key)); + if (sentry_value_is_null(attribute)) + { + return FSentryVariant(); + } + + sentry_value_t attributeValue = sentry_value_get_by_key(attribute, "value"); + return FGenericPlatformSentryConverters::VariantToUnreal(attributeValue); +} + +bool FGenericPlatformSentryMetric::TryGetAttribute(const FString& key, FSentryVariant& value) const +{ + sentry_value_t attributes = sentry_value_get_by_key(Metric, "attributes"); + if (sentry_value_is_null(attributes)) + { + return false; + } + + sentry_value_t attribute = sentry_value_get_by_key(attributes, TCHAR_TO_UTF8(*key)); + if (sentry_value_is_null(attribute)) + { + return false; + } + + sentry_value_t attributeValue = sentry_value_get_by_key(attribute, "value"); + if (sentry_value_is_null(attributeValue)) + { + return false; + } + + value = FGenericPlatformSentryConverters::VariantToUnreal(attributeValue); + + return true; +} + +void FGenericPlatformSentryMetric::RemoveAttribute(const FString& key) +{ + sentry_value_t attributes = sentry_value_get_by_key(Metric, "attributes"); + if (sentry_value_is_null(attributes)) + { + return; + } + + sentry_value_remove_by_key(attributes, TCHAR_TO_UTF8(*key)); +} + +void FGenericPlatformSentryMetric::AddAttributes(const TMap& attributes) +{ + for (const auto& pair : attributes) + { + SetAttribute(pair.Key, pair.Value); + } +} + +#endif // USE_SENTRY_NATIVE diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.h b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.h new file mode 100644 index 000000000..c29f05207 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.h @@ -0,0 +1,41 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "Convenience/GenericPlatformSentryInclude.h" + +#include "Interface/SentryMetricInterface.h" + +#if USE_SENTRY_NATIVE + +class FGenericPlatformSentryMetric : public ISentryMetric +{ +public: + FGenericPlatformSentryMetric(); + FGenericPlatformSentryMetric(sentry_value_t metric); + virtual ~FGenericPlatformSentryMetric() override = default; + + sentry_value_t GetNativeObject(); + + virtual void SetName(const FString& name) override; + virtual FString GetName() const override; + virtual void SetType(const FString& type) override; + virtual FString GetType() const override; + virtual void SetValue(float value) override; + virtual float GetValue() const override; + virtual void SetUnit(const FString& unit) override; + virtual FString GetUnit() const override; + + virtual void SetAttribute(const FString& key, const FSentryVariant& value) override; + virtual FSentryVariant GetAttribute(const FString& key) const override; + virtual bool TryGetAttribute(const FString& key, FSentryVariant& value) const override; + virtual void RemoveAttribute(const FString& key) override; + virtual void AddAttributes(const TMap& attributes) override; + +private: + sentry_value_t Metric; +}; + +typedef FGenericPlatformSentryMetric FPlatformSentryMetric; + +#endif // USE_SENTRY_NATIVE diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp index 7f4f6ef9a..82abca45c 100644 --- a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp @@ -7,6 +7,7 @@ #include "GenericPlatformSentryFeedback.h" #include "GenericPlatformSentryId.h" #include "GenericPlatformSentryLog.h" +#include "GenericPlatformSentryMetric.h" #include "GenericPlatformSentrySamplingContext.h" #include "GenericPlatformSentryScope.h" #include "GenericPlatformSentryTransaction.h" @@ -15,11 +16,13 @@ #include "SentryBeforeBreadcrumbHandler.h" #include "SentryBeforeLogHandler.h" +#include "SentryBeforeMetricHandler.h" #include "SentryBeforeSendHandler.h" #include "SentryBreadcrumb.h" #include "SentryDefines.h" #include "SentryEvent.h" #include "SentryLog.h" +#include "SentryMetric.h" #include "SentryModule.h" #include "SentrySamplingContext.h" #include "SentrySettings.h" @@ -113,6 +116,16 @@ static void PrintVerboseLog(sentry_level_t level, const char* message, va_list a return log; } +/* static */ sentry_value_t FGenericPlatformSentrySubsystem::HandleBeforeMetric(sentry_value_t metric, void* closure) +{ + if (closure) + { + return StaticCast(closure)->OnBeforeMetric(metric, closure); + } + + return metric; +} + sentry_value_t FGenericPlatformSentrySubsystem::OnBeforeSend(sentry_value_t event, void* hint, void* closure, bool isCrash) { if (!closure || this != closure) @@ -195,6 +208,33 @@ sentry_value_t FGenericPlatformSentrySubsystem::OnBeforeLog(sentry_value_t log, return ProcessedLogData ? log : sentry_value_new_null(); } +sentry_value_t FGenericPlatformSentrySubsystem::OnBeforeMetric(sentry_value_t metric, void* closure) +{ + if (!closure || this != closure) + { + return metric; + } + + USentryBeforeMetricHandler* Handler = GetBeforeMetricHandler(); + if (!Handler) + { + // If custom handler isn't set skip further processing + return metric; + } + + if (!SentryCallbackUtils::IsCallbackSafeToRun()) + { + return metric; + } + + // Create USentryMetric object using the metric wrapper + USentryMetric* MetricData = USentryMetric::Create(MakeShareable(new FGenericPlatformSentryMetric(metric))); + + USentryMetric* ProcessedMetricData = Handler->HandleBeforeMetric(MetricData); + + return ProcessedMetricData ? metric : sentry_value_new_null(); +} + sentry_value_t FGenericPlatformSentrySubsystem::OnCrash(const sentry_ucontext_t* uctx, sentry_value_t event, void* closure) { if (isScreenshotAttachmentEnabled) @@ -297,6 +337,7 @@ FGenericPlatformSentrySubsystem::FGenericPlatformSentrySubsystem() : beforeSend(nullptr) , beforeBreadcrumb(nullptr) , beforeLog(nullptr) + , beforeMetric(nullptr) , sampler(nullptr) , crashReporter(nullptr) , isEnabled(false) @@ -307,11 +348,12 @@ FGenericPlatformSentrySubsystem::FGenericPlatformSentrySubsystem() { } -void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) +void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) { beforeSend = beforeSendHandler; beforeBreadcrumb = beforeBreadcrumbHandler; beforeLog = beforeLogHandler; + beforeMetric = beforeMetricHandler; sampler = traceSampler; sentry_options_t* options = sentry_options_new(); @@ -382,6 +424,8 @@ void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* se sentry_options_set_logger_enabled_when_crashed(options, settings->EnableOnCrashLogging); sentry_options_set_enable_logs(options, settings->EnableStructuredLogging); sentry_options_set_logs_with_attributes(options, true); + sentry_options_set_enable_metrics(options, settings->EnableMetrics); + sentry_options_set_before_send_metric(options, HandleBeforeMetric, this); if (settings->bRequireUserConsent) { @@ -487,21 +531,7 @@ void FGenericPlatformSentrySubsystem::AddLog(const FString& Message, ESentryLeve { FTCHARToUTF8 MessageUtf8(*Message); - // Only create attributes object if we have per-log attributes. - // Passing null preserves global attributes set via SetAttribute(). - sentry_value_t attributes; - if (Attributes.Num() > 0) - { - attributes = sentry_value_new_object(); - for (auto it = Attributes.CreateConstIterator(); it; ++it) - { - sentry_value_set_by_key(attributes, TCHAR_TO_UTF8(*it.Key()), FGenericPlatformSentryConverters::VariantToAttributeNative(it.Value())); - } - } - else - { - attributes = sentry_value_new_null(); - } + sentry_value_t attributes = FGenericPlatformSentryConverters::VariantMapToAttributesNative(Attributes); switch (Level) { @@ -524,6 +554,21 @@ void FGenericPlatformSentrySubsystem::AddLog(const FString& Message, ESentryLeve } } +void FGenericPlatformSentrySubsystem::AddCount(const FString& Key, int32 Value, const TMap& Attributes) +{ + sentry_metrics_count(TCHAR_TO_UTF8(*Key), Value, FGenericPlatformSentryConverters::VariantMapToAttributesNative(Attributes)); +} + +void FGenericPlatformSentrySubsystem::AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) +{ + sentry_metrics_distribution(TCHAR_TO_UTF8(*Key), Value, TCHAR_TO_UTF8(*Unit), FGenericPlatformSentryConverters::VariantMapToAttributesNative(Attributes)); +} + +void FGenericPlatformSentrySubsystem::AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) +{ + sentry_metrics_gauge(TCHAR_TO_UTF8(*Key), Value, TCHAR_TO_UTF8(*Unit), FGenericPlatformSentryConverters::VariantMapToAttributesNative(Attributes)); +} + void FGenericPlatformSentrySubsystem::ClearBreadcrumbs() { // Not implemented in sentry-native @@ -855,6 +900,11 @@ USentryBeforeLogHandler* FGenericPlatformSentrySubsystem::GetBeforeLogHandler() return beforeLog; } +USentryBeforeMetricHandler* FGenericPlatformSentrySubsystem::GetBeforeMetricHandler() const +{ + return beforeMetric; +} + USentryTraceSampler* FGenericPlatformSentrySubsystem::GetTraceSampler() const { return sampler; diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h index 02a59d19e..84a0d6a0a 100644 --- a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h @@ -19,13 +19,16 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem public: FGenericPlatformSentrySubsystem(); - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; virtual void Close() override; virtual bool IsEnabled() override; virtual ESentryCrashedLastRun IsCrashedLastRun() override; virtual void AddBreadcrumb(TSharedPtr breadcrumb) override; virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap& Data, ESentryLevel Level) override; virtual void AddLog(const FString& Message, ESentryLevel Level, const TMap& Attributes) override; + virtual void AddCount(const FString& Key, int32 Value, const TMap& Attributes) override; + virtual void AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override; + virtual void AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override; virtual void ClearBreadcrumbs() override; virtual void AddAttachment(TSharedPtr attachment) override; virtual void RemoveAttachment(TSharedPtr attachment) override; @@ -61,6 +64,7 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem USentryBeforeSendHandler* GetBeforeSendHandler() const; USentryBeforeBreadcrumbHandler* GetBeforeBreadcrumbHandler() const; USentryBeforeLogHandler* GetBeforeLogHandler() const; + USentryBeforeMetricHandler* GetBeforeMetricHandler() const; USentryTraceSampler* GetTraceSampler() const; void TryCaptureScreenshot(); @@ -81,6 +85,7 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem virtual sentry_value_t OnBeforeSend(sentry_value_t event, void* hint, void* closure, bool isCrash); virtual sentry_value_t OnBeforeBreadcrumb(sentry_value_t breadcrumb, void* hint, void* closure); virtual sentry_value_t OnBeforeLog(sentry_value_t log, void* closure); + virtual sentry_value_t OnBeforeMetric(sentry_value_t metric, void* closure); virtual sentry_value_t OnCrash(const sentry_ucontext_t* uctx, sentry_value_t event, void* closure); virtual double OnTraceSampling(const sentry_transaction_context_t* transaction_ctx, sentry_value_t custom_sampling_ctx, const int* parent_sampled); @@ -100,12 +105,14 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem static sentry_value_t HandleBeforeSend(sentry_value_t event, void* hint, void* closure); static sentry_value_t HandleBeforeBreadcrumb(sentry_value_t breadcrumb, void* hint, void* closure); static sentry_value_t HandleBeforeLog(sentry_value_t log, void* closure); + static sentry_value_t HandleBeforeMetric(sentry_value_t metric, void* closure); static sentry_value_t HandleOnCrash(const sentry_ucontext_t* uctx, sentry_value_t event, void* closure); static double HandleTraceSampling(const sentry_transaction_context_t* transaction_ctx, sentry_value_t custom_sampling_ctx, const int* parent_sampled, void* closure); USentryBeforeSendHandler* beforeSend; USentryBeforeBreadcrumbHandler* beforeBreadcrumb; USentryBeforeLogHandler* beforeLog; + USentryBeforeMetricHandler* beforeMetric; USentryTraceSampler* sampler; TSharedPtr crashReporter; diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.cpp b/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.cpp index a1629a8d5..806ad72c0 100644 --- a/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.cpp +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.cpp @@ -156,6 +156,22 @@ sentry_value_t FGenericPlatformSentryConverters::VariantToAttributeNative(const return sentry_value_new_attribute(value, nullptr); } +sentry_value_t FGenericPlatformSentryConverters::VariantMapToAttributesNative(const TMap& map) +{ + if (map.Num() == 0) + { + return sentry_value_new_null(); + } + + sentry_value_t attributes = sentry_value_new_object(); + for (auto it = map.CreateConstIterator(); it; ++it) + { + sentry_value_set_by_key(attributes, TCHAR_TO_UTF8(*it.Key()), VariantToAttributeNative(it.Value())); + } + + return attributes; +} + sentry_value_t FGenericPlatformSentryConverters::AddressToNative(uint64 address) { char buffer[32]; diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.h b/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.h index 821312171..965329f82 100644 --- a/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.h +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/Infrastructure/GenericPlatformSentryConverters.h @@ -23,6 +23,7 @@ class FGenericPlatformSentryConverters static sentry_value_t VariantArrayToNative(const TArray& array); static sentry_value_t VariantMapToNative(const TMap& map); static sentry_value_t VariantToAttributeNative(const FSentryVariant& variant); + static sentry_value_t VariantMapToAttributesNative(const TMap& map); static sentry_value_t AddressToNative(uint64 address); static sentry_value_t CallstackToNative(const TArray& callstack); diff --git a/plugin-dev/Source/Sentry/Private/HAL/PlatformSentryMetric.h b/plugin-dev/Source/Sentry/Private/HAL/PlatformSentryMetric.h new file mode 100644 index 000000000..8bbc3de9d --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/HAL/PlatformSentryMetric.h @@ -0,0 +1,18 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "HAL/Platform.h" + +#if PLATFORM_ANDROID +#include "Android/AndroidSentryMetric.h" +#elif USE_SENTRY_NATIVE +#include "GenericPlatform/GenericPlatformSentryMetric.h" +#else +#include "Null/NullSentryMetric.h" +#endif + +static TSharedPtr CreateSharedSentryMetric() +{ + return MakeShareable(new FPlatformSentryMetric); +} diff --git a/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp index 38264e51d..d7eacb407 100644 --- a/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.cpp @@ -81,14 +81,14 @@ void InstallSentrySignalHandler() sigaction(SIGSYS, &Action, NULL); } -void FIOSSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) +void FIOSSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) { GIOSSentrySubsystem = this; SaveDefaultSignalHandlers(); InstallSentrySignalHandler(); - FAppleSentrySubsystem::InitWithSettings(settings, beforeSendHandler, beforeBreadcrumbHandler, beforeLogHandler, traceSampler); + FAppleSentrySubsystem::InitWithSettings(settings, beforeSendHandler, beforeBreadcrumbHandler, beforeLogHandler, beforeMetricHandler, traceSampler); } void FIOSSentrySubsystem::HandleAssert() diff --git a/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h index 4d0f5133d..0c61d0cf0 100644 --- a/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/IOS/IOSSentrySubsystem.h @@ -7,7 +7,7 @@ class FIOSSentrySubsystem : public FAppleSentrySubsystem { public: - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; virtual void HandleAssert() override; diff --git a/plugin-dev/Source/Sentry/Private/Interface/SentryMetricInterface.h b/plugin-dev/Source/Sentry/Private/Interface/SentryMetricInterface.h new file mode 100644 index 000000000..b8683b68a --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Interface/SentryMetricInterface.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +#include "SentryDataTypes.h" +#include "SentryVariant.h" + +class ISentryMetric +{ + +public: + virtual ~ISentryMetric() = default; + + virtual void SetName(const FString& name) = 0; + virtual FString GetName() const = 0; + virtual void SetType(const FString& type) = 0; + virtual FString GetType() const = 0; + virtual void SetValue(float value) = 0; + virtual float GetValue() const = 0; + virtual void SetUnit(const FString& unit) = 0; + virtual FString GetUnit() const = 0; + + virtual void SetAttribute(const FString& key, const FSentryVariant& value) = 0; + virtual FSentryVariant GetAttribute(const FString& key) const = 0; + virtual bool TryGetAttribute(const FString& key, FSentryVariant& value) const = 0; + virtual void RemoveAttribute(const FString& key) = 0; + virtual void AddAttributes(const TMap& attributes) = 0; +}; diff --git a/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h b/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h index dbba117b3..49299a2b9 100644 --- a/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h +++ b/plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h @@ -22,6 +22,7 @@ class USentrySettings; class USentryBeforeSendHandler; class USentryBeforeLogHandler; class USentryBeforeBreadcrumbHandler; +class USentryBeforeMetricHandler; class USentryTraceSampler; DECLARE_DELEGATE_OneParam(FSentryScopeDelegate, TSharedPtr); @@ -32,13 +33,16 @@ class ISentrySubsystem virtual ~ISentrySubsystem() = default; /** Methods that map directly to the platform's Sentry SDK API */ - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) = 0; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) = 0; virtual void Close() = 0; virtual bool IsEnabled() = 0; virtual ESentryCrashedLastRun IsCrashedLastRun() = 0; virtual void AddBreadcrumb(TSharedPtr breadcrumb) = 0; virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap& Data, ESentryLevel Level) = 0; virtual void AddLog(const FString& Message, ESentryLevel Level, const TMap& Attributes) = 0; + virtual void AddCount(const FString& Key, int32 Value, const TMap& Attributes) = 0; + virtual void AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) = 0; + virtual void AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) = 0; virtual void ClearBreadcrumbs() = 0; virtual void AddAttachment(TSharedPtr attachment) = 0; virtual void RemoveAttachment(TSharedPtr attachment) = 0; diff --git a/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.cpp index 7382dd207..dbccd6710 100644 --- a/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.cpp @@ -13,9 +13,9 @@ #if USE_SENTRY_NATIVE -void FLinuxSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryTraceSampler* TraceSampler) +void FLinuxSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryBeforeMetricHandler* BeforeMetricHandler, USentryTraceSampler* TraceSampler) { - FGenericPlatformSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, TraceSampler); + FGenericPlatformSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, BeforeMetricHandler, TraceSampler); if (Settings->EnableCrashReporterContextPropagation) { diff --git a/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.h index a83b28ede..c37ccd47d 100644 --- a/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Linux/LinuxSentrySubsystem.h @@ -9,7 +9,7 @@ class FLinuxSentrySubsystem : public FGenericPlatformSentrySubsystem { public: - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; protected: virtual void ConfigureHandlerPath(sentry_options_t* Options) override; diff --git a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp index 1c9572e1d..43a3d425c 100644 --- a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp @@ -14,9 +14,9 @@ #include "Misc/FileHelper.h" #include "Misc/Paths.h" -void FMacSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) +void FMacSentrySubsystem::InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) { - FAppleSentrySubsystem::InitWithSettings(settings, beforeSendHandler, beforeBreadcrumbHandler, beforeLogHandler, traceSampler); + FAppleSentrySubsystem::InitWithSettings(settings, beforeSendHandler, beforeBreadcrumbHandler, beforeLogHandler, beforeMetricHandler, traceSampler); if (IsEnabled() && isScreenshotAttachmentEnabled) { diff --git a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h index 3ec9a8660..b6de9466a 100644 --- a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h @@ -7,7 +7,7 @@ class FMacSentrySubsystem : public FAppleSentrySubsystem { public: - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; virtual void Close() override; virtual TSharedPtr CaptureEnsure(const FString& type, const FString& message) override; diff --git a/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp index 1e2ecfc18..9100e5310 100644 --- a/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.cpp @@ -22,7 +22,7 @@ #include "GenericPlatform/GenericPlatformMisc.h" #endif -void FMicrosoftSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryTraceSampler* TraceSampler) +void FMicrosoftSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryBeforeMetricHandler* BeforeMetricHandler, USentryTraceSampler* TraceSampler) { // Initialize crash logger if enabled if (Settings->EnableOnCrashLogging) @@ -39,7 +39,7 @@ void FMicrosoftSentrySubsystem::InitWithSettings(const USentrySettings* Settings } } - FGenericPlatformSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, TraceSampler); + FGenericPlatformSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, BeforeMetricHandler, TraceSampler); #if !UE_VERSION_OLDER_THAN(5, 2, 0) if (IsEnabled()) diff --git a/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.h index 07d0f1841..330df65c2 100644 --- a/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Microsoft/MicrosoftSentrySubsystem.h @@ -11,7 +11,7 @@ class FMicrosoftSentrySubsystem : public FGenericPlatformSentrySubsystem { public: - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override; + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override; virtual void Close() override; protected: diff --git a/plugin-dev/Source/Sentry/Private/Null/NullSentryMetric.h b/plugin-dev/Source/Sentry/Private/Null/NullSentryMetric.h new file mode 100644 index 000000000..e93d95245 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Null/NullSentryMetric.h @@ -0,0 +1,29 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "Interface/SentryMetricInterface.h" + +class FNullSentryMetric : public ISentryMetric +{ +public: + FNullSentryMetric() {} + virtual ~FNullSentryMetric() override = default; + + virtual void SetName(const FString& name) override {} + virtual FString GetName() const override { return FString(); } + virtual void SetType(const FString& type) override {} + virtual FString GetType() const override { return FString(); } + virtual void SetValue(float value) override {} + virtual float GetValue() const override { return 0.0f; } + virtual void SetUnit(const FString& unit) override {} + virtual FString GetUnit() const override { return FString(); } + + virtual void SetAttribute(const FString& key, const FSentryVariant& value) override {} + virtual FSentryVariant GetAttribute(const FString& key) const override { return FSentryVariant(); } + virtual bool TryGetAttribute(const FString& key, FSentryVariant& value) const override { return false; } + virtual void RemoveAttribute(const FString& key) override {} + virtual void AddAttributes(const TMap& attributes) override {} +}; + +typedef FNullSentryMetric FPlatformSentryMetric; diff --git a/plugin-dev/Source/Sentry/Private/Null/NullSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Null/NullSentrySubsystem.h index 167823fd3..f3bfe0562 100644 --- a/plugin-dev/Source/Sentry/Private/Null/NullSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Null/NullSentrySubsystem.h @@ -9,13 +9,16 @@ class FNullSentrySubsystem : public ISentrySubsystem public: virtual ~FNullSentrySubsystem() override = default; - virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryTraceSampler* traceSampler) override {} + virtual void InitWithSettings(const USentrySettings* settings, USentryBeforeSendHandler* beforeSendHandler, USentryBeforeBreadcrumbHandler* beforeBreadcrumbHandler, USentryBeforeLogHandler* beforeLogHandler, USentryBeforeMetricHandler* beforeMetricHandler, USentryTraceSampler* traceSampler) override {} virtual void Close() override {} virtual bool IsEnabled() override { return false; } virtual ESentryCrashedLastRun IsCrashedLastRun() override { return ESentryCrashedLastRun::NotEvaluated; } virtual void AddBreadcrumb(TSharedPtr breadcrumb) override {} virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap& Data, ESentryLevel Level) override {} virtual void AddLog(const FString& Message, ESentryLevel Level, const TMap& Attributes) override {} + virtual void AddCount(const FString& Key, int32 Value, const TMap& Attributes) override {} + virtual void AddDistribution(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override {} + virtual void AddGauge(const FString& Key, float Value, const FString& Unit, const TMap& Attributes) override {} virtual void ClearBreadcrumbs() override {} virtual void AddAttachment(TSharedPtr attachment) override {} virtual void RemoveAttachment(TSharedPtr attachment) override {} diff --git a/plugin-dev/Source/Sentry/Private/SentryBeforeMetricHandler.cpp b/plugin-dev/Source/Sentry/Private/SentryBeforeMetricHandler.cpp new file mode 100644 index 000000000..37989835a --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/SentryBeforeMetricHandler.cpp @@ -0,0 +1,9 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "SentryBeforeMetricHandler.h" +#include "SentryMetric.h" + +USentryMetric* USentryBeforeMetricHandler::HandleBeforeMetric_Implementation(USentryMetric* Metric) +{ + return Metric; +} diff --git a/plugin-dev/Source/Sentry/Private/SentryMetric.cpp b/plugin-dev/Source/Sentry/Private/SentryMetric.cpp new file mode 100644 index 000000000..58733ba91 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/SentryMetric.cpp @@ -0,0 +1,132 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "SentryMetric.h" + +#include "Interface/SentryMetricInterface.h" + +#include "HAL/PlatformSentryMetric.h" + +void USentryMetric::Initialize() +{ + NativeImpl = CreateSharedSentryMetric(); +} + +void USentryMetric::SetName(const FString& InName) +{ + if (NativeImpl) + NativeImpl->SetName(InName); +} + +FString USentryMetric::GetName() const +{ + if (NativeImpl) + return NativeImpl->GetName(); + + return FString(); +} + +void USentryMetric::SetType(ESentryMetricType InType) +{ + if (InType == ESentryMetricType::Unknown) + return; + + if (NativeImpl) + NativeImpl->SetType(MetricTypeToString(InType)); +} + +ESentryMetricType USentryMetric::GetType() const +{ + if (NativeImpl) + return StringToMetricType(NativeImpl->GetType()); + + return ESentryMetricType::Unknown; +} + +void USentryMetric::SetValue(float InValue) +{ + if (NativeImpl) + NativeImpl->SetValue(InValue); +} + +float USentryMetric::GetValue() const +{ + if (NativeImpl) + return NativeImpl->GetValue(); + + return 0.0f; +} + +void USentryMetric::SetUnit(const FSentryUnit& InUnit) +{ + if (NativeImpl) + NativeImpl->SetUnit(InUnit.ToString()); +} + +FSentryUnit USentryMetric::GetUnit() const +{ + if (NativeImpl) + return FSentryUnit(NativeImpl->GetUnit()); + + return FSentryUnit(); +} + +void USentryMetric::SetAttribute(const FString& Key, const FSentryVariant& Value) +{ + if (NativeImpl) + NativeImpl->SetAttribute(Key, Value); +} + +FSentryVariant USentryMetric::GetAttribute(const FString& Key) const +{ + if (NativeImpl) + return NativeImpl->GetAttribute(Key); + + return FSentryVariant(); +} + +bool USentryMetric::TryGetAttribute(const FString& Key, FSentryVariant& Value) const +{ + if (NativeImpl) + return NativeImpl->TryGetAttribute(Key, Value); + + return false; +} + +void USentryMetric::RemoveAttribute(const FString& Key) +{ + if (NativeImpl) + NativeImpl->RemoveAttribute(Key); +} + +void USentryMetric::AddAttributes(const TMap& Attributes) +{ + if (NativeImpl) + NativeImpl->AddAttributes(Attributes); +} + +FString USentryMetric::MetricTypeToString(ESentryMetricType Type) +{ + switch (Type) + { + case ESentryMetricType::Counter: + return TEXT("counter"); + case ESentryMetricType::Gauge: + return TEXT("gauge"); + case ESentryMetricType::Distribution: + return TEXT("distribution"); + default: + return TEXT(""); + } +} + +ESentryMetricType USentryMetric::StringToMetricType(const FString& Type) +{ + if (Type == TEXT("counter")) + return ESentryMetricType::Counter; + if (Type == TEXT("gauge")) + return ESentryMetricType::Gauge; + if (Type == TEXT("distribution")) + return ESentryMetricType::Distribution; + + return ESentryMetricType::Unknown; +} diff --git a/plugin-dev/Source/Sentry/Private/SentrySettings.cpp b/plugin-dev/Source/Sentry/Private/SentrySettings.cpp index 39a190b2d..5aa184315 100644 --- a/plugin-dev/Source/Sentry/Private/SentrySettings.cpp +++ b/plugin-dev/Source/Sentry/Private/SentrySettings.cpp @@ -27,6 +27,7 @@ USentrySettings::USentrySettings(const FObjectInitializer& ObjectInitializer) , StructuredLoggingCategories() , StructuredLoggingLevels() , bSendBreadcrumbsWithStructuredLogging(false) + , EnableMetrics(false) , MaxBreadcrumbs(100) , AutomaticBreadcrumbs() , AutomaticBreadcrumbsForLogs() diff --git a/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp index e6f24843c..ae422adaa 100644 --- a/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/SentrySubsystem.cpp @@ -4,6 +4,7 @@ #include "SentryBeforeBreadcrumbHandler.h" #include "SentryBeforeLogHandler.h" +#include "SentryBeforeMetricHandler.h" #include "SentryBeforeSendHandler.h" #include "SentryBreadcrumb.h" #include "SentryDefines.h" @@ -113,12 +114,17 @@ void USentrySubsystem::Initialize() ? NewObject(this, static_cast(Settings->BeforeLogHandler)) : nullptr; + BeforeMetricHandler = + Settings->BeforeMetricHandler != nullptr + ? NewObject(this, static_cast(Settings->BeforeMetricHandler)) + : nullptr; + TraceSampler = Settings->TracesSampler != nullptr ? NewObject(this, static_cast(Settings->TracesSampler)) : nullptr; - SubsystemNativeImpl->InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, TraceSampler); + SubsystemNativeImpl->InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, BeforeMetricHandler, TraceSampler); if (!SubsystemNativeImpl->IsEnabled()) { @@ -289,6 +295,51 @@ void USentrySubsystem::LogFatalWithAttributes(const FString& Message, const TMap AddLog(Message, ESentryLevel::Fatal, Attributes, Category); } +void USentrySubsystem::AddCount(const FString& Key, int32 Value) +{ + AddCountWithAttributes(Key, Value, TMap()); +} + +void USentrySubsystem::AddCountWithAttributes(const FString& Key, int32 Value, const TMap& Attributes) +{ + if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) + { + return; + } + + SubsystemNativeImpl->AddCount(Key, Value, Attributes); +} + +void USentrySubsystem::AddDistribution(const FString& Key, float Value, const FSentryUnit& Unit) +{ + AddDistributionWithAttributes(Key, Value, Unit, TMap()); +} + +void USentrySubsystem::AddDistributionWithAttributes(const FString& Key, float Value, const FSentryUnit& Unit, const TMap& Attributes) +{ + if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) + { + return; + } + + SubsystemNativeImpl->AddDistribution(Key, Value, Unit.ToString(), Attributes); +} + +void USentrySubsystem::AddGauge(const FString& Key, float Value, const FSentryUnit& Unit) +{ + AddGaugeWithAttributes(Key, Value, Unit, TMap()); +} + +void USentrySubsystem::AddGaugeWithAttributes(const FString& Key, float Value, const FSentryUnit& Unit, const TMap& Attributes) +{ + if (!SubsystemNativeImpl || !SubsystemNativeImpl->IsEnabled()) + { + return; + } + + SubsystemNativeImpl->AddGauge(Key, Value, Unit.ToString(), Attributes); +} + void USentrySubsystem::ClearBreadcrumbs() { check(SubsystemNativeImpl); diff --git a/plugin-dev/Source/Sentry/Private/SentryUnit.cpp b/plugin-dev/Source/Sentry/Private/SentryUnit.cpp new file mode 100644 index 000000000..1752b0b82 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/SentryUnit.cpp @@ -0,0 +1,48 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "SentryUnit.h" + +FSentryUnit::FSentryUnit() + : Unit(ESentryUnit::None) +{ +} + +FSentryUnit::FSentryUnit(ESentryUnit InUnit) + : Unit(InUnit) +{ +} + +FSentryUnit::FSentryUnit(const FString& InCustomUnit) + : Unit(ESentryUnit::None), CustomUnit(InCustomUnit) +{ +} + +FString FSentryUnit::ToString() const +{ + if (!CustomUnit.IsEmpty()) + { + return CustomUnit; + } + + if (Unit == ESentryUnit::None) + { + return TEXT(""); + } + + return StaticEnum()->GetNameStringByValue(static_cast(Unit)).ToLower(); +} + +FSentryUnit USentryUnitHelper::MakeSentryUnit(ESentryUnit Unit) +{ + return FSentryUnit(Unit); +} + +FSentryUnit USentryUnitHelper::MakeSentryCustomUnit(const FString& Unit) +{ + return FSentryUnit(Unit); +} + +FString USentryUnitHelper::ToString(const FSentryUnit& Unit) +{ + return Unit.ToString(); +} diff --git a/plugin-dev/Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp b/plugin-dev/Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp new file mode 100644 index 000000000..3b51ae8be --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp @@ -0,0 +1,57 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "SentryBeforeMetricHandler.h" +#include "Engine/Engine.h" +#include "SentryMetric.h" +#include "SentrySettings.h" +#include "SentrySubsystem.h" +#include "SentryTestBeforeMetricHandler.h" +#include "SentryTests.h" + +#include "Misc/AutomationTest.h" + +#include "HAL/PlatformSentryMetric.h" + +TDelegate UTestBeforeMetricHandler::OnTestBeforeMetricHandler; + +#if WITH_AUTOMATION_TESTS && (PLATFORM_ANDROID || USE_SENTRY_NATIVE) + +BEGIN_DEFINE_SPEC(SentryBeforeMetricHandlerSpec, "Sentry.SentryBeforeMetricHandler", EAutomationTestFlags::ProductFilter | SentryApplicationContextMask) +END_DEFINE_SPEC(SentryBeforeMetricHandlerSpec) + +void SentryBeforeMetricHandlerSpec::Define() +{ + Describe("BeforeMetricHandler functionality", [this]() + { + It("should be called when metric is emitted", [this]() + { + USentrySubsystem* SentrySubsystem = GEngine->GetEngineSubsystem(); + + bool bHandlerCalled = false; + const FString TestKey = TEXT("test.counter"); + const int32 TestValue = 5; + + SentrySubsystem->InitializeWithSettings(FConfigureSettingsNativeDelegate::CreateLambda([=](USentrySettings* Settings) + { + Settings->EnableMetrics = true; + Settings->BeforeMetricHandler = UTestBeforeMetricHandler::StaticClass(); + })); + + UTestBeforeMetricHandler::OnTestBeforeMetricHandler.BindLambda([this, &bHandlerCalled, TestKey](USentryMetric* MetricData) + { + bHandlerCalled = true; + TestEqual("Handler received correct name", MetricData->GetName(), TestKey); + TestEqual("Handler received correct type", MetricData->GetType(), ESentryMetricType::Counter); + }); + + SentrySubsystem->AddCount(TestKey, TestValue); + + TestTrue("BeforeMetricHandler should be called", bHandlerCalled); + + UTestBeforeMetricHandler::OnTestBeforeMetricHandler.Unbind(); + SentrySubsystem->Close(); + }); + }); +} + +#endif diff --git a/plugin-dev/Source/Sentry/Private/Tests/SentryMetric.spec.cpp b/plugin-dev/Source/Sentry/Private/Tests/SentryMetric.spec.cpp new file mode 100644 index 000000000..42d1e5e6c --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Tests/SentryMetric.spec.cpp @@ -0,0 +1,102 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "SentryMetric.h" +#include "SentryTests.h" + +#include "Misc/AutomationTest.h" + +#include "HAL/PlatformSentryMetric.h" + +#if WITH_AUTOMATION_TESTS && (PLATFORM_ANDROID || USE_SENTRY_NATIVE) + +BEGIN_DEFINE_SPEC(SentryMetricSpec, "Sentry.SentryMetric", EAutomationTestFlags::ProductFilter | SentryApplicationContextMask) + USentryMetric* SentryMetric; +END_DEFINE_SPEC(SentryMetricSpec) + +void SentryMetricSpec::Define() +{ + BeforeEach([this]() + { + SentryMetric = USentryMetric::Create(CreateSharedSentryMetric()); + }); + + Describe("Metric params", [this]() + { + It("should persist their values", [this]() + { + const FString TestName = TEXT("request.duration"); + const ESentryMetricType TestType = ESentryMetricType::Gauge; + const float TestValue = 42.5f; + const FSentryUnit TestUnit = FSentryUnit(ESentryUnit::Millisecond); + + SentryMetric->SetName(TestName); + SentryMetric->SetType(TestType); + SentryMetric->SetValue(TestValue); + SentryMetric->SetUnit(TestUnit); + + TestEqual("Metric name", SentryMetric->GetName(), TestName); + TestEqual("Metric type", SentryMetric->GetType(), TestType); + TestEqual("Metric value", SentryMetric->GetValue(), TestValue); + TestEqual("Metric unit", SentryMetric->GetUnit().ToString(), TestUnit.ToString()); + }); + + It("should handle all metric types", [this]() + { + SentryMetric->SetType(ESentryMetricType::Counter); + TestEqual("Counter type", SentryMetric->GetType(), ESentryMetricType::Counter); + + SentryMetric->SetType(ESentryMetricType::Gauge); + TestEqual("Gauge type", SentryMetric->GetType(), ESentryMetricType::Gauge); + + SentryMetric->SetType(ESentryMetricType::Distribution); + TestEqual("Distribution type", SentryMetric->GetType(), ESentryMetricType::Distribution); + }); + + It("should reject Unknown type", [this]() + { + SentryMetric->SetType(ESentryMetricType::Counter); + SentryMetric->SetType(ESentryMetricType::Unknown); + + TestEqual("Type should remain Counter", SentryMetric->GetType(), ESentryMetricType::Counter); + }); + + It("should handle empty name", [this]() + { + const FString EmptyName = TEXT(""); + + SentryMetric->SetName(EmptyName); + + TestEqual("Empty name", SentryMetric->GetName(), EmptyName); + }); + + It("should handle predefined unit round-trip", [this]() + { + SentryMetric->SetUnit(FSentryUnit(ESentryUnit::Nanosecond)); + TestEqual("Nanosecond unit", SentryMetric->GetUnit().ToString(), TEXT("nanosecond")); + + SentryMetric->SetUnit(FSentryUnit(ESentryUnit::Megabyte)); + TestEqual("Megabyte unit", SentryMetric->GetUnit().ToString(), TEXT("megabyte")); + + SentryMetric->SetUnit(FSentryUnit(ESentryUnit::Percent)); + TestEqual("Percent unit", SentryMetric->GetUnit().ToString(), TEXT("percent")); + }); + + It("should handle custom unit string", [this]() + { + const FString CustomUnit = TEXT("requests"); + + SentryMetric->SetUnit(FSentryUnit(CustomUnit)); + + TestEqual("Custom unit", SentryMetric->GetUnit().ToString(), CustomUnit); + }); + + It("should handle None unit as empty string", [this]() + { + SentryMetric->SetUnit(FSentryUnit(ESentryUnit::None)); + + TestEqual("None unit", SentryMetric->GetUnit().ToString(), TEXT("")); + }); + }); +} + +#endif diff --git a/plugin-dev/Source/Sentry/Private/Tests/SentryTestBeforeMetricHandler.h b/plugin-dev/Source/Sentry/Private/Tests/SentryTestBeforeMetricHandler.h new file mode 100644 index 000000000..8de3a8e15 --- /dev/null +++ b/plugin-dev/Source/Sentry/Private/Tests/SentryTestBeforeMetricHandler.h @@ -0,0 +1,22 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "SentryBeforeMetricHandler.h" +#include "SentryMetric.h" + +#include "SentryTestBeforeMetricHandler.generated.h" + +UCLASS() +class UTestBeforeMetricHandler : public USentryBeforeMetricHandler +{ + GENERATED_BODY() +public: + virtual USentryMetric* HandleBeforeMetric_Implementation(USentryMetric* Metric) override + { + OnTestBeforeMetricHandler.ExecuteIfBound(Metric); + return Super::HandleBeforeMetric_Implementation(Metric); + } + + static TDelegate OnTestBeforeMetricHandler; +}; diff --git a/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.cpp index e30ef7db9..39b5384ff 100644 --- a/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.cpp @@ -11,13 +11,13 @@ #include "Misc/Paths.h" -void FWindowsSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryTraceSampler* TraceSampler) +void FWindowsSentrySubsystem::InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryBeforeMetricHandler* BeforeMetricHandler, USentryTraceSampler* TraceSampler) { // Detect Wine/Proton before initializing WineProtonInfo = FSentryPlatformDetectionUtils::DetectWineProton(); // Call parent implementation (handles crash logger initialization) - FMicrosoftSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, TraceSampler); + FMicrosoftSentrySubsystem::InitWithSettings(Settings, BeforeSendHandler, BeforeBreadcrumbHandler, BeforeLogHandler, BeforeMetricHandler, TraceSampler); // Add Wine/Proton context for all events if detected if (WineProtonInfo.bIsRunningUnderWine && IsEnabled()) diff --git a/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.h index 4c5b0e062..110bd4765 100644 --- a/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Windows/WindowsSentrySubsystem.h @@ -11,7 +11,7 @@ class FWindowsSentrySubsystem : public FMicrosoftSentrySubsystem { public: - virtual void InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryTraceSampler* TraceSampler) override; + virtual void InitWithSettings(const USentrySettings* Settings, USentryBeforeSendHandler* BeforeSendHandler, USentryBeforeBreadcrumbHandler* BeforeBreadcrumbHandler, USentryBeforeLogHandler* BeforeLogHandler, USentryBeforeMetricHandler* BeforeMetricHandler, USentryTraceSampler* TraceSampler) override; protected: virtual void ConfigureHandlerPath(sentry_options_t* Options) override; diff --git a/plugin-dev/Source/Sentry/Public/SentryBeforeMetricHandler.h b/plugin-dev/Source/Sentry/Public/SentryBeforeMetricHandler.h new file mode 100644 index 000000000..ace69efa5 --- /dev/null +++ b/plugin-dev/Source/Sentry/Public/SentryBeforeMetricHandler.h @@ -0,0 +1,21 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "SentryDataTypes.h" + +#include "SentryBeforeMetricHandler.generated.h" + +class USentryMetric; + +UCLASS(Blueprintable) +class SENTRY_API USentryBeforeMetricHandler : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintNativeEvent) + USentryMetric* HandleBeforeMetric(USentryMetric* Metric); + virtual USentryMetric* HandleBeforeMetric_Implementation(USentryMetric* Metric); +}; diff --git a/plugin-dev/Source/Sentry/Public/SentryDataTypes.h b/plugin-dev/Source/Sentry/Public/SentryDataTypes.h index 121036aba..b2c54e669 100644 --- a/plugin-dev/Source/Sentry/Public/SentryDataTypes.h +++ b/plugin-dev/Source/Sentry/Public/SentryDataTypes.h @@ -31,3 +31,12 @@ enum class EUserConsent : uint8 Given, Unknown }; + +UENUM(BlueprintType) +enum class ESentryMetricType : uint8 +{ + Unknown, + Counter, + Gauge, + Distribution +}; diff --git a/plugin-dev/Source/Sentry/Public/SentryMetric.h b/plugin-dev/Source/Sentry/Public/SentryMetric.h new file mode 100644 index 000000000..45342b219 --- /dev/null +++ b/plugin-dev/Source/Sentry/Public/SentryMetric.h @@ -0,0 +1,82 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "SentryDataTypes.h" +#include "SentryImplWrapper.h" +#include "SentryUnit.h" +#include "SentryVariant.h" + +#include "SentryMetric.generated.h" + +class ISentryMetric; + +/** + * Data structure representing a metric entry for Sentry. + */ +UCLASS(BlueprintType, NotBlueprintable, HideDropdown) +class SENTRY_API USentryMetric : public UObject, public TSentryImplWrapper +{ + GENERATED_BODY() + +public: + /** Initializes the metric data. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void Initialize(); + + /** Sets the metric name. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void SetName(const FString& Name); + + /** Gets the metric name. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + FString GetName() const; + + /** Sets the metric type (counter, gauge, distribution). */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void SetType(ESentryMetricType Type); + + /** Gets the metric type. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + ESentryMetricType GetType() const; + + /** Sets the metric value. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void SetValue(float Value); + + /** Gets the metric value. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + float GetValue() const; + + /** Sets the metric unit. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void SetUnit(const FSentryUnit& Unit); + + /** Gets the metric unit. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + FSentryUnit GetUnit() const; + + /** Sets an attribute of the metric. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void SetAttribute(const FString& Key, const FSentryVariant& Value); + + /** Gets an attribute of the metric. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + FSentryVariant GetAttribute(const FString& Key) const; + + /** Tries to get an attribute of the metric. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + bool TryGetAttribute(const FString& Key, FSentryVariant& Value) const; + + /** Removes the attribute of the metric with the specified key. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void RemoveAttribute(const FString& Key); + + /** Adds attributes to the metric. */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void AddAttributes(const TMap& Attributes); + +private: + static FString MetricTypeToString(ESentryMetricType Type); + static ESentryMetricType StringToMetricType(const FString& Type); +}; diff --git a/plugin-dev/Source/Sentry/Public/SentrySettings.h b/plugin-dev/Source/Sentry/Public/SentrySettings.h index 602329474..b3f873a0b 100644 --- a/plugin-dev/Source/Sentry/Public/SentrySettings.h +++ b/plugin-dev/Source/Sentry/Public/SentrySettings.h @@ -12,6 +12,7 @@ class USentryBeforeSendHandler; class USentryBeforeBreadcrumbHandler; class USentryBeforeLogHandler; +class USentryBeforeMetricHandler; class USentryTraceSampler; UENUM(BlueprintType) @@ -274,6 +275,10 @@ class SENTRY_API USentrySettings : public UObject Meta = (DisplayName = "Also send breadcrumbs", ToolTip = "Whether to also send breadcrumbs when structured logging is enabled.", EditCondition = "EnableStructuredLogging")) bool bSendBreadcrumbsWithStructuredLogging; + UPROPERTY(Config, EditAnywhere, Category = "General|Metrics", + Meta = (DisplayName = "Enable metrics", ToolTip = "Flag indicating whether to enable the Sentry metrics API for tracking counters, distributions, and gauges.")) + bool EnableMetrics; + UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "General|Breadcrumbs", Meta = (DisplayName = "Max breadcrumbs", Tooltip = "Total amount of breadcrumbs that should be captured.")) int32 MaxBreadcrumbs; @@ -322,6 +327,10 @@ class SENTRY_API USentrySettings : public UObject Meta = (DisplayName = "Custom `beforeLog` event handler", ToolTip = "Custom handler for processing structured logs before sending them to Sentry.")) TSubclassOf BeforeLogHandler; + UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "General|Hooks", + Meta = (DisplayName = "Custom `beforeMetric` event handler", ToolTip = "Custom handler for processing metrics before sending them to Sentry.")) + TSubclassOf BeforeMetricHandler; + UPROPERTY(Config, EditAnywhere, Category = "General|Windows", Meta = (DisplayName = "Override Windows default crash capturing mechanism (UE 5.2+)", ToolTip = "Flag indicating whether to capture crashes automatically on Windows as an alternative to Crash Reporter.")) bool EnableAutoCrashCapturing; diff --git a/plugin-dev/Source/Sentry/Public/SentrySubsystem.h b/plugin-dev/Source/Sentry/Public/SentrySubsystem.h index bf1dfccb8..cce45a7da 100644 --- a/plugin-dev/Source/Sentry/Public/SentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Public/SentrySubsystem.h @@ -8,6 +8,7 @@ #include "SentryDataTypes.h" #include "SentryScope.h" #include "SentryTransactionOptions.h" +#include "SentryUnit.h" #include "SentryVariant.h" #include "SentrySubsystem.generated.h" @@ -20,6 +21,7 @@ class USentryUser; class USentryBeforeSendHandler; class USentryBeforeBreadcrumbHandler; class USentryBeforeLogHandler; +class USentryBeforeMetricHandler; class USentryTransaction; class USentryTraceSampler; class USentryTransactionContext; @@ -203,6 +205,70 @@ class SENTRY_API USentrySubsystem : public UEngineSubsystem UFUNCTION(BlueprintCallable, Category = "Sentry", meta = (AutoCreateRefTerm = "Attributes")) void LogFatalWithAttributes(const FString& Message, const TMap& Attributes, const FString& Category = TEXT("LogSentrySdk")); + /** + * Emits a Counter metric. + * Counters track a value that can only be incremented. + * + * @param Key The name of the metric. + * @param Value The value to increment by (default 1). + */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void AddCount(const FString& Key, int32 Value); + + /** + * Emits a Counter metric with attributes. + * + * @param Key The name of the metric. + * @param Value The value to increment by. + * @param Attributes Structured attributes to attach to the metric. + */ + UFUNCTION(BlueprintCallable, Category = "Sentry", meta = (AutoCreateRefTerm = "Attributes")) + void AddCountWithAttributes(const FString& Key, int32 Value, const TMap& Attributes); + + /** + * Emits a Distribution metric. + * Distributions track the statistical distribution of values. + * + * @param Key The name of the metric. + * @param Value The value to record. + * @param Unit The unit of measurement for the metric value. + */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void AddDistribution(const FString& Key, float Value, const FSentryUnit& Unit); + + /** + * Emits a Distribution metric with attributes. + * + * @param Key The name of the metric. + * @param Value The value to record. + * @param Unit The unit of measurement for the metric value. + * @param Attributes Structured attributes to attach to the metric. + */ + UFUNCTION(BlueprintCallable, Category = "Sentry", meta = (AutoCreateRefTerm = "Attributes")) + void AddDistributionWithAttributes(const FString& Key, float Value, const FSentryUnit& Unit, const TMap& Attributes); + + /** + * Emits a Gauge metric. + * Gauges track a value that can go up and down. + * + * @param Key The name of the metric. + * @param Value The current gauge value. + * @param Unit The unit of measurement for the metric value. + */ + UFUNCTION(BlueprintCallable, Category = "Sentry") + void AddGauge(const FString& Key, float Value, const FSentryUnit& Unit); + + /** + * Emits a Gauge metric with attributes. + * + * @param Key The name of the metric. + * @param Value The current gauge value. + * @param Unit The unit of measurement for the metric value. + * @param Attributes Structured attributes to attach to the metric. + */ + UFUNCTION(BlueprintCallable, Category = "Sentry", meta = (AutoCreateRefTerm = "Attributes")) + void AddGaugeWithAttributes(const FString& Key, float Value, const FSentryUnit& Unit, const TMap& Attributes); + /** * Clear all breadcrumbs of the current Scope. * @@ -522,6 +588,9 @@ class SENTRY_API USentrySubsystem : public UEngineSubsystem UPROPERTY() USentryBeforeLogHandler* BeforeLogHandler; + UPROPERTY() + USentryBeforeMetricHandler* BeforeMetricHandler; + UPROPERTY() USentryTraceSampler* TraceSampler; diff --git a/plugin-dev/Source/Sentry/Public/SentryUnit.h b/plugin-dev/Source/Sentry/Public/SentryUnit.h new file mode 100644 index 000000000..0303b7eba --- /dev/null +++ b/plugin-dev/Source/Sentry/Public/SentryUnit.h @@ -0,0 +1,90 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "SentryUnit.generated.h" + +/** + * Predefined measurement units for Sentry metrics. + */ +UENUM(BlueprintType) +enum class ESentryUnit : uint8 +{ + None, + + // Duration + Nanosecond, + Microsecond, + Millisecond, + Second, + Minute, + Hour, + Day, + Week, + + // Information + Bit, + Byte, + Kilobyte, + Kibibyte, + Megabyte, + Mebibyte, + Gigabyte, + Gibibyte, + Terabyte, + Tebibyte, + Petabyte, + Pebibyte, + Exabyte, + Exbibyte, + + // Fraction + Ratio, + Percent +}; + +/** + * A measurement unit that can be attached to a metric value. + * Use predefined units from the enum or provide a custom string. + */ +USTRUCT(BlueprintType, meta = (HasNativeMake = "/Script/Sentry.SentryUnitHelper.MakeSentryUnit")) +struct SENTRY_API FSentryUnit +{ + GENERATED_BODY() + + FSentryUnit(); + FSentryUnit(ESentryUnit InUnit); + FSentryUnit(const FString& InCustomUnit); + + /** Converts the unit to its string representation. */ + FString ToString() const; + +private: + ESentryUnit Unit; + FString CustomUnit; +}; + +/** + * Blueprint factory functions for FSentryUnit. + */ +UCLASS() +class SENTRY_API USentryUnitHelper : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Creates a measurement unit from a predefined type. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + static FSentryUnit MakeSentryUnit(ESentryUnit Unit); + + /** Creates a measurement unit from a custom string. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + static FSentryUnit MakeSentryCustomUnit(const FString& Unit); + + /** Converts the unit to its string representation. */ + UFUNCTION(BlueprintPure, Category = "Sentry") + static FString ToString(const FSentryUnit& Unit); +}; diff --git a/sample/Content/Misc/BP_TestBeforeMetricHandler.uasset b/sample/Content/Misc/BP_TestBeforeMetricHandler.uasset new file mode 100644 index 000000000..bf90ffb4c Binary files /dev/null and b/sample/Content/Misc/BP_TestBeforeMetricHandler.uasset differ diff --git a/sample/Content/UI/W_SentryDemo.uasset b/sample/Content/UI/W_SentryDemo.uasset index 43738ce7e..cb0db7454 100644 Binary files a/sample/Content/UI/W_SentryDemo.uasset and b/sample/Content/UI/W_SentryDemo.uasset differ diff --git a/scripts/packaging/pack.ps1 b/scripts/packaging/pack.ps1 index e7079598f..d3ab2da98 100644 --- a/scripts/packaging/pack.ps1 +++ b/scripts/packaging/pack.ps1 @@ -70,6 +70,7 @@ function packFiles() -replace 'USentryBeforeSendHandler\* BeforeSendHandler;', 'TObjectPtr BeforeSendHandler;' ` -replace 'USentryBeforeBreadcrumbHandler\* BeforeBreadcrumbHandler;', 'TObjectPtr BeforeBreadcrumbHandler;' ` -replace 'USentryBeforeLogHandler\* BeforeLogHandler;', 'TObjectPtr BeforeLogHandler;' ` + -replace 'USentryBeforeMetricHandler\* BeforeMetricHandler;', 'TObjectPtr BeforeMetricHandler;' ` -replace 'USentryTraceSampler\* TraceSampler;', 'TObjectPtr TraceSampler;' } diff --git a/scripts/packaging/package.snapshot b/scripts/packaging/package.snapshot index 10e95c88f..d34ade762 100644 --- a/scripts/packaging/package.snapshot +++ b/scripts/packaging/package.snapshot @@ -25,6 +25,8 @@ Source/Sentry/Private/Android/AndroidSentryLog.cpp Source/Sentry/Private/Android/AndroidSentryLog.h Source/Sentry/Private/Android/AndroidSentryMessage.cpp Source/Sentry/Private/Android/AndroidSentryMessage.h +Source/Sentry/Private/Android/AndroidSentryMetric.cpp +Source/Sentry/Private/Android/AndroidSentryMetric.h Source/Sentry/Private/Android/AndroidSentrySamplingContext.cpp Source/Sentry/Private/Android/AndroidSentrySamplingContext.h Source/Sentry/Private/Android/AndroidSentryScope.cpp @@ -99,6 +101,8 @@ Source/Sentry/Private/GenericPlatform/GenericPlatformSentryId.cpp Source/Sentry/Private/GenericPlatform/GenericPlatformSentryId.h Source/Sentry/Private/GenericPlatform/GenericPlatformSentryLog.cpp Source/Sentry/Private/GenericPlatform/GenericPlatformSentryLog.h +Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.cpp +Source/Sentry/Private/GenericPlatform/GenericPlatformSentryMetric.h Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySamplingContext.cpp Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySamplingContext.h Source/Sentry/Private/GenericPlatform/GenericPlatformSentryScope.cpp @@ -123,6 +127,7 @@ Source/Sentry/Private/HAL/PlatformSentryFeedback.h Source/Sentry/Private/HAL/PlatformSentryHint.h Source/Sentry/Private/HAL/PlatformSentryId.h Source/Sentry/Private/HAL/PlatformSentryLog.h +Source/Sentry/Private/HAL/PlatformSentryMetric.h Source/Sentry/Private/HAL/PlatformSentrySamplingContext.h Source/Sentry/Private/HAL/PlatformSentryScope.h Source/Sentry/Private/HAL/PlatformSentrySpan.h @@ -137,6 +142,7 @@ Source/Sentry/Private/Interface/SentryFeedbackInterface.h Source/Sentry/Private/Interface/SentryHintInterface.h Source/Sentry/Private/Interface/SentryIdInterface.h Source/Sentry/Private/Interface/SentryLogInterface.h +Source/Sentry/Private/Interface/SentryMetricInterface.h Source/Sentry/Private/Interface/SentrySamplingContextInterface.h Source/Sentry/Private/Interface/SentryScopeInterface.h Source/Sentry/Private/Interface/SentrySpanInterface.h @@ -167,6 +173,7 @@ Source/Sentry/Private/Null/NullSentryFeedback.h Source/Sentry/Private/Null/NullSentryHint.h Source/Sentry/Private/Null/NullSentryId.h Source/Sentry/Private/Null/NullSentryLog.h +Source/Sentry/Private/Null/NullSentryMetric.h Source/Sentry/Private/Null/NullSentrySamplingContext.h Source/Sentry/Private/Null/NullSentryScope.h Source/Sentry/Private/Null/NullSentrySpan.h @@ -177,6 +184,7 @@ Source/Sentry/Private/Null/NullSentryUser.h Source/Sentry/Private/SentryAttachment.cpp Source/Sentry/Private/SentryBeforeBreadcrumbHandler.cpp Source/Sentry/Private/SentryBeforeLogHandler.cpp +Source/Sentry/Private/SentryBeforeMetricHandler.cpp Source/Sentry/Private/SentryBeforeSendHandler.cpp Source/Sentry/Private/SentryBreadcrumb.cpp Source/Sentry/Private/SentryDefines.h @@ -186,6 +194,7 @@ Source/Sentry/Private/SentryFeedback.cpp Source/Sentry/Private/SentryHint.cpp Source/Sentry/Private/SentryLibrary.cpp Source/Sentry/Private/SentryLog.cpp +Source/Sentry/Private/SentryMetric.cpp Source/Sentry/Private/SentryModule.cpp Source/Sentry/Private/SentryOutputDevice.cpp Source/Sentry/Private/SentrySamplingContext.cpp @@ -196,18 +205,22 @@ Source/Sentry/Private/SentrySubsystem.cpp Source/Sentry/Private/SentryTraceSampler.cpp Source/Sentry/Private/SentryTransaction.cpp Source/Sentry/Private/SentryTransactionContext.cpp +Source/Sentry/Private/SentryUnit.cpp Source/Sentry/Private/SentryUser.cpp Source/Sentry/Private/SentryVariant.cpp Source/Sentry/Private/Tests/SentryBeforeLogHandler.spec.cpp +Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp Source/Sentry/Private/Tests/SentryBreadcrumb.spec.cpp Source/Sentry/Private/Tests/SentryEvent.spec.cpp Source/Sentry/Private/Tests/SentryFeedback.spec.cpp Source/Sentry/Private/Tests/SentryLog.spec.cpp Source/Sentry/Private/Tests/SentryLogAttributes.spec.cpp +Source/Sentry/Private/Tests/SentryMetric.spec.cpp Source/Sentry/Private/Tests/SentryScope.spec.cpp Source/Sentry/Private/Tests/SentryScopeBeforeSendHandler.h Source/Sentry/Private/Tests/SentrySubsystem.spec.cpp Source/Sentry/Private/Tests/SentryTestBeforeLogHandler.h +Source/Sentry/Private/Tests/SentryTestBeforeMetricHandler.h Source/Sentry/Private/Tests/SentryTests.h Source/Sentry/Private/Tests/SentryTraceSampling.spec.cpp Source/Sentry/Private/Tests/SentryTraceSamplingHandler.h @@ -230,6 +243,7 @@ Source/Sentry/Private/Windows/WindowsSentrySubsystem.h Source/Sentry/Public/SentryAttachment.h Source/Sentry/Public/SentryBeforeBreadcrumbHandler.h Source/Sentry/Public/SentryBeforeLogHandler.h +Source/Sentry/Public/SentryBeforeMetricHandler.h Source/Sentry/Public/SentryBeforeSendHandler.h Source/Sentry/Public/SentryBreadcrumb.h Source/Sentry/Public/SentryDataTypes.h @@ -240,6 +254,7 @@ Source/Sentry/Public/SentryHint.h Source/Sentry/Public/SentryImplWrapper.h Source/Sentry/Public/SentryLibrary.h Source/Sentry/Public/SentryLog.h +Source/Sentry/Public/SentryMetric.h Source/Sentry/Public/SentryModule.h Source/Sentry/Public/SentryOutputDevice.h Source/Sentry/Public/SentrySamplingContext.h @@ -251,6 +266,7 @@ Source/Sentry/Public/SentryTraceSampler.h Source/Sentry/Public/SentryTransaction.h Source/Sentry/Public/SentryTransactionContext.h Source/Sentry/Public/SentryTransactionOptions.h +Source/Sentry/Public/SentryUnit.h Source/Sentry/Public/SentryUser.h Source/Sentry/Public/SentryVariant.h Source/Sentry/Sentry_Android_UPL.xml