From 6be1e0124c2c1349f4cc70bf46d1767c3d2b8c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:37:49 +0100 Subject: [PATCH 01/26] feat(metrics): Trace-connected Metrics (Implementation) --- ...chmarks.cs => BatchProcessorBenchmarks.cs} | 12 +- ....BatchProcessorBenchmarks-report-github.md | 24 ++ ...gBatchProcessorBenchmarks-report-github.md | 24 -- src/Sentry/BindableSentryOptions.cs | 17 +- src/Sentry/Extensibility/DisabledHub.cs | 7 +- src/Sentry/Extensibility/HubAdapter.cs | 7 +- src/Sentry/IHub.cs | 13 + ...cturedLogBatchBuffer.cs => BatchBuffer.cs} | 48 +-- ...LogBatchProcessor.cs => BatchProcessor.cs} | 79 ++-- .../Internal/DefaultSentryStructuredLogger.cs | 5 +- .../Internal/DefaultSentryTraceMetrics.cs | 76 ++++ .../Internal/DisabledSentryTraceMetrics.cs | 34 ++ src/Sentry/Internal/Hub.cs | 7 +- src/Sentry/Protocol/Envelopes/Envelope.cs | 12 + src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 13 + src/Sentry/Protocol/StructuredLog.cs | 6 + src/Sentry/Protocol/TraceMetric.cs | 43 +++ src/Sentry/SentryMetric.Factory.cs | 48 +++ src/Sentry/SentryMetric.Interface.cs | 12 + src/Sentry/SentryMetric.cs | 357 ++++++++++++++++++ src/Sentry/SentryMetricType.cs | 54 +++ src/Sentry/SentryOptions.Callback.cs | 109 ++++++ src/Sentry/SentryOptions.cs | 49 ++- src/Sentry/SentrySdk.cs | 26 ++ src/Sentry/SentryTraceMetrics.Public.cs | 124 ++++++ src/Sentry/SentryTraceMetrics.cs | 63 ++++ test/Sentry.Testing/BindableTests.cs | 1 + .../InMemorySentryTraceMetrics.cs | 114 ++++++ ...iApprovalTests.Run.DotNet10_0.verified.txt | 68 ++++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 68 ++++ ...piApprovalTests.Run.DotNet9_0.verified.txt | 68 ++++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 59 +++ ...atchBufferTests.cs => BatchBufferTests.cs} | 43 ++- ...ocessorTests.cs => BatchProcessorTests.cs} | 13 +- .../SentryStructuredLoggerTests.cs | 4 +- 35 files changed, 1585 insertions(+), 122 deletions(-) rename benchmarks/Sentry.Benchmarks/{StructuredLogBatchProcessorBenchmarks.cs => BatchProcessorBenchmarks.cs} (71%) create mode 100644 benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md delete mode 100644 benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md rename src/Sentry/Internal/{StructuredLogBatchBuffer.cs => BatchBuffer.cs} (83%) rename src/Sentry/Internal/{StructuredLogBatchProcessor.cs => BatchProcessor.cs} (54%) create mode 100644 src/Sentry/Internal/DefaultSentryTraceMetrics.cs create mode 100644 src/Sentry/Internal/DisabledSentryTraceMetrics.cs create mode 100644 src/Sentry/Protocol/TraceMetric.cs create mode 100644 src/Sentry/SentryMetric.Factory.cs create mode 100644 src/Sentry/SentryMetric.Interface.cs create mode 100644 src/Sentry/SentryMetric.cs create mode 100644 src/Sentry/SentryMetricType.cs create mode 100644 src/Sentry/SentryOptions.Callback.cs create mode 100644 src/Sentry/SentryTraceMetrics.Public.cs create mode 100644 src/Sentry/SentryTraceMetrics.cs create mode 100644 test/Sentry.Testing/InMemorySentryTraceMetrics.cs rename test/Sentry.Tests/Internals/{StructuredLogBatchBufferTests.cs => BatchBufferTests.cs} (82%) rename test/Sentry.Tests/Internals/{StructuredLogBatchProcessorTests.cs => BatchProcessorTests.cs} (91%) diff --git a/benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs b/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs similarity index 71% rename from benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs rename to benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs index 4a2d1fc981..37e1fcc411 100644 --- a/benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs +++ b/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs @@ -1,13 +1,19 @@ using BenchmarkDotNet.Attributes; using Sentry.Extensibility; using Sentry.Internal; +using Sentry.Protocol; namespace Sentry.Benchmarks; -public class StructuredLogBatchProcessorBenchmarks +/// +/// (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only. +/// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both. +/// For comparability of results, we still benchmark with , rather than . +/// +public class BatchProcessorBenchmarks { private Hub _hub; - private StructuredLogBatchProcessor _batchProcessor; + private BatchProcessor _batchProcessor; private SentryLog _log; [Params(10, 100)] @@ -29,7 +35,7 @@ public void Setup() var clientReportRecorder = new NullClientReportRecorder(); _hub = new Hub(options, DisabledHub.Instance); - _batchProcessor = new StructuredLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, null); + _batchProcessor = new BatchProcessor(_hub, BatchCount, batchInterval, StructuredLog.Capture, clientReportRecorder, null); _log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message"); } diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md new file mode 100644 index 0000000000..d298c9a815 --- /dev/null +++ b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md @@ -0,0 +1,24 @@ +``` + +BenchmarkDotNet v0.13.12, macOS 26.1 (25B78) [Darwin 25.1.0] +Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores +.NET SDK 10.0.100 + [Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD + DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD + + +``` +| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Median | Gen0 | Allocated | +|------------------------- |----------- |-------------------- |-------------:|------------:|-------------:|-------------:|-------:|----------:| +| **EnqueueAndFlush** | **10** | **100** | **1,896.9 ns** | **9.94 ns** | **8.81 ns** | **1,894.2 ns** | **0.6104** | **5 KB** | +| EnqueueAndFlush_Parallel | 10 | 100 | 16,520.9 ns | 327.78 ns | 746.51 ns | 16,350.4 ns | 1.1292 | 9.29 KB | +| **EnqueueAndFlush** | **10** | **200** | **4,085.5 ns** | **80.03 ns** | **74.86 ns** | **4,087.1 ns** | **1.2207** | **10 KB** | +| EnqueueAndFlush_Parallel | 10 | 200 | 39,371.8 ns | 776.85 ns | 1,360.59 ns | 38,725.0 ns | 1.6479 | 13.6 KB | +| **EnqueueAndFlush** | **10** | **1000** | **18,829.3 ns** | **182.18 ns** | **142.24 ns** | **18,836.4 ns** | **6.1035** | **50 KB** | +| EnqueueAndFlush_Parallel | 10 | 1000 | 151,934.1 ns | 2,631.83 ns | 3,232.12 ns | 151,495.9 ns | 3.6621 | 31.31 KB | +| **EnqueueAndFlush** | **100** | **100** | **864.9 ns** | **2.16 ns** | **1.68 ns** | **865.0 ns** | **0.1469** | **1.2 KB** | +| EnqueueAndFlush_Parallel | 100 | 100 | 7,414.9 ns | 74.86 ns | 70.02 ns | 7,405.9 ns | 0.5722 | 4.61 KB | +| **EnqueueAndFlush** | **100** | **200** | **1,836.9 ns** | **15.28 ns** | **12.76 ns** | **1,834.9 ns** | **0.2937** | **2.41 KB** | +| EnqueueAndFlush_Parallel | 100 | 200 | 37,119.5 ns | 726.04 ns | 1,252.39 ns | 36,968.9 ns | 0.8545 | 7.27 KB | +| **EnqueueAndFlush** | **100** | **1000** | **8,567.2 ns** | **84.25 ns** | **74.68 ns** | **8,547.4 ns** | **1.4648** | **12.03 KB** | +| EnqueueAndFlush_Parallel | 100 | 1000 | 255,284.5 ns | 5,095.08 ns | 12,593.77 ns | 258,313.9 ns | 1.9531 | 19.02 KB | diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md deleted file mode 100644 index 8461170b2c..0000000000 --- a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md +++ /dev/null @@ -1,24 +0,0 @@ -``` - -BenchmarkDotNet v0.13.12, macOS 15.6 (24G84) [Darwin 24.6.0] -Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores -.NET SDK 9.0.301 - [Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD - DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD - - -``` -| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Gen0 | Allocated | -|------------------------- |----------- |-------------------- |-------------:|------------:|------------:|-------:|----------:| -| **EnqueueAndFlush** | **10** | **100** | **1,793.4 ns** | **13.75 ns** | **12.86 ns** | **0.6104** | **5 KB** | -| EnqueueAndFlush_Parallel | 10 | 100 | 18,550.8 ns | 368.24 ns | 889.34 ns | 1.1292 | 9.16 KB | -| **EnqueueAndFlush** | **10** | **200** | **3,679.8 ns** | **18.65 ns** | **16.53 ns** | **1.2207** | **10 KB** | -| EnqueueAndFlush_Parallel | 10 | 200 | 41,246.4 ns | 508.07 ns | 475.25 ns | 1.7090 | 14.04 KB | -| **EnqueueAndFlush** | **10** | **1000** | **17,239.1 ns** | **62.50 ns** | **58.46 ns** | **6.1035** | **50 KB** | -| EnqueueAndFlush_Parallel | 10 | 1000 | 192,059.3 ns | 956.92 ns | 895.11 ns | 4.3945 | 37.52 KB | -| **EnqueueAndFlush** | **100** | **100** | **866.7 ns** | **1.99 ns** | **1.77 ns** | **0.1469** | **1.2 KB** | -| EnqueueAndFlush_Parallel | 100 | 100 | 6,714.8 ns | 100.75 ns | 94.24 ns | 0.5569 | 4.52 KB | -| **EnqueueAndFlush** | **100** | **200** | **1,714.5 ns** | **3.20 ns** | **3.00 ns** | **0.2937** | **2.41 KB** | -| EnqueueAndFlush_Parallel | 100 | 200 | 43,842.8 ns | 860.74 ns | 1,718.99 ns | 0.9155 | 7.51 KB | -| **EnqueueAndFlush** | **100** | **1000** | **8,537.8 ns** | **9.80 ns** | **9.17 ns** | **1.4648** | **12.03 KB** | -| EnqueueAndFlush_Parallel | 100 | 1000 | 313,421.4 ns | 6,159.27 ns | 6,846.01 ns | 1.9531 | 18.37 KB | diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs index bbadddaf33..52abdf7df9 100644 --- a/src/Sentry/BindableSentryOptions.cs +++ b/src/Sentry/BindableSentryOptions.cs @@ -1,7 +1,7 @@ namespace Sentry; /// -/// Contains representations of the subset of properties in SentryOptions that can be set from ConfigurationBindings. +/// Contains representations of the subset of properties in that can be set from ConfigurationBindings. /// Note that all of these properties are nullable, so that if they are not present in configuration, the values from /// the type being bound to will be preserved. /// @@ -56,6 +56,8 @@ internal partial class BindableSentryOptions public bool? EnableSpotlight { get; set; } public string? SpotlightUrl { get; set; } + public ExperimentalSentryOptions? Experimental { get; set; } + public void ApplyTo(SentryOptions options) { options.IsGlobalModeEnabled = IsGlobalModeEnabled ?? options.IsGlobalModeEnabled; @@ -106,6 +108,11 @@ public void ApplyTo(SentryOptions options) options.EnableSpotlight = EnableSpotlight ?? options.EnableSpotlight; options.SpotlightUrl = SpotlightUrl ?? options.SpotlightUrl; + if (Experimental is { } experimental) + { + options.Experimental.EnableMetrics = experimental.EnableMetrics ?? options.Experimental.EnableMetrics; + } + #if ANDROID Android.ApplyTo(options.Android); Native.ApplyTo(options.Native); @@ -113,4 +120,12 @@ public void ApplyTo(SentryOptions options) Native.ApplyTo(options.Native); #endif } + + /// + /// Bindable Options for . + /// + internal class ExperimentalSentryOptions + { + public bool? EnableMetrics { get; set; } + } } diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index e8115c03f9..273fba0783 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -1,6 +1,5 @@ using Sentry.Internal; using Sentry.Protocol.Envelopes; -using Sentry.Protocol.Metrics; namespace Sentry.Extensibility; @@ -267,4 +266,10 @@ public void Dispose() /// Disabled Logger. /// public SentryStructuredLogger Logger => DisabledSentryStructuredLogger.Instance; + + /// + /// Disabled Metrics. + /// + [Experimental("SENTRYTRACECONNECTEDMETRICS")] + public SentryTraceMetrics Metrics => DisabledSentryTraceMetrics.Instance; } diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 45499369e6..f02b50b589 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -1,6 +1,5 @@ using Sentry.Infrastructure; using Sentry.Protocol.Envelopes; -using Sentry.Protocol.Metrics; namespace Sentry.Extensibility; @@ -37,6 +36,12 @@ private HubAdapter() { } /// public SentryStructuredLogger Logger { [DebuggerStepThrough] get => SentrySdk.Logger; } + /// + /// Forwards the call to . + /// + [Experimental("SENTRYTRACECONNECTEDMETRICS")] + public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => SentrySdk.Experimental.Metrics; } + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 076f454f3f..ee44057413 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -29,6 +29,19 @@ public interface IHub : ISentryClient, ISentryScopeManager /// public SentryStructuredLogger Logger { get; } + /// + /// Generates and sends metrics to Sentry. + /// + /// + /// Available options: + /// + /// + /// + /// + /// + [Experimental("SENTRYTRACECONNECTEDMETRICS")] + public SentryTraceMetrics Metrics { get; } + /// /// Starts a transaction. /// diff --git a/src/Sentry/Internal/StructuredLogBatchBuffer.cs b/src/Sentry/Internal/BatchBuffer.cs similarity index 83% rename from src/Sentry/Internal/StructuredLogBatchBuffer.cs rename to src/Sentry/Internal/BatchBuffer.cs index a9f49216b7..78ae1ab063 100644 --- a/src/Sentry/Internal/StructuredLogBatchBuffer.cs +++ b/src/Sentry/Internal/BatchBuffer.cs @@ -3,7 +3,7 @@ namespace Sentry.Internal; /// -/// A wrapper over an , intended for reusable buffering. +/// A wrapper over an , intended for reusable buffering for . /// /// /// Must be attempted to flush via when either the is reached, @@ -12,15 +12,15 @@ namespace Sentry.Internal; /// allowing multiple threads for or exclusive access for . /// [DebuggerDisplay("Name = {Name}, Capacity = {Capacity}, Additions = {_additions}, AddCount = {AddCount}, IsDisposed = {_disposed}")] -internal sealed class StructuredLogBatchBuffer : IDisposable +internal sealed class BatchBuffer : IDisposable { - private readonly SentryLog[] _array; + private readonly TItem[] _array; private int _additions; private readonly ScopedCountdownLock _addLock; private readonly Timer _timer; private readonly TimeSpan _timeout; - private readonly Action _timeoutExceededAction; + private readonly Action> _timeoutExceededAction; private volatile bool _disposed; @@ -31,12 +31,12 @@ internal sealed class StructuredLogBatchBuffer : IDisposable /// When the timeout exceeds after an item has been added and the not yet been exceeded, is invoked. /// The operation to execute when the exceeds if the buffer is neither empty nor full. /// Name of the new buffer. - public StructuredLogBatchBuffer(int capacity, TimeSpan timeout, Action timeoutExceededAction, string? name = null) + public BatchBuffer(int capacity, TimeSpan timeout, Action> timeoutExceededAction, string? name = null) { ThrowIfLessThanTwo(capacity, nameof(capacity)); ThrowIfNegativeOrZero(timeout, nameof(timeout)); - _array = new SentryLog[capacity]; + _array = new TItem[capacity]; _additions = 0; _addLock = new ScopedCountdownLock(); @@ -72,18 +72,18 @@ public StructuredLogBatchBuffer(int capacity, TimeSpan timeout, Action /// Element attempted to be added. - /// An describing the result of the thread-safe operation. - internal StructuredLogBatchBufferAddStatus Add(SentryLog item) + /// An describing the result of the thread-safe operation. + internal BatchBufferAddStatus Add(TItem item) { if (_disposed) { - return StructuredLogBatchBufferAddStatus.IgnoredIsDisposed; + return BatchBufferAddStatus.IgnoredIsDisposed; } using var scope = _addLock.TryEnterCounterScope(); if (!scope.IsEntered) { - return StructuredLogBatchBufferAddStatus.IgnoredIsFlushing; + return BatchBufferAddStatus.IgnoredIsFlushing; } var count = Interlocked.Increment(ref _additions); @@ -92,24 +92,24 @@ internal StructuredLogBatchBufferAddStatus Add(SentryLog item) { EnableTimer(); _array[count - 1] = item; - return StructuredLogBatchBufferAddStatus.AddedFirst; + return BatchBufferAddStatus.AddedFirst; } if (count < _array.Length) { _array[count - 1] = item; - return StructuredLogBatchBufferAddStatus.Added; + return BatchBufferAddStatus.Added; } if (count == _array.Length) { DisableTimer(); _array[count - 1] = item; - return StructuredLogBatchBufferAddStatus.AddedLast; + return BatchBufferAddStatus.AddedLast; } Debug.Assert(count > _array.Length); - return StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded; + return BatchBufferAddStatus.IgnoredCapacityExceeded; } /// @@ -157,7 +157,7 @@ internal void OnIntervalElapsed(object? state) /// Returns a new Array consisting of the elements successfully added. /// /// An Array with Length of successful additions. - private SentryLog[] ToArrayAndClear() + private TItem[] ToArrayAndClear() { var additions = _additions; var length = _array.Length; @@ -173,7 +173,7 @@ private SentryLog[] ToArrayAndClear() /// /// The Length of the buffer a new Array is created from. /// An Array with Length of . - private SentryLog[] ToArrayAndClear(int length) + private TItem[] ToArrayAndClear(int length) { Debug.Assert(_addLock.IsSet); @@ -182,14 +182,14 @@ private SentryLog[] ToArrayAndClear(int length) return array; } - private SentryLog[] ToArray(int length) + private TItem[] ToArray(int length) { if (length == 0) { - return Array.Empty(); + return Array.Empty(); } - var array = new SentryLog[length]; + var array = new TItem[length]; Array.Copy(_array, array, length); return array; } @@ -253,17 +253,17 @@ static void ThrowNegativeOrZero(TimeSpan value, string paramName) /// A scope than ensures only a single operation is in progress, /// and blocks the calling thread until all operations have finished. /// When is , no more can be started, - /// which will then return immediately. + /// which will then return immediately. /// /// /// Only when scope . /// internal ref struct FlushScope : IDisposable { - private StructuredLogBatchBuffer? _lockObj; + private BatchBuffer? _lockObj; private ScopedCountdownLock.LockScope _scope; - internal FlushScope(StructuredLogBatchBuffer lockObj, ScopedCountdownLock.LockScope scope) + internal FlushScope(BatchBuffer lockObj, ScopedCountdownLock.LockScope scope) { Debug.Assert(scope.IsEntered); _lockObj = lockObj; @@ -272,7 +272,7 @@ internal FlushScope(StructuredLogBatchBuffer lockObj, ScopedCountdownLock.LockSc internal bool IsEntered => _scope.IsEntered; - internal SentryLog[] Flush() + internal TItem[] Flush() { var lockObj = _lockObj; if (lockObj is not null) @@ -300,7 +300,7 @@ public void Dispose() } } -internal enum StructuredLogBatchBufferAddStatus : byte +internal enum BatchBufferAddStatus : byte { AddedFirst, Added, diff --git a/src/Sentry/Internal/StructuredLogBatchProcessor.cs b/src/Sentry/Internal/BatchProcessor.cs similarity index 54% rename from src/Sentry/Internal/StructuredLogBatchProcessor.cs rename to src/Sentry/Internal/BatchProcessor.cs index 2fe5db924e..f71f2498e0 100644 --- a/src/Sentry/Internal/StructuredLogBatchProcessor.cs +++ b/src/Sentry/Internal/BatchProcessor.cs @@ -1,58 +1,59 @@ using Sentry.Extensibility; -using Sentry.Protocol; -using Sentry.Protocol.Envelopes; namespace Sentry.Internal; /// -/// The Batch Processor for Sentry Logs. +/// The Sentry Batch Processor. /// /// /// Uses a double buffer strategy to achieve synchronous and lock-free adding. /// Switches the active buffer either when full or timeout exceeded (after first item added). -/// Logs are dropped when both buffers are either full or being flushed. -/// Logs are not enqueued when the Hub is disabled (Hub is being or has been disposed). +/// Items are dropped when both buffers are either full or being flushed. +/// Items are not enqueued when the Hub is disabled (Hub is being or has been disposed). /// Flushing blocks the calling thread until all pending add operations have completed. /// /// Implementation: -/// - When Hub is disabled (i.e. disposed), does not enqueue log -/// - Try to enqueue log into currently active buffer -/// - when currently active buffer is full, try to enqueue log into the other buffer -/// - when the other buffer is also full, or currently being flushed, then the log is dropped and a discarded event is recorded as a client report +/// - When Hub is disabled (i.e. disposed), does not enqueue item +/// - Try to enqueue item into currently active buffer +/// - when currently active buffer is full, try to enqueue item into the other buffer +/// - when the other buffer is also full, or currently being flushed, then the item is dropped and a discarded event is recorded as a client report /// - Swap currently active buffer when /// - buffer is full /// - timeout has exceeded -/// - Batch and Capture logs after swapping currently active buffer +/// - Batch and Capture items after swapping currently active buffer /// - wait until all pending add/enqueue operations have completed (required for timeout) -/// - flush the buffer and capture an envelope containing the batched logs -/// - After flush, logs can be enqueued again into the buffer +/// - flush the buffer and capture an envelope containing the batched items +/// - After flush, items can be enqueued again into the buffer /// /// -/// Sentry Logs /// Sentry Batch Processor /// OpenTelemetry Batch Processor -internal sealed class StructuredLogBatchProcessor : IDisposable +/// Sentry Logs +/// Sentry Metrics +internal sealed class BatchProcessor : IDisposable { private readonly IHub _hub; + private readonly Action _sendAction; private readonly IClientReportRecorder _clientReportRecorder; private readonly IDiagnosticLogger? _diagnosticLogger; - private readonly StructuredLogBatchBuffer _buffer1; - private readonly StructuredLogBatchBuffer _buffer2; - private volatile StructuredLogBatchBuffer _activeBuffer; + private readonly BatchBuffer _buffer1; + private readonly BatchBuffer _buffer2; + private volatile BatchBuffer _activeBuffer; - public StructuredLogBatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger) + public BatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, Action sendAction, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger) { _hub = hub; + _sendAction = sendAction; _clientReportRecorder = clientReportRecorder; _diagnosticLogger = diagnosticLogger; - _buffer1 = new StructuredLogBatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 1"); - _buffer2 = new StructuredLogBatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 2"); + _buffer1 = new BatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 1"); + _buffer2 = new BatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 2"); _activeBuffer = _buffer1; } - internal void Enqueue(SentryLog log) + internal void Enqueue(TItem item) { if (!_hub.IsEnabled) { @@ -61,21 +62,21 @@ internal void Enqueue(SentryLog log) var activeBuffer = _activeBuffer; - if (!TryEnqueue(activeBuffer, log)) + if (!TryEnqueue(activeBuffer, item)) { activeBuffer = ReferenceEquals(activeBuffer, _buffer1) ? _buffer2 : _buffer1; - if (!TryEnqueue(activeBuffer, log)) + if (!TryEnqueue(activeBuffer, item)) { _clientReportRecorder.RecordDiscardedEvent(DiscardReason.Backpressure, DataCategory.Default, 1); - _diagnosticLogger?.LogInfo("Log Buffer full ... dropping log"); + _diagnosticLogger?.LogInfo("{0}-Buffer full ... dropping {0}", typeof(TItem).Name); } } } internal void Flush() { - CaptureLogs(_buffer1); - CaptureLogs(_buffer2); + CaptureItems(_buffer1); + CaptureItems(_buffer2); } /// @@ -90,50 +91,50 @@ internal void OnIntervalElapsed() activeBuffer.OnIntervalElapsed(activeBuffer); } - private bool TryEnqueue(StructuredLogBatchBuffer buffer, SentryLog log) + private bool TryEnqueue(BatchBuffer buffer, TItem item) { - var status = buffer.Add(log); + var status = buffer.Add(item); - if (status is StructuredLogBatchBufferAddStatus.AddedLast) + if (status is BatchBufferAddStatus.AddedLast) { SwapActiveBuffer(buffer); - CaptureLogs(buffer); + CaptureItems(buffer); return true; } - return status is StructuredLogBatchBufferAddStatus.AddedFirst or StructuredLogBatchBufferAddStatus.Added; + return status is BatchBufferAddStatus.AddedFirst or BatchBufferAddStatus.Added; } - private void SwapActiveBuffer(StructuredLogBatchBuffer currentActiveBuffer) + private void SwapActiveBuffer(BatchBuffer currentActiveBuffer) { var newActiveBuffer = ReferenceEquals(currentActiveBuffer, _buffer1) ? _buffer2 : _buffer1; _ = Interlocked.CompareExchange(ref _activeBuffer, newActiveBuffer, currentActiveBuffer); } - private void CaptureLogs(StructuredLogBatchBuffer buffer) + private void CaptureItems(BatchBuffer buffer) { - SentryLog[]? logs = null; + TItem[]? items = null; using (var scope = buffer.TryEnterFlushScope()) { if (scope.IsEntered) { - logs = scope.Flush(); + items = scope.Flush(); } } - if (logs is not null && logs.Length != 0) + if (items is not null && items.Length != 0) { - _ = _hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(logs))); + _sendAction(_hub, items); } } - private void OnTimeoutExceeded(StructuredLogBatchBuffer buffer) + private void OnTimeoutExceeded(BatchBuffer buffer) { if (!buffer.IsEmpty) { SwapActiveBuffer(buffer); - CaptureLogs(buffer); + CaptureItems(buffer); } } diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 3e42cc7005..9d4883f716 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -1,5 +1,6 @@ using Sentry.Extensibility; using Sentry.Infrastructure; +using Sentry.Protocol; namespace Sentry.Internal; @@ -9,7 +10,7 @@ internal sealed class DefaultSentryStructuredLogger : SentryStructuredLogger, ID private readonly SentryOptions _options; private readonly ISystemClock _clock; - private readonly StructuredLogBatchProcessor _batchProcessor; + private readonly BatchProcessor _batchProcessor; internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) { @@ -20,7 +21,7 @@ internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemC _options = options; _clock = clock; - _batchProcessor = new StructuredLogBatchProcessor(hub, batchCount, batchInterval, _options.ClientReportRecorder, _options.DiagnosticLogger); + _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, StructuredLog.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger); } /// diff --git a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs new file mode 100644 index 0000000000..89e3433c8d --- /dev/null +++ b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs @@ -0,0 +1,76 @@ +using Sentry.Extensibility; +using Sentry.Infrastructure; +using Sentry.Protocol; + +namespace Sentry.Internal; + +internal sealed class DefaultSentryTraceMetrics : SentryTraceMetrics, IDisposable +{ + private readonly IHub _hub; + private readonly SentryOptions _options; + private readonly ISystemClock _clock; + + private readonly BatchProcessor _batchProcessor; + + internal DefaultSentryTraceMetrics(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) + { + Debug.Assert(hub.IsEnabled); + Debug.Assert(options.Experimental is { EnableMetrics: true }); + + _hub = hub; + _options = options; + _clock = clock; + + _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, TraceMetric.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger); + } + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + { + var metric = SentryMetric.Create(_hub, _clock, type, name, value, unit, attributes, scope); + CaptureMetric(metric); + } + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct + { + var metric = SentryMetric.Create(_hub, _clock, type, name, value, unit, attributes, scope); + CaptureMetric(metric); + } + + /// + protected internal override void CaptureMetric(SentryMetric metric) where T : struct + { + var configuredMetric = metric; + + if (_options.Experimental.BeforeSendMetricInternal is { } beforeSendMetric) + { + try + { + configuredMetric = beforeSendMetric.Invoke(metric); + } + catch (Exception e) + { + _options.DiagnosticLogger?.LogError(e, "The BeforeSendMetric callback threw an exception. The Metric will be dropped."); + return; + } + } + + if (configuredMetric is not null) + { + _batchProcessor.Enqueue(configuredMetric); + } + } + + /// + protected internal override void Flush() + { + _batchProcessor.Flush(); + } + + /// + public void Dispose() + { + _batchProcessor.Dispose(); + } +} diff --git a/src/Sentry/Internal/DisabledSentryTraceMetrics.cs b/src/Sentry/Internal/DisabledSentryTraceMetrics.cs new file mode 100644 index 0000000000..750f6717c1 --- /dev/null +++ b/src/Sentry/Internal/DisabledSentryTraceMetrics.cs @@ -0,0 +1,34 @@ +namespace Sentry.Internal; + +internal sealed class DisabledSentryTraceMetrics : SentryTraceMetrics +{ + internal static DisabledSentryTraceMetrics Instance { get; } = new DisabledSentryTraceMetrics(); + + internal DisabledSentryTraceMetrics() + { + } + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + { + // disabled + } + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct + { + // disabled + } + + /// + protected internal override void CaptureMetric(SentryMetric metric) where T : struct + { + // disabled + } + + /// + protected internal override void Flush() + { + // disabled + } +} diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index f44a288b33..dd789b9155 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,6 +1,5 @@ using Sentry.Extensibility; using Sentry.Infrastructure; -using Sentry.Integrations; using Sentry.Internal.Extensions; using Sentry.Protocol.Envelopes; using Sentry.Protocol.Metrics; @@ -82,6 +81,7 @@ internal Hub( } Logger = SentryStructuredLogger.Create(this, options, _clock); + Metrics = SentryTraceMetrics.Create(this, options, _clock); #if MEMORY_DUMP_SUPPORTED if (options.HeapDumpOptions is not null) @@ -819,6 +819,7 @@ public async Task FlushAsync(TimeSpan timeout) try { Logger.Flush(); + Metrics.Flush(); await CurrentClient.FlushAsync(timeout).ConfigureAwait(false); } catch (Exception e) @@ -853,7 +854,9 @@ public void Dispose() #endif Logger.Flush(); + Metrics.Flush(); (Logger as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryStructuredLogger + (Metrics as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryTraceMetrics try { @@ -884,4 +887,6 @@ public void Dispose() public SentryId LastEventId => CurrentScope.LastEventId; public SentryStructuredLogger Logger { get; } + + public SentryTraceMetrics Metrics { get; } } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index e9b06cff00..e3e85da713 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -446,6 +446,18 @@ internal static Envelope FromLog(StructuredLog log) return new Envelope(header, items); } + internal static Envelope FromMetric(TraceMetric metric) + { + var header = DefaultHeader; + + var items = new[] + { + EnvelopeItem.FromMetric(metric), + }; + + return new Envelope(header, items); + } + private static async Task> DeserializeHeaderAsync( Stream stream, CancellationToken cancellationToken = default) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 976c9a1305..21ab3ba25d 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -25,6 +25,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable internal const string TypeValueMetric = "statsd"; internal const string TypeValueCodeLocations = "metric_meta"; internal const string TypeValueLog = "log"; + internal const string TypeValueTraceMetric = "trace_metric"; private const string LengthKey = "length"; private const string FileNameKey = "filename"; @@ -369,6 +370,18 @@ internal static EnvelopeItem FromLog(StructuredLog log) return new EnvelopeItem(header, new JsonSerializable(log)); } + internal static EnvelopeItem FromMetric(TraceMetric metric) + { + var header = new Dictionary(3, StringComparer.Ordinal) + { + [TypeKey] = TypeValueTraceMetric, + ["item_count"] = metric.Length, + ["content_type"] = "application/vnd.sentry.items.trace-metric+json", + }; + + return new EnvelopeItem(header, new JsonSerializable(metric)); + } + private static async Task> DeserializeHeaderAsync( Stream stream, CancellationToken cancellationToken = default) diff --git a/src/Sentry/Protocol/StructuredLog.cs b/src/Sentry/Protocol/StructuredLog.cs index 6543d31ffc..6ff161e303 100644 --- a/src/Sentry/Protocol/StructuredLog.cs +++ b/src/Sentry/Protocol/StructuredLog.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Protocol.Envelopes; namespace Sentry.Protocol; @@ -34,4 +35,9 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndArray(); writer.WriteEndObject(); } + + internal static void Capture(IHub hub, SentryLog[] logs) + { + _ = hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(logs))); + } } diff --git a/src/Sentry/Protocol/TraceMetric.cs b/src/Sentry/Protocol/TraceMetric.cs new file mode 100644 index 0000000000..cbcea00a57 --- /dev/null +++ b/src/Sentry/Protocol/TraceMetric.cs @@ -0,0 +1,43 @@ +using Sentry.Extensibility; +using Sentry.Protocol.Envelopes; + +namespace Sentry.Protocol; + +/// +/// Represents the Sentry protocol for Trace-connected Metrics. +/// +/// +/// Sentry Docs: . +/// Sentry Developer Documentation: . +/// +internal sealed class TraceMetric : ISentryJsonSerializable +{ + private readonly ISentryMetric[] _items; + + public TraceMetric(ISentryMetric[] metrics) + { + _items = metrics; + } + + public int Length => _items.Length; + public ReadOnlySpan Items => _items; + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + writer.WriteStartArray("items"); + + foreach (var metric in _items) + { + metric.WriteTo(writer, logger); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } + + internal static void Capture(IHub hub, ISentryMetric[] metrics) + { + _ = hub.CaptureEnvelope(Envelope.FromMetric(new TraceMetric(metrics))); + } +} diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs new file mode 100644 index 0000000000..d72769e241 --- /dev/null +++ b/src/Sentry/SentryMetric.Factory.cs @@ -0,0 +1,48 @@ +using Sentry.Infrastructure; + +namespace Sentry; + +internal static class SentryMetric +{ + internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + { + Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); + + var timestamp = clock.GetUtcNow(); + SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); //TODO: move + + var metric = new SentryMetric(timestamp, traceId, type, name, value) + { + SpanId = spanId, + Unit = unit, + }; + + scope ??= hub.GetScope(); + metric.Apply(scope); + + metric.SetAttributes(attributes); + + return metric; + } + + internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct + { + Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); + + var timestamp = clock.GetUtcNow(); + SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + var metric = new SentryMetric(timestamp, traceId, type, name, value) + { + SpanId = spanId, + Unit = unit, + }; + + scope ??= hub.GetScope(); + metric.Apply(scope); + + metric.SetAttributes(attributes); + + return metric; + } +} diff --git a/src/Sentry/SentryMetric.Interface.cs b/src/Sentry/SentryMetric.Interface.cs new file mode 100644 index 0000000000..8a91ad868a --- /dev/null +++ b/src/Sentry/SentryMetric.Interface.cs @@ -0,0 +1,12 @@ +using Sentry.Extensibility; + +namespace Sentry; + +/// +/// Internal non-generic representation of . +/// +internal interface ISentryMetric +{ + /// + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger); +} diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs new file mode 100644 index 0000000000..d7cbe39c34 --- /dev/null +++ b/src/Sentry/SentryMetric.cs @@ -0,0 +1,357 @@ +using Sentry.Extensibility; +using Sentry.Protocol; + +namespace Sentry; + +/// +/// Represents a Sentry Trace-connected Metric. +/// +/// The numeric type of the metric. +/// +/// Sentry Docs: . +/// Sentry Developer Documentation: . +/// Sentry .NET SDK Docs: . +/// +[DebuggerDisplay(@"SentryMetric \{ Type = {Type}, Name = '{Name}', Value = {Value} \}")] +public sealed class SentryMetric : ISentryMetric where T : struct +{ + private Dictionary? _attributes; + + [SetsRequiredMembers] + internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name, T value) + { + Timestamp = timestamp; + TraceId = traceId; + Type = type; + Name = name; + Value = value; + } + + /// + /// Timestamp indicating when the metric was recorded. + /// + /// + /// Sent as seconds since the Unix epoch. + /// + public required DateTimeOffset Timestamp { get; init; } + + /// + /// The trace id of the metric. + /// + /// + /// The value should be 16 random bytes encoded as a hex string (32 characters long). + /// The trace id should be grabbed from the current propagation context in the SDK. + /// + public required SentryId TraceId { get; init; } + + /// + /// The type of metric. + /// + /// + /// One of: + /// + /// + /// Type + /// Description + /// + /// + /// counter + /// A metric that increments counts. + /// + /// + /// gauge + /// A metric that tracks a value that can go up or down. + /// + /// + /// distribution + /// A metric that tracks the statistical distribution of values. + /// + /// + /// + public required SentryMetricType Type { get; init; } + + /// + /// The name of the metric. + /// + /// + /// This should follow a hierarchical naming convention using dots as separators (e.g., api.response_time, db.query.duration). + /// + public required string Name { get; init; } + + /// + /// The numeric value of the metric. + /// + /// + /// The interpretation depends on the metric type: + /// + /// + /// Type + /// Description + /// + /// + /// counter + /// The count to increment by (should default to ). + /// + /// + /// gauge + /// The current value. + /// + /// + /// distribution + /// A single measured value. + /// + /// + /// + public required T Value { get; init; } + + /// + /// The span id of the span that was active when the metric was emitted. + /// + /// + /// The value should be 8 random bytes encoded as a hex string (16 characters long). + /// The span id should be grabbed from the current active span in the SDK. + /// + public SpanId? SpanId { get; init; } + + /// + /// The unit of measurement for the metric value. + /// + /// + /// Only used for and . + /// + public string? Unit { get; init; } + + /// + /// A dictionary of key-value pairs of arbitrary data attached to the metric. + /// + /// + /// Attributes must also declare the type of the value. + /// Supported Types: + /// + /// + /// Type + /// Comment + /// + /// + /// string + /// + /// + /// + /// boolean + /// + /// + /// + /// integer + /// 64-bit signed integer + /// + /// + /// double + /// 64-bit floating point number + /// + /// + /// Integers should be 64-bit signed integers. + /// For 64-bit unsigned integers, use the string type to avoid overflow issues until unsigned integers are natively supported. + /// + public IReadOnlyDictionary Attributes + { + get + { +#if NET8_0_OR_GREATER + return _attributes ?? (IReadOnlyDictionary)ReadOnlyDictionary.Empty; +#else + return _attributes ?? EmptyAttributes; +#endif + } + } + + /// + /// Set a key-value pair of data attached to the metric. + /// + public void SetAttribute(string key, object value) + { + _attributes ??= new Dictionary(); + + _attributes[key] = new SentryAttribute(value); + } + + internal void SetAttributes(IEnumerable>? attributes) + { + if (attributes is null) + { + return; + } + + if (_attributes is null) + { + if (attributes.TryGetNonEnumeratedCount(out var count)) + { + _attributes = new Dictionary(count); + } + else + { + _attributes = new Dictionary(); + } + } + else + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + if (attributes.TryGetNonEnumeratedCount(out var count)) + { + _ = _attributes.EnsureCapacity(_attributes.Count + count); + } +#endif + } + + foreach (var attribute in attributes) + { + _attributes[attribute.Key] = attribute.Value; + } + } + + internal void SetAttributes(ReadOnlySpan> attributes) + { + if (attributes.IsEmpty) + { + return; + } + + if (_attributes is null) + { + _attributes = new Dictionary(attributes.Length); + } + else + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length); +#endif + } + + foreach (var attribute in attributes) + { + _attributes[attribute.Key] = attribute.Value; + } + } + + internal void Apply(Scope? scope) + { + } + + void ISentryMetric.WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + +#if NET9_0_OR_GREATER + writer.WriteNumber("timestamp", Timestamp.ToUnixTimeMilliseconds() / (double)TimeSpan.MillisecondsPerSecond); +#else + writer.WriteNumber("timestamp", Timestamp.ToUnixTimeMilliseconds() / 1_000.0); +#endif + + writer.WriteString("type", Type.ToProtocolString(logger)); + writer.WriteString("name", Name); + writer.WriteMetricValue("value", Value); + + writer.WritePropertyName("trace_id"); + TraceId.WriteTo(writer, logger); + + if (SpanId.HasValue) + { + writer.WritePropertyName("span_id"); + SpanId.Value.WriteTo(writer, logger); + } + + if (Unit is not null) + { + writer.WriteString("unit", Unit); + } + + if (_attributes is not null && _attributes.Count != 0) + { + writer.WritePropertyName("attributes"); + writer.WriteStartObject(); + + foreach (var attribute in _attributes) + { + SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); + } + + writer.WriteEndObject(); + } + + writer.WriteEndObject(); + } + +#if !NET8_0_OR_GREATER + private static IReadOnlyDictionary EmptyAttributes { get; } = new ReadOnlyDictionary(new Dictionary()); +#endif +} + +// TODO: remove after upgrading 14.0 and updating +#if !NET6_0_OR_GREATER +file static class EnumerableExtensions +{ + internal static bool TryGetNonEnumeratedCount(this IEnumerable source, out int count) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source is ICollection genericCollection) + { + count = genericCollection.Count; + return true; + } + + if (source is ICollection collection) + { + count = collection.Count; + return true; + } + + count = 0; + return false; + } +} +#endif + +file static class Utf8JsonWriterExtensions +{ + //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. + internal static void WriteMetricValue(this Utf8JsonWriter writer, string propertyName, T value) where T : struct + { + var type = typeof(T); + + if (type == typeof(byte)) + { + writer.WriteNumber(propertyName, (byte)(object)value); + } + else if (type == typeof(short)) + { + writer.WriteNumber(propertyName, (short)(object)value); + } + else if (type == typeof(int)) + { + writer.WriteNumber(propertyName, (int)(object)value); + } + else if (type == typeof(long)) + { + writer.WriteNumber(propertyName, (long)(object)value); + } + else if (type == typeof(double)) + { + writer.WriteNumber(propertyName, (double)(object)value); + } + else if (type == typeof(float)) + { + writer.WriteNumber(propertyName, (float)(object)value); + } + else if (type == typeof(decimal)) + { + writer.WriteNumber(propertyName, (decimal)(object)value); + } + else + { + Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); + } + } +} diff --git a/src/Sentry/SentryMetricType.cs b/src/Sentry/SentryMetricType.cs new file mode 100644 index 0000000000..df6601942c --- /dev/null +++ b/src/Sentry/SentryMetricType.cs @@ -0,0 +1,54 @@ +using Sentry.Extensibility; + +namespace Sentry; + +/// +/// The type of metric. +/// +public enum SentryMetricType +{ + /// + /// A metric that increments counts. + /// + /// + /// represents the count to increment by. + /// By default: . + /// + Counter, + + /// + /// A metric that tracks a value that can go up or down. + /// + /// + /// represents the current value. + /// + Gauge, + + /// + /// A metric that tracks the statistical distribution of values. + /// + /// + /// represents a single measured value. + /// + Distribution, +} + +internal static class SentryMetricTypeExtensions +{ + internal static string ToProtocolString(this SentryMetricType type, IDiagnosticLogger? logger) + { + return type switch + { + SentryMetricType.Counter => "counter", + SentryMetricType.Gauge => "gauge", + SentryMetricType.Distribution => "distribution", + _ => IsNotDefined(type, logger), + }; + + static string IsNotDefined(SentryMetricType type, IDiagnosticLogger? logger) + { + logger?.LogDebug("Metric type {0} is not defined.", type); + return "unknown"; + } + } +} diff --git a/src/Sentry/SentryOptions.Callback.cs b/src/Sentry/SentryOptions.Callback.cs new file mode 100644 index 0000000000..3eea6eac21 --- /dev/null +++ b/src/Sentry/SentryOptions.Callback.cs @@ -0,0 +1,109 @@ +using Sentry.Extensibility; + +namespace Sentry; + +public partial class SentryOptions +{ +#if NET6_0_OR_GREATER + /// + /// Similar to . + /// +#endif + internal sealed class TraceMetricsCallbacks + { + //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. + private Func, SentryMetric?> _beforeSendMetricInt32; + private Func, SentryMetric?> _beforeSendMetricByte; + private Func, SentryMetric?> _beforeSendMetricInt16; + private Func, SentryMetric?> _beforeSendMetricInt64; + private Func, SentryMetric?> _beforeSendMetricSingle; + private Func, SentryMetric?> _beforeSendMetricDouble; + private Func, SentryMetric?> _beforeSendMetricDecimal; + + internal TraceMetricsCallbacks() + { + _beforeSendMetricByte = static traceMetric => traceMetric; + _beforeSendMetricInt16 = static traceMetric => traceMetric; + _beforeSendMetricInt32 = static traceMetric => traceMetric; + _beforeSendMetricInt64 = static traceMetric => traceMetric; + _beforeSendMetricSingle = static traceMetric => traceMetric; + _beforeSendMetricDouble = static traceMetric => traceMetric; + _beforeSendMetricDecimal = static traceMetric => traceMetric; + } + + //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. + internal void Set(Func, SentryMetric?> beforeSendMetric) where T : struct + { + beforeSendMetric ??= static traceMetric => traceMetric; + + if (typeof(T) == typeof(byte)) + { + _beforeSendMetricByte = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(int)) + { + _beforeSendMetricInt32 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(float)) + { + _beforeSendMetricSingle = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(double)) + { + _beforeSendMetricDouble = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(decimal)) + { + _beforeSendMetricDecimal = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(short)) + { + _beforeSendMetricInt16 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(long)) + { + _beforeSendMetricInt64 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else + { + SentrySdk.CurrentOptions?._diagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, double, and decimal.", typeof(T)); + } + } + + //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. + internal SentryMetric? Invoke(SentryMetric metric) where T : struct + { + if (typeof(T) == typeof(byte)) + { + return (SentryMetric?)(object?)_beforeSendMetricByte.Invoke((SentryMetric)(object)metric); + } + if (typeof(T) == typeof(short)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt16.Invoke((SentryMetric)(object)metric); + } + if (typeof(T) == typeof(int)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt32.Invoke((SentryMetric)(object)metric); + } + if (typeof(T) == typeof(long)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt64.Invoke((SentryMetric)(object)metric); + } + if (typeof(T) == typeof(float)) + { + return (SentryMetric?)(object?)_beforeSendMetricSingle.Invoke((SentryMetric)(object)metric); + } + if (typeof(T) == typeof(double)) + { + return (SentryMetric?)(object?)_beforeSendMetricDouble.Invoke((SentryMetric)(object)metric); + } + if (typeof(T) == typeof(decimal)) + { + return (SentryMetric?)(object?)_beforeSendMetricDecimal.Invoke((SentryMetric)(object)metric); + } + + System.Diagnostics.Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); + return null; + } + } +} diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index c769b98370..6db285068a 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -31,7 +31,7 @@ namespace Sentry; #if __MOBILE__ public partial class SentryOptions #else -public class SentryOptions +public partial class SentryOptions #endif { private Dictionary? _defaultTags; @@ -1905,4 +1905,51 @@ internal static List GetDefaultInAppExclude() => "ServiceStack", "Java.Interop", ]; + + /// + /// Sentry features that are currently in an experimental state. + /// + /// + /// Experimental features are subject to binary, source and behavioral breaking changes in future updates. + /// + public ExperimentalSentryOptions Experimental { get; } = new(); + + /// + /// Sentry features that are currently in an experimental state. + /// + /// + /// Experimental features are subject to binary, source and behavioral breaking changes in future updates. + /// + public class ExperimentalSentryOptions + { + private TraceMetricsCallbacks? _beforeSendMetric; + + internal ExperimentalSentryOptions() + { + } + + internal TraceMetricsCallbacks? BeforeSendMetricInternal => _beforeSendMetric; + + /// + /// When set to , the SDK does not generate and send metrics to Sentry via . + /// Defaults to . + /// + /// + public bool EnableMetrics { get; set; } = true; + + /// + /// Sets a callback function to be invoked before sending the metric to Sentry. + /// When the delegate throws an during invocation, the metric will not be captured. + /// + /// + /// It can be used to modify the metric object before being sent to Sentry. + /// To prevent the metric from being sent to Sentry, return . + /// + /// + public void SetBeforeSendMetric(Func, SentryMetric?> beforeSendMetric) where T : struct + { + _beforeSendMetric ??= new TraceMetricsCallbacks(); + _beforeSendMetric.Set(beforeSendMetric); + } + } } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 46da99658e..57b79673ed 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -857,4 +857,30 @@ public static void CauseCrash(CrashType crashType) [DllImport("libc", EntryPoint = "strlen")] private static extern IntPtr NativeStrlenLibC(IntPtr strt); #endif + + /// + /// Sentry features that are currently in an experimental state. + /// + /// + /// Experimental features are subject to binary, source and behavioral breaking changes in future updates. + /// + public static ExperimentalSentrySdk Experimental { get; } = new(); + + /// + /// Sentry features that are currently in an experimental state. + /// + /// + /// Experimental features are subject to binary, source and behavioral breaking changes in future updates. + /// + public sealed class ExperimentalSentrySdk + { + internal ExperimentalSentrySdk() + { + } + +#pragma warning disable SENTRYTRACECONNECTEDMETRICS + /// + public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => CurrentHub.Metrics; } +#pragma warning restore SENTRYTRACECONNECTEDMETRICS + } } diff --git a/src/Sentry/SentryTraceMetrics.Public.cs b/src/Sentry/SentryTraceMetrics.Public.cs new file mode 100644 index 0000000000..3bf1a75985 --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Public.cs @@ -0,0 +1,124 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void AddCounter(string name, T value, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, [], scope); + } + + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void AddCounter(string name, T value, IEnumerable>? attributes = null, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); + } + + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void AddCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit = null, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit = null, IEnumerable>? attributes = null, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit = null, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit = null, IEnumerable>? attributes = null, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); + } +} diff --git a/src/Sentry/SentryTraceMetrics.cs b/src/Sentry/SentryTraceMetrics.cs new file mode 100644 index 0000000000..9c0be19f8e --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.cs @@ -0,0 +1,63 @@ +using Sentry.Infrastructure; +using Sentry.Internal; + +namespace Sentry; + +/// +/// Creates and sends metrics to Sentry. +/// +public abstract partial class SentryTraceMetrics +{ + internal static SentryTraceMetrics Create(IHub hub, SentryOptions options, ISystemClock clock) + => Create(hub, options, clock, 100, TimeSpan.FromSeconds(5)); + + internal static SentryTraceMetrics Create(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) + { + return options.Experimental.EnableMetrics + ? new DefaultSentryTraceMetrics(hub, options, clock, batchCount, batchInterval) + : DisabledSentryTraceMetrics.Instance; + } + + private protected SentryTraceMetrics() + { + } + + /// + /// Buffers a Sentry Metric item + /// via the associated Batch Processor. + /// + /// The type of metric. + /// The name of the metric. + /// The numeric value of the metric. + /// The unit of measurement for the metric value. + /// A dictionary of key-value pairs of arbitrary data attached to the metric. + /// The optional scope to capture the metric with. + /// The numeric type of the metric. + private protected abstract void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct; + + /// + /// Buffers a Sentry Metric item + /// via the associated Batch Processor. + /// + /// The type of metric. + /// The name of the metric. + /// The numeric value of the metric. + /// The unit of measurement for the metric value. + /// A dictionary of key-value pairs of arbitrary data attached to the metric. + /// The optional scope to capture the metric with. + /// The numeric type of the metric. + private protected abstract void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct; + + /// + /// Buffers a Sentry Metric item + /// via the associated Batch Processor. + /// + /// The metric. + /// The numeric type of the metric. + protected internal abstract void CaptureMetric(SentryMetric metric) where T : struct; + + /// + /// Clears all buffers for this metrics and causes any buffered metrics to be sent by the underlying . + /// + protected internal abstract void Flush(); +} diff --git a/test/Sentry.Testing/BindableTests.cs b/test/Sentry.Testing/BindableTests.cs index 68dd553a36..69968238c8 100644 --- a/test/Sentry.Testing/BindableTests.cs +++ b/test/Sentry.Testing/BindableTests.cs @@ -32,6 +32,7 @@ private static IEnumerable GetBindableProperties(IEnumerable Entries { get; } = new(); + internal List Metrics { get; } = new(); + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + { + Entries.Add(MetricEntry.Create(type, name, value, unit, attributes, scope)); + } + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct + { + Entries.Add(MetricEntry.Create(type, name, value, unit, attributes.ToArray(), scope)); + } + + /// + protected internal override void CaptureMetric(SentryMetric metric) where T : struct + { + Metrics.Add(metric); + } + + /// + protected internal override void Flush() + { + // no-op + } + + public sealed class MetricEntry : IEquatable + { + public static MetricEntry Create(SentryMetricType type, string name, object value, string? unit, IEnumerable>? attributes, Scope? scope) + { + return new MetricEntry(type, name, value, unit, attributes is null ? ImmutableDictionary.Empty : attributes.ToImmutableDictionary(), scope); + } + + private MetricEntry(SentryMetricType type, string name, object value, string? unit, ImmutableDictionary attributes, Scope? scope) + { + Type = type; + Name = name; + Value = value; + Unit = unit; + Attributes = attributes; + Scope = scope; + } + + public SentryMetricType Type { get; } + public string Name { get; } + public object Value { get; } + public string? Unit { get; } + public ImmutableDictionary Attributes { get; } + public Scope? Scope { get; } + + public void AssertEqual(SentryMetricType type, string name, object value, string? unit, IEnumerable>? attributes, Scope? scope) + { + var expected = Create(type, name, value, unit, attributes, scope); + Assert.Equal(expected, this); + } + + public bool Equals(MetricEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Type == other.Type + && string.Equals(Name, other.Name, StringComparison.Ordinal) + && Value.Equals(other.Value) + && string.Equals(Unit, other.Unit, StringComparison.Ordinal) + && Attributes.SequenceEqual(other.Attributes, AttributeEqualityComparer.Instance) + && ReferenceEquals(Scope, other.Scope); + } + + public override bool Equals(object? obj) + { + return obj is MetricEntry other && Equals(other); + } + + public override int GetHashCode() + { + throw new UnreachableException(); + } + } + + private sealed class AttributeEqualityComparer : IEqualityComparer> + { + public static AttributeEqualityComparer Instance { get; } = new AttributeEqualityComparer(); + + private AttributeEqualityComparer() + { + } + + public bool Equals(KeyValuePair x, KeyValuePair y) + { + return string.Equals(x.Key, y.Key, StringComparison.Ordinal) + && Equals(x.Value, y.Value); + } + + public int GetHashCode(KeyValuePair obj) + { + return HashCode.Combine(obj.Key, obj.Value); + } + } +} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 458d39be99..123881cb39 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -197,6 +197,8 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -664,6 +666,32 @@ namespace Sentry protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, + } + [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] + [System.Runtime.CompilerServices.RequiredMember] + public sealed class SentryMetric + where T : struct + { + public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } + [System.Runtime.CompilerServices.RequiredMember] + public string Name { get; init; } + public Sentry.SpanId? SpanId { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public System.DateTimeOffset Timestamp { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public Sentry.SentryId TraceId { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public Sentry.SentryMetricType Type { get; init; } + public string? Unit { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public T Value { get; init; } + public void SetAttribute(string key, object value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -715,6 +743,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool EnableSpotlight { get; set; } public string? Environment { get; set; } + public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -802,6 +831,12 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } + public class ExperimentalSentryOptions + { + public bool EnableMetrics { get; set; } + public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) + where T : struct { } + } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable { @@ -831,6 +866,7 @@ namespace Sentry } public static class SentrySdk { + public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; } public static bool IsEnabled { get; } public static bool IsSessionActive { get; } public static Sentry.SentryId LastEventId { get; } @@ -894,6 +930,10 @@ namespace Sentry public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { } public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { } public static void UnsetTag(string key) { } + public sealed class ExperimentalSentrySdk + { + public Sentry.SentryTraceMetrics Metrics { get; } + } } public class SentrySession : Sentry.ISentrySession { @@ -1011,6 +1051,30 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + public void AddCounter(string name, T value, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + protected abstract void Flush(); + public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1385,6 +1449,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1432,6 +1498,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 458d39be99..123881cb39 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -197,6 +197,8 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -664,6 +666,32 @@ namespace Sentry protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, + } + [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] + [System.Runtime.CompilerServices.RequiredMember] + public sealed class SentryMetric + where T : struct + { + public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } + [System.Runtime.CompilerServices.RequiredMember] + public string Name { get; init; } + public Sentry.SpanId? SpanId { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public System.DateTimeOffset Timestamp { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public Sentry.SentryId TraceId { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public Sentry.SentryMetricType Type { get; init; } + public string? Unit { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public T Value { get; init; } + public void SetAttribute(string key, object value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -715,6 +743,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool EnableSpotlight { get; set; } public string? Environment { get; set; } + public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -802,6 +831,12 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } + public class ExperimentalSentryOptions + { + public bool EnableMetrics { get; set; } + public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) + where T : struct { } + } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable { @@ -831,6 +866,7 @@ namespace Sentry } public static class SentrySdk { + public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; } public static bool IsEnabled { get; } public static bool IsSessionActive { get; } public static Sentry.SentryId LastEventId { get; } @@ -894,6 +930,10 @@ namespace Sentry public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { } public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { } public static void UnsetTag(string key) { } + public sealed class ExperimentalSentrySdk + { + public Sentry.SentryTraceMetrics Metrics { get; } + } } public class SentrySession : Sentry.ISentrySession { @@ -1011,6 +1051,30 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + public void AddCounter(string name, T value, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + protected abstract void Flush(); + public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1385,6 +1449,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1432,6 +1498,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 458d39be99..123881cb39 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -197,6 +197,8 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -664,6 +666,32 @@ namespace Sentry protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, + } + [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] + [System.Runtime.CompilerServices.RequiredMember] + public sealed class SentryMetric + where T : struct + { + public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } + [System.Runtime.CompilerServices.RequiredMember] + public string Name { get; init; } + public Sentry.SpanId? SpanId { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public System.DateTimeOffset Timestamp { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public Sentry.SentryId TraceId { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public Sentry.SentryMetricType Type { get; init; } + public string? Unit { get; init; } + [System.Runtime.CompilerServices.RequiredMember] + public T Value { get; init; } + public void SetAttribute(string key, object value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -715,6 +743,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool EnableSpotlight { get; set; } public string? Environment { get; set; } + public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -802,6 +831,12 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } + public class ExperimentalSentryOptions + { + public bool EnableMetrics { get; set; } + public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) + where T : struct { } + } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable { @@ -831,6 +866,7 @@ namespace Sentry } public static class SentrySdk { + public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; } public static bool IsEnabled { get; } public static bool IsSessionActive { get; } public static Sentry.SentryId LastEventId { get; } @@ -894,6 +930,10 @@ namespace Sentry public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { } public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { } public static void UnsetTag(string key) { } + public sealed class ExperimentalSentrySdk + { + public Sentry.SentryTraceMetrics Metrics { get; } + } } public class SentrySession : Sentry.ISentrySession { @@ -1011,6 +1051,30 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + public void AddCounter(string name, T value, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + protected abstract void Flush(); + public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1385,6 +1449,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1432,6 +1498,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index f2b22fbfdb..b075f5c3ce 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -185,6 +185,7 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } + Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -646,6 +647,26 @@ namespace Sentry protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, + } + [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] + public sealed class SentryMetric + where T : struct + { + public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } + public string Name { get; init; } + public Sentry.SpanId? SpanId { get; init; } + public System.DateTimeOffset Timestamp { get; init; } + public Sentry.SentryId TraceId { get; init; } + public Sentry.SentryMetricType Type { get; init; } + public string? Unit { get; init; } + public T Value { get; init; } + public void SetAttribute(string key, object value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -697,6 +718,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool EnableSpotlight { get; set; } public string? Environment { get; set; } + public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -778,6 +800,12 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } + public class ExperimentalSentryOptions + { + public bool EnableMetrics { get; set; } + public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) + where T : struct { } + } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable { @@ -807,6 +835,7 @@ namespace Sentry } public static class SentrySdk { + public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; } public static bool IsEnabled { get; } public static bool IsSessionActive { get; } public static Sentry.SentryId LastEventId { get; } @@ -870,6 +899,10 @@ namespace Sentry public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { } public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { } public static void UnsetTag(string key) { } + public sealed class ExperimentalSentrySdk + { + public Sentry.SentryTraceMetrics Metrics { get; } + } } public class SentrySession : Sentry.ISentrySession { @@ -987,6 +1020,30 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + public void AddCounter(string name, T value, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + protected abstract void Flush(); + public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + where T : struct { } + } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1361,6 +1418,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1408,6 +1466,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/Internals/StructuredLogBatchBufferTests.cs b/test/Sentry.Tests/Internals/BatchBufferTests.cs similarity index 82% rename from test/Sentry.Tests/Internals/StructuredLogBatchBufferTests.cs rename to test/Sentry.Tests/Internals/BatchBufferTests.cs index 77d8d34ebd..51cdec4512 100644 --- a/test/Sentry.Tests/Internals/StructuredLogBatchBufferTests.cs +++ b/test/Sentry.Tests/Internals/BatchBufferTests.cs @@ -2,7 +2,12 @@ namespace Sentry.Tests.Internals; -public class StructuredLogBatchBufferTests +/// +/// (formerly "Sentry.Internal.StructuredLogBatchBuffer") was originally developed as Batch Buffer for Logs only. +/// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both. +/// These tests are still using , rather than . +/// +public class BatchBufferTests { private sealed class Fixture { @@ -10,14 +15,14 @@ private sealed class Fixture public TimeSpan Timeout { get; set; } = System.Threading.Timeout.InfiniteTimeSpan; public string? Name { get; set; } - public List TimeoutExceededInvocations { get; } = []; + public List> TimeoutExceededInvocations { get; } = []; - public StructuredLogBatchBuffer GetSut() + public BatchBuffer GetSut() { - return new StructuredLogBatchBuffer(Capacity, Timeout, OnTimeoutExceeded, Name); + return new BatchBuffer(Capacity, Timeout, OnTimeoutExceeded, Name); } - private void OnTimeoutExceeded(StructuredLogBatchBuffer buffer) + private void OnTimeoutExceeded(BatchBuffer buffer) { TimeoutExceededInvocations.Add(buffer); } @@ -72,16 +77,16 @@ public void Add_CapacityTwo_CanAddTwice() buffer.Capacity.Should().Be(2); buffer.IsEmpty.Should().BeTrue(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.AddedFirst); + buffer.Add("one").Should().Be(BatchBufferAddStatus.AddedFirst); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("two").Should().Be(StructuredLogBatchBufferAddStatus.AddedLast); + buffer.Add("two").Should().Be(BatchBufferAddStatus.AddedLast); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("three").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded); + buffer.Add("three").Should().Be(BatchBufferAddStatus.IgnoredCapacityExceeded); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("four").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded); + buffer.Add("four").Should().Be(BatchBufferAddStatus.IgnoredCapacityExceeded); buffer.IsEmpty.Should().BeFalse(); } @@ -94,16 +99,16 @@ public void Add_CapacityThree_CanAddThrice() buffer.Capacity.Should().Be(3); buffer.IsEmpty.Should().BeTrue(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.AddedFirst); + buffer.Add("one").Should().Be(BatchBufferAddStatus.AddedFirst); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("two").Should().Be(StructuredLogBatchBufferAddStatus.Added); + buffer.Add("two").Should().Be(BatchBufferAddStatus.Added); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("three").Should().Be(StructuredLogBatchBufferAddStatus.AddedLast); + buffer.Add("three").Should().Be(BatchBufferAddStatus.AddedLast); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("four").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded); + buffer.Add("four").Should().Be(BatchBufferAddStatus.IgnoredCapacityExceeded); buffer.IsEmpty.Should().BeFalse(); } @@ -115,12 +120,12 @@ public void Add_Flushing_CannotAdd() var flushScope = buffer.TryEnterFlushScope(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredIsFlushing); + buffer.Add("one").Should().Be(BatchBufferAddStatus.IgnoredIsFlushing); buffer.IsEmpty.Should().BeTrue(); flushScope.Dispose(); - buffer.Add("two").Should().Be(StructuredLogBatchBufferAddStatus.AddedFirst); + buffer.Add("two").Should().Be(BatchBufferAddStatus.AddedFirst); buffer.IsEmpty.Should().BeFalse(); } @@ -132,7 +137,7 @@ public void Add_Disposed_CannotAdd() buffer.Dispose(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredIsDisposed); + buffer.Add("one").Should().Be(BatchBufferAddStatus.IgnoredIsDisposed); buffer.IsEmpty.Should().BeTrue(); } @@ -311,7 +316,7 @@ public void OnIntervalElapsed_Disposed_DoesNotInvokeCallback() } // cannot use xUnit's Throws() nor Fluent Assertions' ThrowExactly() because the FlushScope is a ref struct - private static void AssertFlushThrows(StructuredLogBatchBuffer.FlushScope flushScope) + private static void AssertFlushThrows(BatchBuffer.FlushScope flushScope) where T : Exception { Exception? exception = null; @@ -329,9 +334,9 @@ private static void AssertFlushThrows(StructuredLogBatchBuffer.FlushScope flu } } -file static class StructuredLogBatchBufferHelpers +file static class BatchBufferHelpers { - public static StructuredLogBatchBufferAddStatus Add(this StructuredLogBatchBuffer buffer, string item) + public static BatchBufferAddStatus Add(this BatchBuffer buffer, string item) { SentryLog log = new(DateTimeOffset.MinValue, SentryId.Empty, SentryLogLevel.Trace, item); return buffer.Add(log); diff --git a/test/Sentry.Tests/Internals/StructuredLogBatchProcessorTests.cs b/test/Sentry.Tests/Internals/BatchProcessorTests.cs similarity index 91% rename from test/Sentry.Tests/Internals/StructuredLogBatchProcessorTests.cs rename to test/Sentry.Tests/Internals/BatchProcessorTests.cs index 12279f6115..83b1feca7f 100644 --- a/test/Sentry.Tests/Internals/StructuredLogBatchProcessorTests.cs +++ b/test/Sentry.Tests/Internals/BatchProcessorTests.cs @@ -2,7 +2,12 @@ namespace Sentry.Tests.Internals; -public class StructuredLogBatchProcessorTests : IDisposable +/// +/// (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only. +/// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both. +/// These tests are still using , rather than . +/// +public class BatchProcessorTests : IDisposable { private sealed class Fixture { @@ -35,9 +40,9 @@ public void DisableHub() _hub.IsEnabled.Returns(false); } - public StructuredLogBatchProcessor GetSut(int batchCount) + public BatchProcessor GetSut(int batchCount) { - return new StructuredLogBatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, ClientReportRecorder, DiagnosticLogger); + return new BatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, StructuredLog.Capture, ClientReportRecorder, DiagnosticLogger); } } @@ -128,7 +133,7 @@ public async Task Enqueue_Concurrency_CaptureEnvelopes() { tasks[i] = Task.Factory.StartNew(static state => { - var (sync, logsPerTask, taskIndex, processor) = ((ManualResetEvent, int, int, StructuredLogBatchProcessor))state!; + var (sync, logsPerTask, taskIndex, processor) = ((ManualResetEvent, int, int, BatchProcessor))state!; sync.WaitOne(5_000); for (var i = 0; i < logsPerTask; i++) { diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 008a56df19..3a038fd840 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -240,9 +240,9 @@ public void Dispose_BeforeLog_DoesNotCaptureEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Info); - entry.Message.Should().Be("Log Buffer full ... dropping log"); + entry.Message.Should().Be("{0}-Buffer full ... dropping {0}"); entry.Exception.Should().BeNull(); - entry.Args.Should().BeEmpty(); + entry.Args.Should().BeEquivalentTo([nameof(SentryLog)]); } private static void ConfigureLog(SentryLog log) From 6c5ca237fe3921a32cbebf4681733a822eb9a3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:52:10 +0100 Subject: [PATCH 02/26] add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26bbf20371..2b820b01bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807)) +- Add _experimental_ support for [Sentry trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) ### Dependencies From 9485ca4b73336348ae513a3fdca50a95bd73f742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:11:19 +0100 Subject: [PATCH 03/26] more public overloads for convenience --- src/Sentry/SentryTraceMetrics.Counter.cs | 53 ++++++++ src/Sentry/SentryTraceMetrics.Distribution.cs | 68 ++++++++++ src/Sentry/SentryTraceMetrics.Gauge.cs | 68 ++++++++++ src/Sentry/SentryTraceMetrics.Public.cs | 124 ------------------ 4 files changed, 189 insertions(+), 124 deletions(-) create mode 100644 src/Sentry/SentryTraceMetrics.Counter.cs create mode 100644 src/Sentry/SentryTraceMetrics.Distribution.cs create mode 100644 src/Sentry/SentryTraceMetrics.Gauge.cs delete mode 100644 src/Sentry/SentryTraceMetrics.Public.cs diff --git a/src/Sentry/SentryTraceMetrics.Counter.cs b/src/Sentry/SentryTraceMetrics.Counter.cs new file mode 100644 index 0000000000..0e43e2c84e --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Counter.cs @@ -0,0 +1,53 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// The numeric type of the metric. + public void AddCounter(string name, T value) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, [], null); + } + + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void AddCounter(string name, T value, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, [], scope); + } + + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void AddCounter(string name, T value, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); + } + + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void AddCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); + } +} diff --git a/src/Sentry/SentryTraceMetrics.Distribution.cs b/src/Sentry/SentryTraceMetrics.Distribution.cs new file mode 100644 index 0000000000..2a27e1f1ec --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Distribution.cs @@ -0,0 +1,68 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, null, [], null); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); + } +} diff --git a/src/Sentry/SentryTraceMetrics.Gauge.cs b/src/Sentry/SentryTraceMetrics.Gauge.cs new file mode 100644 index 0000000000..1cc15fdcb7 --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Gauge.cs @@ -0,0 +1,68 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The numeric type of the metric. + public void RecordGauge(string name, T value) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, null, [], null); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], null); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + public void RecordGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); + } +} diff --git a/src/Sentry/SentryTraceMetrics.Public.cs b/src/Sentry/SentryTraceMetrics.Public.cs deleted file mode 100644 index 3bf1a75985..0000000000 --- a/src/Sentry/SentryTraceMetrics.Public.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Sentry; - -public abstract partial class SentryTraceMetrics -{ - /// - /// Increment a counter. - /// - /// The name of the metric. - /// The value of the metric. - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void AddCounter(string name, T value, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Counter, name, value, null, [], scope); - } - - /// - /// Increment a counter. - /// - /// The name of the metric. - /// The value of the metric. - /// A dictionary of attributes (key-value pairs with type information). - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void AddCounter(string name, T value, IEnumerable>? attributes = null, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); - } - - /// - /// Increment a counter. - /// - /// The name of the metric. - /// The value of the metric. - /// A dictionary of attributes (key-value pairs with type information). - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void AddCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); - } - - /// - /// Set a gauge value. - /// - /// The name of the metric. - /// The value of the metric. - /// The unit of measurement. - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit = null, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope); - } - - /// - /// Set a gauge value. - /// - /// The name of the metric. - /// The value of the metric. - /// The unit of measurement. - /// A dictionary of attributes (key-value pairs with type information). - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit = null, IEnumerable>? attributes = null, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); - } - - /// - /// Set a gauge value. - /// - /// The name of the metric. - /// The value of the metric. - /// The unit of measurement. - /// A dictionary of attributes (key-value pairs with type information). - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); - } - - /// - /// Add a distribution value. - /// - /// The name of the metric. - /// The value of the metric. - /// The unit of measurement. - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit = null, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); - } - - /// - /// Add a distribution value. - /// - /// The name of the metric. - /// The value of the metric. - /// The unit of measurement. - /// A dictionary of attributes (key-value pairs with type information). - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit = null, IEnumerable>? attributes = null, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); - } - - /// - /// Add a distribution value. - /// - /// The name of the metric. - /// The value of the metric. - /// The unit of measurement. - /// A dictionary of attributes (key-value pairs with type information). - /// The scope to capture the metric with. - /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct - { - CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); - } -} From aa46acb4ab9be80e48a340d72e8a5801fa835c7e Mon Sep 17 00:00:00 2001 From: Flash0ver <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:44:34 +0100 Subject: [PATCH 04/26] more public overloads for convenience (API Approval Tests) --- ...iApprovalTests.Run.DotNet10_0.verified.txt | 24 +++++++++++++------ ...piApprovalTests.Run.DotNet8_0.verified.txt | 24 +++++++++++++------ ...piApprovalTests.Run.DotNet9_0.verified.txt | 24 +++++++++++++------ .../ApiApprovalTests.Run.Net4_8.verified.txt | 24 +++++++++++++------ 4 files changed, 68 insertions(+), 28 deletions(-) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 123881cb39..9af07b8032 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -1053,26 +1053,36 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value, Sentry.Scope? scope = null) + public void AddCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void AddCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void CaptureMetric(Sentry.SentryMetric metric) where T : struct; protected abstract void Flush(); - public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordDistribution(string name, T value) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 123881cb39..9af07b8032 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -1053,26 +1053,36 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value, Sentry.Scope? scope = null) + public void AddCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void AddCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void CaptureMetric(Sentry.SentryMetric metric) where T : struct; protected abstract void Flush(); - public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordDistribution(string name, T value) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 123881cb39..9af07b8032 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -1053,26 +1053,36 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value, Sentry.Scope? scope = null) + public void AddCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void AddCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void CaptureMetric(Sentry.SentryMetric metric) where T : struct; protected abstract void Flush(); - public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordDistribution(string name, T value) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index b075f5c3ce..cd46b85b55 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -1022,26 +1022,36 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value, Sentry.Scope? scope = null) + public void AddCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void AddCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void CaptureMetric(Sentry.SentryMetric metric) where T : struct; protected abstract void Flush(); - public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordDistribution(string name, T value) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null) + public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext From e9c349edc4c80817fa89d3cb2de09f42eb8a5f3b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 9 Jan 2026 21:21:58 +0000 Subject: [PATCH 05/26] release: 6.1.0-alpha.1 --- CHANGELOG.md | 2 +- Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b820b01bb..d60e0d291a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 6.1.0-alpha.1 ### Features diff --git a/Directory.Build.props b/Directory.Build.props index 0c8421134f..7895db4238 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0 + 6.1.0-alpha.1 13 true true From e65db5f13c9b976fedf3b4b726e20fb5659742a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:55:19 +0100 Subject: [PATCH 06/26] rename APIs, validate parameters, fix attributes --- CHANGELOG.md | 18 ++ Directory.Build.props | 1 + src/Sentry/Extensibility/DisabledHub.cs | 2 +- src/Sentry/Extensibility/HubAdapter.cs | 2 +- src/Sentry/IHub.cs | 2 +- src/Sentry/Internal/BatchProcessor.cs | 4 +- .../Internal/DefaultSentryTraceMetrics.cs | 31 ++- src/Sentry/SentryMetric.Factory.cs | 53 ++++- src/Sentry/SentryMetric.cs | 187 ++++++++++-------- src/Sentry/SentryOptions.Callback.cs | 109 ---------- src/Sentry/SentryOptions.cs | 22 ++- src/Sentry/SentryTraceMetrics.Counter.cs | 12 +- src/Sentry/SentryTraceMetrics.Distribution.cs | 15 +- src/Sentry/SentryTraceMetrics.Gauge.cs | 15 +- src/Sentry/SentryTraceMetricsCallbacks.cs | 98 +++++++++ .../InMemorySentryTraceMetrics.cs | 6 + ...iApprovalTests.Run.DotNet10_0.verified.txt | 45 ++--- ...piApprovalTests.Run.DotNet8_0.verified.txt | 45 ++--- ...piApprovalTests.Run.DotNet9_0.verified.txt | 45 ++--- .../ApiApprovalTests.Run.Net4_8.verified.txt | 39 ++-- 20 files changed, 440 insertions(+), 311 deletions(-) delete mode 100644 src/Sentry/SentryOptions.Callback.cs create mode 100644 src/Sentry/SentryTraceMetricsCallbacks.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d60e0d291a..a82b5f3071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## Unreleased + +### BREAKING CHANGES + +- Rename [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) APIs to avoid implying aggregation ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) + - from `AddCounter` to `EmitCounter` + - from `RecordDistribution` to `EmitDistribution` + - from `RecordGauge` to `EmitGauge` + +### Features + +- Validate [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) + +### Fixes + +- Attributes for [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) set via `SetBeforeSendLog` callback ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) +- Disallow unsupported 128-bit floating point numbers (i.e. `decimal`) for [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) + ## 6.1.0-alpha.1 ### Features diff --git a/Directory.Build.props b/Directory.Build.props index 7895db4238..7e15f5723e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,6 +13,7 @@ $(NoWarn);SENTRY0001 + $(NoWarn);SENTRYTRACECONNECTEDMETRICS $(NoWarn);CS8002 diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 273fba0783..6c6a7ae31d 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -270,6 +270,6 @@ public void Dispose() /// /// Disabled Metrics. /// - [Experimental("SENTRYTRACECONNECTEDMETRICS")] + [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] public SentryTraceMetrics Metrics => DisabledSentryTraceMetrics.Instance; } diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index f02b50b589..6ec4c79104 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -39,7 +39,7 @@ private HubAdapter() { } /// /// Forwards the call to . /// - [Experimental("SENTRYTRACECONNECTEDMETRICS")] + [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => SentrySdk.Experimental.Metrics; } /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index ee44057413..2d48844a9e 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -39,7 +39,7 @@ public interface IHub : ISentryClient, ISentryScopeManager /// /// /// - [Experimental("SENTRYTRACECONNECTEDMETRICS")] + [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] public SentryTraceMetrics Metrics { get; } /// diff --git a/src/Sentry/Internal/BatchProcessor.cs b/src/Sentry/Internal/BatchProcessor.cs index f71f2498e0..bb37bc474e 100644 --- a/src/Sentry/Internal/BatchProcessor.cs +++ b/src/Sentry/Internal/BatchProcessor.cs @@ -30,7 +30,7 @@ namespace Sentry.Internal; /// OpenTelemetry Batch Processor /// Sentry Logs /// Sentry Metrics -internal sealed class BatchProcessor : IDisposable +internal sealed class BatchProcessor : IDisposable where TItem : notnull { private readonly IHub _hub; private readonly Action _sendAction; @@ -68,7 +68,7 @@ internal void Enqueue(TItem item) if (!TryEnqueue(activeBuffer, item)) { _clientReportRecorder.RecordDiscardedEvent(DiscardReason.Backpressure, DataCategory.Default, 1); - _diagnosticLogger?.LogInfo("{0}-Buffer full ... dropping {0}", typeof(TItem).Name); + _diagnosticLogger?.LogInfo("{0}-Buffer full ... dropping {0}", item.GetType().Name); } } } diff --git a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs index 89e3433c8d..df02cfa44f 100644 --- a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs +++ b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs @@ -27,20 +27,47 @@ internal DefaultSentryTraceMetrics(IHub hub, SentryOptions options, ISystemClock /// private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct { - var metric = SentryMetric.Create(_hub, _clock, type, name, value, unit, attributes, scope); + if (!SentryMetric.IsSupported(typeof(T))) + { + _options.DiagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); + return; + } + + if (string.IsNullOrEmpty(name)) + { + _options.DiagnosticLogger?.LogWarning("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}", type.ToString(), typeof(T)); + return; + } + + var metric = SentryMetric.Create(_hub, _options, _clock, type, name, value, unit, attributes, scope); CaptureMetric(metric); } /// private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct { - var metric = SentryMetric.Create(_hub, _clock, type, name, value, unit, attributes, scope); + if (!SentryMetric.IsSupported(typeof(T))) + { + _options.DiagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); + return; + } + + if (string.IsNullOrEmpty(name)) + { + _options.DiagnosticLogger?.LogWarning("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}", type.ToString(), typeof(T)); + return; + } + + var metric = SentryMetric.Create(_hub, _options, _clock, type, name, value, unit, attributes, scope); CaptureMetric(metric); } /// protected internal override void CaptureMetric(SentryMetric metric) where T : struct { + Debug.Assert(SentryMetric.IsSupported(typeof(T))); + Debug.Assert(!string.IsNullOrEmpty(metric.Name)); + var configuredMetric = metric; if (_options.Experimental.BeforeSendMetricInternal is { } beforeSendMetric) diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index d72769e241..290d941b1f 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -1,15 +1,18 @@ using Sentry.Infrastructure; +using Sentry.Internal; namespace Sentry; internal static class SentryMetric { - internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct { + Debug.Assert(IsSupported()); + Debug.Assert(!string.IsNullOrEmpty(name)); Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); var timestamp = clock.GetUtcNow(); - SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); //TODO: move + GetTraceIdAndSpanId(hub, out var traceId, out var spanId); var metric = new SentryMetric(timestamp, traceId, type, name, value) { @@ -18,6 +21,7 @@ internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMe }; scope ??= hub.GetScope(); + metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); metric.Apply(scope); metric.SetAttributes(attributes); @@ -25,12 +29,14 @@ internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMe return metric; } - internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct + internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct { + Debug.Assert(IsSupported()); + Debug.Assert(!string.IsNullOrEmpty(name)); Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); var timestamp = clock.GetUtcNow(); - SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + GetTraceIdAndSpanId(hub, out var traceId, out var spanId); var metric = new SentryMetric(timestamp, traceId, type, name, value) { @@ -39,10 +45,49 @@ internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMe }; scope ??= hub.GetScope(); + metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); metric.Apply(scope); metric.SetAttributes(attributes); return metric; } + + internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) + { + var activeSpan = hub.GetSpan(); + if (activeSpan is not null) + { + traceId = activeSpan.TraceId; + spanId = activeSpan.SpanId; + return; + } + + // set "span_id" to the ID of the Span that was active when the Metric was emitted + // do not set "span_id" if there was no active Span + spanId = null; + + var scope = hub.GetScope(); + if (scope is not null) + { + traceId = scope.PropagationContext.TraceId; + return; + } + + Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable."); + traceId = SentryId.Empty; + } + + private static bool IsSupported() where T : struct + { + var valueType = typeof(T); + return IsSupported(valueType); + } + + internal static bool IsSupported(Type valueType) + { + return valueType == typeof(long) || valueType == typeof(double) + || valueType == typeof(int) || valueType == typeof(float) + || valueType == typeof(short) || valueType == typeof(byte); + } } diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs index d7cbe39c34..ba62ca2d7d 100644 --- a/src/Sentry/SentryMetric.cs +++ b/src/Sentry/SentryMetric.cs @@ -15,7 +15,7 @@ namespace Sentry; [DebuggerDisplay(@"SentryMetric \{ Type = {Type}, Name = '{Name}', Value = {Value} \}")] public sealed class SentryMetric : ISentryMetric where T : struct { - private Dictionary? _attributes; + private readonly Dictionary _attributes; [SetsRequiredMembers] internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name, T value) @@ -25,6 +25,8 @@ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricTy Type = type; Name = name; Value = value; + // 7 is the number of built-in attributes, so we start with that. + _attributes = new Dictionary(7); } /// @@ -38,10 +40,6 @@ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricTy /// /// The trace id of the metric. /// - /// - /// The value should be 16 random bytes encoded as a hex string (32 characters long). - /// The trace id should be grabbed from the current propagation context in the SDK. - /// public required SentryId TraceId { get; init; } /// @@ -107,10 +105,6 @@ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricTy /// /// The span id of the span that was active when the metric was emitted. /// - /// - /// The value should be 8 random bytes encoded as a hex string (16 characters long). - /// The span id should be grabbed from the current active span in the SDK. - /// public SpanId? SpanId { get; init; } /// @@ -122,58 +116,116 @@ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricTy public string? Unit { get; init; } /// - /// A dictionary of key-value pairs of arbitrary data attached to the metric. + /// Gets the attribute value associated with the specified key. /// /// - /// Attributes must also declare the type of the value. - /// Supported Types: + /// Returns if the contains an attribute with the specified key which is of type and it's value is not . + /// Otherwise . + /// Supported types: /// /// /// Type - /// Comment + /// Range /// /// /// string - /// + /// and /// /// /// boolean - /// + /// and /// /// /// integer - /// 64-bit signed integer + /// 64-bit signed integral numeric types /// /// /// double - /// 64-bit floating point number + /// 64-bit floating-point numeric types + /// + /// + /// Unsupported types: + /// + /// + /// Type + /// Result + /// + /// + /// + /// ToString as "type": "string" + /// + /// + /// Collections + /// ToString as "type": "string" + /// + /// + /// + /// ignored /// /// - /// Integers should be 64-bit signed integers. - /// For 64-bit unsigned integers, use the string type to avoid overflow issues until unsigned integers are natively supported. /// - public IReadOnlyDictionary Attributes + /// + public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value) { - get + if (_attributes.TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue) { -#if NET8_0_OR_GREATER - return _attributes ?? (IReadOnlyDictionary)ReadOnlyDictionary.Empty; -#else - return _attributes ?? EmptyAttributes; -#endif + value = attributeValue; + return true; } + + value = default; + return false; } /// /// Set a key-value pair of data attached to the metric. /// - public void SetAttribute(string key, object value) + public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { - _attributes ??= new Dictionary(); + if (value is null) + { + return; + } _attributes[key] = new SentryAttribute(value); } + internal void SetAttribute(string key, string value) + { + _attributes[key] = new SentryAttribute(value, "string"); + } + + internal void SetAttribute(string key, char value) + { + _attributes[key] = new SentryAttribute(value.ToString(), "string"); + } + + internal void SetAttribute(string key, int value) + { + _attributes[key] = new SentryAttribute(value, "integer"); + } + + internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) + { + var environment = options.SettingLocator.GetEnvironment(); + SetAttribute("sentry.environment", environment); + + var release = options.SettingLocator.GetRelease(); + if (release is not null) + { + SetAttribute("sentry.release", release); + } + + if (sdk.Name is { } name) + { + SetAttribute("sentry.sdk.name", name); + } + if (sdk.Version is { } version) + { + SetAttribute("sentry.sdk.version", version); + } + } + internal void SetAttributes(IEnumerable>? attributes) { if (attributes is null) @@ -181,30 +233,16 @@ internal void SetAttributes(IEnumerable>? attribute return; } - if (_attributes is null) +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + if (attributes.TryGetNonEnumeratedCount(out var count)) { - if (attributes.TryGetNonEnumeratedCount(out var count)) - { - _attributes = new Dictionary(count); - } - else - { - _attributes = new Dictionary(); - } + _ = _attributes.EnsureCapacity(_attributes.Count + count); } - else - { -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - if (attributes.TryGetNonEnumeratedCount(out var count)) - { - _ = _attributes.EnsureCapacity(_attributes.Count + count); - } #endif - } foreach (var attribute in attributes) { - _attributes[attribute.Key] = attribute.Value; + _attributes[attribute.Key] = new SentryAttribute(attribute.Value); } } @@ -215,20 +253,13 @@ internal void SetAttributes(ReadOnlySpan> attribute return; } - if (_attributes is null) - { - _attributes = new Dictionary(attributes.Length); - } - else - { #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length); + _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length); #endif - } foreach (var attribute in attributes) { - _attributes[attribute.Key] = attribute.Value; + _attributes[attribute.Key] = new SentryAttribute(attribute.Value); } } @@ -264,25 +295,18 @@ void ISentryMetric.WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteString("unit", Unit); } - if (_attributes is not null && _attributes.Count != 0) - { - writer.WritePropertyName("attributes"); - writer.WriteStartObject(); - - foreach (var attribute in _attributes) - { - SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); - } + writer.WritePropertyName("attributes"); + writer.WriteStartObject(); - writer.WriteEndObject(); + foreach (var attribute in _attributes) + { + SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); } writer.WriteEndObject(); - } -#if !NET8_0_OR_GREATER - private static IReadOnlyDictionary EmptyAttributes { get; } = new ReadOnlyDictionary(new Dictionary()); -#endif + writer.WriteEndObject(); + } } // TODO: remove after upgrading 14.0 and updating @@ -316,38 +340,33 @@ internal static bool TryGetNonEnumeratedCount(this IEnumerable file static class Utf8JsonWriterExtensions { - //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. internal static void WriteMetricValue(this Utf8JsonWriter writer, string propertyName, T value) where T : struct { var type = typeof(T); - if (type == typeof(byte)) + if (type == typeof(long)) { - writer.WriteNumber(propertyName, (byte)(object)value); + writer.WriteNumber(propertyName, (long)(object)value); } - else if (type == typeof(short)) + else if (type == typeof(double)) { - writer.WriteNumber(propertyName, (short)(object)value); + writer.WriteNumber(propertyName, (double)(object)value); } else if (type == typeof(int)) { writer.WriteNumber(propertyName, (int)(object)value); } - else if (type == typeof(long)) - { - writer.WriteNumber(propertyName, (long)(object)value); - } - else if (type == typeof(double)) - { - writer.WriteNumber(propertyName, (double)(object)value); - } else if (type == typeof(float)) { writer.WriteNumber(propertyName, (float)(object)value); } - else if (type == typeof(decimal)) + else if (type == typeof(short)) { - writer.WriteNumber(propertyName, (decimal)(object)value); + writer.WriteNumber(propertyName, (short)(object)value); + } + else if (type == typeof(byte)) + { + writer.WriteNumber(propertyName, (byte)(object)value); } else { diff --git a/src/Sentry/SentryOptions.Callback.cs b/src/Sentry/SentryOptions.Callback.cs deleted file mode 100644 index 3eea6eac21..0000000000 --- a/src/Sentry/SentryOptions.Callback.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Sentry.Extensibility; - -namespace Sentry; - -public partial class SentryOptions -{ -#if NET6_0_OR_GREATER - /// - /// Similar to . - /// -#endif - internal sealed class TraceMetricsCallbacks - { - //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. - private Func, SentryMetric?> _beforeSendMetricInt32; - private Func, SentryMetric?> _beforeSendMetricByte; - private Func, SentryMetric?> _beforeSendMetricInt16; - private Func, SentryMetric?> _beforeSendMetricInt64; - private Func, SentryMetric?> _beforeSendMetricSingle; - private Func, SentryMetric?> _beforeSendMetricDouble; - private Func, SentryMetric?> _beforeSendMetricDecimal; - - internal TraceMetricsCallbacks() - { - _beforeSendMetricByte = static traceMetric => traceMetric; - _beforeSendMetricInt16 = static traceMetric => traceMetric; - _beforeSendMetricInt32 = static traceMetric => traceMetric; - _beforeSendMetricInt64 = static traceMetric => traceMetric; - _beforeSendMetricSingle = static traceMetric => traceMetric; - _beforeSendMetricDouble = static traceMetric => traceMetric; - _beforeSendMetricDecimal = static traceMetric => traceMetric; - } - - //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. - internal void Set(Func, SentryMetric?> beforeSendMetric) where T : struct - { - beforeSendMetric ??= static traceMetric => traceMetric; - - if (typeof(T) == typeof(byte)) - { - _beforeSendMetricByte = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(int)) - { - _beforeSendMetricInt32 = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(float)) - { - _beforeSendMetricSingle = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(double)) - { - _beforeSendMetricDouble = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(decimal)) - { - _beforeSendMetricDecimal = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(short)) - { - _beforeSendMetricInt16 = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(long)) - { - _beforeSendMetricInt64 = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else - { - SentrySdk.CurrentOptions?._diagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, double, and decimal.", typeof(T)); - } - } - - //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number. - internal SentryMetric? Invoke(SentryMetric metric) where T : struct - { - if (typeof(T) == typeof(byte)) - { - return (SentryMetric?)(object?)_beforeSendMetricByte.Invoke((SentryMetric)(object)metric); - } - if (typeof(T) == typeof(short)) - { - return (SentryMetric?)(object?)_beforeSendMetricInt16.Invoke((SentryMetric)(object)metric); - } - if (typeof(T) == typeof(int)) - { - return (SentryMetric?)(object?)_beforeSendMetricInt32.Invoke((SentryMetric)(object)metric); - } - if (typeof(T) == typeof(long)) - { - return (SentryMetric?)(object?)_beforeSendMetricInt64.Invoke((SentryMetric)(object)metric); - } - if (typeof(T) == typeof(float)) - { - return (SentryMetric?)(object?)_beforeSendMetricSingle.Invoke((SentryMetric)(object)metric); - } - if (typeof(T) == typeof(double)) - { - return (SentryMetric?)(object?)_beforeSendMetricDouble.Invoke((SentryMetric)(object)metric); - } - if (typeof(T) == typeof(decimal)) - { - return (SentryMetric?)(object?)_beforeSendMetricDecimal.Invoke((SentryMetric)(object)metric); - } - - System.Diagnostics.Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); - return null; - } - } -} diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 6db285068a..3f6bc689e4 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -31,7 +31,7 @@ namespace Sentry; #if __MOBILE__ public partial class SentryOptions #else -public partial class SentryOptions +public class SentryOptions #endif { private Dictionary? _defaultTags; @@ -1370,6 +1370,8 @@ public SentryOptions() ); NetworkStatusListener = new PollingNetworkStatusListener(this); + + Experimental = new ExperimentalSentryOptions(this); } /// @@ -1912,7 +1914,7 @@ internal static List GetDefaultInAppExclude() => /// /// Experimental features are subject to binary, source and behavioral breaking changes in future updates. /// - public ExperimentalSentryOptions Experimental { get; } = new(); + public ExperimentalSentryOptions Experimental { get; } /// /// Sentry features that are currently in an experimental state. @@ -1922,13 +1924,15 @@ internal static List GetDefaultInAppExclude() => /// public class ExperimentalSentryOptions { - private TraceMetricsCallbacks? _beforeSendMetric; + private readonly SentryOptions _options; + private SentryTraceMetricsCallbacks? _beforeSendMetric; - internal ExperimentalSentryOptions() + internal ExperimentalSentryOptions(SentryOptions options) { + _options = options; } - internal TraceMetricsCallbacks? BeforeSendMetricInternal => _beforeSendMetric; + internal SentryTraceMetricsCallbacks? BeforeSendMetricInternal => _beforeSendMetric; /// /// When set to , the SDK does not generate and send metrics to Sentry via . @@ -1938,18 +1942,20 @@ internal ExperimentalSentryOptions() public bool EnableMetrics { get; set; } = true; /// - /// Sets a callback function to be invoked before sending the metric to Sentry. + /// Sets a callback function to be invoked before sending the metric of numeric type to Sentry. /// When the delegate throws an during invocation, the metric will not be captured. /// + /// The numeric type of the metric. /// /// It can be used to modify the metric object before being sent to Sentry. /// To prevent the metric from being sent to Sentry, return . + /// Supported numeric value types for are , , , , , and . /// /// public void SetBeforeSendMetric(Func, SentryMetric?> beforeSendMetric) where T : struct { - _beforeSendMetric ??= new TraceMetricsCallbacks(); - _beforeSendMetric.Set(beforeSendMetric); + _beforeSendMetric ??= new SentryTraceMetricsCallbacks(); + _beforeSendMetric.Set(beforeSendMetric, _options.DiagnosticLogger); } } } diff --git a/src/Sentry/SentryTraceMetrics.Counter.cs b/src/Sentry/SentryTraceMetrics.Counter.cs index 0e43e2c84e..60f87f2d78 100644 --- a/src/Sentry/SentryTraceMetrics.Counter.cs +++ b/src/Sentry/SentryTraceMetrics.Counter.cs @@ -8,7 +8,8 @@ public abstract partial class SentryTraceMetrics /// The name of the metric. /// The value of the metric. /// The numeric type of the metric. - public void AddCounter(string name, T value) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value) where T : struct { CaptureMetric(SentryMetricType.Counter, name, value, null, [], null); } @@ -20,7 +21,8 @@ public void AddCounter(string name, T value) where T : struct /// The value of the metric. /// The scope to capture the metric with. /// The numeric type of the metric. - public void AddCounter(string name, T value, Scope? scope) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Counter, name, value, null, [], scope); } @@ -33,7 +35,8 @@ public void AddCounter(string name, T value, Scope? scope) where T : struct /// A dictionary of attributes (key-value pairs with type information). /// The scope to capture the metric with. /// The numeric type of the metric. - public void AddCounter(string name, T value, IEnumerable>? attributes, Scope? scope = null) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); } @@ -46,7 +49,8 @@ public void AddCounter(string name, T value, IEnumerableA dictionary of attributes (key-value pairs with type information). /// The scope to capture the metric with. /// The numeric type of the metric. - public void AddCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); } diff --git a/src/Sentry/SentryTraceMetrics.Distribution.cs b/src/Sentry/SentryTraceMetrics.Distribution.cs index 2a27e1f1ec..b31d9bd626 100644 --- a/src/Sentry/SentryTraceMetrics.Distribution.cs +++ b/src/Sentry/SentryTraceMetrics.Distribution.cs @@ -8,7 +8,8 @@ public abstract partial class SentryTraceMetrics /// The name of the metric. /// The value of the metric. /// The numeric type of the metric. - public void RecordDistribution(string name, T value) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, null, [], null); } @@ -20,7 +21,8 @@ public void RecordDistribution(string name, T value) where T : struct /// The value of the metric. /// The unit of measurement. /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null); } @@ -33,7 +35,8 @@ public void RecordDistribution(string name, T value, string? unit) where T : /// The unit of measurement. /// The scope to capture the metric with. /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit, Scope? scope) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); } @@ -47,7 +50,8 @@ public void RecordDistribution(string name, T value, string? unit, Scope? sco /// A dictionary of attributes (key-value pairs with type information). /// The scope to capture the metric with. /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); } @@ -61,7 +65,8 @@ public void RecordDistribution(string name, T value, string? unit, IEnumerabl /// A dictionary of attributes (key-value pairs with type information). /// The scope to capture the metric with. /// The numeric type of the metric. - public void RecordDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); } diff --git a/src/Sentry/SentryTraceMetrics.Gauge.cs b/src/Sentry/SentryTraceMetrics.Gauge.cs index 1cc15fdcb7..7d46ab1f90 100644 --- a/src/Sentry/SentryTraceMetrics.Gauge.cs +++ b/src/Sentry/SentryTraceMetrics.Gauge.cs @@ -8,7 +8,8 @@ public abstract partial class SentryTraceMetrics /// The name of the metric. /// The value of the metric. /// The numeric type of the metric. - public void RecordGauge(string name, T value) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, null, [], null); } @@ -20,7 +21,8 @@ public void RecordGauge(string name, T value) where T : struct /// The value of the metric. /// The unit of measurement. /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], null); } @@ -33,7 +35,8 @@ public void RecordGauge(string name, T value, string? unit) where T : struct /// The unit of measurement. /// The scope to capture the metric with. /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit, Scope? scope) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope); } @@ -47,7 +50,8 @@ public void RecordGauge(string name, T value, string? unit, Scope? scope) whe /// A dictionary of attributes (key-value pairs with type information). /// The scope to capture the metric with. /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); } @@ -61,7 +65,8 @@ public void RecordGauge(string name, T value, string? unit, IEnumerableA dictionary of attributes (key-value pairs with type information). /// The scope to capture the metric with. /// The numeric type of the metric. - public void RecordGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); } diff --git a/src/Sentry/SentryTraceMetricsCallbacks.cs b/src/Sentry/SentryTraceMetricsCallbacks.cs new file mode 100644 index 0000000000..d8da174890 --- /dev/null +++ b/src/Sentry/SentryTraceMetricsCallbacks.cs @@ -0,0 +1,98 @@ +using Sentry.Extensibility; + +namespace Sentry; + +#if NET6_0_OR_GREATER +/// +/// Similar to . +/// +#endif +internal sealed class SentryTraceMetricsCallbacks +{ + private Func, SentryMetric?> _beforeSendMetricByte; + private Func, SentryMetric?> _beforeSendMetricInt16; + private Func, SentryMetric?> _beforeSendMetricInt32; + private Func, SentryMetric?> _beforeSendMetricInt64; + private Func, SentryMetric?> _beforeSendMetricSingle; + private Func, SentryMetric?> _beforeSendMetricDouble; + + internal SentryTraceMetricsCallbacks() + { + _beforeSendMetricByte = static traceMetric => traceMetric; + _beforeSendMetricInt16 = static traceMetric => traceMetric; + _beforeSendMetricInt32 = static traceMetric => traceMetric; + _beforeSendMetricInt64 = static traceMetric => traceMetric; + _beforeSendMetricSingle = static traceMetric => traceMetric; + _beforeSendMetricDouble = static traceMetric => traceMetric; + } + + internal void Set(Func, SentryMetric?> beforeSendMetric, IDiagnosticLogger? diagnosticLogger) where T : struct + { + beforeSendMetric ??= static traceMetric => traceMetric; + + if (typeof(T) == typeof(long)) + { + _beforeSendMetricInt64 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(double)) + { + _beforeSendMetricDouble = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(int)) + { + _beforeSendMetricInt32 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(float)) + { + _beforeSendMetricSingle = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(short)) + { + _beforeSendMetricInt16 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(byte)) + { + _beforeSendMetricByte = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else + { + diagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); + } + } + + internal SentryMetric? Invoke(SentryMetric metric) where T : struct + { + if (typeof(T) == typeof(long)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt64.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(double)) + { + return (SentryMetric?)(object?)_beforeSendMetricDouble.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(int)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt32.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(float)) + { + return (SentryMetric?)(object?)_beforeSendMetricSingle.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(short)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt16.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(byte)) + { + return (SentryMetric?)(object?)_beforeSendMetricByte.Invoke((SentryMetric)(object)metric); + } + + Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); + return null; + } +} diff --git a/test/Sentry.Testing/InMemorySentryTraceMetrics.cs b/test/Sentry.Testing/InMemorySentryTraceMetrics.cs index c7d8c4e5f7..4a2aa73cda 100644 --- a/test/Sentry.Testing/InMemorySentryTraceMetrics.cs +++ b/test/Sentry.Testing/InMemorySentryTraceMetrics.cs @@ -55,6 +55,12 @@ private MetricEntry(SentryMetricType type, string name, object value, string? un public ImmutableDictionary Attributes { get; } public Scope? Scope { get; } + public void AssertEqual(SentryMetricType type, string name, object value) + { + var expected = Create(type, name, value, null, null, null); + Assert.Equal(expected, this); + } + public void AssertEqual(SentryMetricType type, string name, object value, string? unit, IEnumerable>? attributes, Scope? scope) { var expected = Create(type, name, value, unit, attributes, scope); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 9af07b8032..bdece520b6 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -197,7 +197,7 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); @@ -677,7 +677,6 @@ namespace Sentry public sealed class SentryMetric where T : struct { - public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } [System.Runtime.CompilerServices.RequiredMember] public string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -690,7 +689,9 @@ namespace Sentry public string? Unit { get; init; } [System.Runtime.CompilerServices.RequiredMember] public T Value { get; init; } - public void SetAttribute(string key, object value) { } + public void SetAttribute(string key, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } } public enum SentryMonitorInterval { @@ -1053,37 +1054,37 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value) + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, Sentry.Scope? scope) + public void EmitCounter(string name, T value, Sentry.Scope? scope) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; - protected abstract void Flush(); - public void RecordDistribution(string name, T value) + public void EmitDistribution(string name, T value) where T : struct { } - public void RecordDistribution(string name, T value, string? unit) + public void EmitDistribution(string name, T value, string? unit) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value) + public void EmitGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit) + public void EmitGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + protected abstract void Flush(); } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { @@ -1459,7 +1460,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } @@ -1508,7 +1509,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 9af07b8032..bdece520b6 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -197,7 +197,7 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); @@ -677,7 +677,6 @@ namespace Sentry public sealed class SentryMetric where T : struct { - public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } [System.Runtime.CompilerServices.RequiredMember] public string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -690,7 +689,9 @@ namespace Sentry public string? Unit { get; init; } [System.Runtime.CompilerServices.RequiredMember] public T Value { get; init; } - public void SetAttribute(string key, object value) { } + public void SetAttribute(string key, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } } public enum SentryMonitorInterval { @@ -1053,37 +1054,37 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value) + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, Sentry.Scope? scope) + public void EmitCounter(string name, T value, Sentry.Scope? scope) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; - protected abstract void Flush(); - public void RecordDistribution(string name, T value) + public void EmitDistribution(string name, T value) where T : struct { } - public void RecordDistribution(string name, T value, string? unit) + public void EmitDistribution(string name, T value, string? unit) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value) + public void EmitGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit) + public void EmitGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + protected abstract void Flush(); } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { @@ -1459,7 +1460,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } @@ -1508,7 +1509,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 9af07b8032..bdece520b6 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -197,7 +197,7 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] Sentry.SentryTraceMetrics Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); @@ -677,7 +677,6 @@ namespace Sentry public sealed class SentryMetric where T : struct { - public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } [System.Runtime.CompilerServices.RequiredMember] public string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -690,7 +689,9 @@ namespace Sentry public string? Unit { get; init; } [System.Runtime.CompilerServices.RequiredMember] public T Value { get; init; } - public void SetAttribute(string key, object value) { } + public void SetAttribute(string key, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } } public enum SentryMonitorInterval { @@ -1053,37 +1054,37 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value) + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, Sentry.Scope? scope) + public void EmitCounter(string name, T value, Sentry.Scope? scope) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; - protected abstract void Flush(); - public void RecordDistribution(string name, T value) + public void EmitDistribution(string name, T value) where T : struct { } - public void RecordDistribution(string name, T value, string? unit) + public void EmitDistribution(string name, T value, string? unit) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value) + public void EmitGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit) + public void EmitGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + protected abstract void Flush(); } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { @@ -1459,7 +1460,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] public Sentry.SentryTraceMetrics Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } @@ -1508,7 +1509,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")] + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] public Sentry.SentryTraceMetrics Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index cd46b85b55..e441a4c59f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -657,7 +657,6 @@ namespace Sentry public sealed class SentryMetric where T : struct { - public System.Collections.Generic.IReadOnlyDictionary Attributes { get; } public string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } public System.DateTimeOffset Timestamp { get; init; } @@ -665,7 +664,9 @@ namespace Sentry public Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } public T Value { get; init; } - public void SetAttribute(string key, object value) { } + public void SetAttribute(string key, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } } public enum SentryMonitorInterval { @@ -1022,37 +1023,37 @@ namespace Sentry } public abstract class SentryTraceMetrics { - public void AddCounter(string name, T value) + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) where T : struct { } - public void AddCounter(string name, T value, Sentry.Scope? scope) + public void EmitCounter(string name, T value, Sentry.Scope? scope) where T : struct { } - public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; - protected abstract void Flush(); - public void RecordDistribution(string name, T value) + public void EmitDistribution(string name, T value) where T : struct { } - public void RecordDistribution(string name, T value, string? unit) + public void EmitDistribution(string name, T value, string? unit) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value) + public void EmitGauge(string name, T value) where T : struct { } - public void RecordGauge(string name, T value, string? unit) + public void EmitGauge(string name, T value, string? unit) where T : struct { } - public void RecordGauge(string name, T value, string? unit, Sentry.Scope? scope) + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } - public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + protected abstract void Flush(); } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { From 5228fbe60d2be365b6fdb8f04336dc08fb6ddd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:21:12 +0100 Subject: [PATCH 07/26] feat(metrics): add SentryMetricUnits class for supported units Provides hierarchical constants for metric units supported by Sentry Relay, organized into Duration, Information, and Fraction categories. Co-Authored-By: Claude Sonnet 4.5 --- src/Sentry/SentryMetricUnits.cs | 151 ++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/Sentry/SentryMetricUnits.cs diff --git a/src/Sentry/SentryMetricUnits.cs b/src/Sentry/SentryMetricUnits.cs new file mode 100644 index 0000000000..2f7da07d6e --- /dev/null +++ b/src/Sentry/SentryMetricUnits.cs @@ -0,0 +1,151 @@ +namespace Sentry; + +/// +/// Provides constants for metric units supported by Sentry. +/// +/// +/// While the SDK accepts any string value for metric units, these constants provide +/// type-safe access to units that are officially supported by Sentry's Relay and platform. +/// +/// +/// +public static class SentryMetricUnits +{ + /// + /// Duration units for time measurements. + /// + public static class Duration + { + /// + /// Nanosecond unit (ns). + /// + public static string Nanosecond { get; } = "nanosecond"; + + /// + /// Microsecond unit (μs). + /// + public static string Microsecond { get; } = "microsecond"; + + /// + /// Millisecond unit (ms). + /// + public static string Millisecond { get; } = "millisecond"; + + /// + /// Second unit (s). + /// + public static string Second { get; } = "second"; + + /// + /// Minute unit (min). + /// + public static string Minute { get; } = "minute"; + + /// + /// Hour unit (h). + /// + public static string Hour { get; } = "hour"; + + /// + /// Day unit (d). + /// + public static string Day { get; } = "day"; + + /// + /// Week unit (wk). + /// + public static string Week { get; } = "week"; + } + + /// + /// Information units for data storage and transfer measurements. + /// + public static class Information + { + /// + /// Bit unit (b). + /// + public static string Bit { get; } = "bit"; + + /// + /// Byte unit (B). + /// + public static string Byte { get; } = "byte"; + + /// + /// Kilobyte unit (KB) - 1,000 bytes (decimal). + /// + public static string Kilobyte { get; } = "kilobyte"; + + /// + /// Kibibyte unit (KiB) - 1,024 bytes (binary). + /// + public static string Kibibyte { get; } = "kibibyte"; + + /// + /// Megabyte unit (MB) - 1,000,000 bytes (decimal). + /// + public static string Megabyte { get; } = "megabyte"; + + /// + /// Mebibyte unit (MiB) - 1,048,576 bytes (binary). + /// + public static string Mebibyte { get; } = "mebibyte"; + + /// + /// Gigabyte unit (GB) - 1,000,000,000 bytes (decimal). + /// + public static string Gigabyte { get; } = "gigabyte"; + + /// + /// Gibibyte unit (GiB) - 1,073,741,824 bytes (binary). + /// + public static string Gibibyte { get; } = "gibibyte"; + + /// + /// Terabyte unit (TB) - 1,000,000,000,000 bytes (decimal). + /// + public static string Terabyte { get; } = "terabyte"; + + /// + /// Tebibyte unit (TiB) - 1,099,511,627,776 bytes (binary). + /// + public static string Tebibyte { get; } = "tebibyte"; + + /// + /// Petabyte unit (PB) - 1,000,000,000,000,000 bytes (decimal). + /// + public static string Petabyte { get; } = "petabyte"; + + /// + /// Pebibyte unit (PiB) - 1,125,899,906,842,624 bytes (binary). + /// + public static string Pebibyte { get; } = "pebibyte"; + + /// + /// Exabyte unit (EB) - 1,000,000,000,000,000,000 bytes (decimal). + /// + public static string Exabyte { get; } = "exabyte"; + + /// + /// Exbibyte unit (EiB) - 1,152,921,504,606,846,976 bytes (binary). + /// + public static string Exbibyte { get; } = "exbibyte"; + } + + /// + /// Fraction units for ratio and percentage measurements. + /// + public static class Fraction + { + /// + /// Ratio unit (unitless fraction). + /// + public static string Ratio { get; } = "ratio"; + + /// + /// Percent unit (%). + /// + public static string Percent { get; } = "percent"; + } +} From 32caf06bd729212b33d54096d82f7295be8c9fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:52:50 +0100 Subject: [PATCH 08/26] Update SentryUnits.cs --- .../{SentryMetricUnits.cs => SentryUnits.cs} | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) rename src/Sentry/{SentryMetricUnits.cs => SentryUnits.cs} (58%) diff --git a/src/Sentry/SentryMetricUnits.cs b/src/Sentry/SentryUnits.cs similarity index 58% rename from src/Sentry/SentryMetricUnits.cs rename to src/Sentry/SentryUnits.cs index 2f7da07d6e..f4c1ee51b0 100644 --- a/src/Sentry/SentryMetricUnits.cs +++ b/src/Sentry/SentryUnits.cs @@ -1,33 +1,37 @@ namespace Sentry; /// -/// Provides constants for metric units supported by Sentry. +/// Supported units by Relay and Sentry. +/// Applies to , as well as attributes of and attributes of . /// /// -/// While the SDK accepts any string value for metric units, these constants provide -/// type-safe access to units that are officially supported by Sentry's Relay and platform. +/// Contains the units currently supported by Relay and Sentry. +/// For Metrics and Attributes, currently, custom units are not allowed and will be set to "None" by Relay. /// -/// /// -public static class SentryMetricUnits +/// +public static class SentryUnits { /// - /// Duration units for time measurements. + /// Duration Units. /// public static class Duration { /// /// Nanosecond unit (ns). + /// 10^-9 seconds. /// public static string Nanosecond { get; } = "nanosecond"; /// /// Microsecond unit (μs). + /// 10^-6 seconds. /// public static string Microsecond { get; } = "microsecond"; /// /// Millisecond unit (ms). + /// 10^-3 seconds. /// public static string Millisecond { get; } = "millisecond"; @@ -38,113 +42,136 @@ public static class Duration /// /// Minute unit (min). + /// 60 seconds. /// public static string Minute { get; } = "minute"; /// /// Hour unit (h). + /// 3_600 seconds. /// public static string Hour { get; } = "hour"; /// /// Day unit (d). + /// 86_400 seconds. /// public static string Day { get; } = "day"; /// /// Week unit (wk). + /// 604_800 seconds. /// public static string Week { get; } = "week"; } /// - /// Information units for data storage and transfer measurements. + /// Information Units. /// + /// + /// Note that there are computer systems with a different number of bits per byte. + /// public static class Information { /// /// Bit unit (b). + /// 1/8 of a byte. /// public static string Bit { get; } = "bit"; /// /// Byte unit (B). + /// 8 bits. /// public static string Byte { get; } = "byte"; /// - /// Kilobyte unit (KB) - 1,000 bytes (decimal). + /// Kilobyte unit (kB). + /// 10^3 bytes = 1_000 bytes (decimal). /// public static string Kilobyte { get; } = "kilobyte"; /// - /// Kibibyte unit (KiB) - 1,024 bytes (binary). + /// Kibibyte unit (KiB). + /// 2^10 bytes = 1_024 bytes (binary). /// public static string Kibibyte { get; } = "kibibyte"; /// - /// Megabyte unit (MB) - 1,000,000 bytes (decimal). + /// Megabyte unit (MB). + /// 10^6 bytes = 1_000_000 bytes (decimal). /// public static string Megabyte { get; } = "megabyte"; /// - /// Mebibyte unit (MiB) - 1,048,576 bytes (binary). + /// Mebibyte unit (MiB). + /// 2^20 bytes = 1_048_576 bytes (binary). /// public static string Mebibyte { get; } = "mebibyte"; /// - /// Gigabyte unit (GB) - 1,000,000,000 bytes (decimal). + /// Gigabyte unit (GB). + /// 10^9 bytes = 1_000_000_000 bytes (decimal). /// public static string Gigabyte { get; } = "gigabyte"; /// - /// Gibibyte unit (GiB) - 1,073,741,824 bytes (binary). + /// Gibibyte unit (GiB). + /// 2^30 bytes = 1_073_741_824 bytes (binary). /// public static string Gibibyte { get; } = "gibibyte"; /// - /// Terabyte unit (TB) - 1,000,000,000,000 bytes (decimal). + /// Terabyte unit (TB). + /// 10^12 bytes = 1_000_000_000_000 bytes (decimal). /// public static string Terabyte { get; } = "terabyte"; /// - /// Tebibyte unit (TiB) - 1,099,511,627,776 bytes (binary). + /// Tebibyte unit (TiB). + /// 2^40 bytes = 1_099_511_627_776 bytes (binary). /// public static string Tebibyte { get; } = "tebibyte"; /// - /// Petabyte unit (PB) - 1,000,000,000,000,000 bytes (decimal). + /// Petabyte unit (PB). + /// 10^15 bytes = 1_000_000_000_000_000 bytes (decimal). /// public static string Petabyte { get; } = "petabyte"; /// - /// Pebibyte unit (PiB) - 1,125,899,906,842,624 bytes (binary). + /// Pebibyte unit (PiB). + /// 2^50 bytes = 1_125_899_906_842_624 bytes (binary). /// public static string Pebibyte { get; } = "pebibyte"; /// - /// Exabyte unit (EB) - 1,000,000,000,000,000,000 bytes (decimal). + /// Exabyte unit (EB). + /// 10^18 bytes = 1_000_000_000_000_000_000 bytes (decimal). /// public static string Exabyte { get; } = "exabyte"; /// - /// Exbibyte unit (EiB) - 1,152,921,504,606,846,976 bytes (binary). + /// Exbibyte unit (EiB). + /// 2^60 bytes = 1_152_921_504_606_846_976 bytes (binary). /// public static string Exbibyte { get; } = "exbibyte"; } /// - /// Fraction units for ratio and percentage measurements. + /// Fraction Units. /// public static class Fraction { /// - /// Ratio unit (unitless fraction). + /// Ratio unit. + /// Floating point fraction of 1. /// public static string Ratio { get; } = "ratio"; /// /// Percent unit (%). + /// Ratio expressed as a fraction of 100. 100% equals a ratio of 1.0. /// public static string Percent { get; } = "percent"; } From d7304323bcbafede587bda49e91c3145099ee4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:39:27 +0100 Subject: [PATCH 09/26] update API Approval Tests --- ...iApprovalTests.Run.DotNet10_0.verified.txt | 36 +++++++++++++++++++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 36 +++++++++++++++++++ ...piApprovalTests.Run.DotNet9_0.verified.txt | 36 +++++++++++++++++++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 36 +++++++++++++++++++ 4 files changed, 144 insertions(+) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index bdece520b6..3a08f11ba4 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -1135,6 +1135,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index bdece520b6..3a08f11ba4 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -1135,6 +1135,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index bdece520b6..3a08f11ba4 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -1135,6 +1135,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index e441a4c59f..eeb409de69 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -1104,6 +1104,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } From cc43b385a4482892d1cc16345639b004c92daaa2 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 16 Jan 2026 16:19:18 +0000 Subject: [PATCH 10/26] release: 6.1.0-alpha.2 --- CHANGELOG.md | 2 +- Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a82b5f3071..60e12e5792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 6.1.0-alpha.2 ### BREAKING CHANGES diff --git a/Directory.Build.props b/Directory.Build.props index 7e15f5723e..8e8200f34c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.1.0-alpha.1 + 6.1.0-alpha.2 13 true true From 7e6dbb98774eda96522fd6f98fc10bbb5ea4ebef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:30:23 +0100 Subject: [PATCH 11/26] revert: VersionPrefix to main Co-authored-by: James Crosswell --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8e8200f34c..80f21ce37d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.1.0-alpha.2 + 6.0.0 13 true true From 31dd7bc10d0071dcdd25de9682bd034881347aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:37:05 +0100 Subject: [PATCH 12/26] docs: clean up Changelog --- CHANGELOG.md | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b495299e..6886b8060f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,11 @@ # Changelog -## 6.1.0-alpha.2 - -### BREAKING CHANGES - -- Rename [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) APIs to avoid implying aggregation ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) - - from `AddCounter` to `EmitCounter` - - from `RecordDistribution` to `EmitDistribution` - - from `RecordGauge` to `EmitGauge` - -### Features - -- Validate [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) - -### Fixes - -- Attributes for [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) set via `SetBeforeSendLog` callback ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) -- Disallow unsupported 128-bit floating point numbers (i.e. `decimal`) for [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) - -## 6.1.0-alpha.1 +## Unreleased ### Features -- Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807)) - Add _experimental_ support for [Sentry trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) +- Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807)) ### Fixes @@ -34,11 +16,11 @@ ### Dependencies - Bump Native SDK from v0.12.2 to v0.12.3 ([#4832](https://github.com/getsentry/sentry-dotnet/pull/4832)) - - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0123) - - [diff](https://github.com/getsentry/sentry-native/compare/0.12.2...0.12.3) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0123) + - [diff](https://github.com/getsentry/sentry-native/compare/0.12.2...0.12.3) - Bump Java SDK from v8.28.0 to v8.29.0 ([#4817](https://github.com/getsentry/sentry-dotnet/pull/4817)) - - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8290) - - [diff](https://github.com/getsentry/sentry-java/compare/8.28.0...8.29.0) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8290) + - [diff](https://github.com/getsentry/sentry-java/compare/8.28.0...8.29.0) ## 6.0.0 From 6f3a78557816a40995fff3d7dabae96e44998586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:52:58 +0100 Subject: [PATCH 13/26] docs: fix indentation of Changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6886b8060f..548168ed51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,11 @@ ### Dependencies - Bump Native SDK from v0.12.2 to v0.12.3 ([#4832](https://github.com/getsentry/sentry-dotnet/pull/4832)) - - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0123) - - [diff](https://github.com/getsentry/sentry-native/compare/0.12.2...0.12.3) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0123) + - [diff](https://github.com/getsentry/sentry-native/compare/0.12.2...0.12.3) - Bump Java SDK from v8.28.0 to v8.29.0 ([#4817](https://github.com/getsentry/sentry-dotnet/pull/4817)) - - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8290) - - [diff](https://github.com/getsentry/sentry-java/compare/8.28.0...8.29.0) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8290) + - [diff](https://github.com/getsentry/sentry-java/compare/8.28.0...8.29.0) ## 6.0.0 From 40a163515908b85c65f564fed09f825af9235999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:29:04 +0100 Subject: [PATCH 14/26] test(metrics): Trace-connected Metrics (Tests) (#4839) --- .../Extensibility/DisabledHubTests.cs | 4 + .../Extensibility/HubAdapterTests.cs | 12 + test/Sentry.Tests/HubTests.cs | 118 ++++ .../Sentry.Tests/Protocol/TraceMetricTests.cs | 58 ++ test/Sentry.Tests/SentryLogTests.cs | 2 +- test/Sentry.Tests/SentryMetricTests.cs | 542 ++++++++++++++++++ test/Sentry.Tests/SentryMetricTypeTests.cs | 55 ++ .../SentryStructuredLoggerTests.cs | 2 +- .../SentryTraceMetricsTests.Options.cs | 169 ++++++ .../SentryTraceMetricsTests.Types.cs | 329 +++++++++++ test/Sentry.Tests/SentryTraceMetricsTests.cs | 276 +++++++++ 11 files changed, 1565 insertions(+), 2 deletions(-) create mode 100644 test/Sentry.Tests/Protocol/TraceMetricTests.cs create mode 100644 test/Sentry.Tests/SentryMetricTests.cs create mode 100644 test/Sentry.Tests/SentryMetricTypeTests.cs create mode 100644 test/Sentry.Tests/SentryTraceMetricsTests.Options.cs create mode 100644 test/Sentry.Tests/SentryTraceMetricsTests.Types.cs create mode 100644 test/Sentry.Tests/SentryTraceMetricsTests.cs diff --git a/test/Sentry.Tests/Extensibility/DisabledHubTests.cs b/test/Sentry.Tests/Extensibility/DisabledHubTests.cs index e03f8a82a3..084c5e220b 100644 --- a/test/Sentry.Tests/Extensibility/DisabledHubTests.cs +++ b/test/Sentry.Tests/Extensibility/DisabledHubTests.cs @@ -39,4 +39,8 @@ public void CaptureEvent_EmptyGuid() [Fact] public void Logger_IsDisabled() => Assert.IsType(DisabledHub.Instance.Logger); + + [Fact] + public void Metrics_IsDisabled() + => Assert.IsType(DisabledHub.Instance.Metrics); } diff --git a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs index 26702163cf..2a11f29bea 100644 --- a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs +++ b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs @@ -82,6 +82,18 @@ public void Logger_MockInvoked() element => element.AssertEqual(SentryLogLevel.Warning, "Message")); } + [Fact] + public void Metrics_MockInvoked() + { + var metrics = new InMemorySentryTraceMetrics(); + Hub.Metrics.Returns(metrics); + + HubAdapter.Instance.Metrics.EmitCounter("sentry_tests.hub_adapter_tests.counter", 1); + + Assert.Collection(metrics.Entries, + element => element.AssertEqual(SentryMetricType.Counter, "sentry_tests.hub_adapter_tests.counter", 1)); + } + [Fact] public void EndSession_CrashedStatus_MockInvoked() { diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 17c3474705..395544c947 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1879,6 +1879,124 @@ public void Logger_Dispose_DoesCaptureLog() hub.Logger.Should().BeOfType(); } + [Fact] + public void Metrics_IsDisabled_DoesNotCaptureMetric() + { + // Arrange + _fixture.Options.Experimental.EnableMetrics = false; + var hub = _fixture.GetSut(); + + // Act + hub.Metrics.EmitCounter("sentry_tests.hub_tests.counter", 1); + hub.Metrics.Flush(); + + // Assert + _fixture.Client.Received(0).CaptureEnvelope( + Arg.Is(envelope => + envelope.Items.Single(item => item.Header["type"].Equals("trace_metric")).Payload.GetType().IsAssignableFrom(typeof(JsonSerializable)) + ) + ); + hub.Metrics.Should().BeOfType(); + } + + [Fact] + public void Metrics_IsEnabled_DoesCaptureMetric() + { + // Arrange + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var hub = _fixture.GetSut(); + + // Act + hub.Metrics.EmitCounter("sentry_tests.hub_tests.counter", 1); + hub.Metrics.Flush(); + + // Assert + _fixture.Client.Received(1).CaptureEnvelope( + Arg.Is(envelope => + envelope.Items.Single(item => item.Header["type"].Equals("trace_metric")).Payload.GetType().IsAssignableFrom(typeof(JsonSerializable)) + ) + ); + hub.Metrics.Should().BeOfType(); + } + + [Fact] + public void Metrics_EnableAfterCreate_HasNoEffect() + { + // Arrange + _fixture.Options.Experimental.EnableMetrics = false; + var hub = _fixture.GetSut(); + + // Act + _fixture.Options.Experimental.EnableMetrics = true; + + // Assert + hub.Metrics.Should().BeOfType(); + } + + [Fact] + public void Metrics_DisableAfterCreate_HasNoEffect() + { + // Arrange + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var hub = _fixture.GetSut(); + + // Act + _fixture.Options.Experimental.EnableMetrics = false; + + // Assert + hub.Metrics.Should().BeOfType(); + } + + [Fact] + public async Task Metrics_FlushAsync_DoesCaptureMetric() + { + // Arrange + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var hub = _fixture.GetSut(); + + // Act + hub.Metrics.EmitCounter("sentry_tests.hub_tests.counter", 1); + await hub.FlushAsync(); + + // Assert + _fixture.Client.Received(1).CaptureEnvelope( + Arg.Is(envelope => + envelope.Items.Single(item => item.Header["type"].Equals("trace_metric")).Payload.GetType().IsAssignableFrom(typeof(JsonSerializable)) + ) + ); + await _fixture.Client.Received(1).FlushAsync( + Arg.Is(timeout => + timeout.Equals(_fixture.Options.FlushTimeout) + ) + ); + hub.Metrics.Should().BeOfType(); + } + + [Fact] + public void Metrics_Dispose_DoesCaptureMetric() + { + // Arrange + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var hub = _fixture.GetSut(); + + // Act + hub.Metrics.EmitCounter("sentry_tests.hub_tests.counter", 1); + hub.Dispose(); + + // Assert + _fixture.Client.Received(1).CaptureEnvelope( + Arg.Is(envelope => + envelope.Items.Single(item => item.Header["type"].Equals("trace_metric")).Payload.GetType().IsAssignableFrom(typeof(JsonSerializable)) + ) + ); + _fixture.Client.Received(1).FlushAsync( + Arg.Is(timeout => + timeout.Equals(_fixture.Options.ShutdownTimeout) + ) + ); + hub.Metrics.Should().BeOfType(); + } + [Fact] public void Dispose_IsEnabled_SetToFalse() { diff --git a/test/Sentry.Tests/Protocol/TraceMetricTests.cs b/test/Sentry.Tests/Protocol/TraceMetricTests.cs new file mode 100644 index 0000000000..222bd1dcb6 --- /dev/null +++ b/test/Sentry.Tests/Protocol/TraceMetricTests.cs @@ -0,0 +1,58 @@ +namespace Sentry.Tests.Protocol; + +/// +/// See . +/// See also . +/// +public class TraceMetricTests +{ + private readonly TestOutputDiagnosticLogger _output; + + public TraceMetricTests(ITestOutputHelper output) + { + _output = new TestOutputDiagnosticLogger(output); + } + + [Fact] + public void Type_IsAssignableFrom_ISentryJsonSerializable() + { + var metric = new TraceMetric([]); + + Assert.IsAssignableFrom(metric); + } + + [Fact] + public void Length_One_Single() + { + var metric = new TraceMetric([CreateMetric()]); + + var length = metric.Length; + + Assert.Equal(1, length); + } + + [Fact] + public void Items_One_Single() + { + var metric = new TraceMetric([CreateMetric()]); + + var items = metric.Items; + + Assert.Equal(1, items.Length); + } + + [Fact] + public void WriteTo_Empty_AsJson() + { + var metric = new TraceMetric([]); + + var document = metric.ToJsonDocument(_output); + + Assert.Equal("""{"items":[]}""", document.RootElement.ToString()); + } + + private static SentryMetric CreateMetric() + { + return new SentryMetric(DateTimeOffset.MinValue, SentryId.Empty, SentryMetricType.Counter, "sentry_tests.trace_metric_tests.counter", 1); + } +} diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index fe14705ad1..0901898680 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -33,7 +33,7 @@ public void Protocol_Default_VerifyAttributes() var sdk = new SdkVersion { Name = "Sentry.Test.SDK", - Version = "1.2.3-test+Sentry" + Version = "1.2.3-test+Sentry", }; var log = new SentryLog(Timestamp, TraceId, (SentryLogLevel)24, "message") diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs new file mode 100644 index 0000000000..ecfaf81056 --- /dev/null +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -0,0 +1,542 @@ +using System.Text.Encodings.Web; +using Sentry.PlatformAbstractions; + +namespace Sentry.Tests; + +/// +/// See . +/// See also . +/// +public class SentryMetricTests +{ + private static readonly DateTimeOffset Timestamp = new(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2)); + private static readonly SentryId TraceId = SentryId.Create(); + private static readonly SpanId? SpanId = Sentry.SpanId.Create(); + + private static readonly ISystemClock Clock = new MockClock(Timestamp); + + private readonly TestOutputDiagnosticLogger _output; + + public SentryMetricTests(ITestOutputHelper output) + { + _output = new TestOutputDiagnosticLogger(output); + } + + [Fact] + public void Protocol_Default_VerifyAttributes() + { + var options = new SentryOptions + { + Environment = "my-environment", + Release = "my-release", + }; + var sdk = new SdkVersion + { + Name = "Sentry.Test.SDK", + Version = "1.2.3-test+Sentry", + }; + + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1) + { + SpanId = SpanId, + Unit = "test_unit", + }; + metric.SetAttribute("attribute", "value"); + metric.SetDefaultAttributes(options, sdk); + + metric.Timestamp.Should().Be(Timestamp); + metric.TraceId.Should().Be(TraceId); + metric.Type.Should().Be(SentryMetricType.Counter); + metric.Name.Should().Be("sentry_tests.sentry_metric_tests.counter"); + metric.Value.Should().Be(1); + metric.SpanId.Should().Be(SpanId); + metric.Unit.Should().BeEquivalentTo("test_unit"); + + metric.TryGetAttribute("attribute", out var attribute).Should().BeTrue(); + attribute.Should().Be("value"); + metric.TryGetAttribute("sentry.environment", out var environment).Should().BeTrue(); + environment.Should().Be(options.Environment); + metric.TryGetAttribute("sentry.release", out var release).Should().BeTrue(); + release.Should().Be(options.Release); + metric.TryGetAttribute("sentry.sdk.name", out var name).Should().BeTrue(); + name.Should().Be(sdk.Name); + metric.TryGetAttribute("sentry.sdk.version", out var version).Should().BeTrue(); + version.Should().Be(sdk.Version); + metric.TryGetAttribute("not-found", out var notFound).Should().BeFalse(); + notFound.Should().BeNull(); + } + + [Fact] + public void WriteTo_Envelope_MinimalSerializedSentryMetric() + { + var options = new SentryOptions + { + Environment = "my-environment", + Release = "my-release", + }; + + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + metric.SetDefaultAttributes(options, new SdkVersion()); + + var envelope = Envelope.FromMetric(new TraceMetric([metric])); + + using var stream = new MemoryStream(); + envelope.Serialize(stream, _output, Clock); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var header = JsonDocument.Parse(reader.ReadLine()!); + var item = JsonDocument.Parse(reader.ReadLine()!); + var payload = JsonDocument.Parse(reader.ReadLine()!); + + reader.EndOfStream.Should().BeTrue(); + + header.ToIndentedJsonString().Should().Be($$""" + { + "sdk": { + "name": "{{SdkVersion.Instance.Name}}", + "version": "{{SdkVersion.Instance.Version}}" + }, + "sent_at": "{{Timestamp.Format()}}" + } + """); + + item.ToIndentedJsonString().Should().Match(""" + { + "type": "trace_metric", + "item_count": 1, + "content_type": "application/vnd.sentry.items.trace-metric+json", + "length": ?* + } + """); + + payload.ToIndentedJsonString().Should().Be($$""" + { + "items": [ + { + "timestamp": {{Timestamp.GetTimestamp()}}, + "type": "counter", + "name": "sentry_tests.sentry_metric_tests.counter", + "value": 1, + "trace_id": "{{TraceId.ToString()}}", + "attributes": { + "sentry.environment": { + "value": "my-environment", + "type": "string" + }, + "sentry.release": { + "value": "my-release", + "type": "string" + } + } + } + ] + } + """); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_EnvelopeItem_MaximalSerializedSentryMetric() + { + var options = new SentryOptions + { + Environment = "my-environment", + Release = "my-release", + }; + + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1) + { + SpanId = SpanId, + Unit = "test_unit", + }; + metric.SetAttribute("string-attribute", "string-value"); + metric.SetAttribute("boolean-attribute", true); + metric.SetAttribute("integer-attribute", 3); + metric.SetAttribute("double-attribute", 4.4); + metric.SetDefaultAttributes(options, new SdkVersion { Name = "Sentry.Test.SDK", Version = "1.2.3-test+Sentry" }); + + var envelope = EnvelopeItem.FromMetric(new TraceMetric([metric])); + + using var stream = new MemoryStream(); + envelope.Serialize(stream, _output); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + var item = JsonDocument.Parse(reader.ReadLine()!); + var payload = JsonDocument.Parse(reader.ReadLine()!); + + reader.EndOfStream.Should().BeTrue(); + + item.ToIndentedJsonString().Should().Match(""" + { + "type": "trace_metric", + "item_count": 1, + "content_type": "application/vnd.sentry.items.trace-metric+json", + "length": ?* + } + """); + + payload.ToIndentedJsonString().Should().Be($$""" + { + "items": [ + { + "timestamp": {{Timestamp.GetTimestamp()}}, + "type": "counter", + "name": "sentry_tests.sentry_metric_tests.counter", + "value": 1, + "trace_id": "{{TraceId.ToString()}}", + "span_id": "{{SpanId.ToString()}}", + "unit": "test_unit", + "attributes": { + "string-attribute": { + "value": "string-value", + "type": "string" + }, + "boolean-attribute": { + "value": true, + "type": "boolean" + }, + "integer-attribute": { + "value": 3, + "type": "integer" + }, + "double-attribute": { + "value": {{4.4.Format()}}, + "type": "double" + }, + "sentry.environment": { + "value": "my-environment", + "type": "string" + }, + "sentry.release": { + "value": "my-release", + "type": "string" + }, + "sentry.sdk.name": { + "value": "Sentry.Test.SDK", + "type": "string" + }, + "sentry.sdk.version": { + "value": "1.2.3-test+Sentry", + "type": "string" + } + } + } + ] + } + """); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_NumericValueType_Byte() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var value = document.RootElement.GetProperty("value"); + + value.ValueKind.Should().Be(JsonValueKind.Number); + value.TryGetByte(out var @byte).Should().BeTrue(); + @byte.Should().Be(1); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_NumericValueType_Int16() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var value = document.RootElement.GetProperty("value"); + + value.ValueKind.Should().Be(JsonValueKind.Number); + value.TryGetInt16(out var @short).Should().BeTrue(); + @short.Should().Be(1); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_NumericValueType_Int32() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var value = document.RootElement.GetProperty("value"); + + value.ValueKind.Should().Be(JsonValueKind.Number); + value.TryGetInt32(out var @int).Should().BeTrue(); + @int.Should().Be(1); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_NumericValueType_Int64() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var value = document.RootElement.GetProperty("value"); + + value.ValueKind.Should().Be(JsonValueKind.Number); + value.TryGetInt64(out var @long).Should().BeTrue(); + @long.Should().Be(1L); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_NumericValueType_Single() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var value = document.RootElement.GetProperty("value"); + + value.ValueKind.Should().Be(JsonValueKind.Number); + value.TryGetSingle(out var @float).Should().BeTrue(); + @float.Should().Be(1f); + + _output.Entries.Should().BeEmpty(); + } + + [Fact] + public void WriteTo_NumericValueType_Double() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var value = document.RootElement.GetProperty("value"); + + value.ValueKind.Should().Be(JsonValueKind.Number); + value.TryGetDouble(out var @double).Should().BeTrue(); + @double.Should().Be(1d); + + _output.Entries.Should().BeEmpty(); + } + +#if DEBUG && (NET || NETCOREAPP) + [Fact] + public void WriteTo_NumericValueType_Decimal() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + + var exception = Assert.ThrowsAny(() => metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output)); + exception.Message.Should().Contain($"Unhandled Metric Type {typeof(decimal)}."); + exception.Message.Should().Contain("This instruction should be unreachable."); + + _output.Entries.Should().BeEmpty(); + } +#endif + + [Fact] + public void WriteTo_Attributes_AsJson() + { + var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); + metric.SetAttribute("sbyte", sbyte.MinValue); + metric.SetAttribute("byte", byte.MaxValue); + metric.SetAttribute("short", short.MinValue); + metric.SetAttribute("ushort", ushort.MaxValue); + metric.SetAttribute("int", int.MinValue); + metric.SetAttribute("uint", uint.MaxValue); + metric.SetAttribute("long", long.MinValue); + metric.SetAttribute("ulong", ulong.MaxValue); +#if NET5_0_OR_GREATER + metric.SetAttribute("nint", nint.MinValue); + metric.SetAttribute("nuint", nuint.MaxValue); +#endif + metric.SetAttribute("float", 1f); + metric.SetAttribute("double", 2d); + metric.SetAttribute("decimal", 3m); + metric.SetAttribute("bool", true); + metric.SetAttribute("char", 'c'); + metric.SetAttribute("string", "string"); +#if (NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER) + metric.SetAttribute("object", KeyValuePair.Create("key", "value")); +#else + metric.SetAttribute("object", new KeyValuePair("key", "value")); +#endif + metric.SetAttribute("null", null!); + + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var attributes = document.RootElement.GetProperty("attributes"); + Assert.Collection(attributes.EnumerateObject().ToArray(), + property => property.AssertAttributeInteger("sbyte", json => json.GetSByte(), sbyte.MinValue), + property => property.AssertAttributeInteger("byte", json => json.GetByte(), byte.MaxValue), + property => property.AssertAttributeInteger("short", json => json.GetInt16(), short.MinValue), + property => property.AssertAttributeInteger("ushort", json => json.GetUInt16(), ushort.MaxValue), + property => property.AssertAttributeInteger("int", json => json.GetInt32(), int.MinValue), + property => property.AssertAttributeInteger("uint", json => json.GetUInt32(), uint.MaxValue), + property => property.AssertAttributeInteger("long", json => json.GetInt64(), long.MinValue), + property => property.AssertAttributeString("ulong", json => json.GetString(), ulong.MaxValue.ToString(NumberFormatInfo.InvariantInfo)), +#if NET5_0_OR_GREATER + property => property.AssertAttributeInteger("nint", json => json.GetInt64(), nint.MinValue), + property => property.AssertAttributeString("nuint", json => json.GetString(), nuint.MaxValue.ToString(NumberFormatInfo.InvariantInfo)), +#endif + property => property.AssertAttributeDouble("float", json => json.GetSingle(), 1f), + property => property.AssertAttributeDouble("double", json => json.GetDouble(), 2d), + property => property.AssertAttributeString("decimal", json => json.GetString(), 3m.ToString(NumberFormatInfo.InvariantInfo)), + property => property.AssertAttributeBoolean("bool", json => json.GetBoolean(), true), + property => property.AssertAttributeString("char", json => json.GetString(), "c"), + property => property.AssertAttributeString("string", json => json.GetString(), "string"), + property => property.AssertAttributeString("object", json => json.GetString(), "[key, value]") + ); + Assert.Collection(_output.Entries, + entry => entry.Message.Should().Match("*ulong*is not supported*overflow*"), +#if NET5_0_OR_GREATER + entry => entry.Message.Should().Match("*nuint*is not supported*64-bit*"), +#endif + entry => entry.Message.Should().Match("*decimal*is not supported*overflow*"), + entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*"), + entry => entry.Message.Should().Match("*null*is not supported*ignored*") + ); + } + + [Fact] + public void GetTraceIdAndSpanId_WithActiveSpan_HasBothTraceIdAndSpanId() + { + // Arrange + var span = Substitute.For(); + span.TraceId.Returns(SentryId.Create()); + span.SpanId.Returns(Sentry.SpanId.Create()); + + var hub = Substitute.For(); + hub.GetSpan().Returns(span); + + // Act + SentryMetric.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + // Assert + traceId.Should().Be(span.TraceId); + spanId.Should().Be(span.SpanId); + } + + [Fact] + public void GetTraceIdAndSpanId_WithoutActiveSpan_HasOnlyTraceIdButNoSpanId() + { + // Arrange + var hub = Substitute.For(); + hub.GetSpan().Returns((ISpan)null); + + var scope = new Scope(); + hub.SubstituteConfigureScope(scope); + + // Act + SentryMetric.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + // Assert + traceId.Should().Be(scope.PropagationContext.TraceId); + spanId.Should().BeNull(); + } + + [Fact] + public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable() + { + // Arrange + var hub = Substitute.For(); + hub.GetSpan().Returns((ISpan)null); + + // Act + SentryMetric.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + + // Assert + traceId.Should().Be(SentryId.Empty); + spanId.Should().BeNull(); + } +} + +file static class AssertExtensions +{ + public static void AssertAttributeString(this JsonProperty attribute, string name, Func getValue, T value) + { + attribute.AssertAttribute(name, "string", getValue, value); + } + + public static void AssertAttributeBoolean(this JsonProperty attribute, string name, Func getValue, T value) + { + attribute.AssertAttribute(name, "boolean", getValue, value); + } + + public static void AssertAttributeInteger(this JsonProperty attribute, string name, Func getValue, T value) + { + attribute.AssertAttribute(name, "integer", getValue, value); + } + + public static void AssertAttributeDouble(this JsonProperty attribute, string name, Func getValue, T value) + { + attribute.AssertAttribute(name, "double", getValue, value); + } + + private static void AssertAttribute(this JsonProperty attribute, string name, string type, Func getValue, T value) + { + Assert.Equal(name, attribute.Name); + Assert.Collection(attribute.Value.EnumerateObject().ToArray(), + property => + { + Assert.Equal("value", property.Name); + Assert.Equal(value, getValue(property.Value)); + }, property => + { + Assert.Equal("type", property.Name); + Assert.Equal(type, property.Value.GetString()); + }); + } +} + +file static class DateTimeOffsetExtensions +{ + public static string GetTimestamp(this DateTimeOffset value) + { + var timestamp = value.ToUnixTimeMilliseconds() / 1_000.0; + return timestamp.ToString(NumberFormatInfo.InvariantInfo); + } +} + +file static class JsonFormatterExtensions +{ + public static string Format(this DateTimeOffset value) + { + return value.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo); + } + + public static string Format(this double value) + { + if (SentryRuntime.Current.IsNetFx() || SentryRuntime.Current.IsMono()) + { + // since .NET Core 3.0, the Floating-Point Formatter returns the shortest roundtrippable string, rather than the exact string + // e.g. on .NET Framework (Windows) + // * 2.2.ToString() -> 2.2000000000000002 + // * 4.4.ToString() -> 4.4000000000000004 + // see https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/ + + var utf16Text = value.ToString("G17", NumberFormatInfo.InvariantInfo); + var utf8Bytes = Encoding.UTF8.GetBytes(utf16Text); + return Encoding.UTF8.GetString(utf8Bytes); + } + + return value.ToString(NumberFormatInfo.InvariantInfo); + } +} + +file static class JsonDocumentExtensions +{ + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + private static readonly JsonSerializerOptions Options = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true, + }; + + public static string ToIndentedJsonString(this JsonDocument document) + { + var json = JsonSerializer.Serialize(document, Options); + + // Standardize on \n on all platforms, for consistency in tests. + return IsWindows ? json.Replace("\r\n", "\n") : json; + } +} diff --git a/test/Sentry.Tests/SentryMetricTypeTests.cs b/test/Sentry.Tests/SentryMetricTypeTests.cs new file mode 100644 index 0000000000..cb35155342 --- /dev/null +++ b/test/Sentry.Tests/SentryMetricTypeTests.cs @@ -0,0 +1,55 @@ +namespace Sentry.Tests; + +/// +/// +/// +public class SentryMetricTypeTests +{ + private readonly InMemoryDiagnosticLogger _logger; + + public SentryMetricTypeTests() + { + _logger = new InMemoryDiagnosticLogger(); + } + + [Theory] + [InlineData(SentryMetricType.Counter, "counter")] + [InlineData(SentryMetricType.Gauge, "gauge")] + [InlineData(SentryMetricType.Distribution, "distribution")] + public void Protocol_WithinRange_Valid(SentryMetricType type, string expected) + { +#if NET5_0_OR_GREATER + Assert.True(Enum.IsDefined(type)); +#else + Assert.True(Enum.IsDefined(typeof(SentryMetricType), type)); +#endif + + var actual = type.ToProtocolString(_logger); + + Assert.Equal(expected, actual); + Assert.Empty(_logger.Entries); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + public void Protocol_OutOfRange_Invalid(int value) + { + var type = (SentryMetricType)value; +#if NET5_0_OR_GREATER + Assert.False(Enum.IsDefined(type)); +#else + Assert.False(Enum.IsDefined(typeof(SentryMetricType), type)); +#endif + + var actual = type.ToProtocolString(_logger); + + Assert.Equal("unknown", actual); + var entry = Assert.Single(_logger.Entries); + Assert.Multiple( + () => Assert.Equal(SentryLevel.Debug, entry.Level), + () => Assert.Equal("Metric type {0} is not defined.", entry.Message), + () => Assert.Null(entry.Exception), + () => Assert.Equal([type], entry.Args)); + } +} diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 3a038fd840..9a2def8c9f 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -251,7 +251,7 @@ private static void ConfigureLog(SentryLog log) } } -internal static class AssertionExtensions +internal static class LoggerAssertionExtensions { public static void AssertEnvelope(this SentryStructuredLoggerTests.Fixture fixture, Envelope envelope, SentryLogLevel level) { diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs b/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs new file mode 100644 index 0000000000..ce3e533c02 --- /dev/null +++ b/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs @@ -0,0 +1,169 @@ +#nullable enable + +namespace Sentry.Tests; + +public partial class SentryTraceMetricsTests +{ + [Fact] + public void EnableMetrics_Default_True() + { + var options = new SentryOptions(); + + options.Experimental.EnableMetrics.Should().BeTrue(); + } + + [Fact] + public void BeforeSendMetric_Default_Null() + { + var options = new SentryOptions(); + + options.Experimental.BeforeSendMetricInternal.Should().BeNull(); + } + + [Fact] + public void BeforeSendMetric_Set_NotNull() + { + _fixture.Options.Experimental.SetBeforeSendMetric(Callback.Nop); + + _fixture.Options.Experimental.BeforeSendMetricInternal.Should().NotBeNull(); + } + + [Fact] + public void BeforeSendMetric_SetByte_InvokeDelegate() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Byte), nameof(Byte)); + return metric; + }); + + var metric = CreateCounter(1); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Byte), out var value).Should().BeTrue(); + value.Should().Be(nameof(Byte)); + } + + [Fact] + public void BeforeSendMetric_SetInt16_InvokeDelegate() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Int16), nameof(Int16)); + return metric; + }); + + var metric = CreateCounter(1); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Int16), out var value).Should().BeTrue(); + value.Should().Be(nameof(Int16)); + } + + [Fact] + public void BeforeSendMetric_SetInt32_InvokeDelegate() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Int32), nameof(Int32)); + return metric; + }); + + var metric = CreateCounter(1); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Int32), out var value).Should().BeTrue(); + value.Should().Be(nameof(Int32)); + } + + [Fact] + public void BeforeSendMetric_SetInt64_InvokeDelegate() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Int64), nameof(Int64)); + return metric; + }); + + var metric = CreateCounter(1L); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Int64), out var value).Should().BeTrue(); + value.Should().Be(nameof(Int64)); + } + + [Fact] + public void BeforeSendMetric_SetSingle_InvokeDelegate() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Single), nameof(Single)); + return metric; + }); + + var metric = CreateCounter(1f); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Single), out var value).Should().BeTrue(); + value.Should().Be(nameof(Single)); + } + + [Fact] + public void BeforeSendMetric_SetDouble_InvokeDelegate() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Double), nameof(Double)); + return metric; + }); + + var metric = CreateCounter(1d); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Double), out var value).Should().BeTrue(); + value.Should().Be(nameof(Double)); + } + + [Fact] + public void BeforeSendMetric_SetDecimal_UnsupportedType() + { + _fixture.Options.Experimental.SetBeforeSendMetric(static metric => + { + metric.SetAttribute(nameof(Decimal), nameof(Decimal)); + return metric; + }); + + _fixture.Options.Experimental.BeforeSendMetricInternal.Should().NotBeNull(); + + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Warning); + entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([typeof(decimal)]); + } + + [Fact] + public void BeforeSendMetric_SetNull_NoOp() + { + _fixture.Options.Experimental.SetBeforeSendMetric(null!); + + var metric = CreateCounter(1); + _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); + + metric.TryGetAttribute(nameof(Int32), out var value).Should().BeFalse(); + value.Should().BeNull(); + } + + private static SentryMetric CreateCounter(T value) where T : struct + { + return new SentryMetric(DateTimeOffset.MinValue, SentryId.Empty, SentryMetricType.Counter, "sentry_tests.sentry_trace_metrics_tests.counter", value); + } +} + +file static class Callback where T : struct +{ + internal static SentryMetric? Nop(SentryMetric metric) + { + return metric; + } +} diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Types.cs b/test/Sentry.Tests/SentryTraceMetricsTests.Types.cs new file mode 100644 index 0000000000..432c27b254 --- /dev/null +++ b/test/Sentry.Tests/SentryTraceMetricsTests.Types.cs @@ -0,0 +1,329 @@ +#nullable enable + +namespace Sentry.Tests; + +public partial class SentryTraceMetricsTests +{ + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Enabled_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Disabled_DoesNotCaptureEnvelope(SentryMetricType type) + { + _fixture.Options.Experimental.EnableMetrics = false; + var metrics = _fixture.GetSut(); + + metrics.Emit(type, 1, []); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Attributes_Enabled_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, [new KeyValuePair("attribute-key", "attribute-value")]); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelope(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Attributes_Disabled_DoesNotCaptureEnvelope(SentryMetricType type) + { + _fixture.Options.Experimental.EnableMetrics = false; + var metrics = _fixture.GetSut(); + + metrics.Emit(type, 1, [new KeyValuePair("attribute-key", "attribute-value")]); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Byte_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Int16_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Int32_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Int64_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1L, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Single_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1f, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Double_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1d, []); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Decimal_DoesNotCaptureEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + metrics.Emit(type, 1m, []); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Warning); + entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([typeof(decimal)]); + } + +#if NET5_0_OR_GREATER + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Half_DoesNotCaptureEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + metrics.Emit(type, Half.One, []); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Warning); + entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([typeof(Half)]); + } +#endif + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Enum_DoesNotCaptureEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + metrics.Emit(type, (StringComparison)1, []); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Warning); + entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([typeof(StringComparison)]); + } + + [Theory] + [InlineData(SentryMetricType.Counter, nameof(SentryMetricType.Counter), typeof(int))] + [InlineData(SentryMetricType.Gauge, nameof(SentryMetricType.Gauge), typeof(int))] + [InlineData(SentryMetricType.Distribution, nameof(SentryMetricType.Distribution), typeof(int))] + public void Emit_Name_Null_DoesNotCaptureEnvelope(SentryMetricType type, string arg0, Type arg1) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + metrics.Emit(type, null!, 1); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Warning); + entry.Message.Should().Be("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}"); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([arg0, arg1]); + } + + [Theory] + [InlineData(SentryMetricType.Counter, nameof(SentryMetricType.Counter), typeof(int))] + [InlineData(SentryMetricType.Gauge, nameof(SentryMetricType.Gauge), typeof(int))] + [InlineData(SentryMetricType.Distribution, nameof(SentryMetricType.Distribution), typeof(int))] + public void Emit_Name_Empty_DoesNotCaptureEnvelope(SentryMetricType type, string arg0, Type arg1) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + metrics.Emit(type, "", 1); + metrics.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Warning); + entry.Message.Should().Be("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}"); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([arg0, arg1]); + } +} + +file static class SentryTraceMetricsExtensions +{ + public static void Emit(this SentryTraceMetrics metrics, SentryMetricType type, T value, ReadOnlySpan> attributes) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", value, attributes); + break; + case SentryMetricType.Gauge: + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", attributes); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", attributes); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static void Emit(this SentryTraceMetrics metrics, SentryMetricType type, string name, T value) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + metrics.EmitCounter(name, value); + break; + case SentryMetricType.Gauge: + metrics.EmitGauge(name, value, "measurement_unit"); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution(name, value, "measurement_unit"); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } +} diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.cs b/test/Sentry.Tests/SentryTraceMetricsTests.cs new file mode 100644 index 0000000000..18496a629d --- /dev/null +++ b/test/Sentry.Tests/SentryTraceMetricsTests.cs @@ -0,0 +1,276 @@ +#nullable enable + +namespace Sentry.Tests; + +/// +/// +/// +public partial class SentryTraceMetricsTests : IDisposable +{ + internal sealed class Fixture + { + public Fixture() + { + DiagnosticLogger = new InMemoryDiagnosticLogger(); + Hub = Substitute.For(); + Options = new SentryOptions + { + Debug = true, + DiagnosticLogger = DiagnosticLogger, + }; + Clock = new MockClock(new DateTimeOffset(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2))); + BatchSize = 2; + BatchTimeout = Timeout.InfiniteTimeSpan; + TraceId = SentryId.Create(); + SpanId = Sentry.SpanId.Create(); + + Hub.IsEnabled.Returns(true); + + var span = Substitute.For(); + span.TraceId.Returns(TraceId); + span.SpanId.Returns(SpanId.Value); + Hub.GetSpan().Returns(span); + + ExpectedAttributes = new Dictionary(1) + { + { "attribute-key", "attribute-value" }, + }; + } + + public InMemoryDiagnosticLogger DiagnosticLogger { get; } + public IHub Hub { get; } + public SentryOptions Options { get; } + public ISystemClock Clock { get; } + public int BatchSize { get; set; } + public TimeSpan BatchTimeout { get; set; } + public SentryId TraceId { get; private set; } + public SpanId? SpanId { get; private set; } + + public Dictionary ExpectedAttributes { get; } + + public void WithoutActiveSpan() + { + Hub.GetSpan().Returns((ISpan?)null); + + var scope = new Scope(); + Hub.SubstituteConfigureScope(scope); + TraceId = scope.PropagationContext.TraceId; + SpanId = null; + } + + public SentryTraceMetrics GetSut() => SentryTraceMetrics.Create(Hub, Options, Clock, BatchSize, BatchTimeout); + } + + private readonly Fixture _fixture; + + public SentryTraceMetricsTests() + { + _fixture = new Fixture(); + } + + public void Dispose() + { + _fixture.DiagnosticLogger.Entries.Should().BeEmpty(); + } + + [Fact] + public void Create_Enabled_NewDefaultInstance() + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + + var instance = _fixture.GetSut(); + var other = _fixture.GetSut(); + + instance.Should().BeOfType(); + instance.Should().NotBeSameAs(other); + } + + [Fact] + public void Create_Disabled_CachedDisabledInstance() + { + _fixture.Options.Experimental.EnableMetrics = false; + + var instance = _fixture.GetSut(); + var other = _fixture.GetSut(); + + instance.Should().BeOfType(); + instance.Should().BeSameAs(other); + } + + [Fact] + public void Emit_WithoutActiveSpan_CapturesEnvelope() + { + _fixture.WithoutActiveSpan(); + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1, [new KeyValuePair("attribute-key", "attribute-value")]); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelope(envelope, SentryMetricType.Counter); + } + + [Fact] + public void Emit_WithBeforeSendMetric_InvokesCallback() + { + var invocations = 0; + SentryMetric configuredMetric = null!; + + Assert.True(_fixture.Options.Experimental.EnableMetrics); + _fixture.Options.Experimental.SetBeforeSendMetric((SentryMetric metric) => + { + invocations++; + configuredMetric = metric; + return metric; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1, [new KeyValuePair("attribute-key", "attribute-value")]); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + invocations.Should().Be(1); + _fixture.AssertMetric(configuredMetric, SentryMetricType.Counter); + } + + [Fact] + public void Emit_WhenBeforeSendMetricReturnsNull_DoesNotCaptureEnvelope() + { + var invocations = 0; + + Assert.True(_fixture.Options.Experimental.EnableMetrics); + _fixture.Options.Experimental.SetBeforeSendMetric((SentryMetric metric) => + { + invocations++; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1, [new KeyValuePair("attribute-key", "attribute-value")]); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + invocations.Should().Be(1); + } + + [Fact] + public void Emit_InvalidBeforeSendMetric_DoesNotCaptureEnvelope() + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + _fixture.Options.Experimental.SetBeforeSendMetric(static (SentryMetric metric) => throw new InvalidOperationException()); + var metrics = _fixture.GetSut(); + + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Error); + entry.Message.Should().Be("The BeforeSendMetric callback threw an exception. The Metric will be dropped."); + entry.Exception.Should().BeOfType(); + entry.Args.Should().BeEmpty(); + } + + [Fact] + public void Flush_AfterEmit_CapturesEnvelope() + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Flush(); + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + envelope.Should().BeNull(); + + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1, [new KeyValuePair("attribute-key", "attribute-value")]); + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + envelope.Should().BeNull(); + + metrics.Flush(); + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelope(envelope, SentryMetricType.Counter); + } + + [Fact] + public void Dispose_BeforeEmit_DoesNotCaptureEnvelope() + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + var defaultMetrics = metrics.Should().BeOfType().Which; + defaultMetrics.Dispose(); + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1, [new KeyValuePair("attribute-key", "attribute-value")]); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + var entry = _fixture.DiagnosticLogger.Dequeue(); + entry.Level.Should().Be(SentryLevel.Info); + entry.Message.Should().Be("{0}-Buffer full ... dropping {0}"); + entry.Exception.Should().BeNull(); + entry.Args.Should().BeEquivalentTo([typeof(SentryMetric).Name]); + } +} + +internal static class MetricsAssertionExtensions +{ + public static void AssertEnvelope(this SentryTraceMetricsTests.Fixture fixture, Envelope envelope, SentryMetricType type) where T : struct + { + envelope.Header.Should().ContainSingle().Which.Key.Should().Be("sdk"); + var item = envelope.Items.Should().ContainSingle().Which; + + var metric = item.Payload.Should().BeOfType().Which.Source.Should().BeOfType().Which; + AssertMetric(fixture, metric, type); + + Assert.Collection(item.Header, + element => Assert.Equal(CreateHeader("type", "trace_metric"), element), + element => Assert.Equal(CreateHeader("item_count", 1), element), + element => Assert.Equal(CreateHeader("content_type", "application/vnd.sentry.items.trace-metric+json"), element)); + } + + public static void AssertEnvelopeWithoutAttributes(this SentryTraceMetricsTests.Fixture fixture, Envelope envelope, SentryMetricType type) where T : struct + { + fixture.ExpectedAttributes.Clear(); + AssertEnvelope(fixture, envelope, type); + } + + public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, TraceMetric metric, SentryMetricType type) where T : struct + { + var items = metric.Items; + items.Length.Should().Be(1); + var cast = items[0] as SentryMetric; + Assert.NotNull(cast); + AssertMetric(fixture, cast, type); + } + + public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, SentryMetric metric, SentryMetricType type) where T : struct + { + metric.Timestamp.Should().Be(fixture.Clock.GetUtcNow()); + metric.TraceId.Should().Be(fixture.TraceId); + metric.Type.Should().Be(type); + metric.Name.Should().Be("sentry_tests.sentry_trace_metrics_tests.counter"); + metric.Value.Should().Be(1); + metric.SpanId.Should().Be(fixture.SpanId); + if (metric.Type is SentryMetricType.Gauge or SentryMetricType.Distribution) + { + metric.Unit.Should().Be("measurement_unit"); + } + else + { + metric.Unit.Should().BeNull(); + } + + foreach (var expectedAttribute in fixture.ExpectedAttributes) + { + metric.TryGetAttribute(expectedAttribute.Key, out string? value).Should().BeTrue(); + value.Should().Be(expectedAttribute.Value); + } + } + + private static KeyValuePair CreateHeader(string name, object? value) + { + return new KeyValuePair(name, value); + } +} From 41e67c271d82b4cc65496fc36a84337e0429c3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:30:09 +0100 Subject: [PATCH 15/26] feat(metrics): Trace-connected Metrics (Samples) (#4841) Co-authored-by: Sentry Github Bot --- .../Sentry.Samples.Console.Basic/Program.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index 889dcde450..9a82d0e633 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -9,6 +9,8 @@ * For more advanced features of the SDK, see Sentry.Samples.Console.Customized. */ +using System.Diagnostics; +using System.Net; using System.Net.Http; using static System.Console; @@ -50,6 +52,17 @@ // Drop logs with level Info return log.Level is SentryLogLevel.Info ? null : log; }); + + // Sentry (trace-connected) Metrics via SentrySdk.Experimental.Metrics are enabled by default. + options.Experimental.SetBeforeSendMetric(static metric => + { + // A demonstration of how you can modify the metric object before sending it to Sentry + metric.SetAttribute("operating_system.platform", Environment.OSVersion.Platform.ToString()); + metric.SetAttribute("operating_system.version", Environment.OSVersion.Version.ToString()); + + // Return null to drop the metric + return metric; + }); }); // This starts a new transaction and attaches it to the scope. @@ -71,9 +84,22 @@ async Task FirstFunction() // This is an example of making an HttpRequest. A trace us automatically captured by Sentry for this. var messageHandler = new SentryHttpMessageHandler(); var httpClient = new HttpClient(messageHandler, true); + + var stopwatch = Stopwatch.StartNew(); var html = await httpClient.GetStringAsync("https://example.com/"); + stopwatch.Stop(); + WriteLine(html); + + // Info-Log filtered via "BeforeSendLog" callback SentrySdk.Logger.LogInfo("HTTP Request completed."); + + // Metric modified via "BeforeSendMetric" callback for type "int" before sending it to Sentry + SentrySdk.Experimental.Metrics.EmitCounter("sentry.samples.console.basic.http_requests_completed", 1); + + // Metric sent as is because no "BeforeSendMetric" is set for type "double" + SentrySdk.Experimental.Metrics.EmitDistribution("sentry.samples.console.basic.http_request_duration", stopwatch.Elapsed.TotalSeconds, SentryUnits.Duration.Second, + [new KeyValuePair("http.request.method", HttpMethod.Get.Method), new KeyValuePair("http.response.status_code", (int)HttpStatusCode.OK)]); } async Task SecondFunction() From cb812b0d335187bf5a0c72d64a52c89fdef2feff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:51:47 +0100 Subject: [PATCH 16/26] docs: add comment to sample --- samples/Sentry.Samples.Console.Basic/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index 9a82d0e633..165953a290 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -4,6 +4,7 @@ * - Performance Tracing (Transactions / Spans) * - Release Health (Sessions) * - Logs + * - Metrics * - MSBuild integration for Source Context (see the csproj) * * For more advanced features of the SDK, see Sentry.Samples.Console.Customized. From 7faa68261870245cef1067853666c942cd9b57f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:19:44 +0100 Subject: [PATCH 17/26] feat: add Scope-only overloads to Distribution and Gauge method groups --- src/Sentry/SentryTraceMetrics.Distribution.cs | 13 ++++++++++++ src/Sentry/SentryTraceMetrics.Gauge.cs | 13 ++++++++++++ ...iApprovalTests.Run.DotNet10_0.verified.txt | 20 +++++++++---------- ...piApprovalTests.Run.DotNet8_0.verified.txt | 20 +++++++++---------- ...piApprovalTests.Run.DotNet9_0.verified.txt | 20 +++++++++---------- .../ApiApprovalTests.Run.Net4_8.verified.txt | 14 ++++++++----- 6 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/Sentry/SentryTraceMetrics.Distribution.cs b/src/Sentry/SentryTraceMetrics.Distribution.cs index b31d9bd626..2e8a2fba15 100644 --- a/src/Sentry/SentryTraceMetrics.Distribution.cs +++ b/src/Sentry/SentryTraceMetrics.Distribution.cs @@ -27,6 +27,19 @@ public void EmitDistribution(string name, T value, string? unit) where T : st CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null); } + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, null, [], scope); + } + /// /// Add a distribution value. /// diff --git a/src/Sentry/SentryTraceMetrics.Gauge.cs b/src/Sentry/SentryTraceMetrics.Gauge.cs index 7d46ab1f90..4e1d45f986 100644 --- a/src/Sentry/SentryTraceMetrics.Gauge.cs +++ b/src/Sentry/SentryTraceMetrics.Gauge.cs @@ -27,6 +27,19 @@ public void EmitGauge(string name, T value, string? unit) where T : struct CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], null); } + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, null, [], scope); + } + /// /// Set a gauge value. /// diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 3357950291..44f219cd28 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -668,22 +668,16 @@ namespace Sentry Distribution = 2, } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - [System.Runtime.CompilerServices.RequiredMember] public sealed class SentryMetric where T : struct { - [System.Runtime.CompilerServices.RequiredMember] - public string Name { get; init; } + public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public System.DateTimeOffset Timestamp { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public Sentry.SentryId TraceId { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public Sentry.SentryMetricType Type { get; init; } + public required System.DateTimeOffset Timestamp { get; init; } + public required Sentry.SentryId TraceId { get; init; } + public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public T Value { get; init; } + public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } @@ -1061,6 +1055,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitDistribution(string name, T value, string? unit) where T : struct { } public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) @@ -1071,6 +1067,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitGauge(string name, T value, string? unit) where T : struct { } public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 3357950291..44f219cd28 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -668,22 +668,16 @@ namespace Sentry Distribution = 2, } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - [System.Runtime.CompilerServices.RequiredMember] public sealed class SentryMetric where T : struct { - [System.Runtime.CompilerServices.RequiredMember] - public string Name { get; init; } + public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public System.DateTimeOffset Timestamp { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public Sentry.SentryId TraceId { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public Sentry.SentryMetricType Type { get; init; } + public required System.DateTimeOffset Timestamp { get; init; } + public required Sentry.SentryId TraceId { get; init; } + public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public T Value { get; init; } + public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } @@ -1061,6 +1055,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitDistribution(string name, T value, string? unit) where T : struct { } public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) @@ -1071,6 +1067,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitGauge(string name, T value, string? unit) where T : struct { } public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 3357950291..44f219cd28 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -668,22 +668,16 @@ namespace Sentry Distribution = 2, } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - [System.Runtime.CompilerServices.RequiredMember] public sealed class SentryMetric where T : struct { - [System.Runtime.CompilerServices.RequiredMember] - public string Name { get; init; } + public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public System.DateTimeOffset Timestamp { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public Sentry.SentryId TraceId { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public Sentry.SentryMetricType Type { get; init; } + public required System.DateTimeOffset Timestamp { get; init; } + public required Sentry.SentryId TraceId { get; init; } + public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - [System.Runtime.CompilerServices.RequiredMember] - public T Value { get; init; } + public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } @@ -1061,6 +1055,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitDistribution(string name, T value, string? unit) where T : struct { } public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) @@ -1071,6 +1067,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitGauge(string name, T value, string? unit) where T : struct { } public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index cdadf8c93d..40e1f2a2fe 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -657,13 +657,13 @@ namespace Sentry public sealed class SentryMetric where T : struct { - public string Name { get; init; } + public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } - public System.DateTimeOffset Timestamp { get; init; } - public Sentry.SentryId TraceId { get; init; } - public Sentry.SentryMetricType Type { get; init; } + public required System.DateTimeOffset Timestamp { get; init; } + public required Sentry.SentryId TraceId { get; init; } + public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - public T Value { get; init; } + public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } @@ -1035,6 +1035,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitDistribution(string name, T value, string? unit) where T : struct { } public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) @@ -1045,6 +1047,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } public void EmitGauge(string name, T value, string? unit) where T : struct { } public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) From 4a080e74243a78a0ca6b5c77080ea9e8910de0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:39:35 +0100 Subject: [PATCH 18/26] feat!: make public Metrics type non-generic --- .../Sentry.Samples.Console.Basic/Program.cs | 23 ++- src/Sentry/IHub.cs | 2 +- .../Internal/DefaultSentryTraceMetrics.cs | 8 +- .../Internal/DisabledSentryTraceMetrics.cs | 2 +- src/Sentry/Internal/Polyfills.cs | 29 ++++ src/Sentry/Protocol/TraceMetric.cs | 8 +- src/Sentry/SentryLog.cs | 1 + src/Sentry/SentryMetric.Factory.cs | 2 +- src/Sentry/SentryMetric.Generic.cs | 72 +++++++++ src/Sentry/SentryMetric.Interface.cs | 12 -- src/Sentry/SentryMetric.cs | 94 +++--------- src/Sentry/SentryMetricType.cs | 6 +- src/Sentry/SentryOptions.cs | 14 +- src/Sentry/SentryTraceMetrics.cs | 2 +- src/Sentry/SentryTraceMetricsCallbacks.cs | 98 ------------ src/Sentry/SentryUnits.cs | 2 +- .../InMemorySentryTraceMetrics.cs | 4 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 23 ++- ...piApprovalTests.Run.DotNet8_0.verified.txt | 23 ++- ...piApprovalTests.Run.DotNet9_0.verified.txt | 23 ++- .../ApiApprovalTests.Run.Net4_8.verified.txt | 23 ++- test/Sentry.Tests/SentryMetricTests.cs | 16 +- .../SentryTraceMetricsTests.Options.cs | 139 +----------------- .../SentryTraceMetricsTests.Values.cs | 82 +++++++++++ test/Sentry.Tests/SentryTraceMetricsTests.cs | 18 ++- 25 files changed, 308 insertions(+), 418 deletions(-) create mode 100644 src/Sentry/SentryMetric.Generic.cs delete mode 100644 src/Sentry/SentryMetric.Interface.cs delete mode 100644 src/Sentry/SentryTraceMetricsCallbacks.cs create mode 100644 test/Sentry.Tests/SentryTraceMetricsTests.Values.cs diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index 165953a290..5b3dfcdf46 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -55,13 +55,21 @@ }); // Sentry (trace-connected) Metrics via SentrySdk.Experimental.Metrics are enabled by default. - options.Experimental.SetBeforeSendMetric(static metric => + options.Experimental.SetBeforeSendMetric(static metric => { + if (metric.TryGetValue(out int integer) && integer < 0) + { + // Return null to drop the metric + return null; + } + // A demonstration of how you can modify the metric object before sending it to Sentry - metric.SetAttribute("operating_system.platform", Environment.OSVersion.Platform.ToString()); - metric.SetAttribute("operating_system.version", Environment.OSVersion.Version.ToString()); + if (metric.Type is SentryMetricType.Counter) + { + metric.SetAttribute("operating_system.platform", Environment.OSVersion.Platform.ToString()); + metric.SetAttribute("operating_system.version", Environment.OSVersion.Version.ToString()); + } - // Return null to drop the metric return metric; }); }); @@ -95,10 +103,13 @@ async Task FirstFunction() // Info-Log filtered via "BeforeSendLog" callback SentrySdk.Logger.LogInfo("HTTP Request completed."); - // Metric modified via "BeforeSendMetric" callback for type "int" before sending it to Sentry + // Counter-Metric prevented from being sent to Sentry via "BeforeSendMetric" callback + SentrySdk.Experimental.Metrics.EmitCounter("sentry.samples.console.basic.ignore", -1); + + // Counter-Metric modified before sending it to Sentry via "BeforeSendMetric" callback SentrySdk.Experimental.Metrics.EmitCounter("sentry.samples.console.basic.http_requests_completed", 1); - // Metric sent as is because no "BeforeSendMetric" is set for type "double" + // Distribution-Metric sent as is (see "BeforeSendMetric" callback) SentrySdk.Experimental.Metrics.EmitDistribution("sentry.samples.console.basic.http_request_duration", stopwatch.Elapsed.TotalSeconds, SentryUnits.Duration.Second, [new KeyValuePair("http.request.method", HttpMethod.Get.Method), new KeyValuePair("http.response.status_code", (int)HttpStatusCode.OK)]); } diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 2d48844a9e..bdab68b2cf 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -36,7 +36,7 @@ public interface IHub : ISentryClient, ISentryScopeManager /// Available options: /// /// - /// + /// /// /// [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] diff --git a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs index df02cfa44f..446a045ebb 100644 --- a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs +++ b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs @@ -10,7 +10,7 @@ internal sealed class DefaultSentryTraceMetrics : SentryTraceMetrics, IDisposabl private readonly SentryOptions _options; private readonly ISystemClock _clock; - private readonly BatchProcessor _batchProcessor; + private readonly BatchProcessor _batchProcessor; internal DefaultSentryTraceMetrics(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) { @@ -21,7 +21,7 @@ internal DefaultSentryTraceMetrics(IHub hub, SentryOptions options, ISystemClock _options = options; _clock = clock; - _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, TraceMetric.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger); + _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, TraceMetric.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger); } /// @@ -63,12 +63,12 @@ private protected override void CaptureMetric(SentryMetricType type, string n } /// - protected internal override void CaptureMetric(SentryMetric metric) where T : struct + private protected override void CaptureMetric(SentryMetric metric) where T : struct { Debug.Assert(SentryMetric.IsSupported(typeof(T))); Debug.Assert(!string.IsNullOrEmpty(metric.Name)); - var configuredMetric = metric; + SentryMetric? configuredMetric = metric; if (_options.Experimental.BeforeSendMetricInternal is { } beforeSendMetric) { diff --git a/src/Sentry/Internal/DisabledSentryTraceMetrics.cs b/src/Sentry/Internal/DisabledSentryTraceMetrics.cs index 750f6717c1..7d8c10e4da 100644 --- a/src/Sentry/Internal/DisabledSentryTraceMetrics.cs +++ b/src/Sentry/Internal/DisabledSentryTraceMetrics.cs @@ -21,7 +21,7 @@ private protected override void CaptureMetric(SentryMetricType type, string n } /// - protected internal override void CaptureMetric(SentryMetric metric) where T : struct + private protected override void CaptureMetric(SentryMetric metric) where T : struct { // disabled } diff --git a/src/Sentry/Internal/Polyfills.cs b/src/Sentry/Internal/Polyfills.cs index 2113687f8d..f5ce518ec3 100644 --- a/src/Sentry/Internal/Polyfills.cs +++ b/src/Sentry/Internal/Polyfills.cs @@ -62,3 +62,32 @@ public static void WriteRawValue(this Utf8JsonWriter writer, byte[] utf8Json) } } #endif + +// TODO: remove when updating Polyfill: https://github.com/getsentry/sentry-dotnet/pull/4879 +#if !NET6_0_OR_GREATER +internal static class EnumerableExtensions +{ + internal static bool TryGetNonEnumeratedCount(this IEnumerable source, out int count) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source is ICollection genericCollection) + { + count = genericCollection.Count; + return true; + } + + if (source is ICollection collection) + { + count = collection.Count; + return true; + } + + count = 0; + return false; + } +} +#endif diff --git a/src/Sentry/Protocol/TraceMetric.cs b/src/Sentry/Protocol/TraceMetric.cs index cbcea00a57..2ee0ebbce5 100644 --- a/src/Sentry/Protocol/TraceMetric.cs +++ b/src/Sentry/Protocol/TraceMetric.cs @@ -12,15 +12,15 @@ namespace Sentry.Protocol; /// internal sealed class TraceMetric : ISentryJsonSerializable { - private readonly ISentryMetric[] _items; + private readonly SentryMetric[] _items; - public TraceMetric(ISentryMetric[] metrics) + public TraceMetric(SentryMetric[] metrics) { _items = metrics; } public int Length => _items.Length; - public ReadOnlySpan Items => _items; + public ReadOnlySpan Items => _items; public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { @@ -36,7 +36,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndObject(); } - internal static void Capture(IHub hub, ISentryMetric[] metrics) + internal static void Capture(IHub hub, SentryMetric[] metrics) { _ = hub.CaptureEnvelope(Envelope.FromMetric(new TraceMetric(metrics))); } diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index c57a08a84d..b77e7fe793 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -189,6 +189,7 @@ internal void SetOrigin(string origin) SetAttribute("sentry.origin", origin); } + /// internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index 290d941b1f..2d78b1ad94 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -3,7 +3,7 @@ namespace Sentry; -internal static class SentryMetric +public abstract partial class SentryMetric { internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct { diff --git a/src/Sentry/SentryMetric.Generic.cs b/src/Sentry/SentryMetric.Generic.cs new file mode 100644 index 0000000000..9e38adfb62 --- /dev/null +++ b/src/Sentry/SentryMetric.Generic.cs @@ -0,0 +1,72 @@ +using Sentry.Extensibility; + +namespace Sentry; + +/// +/// Internal generic representation of . +/// +/// The numeric type of the metric. +/// +/// We hide some of the generic implementation details from user code. +/// +internal sealed class SentryMetric : SentryMetric where T : struct +{ + private readonly T _value; + + [SetsRequiredMembers] + internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name, T value) + : base(timestamp, traceId, type, name) + { + _value = value; + } + + internal override object Value => _value; + + /// + public override bool TryGetValue(out TValue value) where TValue : struct + { + if (_value is TValue match) + { + value = match; + return true; + } + + value = default; + return false; + } + + private protected override void WriteMetricValueTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + const string propertyName = "value"; + var type = typeof(T); + + if (type == typeof(long)) + { + writer.WriteNumber(propertyName, (long)(object)_value); + } + else if (type == typeof(double)) + { + writer.WriteNumber(propertyName, (double)(object)_value); + } + else if (type == typeof(int)) + { + writer.WriteNumber(propertyName, (int)(object)_value); + } + else if (type == typeof(float)) + { + writer.WriteNumber(propertyName, (float)(object)_value); + } + else if (type == typeof(short)) + { + writer.WriteNumber(propertyName, (short)(object)_value); + } + else if (type == typeof(byte)) + { + writer.WriteNumber(propertyName, (byte)(object)_value); + } + else + { + Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); + } + } +} diff --git a/src/Sentry/SentryMetric.Interface.cs b/src/Sentry/SentryMetric.Interface.cs deleted file mode 100644 index 8a91ad868a..0000000000 --- a/src/Sentry/SentryMetric.Interface.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Sentry.Extensibility; - -namespace Sentry; - -/// -/// Internal non-generic representation of . -/// -internal interface ISentryMetric -{ - /// - public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger); -} diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs index ba62ca2d7d..06d31552f9 100644 --- a/src/Sentry/SentryMetric.cs +++ b/src/Sentry/SentryMetric.cs @@ -6,25 +6,23 @@ namespace Sentry; /// /// Represents a Sentry Trace-connected Metric. /// -/// The numeric type of the metric. /// /// Sentry Docs: . /// Sentry Developer Documentation: . /// Sentry .NET SDK Docs: . /// [DebuggerDisplay(@"SentryMetric \{ Type = {Type}, Name = '{Name}', Value = {Value} \}")] -public sealed class SentryMetric : ISentryMetric where T : struct +public abstract partial class SentryMetric { private readonly Dictionary _attributes; [SetsRequiredMembers] - internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name, T value) + private protected SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name) { Timestamp = timestamp; TraceId = traceId; Type = type; Name = name; - Value = value; // 7 is the number of built-in attributes, so we start with that. _attributes = new Dictionary(7); } @@ -100,7 +98,8 @@ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricTy /// /// /// - public required T Value { get; init; } + // Internal non-generic (boxed to object) read-only property for testing, and usage in DebuggerDisplayAttribute. + internal abstract object Value { get; } /// /// The span id of the span that was active when the metric was emitted. @@ -115,11 +114,20 @@ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricTy /// public string? Unit { get; init; } + /// + /// Gets the metric value if it is of the specified type . + /// + /// When this method returns, contains the metric value, if it is of the specified type ; otherwise, the value for the type of the parameter. This parameter is passed uninitialized. + /// The numeric type of the metric. + /// if this is of type ; otherwise, . + /// Supported numeric value types for are , , , , , and . + public abstract bool TryGetValue(out TValue value) where TValue : struct; + /// /// Gets the attribute value associated with the specified key. /// /// - /// Returns if the contains an attribute with the specified key which is of type and it's value is not . + /// Returns if this contains an attribute with the specified key which is of type and it's value is not . /// Otherwise . /// Supported types: /// @@ -263,11 +271,13 @@ internal void SetAttributes(ReadOnlySpan> attribute } } - internal void Apply(Scope? scope) + private void Apply(Scope? scope) { + //TODO: https://github.com/getsentry/sentry-dotnet/issues/4882 } - void ISentryMetric.WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + /// + internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); @@ -279,7 +289,7 @@ void ISentryMetric.WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteString("type", Type.ToProtocolString(logger)); writer.WriteString("name", Name); - writer.WriteMetricValue("value", Value); + WriteMetricValueTo(writer, logger); writer.WritePropertyName("trace_id"); TraceId.WriteTo(writer, logger); @@ -307,70 +317,6 @@ void ISentryMetric.WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndObject(); } -} - -// TODO: remove after upgrading 14.0 and updating -#if !NET6_0_OR_GREATER -file static class EnumerableExtensions -{ - internal static bool TryGetNonEnumeratedCount(this IEnumerable source, out int count) - { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (source is ICollection genericCollection) - { - count = genericCollection.Count; - return true; - } - - if (source is ICollection collection) - { - count = collection.Count; - return true; - } - - count = 0; - return false; - } -} -#endif - -file static class Utf8JsonWriterExtensions -{ - internal static void WriteMetricValue(this Utf8JsonWriter writer, string propertyName, T value) where T : struct - { - var type = typeof(T); - if (type == typeof(long)) - { - writer.WriteNumber(propertyName, (long)(object)value); - } - else if (type == typeof(double)) - { - writer.WriteNumber(propertyName, (double)(object)value); - } - else if (type == typeof(int)) - { - writer.WriteNumber(propertyName, (int)(object)value); - } - else if (type == typeof(float)) - { - writer.WriteNumber(propertyName, (float)(object)value); - } - else if (type == typeof(short)) - { - writer.WriteNumber(propertyName, (short)(object)value); - } - else if (type == typeof(byte)) - { - writer.WriteNumber(propertyName, (byte)(object)value); - } - else - { - Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); - } - } + private protected abstract void WriteMetricValueTo(Utf8JsonWriter writer, IDiagnosticLogger? logger); } diff --git a/src/Sentry/SentryMetricType.cs b/src/Sentry/SentryMetricType.cs index df6601942c..d40a062077 100644 --- a/src/Sentry/SentryMetricType.cs +++ b/src/Sentry/SentryMetricType.cs @@ -11,7 +11,7 @@ public enum SentryMetricType /// A metric that increments counts. /// /// - /// represents the count to increment by. + /// represents the count to increment by. /// By default: . /// Counter, @@ -20,7 +20,7 @@ public enum SentryMetricType /// A metric that tracks a value that can go up or down. /// /// - /// represents the current value. + /// represents the current value. /// Gauge, @@ -28,7 +28,7 @@ public enum SentryMetricType /// A metric that tracks the statistical distribution of values. /// /// - /// represents a single measured value. + /// represents a single measured value. /// Distribution, } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 3f6bc689e4..645975f580 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1925,14 +1925,14 @@ internal static List GetDefaultInAppExclude() => public class ExperimentalSentryOptions { private readonly SentryOptions _options; - private SentryTraceMetricsCallbacks? _beforeSendMetric; + private Func? _beforeSendMetric; internal ExperimentalSentryOptions(SentryOptions options) { _options = options; } - internal SentryTraceMetricsCallbacks? BeforeSendMetricInternal => _beforeSendMetric; + internal Func? BeforeSendMetricInternal => _beforeSendMetric; /// /// When set to , the SDK does not generate and send metrics to Sentry via . @@ -1942,20 +1942,18 @@ internal ExperimentalSentryOptions(SentryOptions options) public bool EnableMetrics { get; set; } = true; /// - /// Sets a callback function to be invoked before sending the metric of numeric type to Sentry. + /// Sets a callback function to be invoked before sending the metric to Sentry. /// When the delegate throws an during invocation, the metric will not be captured. /// - /// The numeric type of the metric. /// /// It can be used to modify the metric object before being sent to Sentry. /// To prevent the metric from being sent to Sentry, return . - /// Supported numeric value types for are , , , , , and . + /// Supported numeric value types are , , , , , and . /// /// - public void SetBeforeSendMetric(Func, SentryMetric?> beforeSendMetric) where T : struct + public void SetBeforeSendMetric(Func beforeSendMetric) { - _beforeSendMetric ??= new SentryTraceMetricsCallbacks(); - _beforeSendMetric.Set(beforeSendMetric, _options.DiagnosticLogger); + _beforeSendMetric = beforeSendMetric; } } } diff --git a/src/Sentry/SentryTraceMetrics.cs b/src/Sentry/SentryTraceMetrics.cs index 9c0be19f8e..27376c71cb 100644 --- a/src/Sentry/SentryTraceMetrics.cs +++ b/src/Sentry/SentryTraceMetrics.cs @@ -54,7 +54,7 @@ private protected SentryTraceMetrics() /// /// The metric. /// The numeric type of the metric. - protected internal abstract void CaptureMetric(SentryMetric metric) where T : struct; + private protected abstract void CaptureMetric(SentryMetric metric) where T : struct; /// /// Clears all buffers for this metrics and causes any buffered metrics to be sent by the underlying . diff --git a/src/Sentry/SentryTraceMetricsCallbacks.cs b/src/Sentry/SentryTraceMetricsCallbacks.cs deleted file mode 100644 index d8da174890..0000000000 --- a/src/Sentry/SentryTraceMetricsCallbacks.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Sentry.Extensibility; - -namespace Sentry; - -#if NET6_0_OR_GREATER -/// -/// Similar to . -/// -#endif -internal sealed class SentryTraceMetricsCallbacks -{ - private Func, SentryMetric?> _beforeSendMetricByte; - private Func, SentryMetric?> _beforeSendMetricInt16; - private Func, SentryMetric?> _beforeSendMetricInt32; - private Func, SentryMetric?> _beforeSendMetricInt64; - private Func, SentryMetric?> _beforeSendMetricSingle; - private Func, SentryMetric?> _beforeSendMetricDouble; - - internal SentryTraceMetricsCallbacks() - { - _beforeSendMetricByte = static traceMetric => traceMetric; - _beforeSendMetricInt16 = static traceMetric => traceMetric; - _beforeSendMetricInt32 = static traceMetric => traceMetric; - _beforeSendMetricInt64 = static traceMetric => traceMetric; - _beforeSendMetricSingle = static traceMetric => traceMetric; - _beforeSendMetricDouble = static traceMetric => traceMetric; - } - - internal void Set(Func, SentryMetric?> beforeSendMetric, IDiagnosticLogger? diagnosticLogger) where T : struct - { - beforeSendMetric ??= static traceMetric => traceMetric; - - if (typeof(T) == typeof(long)) - { - _beforeSendMetricInt64 = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(double)) - { - _beforeSendMetricDouble = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(int)) - { - _beforeSendMetricInt32 = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(float)) - { - _beforeSendMetricSingle = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(short)) - { - _beforeSendMetricInt16 = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else if (typeof(T) == typeof(byte)) - { - _beforeSendMetricByte = (Func, SentryMetric?>)(object)beforeSendMetric; - } - else - { - diagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); - } - } - - internal SentryMetric? Invoke(SentryMetric metric) where T : struct - { - if (typeof(T) == typeof(long)) - { - return (SentryMetric?)(object?)_beforeSendMetricInt64.Invoke((SentryMetric)(object)metric); - } - - if (typeof(T) == typeof(double)) - { - return (SentryMetric?)(object?)_beforeSendMetricDouble.Invoke((SentryMetric)(object)metric); - } - - if (typeof(T) == typeof(int)) - { - return (SentryMetric?)(object?)_beforeSendMetricInt32.Invoke((SentryMetric)(object)metric); - } - - if (typeof(T) == typeof(float)) - { - return (SentryMetric?)(object?)_beforeSendMetricSingle.Invoke((SentryMetric)(object)metric); - } - - if (typeof(T) == typeof(short)) - { - return (SentryMetric?)(object?)_beforeSendMetricInt16.Invoke((SentryMetric)(object)metric); - } - - if (typeof(T) == typeof(byte)) - { - return (SentryMetric?)(object?)_beforeSendMetricByte.Invoke((SentryMetric)(object)metric); - } - - Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); - return null; - } -} diff --git a/src/Sentry/SentryUnits.cs b/src/Sentry/SentryUnits.cs index f4c1ee51b0..94302b40d1 100644 --- a/src/Sentry/SentryUnits.cs +++ b/src/Sentry/SentryUnits.cs @@ -2,7 +2,7 @@ namespace Sentry; /// /// Supported units by Relay and Sentry. -/// Applies to , as well as attributes of and attributes of . +/// Applies to , as well as attributes of and attributes of . /// /// /// Contains the units currently supported by Relay and Sentry. diff --git a/test/Sentry.Testing/InMemorySentryTraceMetrics.cs b/test/Sentry.Testing/InMemorySentryTraceMetrics.cs index 4a2aa73cda..d35e02c23f 100644 --- a/test/Sentry.Testing/InMemorySentryTraceMetrics.cs +++ b/test/Sentry.Testing/InMemorySentryTraceMetrics.cs @@ -5,7 +5,7 @@ namespace Sentry.Testing; public sealed class InMemorySentryTraceMetrics : SentryTraceMetrics { public List Entries { get; } = new(); - internal List Metrics { get; } = new(); + internal List Metrics { get; } = new(); /// private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct @@ -20,7 +20,7 @@ private protected override void CaptureMetric(SentryMetricType type, string n } /// - protected internal override void CaptureMetric(SentryMetric metric) where T : struct + private protected override void CaptureMetric(SentryMetric metric) where T : struct { Metrics.Add(metric); } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 44f219cd28..fcebbb5269 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -661,15 +661,8 @@ namespace Sentry protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } - public enum SentryMetricType - { - Counter = 0, - Gauge = 1, - Distribution = 2, - } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - public sealed class SentryMetric - where T : struct + public abstract class SentryMetric { public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -677,10 +670,17 @@ namespace Sentry public required Sentry.SentryId TraceId { get; init; } public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + public abstract bool TryGetValue(out TValue value) + where TValue : struct; + } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, } public enum SentryMonitorInterval { @@ -824,8 +824,7 @@ namespace Sentry public class ExperimentalSentryOptions { public bool EnableMetrics { get; set; } - public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) - where T : struct { } + public void SetBeforeSendMetric(System.Func beforeSendMetric) { } } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable @@ -1043,8 +1042,6 @@ namespace Sentry } public abstract class SentryTraceMetrics { - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; public void EmitCounter(string name, T value) where T : struct { } public void EmitCounter(string name, T value, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 44f219cd28..fcebbb5269 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -661,15 +661,8 @@ namespace Sentry protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } - public enum SentryMetricType - { - Counter = 0, - Gauge = 1, - Distribution = 2, - } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - public sealed class SentryMetric - where T : struct + public abstract class SentryMetric { public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -677,10 +670,17 @@ namespace Sentry public required Sentry.SentryId TraceId { get; init; } public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + public abstract bool TryGetValue(out TValue value) + where TValue : struct; + } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, } public enum SentryMonitorInterval { @@ -824,8 +824,7 @@ namespace Sentry public class ExperimentalSentryOptions { public bool EnableMetrics { get; set; } - public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) - where T : struct { } + public void SetBeforeSendMetric(System.Func beforeSendMetric) { } } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable @@ -1043,8 +1042,6 @@ namespace Sentry } public abstract class SentryTraceMetrics { - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; public void EmitCounter(string name, T value) where T : struct { } public void EmitCounter(string name, T value, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 44f219cd28..fcebbb5269 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -661,15 +661,8 @@ namespace Sentry protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } - public enum SentryMetricType - { - Counter = 0, - Gauge = 1, - Distribution = 2, - } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - public sealed class SentryMetric - where T : struct + public abstract class SentryMetric { public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -677,10 +670,17 @@ namespace Sentry public required Sentry.SentryId TraceId { get; init; } public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + public abstract bool TryGetValue(out TValue value) + where TValue : struct; + } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, } public enum SentryMonitorInterval { @@ -824,8 +824,7 @@ namespace Sentry public class ExperimentalSentryOptions { public bool EnableMetrics { get; set; } - public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) - where T : struct { } + public void SetBeforeSendMetric(System.Func beforeSendMetric) { } } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable @@ -1043,8 +1042,6 @@ namespace Sentry } public abstract class SentryTraceMetrics { - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; public void EmitCounter(string name, T value) where T : struct { } public void EmitCounter(string name, T value, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 40e1f2a2fe..7b4c4dc222 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -647,15 +647,8 @@ namespace Sentry protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } - public enum SentryMetricType - { - Counter = 0, - Gauge = 1, - Distribution = 2, - } [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")] - public sealed class SentryMetric - where T : struct + public abstract class SentryMetric { public required string Name { get; init; } public Sentry.SpanId? SpanId { get; init; } @@ -663,10 +656,17 @@ namespace Sentry public required Sentry.SentryId TraceId { get; init; } public required Sentry.SentryMetricType Type { get; init; } public string? Unit { get; init; } - public required T Value { get; init; } public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { } public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + public abstract bool TryGetValue(out TValue value) + where TValue : struct; + } + public enum SentryMetricType + { + Counter = 0, + Gauge = 1, + Distribution = 2, } public enum SentryMonitorInterval { @@ -804,8 +804,7 @@ namespace Sentry public class ExperimentalSentryOptions { public bool EnableMetrics { get; set; } - public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric) - where T : struct { } + public void SetBeforeSendMetric(System.Func beforeSendMetric) { } } } public sealed class SentryPackage : Sentry.ISentryJsonSerializable @@ -1023,8 +1022,6 @@ namespace Sentry } public abstract class SentryTraceMetrics { - protected abstract void CaptureMetric(Sentry.SentryMetric metric) - where T : struct; public void EmitCounter(string name, T value) where T : struct { } public void EmitCounter(string name, T value, Sentry.Scope? scope) diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs index ecfaf81056..1672574679 100644 --- a/test/Sentry.Tests/SentryMetricTests.cs +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -234,7 +234,7 @@ public void WriteTo_NumericValueType_Byte() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var value = document.RootElement.GetProperty("value"); value.ValueKind.Should().Be(JsonValueKind.Number); @@ -249,7 +249,7 @@ public void WriteTo_NumericValueType_Int16() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var value = document.RootElement.GetProperty("value"); value.ValueKind.Should().Be(JsonValueKind.Number); @@ -264,7 +264,7 @@ public void WriteTo_NumericValueType_Int32() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var value = document.RootElement.GetProperty("value"); value.ValueKind.Should().Be(JsonValueKind.Number); @@ -279,7 +279,7 @@ public void WriteTo_NumericValueType_Int64() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var value = document.RootElement.GetProperty("value"); value.ValueKind.Should().Be(JsonValueKind.Number); @@ -294,7 +294,7 @@ public void WriteTo_NumericValueType_Single() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var value = document.RootElement.GetProperty("value"); value.ValueKind.Should().Be(JsonValueKind.Number); @@ -309,7 +309,7 @@ public void WriteTo_NumericValueType_Double() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var value = document.RootElement.GetProperty("value"); value.ValueKind.Should().Be(JsonValueKind.Number); @@ -325,7 +325,7 @@ public void WriteTo_NumericValueType_Decimal() { var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - var exception = Assert.ThrowsAny(() => metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output)); + var exception = Assert.ThrowsAny(() => metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output)); exception.Message.Should().Contain($"Unhandled Metric Type {typeof(decimal)}."); exception.Message.Should().Contain("This instruction should be unreachable."); @@ -362,7 +362,7 @@ public void WriteTo_Attributes_AsJson() #endif metric.SetAttribute("null", null!); - var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); + var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var attributes = document.RootElement.GetProperty("attributes"); Assert.Collection(attributes.EnumerateObject().ToArray(), property => property.AssertAttributeInteger("sbyte", json => json.GetSByte(), sbyte.MinValue), diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs b/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs index ce3e533c02..744f445b63 100644 --- a/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs +++ b/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs @@ -23,147 +23,16 @@ public void BeforeSendMetric_Default_Null() [Fact] public void BeforeSendMetric_Set_NotNull() { - _fixture.Options.Experimental.SetBeforeSendMetric(Callback.Nop); + _fixture.Options.Experimental.SetBeforeSendMetric(static (SentryMetric metric) => metric); _fixture.Options.Experimental.BeforeSendMetricInternal.Should().NotBeNull(); } [Fact] - public void BeforeSendMetric_SetByte_InvokeDelegate() + public void BeforeSendMetric_SetNull_Null() { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Byte), nameof(Byte)); - return metric; - }); + _fixture.Options.Experimental.SetBeforeSendMetric(null!); - var metric = CreateCounter(1); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Byte), out var value).Should().BeTrue(); - value.Should().Be(nameof(Byte)); - } - - [Fact] - public void BeforeSendMetric_SetInt16_InvokeDelegate() - { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Int16), nameof(Int16)); - return metric; - }); - - var metric = CreateCounter(1); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Int16), out var value).Should().BeTrue(); - value.Should().Be(nameof(Int16)); - } - - [Fact] - public void BeforeSendMetric_SetInt32_InvokeDelegate() - { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Int32), nameof(Int32)); - return metric; - }); - - var metric = CreateCounter(1); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Int32), out var value).Should().BeTrue(); - value.Should().Be(nameof(Int32)); - } - - [Fact] - public void BeforeSendMetric_SetInt64_InvokeDelegate() - { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Int64), nameof(Int64)); - return metric; - }); - - var metric = CreateCounter(1L); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Int64), out var value).Should().BeTrue(); - value.Should().Be(nameof(Int64)); - } - - [Fact] - public void BeforeSendMetric_SetSingle_InvokeDelegate() - { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Single), nameof(Single)); - return metric; - }); - - var metric = CreateCounter(1f); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Single), out var value).Should().BeTrue(); - value.Should().Be(nameof(Single)); - } - - [Fact] - public void BeforeSendMetric_SetDouble_InvokeDelegate() - { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Double), nameof(Double)); - return metric; - }); - - var metric = CreateCounter(1d); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Double), out var value).Should().BeTrue(); - value.Should().Be(nameof(Double)); - } - - [Fact] - public void BeforeSendMetric_SetDecimal_UnsupportedType() - { - _fixture.Options.Experimental.SetBeforeSendMetric(static metric => - { - metric.SetAttribute(nameof(Decimal), nameof(Decimal)); - return metric; - }); - - _fixture.Options.Experimental.BeforeSendMetricInternal.Should().NotBeNull(); - - var entry = _fixture.DiagnosticLogger.Dequeue(); - entry.Level.Should().Be(SentryLevel.Warning); - entry.Message.Should().Be("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."); - entry.Exception.Should().BeNull(); - entry.Args.Should().BeEquivalentTo([typeof(decimal)]); - } - - [Fact] - public void BeforeSendMetric_SetNull_NoOp() - { - _fixture.Options.Experimental.SetBeforeSendMetric(null!); - - var metric = CreateCounter(1); - _fixture.Options.Experimental.BeforeSendMetricInternal!.Invoke(metric); - - metric.TryGetAttribute(nameof(Int32), out var value).Should().BeFalse(); - value.Should().BeNull(); - } - - private static SentryMetric CreateCounter(T value) where T : struct - { - return new SentryMetric(DateTimeOffset.MinValue, SentryId.Empty, SentryMetricType.Counter, "sentry_tests.sentry_trace_metrics_tests.counter", value); - } -} - -file static class Callback where T : struct -{ - internal static SentryMetric? Nop(SentryMetric metric) - { - return metric; + _fixture.Options.Experimental.BeforeSendMetricInternal.Should().BeNull(); } } diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Values.cs b/test/Sentry.Tests/SentryTraceMetricsTests.Values.cs new file mode 100644 index 0000000000..2defb697ca --- /dev/null +++ b/test/Sentry.Tests/SentryTraceMetricsTests.Values.cs @@ -0,0 +1,82 @@ +namespace Sentry.Tests; + +public partial class SentryTraceMetricsTests +{ + [Fact] + public void TryGetValue_FromByte_SupportedType() + { + var metric = CreateCounter(1); + + metric.TryGetValue(out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void TryGetValue_FromInt16_SupportedType() + { + var metric = CreateCounter(1); + + metric.TryGetValue(out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void TryGetValue_FromInt32_SupportedType() + { + var metric = CreateCounter(1); + + metric.TryGetValue(out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void TryGetValue_FromInt64_SupportedType() + { + var metric = CreateCounter(1L); + + metric.TryGetValue(out var value).Should().BeTrue(); + value.Should().Be(1L); + } + + [Fact] + public void TryGetValue_FromSingle_SupportedType() + { + var metric = CreateCounter(1f); + + metric.TryGetValue(out var value).Should().BeTrue(); + value.Should().Be(1f); + } + + [Fact] + public void TryGetValue_FromDouble_SupportedType() + { + var metric = CreateCounter(1d); + + metric.TryGetValue(out var value).Should().BeTrue(); + value.Should().Be(1d); + } + + [Fact] + public void TryGetValue_FromDecimal_UnsupportedType() + { + var metric = CreateCounter(1m); + + metric.TryGetValue(out var @byte).Should().BeFalse(); + @byte.Should().Be(0); + metric.TryGetValue(out var @short).Should().BeFalse(); + @short.Should().Be(0); + metric.TryGetValue(out var @int).Should().BeFalse(); + @int.Should().Be(0); + metric.TryGetValue(out var @long).Should().BeFalse(); + @long.Should().Be(0L); + metric.TryGetValue(out var @float).Should().BeFalse(); + @float.Should().Be(0f); + metric.TryGetValue(out var @double).Should().BeFalse(); + @double.Should().Be(0d); + } + + private static SentryMetric CreateCounter(T value) where T : struct + { + return new SentryMetric(DateTimeOffset.MinValue, SentryId.Empty, SentryMetricType.Counter, "sentry_tests.sentry_trace_metrics_tests.counter", value); + } +} diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.cs b/test/Sentry.Tests/SentryTraceMetricsTests.cs index 18496a629d..cfaba091c9 100644 --- a/test/Sentry.Tests/SentryTraceMetricsTests.cs +++ b/test/Sentry.Tests/SentryTraceMetricsTests.cs @@ -118,10 +118,10 @@ public void Emit_WithoutActiveSpan_CapturesEnvelope() public void Emit_WithBeforeSendMetric_InvokesCallback() { var invocations = 0; - SentryMetric configuredMetric = null!; + SentryMetric configuredMetric = null!; Assert.True(_fixture.Options.Experimental.EnableMetrics); - _fixture.Options.Experimental.SetBeforeSendMetric((SentryMetric metric) => + _fixture.Options.Experimental.SetBeforeSendMetric((SentryMetric metric) => { invocations++; configuredMetric = metric; @@ -143,7 +143,7 @@ public void Emit_WhenBeforeSendMetricReturnsNull_DoesNotCaptureEnvelope() var invocations = 0; Assert.True(_fixture.Options.Experimental.EnableMetrics); - _fixture.Options.Experimental.SetBeforeSendMetric((SentryMetric metric) => + _fixture.Options.Experimental.SetBeforeSendMetric((SentryMetric metric) => { invocations++; return null; @@ -160,7 +160,7 @@ public void Emit_WhenBeforeSendMetricReturnsNull_DoesNotCaptureEnvelope() public void Emit_InvalidBeforeSendMetric_DoesNotCaptureEnvelope() { Assert.True(_fixture.Options.Experimental.EnableMetrics); - _fixture.Options.Experimental.SetBeforeSendMetric(static (SentryMetric metric) => throw new InvalidOperationException()); + _fixture.Options.Experimental.SetBeforeSendMetric(static (SentryMetric metric) => throw new InvalidOperationException()); var metrics = _fixture.GetSut(); metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1); @@ -242,16 +242,17 @@ public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, items.Length.Should().Be(1); var cast = items[0] as SentryMetric; Assert.NotNull(cast); - AssertMetric(fixture, cast, type); + AssertMetric(fixture, cast, type); } - public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, SentryMetric metric, SentryMetricType type) where T : struct + public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, SentryMetric metric, SentryMetricType type) where T : struct { + metric.Should().BeOfType>(); metric.Timestamp.Should().Be(fixture.Clock.GetUtcNow()); metric.TraceId.Should().Be(fixture.TraceId); metric.Type.Should().Be(type); metric.Name.Should().Be("sentry_tests.sentry_trace_metrics_tests.counter"); - metric.Value.Should().Be(1); + metric.Value.Should().BeOfType().And.Be(1); metric.SpanId.Should().Be(fixture.SpanId); if (metric.Type is SentryMetricType.Gauge or SentryMetricType.Distribution) { @@ -262,6 +263,9 @@ public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, metric.Unit.Should().BeNull(); } + metric.TryGetValue(out var match).Should().BeTrue(); + match.Should().NotBe(default(T)); + foreach (var expectedAttribute in fixture.ExpectedAttributes) { metric.TryGetAttribute(expectedAttribute.Key, out string? value).Should().BeTrue(); From afb011bf931b3e500c9aa1e73e117f6d6c367987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:25:12 +0100 Subject: [PATCH 19/26] ref: cleanup unused code --- src/Sentry/SentryOptions.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 645975f580..7fc69a600f 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1370,8 +1370,6 @@ public SentryOptions() ); NetworkStatusListener = new PollingNetworkStatusListener(this); - - Experimental = new ExperimentalSentryOptions(this); } /// @@ -1914,7 +1912,7 @@ internal static List GetDefaultInAppExclude() => /// /// Experimental features are subject to binary, source and behavioral breaking changes in future updates. /// - public ExperimentalSentryOptions Experimental { get; } + public ExperimentalSentryOptions Experimental { get; } = new ExperimentalSentryOptions(); /// /// Sentry features that are currently in an experimental state. @@ -1924,12 +1922,10 @@ internal static List GetDefaultInAppExclude() => /// public class ExperimentalSentryOptions { - private readonly SentryOptions _options; private Func? _beforeSendMetric; - internal ExperimentalSentryOptions(SentryOptions options) + internal ExperimentalSentryOptions() { - _options = options; } internal Func? BeforeSendMetricInternal => _beforeSendMetric; From b0786216f5e5c09bb0d1c9af1921ae0bc0c00322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:24:57 +0100 Subject: [PATCH 20/26] ref: remove unused code --- src/Sentry/SentryLog.cs | 1 - src/Sentry/SentryMetric.Factory.cs | 2 -- src/Sentry/SentryMetric.cs | 5 ----- 3 files changed, 8 deletions(-) diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index b77e7fe793..c57a08a84d 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -189,7 +189,6 @@ internal void SetOrigin(string origin) SetAttribute("sentry.origin", origin); } - /// internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index 2d78b1ad94..5fe2b79036 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -22,7 +22,6 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst scope ??= hub.GetScope(); metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); - metric.Apply(scope); metric.SetAttributes(attributes); @@ -46,7 +45,6 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst scope ??= hub.GetScope(); metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); - metric.Apply(scope); metric.SetAttributes(attributes); diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs index 06d31552f9..914e128b8c 100644 --- a/src/Sentry/SentryMetric.cs +++ b/src/Sentry/SentryMetric.cs @@ -271,11 +271,6 @@ internal void SetAttributes(ReadOnlySpan> attribute } } - private void Apply(Scope? scope) - { - //TODO: https://github.com/getsentry/sentry-dotnet/issues/4882 - } - /// internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { From a9e4e9abffc34a81d6fe94ff20579807a0fb4ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:29:56 +0100 Subject: [PATCH 21/26] ref: de-duplicate internal method and unit tests --- .../SentryStructuredLogger.cs | 2 +- src/Sentry.Serilog/SentrySink.Structured.cs | 2 +- src/Sentry/HubExtensions.cs | 30 +++++++++++ .../Internal/DefaultSentryStructuredLogger.cs | 2 +- src/Sentry/SentryLog.cs | 25 --------- src/Sentry/SentryMetric.Factory.cs | 30 +---------- test/Sentry.Tests/HubExtensionsTests.cs | 52 +++++++++++++++++++ test/Sentry.Tests/SentryLogTests.cs | 52 ------------------- test/Sentry.Tests/SentryMetricTests.cs | 52 ------------------- 9 files changed, 87 insertions(+), 160 deletions(-) diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs index 6567811762..e4f5b500d9 100644 --- a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs +++ b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs @@ -41,7 +41,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } var timestamp = _clock.GetUtcNow(); - SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId); + _hub.GetTraceIdAndSpanId(out var traceId, out var spanId); var level = logLevel.ToSentryLogLevel(); Debug.Assert(level != default); diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index cdff9166ac..535c4d1656 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -7,7 +7,7 @@ internal sealed partial class SentrySink { private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEvent logEvent, string formatted, string? template) { - SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + hub.GetTraceIdAndSpanId(out var traceId, out var spanId); GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes); SentryLog log = new(logEvent.Timestamp, traceId, logEvent.Level.ToSentryLogLevel(), formatted) diff --git a/src/Sentry/HubExtensions.cs b/src/Sentry/HubExtensions.cs index 4414c34a64..f48081d0c2 100644 --- a/src/Sentry/HubExtensions.cs +++ b/src/Sentry/HubExtensions.cs @@ -287,4 +287,34 @@ internal static ITransactionTracer StartTransaction( hub.ConfigureScope(scope => current = scope); return current; } + + /// + /// Get of either the currently active Span, or the current Scope. + /// Get only if there is a currently active Span. + /// + /// + /// Intended for use by and . + /// + internal static void GetTraceIdAndSpanId(this IHub hub, out SentryId traceId, out SpanId? spanId) + { + var activeSpan = hub.GetSpan(); + if (activeSpan is not null) + { + traceId = activeSpan.TraceId; + spanId = activeSpan.SpanId; + return; + } + + var scope = hub.GetScope(); + if (scope is not null) + { + traceId = scope.PropagationContext.TraceId; + spanId = null; + return; + } + + Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable."); + traceId = SentryId.Empty; + spanId = null; + } } diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 9d4883f716..3ee39b9b9a 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -28,7 +28,7 @@ internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemC private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action? configureLog) { var timestamp = _clock.GetUtcNow(); - SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId); + _hub.GetTraceIdAndSpanId(out var traceId, out var spanId); string message; try diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index c57a08a84d..9520d7ccc9 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -245,29 +245,4 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndObject(); } - - internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) - { - var activeSpan = hub.GetSpan(); - if (activeSpan is not null) - { - traceId = activeSpan.TraceId; - spanId = activeSpan.SpanId; - return; - } - - // set "span_id" to the ID of the Span that was active when the Log was collected - // do not set "span_id" if there was no active Span - spanId = null; - - var scope = hub.GetScope(); - if (scope is not null) - { - traceId = scope.PropagationContext.TraceId; - return; - } - - Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable."); - traceId = SentryId.Empty; - } } diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index 5fe2b79036..4257ddae44 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -1,5 +1,4 @@ using Sentry.Infrastructure; -using Sentry.Internal; namespace Sentry; @@ -12,7 +11,7 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); var timestamp = clock.GetUtcNow(); - GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + hub.GetTraceIdAndSpanId(out var traceId, out var spanId); var metric = new SentryMetric(timestamp, traceId, type, name, value) { @@ -35,7 +34,7 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); var timestamp = clock.GetUtcNow(); - GetTraceIdAndSpanId(hub, out var traceId, out var spanId); + hub.GetTraceIdAndSpanId(out var traceId, out var spanId); var metric = new SentryMetric(timestamp, traceId, type, name, value) { @@ -51,31 +50,6 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst return metric; } - internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) - { - var activeSpan = hub.GetSpan(); - if (activeSpan is not null) - { - traceId = activeSpan.TraceId; - spanId = activeSpan.SpanId; - return; - } - - // set "span_id" to the ID of the Span that was active when the Metric was emitted - // do not set "span_id" if there was no active Span - spanId = null; - - var scope = hub.GetScope(); - if (scope is not null) - { - traceId = scope.PropagationContext.TraceId; - return; - } - - Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable."); - traceId = SentryId.Empty; - } - private static bool IsSupported() where T : struct { var valueType = typeof(T); diff --git a/test/Sentry.Tests/HubExtensionsTests.cs b/test/Sentry.Tests/HubExtensionsTests.cs index 280518b13d..83295c2134 100644 --- a/test/Sentry.Tests/HubExtensionsTests.cs +++ b/test/Sentry.Tests/HubExtensionsTests.cs @@ -108,4 +108,56 @@ public void AddBreadcrumb_AllFields_CreatesBreadcrumb() Assert.Equal(expectedMessage, crumb.Message); Assert.Equal(expectedTimestamp, crumb.Timestamp); } + + [Fact] + public void GetTraceIdAndSpanId_WithActiveSpan_HasBothTraceIdAndSpanId() + { + // Arrange + var span = Substitute.For(); + span.TraceId.Returns(SentryId.Create()); + span.SpanId.Returns(Sentry.SpanId.Create()); + + var hub = Substitute.For(); + hub.GetSpan().Returns(span); + + // Act + hub.GetTraceIdAndSpanId(out var traceId, out var spanId); + + // Assert + traceId.Should().Be(span.TraceId); + spanId.Should().Be(span.SpanId); + } + + [Fact] + public void GetTraceIdAndSpanId_WithoutActiveSpan_HasOnlyTraceIdButNoSpanId() + { + // Arrange + var hub = Substitute.For(); + hub.GetSpan().Returns((ISpan)null); + + var scope = new Scope(); + hub.SubstituteConfigureScope(scope); + + // Act + hub.GetTraceIdAndSpanId(out var traceId, out var spanId); + + // Assert + traceId.Should().Be(scope.PropagationContext.TraceId); + spanId.Should().BeNull(); + } + + [Fact] + public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable() + { + // Arrange + var hub = Substitute.For(); + hub.GetSpan().Returns((ISpan)null); + + // Act + hub.GetTraceIdAndSpanId(out var traceId, out var spanId); + + // Assert + traceId.Should().Be(SentryId.Empty); + spanId.Should().BeNull(); + } } diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index 0901898680..e6b3ccdcb3 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -406,58 +406,6 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*null*is not supported*ignored*") ); } - - [Fact] - public void GetTraceIdAndSpanId_WithActiveSpan_HasBothTraceIdAndSpanId() - { - // Arrange - var span = Substitute.For(); - span.TraceId.Returns(SentryId.Create()); - span.SpanId.Returns(Sentry.SpanId.Create()); - - var hub = Substitute.For(); - hub.GetSpan().Returns(span); - - // Act - SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); - - // Assert - traceId.Should().Be(span.TraceId); - spanId.Should().Be(span.SpanId); - } - - [Fact] - public void GetTraceIdAndSpanId_WithoutActiveSpan_HasOnlyTraceIdButNoSpanId() - { - // Arrange - var hub = Substitute.For(); - hub.GetSpan().Returns((ISpan)null); - - var scope = new Scope(); - hub.SubstituteConfigureScope(scope); - - // Act - SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); - - // Assert - traceId.Should().Be(scope.PropagationContext.TraceId); - spanId.Should().BeNull(); - } - - [Fact] - public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable() - { - // Arrange - var hub = Substitute.For(); - hub.GetSpan().Returns((ISpan)null); - - // Act - SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); - - // Assert - traceId.Should().Be(SentryId.Empty); - spanId.Should().BeNull(); - } } file static class AssertExtensions diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs index 1672574679..2614c99989 100644 --- a/test/Sentry.Tests/SentryMetricTests.cs +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -395,58 +395,6 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*null*is not supported*ignored*") ); } - - [Fact] - public void GetTraceIdAndSpanId_WithActiveSpan_HasBothTraceIdAndSpanId() - { - // Arrange - var span = Substitute.For(); - span.TraceId.Returns(SentryId.Create()); - span.SpanId.Returns(Sentry.SpanId.Create()); - - var hub = Substitute.For(); - hub.GetSpan().Returns(span); - - // Act - SentryMetric.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); - - // Assert - traceId.Should().Be(span.TraceId); - spanId.Should().Be(span.SpanId); - } - - [Fact] - public void GetTraceIdAndSpanId_WithoutActiveSpan_HasOnlyTraceIdButNoSpanId() - { - // Arrange - var hub = Substitute.For(); - hub.GetSpan().Returns((ISpan)null); - - var scope = new Scope(); - hub.SubstituteConfigureScope(scope); - - // Act - SentryMetric.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); - - // Assert - traceId.Should().Be(scope.PropagationContext.TraceId); - spanId.Should().BeNull(); - } - - [Fact] - public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable() - { - // Arrange - var hub = Substitute.For(); - hub.GetSpan().Returns((ISpan)null); - - // Act - SentryMetric.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); - - // Assert - traceId.Should().Be(SentryId.Empty); - spanId.Should().BeNull(); - } } file static class AssertExtensions From be699bfbce9432956784ae880ad68862a22c6b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:13:04 +0100 Subject: [PATCH 22/26] ref!: rename "trace_metric" type --- src/Sentry/Extensibility/DisabledHub.cs | 2 +- src/Sentry/Extensibility/HubAdapter.cs | 2 +- src/Sentry/IHub.cs | 2 +- ...trics.cs => DefaultSentryMetricEmitter.cs} | 4 +- ...rics.cs => DisabledSentryMetricEmitter.cs} | 6 +- src/Sentry/Internal/Hub.cs | 6 +- ...nter.cs => SentryMetricEmitter.Counter.cs} | 2 +- ...cs => SentryMetricEmitter.Distribution.cs} | 2 +- ....Gauge.cs => SentryMetricEmitter.Gauge.cs} | 2 +- ...TraceMetrics.cs => SentryMetricEmitter.cs} | 12 +-- src/Sentry/SentrySdk.cs | 2 +- ...rics.cs => InMemorySentryMetricEmitter.cs} | 2 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 80 +++++++++---------- ...piApprovalTests.Run.DotNet8_0.verified.txt | 80 +++++++++---------- ...piApprovalTests.Run.DotNet9_0.verified.txt | 80 +++++++++---------- .../ApiApprovalTests.Run.Net4_8.verified.txt | 80 +++++++++---------- .../Extensibility/DisabledHubTests.cs | 2 +- .../Extensibility/HubAdapterTests.cs | 2 +- test/Sentry.Tests/HubTests.cs | 12 +-- ...cs => SentryMetricEmitterTests.Options.cs} | 2 +- ...s.cs => SentryMetricEmitterTests.Types.cs} | 8 +- ....cs => SentryMetricEmitterTests.Values.cs} | 2 +- ...csTests.cs => SentryMetricEmitterTests.cs} | 20 ++--- 23 files changed, 206 insertions(+), 206 deletions(-) rename src/Sentry/Internal/{DefaultSentryTraceMetrics.cs => DefaultSentryMetricEmitter.cs} (94%) rename src/Sentry/Internal/{DisabledSentryTraceMetrics.cs => DisabledSentryMetricEmitter.cs} (78%) rename src/Sentry/{SentryTraceMetrics.Counter.cs => SentryMetricEmitter.Counter.cs} (98%) rename src/Sentry/{SentryTraceMetrics.Distribution.cs => SentryMetricEmitter.Distribution.cs} (99%) rename src/Sentry/{SentryTraceMetrics.Gauge.cs => SentryMetricEmitter.Gauge.cs} (99%) rename src/Sentry/{SentryTraceMetrics.cs => SentryMetricEmitter.cs} (86%) rename test/Sentry.Testing/{InMemorySentryTraceMetrics.cs => InMemorySentryMetricEmitter.cs} (98%) rename test/Sentry.Tests/{SentryTraceMetricsTests.Options.cs => SentryMetricEmitterTests.Options.cs} (95%) rename test/Sentry.Tests/{SentryTraceMetricsTests.Types.cs => SentryMetricEmitterTests.Types.cs} (96%) rename test/Sentry.Tests/{SentryTraceMetricsTests.Values.cs => SentryMetricEmitterTests.Values.cs} (98%) rename test/Sentry.Tests/{SentryTraceMetricsTests.cs => SentryMetricEmitterTests.cs} (91%) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 6c6a7ae31d..03f70ff591 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -271,5 +271,5 @@ public void Dispose() /// Disabled Metrics. /// [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public SentryTraceMetrics Metrics => DisabledSentryTraceMetrics.Instance; + public SentryMetricEmitter Metrics => DisabledSentryMetricEmitter.Instance; } diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 6ec4c79104..2db6900259 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -40,7 +40,7 @@ private HubAdapter() { } /// Forwards the call to . /// [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => SentrySdk.Experimental.Metrics; } + public SentryMetricEmitter Metrics { [DebuggerStepThrough] get => SentrySdk.Experimental.Metrics; } /// /// Forwards the call to . diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index bdab68b2cf..a5a7afd9c9 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -40,7 +40,7 @@ public interface IHub : ISentryClient, ISentryScopeManager /// /// [Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public SentryTraceMetrics Metrics { get; } + public SentryMetricEmitter Metrics { get; } /// /// Starts a transaction. diff --git a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs similarity index 94% rename from src/Sentry/Internal/DefaultSentryTraceMetrics.cs rename to src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 446a045ebb..8cc2349930 100644 --- a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -4,7 +4,7 @@ namespace Sentry.Internal; -internal sealed class DefaultSentryTraceMetrics : SentryTraceMetrics, IDisposable +internal sealed class DefaultSentryMetricEmitter : SentryMetricEmitter, IDisposable { private readonly IHub _hub; private readonly SentryOptions _options; @@ -12,7 +12,7 @@ internal sealed class DefaultSentryTraceMetrics : SentryTraceMetrics, IDisposabl private readonly BatchProcessor _batchProcessor; - internal DefaultSentryTraceMetrics(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) + internal DefaultSentryMetricEmitter(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) { Debug.Assert(hub.IsEnabled); Debug.Assert(options.Experimental is { EnableMetrics: true }); diff --git a/src/Sentry/Internal/DisabledSentryTraceMetrics.cs b/src/Sentry/Internal/DisabledSentryMetricEmitter.cs similarity index 78% rename from src/Sentry/Internal/DisabledSentryTraceMetrics.cs rename to src/Sentry/Internal/DisabledSentryMetricEmitter.cs index 7d8c10e4da..1afef5e90b 100644 --- a/src/Sentry/Internal/DisabledSentryTraceMetrics.cs +++ b/src/Sentry/Internal/DisabledSentryMetricEmitter.cs @@ -1,10 +1,10 @@ namespace Sentry.Internal; -internal sealed class DisabledSentryTraceMetrics : SentryTraceMetrics +internal sealed class DisabledSentryMetricEmitter : SentryMetricEmitter { - internal static DisabledSentryTraceMetrics Instance { get; } = new DisabledSentryTraceMetrics(); + internal static DisabledSentryMetricEmitter Instance { get; } = new DisabledSentryMetricEmitter(); - internal DisabledSentryTraceMetrics() + internal DisabledSentryMetricEmitter() { } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index dd789b9155..6d10090183 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -81,7 +81,7 @@ internal Hub( } Logger = SentryStructuredLogger.Create(this, options, _clock); - Metrics = SentryTraceMetrics.Create(this, options, _clock); + Metrics = SentryMetricEmitter.Create(this, options, _clock); #if MEMORY_DUMP_SUPPORTED if (options.HeapDumpOptions is not null) @@ -856,7 +856,7 @@ public void Dispose() Logger.Flush(); Metrics.Flush(); (Logger as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryStructuredLogger - (Metrics as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryTraceMetrics + (Metrics as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryMetricEmitter try { @@ -888,5 +888,5 @@ public void Dispose() public SentryStructuredLogger Logger { get; } - public SentryTraceMetrics Metrics { get; } + public SentryMetricEmitter Metrics { get; } } diff --git a/src/Sentry/SentryTraceMetrics.Counter.cs b/src/Sentry/SentryMetricEmitter.Counter.cs similarity index 98% rename from src/Sentry/SentryTraceMetrics.Counter.cs rename to src/Sentry/SentryMetricEmitter.Counter.cs index 60f87f2d78..74dd577a5c 100644 --- a/src/Sentry/SentryTraceMetrics.Counter.cs +++ b/src/Sentry/SentryMetricEmitter.Counter.cs @@ -1,6 +1,6 @@ namespace Sentry; -public abstract partial class SentryTraceMetrics +public abstract partial class SentryMetricEmitter { /// /// Increment a counter. diff --git a/src/Sentry/SentryTraceMetrics.Distribution.cs b/src/Sentry/SentryMetricEmitter.Distribution.cs similarity index 99% rename from src/Sentry/SentryTraceMetrics.Distribution.cs rename to src/Sentry/SentryMetricEmitter.Distribution.cs index 2e8a2fba15..45dfbcbdfb 100644 --- a/src/Sentry/SentryTraceMetrics.Distribution.cs +++ b/src/Sentry/SentryMetricEmitter.Distribution.cs @@ -1,6 +1,6 @@ namespace Sentry; -public abstract partial class SentryTraceMetrics +public abstract partial class SentryMetricEmitter { /// /// Add a distribution value. diff --git a/src/Sentry/SentryTraceMetrics.Gauge.cs b/src/Sentry/SentryMetricEmitter.Gauge.cs similarity index 99% rename from src/Sentry/SentryTraceMetrics.Gauge.cs rename to src/Sentry/SentryMetricEmitter.Gauge.cs index 4e1d45f986..22c4f44dc9 100644 --- a/src/Sentry/SentryTraceMetrics.Gauge.cs +++ b/src/Sentry/SentryMetricEmitter.Gauge.cs @@ -1,6 +1,6 @@ namespace Sentry; -public abstract partial class SentryTraceMetrics +public abstract partial class SentryMetricEmitter { /// /// Set a gauge value. diff --git a/src/Sentry/SentryTraceMetrics.cs b/src/Sentry/SentryMetricEmitter.cs similarity index 86% rename from src/Sentry/SentryTraceMetrics.cs rename to src/Sentry/SentryMetricEmitter.cs index 27376c71cb..f2b8c485b3 100644 --- a/src/Sentry/SentryTraceMetrics.cs +++ b/src/Sentry/SentryMetricEmitter.cs @@ -6,19 +6,19 @@ namespace Sentry; /// /// Creates and sends metrics to Sentry. /// -public abstract partial class SentryTraceMetrics +public abstract partial class SentryMetricEmitter { - internal static SentryTraceMetrics Create(IHub hub, SentryOptions options, ISystemClock clock) + internal static SentryMetricEmitter Create(IHub hub, SentryOptions options, ISystemClock clock) => Create(hub, options, clock, 100, TimeSpan.FromSeconds(5)); - internal static SentryTraceMetrics Create(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) + internal static SentryMetricEmitter Create(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval) { return options.Experimental.EnableMetrics - ? new DefaultSentryTraceMetrics(hub, options, clock, batchCount, batchInterval) - : DisabledSentryTraceMetrics.Instance; + ? new DefaultSentryMetricEmitter(hub, options, clock, batchCount, batchInterval) + : DisabledSentryMetricEmitter.Instance; } - private protected SentryTraceMetrics() + private protected SentryMetricEmitter() { } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 57b79673ed..a7a5f7b096 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -880,7 +880,7 @@ internal ExperimentalSentrySdk() #pragma warning disable SENTRYTRACECONNECTEDMETRICS /// - public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => CurrentHub.Metrics; } + public SentryMetricEmitter Metrics { [DebuggerStepThrough] get => CurrentHub.Metrics; } #pragma warning restore SENTRYTRACECONNECTEDMETRICS } } diff --git a/test/Sentry.Testing/InMemorySentryTraceMetrics.cs b/test/Sentry.Testing/InMemorySentryMetricEmitter.cs similarity index 98% rename from test/Sentry.Testing/InMemorySentryTraceMetrics.cs rename to test/Sentry.Testing/InMemorySentryMetricEmitter.cs index d35e02c23f..ecc5e575fa 100644 --- a/test/Sentry.Testing/InMemorySentryTraceMetrics.cs +++ b/test/Sentry.Testing/InMemorySentryMetricEmitter.cs @@ -2,7 +2,7 @@ namespace Sentry.Testing; -public sealed class InMemorySentryTraceMetrics : SentryTraceMetrics +public sealed class InMemorySentryMetricEmitter : SentryMetricEmitter { public List Entries { get; } = new(); internal List Metrics { get; } = new(); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index fcebbb5269..a2182bb19b 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -198,7 +198,7 @@ namespace Sentry Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - Sentry.SentryTraceMetrics Metrics { get; } + Sentry.SentryMetricEmitter Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -676,6 +676,42 @@ namespace Sentry public abstract bool TryGetValue(out TValue value) where TValue : struct; } + public abstract class SentryMetricEmitter + { + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } public enum SentryMetricType { Counter = 0, @@ -921,7 +957,7 @@ namespace Sentry public static void UnsetTag(string key) { } public sealed class ExperimentalSentrySdk { - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } } } public class SentrySession : Sentry.ISentrySession @@ -1040,42 +1076,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } - public abstract class SentryTraceMetrics - { - public void EmitCounter(string name, T value) - where T : struct { } - public void EmitCounter(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value) - where T : struct { } - public void EmitDistribution(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value) - where T : struct { } - public void EmitGauge(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - protected abstract void Flush(); - } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1487,7 +1487,7 @@ namespace Sentry.Extensibility public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1536,7 +1536,7 @@ namespace Sentry.Extensibility public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index fcebbb5269..a2182bb19b 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -198,7 +198,7 @@ namespace Sentry Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - Sentry.SentryTraceMetrics Metrics { get; } + Sentry.SentryMetricEmitter Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -676,6 +676,42 @@ namespace Sentry public abstract bool TryGetValue(out TValue value) where TValue : struct; } + public abstract class SentryMetricEmitter + { + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } public enum SentryMetricType { Counter = 0, @@ -921,7 +957,7 @@ namespace Sentry public static void UnsetTag(string key) { } public sealed class ExperimentalSentrySdk { - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } } } public class SentrySession : Sentry.ISentrySession @@ -1040,42 +1076,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } - public abstract class SentryTraceMetrics - { - public void EmitCounter(string name, T value) - where T : struct { } - public void EmitCounter(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value) - where T : struct { } - public void EmitDistribution(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value) - where T : struct { } - public void EmitGauge(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - protected abstract void Flush(); - } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1487,7 +1487,7 @@ namespace Sentry.Extensibility public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1536,7 +1536,7 @@ namespace Sentry.Extensibility public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index fcebbb5269..a2182bb19b 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -198,7 +198,7 @@ namespace Sentry Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - Sentry.SentryTraceMetrics Metrics { get; } + Sentry.SentryMetricEmitter Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -676,6 +676,42 @@ namespace Sentry public abstract bool TryGetValue(out TValue value) where TValue : struct; } + public abstract class SentryMetricEmitter + { + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } public enum SentryMetricType { Counter = 0, @@ -921,7 +957,7 @@ namespace Sentry public static void UnsetTag(string key) { } public sealed class ExperimentalSentrySdk { - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } } } public class SentrySession : Sentry.ISentrySession @@ -1040,42 +1076,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } - public abstract class SentryTraceMetrics - { - public void EmitCounter(string name, T value) - where T : struct { } - public void EmitCounter(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value) - where T : struct { } - public void EmitDistribution(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value) - where T : struct { } - public void EmitGauge(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - protected abstract void Flush(); - } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1487,7 +1487,7 @@ namespace Sentry.Extensibility public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1536,7 +1536,7 @@ namespace Sentry.Extensibility public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 7b4c4dc222..669e2b095d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -185,7 +185,7 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } - Sentry.SentryTraceMetrics Metrics { get; } + Sentry.SentryMetricEmitter Metrics { get; } void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); @@ -662,6 +662,42 @@ namespace Sentry public abstract bool TryGetValue(out TValue value) where TValue : struct; } + public abstract class SentryMetricEmitter + { + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } public enum SentryMetricType { Counter = 0, @@ -901,7 +937,7 @@ namespace Sentry public static void UnsetTag(string key) { } public sealed class ExperimentalSentrySdk { - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } } } public class SentrySession : Sentry.ISentrySession @@ -1020,42 +1056,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } - public abstract class SentryTraceMetrics - { - public void EmitCounter(string name, T value) - where T : struct { } - public void EmitCounter(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value) - where T : struct { } - public void EmitDistribution(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value) - where T : struct { } - public void EmitGauge(string name, T value, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) - where T : struct { } - public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) - where T : struct { } - protected abstract void Flush(); - } public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public SentryTransaction(Sentry.ITransactionTracer tracer) { } @@ -1466,7 +1466,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } @@ -1514,7 +1514,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } - public Sentry.SentryTraceMetrics Metrics { get; } + public Sentry.SentryMetricEmitter Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/Extensibility/DisabledHubTests.cs b/test/Sentry.Tests/Extensibility/DisabledHubTests.cs index 084c5e220b..40d99b2ba1 100644 --- a/test/Sentry.Tests/Extensibility/DisabledHubTests.cs +++ b/test/Sentry.Tests/Extensibility/DisabledHubTests.cs @@ -42,5 +42,5 @@ public void Logger_IsDisabled() [Fact] public void Metrics_IsDisabled() - => Assert.IsType(DisabledHub.Instance.Metrics); + => Assert.IsType(DisabledHub.Instance.Metrics); } diff --git a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs index 2a11f29bea..f6625270b5 100644 --- a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs +++ b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs @@ -85,7 +85,7 @@ public void Logger_MockInvoked() [Fact] public void Metrics_MockInvoked() { - var metrics = new InMemorySentryTraceMetrics(); + var metrics = new InMemorySentryMetricEmitter(); Hub.Metrics.Returns(metrics); HubAdapter.Instance.Metrics.EmitCounter("sentry_tests.hub_adapter_tests.counter", 1); diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 395544c947..c8bb6ea702 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1896,7 +1896,7 @@ public void Metrics_IsDisabled_DoesNotCaptureMetric() envelope.Items.Single(item => item.Header["type"].Equals("trace_metric")).Payload.GetType().IsAssignableFrom(typeof(JsonSerializable)) ) ); - hub.Metrics.Should().BeOfType(); + hub.Metrics.Should().BeOfType(); } [Fact] @@ -1916,7 +1916,7 @@ public void Metrics_IsEnabled_DoesCaptureMetric() envelope.Items.Single(item => item.Header["type"].Equals("trace_metric")).Payload.GetType().IsAssignableFrom(typeof(JsonSerializable)) ) ); - hub.Metrics.Should().BeOfType(); + hub.Metrics.Should().BeOfType(); } [Fact] @@ -1930,7 +1930,7 @@ public void Metrics_EnableAfterCreate_HasNoEffect() _fixture.Options.Experimental.EnableMetrics = true; // Assert - hub.Metrics.Should().BeOfType(); + hub.Metrics.Should().BeOfType(); } [Fact] @@ -1944,7 +1944,7 @@ public void Metrics_DisableAfterCreate_HasNoEffect() _fixture.Options.Experimental.EnableMetrics = false; // Assert - hub.Metrics.Should().BeOfType(); + hub.Metrics.Should().BeOfType(); } [Fact] @@ -1969,7 +1969,7 @@ await _fixture.Client.Received(1).FlushAsync( timeout.Equals(_fixture.Options.FlushTimeout) ) ); - hub.Metrics.Should().BeOfType(); + hub.Metrics.Should().BeOfType(); } [Fact] @@ -1994,7 +1994,7 @@ public void Metrics_Dispose_DoesCaptureMetric() timeout.Equals(_fixture.Options.ShutdownTimeout) ) ); - hub.Metrics.Should().BeOfType(); + hub.Metrics.Should().BeOfType(); } [Fact] diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Options.cs similarity index 95% rename from test/Sentry.Tests/SentryTraceMetricsTests.Options.cs rename to test/Sentry.Tests/SentryMetricEmitterTests.Options.cs index 744f445b63..e6c5d10132 100644 --- a/test/Sentry.Tests/SentryTraceMetricsTests.Options.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Options.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -public partial class SentryTraceMetricsTests +public partial class SentryMetricEmitterTests { [Fact] public void EnableMetrics_Default_True() diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Types.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs similarity index 96% rename from test/Sentry.Tests/SentryTraceMetricsTests.Types.cs rename to test/Sentry.Tests/SentryMetricEmitterTests.Types.cs index 432c27b254..fe98d1c940 100644 --- a/test/Sentry.Tests/SentryTraceMetricsTests.Types.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -public partial class SentryTraceMetricsTests +public partial class SentryMetricEmitterTests { [Theory] [InlineData(SentryMetricType.Counter)] @@ -289,9 +289,9 @@ public void Emit_Name_Empty_DoesNotCaptureEnvelope(SentryMetricType type, string } } -file static class SentryTraceMetricsExtensions +file static class SentryMetricEmitterExtensions { - public static void Emit(this SentryTraceMetrics metrics, SentryMetricType type, T value, ReadOnlySpan> attributes) where T : struct + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, ReadOnlySpan> attributes) where T : struct { switch (type) { @@ -309,7 +309,7 @@ public static void Emit(this SentryTraceMetrics metrics, SentryMetricType typ } } - public static void Emit(this SentryTraceMetrics metrics, SentryMetricType type, string name, T value) where T : struct + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, string name, T value) where T : struct { switch (type) { diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.Values.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs similarity index 98% rename from test/Sentry.Tests/SentryTraceMetricsTests.Values.cs rename to test/Sentry.Tests/SentryMetricEmitterTests.Values.cs index 2defb697ca..d5d7d7a85b 100644 --- a/test/Sentry.Tests/SentryTraceMetricsTests.Values.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs @@ -1,6 +1,6 @@ namespace Sentry.Tests; -public partial class SentryTraceMetricsTests +public partial class SentryMetricEmitterTests { [Fact] public void TryGetValue_FromByte_SupportedType() diff --git a/test/Sentry.Tests/SentryTraceMetricsTests.cs b/test/Sentry.Tests/SentryMetricEmitterTests.cs similarity index 91% rename from test/Sentry.Tests/SentryTraceMetricsTests.cs rename to test/Sentry.Tests/SentryMetricEmitterTests.cs index cfaba091c9..c3a47eebda 100644 --- a/test/Sentry.Tests/SentryTraceMetricsTests.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.cs @@ -5,7 +5,7 @@ namespace Sentry.Tests; /// /// /// -public partial class SentryTraceMetricsTests : IDisposable +public partial class SentryMetricEmitterTests : IDisposable { internal sealed class Fixture { @@ -58,12 +58,12 @@ public void WithoutActiveSpan() SpanId = null; } - public SentryTraceMetrics GetSut() => SentryTraceMetrics.Create(Hub, Options, Clock, BatchSize, BatchTimeout); + public SentryMetricEmitter GetSut() => SentryMetricEmitter.Create(Hub, Options, Clock, BatchSize, BatchTimeout); } private readonly Fixture _fixture; - public SentryTraceMetricsTests() + public SentryMetricEmitterTests() { _fixture = new Fixture(); } @@ -81,7 +81,7 @@ public void Create_Enabled_NewDefaultInstance() var instance = _fixture.GetSut(); var other = _fixture.GetSut(); - instance.Should().BeOfType(); + instance.Should().BeOfType(); instance.Should().NotBeSameAs(other); } @@ -93,7 +93,7 @@ public void Create_Disabled_CachedDisabledInstance() var instance = _fixture.GetSut(); var other = _fixture.GetSut(); - instance.Should().BeOfType(); + instance.Should().BeOfType(); instance.Should().BeSameAs(other); } @@ -201,7 +201,7 @@ public void Dispose_BeforeEmit_DoesNotCaptureEnvelope() Assert.True(_fixture.Options.Experimental.EnableMetrics); var metrics = _fixture.GetSut(); - var defaultMetrics = metrics.Should().BeOfType().Which; + var defaultMetrics = metrics.Should().BeOfType().Which; defaultMetrics.Dispose(); metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", 1, [new KeyValuePair("attribute-key", "attribute-value")]); @@ -216,7 +216,7 @@ public void Dispose_BeforeEmit_DoesNotCaptureEnvelope() internal static class MetricsAssertionExtensions { - public static void AssertEnvelope(this SentryTraceMetricsTests.Fixture fixture, Envelope envelope, SentryMetricType type) where T : struct + public static void AssertEnvelope(this SentryMetricEmitterTests.Fixture fixture, Envelope envelope, SentryMetricType type) where T : struct { envelope.Header.Should().ContainSingle().Which.Key.Should().Be("sdk"); var item = envelope.Items.Should().ContainSingle().Which; @@ -230,13 +230,13 @@ public static void AssertEnvelope(this SentryTraceMetricsTests.Fixture fixtur element => Assert.Equal(CreateHeader("content_type", "application/vnd.sentry.items.trace-metric+json"), element)); } - public static void AssertEnvelopeWithoutAttributes(this SentryTraceMetricsTests.Fixture fixture, Envelope envelope, SentryMetricType type) where T : struct + public static void AssertEnvelopeWithoutAttributes(this SentryMetricEmitterTests.Fixture fixture, Envelope envelope, SentryMetricType type) where T : struct { fixture.ExpectedAttributes.Clear(); AssertEnvelope(fixture, envelope, type); } - public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, TraceMetric metric, SentryMetricType type) where T : struct + public static void AssertMetric(this SentryMetricEmitterTests.Fixture fixture, TraceMetric metric, SentryMetricType type) where T : struct { var items = metric.Items; items.Length.Should().Be(1); @@ -245,7 +245,7 @@ public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, AssertMetric(fixture, cast, type); } - public static void AssertMetric(this SentryTraceMetricsTests.Fixture fixture, SentryMetric metric, SentryMetricType type) where T : struct + public static void AssertMetric(this SentryMetricEmitterTests.Fixture fixture, SentryMetric metric, SentryMetricType type) where T : struct { metric.Should().BeOfType>(); metric.Timestamp.Should().Be(fixture.Clock.GetUtcNow()); From 98e991947222b58a0c18014d8605620a765403ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:40:55 +0100 Subject: [PATCH 23/26] ref: make BatchProcessor abstract and derive sealed types for Logs and Metrics --- .../BatchProcessorBenchmarks.cs | 3 +- src/Sentry/Internal/BatchProcessor.cs | 38 ++++++++++++++++--- .../Internal/DefaultSentryMetricEmitter.cs | 3 +- .../Internal/DefaultSentryStructuredLogger.cs | 3 +- src/Sentry/Protocol/StructuredLog.cs | 6 --- src/Sentry/Protocol/TraceMetric.cs | 6 --- src/Sentry/SentryLog.cs | 1 - .../Internals/BatchProcessorTests.cs | 4 +- 8 files changed, 38 insertions(+), 26 deletions(-) diff --git a/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs b/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs index 37e1fcc411..51e0394ee1 100644 --- a/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs +++ b/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using Sentry.Extensibility; using Sentry.Internal; -using Sentry.Protocol; namespace Sentry.Benchmarks; @@ -35,7 +34,7 @@ public void Setup() var clientReportRecorder = new NullClientReportRecorder(); _hub = new Hub(options, DisabledHub.Instance); - _batchProcessor = new BatchProcessor(_hub, BatchCount, batchInterval, StructuredLog.Capture, clientReportRecorder, null); + _batchProcessor = new SentryLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, null); _log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message"); } diff --git a/src/Sentry/Internal/BatchProcessor.cs b/src/Sentry/Internal/BatchProcessor.cs index bb37bc474e..e34148bac9 100644 --- a/src/Sentry/Internal/BatchProcessor.cs +++ b/src/Sentry/Internal/BatchProcessor.cs @@ -1,4 +1,6 @@ using Sentry.Extensibility; +using Sentry.Protocol; +using Sentry.Protocol.Envelopes; namespace Sentry.Internal; @@ -30,10 +32,9 @@ namespace Sentry.Internal; /// OpenTelemetry Batch Processor /// Sentry Logs /// Sentry Metrics -internal sealed class BatchProcessor : IDisposable where TItem : notnull +internal abstract class BatchProcessor : IDisposable where TItem : notnull { private readonly IHub _hub; - private readonly Action _sendAction; private readonly IClientReportRecorder _clientReportRecorder; private readonly IDiagnosticLogger? _diagnosticLogger; @@ -41,10 +42,9 @@ internal sealed class BatchProcessor : IDisposable where TItem : notnull private readonly BatchBuffer _buffer2; private volatile BatchBuffer _activeBuffer; - public BatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, Action sendAction, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger) + protected BatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger) { _hub = hub; - _sendAction = sendAction; _clientReportRecorder = clientReportRecorder; _diagnosticLogger = diagnosticLogger; @@ -125,10 +125,12 @@ private void CaptureItems(BatchBuffer buffer) if (items is not null && items.Length != 0) { - _sendAction(_hub, items); + CaptureEnvelope(_hub, items); } } + protected abstract void CaptureEnvelope(IHub hub, TItem[] items); + private void OnTimeoutExceeded(BatchBuffer buffer) { if (!buffer.IsEmpty) @@ -144,3 +146,29 @@ public void Dispose() _buffer2.Dispose(); } } + +internal sealed class SentryLogBatchProcessor : BatchProcessor +{ + internal SentryLogBatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger) + : base(hub, batchCount, batchInterval, clientReportRecorder, diagnosticLogger) + { + } + + protected override void CaptureEnvelope(IHub hub, SentryLog[] items) + { + _ = hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(items))); + } +} + +internal sealed class SentryMetricBatchProcessor : BatchProcessor +{ + internal SentryMetricBatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger) + : base(hub, batchCount, batchInterval, clientReportRecorder, diagnosticLogger) + { + } + + protected override void CaptureEnvelope(IHub hub, SentryMetric[] items) + { + _ = hub.CaptureEnvelope(Envelope.FromMetric(new TraceMetric(items))); + } +} diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 8cc2349930..b8e25964ca 100644 --- a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -1,6 +1,5 @@ using Sentry.Extensibility; using Sentry.Infrastructure; -using Sentry.Protocol; namespace Sentry.Internal; @@ -21,7 +20,7 @@ internal DefaultSentryMetricEmitter(IHub hub, SentryOptions options, ISystemCloc _options = options; _clock = clock; - _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, TraceMetric.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger); + _batchProcessor = new SentryMetricBatchProcessor(hub, batchCount, batchInterval, _options.ClientReportRecorder, _options.DiagnosticLogger); } /// diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs index 3ee39b9b9a..1d74b6dc5e 100644 --- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs +++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs @@ -1,6 +1,5 @@ using Sentry.Extensibility; using Sentry.Infrastructure; -using Sentry.Protocol; namespace Sentry.Internal; @@ -21,7 +20,7 @@ internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemC _options = options; _clock = clock; - _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, StructuredLog.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger); + _batchProcessor = new SentryLogBatchProcessor(hub, batchCount, batchInterval, _options.ClientReportRecorder, _options.DiagnosticLogger); } /// diff --git a/src/Sentry/Protocol/StructuredLog.cs b/src/Sentry/Protocol/StructuredLog.cs index 6ff161e303..6543d31ffc 100644 --- a/src/Sentry/Protocol/StructuredLog.cs +++ b/src/Sentry/Protocol/StructuredLog.cs @@ -1,5 +1,4 @@ using Sentry.Extensibility; -using Sentry.Protocol.Envelopes; namespace Sentry.Protocol; @@ -35,9 +34,4 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndArray(); writer.WriteEndObject(); } - - internal static void Capture(IHub hub, SentryLog[] logs) - { - _ = hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(logs))); - } } diff --git a/src/Sentry/Protocol/TraceMetric.cs b/src/Sentry/Protocol/TraceMetric.cs index 2ee0ebbce5..2cb63d5c5a 100644 --- a/src/Sentry/Protocol/TraceMetric.cs +++ b/src/Sentry/Protocol/TraceMetric.cs @@ -1,5 +1,4 @@ using Sentry.Extensibility; -using Sentry.Protocol.Envelopes; namespace Sentry.Protocol; @@ -35,9 +34,4 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndArray(); writer.WriteEndObject(); } - - internal static void Capture(IHub hub, SentryMetric[] metrics) - { - _ = hub.CaptureEnvelope(Envelope.FromMetric(new TraceMetric(metrics))); - } } diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 9520d7ccc9..315b0a8a56 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -1,5 +1,4 @@ using Sentry.Extensibility; -using Sentry.Internal; using Sentry.Protocol; namespace Sentry; diff --git a/test/Sentry.Tests/Internals/BatchProcessorTests.cs b/test/Sentry.Tests/Internals/BatchProcessorTests.cs index 83b1feca7f..665097a96a 100644 --- a/test/Sentry.Tests/Internals/BatchProcessorTests.cs +++ b/test/Sentry.Tests/Internals/BatchProcessorTests.cs @@ -5,7 +5,7 @@ namespace Sentry.Tests.Internals; /// /// (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only. /// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both. -/// These tests are still using , rather than . +/// These tests are still using and , rather than and . /// public class BatchProcessorTests : IDisposable { @@ -42,7 +42,7 @@ public void DisableHub() public BatchProcessor GetSut(int batchCount) { - return new BatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, StructuredLog.Capture, ClientReportRecorder, DiagnosticLogger); + return new SentryLogBatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, ClientReportRecorder, DiagnosticLogger); } } From 0dc98384ed5a55b67d778d9023246c2f6a6c6ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:27:47 +0100 Subject: [PATCH 24/26] ref: replace new SentryUnits with existing MeasurementUnit --- .../Sentry.Samples.Console.Basic/Program.cs | 2 +- src/Sentry/MeasurementUnit.cs | 2 + src/Sentry/SentryMetric.cs | 2 +- .../SentryMetricEmitter.Distribution.cs | 64 ++++++ src/Sentry/SentryMetricEmitter.Gauge.cs | 61 ++++++ src/Sentry/SentryMetricEmitter.cs | 6 + src/Sentry/SentryUnits.cs | 178 ----------------- ...iApprovalTests.Run.DotNet10_0.verified.txt | 76 ++++---- ...piApprovalTests.Run.DotNet8_0.verified.txt | 76 ++++---- ...piApprovalTests.Run.DotNet9_0.verified.txt | 76 ++++---- .../ApiApprovalTests.Run.Net4_8.verified.txt | 76 ++++---- test/Sentry.Tests/MeasurementUnitTests.cs | 6 + .../SentryMetricEmitterTests.Types.cs | 98 ++++++++++ .../SentryMetricEmitterTests.Values.cs | 184 ++++++++++++++++++ 14 files changed, 583 insertions(+), 324 deletions(-) delete mode 100644 src/Sentry/SentryUnits.cs diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index 5b3dfcdf46..92ddd4e09f 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -110,7 +110,7 @@ async Task FirstFunction() SentrySdk.Experimental.Metrics.EmitCounter("sentry.samples.console.basic.http_requests_completed", 1); // Distribution-Metric sent as is (see "BeforeSendMetric" callback) - SentrySdk.Experimental.Metrics.EmitDistribution("sentry.samples.console.basic.http_request_duration", stopwatch.Elapsed.TotalSeconds, SentryUnits.Duration.Second, + SentrySdk.Experimental.Metrics.EmitDistribution("sentry.samples.console.basic.http_request_duration", stopwatch.Elapsed.TotalSeconds, MeasurementUnit.Duration.Second, [new KeyValuePair("http.request.method", HttpMethod.Get.Method), new KeyValuePair("http.response.status_code", (int)HttpStatusCode.OK)]); } diff --git a/src/Sentry/MeasurementUnit.cs b/src/Sentry/MeasurementUnit.cs index b572e335bd..48fe7452e5 100644 --- a/src/Sentry/MeasurementUnit.cs +++ b/src/Sentry/MeasurementUnit.cs @@ -75,6 +75,8 @@ internal static MeasurementUnit Parse(string? name) /// public override string ToString() => _unit?.ToString().ToLowerInvariant() ?? _name ?? ""; + internal string? ToNullableString() => _unit?.ToString().ToLowerInvariant() ?? _name; + /// public bool Equals(MeasurementUnit other) => Equals(_unit, other._unit) && _name == other._name; diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs index 914e128b8c..ae56f0e019 100644 --- a/src/Sentry/SentryMetric.cs +++ b/src/Sentry/SentryMetric.cs @@ -295,7 +295,7 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) SpanId.Value.WriteTo(writer, logger); } - if (Unit is not null) + if (!string.IsNullOrEmpty(Unit)) { writer.WriteString("unit", Unit); } diff --git a/src/Sentry/SentryMetricEmitter.Distribution.cs b/src/Sentry/SentryMetricEmitter.Distribution.cs index 45dfbcbdfb..862dd6ce15 100644 --- a/src/Sentry/SentryMetricEmitter.Distribution.cs +++ b/src/Sentry/SentryMetricEmitter.Distribution.cs @@ -22,11 +22,25 @@ public void EmitDistribution(string name, T value) where T : struct /// The unit of measurement. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitDistribution(string name, T value, string? unit) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null); } + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, MeasurementUnit unit) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), [], null); + } + /// /// Add a distribution value. /// @@ -49,11 +63,29 @@ public void EmitDistribution(string name, T value, Scope? scope) where T : st /// The scope to capture the metric with. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitDistribution(string name, T value, string? unit, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, MeasurementUnit unit, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), [], scope); + } + + + /// /// Add a distribution value. /// @@ -64,6 +96,7 @@ public void EmitDistribution(string name, T value, string? unit, Scope? scope /// The scope to capture the metric with. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitDistribution(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); @@ -79,8 +112,39 @@ public void EmitDistribution(string name, T value, string? unit, IEnumerable< /// The scope to capture the metric with. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, MeasurementUnit unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), attributes, scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, MeasurementUnit unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), attributes, scope); + } } diff --git a/src/Sentry/SentryMetricEmitter.Gauge.cs b/src/Sentry/SentryMetricEmitter.Gauge.cs index 22c4f44dc9..5bafa4e242 100644 --- a/src/Sentry/SentryMetricEmitter.Gauge.cs +++ b/src/Sentry/SentryMetricEmitter.Gauge.cs @@ -22,11 +22,25 @@ public void EmitGauge(string name, T value) where T : struct /// The unit of measurement. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitGauge(string name, T value, string? unit) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], null); } + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, MeasurementUnit unit) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit.ToNullableString(), [], null); + } + /// /// Set a gauge value. /// @@ -49,11 +63,26 @@ public void EmitGauge(string name, T value, Scope? scope) where T : struct /// The scope to capture the metric with. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitGauge(string name, T value, string? unit, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope); } + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, MeasurementUnit unit, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit.ToNullableString(), [], scope); + } + /// /// Set a gauge value. /// @@ -64,6 +93,7 @@ public void EmitGauge(string name, T value, string? unit, Scope? scope) where /// The scope to capture the metric with. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitGauge(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); @@ -79,8 +109,39 @@ public void EmitGauge(string name, T value, string? unit, IEnumerableThe scope to capture the metric with. /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, MeasurementUnit unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit.ToNullableString(), attributes, scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] public void EmitGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, MeasurementUnit unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit.ToNullableString(), attributes, scope); + } } diff --git a/src/Sentry/SentryMetricEmitter.cs b/src/Sentry/SentryMetricEmitter.cs index f2b8c485b3..69bc521469 100644 --- a/src/Sentry/SentryMetricEmitter.cs +++ b/src/Sentry/SentryMetricEmitter.cs @@ -61,3 +61,9 @@ private protected SentryMetricEmitter() /// protected internal abstract void Flush(); } + +public abstract partial class SentryMetricEmitter +{ + internal const string ObsoleteStringUnitForwardCompatibility = + $"Custom units may be supported in the future. The {nameof(String)}-based overloads are for forward compatibility. The {nameof(MeasurementUnit)}-based overloads are currently preferred."; +} diff --git a/src/Sentry/SentryUnits.cs b/src/Sentry/SentryUnits.cs deleted file mode 100644 index 94302b40d1..0000000000 --- a/src/Sentry/SentryUnits.cs +++ /dev/null @@ -1,178 +0,0 @@ -namespace Sentry; - -/// -/// Supported units by Relay and Sentry. -/// Applies to , as well as attributes of and attributes of . -/// -/// -/// Contains the units currently supported by Relay and Sentry. -/// For Metrics and Attributes, currently, custom units are not allowed and will be set to "None" by Relay. -/// -/// -/// -public static class SentryUnits -{ - /// - /// Duration Units. - /// - public static class Duration - { - /// - /// Nanosecond unit (ns). - /// 10^-9 seconds. - /// - public static string Nanosecond { get; } = "nanosecond"; - - /// - /// Microsecond unit (μs). - /// 10^-6 seconds. - /// - public static string Microsecond { get; } = "microsecond"; - - /// - /// Millisecond unit (ms). - /// 10^-3 seconds. - /// - public static string Millisecond { get; } = "millisecond"; - - /// - /// Second unit (s). - /// - public static string Second { get; } = "second"; - - /// - /// Minute unit (min). - /// 60 seconds. - /// - public static string Minute { get; } = "minute"; - - /// - /// Hour unit (h). - /// 3_600 seconds. - /// - public static string Hour { get; } = "hour"; - - /// - /// Day unit (d). - /// 86_400 seconds. - /// - public static string Day { get; } = "day"; - - /// - /// Week unit (wk). - /// 604_800 seconds. - /// - public static string Week { get; } = "week"; - } - - /// - /// Information Units. - /// - /// - /// Note that there are computer systems with a different number of bits per byte. - /// - public static class Information - { - /// - /// Bit unit (b). - /// 1/8 of a byte. - /// - public static string Bit { get; } = "bit"; - - /// - /// Byte unit (B). - /// 8 bits. - /// - public static string Byte { get; } = "byte"; - - /// - /// Kilobyte unit (kB). - /// 10^3 bytes = 1_000 bytes (decimal). - /// - public static string Kilobyte { get; } = "kilobyte"; - - /// - /// Kibibyte unit (KiB). - /// 2^10 bytes = 1_024 bytes (binary). - /// - public static string Kibibyte { get; } = "kibibyte"; - - /// - /// Megabyte unit (MB). - /// 10^6 bytes = 1_000_000 bytes (decimal). - /// - public static string Megabyte { get; } = "megabyte"; - - /// - /// Mebibyte unit (MiB). - /// 2^20 bytes = 1_048_576 bytes (binary). - /// - public static string Mebibyte { get; } = "mebibyte"; - - /// - /// Gigabyte unit (GB). - /// 10^9 bytes = 1_000_000_000 bytes (decimal). - /// - public static string Gigabyte { get; } = "gigabyte"; - - /// - /// Gibibyte unit (GiB). - /// 2^30 bytes = 1_073_741_824 bytes (binary). - /// - public static string Gibibyte { get; } = "gibibyte"; - - /// - /// Terabyte unit (TB). - /// 10^12 bytes = 1_000_000_000_000 bytes (decimal). - /// - public static string Terabyte { get; } = "terabyte"; - - /// - /// Tebibyte unit (TiB). - /// 2^40 bytes = 1_099_511_627_776 bytes (binary). - /// - public static string Tebibyte { get; } = "tebibyte"; - - /// - /// Petabyte unit (PB). - /// 10^15 bytes = 1_000_000_000_000_000 bytes (decimal). - /// - public static string Petabyte { get; } = "petabyte"; - - /// - /// Pebibyte unit (PiB). - /// 2^50 bytes = 1_125_899_906_842_624 bytes (binary). - /// - public static string Pebibyte { get; } = "pebibyte"; - - /// - /// Exabyte unit (EB). - /// 10^18 bytes = 1_000_000_000_000_000_000 bytes (decimal). - /// - public static string Exabyte { get; } = "exabyte"; - - /// - /// Exbibyte unit (EiB). - /// 2^60 bytes = 1_152_921_504_606_846_976 bytes (binary). - /// - public static string Exbibyte { get; } = "exbibyte"; - } - - /// - /// Fraction Units. - /// - public static class Fraction - { - /// - /// Ratio unit. - /// Floating point fraction of 1. - /// - public static string Ratio { get; } = "ratio"; - - /// - /// Percent unit (%). - /// Ratio expressed as a fraction of 100. 100% equals a ratio of 1.0. - /// - public static string Percent { get; } = "percent"; - } -} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index a2182bb19b..64869d4587 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -688,26 +688,66 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitDistribution(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitGauge(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void Flush(); @@ -1125,42 +1165,6 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } - public static class SentryUnits - { - public static class Duration - { - public static string Day { get; } - public static string Hour { get; } - public static string Microsecond { get; } - public static string Millisecond { get; } - public static string Minute { get; } - public static string Nanosecond { get; } - public static string Second { get; } - public static string Week { get; } - } - public static class Fraction - { - public static string Percent { get; } - public static string Ratio { get; } - } - public static class Information - { - public static string Bit { get; } - public static string Byte { get; } - public static string Exabyte { get; } - public static string Exbibyte { get; } - public static string Gibibyte { get; } - public static string Gigabyte { get; } - public static string Kibibyte { get; } - public static string Kilobyte { get; } - public static string Mebibyte { get; } - public static string Megabyte { get; } - public static string Pebibyte { get; } - public static string Petabyte { get; } - public static string Tebibyte { get; } - public static string Terabyte { get; } - } - } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index a2182bb19b..64869d4587 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -688,26 +688,66 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitDistribution(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitGauge(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void Flush(); @@ -1125,42 +1165,6 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } - public static class SentryUnits - { - public static class Duration - { - public static string Day { get; } - public static string Hour { get; } - public static string Microsecond { get; } - public static string Millisecond { get; } - public static string Minute { get; } - public static string Nanosecond { get; } - public static string Second { get; } - public static string Week { get; } - } - public static class Fraction - { - public static string Percent { get; } - public static string Ratio { get; } - } - public static class Information - { - public static string Bit { get; } - public static string Byte { get; } - public static string Exabyte { get; } - public static string Exbibyte { get; } - public static string Gibibyte { get; } - public static string Gigabyte { get; } - public static string Kibibyte { get; } - public static string Kilobyte { get; } - public static string Mebibyte { get; } - public static string Megabyte { get; } - public static string Pebibyte { get; } - public static string Petabyte { get; } - public static string Tebibyte { get; } - public static string Terabyte { get; } - } - } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index a2182bb19b..64869d4587 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -688,26 +688,66 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitDistribution(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitGauge(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void Flush(); @@ -1125,42 +1165,6 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } - public static class SentryUnits - { - public static class Duration - { - public static string Day { get; } - public static string Hour { get; } - public static string Microsecond { get; } - public static string Millisecond { get; } - public static string Minute { get; } - public static string Nanosecond { get; } - public static string Second { get; } - public static string Week { get; } - } - public static class Fraction - { - public static string Percent { get; } - public static string Ratio { get; } - } - public static class Information - { - public static string Bit { get; } - public static string Byte { get; } - public static string Exabyte { get; } - public static string Exbibyte { get; } - public static string Gibibyte { get; } - public static string Gigabyte { get; } - public static string Kibibyte { get; } - public static string Kilobyte { get; } - public static string Mebibyte { get; } - public static string Megabyte { get; } - public static string Pebibyte { get; } - public static string Petabyte { get; } - public static string Tebibyte { get; } - public static string Terabyte { get; } - } - } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 669e2b095d..fbf517dd1e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -674,26 +674,66 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitDistribution(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } public void EmitGauge(string name, T value) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) + where T : struct { } public void EmitGauge(string name, T value, Sentry.Scope? scope) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, Sentry.Scope? scope) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } protected abstract void Flush(); @@ -1105,42 +1145,6 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } - public static class SentryUnits - { - public static class Duration - { - public static string Day { get; } - public static string Hour { get; } - public static string Microsecond { get; } - public static string Millisecond { get; } - public static string Minute { get; } - public static string Nanosecond { get; } - public static string Second { get; } - public static string Week { get; } - } - public static class Fraction - { - public static string Percent { get; } - public static string Ratio { get; } - } - public static class Information - { - public static string Bit { get; } - public static string Byte { get; } - public static string Exabyte { get; } - public static string Exbibyte { get; } - public static string Gibibyte { get; } - public static string Gigabyte { get; } - public static string Kibibyte { get; } - public static string Kilobyte { get; } - public static string Mebibyte { get; } - public static string Megabyte { get; } - public static string Pebibyte { get; } - public static string Petabyte { get; } - public static string Tebibyte { get; } - public static string Terabyte { get; } - } - } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } diff --git a/test/Sentry.Tests/MeasurementUnitTests.cs b/test/Sentry.Tests/MeasurementUnitTests.cs index fad3b2ea1f..64fca21525 100644 --- a/test/Sentry.Tests/MeasurementUnitTests.cs +++ b/test/Sentry.Tests/MeasurementUnitTests.cs @@ -7,6 +7,7 @@ public void DefaultEmpty() { MeasurementUnit m = new(); Assert.Equal("", m.ToString()); + Assert.Null(m.ToNullableString()); } [Fact] @@ -21,6 +22,7 @@ public void CanUseNoneUnit() { var m = MeasurementUnit.None; Assert.Equal("none", m.ToString()); + Assert.Equal("none", m.ToNullableString()); } [Fact] @@ -28,6 +30,7 @@ public void CanUseDurationUnits() { MeasurementUnit m = MeasurementUnit.Duration.Second; Assert.Equal("second", m.ToString()); + Assert.Equal("second", m.ToNullableString()); } [Fact] @@ -35,6 +38,7 @@ public void CanUseInformationUnits() { MeasurementUnit m = MeasurementUnit.Information.Byte; Assert.Equal("byte", m.ToString()); + Assert.Equal("byte", m.ToNullableString()); } [Fact] @@ -42,6 +46,7 @@ public void CanUseFractionUnits() { MeasurementUnit m = MeasurementUnit.Fraction.Percent; Assert.Equal("percent", m.ToString()); + Assert.Equal("percent", m.ToNullableString()); } [Fact] @@ -49,6 +54,7 @@ public void CanUseCustomUnits() { var m = MeasurementUnit.Custom("foo"); Assert.Equal("foo", m.ToString()); + Assert.Equal("foo", m.ToNullableString()); } [Fact] diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs index fe98d1c940..a616d6761f 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs @@ -287,8 +287,72 @@ public void Emit_Name_Empty_DoesNotCaptureEnvelope(SentryMetricType type, string entry.Exception.Should().BeNull(); entry.Args.Should().BeEquivalentTo([arg0, arg1]); } + + [Fact] + public void Type_EmitMethods_StringUnitParameterIsObsoleteForForwardCompatibility() + { + var type = typeof(SentryMetricEmitter); + + type.Methods() + .Where(static method => method.IsPublic && method.ReturnType == typeof(void) && method.IsGenericMethod && method.Name.StartsWith("Emit")) + .Should().NotBeEmpty().And.AllSatisfy(static method => + { + var unitParameter = method.GetParameters().SingleOrDefault(static parameter => parameter.Name == "unit"); + + if (unitParameter is null || unitParameter.ParameterType == typeof(MeasurementUnit)) + { + method.GetCustomAttribute().Should().BeNull("because Method '{0}' does not take a 'unit' as a 'string'", method); + } + else + { + unitParameter.ParameterType.Should().Be(typeof(string)); + + var obsolete = method.GetCustomAttribute(); + obsolete.Should().NotBeNull("because Method '{0}' does take a 'unit' as a 'string'", method); + obsolete.Message.Should().Be(SentryMetricEmitter.ObsoleteStringUnitForwardCompatibility); + obsolete.IsError.Should().BeFalse(); + } + }); + } + + [Theory] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Unit_String_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, "measurement_unit"); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } + + [Theory] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_Unit_MeasurementUnit_CapturesEnvelope(SentryMetricType type) + { + Assert.True(_fixture.Options.Experimental.EnableMetrics); + var metrics = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + metrics.Emit(type, 1, MeasurementUnit.Custom("measurement_unit")); + metrics.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, type); + } } +[Obsolete(SentryMetricEmitter.ObsoleteStringUnitForwardCompatibility)] file static class SentryMetricEmitterExtensions { public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, ReadOnlySpan> attributes) where T : struct @@ -326,4 +390,38 @@ public static void Emit(this SentryMetricEmitter metrics, SentryMetricType ty throw new ArgumentOutOfRangeException(nameof(type), type, null); } } + + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, string? unit) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + throw new NotSupportedException($"{nameof(SentryMetric<>.Unit)} for {nameof(SentryMetricType.Counter)} is not supported."); + case SentryMetricType.Gauge: + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.counter", value, unit); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.counter", value, unit); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, MeasurementUnit unit) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + throw new NotSupportedException($"{nameof(SentryMetric<>.Unit)} for {nameof(SentryMetricType.Counter)} is not supported."); + case SentryMetricType.Gauge: + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.counter", value, unit); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.counter", value, unit); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } } diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs index d5d7d7a85b..d8a3d251b3 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Values.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Sentry.Tests; public partial class SentryMetricEmitterTests @@ -75,6 +77,188 @@ public void TryGetValue_FromDecimal_UnsupportedType() @double.Should().Be(0d); } + // see: https://develop.sentry.dev/sdk/telemetry/attributes/#units + // see: https://getsentry.github.io/relay/relay_metrics/enum.MetricUnit.html + [Theory] + [InlineData(MeasurementUnit.Duration.Nanosecond, "nanosecond")] + [InlineData(MeasurementUnit.Duration.Microsecond, "microsecond")] + [InlineData(MeasurementUnit.Duration.Millisecond, "millisecond")] + [InlineData(MeasurementUnit.Duration.Second, "second")] + [InlineData(MeasurementUnit.Duration.Minute, "minute")] + [InlineData(MeasurementUnit.Duration.Hour, "hour")] + [InlineData(MeasurementUnit.Duration.Day, "day")] + [InlineData(MeasurementUnit.Duration.Week, "week")] + [InlineData(MeasurementUnit.Information.Bit, "bit")] + [InlineData(MeasurementUnit.Information.Byte, "byte")] + [InlineData(MeasurementUnit.Information.Kilobyte, "kilobyte")] + [InlineData(MeasurementUnit.Information.Kibibyte, "kibibyte")] + [InlineData(MeasurementUnit.Information.Megabyte, "megabyte")] + [InlineData(MeasurementUnit.Information.Mebibyte, "mebibyte")] + [InlineData(MeasurementUnit.Information.Gigabyte, "gigabyte")] + [InlineData(MeasurementUnit.Information.Gibibyte, "gibibyte")] + [InlineData(MeasurementUnit.Information.Terabyte, "terabyte")] + [InlineData(MeasurementUnit.Information.Tebibyte, "tebibyte")] + [InlineData(MeasurementUnit.Information.Petabyte, "petabyte")] + [InlineData(MeasurementUnit.Information.Pebibyte, "pebibyte")] + [InlineData(MeasurementUnit.Information.Exabyte, "exabyte")] + [InlineData(MeasurementUnit.Information.Exbibyte, "exbibyte")] + [InlineData(MeasurementUnit.Fraction.Ratio, "ratio")] + [InlineData(MeasurementUnit.Fraction.Percent, "percent")] + public void Emit_Unit_MeasurementUnit_Predefined(MeasurementUnit unit, string expected) + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.gauge", 1, unit); + + captured.Should().NotBeNull(); + captured.Unit.Should().Be(expected); + } + + [Fact] + public void Emit_Unit_MeasurementUnit_None() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.None); + + captured.Should().NotBeNull(); + captured.Unit.Should().Be("none"); + } + + [Fact] + public void Emit_Unit_MeasurementUnit_Custom() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Custom("custom_unit")); + + captured.Should().NotBeNull(); + captured.Unit.Should().Be("custom_unit"); + } + + [Fact] + public void Emit_Unit_MeasurementUnit_Empty() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Custom("")); + + captured.Should().NotBeNull(); + captured.Unit.Should().BeEmpty(); + } + + [Fact] + public void Emit_Unit_MeasurementUnit_Null() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Parse(null)); + + captured.Should().NotBeNull(); + captured.Unit.Should().BeNull(); + } + + [Fact] + public void Emit_Unit_MeasurementUnit_Default() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.gauge", 1, default(MeasurementUnit)); + + captured.Should().NotBeNull(); + captured.Unit.Should().BeNull(); + } + + [Fact] + [Obsolete(SentryMetricEmitter.ObsoleteStringUnitForwardCompatibility)] + public void Emit_Unit_String_Custom() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.distribution", 1, "custom_unit"); + + captured.Should().NotBeNull(); + captured.Unit.Should().Be("custom_unit"); + } + + [Fact] + [Obsolete(SentryMetricEmitter.ObsoleteStringUnitForwardCompatibility)] + public void Emit_Unit_String_Empty() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.distribution", 1, ""); + + captured.Should().NotBeNull(); + captured.Unit.Should().BeEmpty(); + } + + [Fact] + [Obsolete(SentryMetricEmitter.ObsoleteStringUnitForwardCompatibility)] + public void Emit_Unit_String_Null() + { + SentryMetric? captured = null; + _fixture.Options.Experimental.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.distribution", 1, (string?)null); + + captured.Should().NotBeNull(); + captured.Unit.Should().BeNull(); + } + private static SentryMetric CreateCounter(T value) where T : struct { return new SentryMetric(DateTimeOffset.MinValue, SentryId.Empty, SentryMetricType.Counter, "sentry_tests.sentry_trace_metrics_tests.counter", value); From e56b0a46b6c780bf9d134cae9da2f3bf5bca36e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:09:38 +0100 Subject: [PATCH 25/26] ref: dedupe --- src/Sentry/SentryMetric.Factory.cs | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index 4257ddae44..fe6d0e7b2d 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -4,7 +4,7 @@ namespace Sentry; public abstract partial class SentryMetric { - internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + private static SentryMetric CreateCore(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, Scope? scope) where T : struct { Debug.Assert(IsSupported()); Debug.Assert(!string.IsNullOrEmpty(name)); @@ -22,31 +22,20 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst scope ??= hub.GetScope(); metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); - metric.SetAttributes(attributes); + return metric; + } + internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + { + var metric = CreateCore(hub, options, clock, type, name, value, unit, scope); + metric.SetAttributes(attributes); return metric; } internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct { - Debug.Assert(IsSupported()); - Debug.Assert(!string.IsNullOrEmpty(name)); - Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}."); - - var timestamp = clock.GetUtcNow(); - hub.GetTraceIdAndSpanId(out var traceId, out var spanId); - - var metric = new SentryMetric(timestamp, traceId, type, name, value) - { - SpanId = spanId, - Unit = unit, - }; - - scope ??= hub.GetScope(); - metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); - + var metric = CreateCore(hub, options, clock, type, name, value, unit, scope); metric.SetAttributes(attributes); - return metric; } From 0798441d813c5c11c7ba104511657531741aaa58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:10:46 +0100 Subject: [PATCH 26/26] test: make reflection test a bit easier to read --- test/Sentry.Tests/SentryMetricEmitterTests.Types.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs index a616d6761f..38b2d40ff0 100644 --- a/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Types.cs @@ -289,15 +289,15 @@ public void Emit_Name_Empty_DoesNotCaptureEnvelope(SentryMetricType type, string } [Fact] - public void Type_EmitMethods_StringUnitParameterIsObsoleteForForwardCompatibility() + public void Type_EmitMethods_StringUnitParameterOverloadsAreObsoleteForForwardCompatibility() { var type = typeof(SentryMetricEmitter); type.Methods() - .Where(static method => method.IsPublic && method.ReturnType == typeof(void) && method.IsGenericMethod && method.Name.StartsWith("Emit")) - .Should().NotBeEmpty().And.AllSatisfy(static method => + .Where(method => method.IsPublic && method.ReturnType == typeof(void) && method.IsGenericMethod && method.Name.StartsWith("Emit")) + .Should().NotBeEmpty().And.AllSatisfy(method => { - var unitParameter = method.GetParameters().SingleOrDefault(static parameter => parameter.Name == "unit"); + var unitParameter = method.GetParameters().SingleOrDefault(parameter => parameter.Name == "unit"); if (unitParameter is null || unitParameter.ParameterType == typeof(MeasurementUnit)) {