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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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 de065c716d224fd69062ce86e135c9aaeeb7e981 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 16:04:11 +0100 Subject: [PATCH 07/12] feat(metrics): Trace-connected Metrics (Analyzers) --- .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 8 + .../TraceConnectedMetricsAnalyzer.cs | 107 +++++++++ .../DiagnosticCategories.cs | 6 + .../DiagnosticIds.cs | 6 + .../Sentry.Compiler.Extensions.csproj | 8 +- .../SymbolDisplayFormats.cs | 12 + .../TraceConnectedMetricsAnalyzerTests.cs | 209 ++++++++++++++++++ .../Sentry.Compiler.Extensions.Tests.csproj | 10 +- .../SolutionTransforms.cs | 37 ++++ .../TargetFramework.cs | 23 ++ 11 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md create mode 100644 src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md create mode 100644 src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs create mode 100644 src/Sentry.Compiler.Extensions/DiagnosticCategories.cs create mode 100644 src/Sentry.Compiler.Extensions/DiagnosticIds.cs create mode 100644 src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs create mode 100644 test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs create mode 100644 test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs create mode 100644 test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs diff --git a/src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..60b59dd99b --- /dev/null +++ b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..f8fc0f9c86 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +SENTRY1001 | Support | Warning | TraceConnectedMetricsAnalyzer \ No newline at end of file diff --git a/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs new file mode 100644 index 0000000000..7af73b2c34 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs @@ -0,0 +1,107 @@ +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Sentry.Compiler.Extensions.Analyzers; + +/// +/// Guide consumers to use the public API of Sentry Trace-connected Metrics correctly. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class TraceConnectedMetricsAnalyzer : DiagnosticAnalyzer +{ + private static readonly string Title = "Unsupported numeric type of Metric"; + private static readonly string MessageFormat = "{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."; + private static readonly string Description = "Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number."; + + private static readonly DiagnosticDescriptor Rule = new( + id: DiagnosticIds.Sentry1001, + title: Title, + messageFormat: MessageFormat, + category: DiagnosticCategories.Support, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: Description, + helpLinkUri: null + ); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(Execute, OperationKind.Invocation); + } + + private static void Execute(OperationAnalysisContext context) + { + Debug.Assert(context.Operation.Language == LanguageNames.CSharp); + Debug.Assert(context.Operation.Kind is OperationKind.Invocation); + + context.CancellationToken.ThrowIfCancellationRequested(); + + if (context.Operation is not IInvocationOperation invocation) + { + return; + } + + var method = invocation.TargetMethod; + if (method.DeclaredAccessibility != Accessibility.Public || method.IsAbstract || method.IsVirtual || method.IsStatic || !method.ReturnsVoid || method.Parameters.Length == 0) + { + return; + } + + if (!method.IsGenericMethod || method.Arity != 1 || method.TypeArguments.Length != 1) + { + return; + } + + if (!method.Name.Equals("EmitCounter", StringComparison.Ordinal) && + !method.Name.Equals("EmitGauge", StringComparison.Ordinal) && + !method.Name.Equals("EmitDistribution", StringComparison.Ordinal)) + { + return; + } + + if (method.ContainingAssembly is null || method.ContainingAssembly.Name != "Sentry") + { + return; + } + + if (method.ContainingNamespace is null || method.ContainingNamespace.Name != "Sentry") + { + return; + } + + var typeArgument = method.TypeArguments[0]; + if (typeArgument.SpecialType is SpecialType.System_Byte or SpecialType.System_Int16 or SpecialType.System_Int32 or SpecialType.System_Int64 or SpecialType.System_Single or SpecialType.System_Double) + { + return; + } + + if (typeArgument is ITypeParameterSymbol) + { + return; + } + + var sentryType = context.Compilation.GetTypeByMetadataName("Sentry.SentryTraceMetrics"); + if (sentryType is null) + { + return; + } + + if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, sentryType)) + { + return; + } + + var location = invocation.Syntax.GetLocation(); + var diagnostic = Diagnostic.Create(Rule, location, typeArgument.ToDisplayString(SymbolDisplayFormats.FullNameFormat)); + context.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Sentry.Compiler.Extensions/DiagnosticCategories.cs b/src/Sentry.Compiler.Extensions/DiagnosticCategories.cs new file mode 100644 index 0000000000..3fe408be60 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/DiagnosticCategories.cs @@ -0,0 +1,6 @@ +namespace Sentry.Compiler.Extensions; + +internal static class DiagnosticCategories +{ + internal const string Support = nameof(Support); +} diff --git a/src/Sentry.Compiler.Extensions/DiagnosticIds.cs b/src/Sentry.Compiler.Extensions/DiagnosticIds.cs new file mode 100644 index 0000000000..fa2f8a3d94 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/DiagnosticIds.cs @@ -0,0 +1,6 @@ +namespace Sentry.Compiler.Extensions; + +internal static class DiagnosticIds +{ + internal const string Sentry1001 = "SENTRY1001"; +} diff --git a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj index 9fb31748a3..6ec6654892 100644 --- a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj +++ b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj @@ -15,11 +15,17 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + diff --git a/src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs b/src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs new file mode 100644 index 0000000000..85af2df64b --- /dev/null +++ b/src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis; + +namespace Sentry.Compiler.Extensions; + +internal static class SymbolDisplayFormats +{ + internal static SymbolDisplayFormat FullNameFormat { get; } = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters + ); +} diff --git a/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs new file mode 100644 index 0000000000..91249b450c --- /dev/null +++ b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs @@ -0,0 +1,209 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Sentry.Compiler.Extensions.Analyzers; + +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier; + +namespace Sentry.Compiler.Extensions.Tests.Analyzers; + +public class TraceConnectedMetricsAnalyzerTests +{ + [Fact] + public async Task NoCode_NoDiagnostics() + { + await Verifier.VerifyAnalyzerAsync(""); + } + + [Fact] + public async Task NoInvocations_NoDiagnostics() + { + var test = new CSharpAnalyzerTest + { + TestState = + { + ReferenceAssemblies = TargetFramework.ReferenceAssemblies, + AdditionalReferences = { typeof(SentryTraceMetrics).Assembly }, + Sources = + { + """ + #nullable enable + using Sentry; + + public class AnalyzerTest + { + public void Test(IHub hub) + { + var metrics = SentrySdk.Experimental.Metrics; + + _ = metrics.GetType(); + + #pragma warning disable SENTRYTRACECONNECTEDMETRICS + _ = hub.Metrics.GetType(); + #pragma warning restore SENTRYTRACECONNECTEDMETRICS + + _ = SentrySdk.Experimental.Metrics.Equals(null); + _ = SentrySdk.Experimental.Metrics.GetHashCode(); + _ = SentrySdk.Experimental.Metrics.GetType(); + _ = SentrySdk.Experimental.Metrics.ToString(); + } + } + """ + }, + ExpectedDiagnostics = { }, + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task SupportedInvocations_NoDiagnostics() + { + var test = new CSharpAnalyzerTest + { + TestState = + { + ReferenceAssemblies = TargetFramework.ReferenceAssemblies, + AdditionalReferences = { typeof(SentryTraceMetrics).Assembly }, + Sources = + { + """ + #nullable enable + using Sentry; + + public class AnalyzerTest + { + public void Test(IHub hub) + { + var scope = new Scope(new SentryOptions()); + var metrics = SentrySdk.Experimental.Metrics; + + #pragma warning disable SENTRYTRACECONNECTEDMETRICS + metrics.EmitCounter("name", 1); + hub.Metrics.EmitCounter("name", 1f); + SentrySdk.Experimental.Metrics.EmitCounter("name", 1.1d, [], scope); + + metrics.EmitGauge("name", 2); + hub.Metrics.EmitGauge("name", 2f); + SentrySdk.Experimental.Metrics.EmitGauge("name", 2.2d, "unit", [], scope); + + metrics.EmitDistribution("name", 3); + hub.Metrics.EmitDistribution("name", 3f); + SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3d, "unit", [], scope); + #pragma warning restore SENTRYTRACECONNECTEDMETRICS + } + } + + public static class Extensions + { + public static void EmitCounter(this SentryTraceMetrics metrics) where T : struct + { + metrics.EmitCounter("default", default(T), [], null); + } + + public static void EmitCounter(this SentryTraceMetrics metrics, string name) where T : struct + { + metrics.EmitCounter(name, default(T), [], null); + } + + public static void EmitGauge(this SentryTraceMetrics metrics) where T : struct + { + metrics.EmitGauge("default", default(T), null, [], null); + } + + public static void EmitGauge(this SentryTraceMetrics metrics, string name) where T : struct + { + metrics.EmitGauge(name, default(T), null, [], null); + } + + public static void EmitDistribution(this SentryTraceMetrics metrics) where T : struct + { + metrics.EmitDistribution("default", default(T), null, [], null); + } + + public static void EmitDistribution(this SentryTraceMetrics metrics, string name) where T : struct + { + metrics.EmitDistribution(name, default(T), null, [], null); + } + } + """ + }, + ExpectedDiagnostics = { }, + }, + SolutionTransforms = { SolutionTransforms.Nullable }, + }; + + await test.RunAsync(); + } + + [Fact] + public async Task UnsupportedInvocations_ReportDiagnostics() + { + var test = new CSharpAnalyzerTest + { + TestState = + { + ReferenceAssemblies = TargetFramework.ReferenceAssemblies, + AdditionalReferences = { typeof(SentryTraceMetrics).Assembly }, + Sources = + { + """ + #nullable enable + using System; + using Sentry; + + public class AnalyzerTest + { + public void Test(IHub hub) + { + var scope = new Scope(new SentryOptions()); + var metrics = SentrySdk.Experimental.Metrics; + + #pragma warning disable SENTRYTRACECONNECTEDMETRICS + {|#0:metrics.EmitCounter("name", (uint)1)|#0}; + {|#1:hub.Metrics.EmitCounter("name", (StringComparison)1f)|#1}; + {|#2:SentrySdk.Experimental.Metrics.EmitCounter("name", 1.1m, [], scope)|#2}; + + {|#3:metrics.EmitGauge("name", (uint)2)|#3}; + {|#4:hub.Metrics.EmitGauge("name", (StringComparison)2f)|#4}; + {|#5:SentrySdk.Experimental.Metrics.EmitGauge("name", 2.2m, "unit", [], scope)|#5}; + + {|#6:metrics.EmitDistribution("name", (uint)3)|#6}; + {|#7:hub.Metrics.EmitDistribution("name", (StringComparison)3f)|#7}; + {|#8:SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3m, "unit", [], scope)|#8}; + #pragma warning restore SENTRYTRACECONNECTEDMETRICS + } + } + """ + }, + ExpectedDiagnostics = + { + CreateDiagnostic(0, typeof(uint)), + CreateDiagnostic(1, typeof(StringComparison)), + CreateDiagnostic(2, typeof(decimal)), + CreateDiagnostic(3, typeof(uint)), + CreateDiagnostic(4, typeof(StringComparison)), + CreateDiagnostic(5, typeof(decimal)), + CreateDiagnostic(6, typeof(uint)), + CreateDiagnostic(7, typeof(StringComparison)), + CreateDiagnostic(8, typeof(decimal)), + }, + } + }; + + await test.RunAsync(); + } + + private static DiagnosticResult CreateDiagnostic(int markupKey, Type type) + { + Assert.NotNull(type.FullName); + + return Verifier.Diagnostic("SENTRY1001") + .WithSeverity(DiagnosticSeverity.Warning) + .WithArguments(type.FullName) + .WithMessage($"{type.FullName} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.") + .WithMessageFormat("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.") + .WithLocation(markupKey); + } +} diff --git a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj index 3265c3c21c..159e1c3ccc 100644 --- a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj +++ b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj @@ -7,14 +7,14 @@ - - - - + + + + - + diff --git a/test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs b/test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs new file mode 100644 index 0000000000..bc4bfb44ce --- /dev/null +++ b/test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Sentry.Compiler.Extensions.Tests; + +internal static class SolutionTransforms +{ + private static readonly ImmutableDictionary s_nullableWarnings = GetNullableWarningsFromCompiler(); + + internal static Func Nullable { get; } = static (solution, projectId) => + { + var project = solution.GetProject(projectId); + Assert.NotNull(project); + + var compilationOptions = project.CompilationOptions; + Assert.NotNull(compilationOptions); + + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(s_nullableWarnings)); + + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + return solution; + }; + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, Environment.CurrentDirectory, Environment.CurrentDirectory, null); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + // Workaround for https://github.com/dotnet/roslyn/issues/41610 + nullableWarnings = nullableWarnings + .SetItem("CS8632", ReportDiagnostic.Error) + .SetItem("CS8669", ReportDiagnostic.Error); + + return nullableWarnings; + } +} diff --git a/test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs b/test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs new file mode 100644 index 0000000000..3e8191b8c4 --- /dev/null +++ b/test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis.Testing; + +namespace Sentry.Compiler.Extensions.Tests; + +internal static class TargetFramework +{ + internal static ReferenceAssemblies ReferenceAssemblies + { + get + { +#if NET8_0 + return ReferenceAssemblies.Net.Net80; +#elif NET9_0 + return ReferenceAssemblies.Net.Net90; +#elif NET10_0 + return ReferenceAssemblies.Net.Net100; +#else +#warning Target Framework not implemented. + throw new NotImplementedException(); +#endif + } + } +} 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 08/12] 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 09/12] 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 10/12] 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 d2a02e1ea604279a67a8124024caa0620c0a782b Mon Sep 17 00:00:00 2001 From: Flash0ver <38893694+Flash0ver@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:58:56 +0100 Subject: [PATCH 11/12] ref: align public compiler extensions with internal analyzer project --- .../Sentry.Compiler.Extensions.csproj | 12 ++++++++++-- .../Sentry.Compiler.Extensions.Tests.csproj | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj index 6ec6654892..8e5a6c88aa 100644 --- a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj +++ b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj @@ -18,14 +18,22 @@ + + + + + - - + + diff --git a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj index 159e1c3ccc..e005a20dc9 100644 --- a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj +++ b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj @@ -19,4 +19,8 @@ + + + + From b87bc4dd3b9aa427f3ec425826b567dfdfe775ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:23:11 +0100 Subject: [PATCH 12/12] feat: also analyze SetBeforeSendMetric invocation --- .../TraceConnectedMetricsAnalyzer.cs | 19 +++-- .../TraceConnectedMetricsAnalyzerTests.cs | 85 ++++++++++++++----- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs index 7af73b2c34..ff8dd50f95 100644 --- a/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs +++ b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs @@ -61,19 +61,26 @@ private static void Execute(OperationAnalysisContext context) return; } - if (!method.Name.Equals("EmitCounter", StringComparison.Ordinal) && - !method.Name.Equals("EmitGauge", StringComparison.Ordinal) && - !method.Name.Equals("EmitDistribution", StringComparison.Ordinal)) + if (method.ContainingAssembly is null || method.ContainingAssembly.Name != "Sentry") { return; } - if (method.ContainingAssembly is null || method.ContainingAssembly.Name != "Sentry") + if (method.ContainingNamespace is null || method.ContainingNamespace.Name != "Sentry") { return; } - if (method.ContainingNamespace is null || method.ContainingNamespace.Name != "Sentry") + string fullyQualifiedMetadataName; + if (method.Name is "EmitCounter" or "EmitGauge" or "EmitDistribution") + { + fullyQualifiedMetadataName = "Sentry.SentryTraceMetrics"; + } + else if (method.Name is "SetBeforeSendMetric") + { + fullyQualifiedMetadataName = "Sentry.SentryOptions+ExperimentalSentryOptions"; + } + else { return; } @@ -89,7 +96,7 @@ private static void Execute(OperationAnalysisContext context) return; } - var sentryType = context.Compilation.GetTypeByMetadataName("Sentry.SentryTraceMetrics"); + var sentryType = context.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); if (sentryType is null) { return; diff --git a/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs index 91249b450c..a07f701b55 100644 --- a/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs +++ b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs @@ -32,7 +32,12 @@ public async Task NoInvocations_NoDiagnostics() public class AnalyzerTest { - public void Test(IHub hub) + public void Init(SentryOptions options) + { + options.Experimental.EnableMetrics = false; + } + + public void Emit(IHub hub) { var metrics = SentrySdk.Experimental.Metrics; @@ -74,7 +79,14 @@ public async Task SupportedInvocations_NoDiagnostics() public class AnalyzerTest { - public void Test(IHub hub) + public void Init(SentryOptions options) + { + options.Experimental.SetBeforeSendMetric(static SentryMetric? (SentryMetric metric) => metric); + options.Experimental.SetBeforeSendMetric(BeforeSendMetric); + options.Experimental.SetBeforeSendMetric(OnBeforeSendMetric); + } + + public void Emit(IHub hub) { var scope = new Scope(new SentryOptions()); var metrics = SentrySdk.Experimental.Metrics; @@ -93,6 +105,16 @@ public void Test(IHub hub) SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3d, "unit", [], scope); #pragma warning restore SENTRYTRACECONNECTEDMETRICS } + + private static SentryMetric? BeforeSendMetric(SentryMetric metric) where T : struct + { + return metric; + } + + private static SentryMetric? OnBeforeSendMetric(SentryMetric metric) + { + return metric; + } } public static class Extensions @@ -155,39 +177,60 @@ public async Task UnsupportedInvocations_ReportDiagnostics() public class AnalyzerTest { - public void Test(IHub hub) + public void Init(SentryOptions options) + { + {|#0:options.Experimental.SetBeforeSendMetric(static SentryMetric? (SentryMetric metric) => metric)|#0}; + {|#1:options.Experimental.SetBeforeSendMetric(BeforeSendMetric)|#1}; + {|#2:options.Experimental.SetBeforeSendMetric(OnBeforeSendMetric)|#2}; + } + + public void Emit(IHub hub) { var scope = new Scope(new SentryOptions()); var metrics = SentrySdk.Experimental.Metrics; #pragma warning disable SENTRYTRACECONNECTEDMETRICS - {|#0:metrics.EmitCounter("name", (uint)1)|#0}; - {|#1:hub.Metrics.EmitCounter("name", (StringComparison)1f)|#1}; - {|#2:SentrySdk.Experimental.Metrics.EmitCounter("name", 1.1m, [], scope)|#2}; + {|#10:metrics.EmitCounter("name", (uint)1)|#10}; + {|#11:hub.Metrics.EmitCounter("name", (StringComparison)1f)|#11}; + {|#12:SentrySdk.Experimental.Metrics.EmitCounter("name", 1.1m, [], scope)|#12}; - {|#3:metrics.EmitGauge("name", (uint)2)|#3}; - {|#4:hub.Metrics.EmitGauge("name", (StringComparison)2f)|#4}; - {|#5:SentrySdk.Experimental.Metrics.EmitGauge("name", 2.2m, "unit", [], scope)|#5}; + {|#13:metrics.EmitGauge("name", (uint)2)|#13}; + {|#14:hub.Metrics.EmitGauge("name", (StringComparison)2f)|#14}; + {|#15:SentrySdk.Experimental.Metrics.EmitGauge("name", 2.2m, "unit", [], scope)|#15}; - {|#6:metrics.EmitDistribution("name", (uint)3)|#6}; - {|#7:hub.Metrics.EmitDistribution("name", (StringComparison)3f)|#7}; - {|#8:SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3m, "unit", [], scope)|#8}; + {|#16:metrics.EmitDistribution("name", (uint)3)|#16}; + {|#17:hub.Metrics.EmitDistribution("name", (StringComparison)3f)|#17}; + {|#18:SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3m, "unit", [], scope)|#18}; #pragma warning restore SENTRYTRACECONNECTEDMETRICS } + + private static SentryMetric? BeforeSendMetric(SentryMetric metric) where T : struct + { + return metric; + } + + private static SentryMetric? OnBeforeSendMetric(SentryMetric metric) + { + return metric; + } } """ }, ExpectedDiagnostics = { - CreateDiagnostic(0, typeof(uint)), - CreateDiagnostic(1, typeof(StringComparison)), - CreateDiagnostic(2, typeof(decimal)), - CreateDiagnostic(3, typeof(uint)), - CreateDiagnostic(4, typeof(StringComparison)), - CreateDiagnostic(5, typeof(decimal)), - CreateDiagnostic(6, typeof(uint)), - CreateDiagnostic(7, typeof(StringComparison)), - CreateDiagnostic(8, typeof(decimal)), + CreateDiagnostic(0, typeof(sbyte)), + CreateDiagnostic(1, typeof(ushort)), + CreateDiagnostic(2, typeof(ulong)), + + CreateDiagnostic(10, typeof(uint)), + CreateDiagnostic(11, typeof(StringComparison)), + CreateDiagnostic(12, typeof(decimal)), + CreateDiagnostic(13, typeof(uint)), + CreateDiagnostic(14, typeof(StringComparison)), + CreateDiagnostic(15, typeof(decimal)), + CreateDiagnostic(16, typeof(uint)), + CreateDiagnostic(17, typeof(StringComparison)), + CreateDiagnostic(18, typeof(decimal)), }, } };