diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26bbf20371..a82b5f3071 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,28 @@
## 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
- 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
diff --git a/Directory.Build.props b/Directory.Build.props
index 0c8421134f..7e15f5723e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 6.0.0
+ 6.1.0-alpha.1
13
true
true
@@ -13,6 +13,7 @@
$(NoWarn);SENTRY0001
+ $(NoWarn);SENTRYTRACECONNECTEDMETRICS
$(NoWarn);CS8002
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.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..ff8dd50f95
--- /dev/null
+++ b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs
@@ -0,0 +1,114 @@
+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.ContainingAssembly is null || method.ContainingAssembly.Name != "Sentry")
+ {
+ return;
+ }
+
+ if (method.ContainingNamespace is null || method.ContainingNamespace.Name != "Sentry")
+ {
+ return;
+ }
+
+ 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;
+ }
+
+ 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(fullyQualifiedMetadataName);
+ 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..8e5a6c88aa 100644
--- a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj
+++ b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj
@@ -15,11 +15,25 @@
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/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..6c6a7ae31d 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", 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 45499369e6..6ec4c79104 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", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")]
+ 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..2d48844a9e 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", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")]
+ 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..bb37bc474e 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 where TItem : notnull
{
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}", item.GetType().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..df02cfa44f
--- /dev/null
+++ b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs
@@ -0,0 +1,103 @@
+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
+ {
+ 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
+ {
+ 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)
+ {
+ 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..290d941b1f
--- /dev/null
+++ b/src/Sentry/SentryMetric.Factory.cs
@@ -0,0 +1,93 @@
+using Sentry.Infrastructure;
+using Sentry.Internal;
+
+namespace Sentry;
+
+internal static class SentryMetric
+{
+ internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct
+ {
+ 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();
+ 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.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance);
+ metric.Apply(scope);
+
+ metric.SetAttributes(attributes);
+
+ return metric;
+ }
+
+ internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct
+ {
+ Debug.Assert(IsSupported());
+ Debug.Assert(!string.IsNullOrEmpty(name));
+ Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}.");
+
+ var timestamp = clock.GetUtcNow();
+ 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.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.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..ba62ca2d7d
--- /dev/null
+++ b/src/Sentry/SentryMetric.cs
@@ -0,0 +1,376 @@
+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 readonly 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;
+ // 7 is the number of built-in attributes, so we start with that.
+ _attributes = new Dictionary(7);
+ }
+
+ ///
+ /// 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.
+ ///
+ 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.
+ ///
+ public SpanId? SpanId { get; init; }
+
+ ///
+ /// The unit of measurement for the metric value.
+ ///
+ ///
+ /// Only used for and .
+ ///
+ public string? Unit { get; init; }
+
+ ///
+ /// Gets the attribute value associated with the specified key.
+ ///
+ ///
+ /// Returns if the contains an attribute with the specified key which is of type and it's value is not .
+ /// Otherwise .
+ /// Supported types:
+ ///
+ ///
+ /// Type
+ /// Range
+ ///
+ /// -
+ /// string
+ /// and
+ ///
+ /// -
+ /// boolean
+ /// and
+ ///
+ /// -
+ /// integer
+ /// 64-bit signed integral numeric types
+ ///
+ /// -
+ /// double
+ /// 64-bit floating-point numeric types
+ ///
+ ///
+ /// Unsupported types:
+ ///
+ ///
+ /// Type
+ /// Result
+ ///
+ /// -
+ ///
+ /// ToString as "type": "string"
+ ///
+ /// -
+ /// Collections
+ /// ToString as "type": "string"
+ ///
+ /// -
+ ///
+ /// ignored
+ ///
+ ///
+ ///
+ ///
+ public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value)
+ {
+ if (_attributes.TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue)
+ {
+ value = attributeValue;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ ///
+ /// Set a key-value pair of data attached to the metric.
+ ///
+ public void SetAttribute(string key, TAttribute value) where TAttribute : notnull
+ {
+ 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)
+ {
+ return;
+ }
+
+#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] = new SentryAttribute(attribute.Value);
+ }
+ }
+
+ internal void SetAttributes(ReadOnlySpan> attributes)
+ {
+ if (attributes.IsEmpty)
+ {
+ return;
+ }
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length);
+#endif
+
+ foreach (var attribute in attributes)
+ {
+ _attributes[attribute.Key] = new SentryAttribute(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);
+ }
+
+ writer.WritePropertyName("attributes");
+ writer.WriteStartObject();
+
+ foreach (var attribute in _attributes)
+ {
+ SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger);
+ }
+
+ writer.WriteEndObject();
+
+ writer.WriteEndObject();
+ }
+}
+
+// TODO: remove after upgrading 14.0 and updating
+#if !NET6_0_OR_GREATER
+file static class EnumerableExtensions
+{
+ internal static bool TryGetNonEnumeratedCount(this IEnumerable source, out int count)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (source is ICollection genericCollection)
+ {
+ count = genericCollection.Count;
+ return true;
+ }
+
+ if (source is ICollection collection)
+ {
+ count = collection.Count;
+ return true;
+ }
+
+ count = 0;
+ return false;
+ }
+}
+#endif
+
+file static class Utf8JsonWriterExtensions
+{
+ internal static void WriteMetricValue(this Utf8JsonWriter writer, string propertyName, T value) where T : struct
+ {
+ var type = typeof(T);
+
+ if (type == typeof(long))
+ {
+ writer.WriteNumber(propertyName, (long)(object)value);
+ }
+ else if (type == typeof(double))
+ {
+ writer.WriteNumber(propertyName, (double)(object)value);
+ }
+ else if (type == typeof(int))
+ {
+ writer.WriteNumber(propertyName, (int)(object)value);
+ }
+ else if (type == typeof(float))
+ {
+ writer.WriteNumber(propertyName, (float)(object)value);
+ }
+ else if (type == typeof(short))
+ {
+ writer.WriteNumber(propertyName, (short)(object)value);
+ }
+ else if (type == typeof(byte))
+ {
+ writer.WriteNumber(propertyName, (byte)(object)value);
+ }
+ else
+ {
+ Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable.");
+ }
+ }
+}
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.cs b/src/Sentry/SentryOptions.cs
index c769b98370..3f6bc689e4 100644
--- a/src/Sentry/SentryOptions.cs
+++ b/src/Sentry/SentryOptions.cs
@@ -1370,6 +1370,8 @@ public SentryOptions()
);
NetworkStatusListener = new PollingNetworkStatusListener(this);
+
+ Experimental = new ExperimentalSentryOptions(this);
}
///
@@ -1905,4 +1907,55 @@ 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; }
+
+ ///
+ /// 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 readonly SentryOptions _options;
+ private SentryTraceMetricsCallbacks? _beforeSendMetric;
+
+ internal ExperimentalSentryOptions(SentryOptions options)
+ {
+ _options = options;
+ }
+
+ internal SentryTraceMetricsCallbacks? 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 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 SentryTraceMetricsCallbacks();
+ _beforeSendMetric.Set(beforeSendMetric, _options.DiagnosticLogger);
+ }
+ }
}
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.Counter.cs b/src/Sentry/SentryTraceMetrics.Counter.cs
new file mode 100644
index 0000000000..60f87f2d78
--- /dev/null
+++ b/src/Sentry/SentryTraceMetrics.Counter.cs
@@ -0,0 +1,57 @@
+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.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitCounter(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.
+ /// 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);
+ }
+
+ ///
+ /// 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.
+ /// 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);
+ }
+
+ ///
+ /// 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.
+ /// 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
new file mode 100644
index 0000000000..b31d9bd626
--- /dev/null
+++ b/src/Sentry/SentryTraceMetrics.Distribution.cs
@@ -0,0 +1,73 @@
+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.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitDistribution(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.
+ /// 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);
+ }
+
+ ///
+ /// Add a distribution value.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The unit of measurement.
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitDistribution(string name, T value, 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.
+ /// 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);
+ }
+
+ ///
+ /// Add a distribution value.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The unit of measurement.
+ /// A dictionary of attributes (key-value pairs with type information).
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitDistribution(string name, T value, 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..7d46ab1f90
--- /dev/null
+++ b/src/Sentry/SentryTraceMetrics.Gauge.cs
@@ -0,0 +1,73 @@
+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.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitGauge(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.
+ /// 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);
+ }
+
+ ///
+ /// Set a gauge value.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The unit of measurement.
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitGauge(string name, T value, 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.
+ /// 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);
+ }
+
+ ///
+ /// Set a gauge value.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The unit of measurement.
+ /// A dictionary of attributes (key-value pairs with type information).
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ /// Supported numeric value types for are , , , , , and .
+ public void EmitGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Gauge, 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/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/src/Sentry/SentryUnits.cs b/src/Sentry/SentryUnits.cs
new file mode 100644
index 0000000000..f4c1ee51b0
--- /dev/null
+++ b/src/Sentry/SentryUnits.cs
@@ -0,0 +1,178 @@
+namespace Sentry;
+
+///
+/// Supported units by Relay and Sentry.
+/// Applies to , as well as attributes of and attributes of .
+///
+///
+/// Contains the units currently supported by Relay and Sentry.
+/// For Metrics and Attributes, currently, custom units are not allowed and will be set to "None" by Relay.
+///
+///
+///
+public static class SentryUnits
+{
+ ///
+ /// Duration Units.
+ ///
+ public static class Duration
+ {
+ ///
+ /// Nanosecond unit (ns).
+ /// 10^-9 seconds.
+ ///
+ public static string Nanosecond { get; } = "nanosecond";
+
+ ///
+ /// Microsecond unit (μs).
+ /// 10^-6 seconds.
+ ///
+ public static string Microsecond { get; } = "microsecond";
+
+ ///
+ /// Millisecond unit (ms).
+ /// 10^-3 seconds.
+ ///
+ public static string Millisecond { get; } = "millisecond";
+
+ ///
+ /// Second unit (s).
+ ///
+ public static string Second { get; } = "second";
+
+ ///
+ /// Minute unit (min).
+ /// 60 seconds.
+ ///
+ public static string Minute { get; } = "minute";
+
+ ///
+ /// Hour unit (h).
+ /// 3_600 seconds.
+ ///
+ public static string Hour { get; } = "hour";
+
+ ///
+ /// Day unit (d).
+ /// 86_400 seconds.
+ ///
+ public static string Day { get; } = "day";
+
+ ///
+ /// Week unit (wk).
+ /// 604_800 seconds.
+ ///
+ public static string Week { get; } = "week";
+ }
+
+ ///
+ /// Information Units.
+ ///
+ ///
+ /// Note that there are computer systems with a different number of bits per byte.
+ ///
+ public static class Information
+ {
+ ///
+ /// Bit unit (b).
+ /// 1/8 of a byte.
+ ///
+ public static string Bit { get; } = "bit";
+
+ ///
+ /// Byte unit (B).
+ /// 8 bits.
+ ///
+ public static string Byte { get; } = "byte";
+
+ ///
+ /// Kilobyte unit (kB).
+ /// 10^3 bytes = 1_000 bytes (decimal).
+ ///
+ public static string Kilobyte { get; } = "kilobyte";
+
+ ///
+ /// Kibibyte unit (KiB).
+ /// 2^10 bytes = 1_024 bytes (binary).
+ ///
+ public static string Kibibyte { get; } = "kibibyte";
+
+ ///
+ /// Megabyte unit (MB).
+ /// 10^6 bytes = 1_000_000 bytes (decimal).
+ ///
+ public static string Megabyte { get; } = "megabyte";
+
+ ///
+ /// Mebibyte unit (MiB).
+ /// 2^20 bytes = 1_048_576 bytes (binary).
+ ///
+ public static string Mebibyte { get; } = "mebibyte";
+
+ ///
+ /// Gigabyte unit (GB).
+ /// 10^9 bytes = 1_000_000_000 bytes (decimal).
+ ///
+ public static string Gigabyte { get; } = "gigabyte";
+
+ ///
+ /// Gibibyte unit (GiB).
+ /// 2^30 bytes = 1_073_741_824 bytes (binary).
+ ///
+ public static string Gibibyte { get; } = "gibibyte";
+
+ ///
+ /// Terabyte unit (TB).
+ /// 10^12 bytes = 1_000_000_000_000 bytes (decimal).
+ ///
+ public static string Terabyte { get; } = "terabyte";
+
+ ///
+ /// Tebibyte unit (TiB).
+ /// 2^40 bytes = 1_099_511_627_776 bytes (binary).
+ ///
+ public static string Tebibyte { get; } = "tebibyte";
+
+ ///
+ /// Petabyte unit (PB).
+ /// 10^15 bytes = 1_000_000_000_000_000 bytes (decimal).
+ ///
+ public static string Petabyte { get; } = "petabyte";
+
+ ///
+ /// Pebibyte unit (PiB).
+ /// 2^50 bytes = 1_125_899_906_842_624 bytes (binary).
+ ///
+ public static string Pebibyte { get; } = "pebibyte";
+
+ ///
+ /// Exabyte unit (EB).
+ /// 10^18 bytes = 1_000_000_000_000_000_000 bytes (decimal).
+ ///
+ public static string Exabyte { get; } = "exabyte";
+
+ ///
+ /// Exbibyte unit (EiB).
+ /// 2^60 bytes = 1_152_921_504_606_846_976 bytes (binary).
+ ///
+ public static string Exbibyte { get; } = "exbibyte";
+ }
+
+ ///
+ /// Fraction Units.
+ ///
+ public static class Fraction
+ {
+ ///
+ /// Ratio unit.
+ /// Floating point fraction of 1.
+ ///
+ public static string Ratio { get; } = "ratio";
+
+ ///
+ /// Percent unit (%).
+ /// Ratio expressed as a fraction of 100. 100% equals a ratio of 1.0.
+ ///
+ public static string Percent { get; } = "percent";
+ }
+}
diff --git a/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs
new file mode 100644
index 0000000000..a07f701b55
--- /dev/null
+++ b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs
@@ -0,0 +1,252 @@
+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 Init(SentryOptions options)
+ {
+ options.Experimental.EnableMetrics = false;
+ }
+
+ public void Emit(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 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;
+
+ #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
+ }
+
+ private static SentryMetric? BeforeSendMetric(SentryMetric metric) where T : struct
+ {
+ return metric;
+ }
+
+ private static SentryMetric? OnBeforeSendMetric(SentryMetric metric)
+ {
+ return metric;
+ }
+ }
+
+ 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 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
+ {|#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};
+
+ {|#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};
+
+ {|#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(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)),
+ },
+ }
+ };
+
+ 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..e005a20dc9 100644
--- a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj
+++ b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj
@@ -7,16 +7,20 @@
-
-
-
-
+
+
+
+
-
+
+
+
+
+
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
+ }
+ }
+}
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)
+ {
+ 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);
+ 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..3a08f11ba4 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", 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);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope);
@@ -664,6 +666,33 @@ 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
+ {
+ [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, TAttribute value)
+ where TAttribute : notnull { }
+ public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { }
+ }
public enum SentryMonitorInterval
{
Year = 0,
@@ -715,6 +744,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 +832,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 +867,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 +931,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 +1052,40 @@ namespace Sentry
public override string ToString() { }
public static Sentry.SentryTraceHeader? Parse(string value) { }
}
+ public abstract class SentryTraceMetrics
+ {
+ protected abstract void CaptureMetric(Sentry.SentryMetric metric)
+ where T : struct;
+ public void EmitCounter(string name, T value)
+ where T : struct { }
+ public void EmitCounter(string name, T value, Sentry.Scope? scope)
+ where T : struct { }
+ public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void EmitDistribution(string name, T value)
+ where T : struct { }
+ public void EmitDistribution(string name, T value, string? unit)
+ where T : struct { }
+ public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope)
+ where T : struct { }
+ public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void EmitGauge(string name, T value)
+ where T : struct { }
+ public void EmitGauge(string name, T value, string? unit)
+ where T : struct { }
+ public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope)
+ where T : struct { }
+ public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ protected abstract void Flush();
+ }
public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext
{
public SentryTransaction(Sentry.ITransactionTracer tracer) { }
@@ -1060,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() { }
@@ -1385,6 +1496,8 @@ namespace Sentry.Extensibility
public bool IsSessionActive { get; }
public Sentry.SentryId LastEventId { get; }
public Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")]
+ public Sentry.SentryTraceMetrics Metrics { get; }
public 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 +1545,8 @@ namespace Sentry.Extensibility
public bool IsSessionActive { get; }
public Sentry.SentryId LastEventId { get; }
public Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")]
+ public Sentry.SentryTraceMetrics Metrics { get; }
public 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..3a08f11ba4 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", 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);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope);
@@ -664,6 +666,33 @@ 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
+ {
+ [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, TAttribute value)
+ where TAttribute : notnull { }
+ public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { }
+ }
public enum SentryMonitorInterval
{
Year = 0,
@@ -715,6 +744,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 +832,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 +867,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 +931,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 +1052,40 @@ namespace Sentry
public override string ToString() { }
public static Sentry.SentryTraceHeader? Parse(string value) { }
}
+ public abstract class SentryTraceMetrics
+ {
+ protected abstract void CaptureMetric(Sentry.SentryMetric