diff --git a/integration-test/Integration.Android.Tests.ps1 b/integration-test/Integration.Android.Tests.ps1 index 98c0a8db5..f82e3aedb 100644 --- a/integration-test/Integration.Android.Tests.ps1 +++ b/integration-test/Integration.Android.Tests.ps1 @@ -179,6 +179,16 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa $global:AndroidLogResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $logIntentArgs Write-Host "Log test exit code: $($global:AndroidLogResult.ExitCode)" -ForegroundColor Cyan + + # ========================================== + # RUN 4: Metric test - captures custom metric + # ========================================== + + Write-Host "Running metric-capture test on $Platform..." -ForegroundColor Yellow + $metricIntentArgs = "-e cmdline '-metric-capture -ini:Engine:[/Script/Sentry.SentrySettings]:EnableMetrics=True -ini:Engine:[/Script/Sentry.SentrySettings]:BeforeMetricHandler=/Script/SentryPlayground.CppBeforeMetricHandler'" + $global:AndroidMetricResult = Invoke-DeviceApp -ExecutablePath $script:ActivityName -Arguments $metricIntentArgs + + Write-Host "Metric test exit code: $($global:AndroidMetricResult.ExitCode)" -ForegroundColor Cyan } AfterAll { @@ -403,6 +413,124 @@ Describe 'Sentry Unreal Android Integration Tests ()' -ForEach $TestTa # Note: Global log attributes (SetAttribute/RemoveAttribute on subsystem) are not supported # on Android (sentry-java) - the implementation is a no-op. These are tested in desktop tests only. } + + Context "Metrics Capture Tests" { + BeforeAll { + $script:MetricResult = $global:AndroidMetricResult + $script:CapturedCounterMetrics = @() + $script:CapturedDistributionMetrics = @() + $script:CapturedGaugeMetrics = @() + $script:TestId = $null + + # Parse test ID from output (format: METRIC_TRIGGERED: ) + $metricTriggeredLines = @($script:MetricResult.Output | Where-Object { $_ -match 'METRIC_TRIGGERED: ' }) + if ($metricTriggeredLines.Count -gt 0) { + $script:TestId = ($metricTriggeredLines[0] -split 'METRIC_TRIGGERED: ')[-1].Trim() + Write-Host "Captured Test ID: $($script:TestId)" -ForegroundColor Cyan + + # Fetch all three metric types from Sentry with automatic polling + $metricFields = @('handler_added', 'to_be_removed') + + try { + $script:CapturedCounterMetrics = Get-SentryTestMetric -MetricName 'test.integration.counter' -AttributeName 'test_id' -AttributeValue $script:TestId -Fields $metricFields + } + catch { + Write-Host "Warning (counter): $_" -ForegroundColor Red + } + + try { + $script:CapturedDistributionMetrics = Get-SentryTestMetric -MetricName 'test.integration.distribution' -AttributeName 'test_id' -AttributeValue $script:TestId -Fields $metricFields + } + catch { + Write-Host "Warning (distribution): $_" -ForegroundColor Red + } + + try { + $script:CapturedGaugeMetrics = Get-SentryTestMetric -MetricName 'test.integration.gauge' -AttributeName 'test_id' -AttributeValue $script:TestId -Fields $metricFields + } + catch { + Write-Host "Warning (gauge): $_" -ForegroundColor Red + } + } + else { + Write-Host "Warning: No METRIC_TRIGGERED line found in output" -ForegroundColor Yellow + } + } + + It "Should output METRIC_TRIGGERED with test ID" { + $script:TestId | Should -Not -BeNullOrEmpty + } + + It "Should output TEST_RESULT with success" { + $testResultLine = $script:MetricResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } + $testResultLine | Should -Not -BeNullOrEmpty + $testResultLine | Should -Match '"success"\s*:\s*true' + } + + # Counter metric assertions + It "Should capture counter metric in Sentry" { + $script:CapturedCounterMetrics | Should -Not -BeNullOrEmpty + } + + It "Should have correct counter metric name and type" { + $metric = $script:CapturedCounterMetrics[0] + $metric.'metric.name' | Should -Be 'test.integration.counter' + $metric.'metric.type' | Should -Be 'counter' + } + + It "Should have correct counter metric value" { + $metric = $script:CapturedCounterMetrics[0] + $metric.value | Should -Be 1.0 + } + + # Distribution metric assertions + It "Should capture distribution metric in Sentry" { + $script:CapturedDistributionMetrics | Should -Not -BeNullOrEmpty + } + + It "Should have correct distribution metric name and type" { + $metric = $script:CapturedDistributionMetrics[0] + $metric.'metric.name' | Should -Be 'test.integration.distribution' + $metric.'metric.type' | Should -Be 'distribution' + } + + It "Should have correct distribution metric value" { + $metric = $script:CapturedDistributionMetrics[0] + $metric.value | Should -Be 42.5 + } + + # Gauge metric assertions + It "Should capture gauge metric in Sentry" { + $script:CapturedGaugeMetrics | Should -Not -BeNullOrEmpty + } + + It "Should have correct gauge metric name and type" { + $metric = $script:CapturedGaugeMetrics[0] + $metric.'metric.name' | Should -Be 'test.integration.gauge' + $metric.'metric.type' | Should -Be 'gauge' + } + + It "Should have correct gauge metric value" { + $metric = $script:CapturedGaugeMetrics[0] + $metric.value | Should -Be 15.0 + } + + # BeforeMetricHandler attribute assertions (verified on counter, applies to all) + It "Should have attribute added by BeforeMetricHandler" { + $metric = $script:CapturedCounterMetrics[0] + $metric.'handler_added' | Should -Be 'added_value' + } + + It "Should not have attribute removed by BeforeMetricHandler" { + $metric = $script:CapturedCounterMetrics[0] + $metric.'to_be_removed' | Should -BeNullOrEmpty + } + + It "Should have test_id attribute matching captured ID" { + $metric = $script:CapturedCounterMetrics[0] + $metric.test_id | Should -Be $script:TestId + } + } } AfterAll { diff --git a/integration-test/Integration.Desktop.Tests.ps1 b/integration-test/Integration.Desktop.Tests.ps1 index 9407db7eb..7da21822d 100644 --- a/integration-test/Integration.Desktop.Tests.ps1 +++ b/integration-test/Integration.Desktop.Tests.ps1 @@ -415,6 +415,148 @@ Describe "Sentry Unreal Desktop Integration Tests ()" -ForEach $TestTa $log.global_removed | Should -BeNullOrEmpty } } + + # Metrics are not supported on Apple platforms (macOS/iOS) + Context "Metrics Capture Tests" -Skip:$IsMacOS { + BeforeAll { + $script:MetricResult = $null + $script:CapturedCounterMetrics = @() + $script:CapturedDistributionMetrics = @() + $script:CapturedGaugeMetrics = @() + $script:TestId = $null + + Write-Host "Running metrics capture test..." -ForegroundColor Yellow + + $appArgs = @( + '-nullrhi', # Runs without graphics rendering (headless mode) + '-unattended', # Disables user prompts and interactive dialogs + '-stdout', # Ensures logs are written to stdout on Linux/Unix systems + '-nosplash' # Prevents splash screen and dialogs + ) + + # Override default project settings + $appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:Dsn=$script:DSN" + $appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:EnableMetrics=True" + $appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:BeforeMetricHandler=/Script/SentryPlayground.CppBeforeMetricHandler" + + # -metric-capture triggers integration test metric scenario in the sample app + $script:MetricResult = Invoke-DeviceApp -ExecutablePath $script:AppPath -Arguments ((@('-metric-capture') + $appArgs) -join ' ') + + Write-Host "Metric test executed. Exit code: $($script:MetricResult.ExitCode)" -ForegroundColor Cyan + + # Parse test ID from output (format: METRIC_TRIGGERED: ) + $metricTriggeredLines = @($script:MetricResult.Output | Where-Object { $_ -match 'METRIC_TRIGGERED: ' }) + if ($metricTriggeredLines.Count -gt 0) { + $script:TestId = ($metricTriggeredLines[0] -split 'METRIC_TRIGGERED: ')[-1].Trim() + Write-Host "Captured Test ID: $($script:TestId)" -ForegroundColor Cyan + + # Fetch all three metric types from Sentry with automatic polling + $metricFields = @('handler_added', 'to_be_removed') + + try { + $script:CapturedCounterMetrics = Get-SentryTestMetric -MetricName 'test.integration.counter' -AttributeName 'test_id' -AttributeValue $script:TestId -Fields $metricFields + } + catch { + Write-Host "Warning (counter): $_" -ForegroundColor Red + } + + try { + $script:CapturedDistributionMetrics = Get-SentryTestMetric -MetricName 'test.integration.distribution' -AttributeName 'test_id' -AttributeValue $script:TestId -Fields $metricFields + } + catch { + Write-Host "Warning (distribution): $_" -ForegroundColor Red + } + + try { + $script:CapturedGaugeMetrics = Get-SentryTestMetric -MetricName 'test.integration.gauge' -AttributeName 'test_id' -AttributeValue $script:TestId -Fields $metricFields + } + catch { + Write-Host "Warning (gauge): $_" -ForegroundColor Red + } + } + else { + Write-Host "Warning: No METRIC_TRIGGERED line found in output" -ForegroundColor Yellow + } + } + + It "Should exit cleanly" { + $script:MetricResult.ExitCode | Should -Be 0 + } + + It "Should output METRIC_TRIGGERED with test ID" { + $script:TestId | Should -Not -BeNullOrEmpty + } + + It "Should output TEST_RESULT with success" { + $testResultLine = $script:MetricResult.Output | Where-Object { $_ -match 'TEST_RESULT:' } + $testResultLine | Should -Not -BeNullOrEmpty + $testResultLine | Should -Match '"success"\s*:\s*true' + } + + # Counter metric assertions + It "Should capture counter metric in Sentry" { + $script:CapturedCounterMetrics | Should -Not -BeNullOrEmpty + } + + It "Should have correct counter metric name and type" { + $metric = $script:CapturedCounterMetrics[0] + $metric.'metric.name' | Should -Be 'test.integration.counter' + $metric.'metric.type' | Should -Be 'counter' + } + + It "Should have correct counter metric value" { + $metric = $script:CapturedCounterMetrics[0] + $metric.value | Should -Be 1.0 + } + + # Distribution metric assertions + It "Should capture distribution metric in Sentry" { + $script:CapturedDistributionMetrics | Should -Not -BeNullOrEmpty + } + + It "Should have correct distribution metric name and type" { + $metric = $script:CapturedDistributionMetrics[0] + $metric.'metric.name' | Should -Be 'test.integration.distribution' + $metric.'metric.type' | Should -Be 'distribution' + } + + It "Should have correct distribution metric value" { + $metric = $script:CapturedDistributionMetrics[0] + $metric.value | Should -Be 42.5 + } + + # Gauge metric assertions + It "Should capture gauge metric in Sentry" { + $script:CapturedGaugeMetrics | Should -Not -BeNullOrEmpty + } + + It "Should have correct gauge metric name and type" { + $metric = $script:CapturedGaugeMetrics[0] + $metric.'metric.name' | Should -Be 'test.integration.gauge' + $metric.'metric.type' | Should -Be 'gauge' + } + + It "Should have correct gauge metric value" { + $metric = $script:CapturedGaugeMetrics[0] + $metric.value | Should -Be 15.0 + } + + # BeforeMetricHandler attribute assertions (verified on counter, applies to all) + It "Should have attribute added by BeforeMetricHandler" { + $metric = $script:CapturedCounterMetrics[0] + $metric.'handler_added' | Should -Be 'added_value' + } + + It "Should not have attribute removed by BeforeMetricHandler" { + $metric = $script:CapturedCounterMetrics[0] + $metric.'to_be_removed' | Should -BeNullOrEmpty + } + + It "Should have test_id attribute matching captured ID" { + $metric = $script:CapturedCounterMetrics[0] + $metric.test_id | Should -Be $script:TestId + } + } } AfterAll { diff --git a/plugin-dev/Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp b/plugin-dev/Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp deleted file mode 100644 index 3b51ae8be..000000000 --- a/plugin-dev/Source/Sentry/Private/Tests/SentryBeforeMetricHandler.spec.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// 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/SentryTestBeforeMetricHandler.h b/plugin-dev/Source/Sentry/Private/Tests/SentryTestBeforeMetricHandler.h deleted file mode 100644 index 8de3a8e15..000000000 --- a/plugin-dev/Source/Sentry/Private/Tests/SentryTestBeforeMetricHandler.h +++ /dev/null @@ -1,22 +0,0 @@ -// 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/sample/Source/SentryPlayground/CppBeforeMetricHandler.cpp b/sample/Source/SentryPlayground/CppBeforeMetricHandler.cpp new file mode 100644 index 000000000..c795af3dd --- /dev/null +++ b/sample/Source/SentryPlayground/CppBeforeMetricHandler.cpp @@ -0,0 +1,13 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#include "CppBeforeMetricHandler.h" + +#include "SentryMetric.h" + +USentryMetric* UCppBeforeMetricHandler::HandleBeforeMetric_Implementation(USentryMetric* Metric) +{ + Metric->SetAttribute(TEXT("handler_added"), FSentryVariant(TEXT("added_value"))); + Metric->RemoveAttribute(TEXT("to_be_removed")); + + return Super::HandleBeforeMetric_Implementation(Metric); +} diff --git a/sample/Source/SentryPlayground/CppBeforeMetricHandler.h b/sample/Source/SentryPlayground/CppBeforeMetricHandler.h new file mode 100644 index 000000000..be9d61f6a --- /dev/null +++ b/sample/Source/SentryPlayground/CppBeforeMetricHandler.h @@ -0,0 +1,18 @@ +// Copyright (c) 2025 Sentry. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +#include "SentryBeforeMetricHandler.h" + +#include "CppBeforeMetricHandler.generated.h" + +UCLASS() +class SENTRYPLAYGROUND_API UCppBeforeMetricHandler : public USentryBeforeMetricHandler +{ + GENERATED_BODY() + +public: + virtual USentryMetric* HandleBeforeMetric_Implementation(USentryMetric* Metric) override; +}; diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp index 2be19df9d..a4ee23625 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.cpp @@ -7,6 +7,7 @@ #include "SentrySettings.h" #include "SentryPlaygroundUtils.h" #include "SentryUser.h" +#include "SentryUnit.h" #include "CoreGlobals.h" #include "HAL/Platform.h" @@ -29,6 +30,7 @@ void USentryPlaygroundGameInstance::Init() FParse::Param(*CommandLine, TEXT("crash-memory-corruption")) || FParse::Param(*CommandLine, TEXT("message-capture")) || FParse::Param(*CommandLine, TEXT("log-capture")) || + FParse::Param(*CommandLine, TEXT("metric-capture")) || FParse::Param(*CommandLine, TEXT("init-only"))) { RunIntegrationTest(CommandLine); @@ -80,6 +82,10 @@ void USentryPlaygroundGameInstance::RunIntegrationTest(const FString& CommandLin { RunLogTest(); } + else if (FParse::Param(*CommandLine, TEXT("metric-capture"))) + { + RunMetricTest(); + } else if (FParse::Param(*CommandLine, TEXT("init-only"))) { RunInitOnly(); @@ -167,6 +173,38 @@ void USentryPlaygroundGameInstance::RunLogTest() CompleteTestWithResult(TEXT("log-capture"), true, TEXT("Test complete")); } +void USentryPlaygroundGameInstance::RunMetricTest() +{ + USentrySubsystem* SentrySubsystem = GEngine->GetEngineSubsystem(); + + FString TestId = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens); + + TMap CounterAttributes; + CounterAttributes.Add(TEXT("test_id"), FSentryVariant(TestId)); + CounterAttributes.Add(TEXT("to_be_removed"), FSentryVariant(TEXT("original_value"))); + + TMap DistributionAttributes; + DistributionAttributes.Add(TEXT("test_id"), FSentryVariant(TestId)); + DistributionAttributes.Add(TEXT("to_be_removed"), FSentryVariant(TEXT("original_value"))); + + TMap GaugeAttributes; + GaugeAttributes.Add(TEXT("test_id"), FSentryVariant(TestId)); + GaugeAttributes.Add(TEXT("to_be_removed"), FSentryVariant(TEXT("original_value"))); + + SentrySubsystem->AddCountWithAttributes(TEXT("test.integration.counter"), 1, CounterAttributes); + SentrySubsystem->AddDistributionWithAttributes(TEXT("test.integration.distribution"), 42.5f, FSentryUnit(ESentryUnit::Millisecond), DistributionAttributes); + SentrySubsystem->AddGaugeWithAttributes(TEXT("test.integration.gauge"), 15.0f, FSentryUnit(ESentryUnit::Byte), GaugeAttributes); + + UE_LOG(LogSentrySample, Display, TEXT("METRIC_TRIGGERED: %s\n"), *TestId); + + // Ensure events were flushed + SentrySubsystem->Close(); + + FPlatformProcess::Sleep(1.0f); + + CompleteTestWithResult(TEXT("metric-capture"), true, TEXT("Test complete")); +} + void USentryPlaygroundGameInstance::RunInitOnly() { USentrySubsystem* SentrySubsystem = GEngine->GetEngineSubsystem(); diff --git a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h index d633321b3..c9fa34fe3 100644 --- a/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h +++ b/sample/Source/SentryPlayground/SentryPlaygroundGameInstance.h @@ -24,6 +24,7 @@ class SENTRYPLAYGROUND_API USentryPlaygroundGameInstance : public UGameInstance void RunCrashTest(ESentryAppTerminationType CrashType); void RunMessageTest(); void RunLogTest(); + void RunMetricTest(); void RunInitOnly(); void ConfigureTestContext(); diff --git a/scripts/packaging/package.snapshot b/scripts/packaging/package.snapshot index 4b77a49b8..b84611024 100644 --- a/scripts/packaging/package.snapshot +++ b/scripts/packaging/package.snapshot @@ -208,7 +208,6 @@ Source/Sentry/Private/SentryTransactionContext.cpp Source/Sentry/Private/SentryUnit.cpp Source/Sentry/Private/SentryUser.cpp Source/Sentry/Private/SentryVariant.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 @@ -217,7 +216,6 @@ 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/SentryTestBeforeMetricHandler.h Source/Sentry/Private/Tests/SentryTests.h Source/Sentry/Private/Tests/SentryTraceSampling.spec.cpp Source/Sentry/Private/Tests/SentryTraceSamplingHandler.h