Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ gh pr view --json number -q '.number'
- Maintain **backwards compatibility** — avoid breaking public API without strong justification
- Platform-specific code lives in `src/Sentry/Platforms/` and is conditionally compiled

## Adding New Options (AOT Compatibility)

`SentryOptions` is **not** bound directly from configuration. Instead, a parallel `BindableSentryOptions` class (`src/Sentry/BindableSentryOptions.cs`) exists for AOT-safe configuration binding.

When adding a configurable property to any of the classes descending from `SentryOptions`:

1. Add the property to `SentryOptions` as normal.
2. Add a matching **nullable** property to `BindableSentryOptions`. Use only simple/primitive types the source generator can handle. For complex types (e.g., `IReadOnlyList<StringOrRegex>`), use a simpler surrogate (e.g., `List<string>?`) and convert in `ApplyTo`.
3. Add a line in `BindableSentryOptions.ApplyTo`: `options.MyProp = MyProp ?? options.MyProp;`
4. Run the relevant bindable options test (e.g., `BindableSentryOptionsTests`) — the `BindableProperties_MatchOptionsProperties` test will fail if any bindable property is missing from the bindable class.

The same pattern applies to `BindableSentryAspNetCoreOptions`, `BindableSentryMauiOptions`, `BindableSentryLoggingOptions`, and the platform-specific partial classes under `src/Sentry/Platforms/`.

## Commit Attribution

AI commits MUST include:
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry/BindableSentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal partial class BindableSentryOptions
public string? CacheDirectoryPath { get; set; }
public bool? CaptureFailedRequests { get; set; }
public List<string>? FailedRequestTargets { get; set; }
public List<int>? TraceIgnoreStatusCodes { get; set; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

issue: potential inconsistency

I wanted to check whether we are consistent with the other IList<HttpStatusCodeRange>-based Option, which is FailedRequestStatusCodes.

But FailedRequestStatusCodes does not exist on the BindableSentryOptions.

Now I'm wondering, whether BindableSentryOptions.FailedRequestStatusCodes does not exist because

  • we forgot it
  • we didn't add it because of scalar status codes vs Status-Code-Ranges

If the reasoning is the latter, then I guess we should not add this BindableSentryOptions as well, for consistency.

If, instead of only allowing scalar bindable options,
we also want to be able to depict Ranges via BindableSentryOptions, perhaps we could enable parsing from

{
  "Sentry": {
    "TraceIgnoreStatusCodes": [[301, 303], [305, 399], [401, 404]],
    "TraceIgnoreStatusCodes": [ {"start": 301, "end": 303}, {"start": 305, "end": 399}, {"start": 401, "end":404}]
  }
}

or something like that.

If we'd like to do that, for both this TraceIgnoreStatusCodes, and the already existing SentryOptions.FailedRequestStatusCodes, perhaps we want to hold off on this Bindable-Option for now, and introduce Bindable-HttpStatusCodeRange-Collections in a separate issue/PR.

What do you think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

But FailedRequestStatusCodes does not exist on the BindableSentryOptions.

Ideally as many of the options as possible would also be configurable via bindings (e.g. apssettings.json).

I think I didn't solve for FailedRequestStatusCodes specifically because the broader AOT problem was huge in scope. The shortest path to completion was simply to omit any non trivial properties (primitives) from the bindable options and require SDK users configure these instead via code.

I'm not really sure how many people will use the configuration bindings for these two properties, so a relatively simple solution to start with is probably not a bad thing. This PR adds the ability to configure lists of explicit codes to ignore via bindings... if people want to do more complex things like ranges, that would have to be done in code.

We could create a custom binding that was more sophisticated... it's certainly technically possible but would date longer to build and test. We have a backlog of over 300 issues so I'd recommend we just go with this simpler solution for the time being (or omit the ability to configure trace ignore status codes via bindings entirely).

We could also, for consistency, add an issue to the backlog to enable some similar bindable options for FailedRequestStatusCodes... with a medium priority initially (unless we get feedback from the community that this would be useful).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added #5061 as a follow up task to address the inconsistency:

public bool? DisableFileWrite { get; set; }
public TimeSpan? InitCacheFlushTimeout { get; set; }
public Dictionary<string, string>? DefaultTags { get; set; }
Expand Down Expand Up @@ -90,6 +91,7 @@ public void ApplyTo(SentryOptions options)
options.CacheDirectoryPath = CacheDirectoryPath ?? options.CacheDirectoryPath;
options.CaptureFailedRequests = CaptureFailedRequests ?? options.CaptureFailedRequests;
options.FailedRequestTargets = FailedRequestTargets?.Select(s => new StringOrRegex(s)).ToList() ?? options.FailedRequestTargets;
options.TraceIgnoreStatusCodes = TraceIgnoreStatusCodes?.Select(code => new HttpStatusCodeRange(code)).ToList<HttpStatusCodeRange>() ?? options.TraceIgnoreStatusCodes;
options.DisableFileWrite = DisableFileWrite ?? options.DisableFileWrite;
options.InitCacheFlushTimeout = InitCacheFlushTimeout ?? options.InitCacheFlushTimeout;
options.DefaultTags = DefaultTags ?? options.DefaultTags;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Sentry.Extensibility;
using Sentry.Internal.OpenTelemetry;

namespace Sentry.Internal;

internal class TraceIgnoreStatusCodeTransactionProcessor : ISentryTransactionProcessor
{
private readonly SentryOptions _options;

public TraceIgnoreStatusCodeTransactionProcessor(SentryOptions options)
{
_options = options;
}

public SentryTransaction? Process(SentryTransaction transaction)
{
if (_options.TraceIgnoreStatusCodes.Count == 0)
{
return transaction;
}

if (transaction.Data.TryGetValue(OtelSemanticConventions.AttributeHttpResponseStatusCode, out var statusCodeObj)
&& statusCodeObj is int statusCode
&& _options.TraceIgnoreStatusCodes.ContainsStatusCode(statusCode))

Check failure on line 24 in src/Sentry/Internal/TraceIgnoreStatusCodeTransactionProcessor.cs

View check run for this annotation

@sentry/warden / warden: find-bugs

TraceIgnoreStatusCodeTransactionProcessor reads from unpopulated Data dictionary

The processor reads `transaction.Data.TryGetValue(OtelSemanticConventions.AttributeHttpResponseStatusCode, ...)` which returns `Contexts.Trace.Data`. However, when transactions are created from `TransactionTracer` (the normal flow in ASP.NET Core), the HTTP status code set via `SetExtra()` is stored in `TransactionTracer._data`, not in `TransactionTracer.Contexts.Trace.Data`. The `SentryTransaction(ITransactionTracer)` constructor copies `Contexts` but never transfers `tracer.Data` to `Contexts.Trace.Data`. The feature will silently fail to filter transactions by status code.
{
return null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: Sampling Decision

From the dev docs:

If the value of this attribute matches one of the status codes in traceIgnoreStatusCodes, the SDK MUST set the transaction's sampling decision to not sampled.

Should this have other effects as well,
like setting the "sample_rate" on the DynamicSamplingContext as well,
or am I misunderstanding something there?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

At this stage, the Dynamic Sampling Context is no longer relevant... the transaction tracer has already been finished and a snapshot of the transaction has been created in the form of a SentryTransaction (a more or less immutable DTO - certainly it's not used for tracing any longer). Any outbound requests that were made using the DSC have already been completed so there's no way to retrospectively change the sample rate that was communicated for those.

}

return transaction;
}
}
12 changes: 9 additions & 3 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,12 @@ public IDiagnosticLogger? DiagnosticLogger
(500, 599)
};

/// <summary>
/// <para>Transactions will be dropped if the HTTP Response status code matches any of the configured ranges.</para>
/// <para>Defaults to an empty collection (all transactions are captured regardless of status code).</para>
/// </summary>
public IList<HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; } = [];

// The default failed request target list will match anything, but adding to the list should clear that.
private Lazy<IList<StringOrRegex>> _failedRequestTargets = new(() =>
new AutoClearingList<StringOrRegex>(
Expand Down Expand Up @@ -1317,9 +1323,9 @@ public SentryOptions()
SettingLocator = new SettingLocator(this);
_lazyInstallationId = new(() => new InstallationIdHelper(this).TryGetInstallationId());

TransactionProcessorsProviders = new() {
() => TransactionProcessors ?? Enumerable.Empty<ISentryTransactionProcessor>()
};
TransactionProcessorsProviders = [
() => [.. TransactionProcessors ?? [], new TraceIgnoreStatusCodeTransactionProcessor(this)]
];

_clientReportRecorder = new Lazy<IClientReportRecorder>(() => new ClientReportRecorder(this));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@ namespace Sentry
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
public System.Func<Sentry.TransactionSamplingContext, double?>? TracesSampler { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@ namespace Sentry
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
public System.Func<Sentry.TransactionSamplingContext, double?>? TracesSampler { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@ namespace Sentry
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
public System.Func<Sentry.TransactionSamplingContext, double?>? TracesSampler { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ namespace Sentry
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
public System.Func<Sentry.TransactionSamplingContext, double?>? TracesSampler { get; set; }
Expand Down
126 changes: 126 additions & 0 deletions test/Sentry.Tests/TraceIgnoreStatusCodeTransactionProcessorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Sentry.Internal.OpenTelemetry;

namespace Sentry.Tests;

public class TraceIgnoreStatusCodeTransactionProcessorTests
{
private static SentryOptions OptionsWithIgnoredCodes(params HttpStatusCodeRange[] ranges)
{
var options = new SentryOptions();
foreach (var range in ranges)
{
options.TraceIgnoreStatusCodes.Add(range);
}
return options;
}

private static SentryTransaction TransactionWithStatusCode(int statusCode)
{
var transaction = new SentryTransaction("name", "operation");
transaction.SetData(OtelSemanticConventions.AttributeHttpResponseStatusCode, statusCode);
return transaction;
}

[Fact]
public void Process_EmptyIgnoreList_ReturnsTransaction()
{
// Arrange
var options = new SentryOptions();
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);
var transaction = TransactionWithStatusCode(404);

// Act
var result = processor.Process(transaction);

// Assert
result.Should().BeSameAs(transaction);
}

[Fact]
public void Process_StatusCodeNotInIgnoreList_ReturnsTransaction()
{
// Arrange
var options = OptionsWithIgnoredCodes(404);
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);
var transaction = TransactionWithStatusCode(200);

// Act
var result = processor.Process(transaction);

// Assert
result.Should().BeSameAs(transaction);
}

[Fact]
public void Process_StatusCodeInIgnoreList_ReturnsNull()
{
// Arrange
var options = OptionsWithIgnoredCodes(404);
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);
var transaction = TransactionWithStatusCode(404);

// Act
var result = processor.Process(transaction);

// Assert
result.Should().BeNull();
}

[Fact]
public void Process_StatusCodeInIgnoredRange_ReturnsNull()
{
// Arrange
var options = OptionsWithIgnoredCodes((400, 499));
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);
var transaction = TransactionWithStatusCode(404);

// Act
var result = processor.Process(transaction);

// Assert
result.Should().BeNull();
}

[Fact]
public void Process_StatusCodeOutsideIgnoredRange_ReturnsTransaction()
{
// Arrange
var options = OptionsWithIgnoredCodes((400, 499));
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);
var transaction = TransactionWithStatusCode(500);

// Act
var result = processor.Process(transaction);

// Assert
result.Should().BeSameAs(transaction);
}

[Fact]
public void Process_NoStatusCodeExtra_ReturnsTransaction()
{
// Arrange
var options = OptionsWithIgnoredCodes(404);
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);
var transaction = new SentryTransaction("name", "operation");

// Act
var result = processor.Process(transaction);

// Assert
result.Should().BeSameAs(transaction);
}

[Fact]
public void Process_MultipleIgnoredCodes_MatchesAny()
{
// Arrange
var options = OptionsWithIgnoredCodes(404, 429);
var processor = new TraceIgnoreStatusCodeTransactionProcessor(options);

// Act & Assert
processor.Process(TransactionWithStatusCode(404)).Should().BeNull();
processor.Process(TransactionWithStatusCode(429)).Should().BeNull();
processor.Process(TransactionWithStatusCode(200)).Should().NotBeNull();
}
}
Loading