Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6be1e01
feat(metrics): Trace-connected Metrics (Implementation)
Flash0ver Jan 9, 2026
e462457
Merge branch 'main' into feat/trace-connected-metrics-implementation
Flash0ver Jan 9, 2026
6c5ca23
add CHANGELOG entry
Flash0ver Jan 9, 2026
9485ca4
more public overloads for convenience
Flash0ver Jan 9, 2026
aa46acb
more public overloads for convenience (API Approval Tests)
Flash0ver Jan 9, 2026
e9c349e
release: 6.1.0-alpha.1
getsentry-bot Jan 9, 2026
484f17f
Merge branch 'release/6.1.0-alpha.1' into feat/trace-connected-metrics
Jan 11, 2026
e65db5f
rename APIs, validate parameters, fix attributes
Flash0ver Jan 15, 2026
de065c7
feat(metrics): Trace-connected Metrics (Analyzers)
Flash0ver Jan 15, 2026
5228fbe
feat(metrics): add SentryMetricUnits class for supported units
Flash0ver Jan 16, 2026
32caf06
Update SentryUnits.cs
Flash0ver Jan 16, 2026
98991a3
Merge branch 'feat/trace-connected-metrics' into feat/trace-connected…
Flash0ver Jan 16, 2026
d730432
update API Approval Tests
Flash0ver Jan 16, 2026
e7bb36f
Merge branch 'feat/trace-connected-metrics' into feat/trace-connected…
Flash0ver Jan 16, 2026
d2a02e1
ref: align public compiler extensions with internal analyzer project
Flash0ver Jan 19, 2026
b87bc4d
feat: also analyze SetBeforeSendMetric invocation
Flash0ver Jan 19, 2026
4b4bc03
Merge branch 'main' into feat/trace-connected-metrics-analyzers
Flash0ver Feb 13, 2026
68a2c7a
merge: fix
Flash0ver Feb 13, 2026
9855d05
update Analyzer to released API shape
Flash0ver Feb 16, 2026
70a58d0
update Polyfill
Flash0ver Feb 16, 2026
b90d71e
Merge branch 'main' into feat/trace-connected-metrics-analyzers
Flash0ver Feb 24, 2026
f168d5c
Merge branch 'main' into feat/trace-connected-metrics-analyzers
Flash0ver Feb 26, 2026
8af37bb
docs: Add CHANGELOG entry
Flash0ver Feb 26, 2026
5d4128d
feat: change Diagnostic Category to Sentry
Flash0ver Feb 26, 2026
eb69de6
fix: RCS1187: Use constant instead of field
Flash0ver Feb 26, 2026
e8b9376
style: use target-typed new expression
Flash0ver Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Report a new _Diagnostic_ (`SENTRY1001`) when an (experimental) Metrics-API is invoked with an unsupported numeric type ([#4840](https://github.com/getsentry/sentry-dotnet/pull/4840))

### Fixes

- The SDK now logs a `Warning` instead of an `Error` when being ratelimited ([#4927](https://github.com/getsentry/sentry-dotnet/pull/4927))
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

Original file line number Diff line number Diff line change
@@ -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 | Sentry | Warning | TraceConnectedMetricsAnalyzer
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Sentry.Compiler.Extensions.Analyzers;

/// <summary>
/// Guide consumers to use the public API of <see href="https://develop.sentry.dev/sdk/telemetry/metrics/">Sentry Trace-connected Metrics</see> correctly.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TraceConnectedMetricsAnalyzer : DiagnosticAnalyzer
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do we want to do this in general?

This started off as a weekend-project for me.
Do we want to provide this Analyzer in the first place?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid this explosion of overloads per method group,
and be similar to the implementation of System.Diagnostics.Metrics,
we are not compile-time constraining unsupported types,
but are instead run-time constraining unsupported types (no-op and Debug-Diagnostic-Logging).

Maybe not... we're trying to create a compile time constraint here (using analysers) because we don't want to create a compile time constraint using the typing system???

It might be a better experience for SDK users and less effort for us to maintain, simply to add some overloads that convert the other numeric types automatically to supported types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We drafted an alternative API shape, with explicit overloads.

But the devil is in the "API explosion":

-public void EmitCounter<T>(string name, T value) where T : struct
+public void EmitCounter(string name, int value)
+public void EmitCounter(string name, long value)
+// for all EmitCounter overloads
+// for EmitGauge and EmitDistribution method groups

And a follow-up uncertainty is about the Before-Send-Callback:

options.Experimental.SetBeforeSendMetric(static metric =>
{
    if (metric.TryGetValue(out int integer)) // if not emitted as `Int32`, do we want to cast to `Int32`?
    if (metric.TryGetInt32(out int integer)) // similar "explosion" of methods

    return metric;
});

So we decided to keep the API shape as is and actually do go with the Analyzer.
Should we have made a mistake, we can change both the API and the Analyzer for the next major version.

{
private const string Title = "Unsupported numeric type of Metric";
private const string MessageFormat = "{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.";
private const 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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: CHANGELOG

Should we mention this Analyzer, and Analyzer in general, in the CHANGELOG?
If so, in the Features category, or a separate category?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the features section... they're basically there to improve the developer experience for SDK users right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added CHANGELOG entry via 6c5ca23

title: Title,
messageFormat: MessageFormat,
category: DiagnosticCategories.Sentry,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description,
helpLinkUri: null
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: docs

Should we document the Analyzer?
If so, we could provide a link to the documentation page here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do after the merge / after releasing it via #4962.

);

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

/// <inheritdoc />
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.IsStatic || 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.SentryMetricEmitter";
}
else if (method.Name is "TryGetValue")
{
fullyQualifiedMetadataName = "Sentry.SentryMetric";
}
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);
}
}
6 changes: 6 additions & 0 deletions src/Sentry.Compiler.Extensions/DiagnosticCategories.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sentry.Compiler.Extensions;

internal static class DiagnosticCategories
{
internal const string Sentry = nameof(Sentry);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Category

I'm a bit uncertain about the Category.

The Category can be used to e.g. configure all Diagnostic of a category, rather than or in addition to configuring specific diagnostics per ID,
e.g. via an .globalconfig file

is_global = true

# Configure this new rule only
dotnet_diagnostic.SENTRY1001.severity = none

# Configure all rules within the "Sentry" Category
dotnet_analyzer_diagnostic.category-Sentry.severity = none

The ideas / variants I am having are

}
6 changes: 6 additions & 0 deletions src/Sentry.Compiler.Extensions/DiagnosticIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sentry.Compiler.Extensions;

internal static class DiagnosticIds
{
internal const string Sentry1001 = "SENTRY1001";
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,36 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
</ItemGroup>

<!--
We use Simon Cropp's Polyfill source-only package to access APIs in lower targets.
https://github.com/SimonCropp/Polyfill
-->
<ItemGroup>
<PackageReference Include="Polyfill" Version="9.8.1" PrivateAssets="all" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: see also #4879

</ItemGroup>
<!-- We currently don't require Polyfills for System.Memory. Ensure the feature is disabled and suppress the MSBuild Warning from Polyfill. -->
<Target Name="BeforePreparePolyfill" BeforeTargets="PreparePolyfill">
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PolyfillNoWarnIncorrectVersion>true</PolyfillNoWarnIncorrectVersion>
</PropertyGroup>
</Target>
<Target Name="AfterPreparePolyfill" AfterTargets="PreparePolyfill" DependsOnTargets="PreparePolyfill">
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<DefineConstants>$([System.String]::Copy('$(DefineConstants)').Replace('FeatureMemory','').Replace(';;',';'))</DefineConstants>
</PropertyGroup>
</Target>

<ItemGroup>
<Using Remove="System.Text.Json" />
<Using Remove="System.Text.Json.Serialization" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;

namespace Sentry.Compiler.Extensions;

internal static class SymbolDisplayFormats
{
internal static SymbolDisplayFormat FullNameFormat { get; } = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters
);
}
Loading
Loading