From 7b3d008caadc589fc14819a475b749beb679ef4d Mon Sep 17 00:00:00 2001 From: Adham Kiwan Date: Sat, 11 Jul 2026 02:56:53 +0300 Subject: [PATCH 1/6] feat: Add IgnoreTransactions option to filter transactions by name Adds a first-class `SentryOptions.IgnoreTransactions` option, a list of substrings or regular expressions. A transaction whose name matches any entry is dropped in `SentryClient.CaptureTransaction` before the BeforeSendTransaction callback, mirroring the `ignoreTransactions` option in the JavaScript and Python SDKs. Previously this could only be achieved by writing a BeforeSendTransaction callback. The new option follows the same shape and matching semantics as the existing `TagFilters`/`TracePropagationTargets` options (StringOrRegex + MatchesSubstringOrRegex), and is wired into BindableSentryOptions for AOT-safe configuration binding. Fixes #2306 Co-Authored-By: Claude Opus 4.8 --- src/Sentry/BindableSentryOptions.cs | 2 + src/Sentry/SentryClient.cs | 9 +++ src/Sentry/SentryOptions.cs | 9 +++ ...iApprovalTests.Run.DotNet10_0.verified.txt | 1 + ...piApprovalTests.Run.DotNet8_0.verified.txt | 1 + ...piApprovalTests.Run.DotNet9_0.verified.txt | 1 + .../ApiApprovalTests.Run.Net4_8.verified.txt | 1 + test/Sentry.Tests/SentryClientTests.cs | 62 +++++++++++++++++++ 8 files changed, 86 insertions(+) diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs index 6815aa0d95..5f2ede7844 100644 --- a/src/Sentry/BindableSentryOptions.cs +++ b/src/Sentry/BindableSentryOptions.cs @@ -11,6 +11,7 @@ internal partial class BindableSentryOptions public bool? EnableScopeSync { get; set; } public bool? EnableBackpressureHandling { get; set; } public List? TagFilters { get; set; } + public List? IgnoreTransactions { get; set; } public bool? SendDefaultPii { get; set; } public bool? IsEnvironmentUser { get; set; } public string? ServerName { get; set; } @@ -66,6 +67,7 @@ public void ApplyTo(SentryOptions options) options.EnableScopeSync = EnableScopeSync ?? options.EnableScopeSync; options.EnableBackpressureHandling = EnableBackpressureHandling ?? options.EnableBackpressureHandling; options.TagFilters = TagFilters?.Select(s => new StringOrRegex(s)).ToList() ?? options.TagFilters; + options.IgnoreTransactions = IgnoreTransactions?.Select(s => new StringOrRegex(s)).ToList() ?? options.IgnoreTransactions; options.SendDefaultPii = SendDefaultPii ?? options.SendDefaultPii; options.IsEnvironmentUser = IsEnvironmentUser ?? options.IsEnvironmentUser; options.ServerName = ServerName ?? options.ServerName; diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 7bbd403aef..234affd8bd 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -163,6 +163,15 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent return; } + if (_options.IgnoreTransactions.MatchesSubstringOrRegex(transaction.Name)) + { + var ignoredSpanCount = transaction.Spans.Count + 1; // 1 for each span + 1 for the transaction itself + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, ignoredSpanCount); + _options.LogInfo("Transaction dropped by IgnoreTransactions option."); + return; + } + // Unfinished transaction can only happen if the user calls this method instead of // transaction.Finish(). // We still send these transactions over, but warn the user not to do it. diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 8000c6fa99..6c15ae41d7 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -243,6 +243,15 @@ internal IEnumerable Integrations /// public IList TagFilters { get; set; } = new List(); + /// + /// A list of transaction names to be ignored. A transaction whose name matches any of the + /// given substrings or regular expression patterns will not be sent to Sentry. + /// + /// + /// This is applied before the BeforeSendTransaction callback. + /// + public IList IgnoreTransactions { get; set; } = new List(); + /// /// The worker used by the client to pass envelopes. /// diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 5bc79de2f1..35d2d30f47 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -822,6 +822,7 @@ namespace Sentry public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } public System.Net.IWebProxy? HttpProxy { get; set; } + public System.Collections.Generic.IList IgnoreTransactions { get; set; } public System.TimeSpan InitCacheFlushTimeout { get; set; } public bool IsEnvironmentUser { get; set; } public bool IsGlobalModeEnabled { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 5bc79de2f1..35d2d30f47 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -822,6 +822,7 @@ namespace Sentry public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } public System.Net.IWebProxy? HttpProxy { get; set; } + public System.Collections.Generic.IList IgnoreTransactions { get; set; } public System.TimeSpan InitCacheFlushTimeout { get; set; } public bool IsEnvironmentUser { get; set; } public bool IsGlobalModeEnabled { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 5bc79de2f1..35d2d30f47 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -822,6 +822,7 @@ namespace Sentry public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } public System.Net.IWebProxy? HttpProxy { get; set; } + public System.Collections.Generic.IList IgnoreTransactions { get; set; } public System.TimeSpan InitCacheFlushTimeout { get; set; } public bool IsEnvironmentUser { get; set; } public bool IsGlobalModeEnabled { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 6c44d57159..fe7b7cab37 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -809,6 +809,7 @@ namespace Sentry public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } public System.Net.IWebProxy? HttpProxy { get; set; } + public System.Collections.Generic.IList IgnoreTransactions { get; set; } public System.TimeSpan InitCacheFlushTimeout { get; set; } public bool IsEnvironmentUser { get; set; } public bool IsGlobalModeEnabled { get; set; } diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 1accc76921..c0c9b7808d 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -1296,6 +1296,68 @@ public void CaptureTransaction_NoOperation_Ignored() _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); } + [Fact] + public void CaptureTransaction_MatchesIgnoreTransactions_Dropped() + { + // Arrange + _fixture.SentryOptions.IgnoreTransactions = new List { "GET /health" }; + var client = _fixture.GetSut(); + + var sentryTransaction = new SentryTransaction("GET /health", "http.server") + { + IsSampled = true, + EndTimestamp = DateTimeOffset.Now // finished + }; + + // Act + client.CaptureTransaction(sentryTransaction); + + // Assert + _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + + var expectedSpanCount = sentryTransaction.Spans.Count + 1; // 1 for each span + one for the root transaction + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, expectedSpanCount); + } + + [Fact] + public void CaptureTransaction_MatchesIgnoreTransactionsRegex_Dropped() + { + // Arrange + _fixture.SentryOptions.IgnoreTransactions = new List { new(new Regex(@"^GET /health/\d+$")) }; + var client = _fixture.GetSut(); + + // Act + client.CaptureTransaction( + new SentryTransaction("GET /health/123", "http.server") + { + IsSampled = true, + EndTimestamp = DateTimeOffset.Now // finished + }); + + // Assert + _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + } + + [Fact] + public void CaptureTransaction_DoesNotMatchIgnoreTransactions_Sent() + { + // Arrange + _fixture.SentryOptions.IgnoreTransactions = new List { "GET /health" }; + var client = _fixture.GetSut(); + + // Act + client.CaptureTransaction( + new SentryTransaction("GET /api/users", "http.server") + { + IsSampled = true, + EndTimestamp = DateTimeOffset.Now // finished + }); + + // Assert + _ = client.Worker.Received(1).EnqueueEnvelope(Arg.Any()); + } + [Fact] public void CaptureTransaction_NotFinished_Sent() { From 8a9cd6d3d6ea7c54c3a6c860d457804106db9242 Mon Sep 17 00:00:00 2001 From: Adham Kiwan Date: Sat, 11 Jul 2026 02:57:53 +0300 Subject: [PATCH 2/6] chore: Add changelog entry for IgnoreTransactions Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08dd24178d..52da6ecbf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features ✨ +- Add `IgnoreTransactions` option to filter out transactions by name, matching substrings or regular expressions against the transaction name ([#5377](https://github.com/getsentry/sentry-dotnet/pull/5377)) - feat: Add exponential backoff and log deduplication to Spotlight transport by @mattico in [#5025](https://github.com/getsentry/sentry-dotnet/pull/5025) ## 6.6.0 From c2d7ae220f03d24cfbf9a357fe28647bca673449 Mon Sep 17 00:00:00 2001 From: Adham Kiwan Date: Sun, 12 Jul 2026 13:52:52 +0300 Subject: [PATCH 3/6] Document IgnoreTransactions ordering and test the contract Expand the IgnoreTransactions doc comment to state that matching transactions are dropped before the BeforeSendTransaction callback runs (so that callback is not invoked for them), matching the JavaScript and Python SDKs. Add a test asserting BeforeSendTransaction is not invoked for an ignored transaction, to pin that ordering. Co-Authored-By: Claude Opus 4.8 --- src/Sentry/SentryOptions.cs | 5 ++++- test/Sentry.Tests/SentryClientTests.cs | 27 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 6c15ae41d7..0dbfd7c99b 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -248,7 +248,10 @@ internal IEnumerable Integrations /// given substrings or regular expression patterns will not be sent to Sentry. /// /// - /// This is applied before the BeforeSendTransaction callback. + /// Matching transactions are dropped before the BeforeSendTransaction callback runs, so that + /// callback is not invoked for them. This mirrors the behavior of the ignoreTransactions + /// option in the Sentry JavaScript and Python SDKs, where the built-in filter runs ahead of the + /// user's before_send_transaction hook. /// public IList IgnoreTransactions { get; set; } = new List(); diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index c0c9b7808d..fe3376ece5 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -1358,6 +1358,33 @@ public void CaptureTransaction_DoesNotMatchIgnoreTransactions_Sent() _ = client.Worker.Received(1).EnqueueEnvelope(Arg.Any()); } + [Fact] + public void CaptureTransaction_MatchesIgnoreTransactions_BeforeSendTransactionNotInvoked() + { + // Arrange: IgnoreTransactions is applied before the BeforeSendTransaction + // callback, so the callback must not observe an ignored transaction. + _fixture.SentryOptions.IgnoreTransactions = new List { "GET /health" }; + var beforeSendTransactionInvoked = false; + _fixture.SentryOptions.SetBeforeSendTransaction((tx, _) => + { + beforeSendTransactionInvoked = true; + return tx; + }); + var client = _fixture.GetSut(); + + // Act + client.CaptureTransaction( + new SentryTransaction("GET /health", "http.server") + { + IsSampled = true, + EndTimestamp = DateTimeOffset.Now // finished + }); + + // Assert + beforeSendTransactionInvoked.Should().BeFalse(); + _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + } + [Fact] public void CaptureTransaction_NotFinished_Sent() { From 1499ffcd4a7471b0f015d6190749d39eae65350f Mon Sep 17 00:00:00 2001 From: Adham Kiwan Date: Sun, 12 Jul 2026 14:04:38 +0300 Subject: [PATCH 4/6] Record ignored transactions under EventProcessor, not BeforeSend IgnoreTransactions is a built-in filter, so its client-report discards should be attributed to EventProcessor (as the exception filter path already does, and as the JS inbound filters do), rather than BeforeSend. BeforeSend is reserved for the user's BeforeSendTransaction callback, which does not run for an ignored transaction, so attributing the drop to it would mislead drop-rate debugging. Co-Authored-By: Claude Opus 4.8 --- src/Sentry/SentryClient.cs | 7 +++++-- test/Sentry.Tests/SentryClientTests.cs | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 234affd8bd..8bb9d5d891 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -165,9 +165,12 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent if (_options.IgnoreTransactions.MatchesSubstringOrRegex(transaction.Name)) { + // IgnoreTransactions is a built-in filter, so discards are recorded under + // EventProcessor (matching the exception filter path and the JS inbound filters), + // not BeforeSend, which is reserved for the user's BeforeSendTransaction callback. var ignoredSpanCount = transaction.Spans.Count + 1; // 1 for each span + 1 for the transaction itself - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction); - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, ignoredSpanCount); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, ignoredSpanCount); _options.LogInfo("Transaction dropped by IgnoreTransactions option."); return; } diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index fe3376ece5..52e6efee9e 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -1316,8 +1316,8 @@ public void CaptureTransaction_MatchesIgnoreTransactions_Dropped() _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); var expectedSpanCount = sentryTransaction.Spans.Count + 1; // 1 for each span + one for the root transaction - _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction); - _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, expectedSpanCount); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, expectedSpanCount); } [Fact] From 1a525c701dc0ca613f4512ed0e61360075b0078f Mon Sep 17 00:00:00 2001 From: Adham Kiwan Date: Sun, 12 Jul 2026 14:13:05 +0300 Subject: [PATCH 5/6] Apply IgnoreTransactions after the sampling check Move the IgnoreTransactions filter to run after the IsSampled check, so a transaction that is sampled out (e.g. via trace propagation) is still attributed to sampling (SampleRate) in client reports even if its name also matches an ignore pattern, rather than being misattributed to the filter (EventProcessor). Sampling is the earlier, primary drop reason; IgnoreTransactions only applies to transactions that would otherwise be sent. Reuses the already-computed span count. Adds a regression test asserting a sampled-out, ignore-matching transaction is recorded under SampleRate. Co-Authored-By: Claude Opus 4.8 --- src/Sentry/SentryClient.cs | 25 +++++++++++++------------ test/Sentry.Tests/SentryClientTests.cs | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 8bb9d5d891..e84ca1eb5f 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -163,18 +163,6 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent return; } - if (_options.IgnoreTransactions.MatchesSubstringOrRegex(transaction.Name)) - { - // IgnoreTransactions is a built-in filter, so discards are recorded under - // EventProcessor (matching the exception filter path and the JS inbound filters), - // not BeforeSend, which is reserved for the user's BeforeSendTransaction callback. - var ignoredSpanCount = transaction.Spans.Count + 1; // 1 for each span + 1 for the transaction itself - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); - _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, ignoredSpanCount); - _options.LogInfo("Transaction dropped by IgnoreTransactions option."); - return; - } - // Unfinished transaction can only happen if the user calls this method instead of // transaction.Finish(). // We still send these transactions over, but warn the user not to do it. @@ -197,6 +185,19 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent return; } + // Applied after the sampling check so that a transaction which is sampled out is + // still attributed to sampling, not to this filter. IgnoreTransactions is a built-in + // filter, so its discards are recorded under EventProcessor (matching the exception + // filter path and the JS inbound filters), not BeforeSend, which is reserved for the + // user's BeforeSendTransaction callback. + if (_options.IgnoreTransactions.MatchesSubstringOrRegex(transaction.Name)) + { + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); + _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, spanCount); + _options.LogInfo("Transaction dropped by IgnoreTransactions option."); + return; + } + scope ??= new Scope(_options); hint ??= new SentryHint(); hint.AddAttachmentsFromScope(scope); diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 52e6efee9e..ab57b04227 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -1385,6 +1385,30 @@ public void CaptureTransaction_MatchesIgnoreTransactions_BeforeSendTransactionNo _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); } + [Fact] + public void CaptureTransaction_SampledOutAndMatchesIgnoreTransactions_RecordedAsSampleRate() + { + // Arrange: a sampled-out transaction whose name also matches an ignore pattern must be + // attributed to sampling (the earlier, primary drop reason), not to IgnoreTransactions. + _fixture.SentryOptions.IgnoreTransactions = new List { "GET /health" }; + var client = _fixture.GetSut(); + + var hub = Substitute.For(); + var transaction = new UnsampledTransaction(hub, new TransactionContext("GET /health", "http.server")); + transaction.StartChild("span1"); + + // Act + client.CaptureTransaction(new SentryTransaction(transaction)); + + // Assert + _ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + + var expectedSpanCount = transaction.Spans.Count + 1; // 1 for each span + one for the root transaction + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Transaction); + _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Span, expectedSpanCount); + _fixture.ClientReportRecorder.DidNotReceive().RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction); + } + [Fact] public void CaptureTransaction_NotFinished_Sent() { From cc311df95dfb608518f10ddf51a90a90535a6be5 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 13 Jul 2026 10:44:48 +1200 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52da6ecbf0..08dd24178d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ ### Features ✨ -- Add `IgnoreTransactions` option to filter out transactions by name, matching substrings or regular expressions against the transaction name ([#5377](https://github.com/getsentry/sentry-dotnet/pull/5377)) - feat: Add exponential backoff and log deduplication to Spotlight transport by @mattico in [#5025](https://github.com/getsentry/sentry-dotnet/pull/5025) ## 6.6.0