diff --git a/CHANGELOG.md b/CHANGELOG.md index e79502e30d..cfb6f7c984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Remove authority from URLs sent to Sentry ([#2365](https://github.com/getsentry/sentry-dotnet/pull/2365)) - Add `Hint` support ([#2351](https://github.com/getsentry/sentry-dotnet/pull/2351)) - Currently, this allows you to manipulate attachments in the various "before" event delegates. - Hints can also be used in event and transaction processors by implementing `ISentryEventProcessorWithHint` or `ISentryTransactionProcessorWithHint`, instead of `ISentryEventProcessor` or `ISentryTransactionProcessor`. diff --git a/src/Sentry/Breadcrumb.cs b/src/Sentry/Breadcrumb.cs index 4e3794bc98..4985c51160 100644 --- a/src/Sentry/Breadcrumb.cs +++ b/src/Sentry/Breadcrumb.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Internal.Extensions; namespace Sentry; @@ -9,6 +10,12 @@ namespace Sentry; [DebuggerDisplay("Message: {" + nameof(Message) + "}, Type: {" + nameof(Type) + "}")] public sealed class Breadcrumb : IJsonSerializable { + private readonly IReadOnlyDictionary? _data; + private readonly string? _message; + + private bool _sendDefaultPii = true; + internal void Redact() => _sendDefaultPii = false; + /// /// A timestamp representing when the breadcrumb occurred. /// @@ -21,7 +28,11 @@ public sealed class Breadcrumb : IJsonSerializable /// If a message is provided, it’s rendered as text and the whitespace is preserved. /// Very long text might be abbreviated in the UI. /// - public string? Message { get; } + public string? Message + { + get => _sendDefaultPii ? _message : _message?.RedactUrl(); + private init => _message = value; + } /// /// The type of breadcrumb. @@ -39,7 +50,17 @@ public sealed class Breadcrumb : IJsonSerializable /// Contains a sub-object whose contents depend on the breadcrumb type. /// Additional parameters that are unsupported by the type are rendered as a key/value table. /// - public IReadOnlyDictionary? Data { get; } + public IReadOnlyDictionary? Data + { + get => _sendDefaultPii + ? _data + : _data?.ToDictionary( + x => x.Key, + x => x.Value.RedactUrl() + ) + ; + private init => _data = value; + } /// /// Dotted strings that indicate what the crumb is or where it comes from. diff --git a/src/Sentry/Internal/PiiExtensions.cs b/src/Sentry/Internal/PiiExtensions.cs new file mode 100644 index 0000000000..7fece7e4e0 --- /dev/null +++ b/src/Sentry/Internal/PiiExtensions.cs @@ -0,0 +1,53 @@ +namespace Sentry.Internal; + +/// +/// Extensions to help redact data that might contain Personally Identifiable Information (PII) before sending it to +/// Sentry. +/// +internal static class PiiExtensions +{ + internal const string RedactedText = "[Filtered]"; + private static readonly Regex AuthRegex = new (@"(?i)\b(https?://.*@.*)\b", RegexOptions.Compiled); + private static readonly Regex UserInfoMatcher = new (@"^(?i)(https?://)(.*@)(.*)$", RegexOptions.Compiled); + + /// + /// Searches for URLs in text data and redacts any PII data from these, as required. + /// + /// The data to be searched + /// + /// The data, if no PII data is present or a copy of the data with PII data redacted otherwise + /// + public static string RedactUrl(this string data) + { + // If the data is empty then we don't need to redact anything + if (string.IsNullOrWhiteSpace(data)) + { + return data; + } + + // The pattern @"(?i)\b(https?://.*@.*)\b" uses the \b word boundary anchors to ensure that the match occurs at + // a word boundary. This allows the URL to be matched even if it is part of a larger text. The (?i) flag ensures + // case-insensitive matching for "https" or "http". + var result = AuthRegex.Replace(data, match => + { + var matchedUrl = match.Groups[1].Value; + return RedactAuth(matchedUrl); + }); + + return result; + } + + private static string RedactAuth(string data) + { + // ^ matches the start of the string. (?i)(https?://) gives a case-insensitive matching of the protocol. + // (.*@) matches the username and password (authentication information). (.*)$ matches the rest of the URL. + var match = UserInfoMatcher.Match(data); + if (match is not { Success: true, Groups.Count: 4 }) + { + return data; + } + var userInfoString = match.Groups[2].Value; + var replacementString = userInfoString.Contains(":") ? "[Filtered]:[Filtered]@" : "[Filtered]@"; + return match.Groups[1].Value + replacementString + match.Groups[3].Value; + } +} diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index c31e7dfabf..25f0648b34 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -147,6 +147,11 @@ public void CaptureTransaction(Transaction transaction, Hint? hint) return; } + if (!_options.SendDefaultPii) + { + processedTransaction.Redact(); + } + CaptureEnvelope(Envelope.FromTransaction(processedTransaction)); } @@ -276,6 +281,11 @@ private SentryId DoSendEvent(SentryEvent @event, Hint? hint, Scope? scope) return SentryId.Empty; } + if (!_options.SendDefaultPii) + { + processedEvent.Redact(); + } + var attachments = hint.Attachments.ToList(); var envelope = Envelope.FromEvent(processedEvent, _options.DiagnosticLogger, attachments, scope.SessionUpdate); return CaptureEnvelope(envelope) ? processedEvent.EventId : SentryId.Empty; diff --git a/src/Sentry/SentryEvent.cs b/src/Sentry/SentryEvent.cs index 423a45ce60..31c8de2ea1 100644 --- a/src/Sentry/SentryEvent.cs +++ b/src/Sentry/SentryEvent.cs @@ -242,6 +242,14 @@ public void SetTag(string key, string value) => public void UnsetTag(string key) => (_tags ??= new Dictionary()).Remove(key); + internal void Redact() + { + foreach (var breadcrumb in Breadcrumbs) + { + breadcrumb.Redact(); + } + } + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { diff --git a/src/Sentry/SentryFailedRequestHandler.cs b/src/Sentry/SentryFailedRequestHandler.cs index 10ec349693..3b9c2c664c 100644 --- a/src/Sentry/SentryFailedRequestHandler.cs +++ b/src/Sentry/SentryFailedRequestHandler.cs @@ -79,24 +79,24 @@ public void HandleResponse(HttpResponseMessage response) var sentryRequest = new Request { - Url = uri?.AbsoluteUri, QueryString = uri?.Query, Method = response.RequestMessage.Method.Method, }; - if (_options.SendDefaultPii) - { - sentryRequest.Cookies = response.RequestMessage.Headers.GetCookies(); - sentryRequest.AddHeaders(response.RequestMessage.Headers); - } - var responseContext = new Response { StatusCode = (short)response.StatusCode, BodySize = bodySize }; - if (_options.SendDefaultPii) + if (!_options.SendDefaultPii) { + sentryRequest.Url = uri?.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped); + } + else + { + sentryRequest.Url = uri?.AbsoluteUri; + sentryRequest.Cookies = response.RequestMessage.Headers.GetCookies(); + sentryRequest.AddHeaders(response.RequestMessage.Headers); responseContext.Cookies = response.Headers.GetCookies(); responseContext.AddHeaders(response.Headers); } diff --git a/src/Sentry/Span.cs b/src/Sentry/Span.cs index 3a76cbd0ad..9673bde909 100644 --- a/src/Sentry/Span.cs +++ b/src/Sentry/Span.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Internal.Extensions; namespace Sentry; @@ -145,4 +146,9 @@ public static Span FromJson(JsonElement json) _extra = data! }; } + + internal void Redact() + { + Description = Description?.RedactUrl(); + } } diff --git a/src/Sentry/Transaction.cs b/src/Sentry/Transaction.cs index ba755ef58a..e4e823fe1c 100644 --- a/src/Sentry/Transaction.cs +++ b/src/Sentry/Transaction.cs @@ -298,6 +298,23 @@ public void SetMeasurement(string name, Measurement measurement) => SpanId, IsSampled); + /// + /// Redacts PII from the transaction + /// + internal void Redact() + { + Description = Description?.RedactUrl(); + foreach (var breadcrumb in Breadcrumbs) + { + breadcrumb.Redact(); + } + + foreach (var span in Spans) + { + span.Redact(); + } + } + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 12a704d825..83ea9c567f 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -39,6 +39,7 @@ + diff --git a/test/Sentry.Tests/Internals/PiiExtensionsTests.cs b/test/Sentry.Tests/Internals/PiiExtensionsTests.cs new file mode 100644 index 0000000000..cf1a1ac414 --- /dev/null +++ b/test/Sentry.Tests/Internals/PiiExtensionsTests.cs @@ -0,0 +1,38 @@ +namespace Sentry.Tests.Internals; + +public class PiiExtensionsTests +{ + [Fact] + public void RedactUrl_Null() + { + var actual = PiiExtensions.RedactUrl(null); + + Assert.Null(actual); + } + + [Theory] + [InlineData("I'm a harmless string.", "doesn't affect ordinary strings")] + [InlineData("htps://user:password@sentry.io?q=1&s=2&token=secret#top", "doesn't affect malformed https urls")] + [InlineData("htp://user:password@sentry.io?q=1&s=2&token=secret#top", "doesn't affect malformed http urls")] + public void RedactUrl_NotNull_WithoutPii(string original, string reason) + { + var actual = original.RedactUrl(); + + actual.Should().Be(original, reason); + } + + [Theory] + [InlineData("https://user:password@sentry.io?q=1&s=2&token=secret#top", "https://[Filtered]:[Filtered]@sentry.io?q=1&s=2&token=secret#top", "strips user info with user and password from https")] + [InlineData("https://user:password@sentry.io", "https://[Filtered]:[Filtered]@sentry.io", "strips user info with user and password from https without query")] + [InlineData("https://user@sentry.io", "https://[Filtered]@sentry.io", "strips user info with user only from https without query")] + [InlineData("http://user:password@sentry.io?q=1&s=2&token=secret#top", "http://[Filtered]:[Filtered]@sentry.io?q=1&s=2&token=secret#top", "strips user info with user and password from http")] + [InlineData("http://user:password@sentry.io", "http://[Filtered]:[Filtered]@sentry.io", "strips user info with user and password from http without query")] + [InlineData("http://user@sentry.io", "http://[Filtered]@sentry.io", "strips user info with user only from http without query")] + [InlineData("GET https://user@sentry.io for goodness", "GET https://[Filtered]@sentry.io for goodness", "strips user info from URL embedded in text")] + public void RedactUrl_NotNull_WithPii(string original, string expected, string reason) + { + var actual = original.RedactUrl(); + + actual.Should().Be(expected, reason); + } +} diff --git a/test/Sentry.Tests/Internals/SentryScopeManagerTests.cs b/test/Sentry.Tests/Internals/SentryScopeManagerTests.cs index e25da4f30e..2c00dae4ef 100644 --- a/test/Sentry.Tests/Internals/SentryScopeManagerTests.cs +++ b/test/Sentry.Tests/Internals/SentryScopeManagerTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions.Execution; using Sentry.Internal.ScopeStack; namespace Sentry.Tests.Internals; diff --git a/test/Sentry.Tests/Protocol/BreadcrumbTests.cs b/test/Sentry.Tests/Protocol/BreadcrumbTests.cs index d9f154b8c0..66d824222e 100644 --- a/test/Sentry.Tests/Protocol/BreadcrumbTests.cs +++ b/test/Sentry.Tests/Protocol/BreadcrumbTests.cs @@ -1,6 +1,6 @@ namespace Sentry.Tests.Protocol; -public class BreadcrumbTests : ImmutableTests +public class BreadcrumbTests { private readonly IDiagnosticLogger _testOutputLogger; @@ -9,6 +9,50 @@ public BreadcrumbTests(ITestOutputHelper output) _testOutputLogger = new TestOutputDiagnosticLogger(output); } + [Fact] + public void Redact_Redacts_Urls() + { + // Arrange + var breadcrumbData = new Dictionary + { + {"url", "https://user@sentry.io"}, + {"method", "GET"}, + {"status_code", "403"} + }; + var timestamp = DateTimeOffset.UtcNow; + var message = "message https://user@sentry.io"; + var type = "fake_type"; + var data = breadcrumbData; + var category = "fake_category"; + var level = BreadcrumbLevel.Error; + + var breadcrumb = new Breadcrumb( + timestamp : timestamp, + message : message, + type : type, + data : breadcrumbData, + category : category, + level : level + ); + + // Act + breadcrumb.Redact(); + + // Assert + using (new AssertionScope()) + { + breadcrumb.Should().NotBeNull(); + breadcrumb.Timestamp.Should().Be(timestamp); + breadcrumb.Message.Should().Be("message https://[Filtered]@sentry.io"); // should be redacted + breadcrumb.Type.Should().Be(type); + breadcrumb.Data?["url"].Should().Be("https://[Filtered]@sentry.io"); // should be redacted + breadcrumb.Data?["method"].Should().Be(breadcrumb.Data?["method"]); + breadcrumb.Data?["status_code"].Should().Be(breadcrumb.Data?["status_code"]); + breadcrumb.Category.Should().Be(category); + breadcrumb.Level.Should().Be(level); + } + } + [Fact] public void SerializeObject_ParameterlessConstructor_IncludesTimestamp() { diff --git a/test/Sentry.Tests/Protocol/SentryEventTests.cs b/test/Sentry.Tests/Protocol/SentryEventTests.cs index 81032eddd7..0beea4069c 100644 --- a/test/Sentry.Tests/Protocol/SentryEventTests.cs +++ b/test/Sentry.Tests/Protocol/SentryEventTests.cs @@ -80,4 +80,91 @@ public void Modules_Getter_NotNull() var evt = new SentryEvent(); Assert.NotNull(evt.Modules); } + + [Fact] + public void Redact_Redacts_Urls() + { + // Arrange + var message = "message123 https://user@not.redacted"; + var logger = "logger123 https://user@not.redacted"; + var platform = "platform123 https://user@not.redacted"; + var serverName = "serverName123 https://user@not.redacted"; + var release = "release123 https://user@not.redacted"; + var distribution = "distribution123 https://user@not.redacted"; + var moduleValue = "module123 https://user@not.redacted"; + var transactionName = "transactionName123 https://user@sentry.io"; + var requestUrl = "https://user@not.redacted"; + var username = "username"; + var email = "bob@foo.com"; + var ipAddress = "127.0.0.1"; + var environment = "environment123 https://user@not.redacted"; + + var breadcrumbMessage = "message https://user@sentry.io"; // should be redacted + var breadcrumbDataValue = "data-value https://user@sentry.io"; // should be redacted + var tagValue = "tag_value https://user@not.redacted"; + + var timestamp = DateTimeOffset.MaxValue; + + var evt = new SentryEvent() + { + Message = message, + Logger = logger, + Platform = platform, + ServerName = serverName, + Release = release, + Distribution = distribution, + TransactionName = transactionName, + Request = new Request + { + Method = "GET", + Url = requestUrl + }, + User = new User + { + Username = username, + Email = email, + IpAddress = ipAddress + }, + Environment = environment, + }; + evt.Modules.Add("module", moduleValue); + evt.AddBreadcrumb(new Breadcrumb(timestamp, breadcrumbMessage)); + evt.AddBreadcrumb(new Breadcrumb( + timestamp, + "message", + "type", + new Dictionary { { "data-key", breadcrumbDataValue } }, + "category", + BreadcrumbLevel.Warning)); + evt.SetTag("tag_key", tagValue); + + // Act + evt.Redact(); + + // Assert + using (new AssertionScope()) + { + evt.Message.Message.Should().Be(message); + evt.Logger.Should().Be(logger); + evt.Platform.Should().Be(platform); + evt.ServerName.Should().Be(serverName); + evt.Release.Should().Be(release); + evt.Distribution.Should().Be(distribution); + evt.Modules["module"].Should().Be(moduleValue); + evt.TransactionName.Should().Be(transactionName); + // We don't redact the User or the Request since, if SendDefaultPii is false, we don't add these to the + // transaction in the SDK anyway (by default they don't get sent... but the user can always override this + // behavior if they need) + evt.Request.Url.Should().Be(requestUrl); + evt.User.Username.Should().Be(username); + evt.User.Email.Should().Be(email); + evt.User.IpAddress.Should().Be(ipAddress); + evt.Environment.Should().Be(environment); + var breadcrumbs = evt.Breadcrumbs.ToArray(); + breadcrumbs.Length.Should().Be(2); + breadcrumbs[0].Message.Should().Be($"message https://{PiiExtensions.RedactedText}@sentry.io"); + breadcrumbs[1].Data?["data-key"].Should().Be($"data-value https://{PiiExtensions.RedactedText}@sentry.io"); + evt.Tags["tag_key"].Should().Be(tagValue); + } + } } diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs index 7d5979b738..55739dfd2f 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -9,6 +9,113 @@ public TransactionTests(ITestOutputHelper output) _testOutputLogger = new TestOutputDiagnosticLogger(output); } + [Fact] + public void Redact_Redacts_Urls() + { + // Arrange + var timestamp = DateTimeOffset.MaxValue; + var name = "name123 https://user@not.redacted"; + var operation = "op123 https://user@not.redacted"; + var description = "desc123 https://user@sentry.io"; // should be redacted + var platform = "platform123 https://user@not.redacted"; + var release = "release123 https://user@not.redacted"; + var distribution = "distribution123 https://user@not.redacted"; + var environment = "environment123 https://user@not.redacted"; + var breadcrumbMessage = "message https://user@sentry.io"; // should be redacted + var breadcrumbDataValue = "data-value https://user@sentry.io"; // should be redacted + var tagValue = "tag_value https://user@not.redacted"; + var context = new TransactionContext( + SpanId.Create(), + SpanId.Create(), + SentryId.Create(), + name, + operation, + description, + SpanStatus.AlreadyExists, + null, + true, + TransactionNameSource.Component + ); + + var txTracer = new TransactionTracer(DisabledHub.Instance, context) + { + Name = name, + Operation = operation, + Description = description, + Platform = platform, + Release = release, + Distribution = distribution, + Status = SpanStatus.Aborted, + // We don't redact the User or the Request since, if SendDefaultPii is false, we don't add these to the + // transaction in the SDK anyway (by default they don't get sent... but the user can always override this + // behavior if they need) + User = new User { Id = "user-id", Username = "username", Email = "bob@foo.com", IpAddress = "127.0.0.1" }, + Request = new Request { Method = "POST", Url = "https://user@not.redacted"}, + Sdk = new SdkVersion { Name = "SDK-test", Version = "1.1.1" }, + Environment = environment, + Level = SentryLevel.Fatal, + Contexts = + { + ["context_key"] = "context_value", + [".NET Framework"] = new Dictionary + { + [".NET Framework"] = "\"v2.0.50727\", \"v3.0\", \"v3.5\"", + [".NET Framework Client"] = "\"v4.8\", \"v4.0.0.0\"", + [".NET Framework Full"] = "\"v4.8\"" + } + } + }; + + txTracer.Sdk.AddPackage(new Package("name", "version")); + txTracer.AddBreadcrumb(new Breadcrumb(timestamp, breadcrumbMessage)); + txTracer.AddBreadcrumb(new Breadcrumb( + timestamp, + "message", + "type", + new Dictionary { { "data-key", breadcrumbDataValue } }, + "category", + BreadcrumbLevel.Warning)); + txTracer.SetTag("tag_key", tagValue); + + var child1 = txTracer.StartChild("child_op123", "child_desc123 https://user@sentry.io"); + child1.Status = SpanStatus.Unimplemented; + child1.SetTag("q", "v"); + child1.SetExtra("f", "p"); + child1.Finish(SpanStatus.Unimplemented); + + var child2 = txTracer.StartChild("child_op999", "child_desc999 https://user:password@sentry.io"); + child2.Status = SpanStatus.OutOfRange; + child2.SetTag("xxx", "zzz"); + child2.SetExtra("f222", "p111"); + child2.Finish(SpanStatus.OutOfRange); + + txTracer.Finish(SpanStatus.Aborted); + + // Act + var transaction = new Transaction(txTracer); + transaction.Redact(); + + // Assert + using (new AssertionScope()) + { + transaction.Name.Should().Be(name); + transaction.Operation.Should().Be(operation); + transaction.Description.Should().Be($"desc123 https://{PiiExtensions.RedactedText}@sentry.io"); + transaction.Platform.Should().Be(platform); + transaction.Release.Should().Be(release); + transaction.Distribution.Should().Be(distribution); + transaction.Environment.Should().Be(environment); + var breadcrumbs = transaction.Breadcrumbs.ToArray(); + breadcrumbs.Length.Should().Be(2); + breadcrumbs.Should().Contain(b => b.Message == $"message https://{PiiExtensions.RedactedText}@sentry.io"); + breadcrumbs.Should().Contain(b => b.Data != null && b.Data["data-key"] == $"data-value https://{PiiExtensions.RedactedText}@sentry.io"); + var spans = transaction.Spans.ToArray(); + spans.Should().Contain(s => s.Operation == "child_op123" && s.Description == $"child_desc123 https://{PiiExtensions.RedactedText}@sentry.io"); + spans.Should().Contain(s => s.Operation == "child_op999" && s.Description == $"child_desc999 https://{PiiExtensions.RedactedText}:{PiiExtensions.RedactedText}@sentry.io"); + transaction.Tags["tag_key"].Should().Be(tagValue); + } + } + [Fact] public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() { diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index 2944346f51..44052c4c92 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -1,4 +1,3 @@ -using FluentAssertions.Execution; namespace Sentry.Tests; public class ScopeTests diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 7268163a1d..fa9bd8aedc 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -246,6 +246,29 @@ public void CaptureEvent_EventAndScope_CopyScopeIntoEvent() Assert.Equal(scope.Breadcrumbs, @event.Breadcrumbs); } + [Fact] + public void CaptureEvent_Redact_Breadcrumbs() + { + // Act + var scope = new Scope(_fixture.SentryOptions); + scope.AddBreadcrumb("Visited https://user@sentry.io in session"); + var @event = new SentryEvent(); + + // Act + Envelope envelope = null; + var sut = _fixture.GetSut(); + sut.Worker.EnqueueEnvelope(Arg.Do(e => envelope = e)); + _ = sut.CaptureEvent(@event, scope); + + // Assert + envelope.Should().NotBeNull(); + envelope.Items.Count.Should().Be(1); + var actual = (SentryEvent)(envelope.Items[0].Payload as JsonSerializable)?.Source; + actual.Should().NotBeNull(); + actual?.Breadcrumbs.Count.Should().Be(1); + actual?.Breadcrumbs.ToArray()[0].Message.Should().Be($"Visited https://{PiiExtensions.RedactedText}@sentry.io in session"); + } + [Fact] public void CaptureEvent_BeforeEvent_RejectEvent() { @@ -830,6 +853,36 @@ public void CaptureTransaction_DisposedClient_DoesNotThrow() }); } + [Fact] + public void CaptureTransaction_Redact_Description() + { + // Arrange + _fixture.SentryOptions.SendDefaultPii = false; + var client = _fixture.GetSut(); + var original = new Transaction( + "test name", + "test operation" + ) + { + IsSampled = true, + Description = "The URL: https://user@sentry.io has PII data in it", + EndTimestamp = DateTimeOffset.Now // finished + }; + + // Act + Envelope envelope = null; + client.Worker.EnqueueEnvelope(Arg.Do(e => envelope = e)); + client.CaptureTransaction(original); + + // Assert + envelope.Should().NotBeNull(); + envelope.Items.Count.Should().Be(1); + var actual = (envelope.Items[0].Payload as JsonSerializable)?.Source as Transaction; + actual?.Name.Should().Be(original.Name); + actual?.Operation.Should().Be(original.Operation); + actual?.Description.Should().Be(original.Description.RedactUrl()); // Should be redacted + } + [Fact] public void CaptureTransaction_BeforeSendTransaction_RejectEvent() { diff --git a/test/Sentry.Tests/SentryFailedRequestHandlerTests.cs b/test/Sentry.Tests/SentryFailedRequestHandlerTests.cs index fd8515f248..9fa415dcd4 100644 --- a/test/Sentry.Tests/SentryFailedRequestHandlerTests.cs +++ b/test/Sentry.Tests/SentryFailedRequestHandlerTests.cs @@ -1,5 +1,3 @@ -using FluentAssertions.Execution; - namespace Sentry.Tests; public class SentryFailedRequestHandlerTests @@ -126,6 +124,29 @@ public void HandleResponse_Capture_FailedRequest() ); } + [Fact] + public void HandleResponse_Capture_FailedRequest_No_Pii() + { + // Arrange + var options = new SentryOptions + { + CaptureFailedRequests = true + }; + var sut = GetSut(options); + + var response = InternalServerErrorResponse(); + var requestUri = new Uri("http://admin:1234@localhost/test/path?query=string#fragment"); + response.RequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + SentryEvent @event = null; + _hub.CaptureEvent(Arg.Do(e => @event = e), Arg.Any()); + sut.HandleResponse(response); + + // Assert + @event.Request.Url.Should().Be("http://localhost/test/path?query=string"); // No admin:1234 + } + [Fact] public void HandleResponse_Capture_RequestAndResponse() {