Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/Sentry/BindableSentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal partial class BindableSentryOptions
public bool? EnableScopeSync { get; set; }
public bool? EnableBackpressureHandling { get; set; }
public List<string>? TagFilters { get; set; }
public List<string>? IgnoreTransactions { get; set; }
public bool? SendDefaultPii { get; set; }
public bool? IsEnvironmentUser { get; set; }
public string? ServerName { get; set; }
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/Sentry/SentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,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.");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Arguably this could be LogDebug but we can tweak that in a follow up if anyone is really unhappy with it.

return;
}

scope ??= new Scope(_options);
hint ??= new SentryHint();
hint.AddAttachmentsFromScope(scope);
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,18 @@ internal IEnumerable<ISdkIntegration> Integrations
/// </summary>
public IList<StringOrRegex> TagFilters { get; set; } = new List<StringOrRegex>();

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Matching transactions are dropped before the BeforeSendTransaction callback runs, so that
/// callback is not invoked for them. This mirrors the behavior of the <c>ignoreTransactions</c>
/// option in the Sentry JavaScript and Python SDKs, where the built-in filter runs ahead of the
/// user's <c>before_send_transaction</c> hook.
/// </remarks>
public IList<StringOrRegex> IgnoreTransactions { get; set; } = new List<StringOrRegex>();

/// <summary>
/// The worker used by the client to pass envelopes.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ namespace Sentry
public System.Collections.Generic.IList<Sentry.StringOrRegex> FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
public System.Net.IWebProxy? HttpProxy { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> IgnoreTransactions { get; set; }
public System.TimeSpan InitCacheFlushTimeout { get; set; }
public bool IsEnvironmentUser { get; set; }
public bool IsGlobalModeEnabled { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ namespace Sentry
public System.Collections.Generic.IList<Sentry.StringOrRegex> FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
public System.Net.IWebProxy? HttpProxy { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> IgnoreTransactions { get; set; }
public System.TimeSpan InitCacheFlushTimeout { get; set; }
public bool IsEnvironmentUser { get; set; }
public bool IsGlobalModeEnabled { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ namespace Sentry
public System.Collections.Generic.IList<Sentry.StringOrRegex> FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
public System.Net.IWebProxy? HttpProxy { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> IgnoreTransactions { get; set; }
public System.TimeSpan InitCacheFlushTimeout { get; set; }
public bool IsEnvironmentUser { get; set; }
public bool IsGlobalModeEnabled { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ namespace Sentry
public System.Collections.Generic.IList<Sentry.StringOrRegex> FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
public System.Net.IWebProxy? HttpProxy { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> IgnoreTransactions { get; set; }
public System.TimeSpan InitCacheFlushTimeout { get; set; }
public bool IsEnvironmentUser { get; set; }
public bool IsGlobalModeEnabled { get; set; }
Expand Down
113 changes: 113 additions & 0 deletions test/Sentry.Tests/SentryClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,119 @@ public void CaptureTransaction_NoOperation_Ignored()
_ = client.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any<Envelope>());
}

[Fact]
public void CaptureTransaction_MatchesIgnoreTransactions_Dropped()
{
// Arrange
_fixture.SentryOptions.IgnoreTransactions = new List<StringOrRegex> { "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<Envelope>());

var expectedSpanCount = sentryTransaction.Spans.Count + 1; // 1 for each span + one for the root transaction
_fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction);
_fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, expectedSpanCount);
}

[Fact]
public void CaptureTransaction_MatchesIgnoreTransactionsRegex_Dropped()
{
// Arrange
_fixture.SentryOptions.IgnoreTransactions = new List<StringOrRegex> { 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<Envelope>());
}

[Fact]
public void CaptureTransaction_DoesNotMatchIgnoreTransactions_Sent()
{
// Arrange
_fixture.SentryOptions.IgnoreTransactions = new List<StringOrRegex> { "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<Envelope>());
}

[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<StringOrRegex> { "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<Envelope>());
}

[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<StringOrRegex> { "GET /health" };
var client = _fixture.GetSut();

var hub = Substitute.For<IHub>();
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<Envelope>());

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()
{
Expand Down
Loading