From c948228323589a15f3f5482a3d7e5a801489f68a Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 27 Jul 2026 10:05:34 +0200 Subject: [PATCH 1/7] feat(auto-metrics): Add experimental auto game-metrics collection for common performance indicators --- CHANGELOG.md | 4 + .../ConfigurationWindow/EnrichmentTab.cs | 60 ++++++++ .../Integrations/AutoMetricsIntegration.cs | 92 ++++++++++++ src/Sentry.Unity/Metrics/FrameTimeMonitor.cs | 92 ++++++++++++ .../Metrics/GameMetricAttributes.cs | 60 ++++++++ src/Sentry.Unity/Metrics/GameStatsMonitor.cs | 57 ++++++++ src/Sentry.Unity/Metrics/GcMonitor.cs | 65 +++++++++ .../Metrics/NetworkMetricsMonitor.cs | 133 ++++++++++++++++++ .../ScriptableSentryUnityOptions.cs | 15 ++ .../SentryMonoBehaviour.Metrics.cs | 70 +++++++++ src/Sentry.Unity/SentryUnityOptions.cs | 43 ++++++ .../.gradle/8.9/checksums/checksums.lock | Bin 0 -> 17 bytes .../8.9/dependencies-accessors/gc.properties | 0 .../.gradle/8.9/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/8.9/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../GradleProject/.gradle/8.9/gc.properties | 0 .../.gradle/9.2.0/checksums/checksums.lock | Bin 0 -> 17 bytes .../.gradle/9.2.0/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/9.2.0/fileHashes/fileHashes.bin | Bin 0 -> 18597 bytes .../.gradle/9.2.0/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../GradleProject/.gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../GradleProject/.gradle/vcs-1/gc.properties | 0 .../.gradle/9.2.0/checksums/checksums.lock | Bin 0 -> 17 bytes .../.gradle/9.2.0/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/9.2.0/fileHashes/fileHashes.bin | Bin 0 -> 18547 bytes .../.gradle/9.2.0/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../IL2CPP/Gradle/.gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../IL2CPP/Gradle/.gradle/vcs-1/gc.properties | 0 .../.gradle/9.2.0/checksums/checksums.lock | Bin 0 -> 17 bytes .../.gradle/9.2.0/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/9.2.0/fileHashes/fileHashes.bin | Bin 0 -> 18547 bytes .../.gradle/9.2.0/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../gradleOut/.gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../gradleOut/.gradle/vcs-1/gc.properties | 0 .../GameMetricAttributesTests.cs | 46 ++++++ 41 files changed, 743 insertions(+) create mode 100644 src/Sentry.Unity/Integrations/AutoMetricsIntegration.cs create mode 100644 src/Sentry.Unity/Metrics/FrameTimeMonitor.cs create mode 100644 src/Sentry.Unity/Metrics/GameMetricAttributes.cs create mode 100644 src/Sentry.Unity/Metrics/GameStatsMonitor.cs create mode 100644 src/Sentry.Unity/Metrics/GcMonitor.cs create mode 100644 src/Sentry.Unity/Metrics/NetworkMetricsMonitor.cs create mode 100644 src/Sentry.Unity/SentryMonoBehaviour.Metrics.cs create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/checksums/checksums.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/dependencies-accessors/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/fileChanges/last-build.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/fileHashes/fileHashes.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/checksums/checksums.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/fileChanges/last-build.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/fileHashes/fileHashes.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/fileHashes/fileHashes.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/buildOutputCleanup/cache.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/vcs-1/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/checksums/checksums.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileChanges/last-build.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileHashes/fileHashes.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileHashes/fileHashes.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/buildOutputCleanup/cache.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/vcs-1/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/checksums/checksums.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/fileChanges/last-build.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/fileHashes/fileHashes.bin create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/fileHashes/fileHashes.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/gc.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/buildOutputCleanup/cache.properties create mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/vcs-1/gc.properties create mode 100644 test/Sentry.Unity.Tests/GameMetricAttributesTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e93a89db..5ec5259b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Added experimental auto game-metrics. When enabled, the SDK periodically collects common performance metrics and sends them to Sentry via the metrics API. ([#2777](https://github.com/getsentry/sentry-unity/pull/2777)) + ### Fixes - The SDK now correctly synchronizes the `Environment` set on the `Scope` events coming from the native layer ([#2764](https://github.com/getsentry/sentry-unity/pull/2764)) diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/EnrichmentTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/EnrichmentTab.cs index 0d5a29783..a28ffacc5 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/EnrichmentTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/EnrichmentTab.cs @@ -103,6 +103,66 @@ internal static void Display(ScriptableSentryUnityOptions options) EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); EditorGUILayout.Space(); + { + GUILayout.Label("EXPERIMENTAL: Auto-collect common game performance metrics and send them " + + "to Sentry. Requires 'Enable Metrics'.", EditorStyles.wordWrappedMiniLabel); + + options.AutoFrameTimeMetrics = EditorGUILayout.BeginToggleGroup( + new GUIContent("Auto Frame-Time Metrics", "Periodically emit frame time and FPS as Sentry metrics."), + options.AutoFrameTimeMetrics); + EditorGUI.indentLevel++; + + options.FrameTimeMetricSampleInterval = EditorGUILayout.IntField( + new GUIContent("Sample Interval (frames)", "Emit a frame-time sample every Nth frame." + + "\nDefault: 30"), + options.FrameTimeMetricSampleInterval); + if (options.FrameTimeMetricSampleInterval < 1) + { + options.FrameTimeMetricSampleInterval = 1; + } + + EditorGUI.indentLevel--; + EditorGUILayout.EndToggleGroup(); + + options.AutoGameStatsMetrics = EditorGUILayout.Toggle( + new GUIContent("Auto Game-Stats Metrics", "Periodically emit memory usage as Sentry metrics."), + options.AutoGameStatsMetrics); + + options.AutoGcMetrics = EditorGUILayout.Toggle( + new GUIContent("Auto GC Metrics", "Periodically emit garbage-collection counts (per generation) " + + "as Sentry metrics."), + options.AutoGcMetrics); + + options.AutoNetworkMetrics = EditorGUILayout.Toggle( + new GUIContent("Auto Network Metrics", "Periodically emit basic multiplayer network metrics " + + "(round-trip time and connected-client count) as Sentry " + + "metrics. Requires the Netcode for GameObjects package."), + options.AutoNetworkMetrics); + + options.GameStatsMetricSampleIntervalSeconds = EditorGUILayout.IntField( + new GUIContent("Game-Stats/GC Interval (s)", "How often, in seconds, the game-stats and GC " + + "metrics are sampled." + + "\nDefault: 60"), + options.GameStatsMetricSampleIntervalSeconds); + if (options.GameStatsMetricSampleIntervalSeconds < 1) + { + options.GameStatsMetricSampleIntervalSeconds = 1; + } + + options.NetworkMetricsSampleIntervalSeconds = EditorGUILayout.IntField( + new GUIContent("Network Interval (s)", "How often, in seconds, the network metrics are sampled." + + "\nDefault: 10"), + options.NetworkMetricsSampleIntervalSeconds); + if (options.NetworkMetricsSampleIntervalSeconds < 1) + { + options.NetworkMetricsSampleIntervalSeconds = 1; + } + } + + EditorGUILayout.Space(); + EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); + EditorGUILayout.Space(); + { options.ReleaseOverride = EditorGUILayout.TextField( new GUIContent("Override Release", "By default release is built from the Application info as: " + diff --git a/src/Sentry.Unity/Integrations/AutoMetricsIntegration.cs b/src/Sentry.Unity/Integrations/AutoMetricsIntegration.cs new file mode 100644 index 000000000..c8004258a --- /dev/null +++ b/src/Sentry.Unity/Integrations/AutoMetricsIntegration.cs @@ -0,0 +1,92 @@ +using System; +using Sentry.Extensibility; +using Sentry.Integrations; + +namespace Sentry.Unity; + +/// +/// Registers the auto-collected game performance metrics (frame time, FPS, memory, GC). Metrics are +/// emitted through the managed metrics API () and sent to Sentry via +/// the regular transport, so no native support is required. Mirrors the Unreal SDK's +/// USentrySubsystem::ConfigurePerformanceMetrics. +/// +internal class AutoMetricsIntegration : ISdkIntegration +{ + private readonly SentryMonoBehaviour _monoBehaviour; + + public AutoMetricsIntegration(SentryMonoBehaviour monoBehaviour) + { + _monoBehaviour = monoBehaviour; + } + + public void Register(IHub hub, SentryOptions sentryOptions) + { + var options = (SentryUnityOptions)sentryOptions; + + var frameTime = options.AutoFrameTimeMetrics; + var gameStats = options.AutoGameStatsMetrics; + var gc = options.AutoGcMetrics; + var network = options.AutoNetworkMetrics; + + // Nothing to collect. + if (!frameTime && !gameStats && !gc && !network) + { + return; + } + + // The managed metrics emitter is a no-op unless the metrics API is enabled. + if (!options.EnableMetrics) + { + options.DiagnosticLogger?.LogWarning( + "Auto game metrics are enabled but 'EnableMetrics' is disabled. No metrics will be collected."); + return; + } + + var logger = options.DiagnosticLogger; + + // Shared attributes (hardware info + current scene) attached to every metric. + var attributes = new GameMetricAttributes(); + + if (frameTime) + { + var monitor = new FrameTimeMonitor(options.FrameTimeMetricSampleInterval, attributes, logger); + _monoBehaviour.StartFrameTimeMetrics(monitor); + logger?.LogInfo("Frame-time metrics enabled (sampling every {0} frames).", + options.FrameTimeMetricSampleInterval); + } + + // Game-stats and GC are both periodic snapshots, so they share a single wall-clock ticker. + Action? periodicSample = null; + + if (gameStats) + { + var monitor = new GameStatsMonitor(attributes, logger); + periodicSample += monitor.Sample; + logger?.LogInfo("Game-stats metrics enabled."); + } + + if (gc) + { + var monitor = new GcMonitor(attributes, logger); + periodicSample += monitor.Sample; + logger?.LogInfo("GC metrics enabled."); + } + + if (periodicSample is not null) + { + var interval = TimeSpan.FromSeconds(Math.Max(1, options.GameStatsMetricSampleIntervalSeconds)); + _monoBehaviour.StartPeriodicMetrics(periodicSample, interval); + logger?.LogInfo("Periodic game metrics sampling every {0}s.", + options.GameStatsMetricSampleIntervalSeconds); + } + + // Network is polled on its own (typically faster) ticker, matching the Unreal SDK. + if (network) + { + var monitor = new NetworkMetricsMonitor(attributes, logger); + var interval = TimeSpan.FromSeconds(Math.Max(1, options.NetworkMetricsSampleIntervalSeconds)); + _monoBehaviour.StartNetworkMetrics(monitor.Sample, interval); + logger?.LogInfo("Network metrics sampling every {0}s.", options.NetworkMetricsSampleIntervalSeconds); + } + } +} diff --git a/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs b/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs new file mode 100644 index 000000000..2c0bc56dd --- /dev/null +++ b/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using Sentry.Extensibility; +using UnityEngine; + +namespace Sentry.Unity; + +/// +/// Samples per-frame performance and emits frame-time and FPS metrics every Nth frame, plus the +/// CPU main-/render-thread times when the engine's has data. +/// Driven once per frame from . Mirrors the Unreal SDK's +/// FSentryPerfFrameTimeMonitor. +/// +internal class FrameTimeMonitor +{ + internal const string FrameTimeMetric = "game.perf.frame_time"; + internal const string FpsMetric = "game.perf.fps"; + internal const string GameThreadMetric = "game.perf.game_thread"; + internal const string RenderThreadMetric = "game.perf.render_thread"; + + private readonly int _sampleInterval; + private readonly GameMetricAttributes _attributes; + private readonly IDiagnosticLogger? _logger; + + // Reused between samples to avoid a per-sample allocation. + private readonly FrameTiming[] _timings = new FrameTiming[1]; + + private int _frameCount; + + public FrameTimeMonitor(int sampleInterval, GameMetricAttributes attributes, IDiagnosticLogger? logger) + { + _sampleInterval = Math.Max(1, sampleInterval); + _attributes = attributes; + _logger = logger; + } + + /// + /// Called once per frame on the main thread. Emits a sample every sampleInterval frames. + /// + public void OnFrame() + { + if (++_frameCount % _sampleInterval != 0) + { + return; + } + + try + { + var deltaSeconds = Time.unscaledDeltaTime; + var attributes = _attributes.Current; + + SentrySdk.Metrics.EmitDistribution( + FrameTimeMetric, deltaSeconds * 1000.0, MeasurementUnit.Duration.Millisecond, attributes); + SentrySdk.Metrics.EmitGauge( + FpsMetric, deltaSeconds > 0f ? 1.0 / deltaSeconds : 0.0, MeasurementUnit.None, attributes); + + EmitThreadTimings(attributes); + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to emit frame-time metrics."); + } + } + + // CPU main-/render-thread times from the FrameTimingManager. These are already tracked by the + // engine (no GPU timer queries are involved), so reading them adds no measurable overhead. GPU + // timing is intentionally not read. The data is only available when 'Frame Timing Stats' is + // enabled (implicit in development builds), so these metrics are emitted best-effort and are + // silently skipped when unavailable. + private void EmitThreadTimings(IReadOnlyList> attributes) + { + FrameTimingManager.CaptureFrameTimings(); + if (FrameTimingManager.GetLatestTimings(1, _timings) == 0) + { + return; + } + + var timing = _timings[0]; + + if (timing.cpuMainThreadFrameTime > 0.0) + { + SentrySdk.Metrics.EmitDistribution( + GameThreadMetric, timing.cpuMainThreadFrameTime, MeasurementUnit.Duration.Millisecond, attributes); + } + + if (timing.cpuRenderThreadFrameTime > 0.0) + { + SentrySdk.Metrics.EmitDistribution( + RenderThreadMetric, timing.cpuRenderThreadFrameTime, MeasurementUnit.Duration.Millisecond, attributes); + } + } +} diff --git a/src/Sentry.Unity/Metrics/GameMetricAttributes.cs b/src/Sentry.Unity/Metrics/GameMetricAttributes.cs new file mode 100644 index 000000000..8a9473c8d --- /dev/null +++ b/src/Sentry.Unity/Metrics/GameMetricAttributes.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Sentry.Unity; + +/// +/// Caches the set of attributes attached to every auto-collected game metric. The hardware +/// attributes are captured once; the active scene name is refreshed whenever the scene changes. +/// Mirrors the Unreal SDK's FSentryPerfMetricAttributes. +/// +internal class GameMetricAttributes +{ + private readonly ISceneManager _sceneManager; + private readonly Action _onActiveSceneChanged; + + // Built and read on the Unity main thread - both the per-frame sampling and the scene-changed + // callback run there - so no synchronization is required. + private KeyValuePair[] _attributes; + + public GameMetricAttributes(ISceneManager? sceneManager = null) + { + _sceneManager = sceneManager ?? SceneManagerAdapter.Instance; + _attributes = Build(_sceneManager.GetActiveScene().Name); + + _onActiveSceneChanged = (_, to) => _attributes = Build(to.Name); + _sceneManager.ActiveSceneChanged += _onActiveSceneChanged; + } + + /// + /// The attributes to attach to metrics emitted for the current frame/scene. + /// + public IReadOnlyList> Current => _attributes; + + private static KeyValuePair[] Build(string? mapName) + { + var attributes = new List>(7); + + void Add(string key, object? value) + { + if (value is not null) + { + attributes.Add(new KeyValuePair(key, value)); + } + } + + Add("gpu.name", MainThreadData.GraphicsDeviceName); + Add("cpu.cores", MainThreadData.ProcessorCount); + // Unity reports the system memory size in megabytes. + Add("ram.gb", MainThreadData.SystemMemorySize.HasValue ? MainThreadData.SystemMemorySize.Value / 1024 : null); + Add("res.x", Screen.width); + Add("res.y", Screen.height); + Add("map", mapName); + Add("platform", Application.platform.ToString()); + + return attributes.ToArray(); + } + + public void Dispose() => _sceneManager.ActiveSceneChanged -= _onActiveSceneChanged; +} diff --git a/src/Sentry.Unity/Metrics/GameStatsMonitor.cs b/src/Sentry.Unity/Metrics/GameStatsMonitor.cs new file mode 100644 index 000000000..5b2d8b0b2 --- /dev/null +++ b/src/Sentry.Unity/Metrics/GameStatsMonitor.cs @@ -0,0 +1,57 @@ +using System; +using Sentry.Extensibility; +using UnityEngine.Profiling; + +namespace Sentry.Unity; + +/// +/// Periodically samples game statistics (memory usage) and emits them as gauge metrics. Mirrors the +/// Unreal SDK's FSentryPerfGameStatsMonitor (Unity has no UObject count, so managed/native heap +/// sizes are reported instead). +/// +internal class GameStatsMonitor +{ + internal const string UsedMemoryMetric = "game.perf.used_memory"; + internal const string ReservedMemoryMetric = "game.perf.reserved_memory"; + internal const string MonoUsedMemoryMetric = "game.perf.mono_used_memory"; + + private readonly GameMetricAttributes _attributes; + private readonly IDiagnosticLogger? _logger; + + public GameStatsMonitor(GameMetricAttributes attributes, IDiagnosticLogger? logger) + { + _attributes = attributes; + _logger = logger; + } + + public void Sample() + { + try + { + var attributes = _attributes.Current; + + // The Profiler returns '0' when a value is not available - skip those. + var used = Profiler.GetTotalAllocatedMemoryLong(); + if (used > 0) + { + SentrySdk.Metrics.EmitGauge(UsedMemoryMetric, used, MeasurementUnit.Information.Byte, attributes); + } + + var reserved = Profiler.GetTotalReservedMemoryLong(); + if (reserved > 0) + { + SentrySdk.Metrics.EmitGauge(ReservedMemoryMetric, reserved, MeasurementUnit.Information.Byte, attributes); + } + + var monoUsed = Profiler.GetMonoUsedSizeLong(); + if (monoUsed > 0) + { + SentrySdk.Metrics.EmitGauge(MonoUsedMemoryMetric, monoUsed, MeasurementUnit.Information.Byte, attributes); + } + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to emit game-stats metrics."); + } + } +} diff --git a/src/Sentry.Unity/Metrics/GcMonitor.cs b/src/Sentry.Unity/Metrics/GcMonitor.cs new file mode 100644 index 000000000..33f2acf4c --- /dev/null +++ b/src/Sentry.Unity/Metrics/GcMonitor.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Sentry.Extensibility; + +namespace Sentry.Unity; + +/// +/// Periodically samples the garbage collector and emits the number of collections per generation +/// since the previous sample as a counter metric. +/// +/// Note: unlike the Unreal SDK's FSentryPerfGCMonitor, this reports collection counts rather +/// than pause duration - Unity's Mono/IL2CPP runtime does not expose a reliable GC pause +/// callback. +/// +/// +internal class GcMonitor +{ + internal const string GcCollectionsMetric = "game.perf.gc_collections"; + + private readonly GameMetricAttributes _attributes; + private readonly IDiagnosticLogger? _logger; + private readonly int[] _previousCounts; + + public GcMonitor(GameMetricAttributes attributes, IDiagnosticLogger? logger) + { + _attributes = attributes; + _logger = logger; + + _previousCounts = new int[GC.MaxGeneration + 1]; + for (var generation = 0; generation < _previousCounts.Length; generation++) + { + _previousCounts[generation] = GC.CollectionCount(generation); + } + } + + public void Sample() + { + try + { + for (var generation = 0; generation < _previousCounts.Length; generation++) + { + var current = GC.CollectionCount(generation); + var delta = current - _previousCounts[generation]; + _previousCounts[generation] = current; + + if (delta <= 0) + { + continue; + } + + // Attach the generation alongside the shared attributes. + var attributes = new List>(_attributes.Current) + { + new("gc.generation", generation) + }; + + SentrySdk.Metrics.EmitCounter(GcCollectionsMetric, delta, attributes); + } + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to emit GC metrics."); + } + } +} diff --git a/src/Sentry.Unity/Metrics/NetworkMetricsMonitor.cs b/src/Sentry.Unity/Metrics/NetworkMetricsMonitor.cs new file mode 100644 index 000000000..d210e582f --- /dev/null +++ b/src/Sentry.Unity/Metrics/NetworkMetricsMonitor.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Sentry.Extensibility; + +namespace Sentry.Unity; + +/// +/// Periodically emits basic multiplayer network metrics for Netcode for GameObjects (NGO): the +/// round-trip time to the server (on clients) and the connected-client count (on the server). +/// +/// NGO is an optional package, so it is accessed via reflection to avoid a hard dependency. Only +/// low-cost, already-cached values are read (RTT and client count); no packet/byte instrumentation +/// is enabled, so there is no measurable runtime overhead. When NGO is not present, or no session +/// is running, the monitor is inert. +/// +/// +internal class NetworkMetricsMonitor +{ + internal const string PingMetric = "game.perf.net.ping"; + internal const string NumClientsMetric = "game.perf.net.num_clients"; + + // In NGO the server is always client id 0. + private const ulong ServerClientId = 0; + + private readonly GameMetricAttributes _attributes; + private readonly IDiagnosticLogger? _logger; + + private readonly PropertyInfo? _singletonProperty; + private readonly PropertyInfo? _isListeningProperty; + private readonly PropertyInfo? _isServerProperty; + private readonly PropertyInfo? _connectedClientsIdsProperty; + private readonly PropertyInfo? _networkConfigProperty; + + // Resolved lazily - the concrete transport type depends on the configured transport. + private PropertyInfo? _networkTransportProperty; + private bool _loggedMissingRtt; + + public NetworkMetricsMonitor(GameMetricAttributes attributes, IDiagnosticLogger? logger) + { + _attributes = attributes; + _logger = logger; + + var networkManagerType = Type.GetType("Unity.Netcode.NetworkManager, Unity.Netcode.Runtime"); + if (networkManagerType is null) + { + _logger?.LogInfo("Network metrics are enabled but Netcode for GameObjects was not found - skipping."); + return; + } + + _singletonProperty = networkManagerType.GetProperty("Singleton", BindingFlags.Public | BindingFlags.Static); + _isListeningProperty = networkManagerType.GetProperty("IsListening"); + _isServerProperty = networkManagerType.GetProperty("IsServer"); + _connectedClientsIdsProperty = networkManagerType.GetProperty("ConnectedClientsIds"); + _networkConfigProperty = networkManagerType.GetProperty("NetworkConfig"); + } + + public void Sample() + { + if (_singletonProperty is null) + { + return; + } + + try + { + var networkManager = _singletonProperty.GetValue(null); + if (networkManager is null) + { + return; + } + + // No active session - nothing to report. + if (_isListeningProperty?.GetValue(networkManager) is not true) + { + return; + } + + var attributes = _attributes.Current; + + if (_isServerProperty?.GetValue(networkManager) is true) + { + if (_connectedClientsIdsProperty?.GetValue(networkManager) is IReadOnlyCollection clients) + { + SentrySdk.Metrics.EmitGauge(NumClientsMetric, clients.Count, MeasurementUnit.None, attributes); + } + } + else + { + var rtt = GetCurrentRtt(networkManager); + if (rtt.HasValue) + { + SentrySdk.Metrics.EmitGauge( + PingMetric, (double)rtt.Value, MeasurementUnit.Duration.Millisecond, attributes); + } + } + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to emit network metrics."); + } + } + + private ulong? GetCurrentRtt(object networkManager) + { + var networkConfig = _networkConfigProperty?.GetValue(networkManager); + if (networkConfig is null) + { + return null; + } + + _networkTransportProperty ??= networkConfig.GetType().GetProperty("NetworkTransport"); + var transport = _networkTransportProperty?.GetValue(networkConfig); + if (transport is null) + { + return null; + } + + var getCurrentRtt = transport.GetType().GetMethod("GetCurrentRtt", new[] { typeof(ulong) }); + if (getCurrentRtt is null) + { + if (!_loggedMissingRtt) + { + _logger?.LogDebug("Network transport does not expose 'GetCurrentRtt' - skipping ping metric."); + _loggedMissingRtt = true; + } + + return null; + } + + return getCurrentRtt.Invoke(transport, new object[] { ServerClientId }) as ulong?; + } +} diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index 1841c1a65..d1b7d49bd 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -74,6 +74,14 @@ public static string GetConfigPath(string? notDefaultConfigName = null) [field: SerializeField] public int MaxViewHierarchyObjectChildCount { get; set; } = 20; [field: SerializeField] public int MaxViewHierarchyDepth { get; set; } = 10; + [field: SerializeField] public bool AutoFrameTimeMetrics { get; set; } = false; + [field: SerializeField] public int FrameTimeMetricSampleInterval { get; set; } = 30; + [field: SerializeField] public bool AutoGameStatsMetrics { get; set; } = false; + [field: SerializeField] public bool AutoGcMetrics { get; set; } = false; + [field: SerializeField] public bool AutoNetworkMetrics { get; set; } = false; + [field: SerializeField] public int GameStatsMetricSampleIntervalSeconds { get; set; } = 60; + [field: SerializeField] public int NetworkMetricsSampleIntervalSeconds { get; set; } = 10; + [field: SerializeField] public bool EnableStructuredLogging { get; set; } = false; [field: SerializeField] public bool StructuredLogOnDebugLog { get; set; } = false; [field: SerializeField] public bool StructuredLogOnDebugLogWarning { get; set; } = true; @@ -189,6 +197,13 @@ internal SentryUnityOptions ToSentryUnityOptions( MaxViewHierarchyRootObjects = MaxViewHierarchyRootObjects, MaxViewHierarchyObjectChildCount = MaxViewHierarchyObjectChildCount, MaxViewHierarchyDepth = MaxViewHierarchyDepth, + AutoFrameTimeMetrics = AutoFrameTimeMetrics, + FrameTimeMetricSampleInterval = FrameTimeMetricSampleInterval, + AutoGameStatsMetrics = AutoGameStatsMetrics, + AutoGcMetrics = AutoGcMetrics, + AutoNetworkMetrics = AutoNetworkMetrics, + GameStatsMetricSampleIntervalSeconds = GameStatsMetricSampleIntervalSeconds, + NetworkMetricsSampleIntervalSeconds = NetworkMetricsSampleIntervalSeconds, MaxBreadcrumbs = MaxBreadcrumbs, ReportAssembliesMode = ReportAssembliesMode, SendDefaultPii = SendDefaultPii, diff --git a/src/Sentry.Unity/SentryMonoBehaviour.Metrics.cs b/src/Sentry.Unity/SentryMonoBehaviour.Metrics.cs new file mode 100644 index 000000000..b8d52b3de --- /dev/null +++ b/src/Sentry.Unity/SentryMonoBehaviour.Metrics.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections; +using UnityEngine; + +namespace Sentry.Unity; + +/// +/// Drives the sampling for the auto-collected game performance metrics. The per-frame coroutine +/// resumes every frame during the player loop, so observes +/// the current frame's on the main thread. The periodic +/// coroutine drives the game-stats and GC monitors at a fixed wall-clock interval. +/// +public partial class SentryMonoBehaviour +{ + private Coroutine? _frameMetricsCoroutine; + private Coroutine? _periodicMetricsCoroutine; + private Coroutine? _networkMetricsCoroutine; + + internal void StartFrameTimeMetrics(FrameTimeMonitor monitor) + { + if (_frameMetricsCoroutine is not null) + { + StopCoroutine(_frameMetricsCoroutine); + } + + _frameMetricsCoroutine = StartCoroutine(FrameMetricsCoroutine(monitor)); + } + + internal void StartPeriodicMetrics(Action sample, TimeSpan interval) + { + if (_periodicMetricsCoroutine is not null) + { + StopCoroutine(_periodicMetricsCoroutine); + } + + _periodicMetricsCoroutine = StartCoroutine(PeriodicMetricsCoroutine(sample, interval)); + } + + internal void StartNetworkMetrics(Action sample, TimeSpan interval) + { + if (_networkMetricsCoroutine is not null) + { + StopCoroutine(_networkMetricsCoroutine); + } + + _networkMetricsCoroutine = StartCoroutine(PeriodicMetricsCoroutine(sample, interval)); + } + + private static IEnumerator FrameMetricsCoroutine(FrameTimeMonitor monitor) + { + // Skip the first frame - the synchronous startup stall would skew the very first sample. + yield return null; + + while (true) + { + monitor.OnFrame(); + yield return null; + } + } + + private static IEnumerator PeriodicMetricsCoroutine(Action sample, TimeSpan interval) + { + var wait = new WaitForSecondsRealtime((float)interval.TotalSeconds); + while (true) + { + yield return wait; + sample(); + } + } +} diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 9c7fd9166..78d70d21f 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -134,6 +134,48 @@ public sealed class SentryUnityOptions : SentryOptions /// public int MaxViewHierarchyDepth { get; set; } = 10; + /// + /// EXPERIMENTAL: Automatically collect per-frame performance metrics (frame time and FPS) and + /// send them to Sentry as metrics. Requires to remain + /// enabled (the default). + /// + public bool AutoFrameTimeMetrics { get; set; } = false; + + /// + /// How often, in frames, the per-frame metrics are sampled. For example, a value of 30 emits a + /// sample every 30th frame. Minimum 1. + /// + public int FrameTimeMetricSampleInterval { get; set; } = 30; + + /// + /// EXPERIMENTAL: Periodically collect game statistics (memory usage) and send them to Sentry as + /// metrics. Requires to remain enabled (the default). + /// + public bool AutoGameStatsMetrics { get; set; } = false; + + /// + /// EXPERIMENTAL: Periodically collect garbage-collection counts (per generation) and send them + /// to Sentry as metrics. Requires to remain enabled. + /// + public bool AutoGcMetrics { get; set; } = false; + + /// + /// EXPERIMENTAL: Periodically collect basic multiplayer network metrics - round-trip time (on + /// clients) and connected-client count (on the server) - and send them to Sentry as metrics. + /// Requires the Netcode for GameObjects package and . + /// + public bool AutoNetworkMetrics { get; set; } = false; + + /// + /// How often, in seconds, the periodic game-stats and GC metrics are sampled. Minimum 1. + /// + public int GameStatsMetricSampleIntervalSeconds { get; set; } = 60; + + /// + /// How often, in seconds, the network metrics are sampled. Minimum 1. + /// + public int NetworkMetricsSampleIntervalSeconds { get; set; } = 10; + /// /// The quality of the attached screenshot /// @@ -494,6 +536,7 @@ internal SentryUnityOptions(IApplication? application = null, AddIntegration(new LifeCycleIntegration(behaviour)); AddIntegration(new TraceGenerationIntegration(behaviour)); AddIntegration(new LowMemoryIntegration()); + AddIntegration(new AutoMetricsIntegration(behaviour)); AddExceptionFilter(new UnityBadGatewayExceptionFilter()); AddExceptionFilter(new UnityWebExceptionFilter()); diff --git a/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/checksums/checksums.lock b/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..f1a2d64a376a9180f68bbfc58da598e033fef134 GIT binary patch literal 17 TcmZQxzvKSi*PX3f7@zC2?gpgLghAoU*brgyS5FkK+009C72oNAZfB*pk z1PBlyK!5;&|01vvA2N|YCasmSpu87CV=@M%*W<10&GvRLU+(S}{|5&<`_s=$&A~9= zDe}b}^PPOnNPqwV0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF{H4G`JZv`hnXFenp3Y9@9=omEqxSmeLt{0BiG;+)B392}u`r9tZjso%$UTF&7w`rK+C#j8 z$oD60|NQOHerwxq2<77C9^o0KXDTB=fB*pk1PBlyK!5-N0t5&UAV7cs0RjZxMPL*U z@^L#%xt2{yVHQFoACt^tI{*A@Hz%nYLcjC4KX{C>SoxpQ5giG;+)B392}u`r9tZjso%$UTF&7w`rK+C#j8 z$oD60|NQOHerwxq2<77C9^o0KXDTB=fB*pk1PBlyK!5-N0t5&UAV7cs0RjZxMPL*U z@^L#%xt2{yVHQFoACt^tI{*A@Hz%nYLcjC4KX{C>SoxpQ5g + new() { ActiveSceneName = activeScene }; + + [Test] + public void Current_ContainsActiveSceneName() + { + var sceneManager = CreateSceneManager("MainMenu"); + + var attributes = new GameMetricAttributes(sceneManager); + + var map = attributes.Current.First(kvp => kvp.Key == "map"); + Assert.AreEqual("MainMenu", map.Value); + } + + [Test] + public void ActiveSceneChanged_RefreshesMapAttribute() + { + var sceneManager = CreateSceneManager("MainMenu"); + var attributes = new GameMetricAttributes(sceneManager); + + sceneManager.OnActiveSceneChanged(new SceneAdapter("MainMenu"), new SceneAdapter("Level1")); + + var map = attributes.Current.First(kvp => kvp.Key == "map"); + Assert.AreEqual("Level1", map.Value); + } + + [Test] + public void Dispose_StopsRefreshingOnSceneChange() + { + var sceneManager = CreateSceneManager("MainMenu"); + var attributes = new GameMetricAttributes(sceneManager); + + attributes.Dispose(); + sceneManager.OnActiveSceneChanged(new SceneAdapter("MainMenu"), new SceneAdapter("Level1")); + + var map = attributes.Current.First(kvp => kvp.Key == "map"); + Assert.AreEqual("MainMenu", map.Value); + } +} From 716f3d20596d51c7da5fe4b748ec54fbecd90bec Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 27 Jul 2026 10:06:40 +0200 Subject: [PATCH 2/7] Remove build artifacts --- .../.gradle/9.2.0/checksums/checksums.lock | Bin 17 -> 0 bytes .../.gradle/9.2.0/fileChanges/last-build.bin | Bin 1 -> 0 bytes .../.gradle/9.2.0/fileHashes/fileHashes.bin | Bin 18547 -> 0 bytes .../.gradle/9.2.0/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../Temp/gradleOut/.gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup/buildOutputCleanup.lock | Bin 17 -> 0 bytes .../.gradle/buildOutputCleanup/cache.properties | 2 -- .../Temp/gradleOut/.gradle/vcs-1/gc.properties | 0 8 files changed, 2 deletions(-) delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/checksums/checksums.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/fileChanges/last-build.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/fileHashes/fileHashes.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/fileHashes/fileHashes.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/gc.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/buildOutputCleanup/buildOutputCleanup.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/buildOutputCleanup/cache.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/vcs-1/gc.properties diff --git a/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/checksums/checksums.lock b/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Temp/gradleOut/.gradle/9.2.0/checksums/checksums.lock deleted file mode 100644 index c2cfc990a8a3cc85751b93652219c6b7f8cb457d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 TcmZRsuY2*}+QOT=7@ziG;+)B392}u`r9tZjso%$UTF&7w`rK+C#j8 z$oD60|NQOHerwxq2<77C9^o0KXDTB=fB*pk1PBlyK!5-N0t5&UAV7cs0RjZxMPL*U z@^L#%xt2{yVHQFoACt^tI{*A@Hz%nYLcjC4KX{C>SoxpQ5g Date: Mon, 27 Jul 2026 10:07:18 +0200 Subject: [PATCH 3/7] Remove build artifacts 2 --- .../.gradle/8.9/checksums/checksums.lock | Bin 17 -> 0 bytes .../8.9/dependencies-accessors/gc.properties | 0 .../.gradle/8.9/fileChanges/last-build.bin | Bin 1 -> 0 bytes .../.gradle/8.9/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../GradleProject/.gradle/8.9/gc.properties | 0 .../.gradle/9.2.0/checksums/checksums.lock | Bin 17 -> 0 bytes .../.gradle/9.2.0/fileChanges/last-build.bin | Bin 1 -> 0 bytes .../.gradle/9.2.0/fileHashes/fileHashes.bin | Bin 18597 -> 0 bytes .../.gradle/9.2.0/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../GradleProject/.gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup/buildOutputCleanup.lock | Bin 17 -> 0 bytes .../.gradle/buildOutputCleanup/cache.properties | 2 -- .../GradleProject/.gradle/vcs-1/gc.properties | 0 13 files changed, 2 deletions(-) delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/checksums/checksums.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/dependencies-accessors/gc.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/fileChanges/last-build.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/fileHashes/fileHashes.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/gc.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/checksums/checksums.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/fileChanges/last-build.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/fileHashes/fileHashes.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/fileHashes/fileHashes.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/9.2.0/gc.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/buildOutputCleanup/buildOutputCleanup.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/buildOutputCleanup/cache.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/vcs-1/gc.properties diff --git a/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/checksums/checksums.lock b/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/GradleProject/.gradle/8.9/checksums/checksums.lock deleted file mode 100644 index f1a2d64a376a9180f68bbfc58da598e033fef134..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 TcmZQxzvKSi*PX3f7@zC2?gpgLghAoU*brgyS5FkK+009C72oNAZfB*pk z1PBlyK!5;&|01vvA2N|YCasmSpu87CV=@M%*W<10&GvRLU+(S}{|5&<`_s=$&A~9= zDe}b}^PPOnNPqwV0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF{H4G`JZv`hnXFenp3Y9@9=omEqxSmeLt{0B Date: Mon, 27 Jul 2026 10:07:50 +0200 Subject: [PATCH 4/7] Remove build artifacts 3 --- .../.gradle/9.2.0/checksums/checksums.lock | Bin 17 -> 0 bytes .../.gradle/9.2.0/fileChanges/last-build.bin | Bin 1 -> 0 bytes .../.gradle/9.2.0/fileHashes/fileHashes.bin | Bin 18547 -> 0 bytes .../.gradle/9.2.0/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../IL2CPP/Gradle/.gradle/9.2.0/gc.properties | 0 .../buildOutputCleanup/buildOutputCleanup.lock | Bin 17 -> 0 bytes .../.gradle/buildOutputCleanup/cache.properties | 2 -- .../IL2CPP/Gradle/.gradle/vcs-1/gc.properties | 0 8 files changed, 2 deletions(-) delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/checksums/checksums.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileChanges/last-build.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileHashes/fileHashes.bin delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileHashes/fileHashes.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/gc.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/buildOutputCleanup/buildOutputCleanup.lock delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/buildOutputCleanup/cache.properties delete mode 100644 test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/vcs-1/gc.properties diff --git a/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/checksums/checksums.lock b/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/checksums/checksums.lock deleted file mode 100644 index 0966946e13ac6145e79d9c3030982e011733d80a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 ScmZQBWOEEXA+NcF0SW*ipaR7J diff --git a/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileChanges/last-build.bin b/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileChanges/last-build.bin deleted file mode 100644 index f76dd238ade08917e6712764a16a22005a50573d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1 IcmZPo000310RR91 diff --git a/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileHashes/fileHashes.bin b/test/Sentry.Unity.Editor.Tests/TestFiles/SymbolsUploadProject/UnityProject/Library/Bee/Android/Prj/IL2CPP/Gradle/.gradle/9.2.0/fileHashes/fileHashes.bin deleted file mode 100644 index 6b07531f1a20ae7110f12625d6ce80ecbcc87515..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18547 zcmeI(v5o;S7y#h1xky+fa@&nUgqRF>iG;+)B392}u`r9tZjso%$UTF&7w`rK+C#j8 z$oD60|NQOHerwxq2<77C9^o0KXDTB=fB*pk1PBlyK!5-N0t5&UAV7cs0RjZxMPL*U z@^L#%xt2{yVHQFoACt^tI{*A@Hz%nYLcjC4KX{C>SoxpQ5g Date: Fri, 31 Jul 2026 11:34:03 +0200 Subject: [PATCH 5/7] download native sdks refactor for bootstrapping --- AGENTS.md | 2 +- bootstrap.ps1 | 9 +++------ build/local-dev.targets | 11 ----------- docs/agent-guides/build.md | 2 +- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8a9726921..a59261401 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ dotnet build ## Native SDKs ```sh -dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity +pwsh scripts/download-native-sdks.ps1 ``` - Requires `gh`. Use when native artifacts are missing; `dotnet build` does not download them. diff --git a/bootstrap.ps1 b/bootstrap.ps1 index f09a7b5f2..a3dc21751 100644 --- a/bootstrap.ps1 +++ b/bootstrap.ps1 @@ -93,14 +93,11 @@ else { Add-Result -Status "WARN" -Name "Unity CLI" -Detail "unity is not available on PATH." -Fix "Install Unity CLI and add it to PATH before running: pwsh scripts/run-tests.ps1" } -if ($hasDotnet -and $hasGh) { - Invoke-Stage -Name "Prebuilt native SDKs" -Action { dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity } -FailureStatus "WARN" -Fix "Authenticate with 'gh auth login' and rerun, or build a platform SDK with: dotnet msbuild /t:BuildSDK src/Sentry.Unity" -} -elseif (-not $hasGh) { - Add-Result -Status "WARN" -Name "Prebuilt native SDKs" -Detail "GitHub CLI is not available." -Fix "Install gh and run 'gh auth login', then rerun: pwsh bootstrap.ps1" +if ($hasGh) { + Invoke-Stage -Name "Prebuilt native SDKs" -Action { & (Join-Path $repoRoot "scripts/download-native-sdks.ps1") -RepoRoot $repoRoot } -FailureStatus "WARN" -Fix "Authenticate with 'gh auth login' and rerun, or build a platform SDK with: dotnet msbuild /t:BuildSDK src/Sentry.Unity" } else { - Add-Result -Status "WARN" -Name "Prebuilt native SDKs" -Detail "dotnet is unavailable." -Fix "Install the SDK pinned in global.json, then rerun: pwsh bootstrap.ps1" + Add-Result -Status "WARN" -Name "Prebuilt native SDKs" -Detail "GitHub CLI is not available." -Fix "Install gh and run 'gh auth login', then rerun: pwsh bootstrap.ps1" } if (Test-Path (Join-Path $repoRoot "modules/sentry-cli.properties")) { diff --git a/build/local-dev.targets b/build/local-dev.targets index 554e2fe16..e9ad32717 100644 --- a/build/local-dev.targets +++ b/build/local-dev.targets @@ -53,15 +53,4 @@ - - - - - diff --git a/docs/agent-guides/build.md b/docs/agent-guides/build.md index 171a9a29f..89e1c8807 100644 --- a/docs/agent-guides/build.md +++ b/docs/agent-guides/build.md @@ -35,7 +35,7 @@ pwsh bootstrap.ps1 ## Prebuilt Native SDKs ```sh -dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity +pwsh scripts/download-native-sdks.ps1 ``` - Downloads missing artifacts from successful GitHub Actions CI runs, preferring `main` then current branch. From 8d5ed65e2b7f756d434f8e70134c62b2f4e199d7 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 31 Jul 2026 12:13:03 +0200 Subject: [PATCH 6/7] improved logging Co-Authored-By: OpenCode --- bootstrap.ps1 | 140 ++++++++++++++++++++++++++++++- scripts/download-native-sdks.ps1 | 3 - scripts/download-sentry-cli.ps1 | 23 +++-- 3 files changed, 154 insertions(+), 12 deletions(-) diff --git a/bootstrap.ps1 b/bootstrap.ps1 index a3dc21751..7b91fdcfb 100644 --- a/bootstrap.ps1 +++ b/bootstrap.ps1 @@ -53,6 +53,128 @@ function Test-Command { return [bool](Get-Command $Name -ErrorAction SilentlyContinue) } +function Test-DirectoryWritable { + param([Parameter(Mandatory)][string] $Path) + + $probePath = Join-Path $Path ".sentry-write-test-$([Guid]::NewGuid())" + try { + New-Item -ItemType Directory -Path $probePath | Out-Null + return $true + } + catch { + return $false + } + finally { + Remove-Item -Path $probePath -Force -ErrorAction SilentlyContinue + } +} + +function Test-WorkloadRestoreNeeded { + param([Parameter(Mandatory)][string] $DotnetPath) + + $requiredWorkloads = @("android") + if ($IsMacOS) { + $requiredWorkloads += "ios", "maccatalyst" + } + + $expectedVersion = (Get-Content (Join-Path $repoRoot "global.json") -Raw | ConvertFrom-Json).sdk.workloadVersion + $previousEAP = $PSNativeCommandUseErrorActionPreference + $PSNativeCommandUseErrorActionPreference = $false + try { + $versionOutput = @(& $DotnetPath workload --version 2>&1) + $versionExitCode = $LASTEXITCODE + $workloadList = @(& $DotnetPath workload list 2>&1) + $listExitCode = $LASTEXITCODE + } + finally { + $PSNativeCommandUseErrorActionPreference = $previousEAP + } + + if ($versionExitCode -ne 0 -or $listExitCode -ne 0) { + return $true + } + + $versionLines = @($versionOutput | ForEach-Object { "$($_)".Trim() } | Where-Object { $_ }) + if ($versionLines.Count -eq 0 -or $versionLines[-1] -ne $expectedVersion) { + return $true + } + + $workloadOutput = $workloadList -join [Environment]::NewLine + foreach ($workload in $requiredWorkloads) { + if ($workloadOutput -notmatch "(?m)^$([regex]::Escape($workload))\s+") { + return $true + } + } + + return $false +} + +function Restore-DotnetWorkloads { + $dotnetPath = (Get-Command dotnet -ErrorAction Stop).Source + $dotnetRoot = Split-Path -Parent $dotnetPath + + if (-not (Test-WorkloadRestoreNeeded -DotnetPath $dotnetPath)) { + Write-Host "Required .NET workloads already installed." -ForegroundColor Green + return + } + + $requiresElevation = -not $IsWindows -and -not (Test-DirectoryWritable -Path $dotnetRoot) + + if ($requiresElevation) { + if (-not (Test-Command "sudo")) { + throw "The .NET installation at '$dotnetRoot' requires elevated permissions, but sudo is unavailable." + } + + Write-Host "Restoring .NET workloads with elevated privileges..." -ForegroundColor Cyan + & sudo -v + } + else { + Write-Host "Restoring .NET workloads..." -ForegroundColor Cyan + } + + # Keep routine restore output out of bootstrap; retain its final error when it fails. + $previousEAP = $PSNativeCommandUseErrorActionPreference + $PSNativeCommandUseErrorActionPreference = $false + $restoreOutput = @() + $restoreExitCode = 0 + try { + if ($requiresElevation) { + $restoreOutput = @(& sudo $dotnetPath workload restore 2>&1) + } + else { + $restoreOutput = @(& $dotnetPath workload restore 2>&1) + } + $restoreExitCode = $LASTEXITCODE + } + finally { + $PSNativeCommandUseErrorActionPreference = $previousEAP + } + + if ($restoreExitCode -ne 0) { + $failureLines = @($restoreOutput | ForEach-Object { "$($_)".Trim() } | Where-Object { $_ }) + $failureDetail = "dotnet workload restore exited with code $restoreExitCode." + if ($failureLines.Count -gt 0) { + $failureDetail = $failureLines[-1] + } + + Write-Host "Workload restore failed: $failureDetail" -ForegroundColor Red + throw $failureDetail + } + + Write-Host "Workloads restored." -ForegroundColor Green +} + +function Get-StatusColor { + param([Parameter(Mandatory)][string] $Status) + + switch ($Status) { + "PASS" { return "Green" } + "WARN" { return "Yellow" } + "FAIL" { return "Red" } + "SKIP" { return "DarkGray" } + } +} + Write-Host "Bootstrapping Sentry Unity from '$repoRoot'" if (-not (Test-Path (Join-Path $repoRoot ".git"))) { @@ -71,12 +193,14 @@ else { $hasDotnet = Test-Command "dotnet" if ($hasDotnet) { Add-Result -Status "PASS" -Name "dotnet" -Detail "Found dotnet." - Invoke-Stage -Name "Workloads" -Action { dotnet workload restore } -FailureStatus "WARN" -Fix "Run: dotnet workload restore. On macOS or Linux, elevate it if dotnet reports inadequate permissions." + Invoke-Stage -Name "Workloads" -Action { Restore-DotnetWorkloads } -FailureStatus "WARN" -Fix "Check the workload restore output, then rerun: pwsh bootstrap.ps1" } else { Add-Result -Status "FAIL" -Name "dotnet" -Detail "dotnet is not available on PATH." -Fix "Install the SDK pinned in global.json, then rerun: pwsh bootstrap.ps1" } +Write-Host "" + $hasGh = Test-Command "gh" if ($hasGh) { Add-Result -Status "PASS" -Name "GitHub CLI" -Detail "Found gh." @@ -100,6 +224,8 @@ else { Add-Result -Status "WARN" -Name "Prebuilt native SDKs" -Detail "GitHub CLI is not available." -Fix "Install gh and run 'gh auth login', then rerun: pwsh bootstrap.ps1" } +Write-Host "" + if (Test-Path (Join-Path $repoRoot "modules/sentry-cli.properties")) { Invoke-Stage -Name "Sentry CLI" -Action { & (Join-Path $repoRoot "scripts/download-sentry-cli.ps1") } -FailureStatus "WARN" -Fix "Check network access, then rerun: pwsh bootstrap.ps1" } @@ -107,13 +233,19 @@ else { Add-Result -Status "WARN" -Name "Sentry CLI" -Detail "sentry-cli submodule files are unavailable." -Fix "Run: git submodule update --init --recursive, then rerun: pwsh bootstrap.ps1" } +Write-Host "" + if ($hasDotnet) { + Write-Host "Building managed SDK..." -ForegroundColor Cyan Invoke-Stage -Name "Managed SDK build" -Action { dotnet build } -Fix "Fix the reported build error, then rerun: dotnet build" } else { Add-Result -Status "FAIL" -Name "Managed SDK build" -Detail "dotnet is unavailable." -Fix "Install the SDK pinned in global.json, then run: dotnet build" } +Write-Host "" +Write-Host "Configuring sample projects..." -ForegroundColor Cyan + $appleSettings = @( (Join-Path $repoRoot "samples/unity-of-bugs/ProjectSettings/ProjectSettings.asset"), (Join-Path $repoRoot "samples/unity-of-bugs-local/ProjectSettings/ProjectSettings.asset") @@ -149,9 +281,11 @@ else { } Write-Host "" -Write-Host "Bootstrap summary" +Write-Host "$($PSStyle.Bold)Bootstrap summary$($PSStyle.Reset)" foreach ($result in $results) { - Write-Host "[$($result.Status)] $($result.Name): $($result.Detail)" + Write-Host "[" -NoNewline + Write-Host $result.Status -ForegroundColor (Get-StatusColor -Status $result.Status) -NoNewline + Write-Host "] $($result.Name): $($result.Detail)" if ($result.Fix) { Write-Host " $($result.Fix)" } diff --git a/scripts/download-native-sdks.ps1 b/scripts/download-native-sdks.ps1 index 1b2333d5e..e58bb7fc1 100644 --- a/scripts/download-native-sdks.ps1 +++ b/scripts/download-native-sdks.ps1 @@ -156,7 +156,6 @@ function Test-TrackedPluginChanges { # Main logic Write-Host "Checking native SDK status..." -ForegroundColor Cyan -Write-Host "" $sdksToDownload = @() @@ -170,8 +169,6 @@ foreach ($sdk in $SDKs) { } } -Write-Host "" - if ($sdksToDownload.Count -eq 0) { Write-Host "All native SDKs are already present." -ForegroundColor Green exit 0 diff --git a/scripts/download-sentry-cli.ps1 b/scripts/download-sentry-cli.ps1 index 9e0dbcfb6..a7eb65017 100644 --- a/scripts/download-sentry-cli.ps1 +++ b/scripts/download-sentry-cli.ps1 @@ -10,12 +10,18 @@ $targetFiles = $platforms | ForEach-Object { Join-Path $targetDir "sentry-cli-$name" } +Write-Host "Checking Sentry CLI status..." -ForegroundColor Cyan + +$missingTargetFiles = @($targetFiles | Where-Object { -not (Test-Path $_) }) +if ($missingTargetFiles.Count -eq 0) { + Write-Host "Sentry CLI already present, skipping download." -ForegroundColor Green + return +} + +Write-Host "Sentry CLI not found, will download." -ForegroundColor Yellow +Write-Host "" + if (Test-Path $targetDir) { - $missingTargetFiles = @($targetFiles | Where-Object { -not (Test-Path $_) }) - if ($missingTargetFiles.Count -eq 0) { - Write-Host "Sentry CLI already downloaded at $targetDir" - return - } Remove-Item -Recurse -Force $targetDir } @@ -30,11 +36,16 @@ foreach ($name in $platforms) } $targetFile = "$targetDir/sentry-cli-$name" - Write-Host "Downloading $targetFile" + Write-Host "Downloading Sentry CLI for $name..." -ForegroundColor Yellow Invoke-WebRequest -Uri "$baseUrl$name" -OutFile $targetFile if (Get-Command 'chmod' -ErrorAction SilentlyContinue) { chmod +x $targetFile } + + Write-Host " Downloaded $name" -ForegroundColor Green } + +Write-Host "" +Write-Host "Sentry CLI download completed successfully!" -ForegroundColor Green From ef8cb9f9517d9bf1361442707e6e2863c1bfcd50 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 31 Jul 2026 13:00:19 +0200 Subject: [PATCH 7/7] make it compile --- src/Sentry.Unity/Metrics/FrameTimeMonitor.cs | 39 +++++++++++++------ .../SentryUnityVersion.cs | 4 +- .../SentryUnityVersionTests.cs | 1 + 3 files changed, 29 insertions(+), 15 deletions(-) rename src/{Sentry.Unity.Editor => Sentry.Unity}/SentryUnityVersion.cs (73%) diff --git a/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs b/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs index 2c0bc56dd..0357f9421 100644 --- a/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs +++ b/src/Sentry.Unity/Metrics/FrameTimeMonitor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using Sentry.Extensibility; using UnityEngine; @@ -21,10 +22,17 @@ internal class FrameTimeMonitor private readonly int _sampleInterval; private readonly GameMetricAttributes _attributes; private readonly IDiagnosticLogger? _logger; + private readonly bool _supportsThreadTimings; // Reused between samples to avoid a per-sample allocation. private readonly FrameTiming[] _timings = new FrameTiming[1]; + // These fields are available starting with Unity 2022.3. + private static readonly FieldInfo? CpuMainThreadFrameTime = + typeof(FrameTiming).GetField("cpuMainThreadFrameTime"); + private static readonly FieldInfo? CpuRenderThreadFrameTime = + typeof(FrameTiming).GetField("cpuRenderThreadFrameTime"); + private int _frameCount; public FrameTimeMonitor(int sampleInterval, GameMetricAttributes attributes, IDiagnosticLogger? logger) @@ -32,6 +40,7 @@ public FrameTimeMonitor(int sampleInterval, GameMetricAttributes attributes, IDi _sampleInterval = Math.Max(1, sampleInterval); _attributes = attributes; _logger = logger; + _supportsThreadTimings = SentryUnityVersion.IsNewerOrEqualThan("2022.3"); } /// @@ -62,13 +71,15 @@ public void OnFrame() } } - // CPU main-/render-thread times from the FrameTimingManager. These are already tracked by the - // engine (no GPU timer queries are involved), so reading them adds no measurable overhead. GPU - // timing is intentionally not read. The data is only available when 'Frame Timing Stats' is - // enabled (implicit in development builds), so these metrics are emitted best-effort and are - // silently skipped when unavailable. + // The per-thread fields are available in newer Unity versions only. Resolve them dynamically so + // this precompiled assembly remains usable with Unity 2021. private void EmitThreadTimings(IReadOnlyList> attributes) { + if (!_supportsThreadTimings) + { + return; + } + FrameTimingManager.CaptureFrameTimings(); if (FrameTimingManager.GetLatestTimings(1, _timings) == 0) { @@ -77,16 +88,20 @@ private void EmitThreadTimings(IReadOnlyList> attri var timing = _timings[0]; - if (timing.cpuMainThreadFrameTime > 0.0) - { - SentrySdk.Metrics.EmitDistribution( - GameThreadMetric, timing.cpuMainThreadFrameTime, MeasurementUnit.Duration.Millisecond, attributes); - } + EmitThreadTiming(GameThreadMetric, CpuMainThreadFrameTime, timing, attributes); + EmitThreadTiming(RenderThreadMetric, CpuRenderThreadFrameTime, timing, attributes); + } - if (timing.cpuRenderThreadFrameTime > 0.0) + private static void EmitThreadTiming( + string metric, + FieldInfo? timingField, + FrameTiming timing, + IReadOnlyList> attributes) + { + if (timingField?.GetValue(timing) is double duration && duration > 0.0) { SentrySdk.Metrics.EmitDistribution( - RenderThreadMetric, timing.cpuRenderThreadFrameTime, MeasurementUnit.Duration.Millisecond, attributes); + metric, duration, MeasurementUnit.Duration.Millisecond, attributes); } } } diff --git a/src/Sentry.Unity.Editor/SentryUnityVersion.cs b/src/Sentry.Unity/SentryUnityVersion.cs similarity index 73% rename from src/Sentry.Unity.Editor/SentryUnityVersion.cs rename to src/Sentry.Unity/SentryUnityVersion.cs index 896abb414..c20eed4f0 100644 --- a/src/Sentry.Unity.Editor/SentryUnityVersion.cs +++ b/src/Sentry.Unity/SentryUnityVersion.cs @@ -2,7 +2,7 @@ using System.Text.RegularExpressions; using Sentry.Unity.Integrations; -namespace Sentry.Unity.Editor; +namespace Sentry.Unity; internal static class SentryUnityVersion { @@ -12,8 +12,6 @@ public static bool IsNewerOrEqualThan(string version, IApplication? application internal static Version? GetVersion(IApplication? application = null) { application ??= ApplicationAdapter.Instance; - // The Unity version format looks like this: '2019.4.38f1', '2022.1.0a17' or '2022.1.1b4', - // but Version() expects only the numerical parts, e.g. `2021.1.0` var unityVersion = Regex.Replace(application.UnityVersion, "^([0-9]+\\.[0-9]+\\.[0-9]+)[a-z].*$", "$1"); return new Version(unityVersion); } diff --git a/test/Sentry.Unity.Editor.Tests/SentryUnityVersionTests.cs b/test/Sentry.Unity.Editor.Tests/SentryUnityVersionTests.cs index bafe12726..f215f19ae 100644 --- a/test/Sentry.Unity.Editor.Tests/SentryUnityVersionTests.cs +++ b/test/Sentry.Unity.Editor.Tests/SentryUnityVersionTests.cs @@ -1,5 +1,6 @@ using System; using NUnit.Framework; +using Sentry.Unity; using Sentry.Unity.Tests.Stubs; namespace Sentry.Unity.Editor.Tests;